Kill Process and Sub-Processes in Ruby on Windows

Kill process and sub-processes in Ruby on Windows

I eventually solved this in the following manner

First I installed the sys-proctable gem

gem install 'sys-proctable'

then used the originally posted code to spawn the process, and the following to kill it (error handling omitted for brevity)

require 'win32/process'
require 'sys/proctable'
include Win32
include Sys

to_kill = .. // PID of spawned process
ProcTable.ps do |proc|
to_kill << proc.pid if to_kill.include?(proc.ppid)
end

Process.kill(9, *to_kill)
to_kill.each do |pid|
Process.waitpid(pid) rescue nil
end

You could change the kill 9 to something a little less offensive of course, but this is the gist of the solution.

What Process.kill signals are available on Windows?

I think I found a solution. To find out what signals your current platform supports, run this:

ruby -e "puts Signal.list"

On Windows:

{"EXIT"=>0, "INT"=>2, "ILL"=>4, "ABRT"=>22, "FPE"=>8, "KILL"=>9, "SEGV"=>11, "TERM"=>15}

Cross-platform way to Check if a (non-child) Process is Running

The easiest way is to test which operation system your ruby is running, and then through the command line use a simple test on linux or windows to check if your program is running.

The easiest way often is not the best way. Yet, be prepared to face some strange code, for example, how to simple kill a process on windows. It is your call.

I would love someone say I'm wrong and that there is a great way to manage processes in ruby being cross-platform. Please do.

How to stop a process from within the tests, when testing a never-ending process?

With the answer from CodeGnome on how to use Timeout::timeout and the answer from andyconhin on how to redirect Process::spawn IO, I came up with two Minitest helpers that can be used as follows:

it "runs a deamon" do
wait_for(timeout: 2) do
wait_for_spawned_io(regexp: /Hello World/, command: ["bin/runner"])
end
end

The helpers are:

def wait_for(timeout: 1, &block)
Timeout::timeout(timeout) do
yield block
end
rescue Timeout::Error
flunk "Test did not pass within #{timeout} seconds"
end

def wait_for_spawned_io(regexp: //, command: [])
buffer = ""

begin
read_pipe, write_pipe = IO.pipe
pid = Process.spawn(command.shelljoin, out: write_pipe, err: write_pipe)

loop do
buffer << read_pipe.readpartial(1000)
break if regexp =~ buffer
end
ensure
read_pipe.close
write_pipe.close
Process.kill("INT", pid)
end

buffer
end

These can be used in a test which allows me to start a subprocess, capture the STDOUT and as soon as it matches the test Regular Expression, it passes, else it will wait 'till timeout and flunk (fail the test).

The loop will capture output and pass the test once it sees matching output. It uses a IO.pipe because that is most transparant for subprocesses (and their children) to write to.

I doubt this will work on Windows. And it needs some cleaning up of the wait_for_spawned_io which is doing slightly too much IMO. Antoher problem is that the Process.kill('INT') might not reach the children which are orphaned but still running after this test has ran. I need to find a way to ensure the entire subtree of processes is killed.

Having a IO.popen command be killed when the caller process is killed

Yes, you just need to install a signal handler in your main process to trap Ctrl-C (SIGINT) and then send that signal to the child process.

This example should explain things:

# Start child and save its pid
io = IO.popen("sleep 600")
pid = io.pid

# Print the output of the ps command (just for demonstration)
puts "Checking #{pid} ..."
system("ps #{pid}")

puts "Installing signal handler..."

Signal.trap("INT") {
# Ctrl-C was pressed...
puts "Caught interrupt - killing child..."

# Kill child process...
Process.kill("INT", pid)

  # This prevents the process from becoming defunct
io.close

# Just for demonstration purposes - check that it died
puts "Checking #{pid} ..."
system("ps #{pid}")

# Forward the INT signal back to the parent
# ...or you could just call "exit" here too.
puts "Forwarding signal to myself..."
Signal.trap("INT", "DEFAULT")
Process.kill("INT", 0)
}

# Make the parent "do some stuff"...
puts "Sleeping parent..."
sleep 600

Output:

> ruby popen_test.rb
Checking 2474 ...
PID TTY STAT TIME COMMAND
2474 pts/0 S+ 0:00 sleep 600
Installing signal handler...
Sleeping parent...

# Press Ctrl-C ...

Caught interrupt - killing child...
Checking 2474 ...
PID TTY STAT TIME COMMAND
Forwarding signal to myself...
kill.rb:20: Interrupt
from kill.rb:24:in `call'
from kill.rb:24:in `sleep'
from kill.rb:24

How do I start a Rails server programatically from a Ruby script

I eventually decided to avoid any ickiness and start the rails server in its own process, as detailed in this post. (Being able to kill it plus its child processes consistently was the main blocker and the original reason I'd considered starting it in a thread instead.)



Related Topics



Leave a reply



Submit