|
CX Framework
Cross-platform C utility framework
|
Two things live in this module: a handle to one process, and a snapshot of the processes running on the machine.
A Process is a reference-counted object. procOpen() attaches to a process by id and returns one. Release it with procRelease() when finished, or objAcquire() it to keep a second reference. Releasing is safe at any time and from any thread; it does not disturb the process itself, only cx's handle on it.
A process id and a process name can always be read. A parent process id usually can. A full executable path frequently cannot: on Unix it is readable only for processes belonging to the same user, and on Windows some protected processes refuse to be opened at all. None of this is treated as an error. A field the system will not report comes back as an empty string, or as PROCESS_InvalidID for a parent id, and the call still succeeds.
Names are shortened by the operating system too: Linux reports the first 15 characters and FreeBSD about 19. Where the executable path is readable, its filename is used for the name instead, so the name is the real one wherever permissions allow.
A process id names a process only while it is running. Once it exits, the operating system is free to give that number to something unrelated.
A handle from procLaunch() is unaffected: cx is the process's parent, so the id cannot be reused until cx collects it. A handle from procOpen() has no such protection, so cx notes when the process started and checks that again on each query. Once the original process is gone, procRunning() reports false and procTerminate() refuses, rather than acting on whatever inherited the number.
procEnum() returns a list of the processes running at the moment it was called, not a live view. Entries can be gone by the time the list is read, and processes started afterwards do not appear. Every platform produces the list this way, so nothing is lost by saying so.
procEnum() tells failure apart from emptiness: it returns false only when the process list could not be read at all. Individual processes that disappear while the list is being built are skipped, and the call still succeeds.
Resolving executable paths costs one extra system call per process, and on Windows a burst of them is exactly the pattern endpoint security products flag, so it is opt-in through PROC_EnumFullPath rather than always on.
procWait() and procRunning() work for any process on every platform. Reading an exit code with procExitCode() is the one place the platforms genuinely differ: on Unix only a process's own parent can collect its exit status, so a process reached through procOpen() rather than procLaunch() cannot report one. There procExitCode() returns false and sets cxerr to CX_NotSupported. Windows has no such restriction. A process cx launched itself reports its exit code everywhere.
Once a launched process has finished, its exit code is remembered on the handle, so procRunning(), procWait() and procExitCode() keep answering correctly afterwards and never have to ask the operating system again.
The same Unix rule applies to a launched child that something else in the program collected first, such as a SIGCHLD handler calling waitpid(-1): cx knows it finished, but not how.
procNotifyExit() registers a callback instead of blocking. It works for handles from both procLaunch() and procOpen(). It is delivered on a thread cx owns, one callback at a time, and never while any internal lock is held – so a callback is free to register further watches, cancel them, or release the handle it was told about.
Registering on a process that has already finished calls the closure immediately, on the calling thread, before returning. That is deliberate: it means there is no window between checking whether a process is still running and asking to be told when it stops, so the obvious-looking race cannot be written.
The callback receives the process id and exit code rather than the Process itself. A caller that wants the object captures it in the closure and so decides its lifetime explicitly, instead of being handed a borrowed pointer whose validity depends on another thread. Where the exit code cannot be known (see above), the callback receives PROC_ExitCodeUnknown.
A registered callback keeps the handle alive until the process exits, even after the caller releases it, so launching a process and forgetting it still delivers the callback. Call procNotifyCancel() to stop watching sooner.
Where no exit watcher is available – WebAssembly, or Linux kernels older than 5.3 – procNotifyExit() returns false for a process that is still running, and takes no action.
procLaunch() runs an executable directly and never goes through a shell, so nothing in the arguments is expanded, redirected or split. On Windows that means a .bat or .cmd file cannot be launched on its own – run cmd.exe with /c and the script as arguments.