Why can't I close SSH connection when having forked a process with ruby? - ruby

Consider the following Ruby script:
fork do
loop do
sleep 1
end
end
As soon as I run this script on a Linux server I'm connected to via SSH, closing the SSH connection subsequently hangs (I have to exit it by typing ~ + RETURN, otherwise the connection remains open / keeps hanging. I'm using ruby 3.0.1p64 and the server OS is Fedora.
The problem even occurs when detaching from the forked process:
pid = fork do
loop do
sleep 1
end
end
Process.detach(pid)
What solves this issue is redirecting stdout and stderr to /dev/null, but I need the output of the parent process to be visible so that's not a feasible solution:
ruby script.rb < /dev/null >& /dev/null
Why is that and is there a way around it?

Found the solution:
pid = fork do
STDIN.reopen File.open("/dev/null", "r")
null_out = File.open "/dev/null", "w"
STDOUT.reopen null_out
STDERR.reopen null_out
loop do
sleep 1
end
end

Related

Track progress of dd command called using open3 in ruby

I am trying to monitor the progress of copying a raspberry-pi OS image to a microSD card. This is similar to Kill a process called using open3 in ruby, except I'm not killing the process, I'm sending it a command for it to issue a progress message.
rpath = device_path.gsub(/disk/, "rdisk")
puts "\n\nCopying image to #{rpath}"
if false
stdout_err, status = Open3.capture2e( "sudo", "dd", "bs=1m", "if=#{source_path}", "of=#{rpath}" )
puts stdout_err
else
cmd = "sudo dd bs=1m if=#{source_path} of=#{rpath}"
Open3.popen2e(cmd) do |stdin, stdout_err, wait_thr|
Thread.new do
stdout_err.each {|l| puts l}
end
Thread.new do
while true
sleep 5
if true
Process.kill("INFO", wait_thr.pid) #Tried INFO, SIGINFO, USR1, SIGUSR1
# all give: `kill': Operation not permitted (Errno::EPERM)
else
stdin.puts 20.chr #Should send ^T -- has no effect, nothing to terminal during flash
end
end
end
wait_thr.value
end
The first section (after 'if false') flashes the image using Open3.capture2e. This works, but of course issues no progress information.
The section after the 'else' flashes the image using Open3.popen2e. It also attempts to display progress by either issuing 'Process.kill("INFO", wait_thr.pid)', or by sending ^T (20.chr) to the stdin stream every 5 seconds.
The Process.kill line generates an "Operation not permitted" error. The stdin.puts line has no effect at all.
One other thing... While the popen2e process is flashing, hitting ctrl-T on the keyboard DOES generate a progress response. I just can't get it to do it programmatically.
Any help is appreciated!
Newer versions of dd have an optional progress bar, as seen here. Even so I think you'll want to rethink how you execute that shell command so that it thinks it's attached to a terminal. Easiest thing to do is fork/exec, like:
cmd = "sudo dd bs=1m if=#{source_path} of=#{rpath} status=progress"
fork do
exec(cmd) # this replaces the forked process with the cmd, giving it direct access to your terminal
end
Process.wait() # waits for the child process to exit
If that's not an option you may want to look into other ways of getting unbuffered output, including just writing a bash script instead of a ruby one.

Is it possible to ignore SIGHUP in Ruby?

I want to create a Ruby script, which will start like this:
$ ruby script.rb &
Then, I will close the console and it must stay alive, working in the background. At the moment I have to run it like this, in order to achive that:
$ nohup ruby script.rb &
I want to get rid of nohup and deal with SIGHUP directly inside the script -- simply ignore it. Is it possible?
Sure, just Signal.trap HUP signal:
def do_fork
$pid = fork do
Signal.trap("HUP") do
puts "Received HUP, ignoring..."
end
Signal.trap("TERM") do
puts "Received TERM, terminating..."
exit(0)
end
while true do sleep(10_000) end
end
Process.detach($pid)
end
do_fork
Copy the code above to some file and run it with ruby file.rb to see it ignores kill -HUP pid and closes on kill -TERM pid.

Killing forked process in ruby with child processes

The following code forks the main processes and runs a command in backticks. The kill at the end of the script only kills the forked process but not it's child processes (i.e. the sleep command).
pid = fork do
Thread.new do
`sleep 20`
end
end
sleep(1)
Process.kill("HUP",pid)
Is there a way to kill all child processes (generated by backtick commands in threads in the forked process) other than searching through the process tree?
Behind the scene both system and backtick operations use fork to fork
the current process and then they execute the given operation using
exec .
Since exec replaces the current process it does not return anything if
the operation is a success. If the operation fails then
`SystemCallError is raised.
http://blog.bigbinary.com/2012/10/18/backtick-system-exec-in-ruby.html
You can use
pid = Process.spawn('sleep 20')
to get the PID of the process immediately. Your code above would change to:
pid = Process.spawn('sleep 20')
sleep(1)
Process.kill('HUP',pid)

Run a command in current terminal in ruby then execute code when it exits

What I need is:
Execute something before calling a system command.
Execute my system command
that involve prompting and getting answers from the user
keeping the effects of ctrl-c on the called command intact
Get the result of my system command and carry on with more ruby code execution
So far I tried something that looks like:
#!/usr/bin/env ruby
p "Foo"
exit_value = exec 'heroku run console'
p "Bar"
exit exit_value
This one fails because exec replaces and terminate current process, so no more ruby code is executed after exec
I've already read this post:
How to run code after ruby Kernel.exec
And I tried to make do with a Kernel#system call:
#!/usr/bin/env ruby
p "Foo"
system 'heroku run console'
p "Bar"
exit $?
This one also fails, because ctrl-c is apparently caught by my ruby process and kills it instead of reaching its intended target.
So, is there a way to deal with these peculiar requirements?
Thanks a lot to hek2mgl for pointing in the right direction:
include Signal
include Process
# Handling SIGINT by doing nothing prevents default behaviour
# (killing both processes)
Signal.trap("INT") {}
# Fork off subprocess (so exec won't exit from your main process)
pid = fork
if pid == nil then
# Child code. Use exec(!) to avoid the signal handler
# getting called in the child.
exec 'heroku run console'
else
# Wait for subprocess to exit
wait pid
# "wait" sets the $? according to the subprocess exit status
exit_status = $?.exitstatus
p "Execute more ruby code"
exit exit_status
end
I would install a signal trap for SIGINT, fork off the sub process, exec the command (to prevent the signal handler from running in parent and child) and kill the subprocess if SIGINT occurs:
include Signal
include Process
# Define a signal handler for SIGINT
Signal.trap("INT") do
if $pid != nil then
# Kill the subprocess
Process.kill("INT", $pid)
else
# Terminate ourself
exit 1
end
end
# Fork off subprocess
# The $ marks the variable as global
$pid = fork
if $pid == nil then
# Child code. Use exec(!) to avoid the signal handler
# getting called in the child.
exec 'bash -c "echo test; sleep 3; echo test"'
end
# Wait for subprocess to exit
wait $pid
# Reset $pid
$pid = nil
exit_status = $?.exitstatus
# Detect whether the child process has been killed
if exit_status == nil then
p "Child process has been killed."
end
p "Execute more ruby code"
...

How can I handle (or prevent) SIGCHLD signals from ruby backquote invocations?

I have a long-running process with some child processes that must be restarted if they exit. To handle clean restarts of these child processes, I trap the exit signal with
trap("CLD") do
cpid = Process.wait
... handle cleanup ...
end
The long-running process occasionally needs to invoke 'curl' using a backquote as in
`/usr/bin/curl -m 60 http://localhost/central/expire`
The problem is that the backquote invocation is causing me to get a SIGCHLD and making my trap fire. This then gets stuck in the CLD trap because Process.wait does not finish. If there happen to be no (non-backquote) child processes at that time, the Process.wait instead gives an Errno::ECHILD exception.
I can circumvent this problem by wrapping the backquote call with this line before:
sig_handler = trap("CLD", "IGNORE") # Ignore child traps
and this line after the backquote invocation:
trap("CLD", sig_handler) # replace the handler
but this means that I may miss a signal from the (non-backquote) child processes during that window, so I'm not really happy with that.
So, is there a better way to do this?
(I am using ruby 1.9.1p243 on GNU/Linux 2.6.22.6 if it matters)
Update:
The code below illustrates the problem (and my current solution for it).
There seems to be some strange timing issue here since I don't always get the ECHILD exception. But just once is enough to mess things up.
#!/usr/bin/env ruby
require 'pp'
trap("CLD") do
cpid = nil
begin
puts "\nIn trap(CLD); about to call Process.wait"
cpid = Process.wait
puts "In trap(CLD); Noting that ssh Child pid #{cpid}: terminated"
puts "Finished Child termination trap"
rescue Errno::ECHILD
puts "Got Errno::ECHILD"
rescue Exception => excep
puts "Exception in CLD trap for process [#{cpid}]"
puts PP.pp(excep, '')
puts excep.backtrace.join("\n")
end
end
#Backtick problem shown (we get an ECHILD most of the time)
puts "About to invoke backticked curl"
`/usr/bin/curl -m 6 http://developer.yahooapis.com/TimeService/V1/getTime?appid=YahooDemo`
sleep 2; sleep 2 # Need two sleeps because the 1st gets terminated early by the trap
puts "Backticked curl returns"
# Using spawn
puts "About to invoke curl using spawn"
cpid = spawn("/usr/bin/curl -m 6 http://developer.yahooapis.com/TimeService/V1/getTime?appid=YahooDemo")
puts "spawned child pid is #{cpid} at #{Time.now}"
Start monitored subprocesses from a subprocess
Just start your tracked and monitored children from a child of your main process that never exits. That way it won't notice the backtick children exiting...
And if you do this, you could avoid the use of SIGCHLD entirely, as you could just use a loop with a wait in it to notice children exit events.
Other ideas:
ignore one SIGCHLD every time you execute a backtick command. ISTM that you might ignore a "real" SIGCHLD by accident this way, but that won't matter, because you would then get a "spurious" one that you would process.

Resources