Feature #22093
openIntroduce `Process::ID` for process IDs returned by `Process.spawn` and `fork`
Description
Currently, process IDs returned by Process.spawn and fork are plain Integer objects. This works, but it makes common process-handling code slightly verbose and loses the opportunity to attach process-specific behavior to the returned value.
For example:
pid = Process.spawn(...)
_, status = Process.wait2(pid)
if status.success?
...
end
I propose introducing Process::ID, an Integer-like object representing a system process ID. Process.spawn and parent-side Process.fork would return Process::ID. Process::ID would provide convenience methods such as #wait and #detach.
This allows process lifecycle code to be written more directly:
pid = Process.spawn(...)
status = pid.wait
if status.success?
...
end
and, when the parent does not intend to wait explicitly:
pid = Process.spawn(...)
pid.detach
Proposal¶
Add Process::ID.
Process::ID would:
- represent a process ID
- provide
#to_iand#to_int - provide
#to_sreturning the decimal PID string - provide
#inspect, like asProcess::Status - provide
#pidas a reader for the integer PID - provide
#wait, returningProcess::Status - provide
#detach, returningProcess::Waiter - support comparison with the underlying integer PID
Process.spawn would return a Process::ID instead of an Integer.
On platforms with fork, parent-side fork and Process.fork would return a Process::ID. The child side would keep the current behavior: nil for fork and 0 for Process._fork.
IO#pid for IO.popen should also return the associated Process::ID when applicable.
Rationale¶
A process ID is currently represented by an Integer, but conceptually it is not just a number. It is a handle-like value used with process APIs such as wait, waitpid, kill, and detach.
Providing a dedicated object makes common operations more discoverable and concise, while keeping compatibility through #to_i/#to_int.
This is similar in spirit to Process::Status: process-related values can still expose primitive information, but the object itself can provide process-specific operations.
#wait is useful when the parent wants to wait for the child and obtain its Process::Status:
pid = Process.spawn("ruby", "-e", "exit 0")
pid.wait.success? #=> true
#detach is useful when the parent intentionally does not want to wait for the child directly, but still wants to avoid leaving a zombie process:
pid = Process.spawn("ruby", "-e", "sleep 1")
waiter = pid.detach
waiter.value #=> #<Process::Status: pid ... exit 0>
This is preferable to tying process cleanup to object finalization. A dfree/GC-based wait would make process reaping depend on GC timing and could unexpectedly consume the child status before explicit Process.wait code gets to it. #detach keeps the ownership transfer explicit.
Compatibility¶
This is a visible compatibility change because Process.spawn(...).class would become Process::ID instead of Integer.
However, code that passes the PID to existing process APIs should continue to work because Process::ID provides #to_int.
Potentially affected code is code that checks the exact class of the returned value, such as pid.instance_of?(Integer), or uses type checks such as Integer === pid. Such code would need to treat the value as integer-like instead, for example by using pid.to_i when an actual Integer object is needed.
For Process._fork, if it is overridden, fork should preserve the object returned by the overridden _fork on the parent side, as long as it is Integer-like. This keeps custom hooks compatible with code that returns PID wrapper objects.
Examples¶
pid = Process.spawn("ruby", "-e", "exit 0")
pid.class #=> Process::ID
pid.to_i #=> 12345
pid.to_s #=> "12345"
pid.wait #=> #<Process::Status: pid 12345 exit 0>
pid = fork { exit! true }
pid.class #=> Process::ID
pid.wait.success? #=> true
pid = Process.spawn("ruby", "-e", "sleep 1")
thread = pid.detach #=> #<Process::Waiter:...>
thread.value #=> #<Process::Status: pid ... exit 0>
Open questions¶
-
Should
Process::IDincludeComparable, or only define<=>?
<=>is useful because PIDs have historically beenIntegerobjects and are sometimes sorted simply to get a deterministic order, for example inProcess.waitallresults. The numeric ordering itself has no process-specific meaning, but preserving sortability is convenient. -
Should
Process::ID#waitaccept the same integer flags as existing wait APIs, provide keyword arguments, or support both?
Integer flags such asProcess::WNOHANGare consistent withProcess.waitpid, while keywords such asnohang: true,untraced: true, andcontinued: truemay be more readable for a new convenience method. -
Should
Process::ID#detachsimply be equivalent toProcess.detach(self)? -
Are there other process-related APIs that should return or preserve
Process::ID?
Updated by byroot (Jean Boussier) 3 days ago
I'm very much in favor of such API as a much more OO and friendly API than passing numeric PIDs around.
Updated by alanwu (Alan Wu) 3 days ago
Nice, and theoretically this enables using pidfd_open(2) underneath the abstraction to deal with pid recycling race conditions. (Whether that's a good idea is off topic.)
Updated by kaiquekandykoga (Kaíque Kandy Koga) 3 days ago
I like that!
Just adding an idea:
process_id = Process.spawn("ruby", "-e", "exit 0")
pid = process_id.pid
process_id2 = Process::ID(pid)
process_id and process_id2 are both Process::ID instances pointing to the same process. This can be handy if I need to persist the PID temporarily and later re‑initialise a fresh Process::ID for it.