Module: Everywhere::ChildProcesses
- Defined in:
- lib/everywhere/child_processes.rb
Overview
A thread-scoped registry of the long-running children a task spawned, so the task can be cancelled without hunting pids by hand.
every dev runs each build in its own thread. When the session ends (or a
build has to be abandoned) we need to kill that thread's xcodebuild or
gradlew and nothing else — a global list would leak entries from threads
that died unexpectedly and would make "which build does this pid belong to?"
unanswerable. Registering against the spawning thread answers it for free.
Only genuinely long-running children register (see Shellout.run_logged!). Short probes — simctl, adb, xcrun — deliberately stay in the CLI's own process group, where a Ctrl-C at the terminal kills them as it always has.
Constant Summary collapse
- LOCK =
Mutex.new
- PIDS =
:everywhere_child_pids- ISOLATE =
:everywhere_isolate_children
Class Method Summary collapse
- .alive?(pid) ⇒ Boolean
-
.isolate! ⇒ Object
Mark this thread's long-running children as needing their own process group — set by TaskPool for every worker.
- .isolated? ⇒ Boolean
- .list ⇒ Object
- .now ⇒ Object
-
.signal(pid, sig) ⇒ Object
Signal a child's whole process group, falling back to the bare pid when it doesn't lead one.
-
.terminate(thread, grace: 5) ⇒ Object
TERM every child
threadspawned, escalating to KILL for anything still alive aftergrace. - .track(pid) ⇒ Object
- .untrack(pid) ⇒ Object
Class Method Details
.alive?(pid) ⇒ Boolean
58 59 60 61 62 63 64 65 |
# File 'lib/everywhere/child_processes.rb', line 58 def alive?(pid) Process.kill(0, pid) true rescue Errno::ESRCH false rescue Errno::EPERM true end |
.isolate! ⇒ Object
Mark this thread's long-running children as needing their own process
group — set by TaskPool for every worker. Outside a worker this is false
and every build / every release keep today's behavior, where a Ctrl-C
at the terminal reaches the compiler directly.
27 |
# File 'lib/everywhere/child_processes.rb', line 27 def isolate! = Thread.current.thread_variable_set(ISOLATE, true) |
.isolated? ⇒ Boolean
29 |
# File 'lib/everywhere/child_processes.rb', line 29 def isolated? = !!Thread.current.thread_variable_get(ISOLATE) |
.list ⇒ Object
69 70 71 72 |
# File 'lib/everywhere/child_processes.rb', line 69 def list Thread.current.thread_variable_get(PIDS) || Thread.current.thread_variable_set(PIDS, []) end |
.now ⇒ Object
67 |
# File 'lib/everywhere/child_processes.rb', line 67 def now = Process.clock_gettime(Process::CLOCK_MONOTONIC) |
.signal(pid, sig) ⇒ Object
Signal a child's whole process group, falling back to the bare pid when it doesn't lead one. Never raises: callers are teardown paths.
48 49 50 51 52 53 54 55 56 |
# File 'lib/everywhere/child_processes.rb', line 48 def signal(pid, sig) Process.kill(sig, -Process.getpgid(pid)) rescue StandardError begin Process.kill(sig, pid) rescue StandardError nil end end |
.terminate(thread, grace: 5) ⇒ Object
TERM every child thread spawned, escalating to KILL for anything still
alive after grace. Returns the pids that had to be killed.
36 37 38 39 40 41 42 43 44 |
# File 'lib/everywhere/child_processes.rb', line 36 def terminate(thread, grace: 5) pids = LOCK.synchronize { (thread.thread_variable_get(PIDS) || []).dup } return [] if pids.empty? pids.each { |pid| signal(pid, "TERM") } deadline = now + grace sleep(0.1) while pids.any? { |pid| alive?(pid) } && now < deadline pids.select { |pid| alive?(pid) }.each { |pid| signal(pid, "KILL") } end |
.track(pid) ⇒ Object
31 |
# File 'lib/everywhere/child_processes.rb', line 31 def track(pid) = LOCK.synchronize { list << pid } |
.untrack(pid) ⇒ Object
32 |
# File 'lib/everywhere/child_processes.rb', line 32 def untrack(pid) = LOCK.synchronize { list.delete(pid) } |