Network Programming with Perl


 
Network Programming with Perl

By Lincoln  D.  Stein

Slots : 1

Table of Contents
Chapter  14.   Bulletproofing Servers

    Content

It is often necessary to reconfigure a server that is already running. Many UNIX daemons follow a convention in which the HUP signal is treated as a command to reinitialize or reset the server. For example, a server that depends on a configuration file might respond to the HUP signal by reparsing the file and reconfiguring itself. HUP was chosen because it is not normally received by a process that has detached itself from the controlling terminal.

As a last iteration of the psychotherapist server, we will rewrite it to respond to the HUP signal by terminating all its current connections, closing the listen socket, and then relaunching itself. We will also modify the TERM handler so that the server terminates all connections and exits. The effect of relaunching a server in this way is that the HUP signal initiates a clean start. Logging is suspended and restarted, memory is reinitialized, and if there were a configuration file, the server would reopen and parse it.

In addition to showing how to handle the HUP signal, this example also illustrates two other techniques:

  1. How to safely change interrupt handlers in a forked child

  2. How to exec () a program when taint mode is activated

Both the main script file and Daemon must be modified to handle the HUP signal properly.

Changes to the Main Script

Figure 14.6 gives the full source listing for eliza_hup.pl , which now contains the HUP -handling code in addition to the chroot, privilege-handling, taint mode, and logging code that we looked at earlier.

Figure 14.6. Psychotherapist server that responds to HUP signal

Lines 1 “12: Module initialization and constants We add the -T switch to the top line of the file, turning on Perl's taint mode. We define ELIZA_HOME and other constants.

Lines 13 “14: Install TERM and HUP handler We install the subroutines do_term() and do_hup() as the handlers for the TERM and HUP signals, respectively. We also install do_term() as the handler for INT .

Line 15: Fetch port from command line We modify this line slightly so that the port argument remains in @ARGV rather than being shifted out of it. This is so that the do_relaunch() routine (which we look at later) will continue to have access to the command-line arguments.

Lines 16 “43: Socket initialization, main loop, and connection handling The only change is in line 25, where instead of calling fork() directly, we call launch_child() , a new function defined in Daemon. This subroutine forks, calls chroot() , and abandons root privileges, as in previous versions of the script. In addition to these functions, launch_child() keeps track of the spawned child PIDs so that we can terminate them gracefully when the server receives a HUP or termination signal.

launch_child() takes two optional arguments: a callback routine to invoke when the child dies and a directory path to chroot() to. The first argument is a code reference. It is invoked by the Daemon module's CHLD handler after calling waitpid() to give our code a chance to do any additional code. We don't need this feature in this example, so we leave the first argument blank (we'll use it in Chapter 16, when we revisit Daemon). We do, however, want launch_child() to chroot() for us, so we provide ELIZA_HOME in the second argument.

Lines 44 “48: do_term() TERM handler The TERM handler logs a message to the system log and calls a new subroutine named kill_children() to terminate all active connections. This subroutine is defined in the revised Daemon module. After kill_children() returns, we exit the server.

Lines 49 “58: do_hup() HUP handler We close the listening socket, terminate active connections with kill_children() , and then call do_relaunch() , another new subroutine defined in the Daemon module. do_relaunch() will try to reexecute the script and won't return if it is successful. If it does return, we die with an error message.

Lines 59 “65: Patches to Chatbot::Eliza As we've done before, we redefine the Chatbot::Eliza::_testquit() subroutine in order to correct a bug in its end-of-file detection. We also define an empty Chatbot::Eliza::DESTROY() subroutine to quash an annoying warning that appears when running this script under some versions of Perl.

Lines 66 “68: Log normal termination We log a message when the server terminates, as in earlier versions.

Changes to the Daemon Module

Most of the interesting changes are in Daemon.pm , which defines a number of new subroutines and modifies some existing ones. The changes can be summarized as follows :

  1. Modify the forking and CHLD -handling routines in order to keep an up-to-date tally of the PIDs corresponding to each of the concurrent connections. We do this in the launch_child() subroutine by adding each child's PID to a global called %CHILDREN , and in the reap_child() signal handler by removing exited children from %CHILDREN .

  2. Modify the forking code so that child processes do not inherit the parent server's interrupt handlers. We discuss the rationale for this in more detail later.

  3. Maintain information about the current working directory so that the daemon can relaunch itself in the same environment in which it was started.

  4. Add the kill_children() function for terminating all active connections.

  5. Add the do_relaunch() function for relaunching the server after a HUP signal is received.

The most novel addition to Daemon.pm is code for blocking and restoring signals in the launch_child() subroutine. In previous versions of the server, we didn't worry much about the fact that the child process inherits the signal handlers of its parent, because the only signal handler installed was the innocuous CHLD handler. However, in the current incarnation of the server, the newly forked child also inherits the parent's HUP handler, which we definitely do not want the child to execute because it will lead to multiple unsuccessful attempts by each child to relaunch the server.

We would like to fork() and then immediately reset the child's HUP handler to "DEFAULT" in order to restore its default behavior. However, there is a slight but real risk that an incoming HUP signal will arrive in the vulnerable period after the child forks but before we have had a chance to reset $SIG{HUP} . The safest course is for the parent to temporarily block signals before forking and then for both child and parent to unblock them after the child's signal handlers have been reset. The sigprocmask () function, available from the POSIX module, makes this possible.

$result = sigprocmask($operation,$newsigset [,$oldsigset])

sigprocmask() manipulates the process's "signal mask", a bitmask that controls what signals the process will or will not receive. By default, processes receive all operating system signals, but you can block some or all signals by installing a new signal mask. The signals are not discarded, but are held waiting until the process unblocks signals.

The first argument to sigprocmask() is an operation to perform on the mask; the second argument is the set of signals to operate on. An optional third argument will receive a copy of the old process mask.

The sigprocmask() operation may be one of three constants:

  • SIG_BLOCK ”The signals indicated by the signal set are added to the process signal mask, blocking them.

  • SIG_UNBLOCK ”The signals indicated by the signal set are removed from the signal mask, unblocking them.

  • SIG_SETMASK ”The process signal mask is cleared completely and replaced with the signals indicated by the signal set.

Signal sets can be created and examined using a small utility class called POSIX::SigSet, which manipulates sets of signals in much the same way that IO::Select manipulates sets of filehandles. To create a new signal set, call POSIX::SigSet->new() with a list of signal constants. The constants are named SIGHUP, SIGTERM , and so forth:

$signals = POSIX::SigSet->new(SIGINT,SIGTERM,SIGHUP);

The $signals signal set can now be passed to sigprocmask() .

To temporarily block the INT, TERM, and HUP signals, we call sigprocmask() with an argument of SIG_BLOCK :

sigprocmask(SIG_BLOCK,$signals);

To unblock the signals, we use SIG_UNBLOCK:

sigprocmask(SIG_UNBLOCK,$signals);

sigprocmask() returns a true value if successful; otherwise , it returns false. See the POSIX POD pages for other set operations that one can perform with the POSIX::SigSet class.

Let's walk through the new Daemon module (Figure 14.7).

Figure 14.7. Daemon module with support for restarting the server

Lines 1 “21: Module setup The only change is the importation of a new set of POSIX functions designated the " :signal_h " group . These functions provide the facility for temporarily blocking signals that we will use in the launch_child() subroutine.

Lines 22 “33: init_server() subroutine This subroutine is identical to previous versions.

Lines 34 “47: become_daemon() subroutine This subroutine is identical to previous versions in all but one respect. Before calling chdir() to make the root directory our current working directory, we remember the current directory in the package global $CWD . This allows us to put things back the way they were before we relaunch the server.

Lines 48 “55: change_privileges() subroutine This is identical to previous versions.

Lines 56 “70: launch_child() subroutine The various operations of forking and initializing the child server processes are now consolidated into a launch_child() subroutine. This subroutine takes a single argument, a directory path which, if provided, is passed to prepare_child() for the chroot() call.

We begin by creating a new POSIX::SigSet containing the INT, CHLD, TERM , and HUP signals, and try to fork. On a fork error, we log a message. If the returned PID is greater than 0, we are in the parent process, so we add the child's PID to %CHILDREN . In the child process, we reset the four signal handlers to their default actions and call prepare_child() to set user privileges and change the root directory.

Before exiting, we unblock any signals that have been received during this period and return the child PID, if any, to the caller. This happens in both the parent and the child.

Lines 71 “79: prepare_child() subroutine This subroutine is identical to the previous versions, except that the chroot() functionality is now conditional on the function's being passed a directory path. In any case, the subroutine overwrites the real UID with the effective UID, abandoning any privileges the child process inherited from its parent.

Lines 80 “85: reap_child() subroutine This subroutine is the CHLD handler. We call waitpid() in a tight loop, retrieving the PIDs of exited children. Each process reaped in this way is deleted from the %CHILDREN global in order to maintain an accurate tally of the active connections.

Lines 86 “90: kill_children() subroutine We send a TERM signal to each of the PIDs of active children. We then enter a loop in which we sleep() until the %CHILDREN hash contains no more keys. The sleep() call is interrupted only when a signal is received, typically after an incoming CHLD . This is an efficient way for the parent to wait until all the child connections have terminated .

Lines 91 “99: do_relaunch() subroutine The job of do_relaunch() is to restore the environment to a state as similar to the way it was when the server was first launched as possible, and then to call exec() to replace the current process with a new instance of the server.

We begin by regaining root privileges by setting the effective UID to the real UID. We now want to restore the original working directory. However, we are running in taint mode, and the chdir() call is taint sensitive. So we pattern match on the working directory saved in $CWD and call chdir() on the extracted directory path.

Next we must set up the arguments to exec() . We get the server name from $0 and the port number argument from $ARGV[0] . However, these are also tainted and cannot be passed directly to exec() , so we must pattern match and extract them in a similar manner. When the new server starts up, it will complain if there is already a PID file present, so we unlink the file.

Finally, we invoke exec() with all the arguments needed to relaunch the server. The first argument is the name of the Perl interpreter, which exec() will search for in the (safe) PATH environment variable. The second is the -T command-line argument to turn on taint mode. The remaining arguments are the script name, which we extracted from $0 , and the port argument. If successful, exec() does not return. Otherwise, we die with an error message.

Lines 100 “142: Remainder of module The remainder of the module is identical to earlier versions.

The following is a transcript of the system log showing the entries generated when I ran the revised server, connected a few times from the local host, and then sent the server an HUP signal. After connecting twice more to confirm that the relaunched server was operating properly, I sent it a TERM signal to shut it down entirely.

Jun 13 05:54:57 pesto eliza_hup.pl[8776]: Server accepting connections on port 1002 Jun 13 05:55:51 pesto eliza_hup.pl[8808]: Accepting a connection from 127.0.0.1 Jun 13 05:56:01 pesto eliza_hup.pl[8810]: Accepting a connection from 127.0.0.1 Jun 13 05:56:08 pesto eliza_hup.pl[8776]: HUP signal received, reinitializing... Jun 13 05:56:08 pesto eliza_hup.pl[8776]: Closing listen socket... Jun 13 05:56:08 pesto eliza_hup.pl[8776]: Terminating children... Jun 13 05:56:08 pesto eliza_hup.pl[8776]: Trying to relaunch... Jun 13 05:56:10 pesto eliza_hup.pl[8811]: Server accepting connections on port 1002 Jun 13 05:56:14 pesto eliza_hup.pl[8815]: Accepting a connection from 127.0.0.1 Jun 13 05:56:19 pesto eliza_hup.pl[8815]: Connection from 127.0.0.1 finished Jun 13 05:56:26 pesto eliza_hup.pl[8817]: Accepting a connection from 127.0.0.1 Jun 13 05:56:28 pesto eliza_hup.pl[8811]: TERM signal received, terminating children... Jun 13 05:56:28 pesto eliza_hup.pl[8811]: Server exiting normally

You can easily extend this technique to other signals. For example, you could use USR1 as a message to activate verbose logging and USR2 to go back to normal logging.


   
Top

Категории