I was talking in particular about the current implementation of Process
in swift-corelibs-foundation, which Subprocess
would eventually replace.
On POSIX and POSIX-like systems (including Linux, Darwin, and FreeBSD) exit codes passed to exit()
are distinguished from signal codes that cause processes to terminate. These two domains are easily distinguished using POSIX-layer API or using Process.terminationStatus
and Process.terminationReason
.
On Windows, signals are not a feature of the kernel and are emulated in userspace. If you trigger a signal in a Windows process (e.g. by calling abort()
to raise SIGABRT
), the system invokes the signal handler for that signal, and the default signal handlers on Windows all… call _Exit(3)
. Like, literally the value 3
. There is then no API that a listening parent process can invoke to determine the signal that was raised—the API you'd use, GetExitCodeProcess()
, just says "the process exited with code 3
." Not terribly useful.
Windows also has a feature called Structured Exception Handling that… is sort of intertwined with C++ exceptions but is also distinct, and has various tendrils in the OS as well as in the Visual C++ compiler. I won't try to explain them further here, except to say that if a process raises a structured exception and doesn't handle it, the system terminates the process as if by calling _Exit(exceptionCode)
, where exceptionCode
is a 32-bit value that encodes the specific exception that occurred. A listening parent process would again be told "the process exited with code exceptionCode
", and there is no way to determine if exceptionCode
is actually an exception code or if the child process just passed the value to exit()
.
Back to Process
: the current Windows implementation of terminationStatus
and terminationReason
does not reliably tell you if a signal was raised. Instead, if the bits of the child process' exit code match one of the patterns Microsoft defines for unhandled structured exceptions, then the bits are unpacked, the low 30 (out of 32) bits are used as terminationStatus
and terminationReason
is set to .uncaughtSignal
.
The result of all this rambling is that abort()
on macOS or Linux is reported as terminationStatus == SIGABRT && terminationReason == .uncaughtSignal
, while on Windows it's reported as terminationStatus == 3 && terminationReason == .exit
, while terminationReason == .uncaughtSignal
is used for an entirely different class of failure modes.
Swift Testing makes a "best effort" to rectify this situation. Since the child processes spawned by Swift Testing are controlled by it, on Windows we replace the default signal handlers (that exit with code 3
) with ones that try to pack the original signal value into an otherwise-unused exception code before calling _Exit()
; the parent process then knows how to recognize these exception codes and unpack them back into signals, so we get more reliable signal reporting on Windows (unless some other code in the child process resets the signal handler before raising, but again, "best effort.")
Hope that helps!