Feature #22093
closedIntroduce `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) 9 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) 9 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) 9 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.
Updated by matz (Yukihiro Matsumoto) 4 days ago
I see the appeal of pid.wait and pid.detach, but I'm not convinced.
A PID is just a number, not a handle in the OS sense, so Process::ID would look handle-like without the safety a real handle (e.g. pidfd) would give. Unlike Process::Status, it would carry only a single integer and exist just to attach methods.
PIDs are also returned by many other APIs (Process.pid, ppid, waitpid, PID files, etc.). Changing only spawn/fork/IO#pid is inconsistent, and changing all of them broadens the compatibility impact.
For the modest ergonomic gain, I'd rather keep PIDs as Integer for now.
Matz.
Updated by nobu (Nobuyoshi Nakada) 3 days ago
- Status changed from Open to Rejected