I have a program that launches a Process that runs the fakeroot
command in order to safely check for package updates without requiring root. However, when I made it into a systemd service, I noticed that it was spawning a lot of faked
processes and never cleaning them up. So i began investigating on why this happened. Here is the minimal code that reproduces the issue:
import Foundation
Task.detached {
let task: Process = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/fakeroot")
task.arguments = ["echo", "hello"]
try task.run()
task.waitUntilExit()
exit(0)
}
RunLoop.main.run()
Simply compile and run this code and then check running processes using a tool like htop
. You'll see a new faked
process running, even after the swift program exited. (You can kill the faked
process with signal 9, btw) I have not tested if this behavior is reproducable on macOS, i have only tested on linux.
However, this similar code works correctly and there is no "zombie" faked
process left running:
import Foundation
let task: Process = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/fakeroot")
task.arguments = ["echo", "hello"]
try task.run()
task.waitUntilExit()
Looking into how fakeroot
and faked
operate, it seems that when the program spawned by fakeroot
exits, it sends a SIGTERM
signal to faked
, which tells it to exit. This is documented in faked.c
. Inspecting the faked
process using kill -d
we can see that the signal was successfully sent by fakeroot
, but is blocked by faked
when launched from an async context. In fact it seems to block a lot of signals:
Pending (process): TERM
Blocked: HUP INT QUIT ABRT USR1 USR2 ALRM TERM STKFLT CONT TSTP TTIN TTOU URG XCPU XFSZ VTALRM WINCH IO PWR RT0 RT1 RT2 RT3 RT4 RT5 RT6 RT7 RT8 RT9 RT10 RT11 RT12 RT13 RT14 RT15 RT16 RT17 RT18 RT19 RT20 RT21 RT22 RT23 RT24 RT25 RT26 RT27 RT28 RT29 RT30
Caught: HUP INT QUIT ILL TRAP ABRT BUS FPE USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD TTIN TTOU URG XCPU XFSZ VTALRM PROF IO PWR SYS RT0 RT1 RT2 RT3 RT4 RT5 RT6 RT7 RT8 RT9 RT10 RT11 RT12 RT13 RT14 RT15 RT16 RT17 RT18 RT19 RT20 RT21 RT22 RT23 RT24 RT25 RT26 RT27 RT28 RT29 RT30
This is not the case when launched from a sync context, which I managed to record here by making fakeroot
run sleep 1000
:
Caught: HUP INT QUIT ILL TRAP ABRT BUS FPE USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD TTIN TTOU URG XCPU XFSZ VTALRM PROF IO PWR SYS RT0 RT1 RT2 RT3 RT4 RT5 RT6 RT7 RT8 RT9 RT10 RT11 RT12 RT13 RT14 RT15 RT16 RT17 RT18 RT19 RT20 RT21 RT22 RT23 RT24 RT25 RT26 RT27 RT28 RT29 RT30
This leads me to believe that libdispatch
is configuring threads to block those signals. In fact, this code sample also blocks signals:
import Foundation
let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now())
timer.setEventHandler {
let task: Process = Process()
task.executableURL = URL(fileURLWithPath: "/usr/bin/fakeroot")
task.arguments = ["echo", "hello"]
try! task.run()
task.waitUntilExit()
exit(0)
}
timer.activate()
RunLoop.main.run()
So my question is: is there a way to configure libdispatch
to not block signals? or is there a better way to handle this?