PHP is a language primarily used by web developers, but even these have problems which have to be scheduled to background processes, like sending newsletters, analysing stats, or simply maintaining the database. The most common way to solve this, is abusing crond for almost everything. When things are getting more complicated, like running jobs every 10 seconds people get really creative like blocking the execution or whatever silly custom.
I decided to do it a bit more the unix-way by writing a daemon process framework. It seems that only few people use PHP in such a way, so I will give you a full introduction here. I wrote several daemons in C already and tried to bring the C world to PHP the best way I could, to be able to use the high level API for all sorts of funny things.
The final daemon process consists of the following features:
- Dynamic load handling
- Process forking
- Custom process title
- PID file management
- Permission management
- Signal handling
- Custom php.ini config
You can either download the complete package on github, or you follow the following instructions:
Create an executable
Create a project folder, a libs folder, the php.ini and the actual daemon executable without contents
mkdir daemon
cd daemon
mkdir lib
touch daemon
touch phpd.ini
touch config.php
chmod 0777 daemon
The executable now needs a Shebang. Passing a custom php.ini is quite tricky and works only exactly the way I propose it here. I figured out how I can do this by reading the PHP source. So, open the daemon file and paste the following lines:
#!/usr/bin/php -c/etc/phpd.ini
<?php
require 'config.php';
After the process starts, it should change it's directory to the actual folder it runs on:
chdir(DAEMON_ROOT);
A little check, if the daemon is run only on cli can prevent strange behavior:
if (PHP_SAPI !== 'cli') {
exit;
}
Now the most important thing is coming, which makes it actually a daemon: The fork of the own process to detach it from the actual shell. There is a config option, which regulates the behavoir of the daemon, but more on this later. The fork is done using pcntl_fork of the pcntl extension, which must be installed. More on this also later on.
if (DAEMON_FORK && 0 !== pcntl_fork()) {
exit;
}
The way a fork() works is, that it clones the actuall process and the return code gives you information on what process you are. 0 means you are in the child, which now is detached from the shell, -1 means there was an error and >0 represents the PID of the child (means we are in the parent). In every case but the client, we can terminate the program and the daemon already runs as the child. An actual daemon however, does quite a bit more. Let's start with logging, that the process just started. We'll define this method and class later:
Main::log(E_NOTICE, DAEMON_NAME . ' started at ' . date(DATE_RFC822));
The next step is, that we define the signal handlers. If you don't know, you can communicate with a process using kill. Even if the unix command sounds a bit harsh, it sends either signals to a process or to the kernel. kill -9 123 will tell the kernel it must force exiting the process 123. However, kill -HUP 123 will give you information, that the user wants something from you. Our daemon will implement a logrotation, but you can do whatever you want. There are also custom signals USR1 and USR2 which can be used in whatever way you like. For now, we just set up things and define the methods later:
// Needed for signals
declare(ticks = 1);
Main::registerSignal();
So far so good. A problem is PHP's error management. We want error verbosity, but just in a log file. So we declare a custom error handler:
set_error_handler("Main::handleError");
If we actually run in daemon mode, we register the console (a feature I'll explain later) and the environment. As the environment registration involves closing stdout and stderror, we shut down the display of errors (which would terminate the process otherwise).
if (DAEMON_FORK) {
Main::registerEnv();
/* Reduce verbosity, we don't have STDOUT/STDERR open anymore*/
ini_set("display_errors", 0);
}
After this, we can go to work and remain there for forever. If the daemon decides to go down for a request of the user or if the job has been done, we do some cleanup and exit the child:
Main::loop();
/* Delete PID file after shutdown */
unlink(PID_FILE);
Main::log(E_NOTICE, DAEMON_NAME . ' shut down normally at ' . date(DATE_RFC822));
Configure the daemon
We used some constants and included the config.php. Let's define some configuration constants and the autoload function to handle class loading more easily:
<?php
// Process name and the root directory
define('DAEMON_NAME', 'daemon');
define('DAEMON_ROOT', '/www/daemon');
// User ID and Group ID of the process
define('DAEMON_UID', 1500);
define('DAEMON_GID', 1500);
// PID and log file paths
define('DAEMON_PID', '/var/run/' . DAEMON_NAME . '.pid');
define('DAEMON_LOG', ini_get('error_log'));
// Boolean to decide if the process needs to be forked, if daemon was not started with `cli`
define('DAEMON_FORK', empty($argv[1]) || 'cli' != $argv[1]);
// Load configuration
define('MAX_RESULT', 100);
define('MIN_SLEEP', 0);
define('MAX_SLEEP', 45);
function __autoload($class) {
require DAEMON_ROOT . '/lib/' . strtolower($class) . '.php';
}
On top of that, I added a custom php.ini, called phpd.ini:
error_reporting = E_WARNING | E_NOTICE | E_ALL | E_STRICT
log_errors = On
display_errors = On
error_log = /www/log/daemon.log
arg_separator.output = "&"
date.timezone = Europe/Berlin
expose_php = Off
mysqli.allow_persistent = 0
mysqli.reconnect = 1
mysqli.allow_local_infile = 0
magic_quotes_gpc = Off
magic_quotes_runtime = Off
magic_quotes_sybase = Off
memory_limit = 512M
ignore_user_abort = On
enable_dl = Off
Declaring the main class
I tried to make the framework quite minimalistic and so it needs only the Main class for actual working. The first method we used was Main::log():
public static function log($code, $msg, $var = null) {
static $codeMap = array(
E_ERROR => "Error",
E_WARNING => "Warning",
E_NOTICE => "Notice"
);
$msg = date('[d-M-Y H:i:s] ') . $codeMap[$code] . ': ' . $msg;
if (null !== $var) {
$msg.= "\n";
$msg.= var_export($var, true);
$msg.= "\n";
$msg.="\n";
}
file_put_contents(DAEMON_LOG, $msg . "\n", FILE_APPEND);
}
We've overtaken PHP's own logging mechanism and need to route it to the log function:
public static function handleError($errno, $errstr, $errfile, $errline, $errctx) {
if (error_reporting() == 0) {
return;
}
Main::log($errno, $errstr . " on line " . $errline . "(" . $errfile . ") -> " . var_export($errctx, true));
/* Don't execute PHP's internal error handler */
return true;
}
We use the pcntl extension again for the signal handling. You can add more, but I define handlers for sigterm (process has to die), sighup (log rotation), sigusr1 (attach the current terminal to the output stream of the process):
public static function registerSignal() {
pcntl_signal(SIGTERM, 'self::_handleSignal');
pcntl_signal(SIGHUP, 'self::_handleSignal');
pcntl_signal(SIGUSR1, 'self::_handleSignal');
}
And the handler itself:
private static function _handleSignal($signo) {
switch ($signo) {
/*
* Attention: The sigterm is only recognized outside a mysqlnd poll()
*/
case SIGTERM:
self::log(E_NOTICE, 'Received SIGTERM, dying...');
self::$run = false;
return;
case SIGHUP:
self::log(E_NOTICE, 'Received SIGHUP, rotate...');
Worker::rotate();
return;
case SIGUSR1:
if (null !== self::$screen) {
@fclose(self::$screen);
}
self::$screen = null;
if (preg_match('|pts/([0-9]+)|', `who`, $out) && !empty($out[1])) {
self::_openConsole('/dev/pts/' . $out[1]);
}
}
}
The used method to open the console is quite simple. The only tricky part is to find the correct opening modifier. Reading the source again, can help on that ("c" = O_WRITE (+O_CREAT), without O_TRUNC and O_APPEND):
private static function _openConsole($screen) {
if (!empty($screen) && false !== ($fd = fopen($screen, "c"))) {
self::$screen = $fd;
}
}
By registering the environment, I mean writing the PID file and changing the user and group of the process, closing the file descriptors and opening the current terminal. The latter is done using the posix extension:
public static function registerEnv() {
file_put_contents(DAEMON_PID, getmypid());
posix_setuid(DAEMON_UID);
posix_setgid(DAEMON_GID);
self::_openConsole(posix_ttyname(STDOUT));
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
}
That's almost it. The last remaining step is to actually loop until the running state has changed. The function uses the proctitle extension, which allows you to change the title of the process in top. That's neat to watch the process doing it's work.
public static function loop() {
// Initialize extern ressources
$db = mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME);
do {
echos("BEGINNING\n", "green");
$load = Worker::run($db);
if (DAEMON_FORK) {
$sleep = MAX_SLEEP + $load * (MIN_SLEEP - MAX_SLEEP);
setproctitle(NAME . ': ' . round(100 * $load, 1) . '%');
echos("Sleep for "); echos($sleep, "magenta"); echos(" seconds\n\n");
sleep($sleep);
} else if (0 == $load) {
break;
}
} while (self::$run);
// Close extern ressources
mysqli_close($db);
}
The worker class is a simple example class, which does an actual job. The github repo shows a simple example for this.
Last word on printing texts. As you've seen above, I always used a function echos. This function prints the given string in a certain color to the console, if someone is reading on the tty device, e.g. the daemon was triggered with kill -HUP.