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

Class Method Details

.alive?(pid) ⇒ Boolean

Returns:

  • (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

Returns:

  • (Boolean)


29
# File 'lib/everywhere/child_processes.rb', line 29

def isolated? = !!Thread.current.thread_variable_get(ISOLATE)

.listObject



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

.nowObject



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) }