
Advanced PHP Programming
.pdf
138 Chapter 5 Implementing with PHP: Standalone Scripts
$children[] = $pid;
}
}
while($children) {
sleep(10); // or perform parent logic
}
pcntl_alarm(0);
It is important to remember to set the alarm timeout to 0 when it is no longer needed; otherwise, it will fire when you do not expect it. Running the script with these modifications yields the following output:
> ./9.php Caught SIGCHLD
Collected exited pid 5011
Caught SIGCHLD
Collected exited pid 5013
Caught SIGALRM
Caught SIGCHLD
Collected killed pid 5014
Collected killed pid 5012
Collected killed pid 5010
In this example, the parent process uses the alarm to clean up (via termination) any child processes that have taken too long to execute.
Other Common Signals
Other common signals you might want to install handlers for are SIGHUP, SIGUSR1, and SIGUSR2.The default behavior for a process when receiving any of these signals is to terminate. SIGHUP is the signal sent at terminal disconnection (when the shell exits). A typical process in the background in your shell terminates when you log out of your terminal session.
If you simply want to ignore these signals, you can instruct a script to ignore them by using the following code:
pcntl_signal(SIGHUP, SIGIGN);
Rather than ignore these three signals, it is common practice to use them to send simple commands to processes—for instance, to reread a configuration file, reopen a logfile, or dump some status information.
Writing Daemons
A daemon is a process that runs in the background, which means that once it is started, it takes no input from the user’s terminal and does not exit when the user’s session ends.

Writing Daemons |
139 |
Once started, daemons traditionally run forever (or until stopped) to perform recurrent tasks or to handle tasks that might last beyond the length of the user’s session.The Apache Web server, sendmail, and the cron daemon crond are examples of common daemons that may be running on your system. Daemonizing scripts is useful for handling long jobs and recurrent back-end tasks.
To successfully be daemonized, a process needs to complete the two following tasks:
nProcess detachment
nProcess independence
In addition, a well-written daemon may optionally perform the following:
nSetting its working directory
nDropping privileges
nGuaranteeing exclusivity
You learned about process detachment earlier in this chapter, in the section “Creating and Managing Child Processes.”The logic is the same as for daemonizing processes, except that you want to end the parent process so that the only running process is detached from the shell.To do this, you execute pnctl_fork() and exit if you are in the parent process (that is, if the return value is greater than zero).
In Unix systems, processes are associated with process groups, so if you kill the leader of a process group, all its associates will terminate as well.The parent process for everything you start in your shell is your shell’s process.Thus, if you create a new process with fork() and do nothing else, the process will still exit when you close the shell.To avoid having this happen, you need the forked process to disassociate itself from its parent process.This is accomplished by calling pcntl_setsid(), which makes the calling process the leader of its own process group.
Finally, to sever any ties between the parent and the child, you need to fork the process a second time.This completes the detachment process. In code, this detachment process looks like this:
if(pcntl_fork()) {
exit;
}
pcntl_setsid(); if(pcntl_fork()) {
exit;
}
# process is now completely daemonized
It is important for the parent to exit after both calls to pcntl_fork(); otherwise, multiple processes will be executing the same code.

140 Chapter 5 Implementing with PHP: Standalone Scripts
Changing the Working Directory
When you’re writing a daemon, it is usually advisable to have it set its own working directory.That way, if you read from or write to any files via a relative path, they will be in the place you expect them to be. Always qualifying your paths is of course a good practice in and of itself, but so is defensive coding.The safest way to change your working directory is to use not only chdir(), but to use chroot() as well.
chroot() is available inside the PHP CLI and CGI versions and requires the program to be running as root. chroot() actually changes the root directory for the process to the specified directory.This makes it impossible to execute any files that do not lie within that directory. chroot() is often used by servers as a security device to ensure that it is impossible for malicious code to modify files outside a specific directory. Keep in mind that while chroot() prevents you from accessing any files outside your new directory, any currently open file resources can still be accessed. For example, the following code opens a logfile, calls chroot() to switch to a data directory, and can still successfully log to the open file resource:
<?php
$logfile = fopen(“/var/log/chroot.log”, “w”); chroot(“/Users/george”);
fputs($logfile, “Hello From Inside The Chroot\n”);
?>
If chroot() is not acceptable for an application, you can call chdir() to set the working directory.This is useful, for instance, if the code needs to load code that can be located anywhere on the system. Note that chdir() provides no security to prevent opening of unauthorized files—only symbolic protection against sloppy coding.
Giving Up Privileges
A classic security precaution when writing Unix daemons is having them drop all unneeded privileges. Like being able to access files outside where they need to be, possessing unneeded privileges is a recipe for trouble. In the event that the code (or PHP itself) has an exploitable flaw, you can minimize damage by ensuring that a daemon is running as a user with minimal rights to alter files on the system.
One way to approach this is to simply execute the daemon as the unprivileged user. This is usually inadequate if the program needs to initially open resources (logfiles, data files, sockets, and so on) that the unprivileged user does not have rights to.
If you are running as the root user, you can drop your privileges by using the posix_setuid() and posiz_setgid() functions. Here is an example that changes the running program’s privileges to those of the user nobody:
$pw= posix_getpwnam(‘nobody’); posix_setuid($pw[‘uid’]); posix_setgid($pw[‘gid’]);

Combining What You’ve Learned: Monitoring Services |
141 |
As with chroot(), any privileged resources that were open prior to dropping privileges remain open, but new ones cannot be created.
Guaranteeing Exclusivity
You often want to require that only one instance of a script can be running at any given time. For daemonizing scripts, this is especially important because running in the background makes it easy to accidentally invoke instances multiple times.
The standard technique for guaranteeing exclusivity is to have scripts lock a specific file (often a lockfile, used exclusively for that purpose) by using flock(). If the lock fails, the script should exit with an error. Here’s an example:
$fp = fopen(“/tmp/.lockfile”, “a”);
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) { fputs(STDERR, “Failed to acquire lock\n”); exit;
}
/* lock successful safe to perform work */
Locking mechanisms are discussed in greater depth in Chapter 10,“Data Component Caching.”
Combining What You’ve Learned: Monitoring
Services
In this section you bring together your skills to write a basic monitoring engine in PHP. Because you never know how your needs will change, you should make it as flexible as possible.
The logger should be able to support arbitrary service checks (for example, HTTP and FTP services) and be able to log events in arbitrary ways (via email, to a logfile, and so on).You, of course, want it to run as a daemon, so you should be able to request it to give its complete current state.
A service needs to implement the following abstract class:
abstract class ServiceCheck {
const FAILURE = 0; const SUCCESS = 1;
protected $timeout = 30; protected $next_attempt;
protected $current_status = ServiceCheck::SUCCESS; protected $previous_status = ServiceCheck::SUCCESS; protected $frequency = 30;
protected $description;
protected $consecutive_failures = 0;

142 Chapter 5 Implementing with PHP: Standalone Scripts
protected $status_time; protected $failure_time; protected $loggers = array();
abstract public function _ _construct($params);
public function _ _call($name, $args)
{
if(isset($this->$name)) { return $this->$name;
}
}
public function set_next_attempt()
{
$this->next_attempt = time() + $this->frequency;
}
public abstract function run();
public function post_run($status)
{
if($status !== $this->current_status) { $this->previous_status = $this->current_status;
}
if($status === self::FAILURE) {
if( $this->current_status === self::FAILURE ) { $this->consecutive_failures++;
}
else {
$this->failure_time = time();
}
}
else { $this->consecutive_failures = 0;
}
$this->status_time = time(); $this->current_status = $status; $this->log_service_event();
}
public function log_current_status()
{
foreach($this->loggers as $logger) { $logger->log_current_status($this);
}
}

Combining What You’ve Learned: Monitoring Services |
143 |
private function log_service_event()
{
foreach($this->loggers as $logger) { $logger->log_service_event($this);
}
}
public function register_logger(ServiceLogger $logger)
{
$this->loggers[] = $logger;
}
}
The _ _call() overload method provides read-only access to the parameters of a
ServiceCheck object:
n timeout—How long the check can hang before it is to be terminated by the engine.
nnext_attempt—When the next attempt to contact this server should be made.
ncurrent_status—The current state of the service: SUCCESS or FAILURE.
nprevious_status—The status before the current one.
nfrequency—How often the service should be checked.
ndescription—A description of the service.
nconsecutive_failures—The number of consecutive times the service check has
failed because it was last successful.
nstatus_time—The last time the service was checked.
nfailure_time—If the status is FAILED, the time that failure occurred.
The class also implements the observer pattern, allowing objects of type ServiceLogger to register themselves and then be called whenever log_current_status() or log_service_event() is called.
The critical function to implement is run(), which defines how the check should be run. It should return SUCCESS if the check succeeded and FAILURE if not.
The post_run() method is called after the service check defined in run() returns. It handles setting the status of the object and performing logging.
The ServiceLogger interface :specifies that a logging class need only implement two methods, log_service_event() and log_current_status(), which are called when a run() check returns and when a generic status request is made, respectively.
The interface is as follows:
interface ServiceLogger {
public function log_service_event(ServiceCheck $service);
public function log_current_status(ServiceCheck $service);
}

144 Chapter 5 Implementing with PHP: Standalone Scripts
Finally, you need to write the engine itself.The idea is similar to the ideas behind the simple programs in the “Writing Daemons” section earlier in this chapter:The server should fork off a new process to handle each check and use a SIGCHLD handler to check the return value of checks when they complete.The maximum number of checks that will be performed simultaneously should be configurable to prevent overutilization of system resources. All the services and logging will be defined in an XML file.
The following is the ServiceCheckRunner class that defines the engine:
class ServiceCheckRunner {
private $num_children; private $services = array(); private $children = array();
public function _ _construct($conf, $num_children)
{
$loggers = array(); $this->num_children = $num_children; $conf = simplexml_load_file($conf);
foreach($conf->loggers->logger as $logger) { $class = new Reflection_Class(“$logger->class”); if($class->isInstantiable()) {
$loggers[“$logger->id”] = $class->newInstance();
}
else {
fputs(STDERR, “{$logger->class} cannot be instantiated.\n”); exit;
}
}
foreach($conf->services->service as $service) { $class = new Reflection_Class(“$service->class”); if($class->isInstantiable()) {
$item = $class->newInstance($service->params); foreach($service->loggers->logger as $logger) { $item->register_logger($loggers[“$logger”]);
}
$this->services[] = $item;
}
else {
fputs(STDERR, “{$service->class} is not instantiable.\n”); exit;
}
}
}

Combining What You’ve Learned: Monitoring Services |
145 |
private function next_attempt_sort($a, $b)
{
if($a->next_attempt() == $b->next_attempt()) { return 0;
}
return ($a->next_attempt() < $b->next_attempt()) ? -1 : 1;
}
private function next()
{
usort($this->services, array($this,’next_attempt_sort’)); return $this->services[0];
}
public function loop()
{
declare(ticks=1);
pcntl_signal(SIGCHLD, array($this, “sig_child”)); pcntl_signal(SIGUSR1, array($this, “sig_usr1”)); while(1) {
$now = time();
if(count($this->children) < $this->num_children) { $service = $this->next();
if($now < $service->next_attempt()) { sleep(1);
continue;
}
$service->set_next_attempt(); if($pid = pcntl_fork()) {
$this->children[$pid] = $service;
}
else { pcntl_alarm($service->timeout()); exit($service->run());
}
}
}
}
public function log_current_status()
{
foreach($this->services as $service) { $service->log_current_status();
}
}

146 Chapter 5 Implementing with PHP: Standalone Scripts
private function sig_child($signal)
{
$status = ServiceCheck::FAILURE; pcntl_signal(SIGCHLD, array($this, “sig_child”)); while(($pid = pcntl_wait($status, WNOHANG)) > 0) {
$service = $this->children[$pid]; unset($this->children[$pid]); if(pcntl_wifexited($status) &&
pcntl_wexitstatus($status) == ServiceCheck::SUCCESS)
{
$status = ServiceCheck::SUCCESS;
}
$service->post_run($status);
}
}
private function sig_usr1($signal)
{
pcntl_signal(SIGUSR1, array($this, “sig_usr1”)); $this->log_current_status();
}
}
This is an elaborate class.The constructor reads in and parses an XML file, creating all the services to be monitored and the loggers to record them.You’ll learn more details on this in a moment.
The loop() method is the main method in the class. It sets the required signal handlers and checks whether a new child process can be created. If the next event (sorted by next_attempt timestamp) is okay to run now, a new process is forked off. Inside the child process, an alarm is set to keep the test from lasting longer than its timeout, and then the test defined by run() is executed.
There are also two signal handlers.The SIGCHLD handler sig_child() collects on the terminated child processes and executes their service’s post_run() method.The SIGUSR1 handler sig_usr1() simply calls the log_current_status() methods of all registered loggers, which can be used to get the current status of the entire system.
As it stands, of course, the monitoring architecture doesn’t do anything. First, you need a service to check.The following is a class that checks whether you get back a 200 Server OK response from an HTTP server:
class HTTP_ServiceCheck extends ServiceCheck
{
public $url;
public function _ _construct($params)
{
foreach($params as $k => $v) { $k = “$k”;

Combining What You’ve Learned: Monitoring Services |
147 |
$this->$k = “$v”;
}
}
public function run()
{
if(is_resource(@fopen($this->url, “r”))) { return ServiceCheck::SUCCESS;
}
else {
return ServiceCheck::FAILURE;
}
}
}
Compared to the framework you built earlier, this service is extremely simple—and that’s the point: the effort goes into building the framework, and the extensions are very simple.
Here is a sample ServiceLogger process that sends an email to an on-call person when a service goes down:
class EmailMe_ServiceLogger implements ServiceLogger { public function log_service_event(ServiceCheck $service)
{
if($service->current_status == ServiceCheck::FAILURE) { $message = “Problem with {$service->description()}\r\n”; mail(‘oncall@example.com’, ‘Service Event’, $message); if($service->consecutive_failures() > 5) {
mail(‘oncall_backup@example.com’, ‘Service Event’, $message);
}
}
}
public function log_current_status(ServiceCheck $service)
{
return;
}
}
If the failure persists beyond the fifth time, the process also sends a message to a backup address. It does not implement a meaningful log_current_status() method.
You implement a ServiceLogger process that writes to the PHP error log whenever a service changes status as follows:
class ErrorLog_ServiceLogger implements ServiceLogger {
public function log_service_event(ServiceCheck $service)
{