module SpawnPassthrough
Constants
- VERSION
Public Class Methods
There are a few gotchas in spawning processes and passing through the current FDs and signals:
First, we cannot use Kernel.exec, because we need to return control to the caller.
Second, we need to somehow capture the exit status, because we assume the caller cares.
Third, we don't want to use a separate process group, because the caller might be used non-interactively, and we want to let other processes signal the caller and let this reach the subprocess we're spawning here.
To do this, we have to handle interrutps as a signal, as opposed to handle an Interrupt exception. The reason for this has to do with how Ruby's wait is implemented (this happens in process.c's rb_waitpid). There are two main considerations here:
-
It automatically resumes when it receives EINTR, so our control is pretty
high-level here.
-
It handles interrupts prior to setting $? (this appears to have changed
between Ruby 2.2 and 2.3, perhaps the newer implementation behaves differently).
Unfortunately, this means that if we receive SIGINT while in Process::wait2, then we never get access to SSH's exitstatus: Ruby throws a Interrupt so we don't have a return value, and it doesn't set $?, so we can't read it back there.
Of course, we can't just call Proces::wait2 again, because at this point, we've reaped our child.
To solve this, we add our own signal handler on SIGINT, which simply proxies SIGINT to SSH if we happen to have a different process group (which shouldn't be the case), just to be safe and let users exit the CLI.
This code was originally implemented for the Aptible CLI.
# File lib/spawn_passthrough.rb, line 41 def self.spawn_passthrough(command) redirection = { in: :in, out: :out, err: :err, close_others: true } pid = Process.spawn(*command, redirection) reset = Signal.trap('SIGINT') do # FIXME: If we're on Windows, we don't really know whether SSH # received SIGINT or not, so for now, we just ignore it. next if Gem.win_platform? begin # SSH should be running in our process group, which means that # if the user sends CTRL+C, we'll both receive it. In this # case, just ignore the signal and let SSH handle it. next if Process.getpgid(Process.pid) == Process.getpgid(pid) # If we get here, then oddly, SSH is not running in our process # group and yet we got the signal. In this case, let's simply # ignore it. Process.kill(:SIGINT, pid) rescue Errno::ESRCH # This could happen if SSH exited after receiving the SIGINT, # Ruby waited it, then ran our signal handler. In this case, we # don't need to do anything, so we proceed. end end begin _, status = Process.wait2(pid) return status.exited? ? status.exitstatus : 128 + status.termsig ensure Signal.trap('SIGINT', reset) end end