Some time ago I found myself setting up yet another Sage server for the ISU Math Department. Each time I set one of these up, I try to do a better job than the time before, and I think I’m finally getting somewhere. However, I keep running into this problem: there’s no good way to run sage as a daemon.
The problem is that starting the sage notebook starts a shell script which starts a shell script which starts python which starts python and so on. This isn’t really that surprising for a service, but it ends up barfing all over the process table and doesn’t provide you with a nice way to clean up the mess. Of course when you run it in a shell and press control-C, everything cleans up fine but simply killing one of the processes won’t work.
So, how did I solve this? Well, like any good Unix admin, I wrote a short perl script. The basic idea is to emulate a shell. It forks, changes the process group id, and then executes so that everything that gets run from there on out is grouped together. It then leaves a helper process lying around (and will put its pid in a file) so that when you kill the helper, everything else dies as well.
The scrpt can be found here:
#! /usr/bin/perl
# Copyright (c) 2012 Faith Ekstrand and Iowa State University
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to permit
# persons to whom the Software is furnished to do so, subject to the
# following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
# USE OR OTHER DEALINGS IN THE SOFTWARE.
use strict;
my $daemon = 0;
my $help = 0;
my $pid_file = '';
my $command = '';
my $infile = '';
my $outfile = '';
my $errfile = '';
my $waittime = 5;
use POSIX;
use Getopt::Long;
my $good_opts = GetOptions ('help|h' => \$help, 'daemon|d!' => \$daemon,
'pid-file|p=s' => \$pid_file, 'command|c=s' => \$command,
'in-file=s' => \$infile, 'out-file=s' => \$outfile,
'err-file=s' => \$errfile, 'wait-time|w=f' => \$waittime);
if ($help or not $good_opts) {
print << "EOT";
Usage:
daemonizer.pl [options] [-- command]
Options:
-h, --help print this help
-d, --(no)daemon run as a daemon (default: off)
-c, --command=command execute the given command
-p, --pid-file=file print the PID to the given file
-w, --wait-time=time specify how long (in minutes) we should wait to
make sure that the child started correctly.
--in-file=file redirect standard input to a file
--out-file=file redirect standard output to a file
--err-file=file redirect standard error to a file (default: if
standard output is redirected, redirect standard
error to the same file)
EOT
exit 0;
}
###
# Subroutines to handle daemonizing.
###
if (! $command) {
$command = join(' ', @ARGV);
}
my $daemon_pgid;
sub daemon_signal_handler {
my $signame = shift;
print "Recieved SIG$signame, passing on to children...\n";
if ($signame == 'INT') {
kill -2, $daemon_pgid;
} elsif ($signame == 'QUIT') {
kill -3, $daemon_pgid;
} elsif ($signame == 'TERM') {
kill -15, $daemon_pgid;
}
}
sub run_daemon {
# Redirect standard input, output, and error as instructed (We want to
# do this first so that even the daemonizer messages will get logged)
if ($infile) {
open STDIN, '<', $infile;
}
if ($outfile) {
open STDOUT, '>', $outfile;
}
# If stderr is not specified, simply dump it to the same place as
# stdout
if ($errfile and $errfile != $outfile) {
open STDERR, '>', $errfile;
} elsif ($outfile) {
open STDERR, '>&STDOUT';
}
# Now we can fork
my $pid = fork;
if (! $pid) {
# Everything from here on out has this group id.
setpgid(0, 0);
exec $command;
}
# The child process will never get here
$daemon_pgid = $pid;
# We'll gracefully handle these two
$SIG{'INT'} = \&daemon_signal_handler;
$SIG{'QUIT'} = \&daemon_signal_handler;
$SIG{'TERM'} = \&daemon_signal_handler;
# We'll make the rash assumption that what we call won't exit until
# everything else has
waitpid $pid, 0;
exit $?;
}
###
# Actually executing the process
###
if ($daemon) {
my $pid = fork();
if ($pid) {
# Wait 5 seconds to make sure the server has properly started
sleep $waittime;
if (waitpid $pid, WNOHANG > 0) {
exit $?;
}
if ($pid_file) {
# If we have a pid file specified, record the pid of the daemon
open PIDFILE, '>', $pid_file;
print PIDFILE "$pid";
close PIDFILE;
}
exit 0;
} else {
# Run the daemon
run_daemon();
}
} else {
run_daemon();
}