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

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"
...

Related

Why can't I close SSH connection when having forked a process with 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

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.

Cleaning up after a ruby script -- trapping signals

My ruby script creates a tempfile and spawns an potentially long-running external process. Neither may continue to exist after the script ends, no matter the way in which the script terminates.
I thought the following lines would take care of things:
stderr = File.open(Tempfile.new(__FILE__),'w')
trap("EXIT") { FileUtils.rm_f stderr.path }
pid = spawn("dd", *ARGV, STDERR => stderr )
trap("EXIT") { FileUtils.rm_f stderr.path; Process.kill pid }
they're supposed to be a rewrite of the following bash code, which seems to work fine,
dd_output=`mktemp`
trap "rm -f $dd_output" EXIT
dd "$#" 2>| $dd_output & pid=$!
trap "rm -f $dd_output; kill $pid" EXIT
but they don't.
If an exception is raised later on, the spawned process doesn't die, otherwise it does.
Could anyone tell me what I'm doing wrong?
Edit:
Traps do work.
The above code has multiple blemishes:
Tempfile takes car of itself -- it is likely to already have been
deleted in the trap handler, which may cause FileUtils.rm_f to raise
another error, preventing.
Process.kill needs a signal -- Process.kill "TERM", pid (or "KILL"). The raised error shadowed the error for my faulty invocation of Process.kill.
Fixed code:
stderr = Tempfile.new(__FILE__)
pid = spawn("dd", *ARGV, STDERR => stderr )
trap("EXIT") { Process.kill "TERM", pid }
Ensure works too.
I think ensure might be able to help you here, it will always execute the code inside. It is similary to Java's finally.
stderr = Tempfile.new(__FILE__)
begin
pid = spawn("dd", *ARGV, STDERR => stderr )
ensure
FileUtils.rm_f stderr.path
Process.kill pid
end
If that doesn't do the trick you could try adding an at_exit handler.

How to handle signals in bash during synchronous execution?

I have a bash script process which at some point executes a long-running subprocess synchronously.
During the run of that subprocess, a signal is sent directly to the bash script process requesting the script to terminate.
Is there any way to intercept that signal, terminate the subprocess and then exit the bash process?
Apparently, bash's signal handling never interrupts synchronous calls?
I cannot control the fact that the termination signal is sent to the bash process. Although if the signal could propagate to the child process, that would also solve my issue.
thanks in advance,
Broes
See the man page of bash, chapter SIGNALS:
If bash is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes. When bash is waiting for an asynchronous command via the wait builtin, the reception of a signal for which a trap has been set will cause the wait builtin to return immediately with an exit status greater than 128, immediately after which the trap is executed.
So, run your external program asynchronously and use wait. Kill it using $!.
Here's a bash utility function I wrote to handle this. It's proved useful and robust. I hope you find it useful.
# Run a command in a way that can be interrupted by a signal (eg SIGTERM)
#
# When bash receives a SIGTERM it normally simply exits. If it's executing a subprocess
# that subprocess isn't signaled. (Typically that's not a problem for interactive shells
# because the entire Process Group gets sent the signal.)
#
# When running a script it's sometimes useful for the script to propagate a SIGTERM
# to the command that was running. We can do that by using the trap builtin to catch
# the signal. But it's a little tricky, per the bash manual:
#
# If bash is waiting for a command to complete and receives a signal for
# which a trap has been set, the trap will not be executed until the
# command completes.
#
# so a script executing a long-running command with a signal trap set won't
# notice the signal until later. There's a way around that though...
#
# When bash is waiting for an asynchronous command via the wait builtin, the
# reception of a signal for which a trap has been set will cause the wait
# builtin to return immediately with an exit status greater than 128,
# immediately after which the trap is executed.
#
# Usage:
#
# interruptable [options] command [args]
#
# Options:
# --killall - put the child into a process group (via setsid)
# and send the SIGTERM to the process group
# --debug - print a message including pid of the child
#
# Usage examples:
#
# interruptable sleep 3600
#
# If not interrupted, the exit status of the specified command is returned.
# If interrupted, the specified command is sent a SIGTERM and the current
# shell exits with a status of 143.
interruptable() {
# handle options
local setsid=""
local debug=false
while true; do
case "${1:-}" in
--killall) setsid=setsid; shift ;;
--debug) debug=true; shift ;;
--*) echo "Invalid option: $1" 1>&2; exit 1;;
*) break;; # no more options
esac
done
# start the specified command
$setsid "$#" &
local child_pid=$!
# arrange to propagate a signal to the child process
trap '
exec 1>&2
set +e
trap "" SIGPIPE # ensure a possible sigpipe from the echo does not prevent the kill
echo "${BASH_SOURCE[0]} caught SIGTERM while executing $* (pid $child_pid), sending SIGTERM to it"
# (race) child may have exited in which case kill will report an error
# if setsid is used then prefix the pid with a "-" to indicate that the signal
# should be sent to the entire process group
kill ${setsid:+-}$child_pid
exit 143
' SIGTERM
# ensure that the trap doesn't persist after we return
trap 'trap - SIGTERM' RETURN
$debug && echo "interruptable wait (child $child_pid, self $$) for: $*"
# An error status from the child process will trigger an exception (via set -e)
# here unless the caller is checking the return status
wait $child_pid # last command, so status of waited for command is returned
}
Yes, the signal can be intercepted with the trap command. See the example below:
#!/bin/bash
function wrap {
local flag=0
trap "flag=1" SIGINT SIGTERM
xeyes &
subppid=$!
while :
do
if [ $flag -ne 0 ] ; then
kill $subppid
break
fi
sleep 1
done
}
flag=0
trap "flag=1" SIGINT SIGTERM
wrap &
wrappid=$!
while : # This is the same as "while true".
do
if [ $flag -ne 0 ] ; then
kill $wrappid
break
fi
sleep 1 # This script is not really doing anything.
done
echo 'end'
What trap basically does is that it executes the command between "". So here the main function is in the while loop below. In every iteration the script checks if the flag is set, if not, it sleeps for a second. Before that, we remembered the pid of the child process via $!. The trap issues the command when the SIGINT or SIGTERM is caught (for other signals see kill manual).
The wrapper function does the same, as the main function. Additionally it calls the actual subprocess function (in this case subprocess is xeyes). When the wrapper function receives SIGTERM signal from the main function (main function also caught one of the signals), the wrapper function can clean up things before actually killing the subprocess. After that it breaks from the while loop and exits the wrapper function. Then the main function also breaks and prints 'end'.
edit:
Hope I understand this correctly, you are forced to execute xeyes &. Then the steps would be as follows (in terminal):
xeyes &
subpid=$!
trap "kill $subpid && exit " SIGINT SIGTERM
.... other stuff
.... more stuff
^C #TERMINATE - this firstly kills xeyes and then exits the terminal

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