Here's a handy bit of code that we use in a few places on the server.
It daemonizes a PHP script. The script MUST write to a PID file every so often (once a minute or so), using the result of posix_getpid(). Code for that is at the bottom.
If the script dies, or the pidfile is corrupt, or it was just never started in the first place, then the script will be started daemonized in the background.
If the script fails to write to the pid file, it will be killed and restarted if the pid file is detected as "old".
For daemonizing, it will ideally use pcntl, which means it can do all the daemonization itself. Since most PHP builds don't come with that though (WHY NOT???), it will also attempt to use system() to run the "nohup" command with the & operator. If that fails, it uses a kindof nohup written in perl, "daemonize.pl".
Note the licensing terms: public domain. This is the agreement under which I hope and plan to eventually release the client source, and all others I write. Whether I can depends on the lawyers, though.
As always, improvements and suggestions appreciated: this is particularly UGLY code, designed for utility! ![]()
[code:1]#!/usr/local/bin/php
<?php
/* This script daemonizes a PHP CLI script.
This code is explicitly released onto the public domain by myself,
Dewi Morgan, the author. As such, it is not subject to copyright.
You may do with it as you will. Credit, while appreciated, need not
be given. This notice does not need to be included.
Call from cron every minute as:
* * * * * /path/to/daemonize.php 2>&1 >> /path/to/logfile
Could be trivially modified to take the script and other globals
as arguments. But that seems a bit pointless to me.
*/
$PHP = "/usr/local/bin/php"; # Remember to change the #! line above, too.
$DEBUG = true; # Set to false to disable logging stuff.
$PWD = "/path/to/demon/script/";
$PIDFILE = $PWD."daemon.pid";
$DAEMON = $PWD."daemon.php";
# Must contain any of nohup, perl, daemonise.pl, is_up.php and this script
# That you intend to use.
$BINDIR = "/path/to/programs/";
# Get the following values from "kill -l" or /usr/include/linux/signal.h
# Should be correct for most unixes.
$SIGTERM = 15;
$SIGKILL = 9;
#to_log("starting daemon.");} # Can get spammy.
if (!empty($argv[1]) && $argv[1] === "-debug") {
$DEBUG = true;
array_shift($argv);
}
# If we've self-bootstrapped, then load the bot and run with it.
if (!empty($argv[1])
&& $argv[1] === "-nohup") {
to_log("arg detected, bootstrapping daemon through require.");
require($DAEMON);
to_log("killing daemon child.");
exit(0);
}
# If we're already started, then don't bother running.
if (empty($argv[1])) {
$test = 0;
$lines = array(0, 0);
$pid_data = "unread";
#to_log("Testing for pre-existing..."); # Can get spammy.
if (file_exists($PIDFILE)) {
$pid_data = file_get_contents($PIDFILE);
$lines = explode("\n", $pid_data);
if (!empty($lines[0]) && is_numeric($lines[0])
&& !empty($lines[1]) && is_numeric($lines[1]))
{
# Kill hung processes.
if ($lines[1] + 120 < time()) { // If it's an OLD PIDFILE...
to_log("OLD PIDFILE found from $lines[1] - ".date("r",$lines[1]).": $pid_data");
zero_file($PIDFILE);
# If found, kill. If it won't die, kill it harder. Then give up.
# In practice, it can take a bit longer than 20 seconds to die, but it
# tries again in a minute's time, and works.
if (is_alive($lines[0])) {
to_log("killing and waiting a few secs...");
posix_kill($lines[0], $SIGTERM);
# Loop for 10 seconds, or until it dies.
for ($i=0; $i<20 && is_alive($lines[0]); $i++) {
sleep(1);
if ($i == 10) { # After ten seconds, kill it harder.
to_log("Failed to kill it. Killing it harder!");
posix_kill($lines[0], $SIGKILL);
}
}
if (is_alive($lines[0])) {
to_log("Failed to kill it. Giving up.");
exit(0);
}
}
}
else {
#to_log("Young PIDFILE found."); # Can get spammy.
}
if (is_alive($lines[0])) {
# to_log("Already running, pid is $lines[0]. Last seen at ".date("r", $lines[1]));
exit(0);
}
else {
to_log("PIDFILE found, but process is dead.");
}
}
else {
to_log("Bad format PIDFILE found, zeroing, restarting...");
zero_file($PIDFILE);
}
}
else {
to_log("PIDFILE not found.");
}
to_log("NOT found running.");
}
# If pcntl_fork() doesn't exist, we need to load ourselves in the background, then die.
if (!function_exists("pcntl_fork")) {
to_log("no pcntl, calling self with nohup and param");
system("${BINDIR}nohup $PHP ${PWD}daemonize.php -nohup &", $return);
if ($return == 0) { exit(0); }
# If there was a problem, we have one more ace in the hole - perl!
to_log("nohup failed with return $return, calling self with perl");
system("${BINDIR}perl ${BINDIR}daemonize.pl &", $return);
to_log("killing daemon parent with return $return");
exit(0);
}
# If we've got the right functions to play with, all the above becomes moot.
to_log("we have pcntl! Doing it all ourselves!");
# Replace file handles
$fh_unused = array(STDIN, STDOUT);
ob_implicit_flush();
# Daemon Rule 1) Fork and exit the parent.
$pid = pcntl_fork();
if ($pid == -1) {
die("could not fork");
}
else if ($pid) {
exit(); # Kill the parent
}
# Daemon Rule 2) become session leader, pg leader, no term
$session_id = posix_setsid();
if (!$session_id) {
die("Could not detach from terminal.");
}
# Daemon Rule 3) cd to /
if (!chdir('/')) { die("Could not cd to rootfs"); }
# Daemon Rule 4) set file creation mask to 0
$oldmask = umask(00);
# Daemon Rule 5) Close unneeded file handles
foreach ($fh_unused as $fh) {
if (!fclose($fh)) { die("Unable to close $fh"); }
}
# Daemon Rule 6) Set up signal handlers where necessary.
pcntl_signal(SIGTERM, "sig_handler");
pcntl_signal(SIGHUP, "sig_handler");
function sig_handler($signo) {
switch ($signo) {
case SIGTERM:
# handle shutdown tasks
die("Caught thud SIGTERM");
break;
case SIGHUP:
# handle restart tasks
die("Caught thud SIGHUP");
break;
default:
# handle all other signals
}
}
# Meat of the daemon goes here.
to_log("requiring the bot...");
require_once($DAEMON);
exit(0);
# Access functions.
function is_alive($pid) {
global $BINDIR;
$result = system("${BINDIR}is_up $pid");
# to_log("is_alive (${BINDIR}is_up $pid) returned $result");
return (0 < $result);
}
function to_log($string) {
global $DEBUG;
if ($DEBUG) {
echo(date("r")." - $string\n");
}
}
function zero_file($PIDFILE) {
$fp = fopen($PIDFILE, "w");
fwrite($fp, ""); # Zero the PIDFILE.
fclose($fp);
}
?>[/code:1]
Because of security limitations imposed when running through cron, an external PHP script is needed, nohup.php
[code:1]
#!/bin/sh
# Wrapper script because in safe mode PHP gloms all those together.
# For security, full paths must be used!
path/to/ps -ef | path/to/grep $1 | path/to/grep daemonize | path/to/wc -l
[/php]
This is daemonize.pl, our perlscript daemonizer, use if all else fails.
[perl]#!/usr/bin/perl -w
use strict;
use POSIX qw( setsid );
my $debug = 1;
my $logfile = q(testlog);
# Season to taste
my @fh_unused = (\*STDIN, \*STDOUT);
open \*STDERR, ">> $ENV{'HOME'}/$logfile";
select((select(\*STDERR), $| = 1)[0]);
{
# Daemon Rule 1) Fork and exit the parent.
my $ppid = $$;
my $pid = fork and exit 0;
! defined $pid and die "No Fork: ", $!;
while (kill 0, $ppid) {
select undef, undef, undef, .001;
};
}
# Daemon Rule 2) become session leader, pg leader, no term
my $session_id = POSIX::setsid();
# Daemon Rule 3) cd to /
chdir '/' or die "Could not cd to rootfs", $!;
# Daemon Rule 4) set file creation mask to 0
my $oldmask = umask 00;
# Daemon Rule 5) Close unneeded file handles
close $_ or die $! for @fh_unused;
# Meat of the daemon goes here.
exec "php daemonize2.php -nohup";
exit 0;
[/code:1]
Finally, this is the code that needs to go into the top of your init code, and the main loop of your code. Note we could use file_put_contents() but the following is more portable.
Init:
[code:1]
$pidfile = ""/path/to/demon/script/daemon.pid";
# Log our PID as the first thing, so other processes don't try to kill us.
echo "writing pidfile $pidfile";
$fp = fopen($pidfile, "w");
fwrite($fp, posix_getpid()."\n".time()."\n");
fclose($fp);
$touch_timer = 0; // Timer so we only touch the pidfile sometimes.
[/code:1]
Main loop:
[code:1]
if ($touch_timer + 15 < time()) { // So the revivifier can tell we've hung.
echo "Touching the file $pidfile to ".time()." - ".date("r")."\n";
$fp = fopen($pidfile, "w");
fwrite($fp, posix_getpid()."\n".time()."\n");
fclose($fp);
$touch_timer = time();
}
[/code:1]



Day in and day out:
What does the is_alive function do? It seems to be missing something. Like a shell script.
???
Re: Day in and day out:
[quote=batman]What does the is_alive function do? It seems to be missing something. Like a shell script.
???[/quote]
Eh, good call, I forgot to include is_up: I'd forgotten that that line had been taken out to a separate script. As explained in the comment, this is because in safe mode, PHP interprets that system call as a call to a ps with an argument something like: "-ef\|grep\ $1\ \|grep\ daemonize\|wc\ -l" - and that just doesn't make sense.
The code is:
[quote][code]#!/bin/sh
# Wrapper script because in safe mode PHP gloms all those together.
ps -ef | grep $1 | grep daemonize | wc -l[/code][/quote]
ps, grep and wc should be in everyone's path, so I left the paths unspecified. But for security, you should replace them with the paths to your specific binaries, so someone can't do Bad Stuff by messing with your path. In fact, for security, you should null out your path.
All the above could probably be done internally by PHP, but I'm lazy. The fact that it fires off four processes instead of one might have caused me to lose sleep ten years ago, but no longer.
Yet another geek.