Daemonising a PHP cli script on a posix system

I’ve been researching how to best write a long running PHP script executed on the command-line, and whilst there are Linux commands you can use to daemonise a command, these can also be written into a php script as well.

The easiest way to daemonise a command on a posix system is to run:

nohup command < /dev/null > /dev/null 2>&1 &

This simply calls nohup to execute the command, which in effect disables the SIGHUP signal (used to tell a child its parent terminal is closing), and backgrounds the process. All standard file descriptors are set to /dev/null to stop the command from reading/outputting anything back to the terminal, and so that when the terminal is closed, reads and writes from stdin/stdout/stderr do not fail.

This is a bit clunky, but the same can be achieved in PHP either by default, or via a command-line argument.

Daemonise

First, to achieve backgrounding, you must fork the process as the current process wont be able to background. Forking will create a new process with the same memory as the parent, which will carry on on the same line the parent forked on. Then detatch it from the terminal using posix_setsid().

switch (pcntl_fork()) {
    case -1:
        die('unable to fork');
    case 0: // this is the child process
        break;
    default: // otherwise this is the parent process
        exit;
}
 
if (posix_setsid() === -1) {
     die('could not setsid');
}

Next, we will replace the duplicated file descriptors (stdin, stdout and stderr) with /dev/null. When a standard file descriptor is closed, it can be replaced with one opened by php. The variables are not important, just the ordering of the fopen calls.

fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
 
$stdIn = fopen('/dev/null', 'r'); // set fd/0
$stdOut = fopen('/dev/null', 'w'); // set fd/1
$stdErr = fopen('php://stdout', 'w'); // a hack to duplicate fd/1 to 2

You may want to switch stderr to a log file instead e.g. fopen(‘/path/to/errorlog’, ‘a’). PHP errors will output to this file as they happen.

Lastly SIGHUP, along with a few additional terminal signals, can be ignored by the process:

pcntl_signal(SIGTSTP, SIG_IGN);
pcntl_signal(SIGTTOU, SIG_IGN);
pcntl_signal(SIGTTIN, SIG_IGN);
pcntl_signal(SIGHUP, SIG_IGN);

So that gives us the basic daemonising effect. The terminal can now be closed, and the process will continue to run until terminated either internally or externally.

Single Instance

Now with most daemon programs you want to make sure that there can be only one instance, and that instance can be tracked for termination later. To do this we can use a locked file that contains the process id. If the file is locked for write, the daemon is running, otherwise it isn’t. Also the file can be read by other scripts to get the process id, which can be used to send signals to the process.

This you would do before any other code.

$lock = fopen('/path/to/pid', 'c+');
if (!flock($lock, LOCK_EX | LOCK_NB)) {
    die('already running');
}
 
switch ($pid = pcntl_fork()) {
    case -1:
        die('unable to fork');
    case 0: // this is the child process
        break;
    default: // otherwise this is the parent process
        fseek($lock, 0);
        ftruncate($lock, 0);
    	fwrite($lock, $pid);
        fflush($lock);
        exit;
}

The lock will prevent another script from continuing. Once the script terminates, the lock will automatically be released allowing the script to be called again.

The full code

The end result is as follows:

$lock = fopen('/path/to/pid', 'c+');
if (!flock($lock, LOCK_EX | LOCK_NB)) {
    die('already running');
}
 
switch ($pid = pcntl_fork()) {
    case -1:
        die('unable to fork');
    case 0: // this is the child process
        break;
    default: // otherwise this is the parent process
        fseek($lock, 0);
        ftruncate($lock, 0);
    	fwrite($lock, $pid);
        fflush($lock);
        exit;
}
 
if (posix_setsid() === -1) {
     die('could not setsid');
}
 
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
 
$stdIn = fopen('/dev/null', 'r'); // set fd/0
$stdOut = fopen('/dev/null', 'w'); // set fd/1
$stdErr = fopen('php://stdout', 'w'); // a hack to duplicate fd/1 to 2
 
pcntl_signal(SIGTSTP, SIG_IGN);
pcntl_signal(SIGTTOU, SIG_IGN);
pcntl_signal(SIGTTIN, SIG_IGN);
pcntl_signal(SIGHUP, SIG_IGN);
 
// do some long running work
sleep(300);

To see the replaced file descriptors have a look at the process’ proc entry.

ls -al /proc/`cat /path/to/pid`/fd
 
total 0
dr-x------ 2 root root  0 May 23 09:41 .
dr-xr-xr-x 5 root root  0 May 23 09:34 ..
lr-x------ 1 root root 64 May 23 09:41 0 -> /dev/null
l-wx------ 1 root root 64 May 23 09:41 1 -> /dev/null
l-wx------ 1 root root 64 May 23 09:41 2 -> /dev/null
lrwx------ 1 root root 64 May 23 09:41 3 -> /path/to/pid

Here is a script that will then kill this daemon:

$lock = fopen('/path/to/pid', 'c+');
if (flock($lock, LOCK_EX | LOCK_NB)) {
	die('process not running');
}
$pid = fgets($lock);
 
posix_kill($pid, SIGTERM);

How about Windows?

None of the versions of Windows support the posix api, however Microsoft have provided a service architecture in all versions of Windows since XP. There is a PECL module win32service, which can help you with dealing with setting up and controlling a PHP service. An example can be seen in the PHP.net documentation.