Browse Source

[master] Servers now create PID files

Merge branch 'trac3769'
Thomas Markwalder 10 years ago
parent
commit
cdce632add

+ 29 - 0
doc/guide/ddns.xml

@@ -128,6 +128,35 @@ strings <userinput>path</userinput>/kea-dhcp-ddns | sed -n 's/;;;; //p'
       Upon start up the module will load its configuration and begin listening
       for NCRs based on that configuration.
       </para>
+
+      <para>
+        During startup the server will attempt to create a PID file of the
+        form: [localstatedir]/[conf name].kea-dhcp-ddns.pid
+        where:
+        <itemizedlist>
+            <listitem>
+            <simpara><command>localstatedir</command>: The value as passed into the
+            build configure script. It defaults defaults to "/usr/local/var".  Note
+            that this value may be overridden at run time by setting the environment
+            variable KEA_PIDFILE_DIR.  This is intended primarily for testing purposes.
+            </simpara>
+            </listitem>
+            <listitem>
+            <simpara><command>conf name</command>: The confguration file name
+            used to start the server, minus all preceding path and file extension.
+            For example, given a pathname of "/usr/local/etc/kea/myconf.txt", the
+            portion used would be "myconf".
+            </simpara>
+            </listitem>
+        </itemizedlist>
+        If the file already exists and contains the PID of a live process,
+        the server will issue a DHCP_DDNS_ALREADY_RUNNING log message and exit. It
+        is possible, though unlikely, that the file is a remnant of a system crash
+        and the process to which the PID belongs is unrelated to Kea.  In such a 
+        case it would be necessary to manually delete the PID file.
+      </para>
+
+
     </section> <!-- end start-stop -->
     <section id="d2-configuration">
       <title>Configuring the DHCP-DDNS Server</title>

+ 27 - 0
doc/guide/dhcp4-srv.xml

@@ -106,6 +106,33 @@ strings <userinput>path</userinput>/kea-dhcp4 | sed -n 's/;;;; //p'
         access. Make sure you run this daemon as root.
       </para>
 
+      <para>
+        During startup the server will attempt to create a PID file of the
+        form: [localstatedir]/[conf name].kea-dhcp4.pid
+        where:
+        <itemizedlist>
+            <listitem>
+            <simpara><command>localstatedir</command>: The value as passed into the
+            build configure script. It defaults defaults to "/usr/local/var".  Note
+            that this value may be overridden at run time by setting the environment
+            variable KEA_PIDFILE_DIR.  This is intended primarily for testing purposes.
+            </simpara>
+            </listitem>
+            <listitem>
+            <simpara><command>conf name</command>: The confguration file name
+            used to start the server, minus all preceding path and file extension.
+            For example, given a pathname of "/usr/local/etc/kea/myconf.txt", the
+            portion used would be "myconf".
+            </simpara>
+            </listitem>
+        </itemizedlist>
+        If the file already exists and contains the PID of a live process,
+        the server will issue a DHCP4_ALREADY_RUNNING log message and exit. It
+        is possible, though unlikely, that the file is a remnant of a system crash
+        and the process to which the PID belongs is unrelated to Kea.  In such a 
+        case it would be necessary to manually delete the PID file.
+      </para>
+
     </section>
 
     <section id="dhcp4-configuration">

+ 26 - 0
doc/guide/dhcp6-srv.xml

@@ -104,6 +104,32 @@ strings <userinput>path</userinput>/kea-dhcp6 | sed -n 's/;;;; //p'
         access. Make sure you run this daemon as root.
       </para>
 
+      <para>
+        During startup the server will attempt to create a PID file of the
+        form: [localstatedir]/[conf name].kea-dhcp6.pid
+        where:
+        <itemizedlist>
+            <listitem>
+            <simpara><command>localstatedir</command>: The value as passed into the
+            build configure script. It defaults defaults to "/usr/local/var".  Note
+            that this value may be overridden at run time by setting the environment
+            variable KEA_PIDFILE_DIR.  This is intended primarily for testing purposes.
+            </simpara>
+            </listitem>
+            <listitem>
+            <simpara><command>conf name</command>: The confguration file name
+            used to start the server, minus all preceding path and file extension.
+            For example, given a pathname of "/usr/local/etc/kea/myconf.txt", the
+            portion used would be "myconf".
+            </simpara>
+            </listitem>
+        </itemizedlist>
+        If the file already exists and contains the PID of a live process,
+        the server will issue a DHCP6_ALREADY_RUNNING log message and exit. It
+        is possible, though unlikely, that the file is a remnant of a system crash
+        and the process to which the PID belongs is unrelated to Kea.  In such a 
+        case it would be necessary to manually delete the PID file.
+      </para>
     </section>
 
     <section id="dhcp6-configuration">

+ 19 - 0
src/bin/d2/d2_messages.mes

@@ -100,6 +100,16 @@ documented in preceding log entries.
 This is an informational message issued after DHCP_DDNS has submitted DNS
 mapping additions which were received and accepted by an appropriate DNS server.
 
+% DHCP_DDNS_ALREADY_RUNNING %1 already running? %2
+This is an error message that occurs when DHCP_DDNS encounters a pre-existing
+PID file which contains the PID of a running process.  This most likely
+indicates an attempt to start a second instance of DHCP_DDNS using the
+same configuration file.  It is possible, though unlikely, that the PID file
+is a remnant left behind by a server crash or power failure and the PID
+it contains refers to a process other than DHCP_DDNS.  In such an event,
+it would be necessary to manually remove the PID file.  The first argument is
+the DHCP_DDNS process name, the second contains the PID and PID file.
+
 % DHCP_DDNS_AT_MAX_TRANSACTIONS application has %1 queued requests but has reached maximum number of %2 concurrent transactions
 This is a debug message that indicates that the application has DHCP_DDNS
 requests in the queue but is working as many concurrent requests as allowed.
@@ -277,6 +287,15 @@ no configured DDNS domains in the DHCP_DDNS configuration.  Either the DHCP_DDNS
 configuration needs to be updated or the source of the FQDN itself should be
 investigated.
 
+% DHCP_DDNS_PID_FILE_ERROR %1 could not create a PID file: %2
+This is an error message that occurs when DHCP_DDNS is unable to create
+its PID file.  The log message should contain details sufficient to
+determine the underlying cause.  The most likely culprits are that
+some portion of the pathname does not exist or a permissions issue. The
+default path is determined by --localstatedir configure paramter but
+may be overridden by setting environment variable, KEA_PIDFILE_DIR.  The
+first argument is the DHCP_DDNS process name.
+
 % DHCP_DDNS_PROCESS_INIT application init invoked
 This is a debug message issued when the DHCP-DDNS application enters
 its initialization method.

+ 15 - 1
src/bin/d2/d_controller.cc

@@ -70,6 +70,8 @@ DControllerBase::launch(int argc, char* argv[], const bool test_mode) {
         throw; // rethrow it
     }
 
+    setProcName(bin_name_);
+
     // It is important that we set a default logger name because this name
     // will be used when the user doesn't provide the logging configuration
     // in the Kea configuration file.
@@ -87,6 +89,18 @@ DControllerBase::launch(int argc, char* argv[], const bool test_mode) {
         Daemon::loggerInit(bin_name_.c_str(), verbose_);
     }
 
+    try {
+        createPIDFile();
+    } catch (const dhcp::DaemonPIDExists& ex) {
+        LOG_FATAL(dctl_logger, DHCP_DDNS_ALREADY_RUNNING)
+                  .arg(bin_name_).arg(ex.what());
+        isc_throw (LaunchError, "Launch Failed: " << ex.what());
+    } catch (const std::exception& ex) {
+        LOG_FATAL(dctl_logger, DHCP_DDNS_PID_FILE_ERROR)
+                  .arg(app_name_).arg(ex.what());
+        isc_throw (LaunchError, "Launch failed: " << ex.what());
+    }
+
     // Log the starting of the service.  Although this is the controller
     // module, use a "DHCP_DDNS_" prefix to the module (to conform to the
     // principle of least astonishment).
@@ -173,7 +187,7 @@ DControllerBase::parseArgs(int argc, char* argv[])
                 isc_throw(InvalidUsage, "configuration file name missing");
             }
 
-            Daemon::init(optarg);
+            setConfigFile(optarg);
             break;
 
         case '?': {

+ 6 - 0
src/bin/d2/d_controller.h

@@ -50,6 +50,12 @@ public:
         isc::Exception(file, line, what) { };
 };
 
+/// @brief Exception thrown when the controller launch fails.
+class LaunchError: public isc::Exception {
+public:
+    LaunchError (const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
 
 /// @brief Exception thrown when the application process fails.
 class ProcessInitError: public isc::Exception {

+ 1 - 0
src/bin/d2/tests/Makefile.am

@@ -13,6 +13,7 @@ check-local:
 	for shtest in $(SHTESTS) ; do \
 	echo Running test: $$shtest ; \
 	export KEA_LOCKFILE_DIR=$(abs_top_builddir); \
+	export KEA_PIDFILE_DIR=$(abs_top_builddir); \
 	${SHELL} $(abs_builddir)/$$shtest || exit ; \
 	done
 

+ 2 - 1
src/bin/d2/tests/d2_process_tests.sh.in

@@ -1,4 +1,4 @@
-# Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2014-2015 Internet Systems Consortium, Inc. ("ISC")
 #
 # Permission to use, copy, modify, and/or distribute this software for any
 # purpose with or without fee is hereby granted, provided that the above
@@ -235,6 +235,7 @@ shutdown_test() {
     test_finish 0
 }
 
+server_pid_file_test "${CONFIG}" DHCP_DDNS_ALREADY_RUNNING
 dynamic_reconfiguration_test
 shutdown_test "dhcp-ddns.sigterm_test" 15
 shutdown_test "dhcp-ddns.sigint_test" 2

+ 3 - 0
src/bin/d2/tests/d2_unittests.cc

@@ -25,6 +25,9 @@ main(int argc, char* argv[]) {
     // src/lib/log/README for info on how to tweak logging
     isc::log::initLogger();
 
+    // Override --localstatedir value for PID files
+    setenv("KEA_PIDFILE_DIR", TEST_DATA_BUILDDIR, 1);
+
     int result = RUN_ALL_TESTS();
 
     return (result);

+ 1 - 1
src/bin/d2/tests/d_controller_unittests.cc

@@ -229,7 +229,7 @@ TEST_F(DStubControllerTest, missingConfigFileArgument) {
     int argc = 2;
 
     // Record start time, and invoke launch().
-    EXPECT_THROW(launch(argc, argv), ProcessInitError);
+    EXPECT_THROW(launch(argc, argv), LaunchError);
 }
 
 /// @brief Tests launch with an operational error during application execution.

+ 11 - 0
src/bin/dhcp4/dhcp4_messages.mes

@@ -19,6 +19,17 @@ This message is printed when DHCPv4 server enabled an interface to be used
 to receive DHCPv4 traffic. IPv4 socket on this interface will be opened once
 Interface Manager starts up procedure of opening sockets.
 
+% DHCP4_ALREADY_RUNNING %1 already running? %2
+This is an error message that occurs when the DHCPv4 server encounters
+a pre-existing PID file which contains the PID of a running process.
+This most likely indicates an attempt to start a second instance of
+the server using the same configuration file.  It is possible, though
+unlikely that the PID file is a remnant left behind by a server crash or
+power failure and the PID it contains refers to a process other than
+the server.  In such an event, it would be necessary to manually remove
+the PID file.  The first argument is the DHCPv4 process name, the
+second contains the PID and PID file.
+
 % DHCP4_BUFFER_RECEIVED received buffer from %1:%2 to %3:%4 over interface %5
 This debug message is logged when the server has received a packet
 over the socket. When the message is logged the contents of the received

+ 0 - 3
src/bin/dhcp4/kea_controller.cc

@@ -168,9 +168,6 @@ namespace dhcp {
 
 void
 ControlledDhcpv4Srv::init(const std::string& file_name) {
-    // Call parent class's init to initialize file name.
-    Daemon::init(file_name);
-
     // Configure the server using JSON file.
     configure(file_name);
 

+ 17 - 2
src/bin/dhcp4/main.cc

@@ -118,6 +118,7 @@ main(int argc, char* argv[]) {
         usage();
     }
 
+
     // Configuration file is required.
     if (config_file.empty()) {
         cerr << "Configuration file not specified." << endl;
@@ -125,7 +126,6 @@ main(int argc, char* argv[]) {
     }
 
     int ret = EXIT_SUCCESS;
-
     try {
         // It is important that we set a default logger name because this name
         // will be used when the user doesn't provide the logging configuration
@@ -145,6 +145,11 @@ main(int argc, char* argv[]) {
         // Remember verbose-mode
         server.setVerbose(verbose_mode);
 
+        // Create our PID file.
+        server.setProcName(DHCP4_NAME);
+        server.setConfigFile(config_file);
+        server.createPIDFile();
+
         try {
             // Initialize the server.
             server.init(config_file);
@@ -173,8 +178,18 @@ main(int argc, char* argv[]) {
 
         LOG_INFO(dhcp4_logger, DHCP4_SHUTDOWN);
 
-    } catch (const std::exception& ex) {
+    } catch (const isc::dhcp::DaemonPIDExists& ex) {
+        // First, we print the error on stderr (that should always work)
+        cerr << DHCP4_NAME << " already running? " << ex.what()
+             << endl;
 
+        // Let's also try to log it using logging system, but we're not
+        // sure if it's usable (the exception may have been thrown from
+        // the logger subsystem)
+        LOG_FATAL(dhcp4_logger, DHCP4_ALREADY_RUNNING)
+                  .arg(DHCP4_NAME).arg(ex.what());
+        ret = EXIT_FAILURE;
+    } catch (const std::exception& ex) {
         // First, we print the error on stderr (that should always work)
         cerr << DHCP4_NAME << ": Fatal error during start up: " << ex.what()
              << endl;

+ 1 - 0
src/bin/dhcp4/tests/Makefile.am

@@ -12,6 +12,7 @@ check-local:
 	for shtest in $(SHTESTS) ; do \
 	echo Running test: $$shtest ; \
 	export KEA_LOCKFILE_DIR=$(abs_top_builddir); \
+	export KEA_PIDFILE_DIR=$(abs_top_builddir); \
 	${SHELL} $(abs_builddir)/$$shtest || exit ; \
 	done
 

+ 1 - 0
src/bin/dhcp4/tests/dhcp4_process_tests.sh.in

@@ -272,6 +272,7 @@ shutdown_test() {
     test_finish 0
 }
 
+server_pid_file_test "${CONFIG}" DHCP4_ALREADY_RUNNING
 dynamic_reconfiguration_test
 shutdown_test "dhcpv4.sigterm_test" 15
 shutdown_test "dhcpv4.sigint_test" 2

+ 1 - 0
src/bin/dhcp4/tests/dhcp4_unittests.cc

@@ -25,6 +25,7 @@ main(int argc, char* argv[]) {
     // src/lib/log/README for info on how to tweak logging
     isc::log::initLogger();
 
+    setenv("KEA_PIDFILE_DIR", TEST_DATA_BUILDDIR, 1);
     int result = RUN_ALL_TESTS();
 
     return (result);

+ 11 - 0
src/bin/dhcp6/dhcp6_messages.mes

@@ -19,6 +19,17 @@ This message is printed when DHCPv6 server enabled an interface to be used
 to receive DHCPv6 traffic. IPv6 socket on this interface will be opened once
 Interface Manager starts up procedure of opening sockets.
 
+% DHCP6_ALREADY_RUNNING %1 already running? %2
+This is an error message that occurs when the DHCPv6 server encounters
+a pre-existing PID file which contains the PID of a running process.
+This most likely indicates an attempt to start a second instance of
+the server using the same configuration file.  It is possible, though
+unlikely that the PID file is a remnant left behind by a server crash or
+power failure and the PID it contains refers to a process other than
+the server.  In such an event, it would be necessary to manually remove
+the PID file.  The first argument is the DHCPv6 process name, the second
+contains the PID and PID file.
+
 % DHCP6_ADD_GLOBAL_STATUS_CODE %1: adding Status Code to DHCPv6 packet: %2
 This message is logged when the server is adding the top-level
 Status Code option. The first argument includes the client and the

+ 0 - 3
src/bin/dhcp6/kea_controller.cc

@@ -174,9 +174,6 @@ namespace dhcp {
 
 void
 ControlledDhcpv6Srv::init(const std::string& file_name) {
-    // Call parent class's init to initialize file name.
-    Daemon::init(file_name);
-
     // Configure the server using JSON file.
     configure(file_name);
 

+ 16 - 0
src/bin/dhcp6/main.cc

@@ -148,6 +148,11 @@ main(int argc, char* argv[]) {
         // Remember verbose-mode
         server.setVerbose(verbose_mode);
 
+        // Create our PID file
+        server.setProcName(DHCP6_NAME);
+        server.setConfigFile(config_file);
+        server.createPIDFile();
+
         try {
             // Initialize the server, e.g. establish control session
             // Read a configuration file
@@ -177,6 +182,17 @@ main(int argc, char* argv[]) {
 
         LOG_INFO(dhcp6_logger, DHCP6_SHUTDOWN);
 
+    } catch (const isc::dhcp::DaemonPIDExists& ex) {
+        // First, we print the error on stderr (that should always work)
+        cerr << DHCP6_NAME << " already running? " << ex.what()
+             << endl;
+
+        // Let's also try to log it using logging system, but we're not
+        // sure if it's usable (the exception may have been thrown from
+        // the logger subsystem)
+        LOG_FATAL(dhcp6_logger, DHCP6_ALREADY_RUNNING)
+                  .arg(DHCP6_NAME).arg(ex.what());
+        ret = EXIT_FAILURE;
     } catch (const std::exception& ex) {
 
         // First, we print the error on stderr (that should always work)

+ 1 - 0
src/bin/dhcp6/tests/Makefile.am

@@ -12,6 +12,7 @@ check-local:
 	for shtest in $(SHTESTS) ; do \
 	echo Running test: $$shtest ; \
 	export KEA_LOCKFILE_DIR=$(abs_top_builddir); \
+	export KEA_PIDFILE_DIR=$(abs_top_builddir); \
 	${SHELL} $(abs_builddir)/$$shtest || exit ; \
 	done
 

+ 1 - 0
src/bin/dhcp6/tests/dhcp6_process_tests.sh.in

@@ -273,6 +273,7 @@ returned %d."
     test_finish 0
 }
 
+server_pid_file_test "${CONFIG}" DHCP6_ALREADY_RUNNING
 dynamic_reconfiguration_test
 shutdown_test "dhcpv6.sigterm_test" 15
 shutdown_test "dhcpv6.sigint_test" 2

+ 1 - 0
src/bin/dhcp6/tests/dhcp6_unittests.cc

@@ -22,6 +22,7 @@ main(int argc, char* argv[]) {
     ::testing::InitGoogleTest(&argc, argv);
     isc::log::initLogger();
 
+    setenv("KEA_PIDFILE_DIR", TEST_DATA_BUILDDIR, 1);
     int result = RUN_ALL_TESTS();
 
     return result;

+ 1 - 0
src/bin/keactrl/tests/Makefile.am

@@ -16,6 +16,7 @@ check-local:
 	chmod +x $(abs_builddir)/$$shtest ; \
 	export KEA_LOCKFILE_DIR=$(abs_top_builddir); \
 	export KEACTRL_BUILD_DIR=$(abs_top_builddir); \
+	export KEA_PIDFILE_DIR=$(abs_top_builddir); \
 	export KEACTRL_CONF=$(abs_top_builddir)/src/bin/keactrl/tests/keactrl_test.conf; \
 	${SHELL} $(abs_builddir)/$$shtest || exit ; \
 	done

+ 128 - 11
src/lib/dhcpsrv/daemon.cc

@@ -13,14 +13,18 @@
 // PERFORMANCE OF THIS SOFTWARE.
 
 #include <config.h>
+#include <cc/data.h>
 #include <dhcpsrv/cfgmgr.h>
 #include <dhcpsrv/daemon.h>
 #include <exceptions/exceptions.h>
-#include <cc/data.h>
-#include <boost/bind.hpp>
-#include <logging.h>
 #include <log/logger_name.h>
 #include <log/logger_support.h>
+#include <logging.h>
+#include <util/filename.h>
+
+#include <boost/bind.hpp>
+
+#include <sstream>
 #include <errno.h>
 
 /// @brief provides default implementation for basic daemon operations
@@ -30,18 +34,22 @@
 namespace isc {
 namespace dhcp {
 
-// This is an initial config file location.
-std::string Daemon::config_file_ = "";
-
 Daemon::Daemon()
-    : signal_set_(), signal_handler_() {
+    : signal_set_(), signal_handler_(), config_file_(""), proc_name_(""),
+    pid_file_dir_(DHCP_DATA_DIR), pid_file_(), am_file_author_(false) {
+
+    // The pid_file_dir can be overridden via environment variable
+    // This is primarily intended to simplify testing
+    const char* const env = getenv("KEA_PIDFILE_DIR");
+    if (env) {
+        pid_file_dir_ = env;
+    }
 }
 
 Daemon::~Daemon() {
-}
-
-void Daemon::init(const std::string& config_file) {
-    config_file_ = config_file;
+    if (pid_file_ && am_file_author_) {
+        pid_file_->deleteFile();
+    }
 }
 
 void Daemon::cleanup() {
@@ -96,5 +104,114 @@ std::string Daemon::getVersion(bool /*extended*/) {
     isc_throw(isc::NotImplemented, "Daemon::getVersion() called");
 }
 
+std::string
+Daemon::getConfigFile() const {
+    return (config_file_);
+}
+
+void
+Daemon::setConfigFile(const std::string& config_file) {
+    config_file_ = config_file;
+}
+
+std::string
+Daemon::getProcName() const {
+    return (proc_name_);
+};
+
+void
+Daemon::setProcName(const std::string& proc_name) {
+    proc_name_ = proc_name;
+}
+
+std::string
+Daemon::getPIDFileDir() const {
+    return(pid_file_dir_);
+}
+
+void
+Daemon::setPIDFileDir(const std::string& pid_file_dir) {
+    pid_file_dir_ = pid_file_dir;
+}
+
+std::string
+Daemon::getPIDFileName() const {
+    if (pid_file_) {
+        return (pid_file_->getFilename());
+    }
+
+    return ("");
+};
+
+void
+Daemon::setPIDFileName(const std::string& pid_file_name) {
+    if (pid_file_) {
+        isc_throw(isc::InvalidOperation, "Daemon::setConfigFile"
+                  " file name already set to:" << pid_file_->getFilename());
+    }
+
+    if (pid_file_name.empty()) {
+        isc_throw(isc::BadValue, "Daemon::setPIDFileName"
+                  " file name may not be empty");
+    }
+
+    pid_file_.reset(new util::PIDFile(pid_file_name));
+};
+
+std::string
+Daemon::makePIDFileName() const {
+    if (config_file_.empty()) {
+        isc_throw(isc::InvalidOperation,
+                  "Daemon::makePIDFileName config file name is not set");
+    }
+
+    if (proc_name_.empty()) {
+        isc_throw(isc::InvalidOperation,
+                  "Daemon::makePIDFileName process name is not set");
+    }
+
+    // Create Filename instance from the config_file_ pathname, so we can
+    // extract the fname component.
+    isc::util::Filename file(config_file_);
+    if (file.name().empty()) {
+        isc_throw(isc::BadValue, "Daemon::makePIDFileName config file:"
+                  << config_file_ << " is missing file name");
+    }
+
+    // Make the pathname for the PID file from the runtime directory,
+    // configuration name and process name.
+    std::ostringstream stream;
+    stream  << pid_file_dir_ << "/" << file.name()
+            << "." << proc_name_ << ".pid";
+
+    return(stream.str());
+};
+
+void
+Daemon::createPIDFile(int pid) {
+    // If pid_file_ hasn't been instantiated explicitly, then do so
+    // using the default name.
+    if (!pid_file_) {
+        setPIDFileName(makePIDFileName());
+    }
+
+    // If we find a pre-existing file containing a live PID we bail.
+    int chk_pid = pid_file_->check();
+    if (chk_pid > 0) {
+        isc_throw(DaemonPIDExists, "Daemon::createPIDFile: PID: " << chk_pid
+                 << " exists, PID file: " << getPIDFileName());
+    }
+
+    if (pid == 0) {
+        // Write the PID of the current process
+        pid_file_->write();
+    } else {
+        // Write the PID we were given
+        pid_file_->write(pid);
+    }
+
+    am_file_author_ = true;
+}
+
 };
 };

+ 78 - 36
src/lib/dhcpsrv/daemon.h

@@ -17,6 +17,7 @@
 
 #include <cc/data.h>
 #include <dhcpsrv/srv_config.h>
+#include <util/pid_file.h>
 #include <util/signal_set.h>
 #include <boost/noncopyable.hpp>
 #include <string>
@@ -24,6 +25,13 @@
 namespace isc {
 namespace dhcp {
 
+/// @brief Exception thrown when a the PID file points to a live PID
+class DaemonPIDExists : public Exception {
+public:
+    DaemonPIDExists(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) { };
+};
+
 /// @brief Base class for all services
 ///
 /// This is the base class that all daemons (DHCPv4, DHCPv6, D2 and possibly
@@ -38,16 +46,6 @@ namespace dhcp {
 /// Dhcpv6Srv) in tests, without going through the hassles of implemeting stub
 /// methods.
 ///
-/// This class comprises a static object holding a location of the configuration
-/// file. The object must be static because it is instantiated by the signal
-/// handler functions, which are static by their nature. The signal handlers
-/// are used to reconfigure a running server and they need access to the
-/// configuration file location. They get this access by calling
-/// @c Daemon::getConfigFile function.
-///
-/// By default, the configuration file location is empty and its actual value
-/// is assigned to the static object in @c Daemon::init function.
-///
 /// Classes derived from @c Daemon may install custom signal handlers using
 /// @c isc::util::SignalSet class. This base class provides a declaration
 /// of the @c SignalSet object that should be initialized in the derived
@@ -71,25 +69,6 @@ public:
     /// virtual destructor as well.
     virtual ~Daemon();
 
-    /// @brief Initializes the server.
-    ///
-    /// Depending on the configuration backend, it establishes msgq session,
-    /// or reads the configuration file.
-    ///
-    /// Note: This function may throw to report enountered problems. It may
-    /// also return false if the initialization was skipped. That may seem
-    /// redundant, but the idea here is that in some cases the configuration
-    /// was read, understood and the decision was made to not start. One
-    /// case where such capability could be needed is when we have a single
-    /// config file for Kea4 and D2, but the DNS Update is disabled. It is
-    /// likely that the D2 will be started, it will analyze its config file,
-    /// decide that it is not needed and will shut down.
-    ///
-    /// @note this method may throw
-    ///
-    /// @param config_file Config file name (may be empty if unused).
-    virtual void init(const std::string& config_file);
-
     /// @brief Performs final deconfiguration.
     ///
     /// Performs configuration backend specific final clean-up. This is called
@@ -103,11 +82,6 @@ public:
     /// @brief Initiates shutdown procedure for the whole DHCPv6 server.
     virtual void shutdown();
 
-    /// @brief Returns config file name.
-    static std::string getConfigFile() {
-        return (config_file_);
-    }
-
     /// @brief Initializes logger
     ///
     /// This method initializes logging system. It also sets the default
@@ -161,6 +135,60 @@ public:
     /// @return text string
     static std::string getVersion(bool extended);
 
+    /// @brief Returns config file name.
+    /// @return text string
+    std::string getConfigFile() const;
+
+    /// @brief Sets the configuration file name
+    ///
+    /// @param config_file pathname of the configuration file
+    void setConfigFile(const std::string& config_file);
+
+    /// @brief returns the process name
+    /// This value is used as when forming the default PID file name
+    /// @return text string
+    std::string getProcName() const;
+
+    /// @brief Sets the process name
+    /// @param proc_name name the process by which the process is recognized
+    void setProcName(const std::string& proc_name);
+
+    /// @brief Returns the directory used when forming default PID file name
+    /// @return text string
+    std::string getPIDFileDir() const;
+
+    /// @brief Sets the PID file directory
+    /// @param pid_file_dir path into which the PID file should be written
+    /// Note the value should not include a trailing slash, '/'
+    void setPIDFileDir(const std::string& pid_file_dir);
+
+    /// @brief Returns the current PID file name
+    /// @return text string
+    std::string getPIDFileName() const;
+
+    /// @brief Sets PID file name
+    ///
+    /// If this method is called prior to calling createPIDFile,
+    /// the value passed in will be treated as the full file name
+    /// for the PID file.  This provides a means to override the
+    /// default file name with an explicit value.
+    ///
+    /// @param pid_file_name file name to be used as the PID file
+    void setPIDFileName(const std::string& pid_file_name);
+
+    /// @brief Creates the PID file
+    ///
+    /// If the PID file name has not been previously set, the method
+    /// uses manufacturePIDFileName() to set it.  If the PID file
+    /// name refers to an existing file whose contents are a PID whose
+    /// process is still alive, the method will throw a DaemonPIDExists
+    /// exception.  Otherwise, the file created (or truncated) and
+    /// the given pid (if not zero) is written to the file.
+    ///
+    /// @param pid PID to write to the file if not zero, otherwise the
+    /// PID of the current process is used.
+    void createPIDFile(int pid = 0);
+
 protected:
 
     /// @brief Invokes handler for the next received signal.
@@ -189,11 +217,25 @@ protected:
     /// it not initialized, the signals will not be handled.
     isc::util::SignalHandler signal_handler_;
 
-private:
+    /// @brief Manufacture the pid file name
+    std::string makePIDFileName() const;
 
+private:
     /// @brief Config file name or empty if config file not used.
-    static std::string config_file_;
+    std::string config_file_;
+
+    /// @brief Name of this process, used when creating its pid file
+    std::string proc_name_;
+
+    /// @brief Pointer to the directory where PID file(s) are written
+    /// It defaults to --localstatedir
+    std::string pid_file_dir_;
+
+    /// @brief Pointer to the PID file for this process
+    isc::util::PIDFilePtr pid_file_;
 
+    /// @brief Flag indicating if this instance created the file
+    bool am_file_author_;
 };
 
 }; // end of isc::dhcp namespace

+ 168 - 12
src/lib/dhcpsrv/tests/daemon_unittest.cc

@@ -34,6 +34,8 @@ namespace dhcp {
 class DaemonImpl : public Daemon {
 public:
     static std::string getVersion(bool extended);
+
+    using Daemon::makePIDFileName;
 };
 
 std::string DaemonImpl::getVersion(bool extended) {
@@ -75,6 +77,172 @@ TEST_F(DaemonTest, constructor) {
     // Check that the verbose mode is not set by default.
     Daemon instance2;
     EXPECT_FALSE(instance2.getVerbose());
+
+    EXPECT_TRUE(instance2.getConfigFile().empty());
+    EXPECT_TRUE(instance2.getProcName().empty());
+    EXPECT_EQ(CfgMgr::instance().getDataDir(),instance2.getPIDFileDir());
+    EXPECT_TRUE(instance2.getPIDFileName().empty());
+}
+
+// Verify config file accessors
+TEST_F(DaemonTest, getSetConfigFile) {
+    Daemon instance;
+
+    EXPECT_NO_THROW(instance.setConfigFile("test.txt"));
+    EXPECT_EQ("test.txt", instance.getConfigFile());
+}
+
+// Verify process name accessors
+TEST_F(DaemonTest, getSetProcName) {
+    Daemon instance;
+
+    EXPECT_NO_THROW(instance.setProcName("myproc"));
+    EXPECT_EQ("myproc", instance.getProcName());
+}
+
+// Verify PID file directory name accessors
+TEST_F(DaemonTest, getSetPIDFileDir) {
+    Daemon instance;
+
+    EXPECT_NO_THROW(instance.setPIDFileDir("/tmp"));
+    EXPECT_EQ("/tmp", instance.getPIDFileDir());
+}
+
+// Verify PID file name accessors.
+TEST_F(DaemonTest, setPIDFileName) {
+    Daemon instance;
+
+    // Verify that PID file name may not be set to empty
+    EXPECT_THROW(instance.setPIDFileName(""), BadValue);
+
+    EXPECT_NO_THROW(instance.setPIDFileName("myproc"));
+    EXPECT_EQ("myproc", instance.getPIDFileName());
+
+    // Verify that setPIDFileName cannot be called twice on the same instance.
+    EXPECT_THROW(instance.setPIDFileName("again"), InvalidOperation);
+}
+
+// Test the getVersion() redefinition
+TEST_F(DaemonTest, getVersion) {
+    EXPECT_THROW(Daemon::getVersion(false), NotImplemented);
+
+    ASSERT_NO_THROW(DaemonImpl::getVersion(false));
+
+    EXPECT_EQ(DaemonImpl::getVersion(false), "BASIC");
+
+    ASSERT_NO_THROW(DaemonImpl::getVersion(true));
+
+    EXPECT_EQ(DaemonImpl::getVersion(true), "EXTENDED");
+}
+
+// Verify makePIDFileName method
+TEST_F(DaemonTest, makePIDFileName) {
+    DaemonImpl instance;
+
+    // Verify that config file cannot be blank
+    instance.setProcName("notblank");
+    EXPECT_THROW(instance.makePIDFileName(), InvalidOperation);
+
+    // Verify that proc name cannot be blank
+    instance.setProcName("");
+    instance.setConfigFile("notblank");
+    EXPECT_THROW(instance.makePIDFileName(), InvalidOperation);
+
+    // Verify that config file must contain a file name
+    instance.setProcName("myproc");
+    instance.setConfigFile(".txt");
+    EXPECT_THROW(instance.makePIDFileName(), BadValue);
+    instance.setConfigFile("/tmp/");
+    EXPECT_THROW(instance.makePIDFileName(), BadValue);
+
+    // Given a valid config file name and proc name we should good to go
+    instance.setConfigFile("/tmp/test.conf");
+    std::string name;
+    EXPECT_NO_THROW(name = instance.makePIDFileName());
+
+    // Make sure the name is as we expect
+    std::ostringstream stream;
+    stream  << CfgMgr::instance().getDataDir() << "/test.myproc.pid";
+    EXPECT_EQ(stream.str(), name);
+
+    // Verify that the default directory can be overridden
+    instance.setPIDFileDir("/tmp");
+    EXPECT_NO_THROW(name = instance.makePIDFileName());
+    EXPECT_EQ("/tmp/test.myproc.pid", name);
+}
+
+// Verifies the creation a PID file and that a pre-existing PID file
+// which points to a live PID causes a throw.
+TEST_F(DaemonTest, createPIDFile) {
+    DaemonImpl instance;
+
+    instance.setConfigFile("test.conf");
+    instance.setProcName("daemon_test");
+    instance.setPIDFileDir(TEST_DATA_BUILDDIR);
+
+    EXPECT_NO_THROW(instance.createPIDFile());
+
+    std::ostringstream stream;
+    stream  << TEST_DATA_BUILDDIR << "/test.daemon_test.pid";
+    EXPECT_EQ(stream.str(), instance.getPIDFileName());
+
+    // If we try again, we should see our own PID file and fail
+    EXPECT_THROW(instance.createPIDFile(), DaemonPIDExists);
+}
+
+// Verifies that a pre-existing PID file which points to a dead PID
+// is overwritten.
+TEST_F(DaemonTest, createPIDFileOverwrite) {
+    DaemonImpl instance;
+
+    // We're going to use fork to generate a PID we KNOW is dead.
+    int pid = fork();
+    ASSERT_GE(pid, 0);
+
+    if (pid == 0) {
+        // This is the child, die right away. Tragic, no?
+        exit (0);
+    }
+
+    // Back in the parent test, we need to wait for the child to die
+    int stat;
+    int ret = waitpid(pid, &stat, 0);
+    ASSERT_EQ(ret, pid);
+
+    // Ok, so we should now have a PID that we know to be dead.
+    // Let's use it to create a PID file.
+    instance.setConfigFile("test.conf");
+    instance.setProcName("daemon_test");
+    instance.setPIDFileDir(TEST_DATA_BUILDDIR);
+    EXPECT_NO_THROW(instance.createPIDFile(pid));
+
+    // If we try to create the PID file again, this should work.
+    EXPECT_NO_THROW(instance.createPIDFile());
+}
+
+// Verifies that Daemon destruction deletes the PID file
+TEST_F(DaemonTest, PIDFileCleanup) {
+    boost::shared_ptr<DaemonImpl> instance;
+    instance.reset(new DaemonImpl);
+
+    instance->setConfigFile("test.conf");
+    instance->setProcName("daemon_test");
+    instance->setPIDFileDir(TEST_DATA_BUILDDIR);
+    EXPECT_NO_THROW(instance->createPIDFile());
+
+    // If we try again, we should see our own PID file
+    EXPECT_THROW(instance->createPIDFile(), DaemonPIDExists);
+
+    // Save the pid file name
+    std::string pid_file_name = instance->getPIDFileName();
+
+    // Now delete the Daemon instance.  This should remove the
+    // PID file.
+    instance.reset();
+
+    struct stat stat_buf;
+    ASSERT_EQ(-1, stat(pid_file_name.c_str(), &stat_buf));
+    EXPECT_EQ(errno, ENOENT);
 }
 
 // Checks that configureLogger method is behaving properly.
@@ -117,18 +285,6 @@ TEST_F(DaemonTest, parsingConsoleOutput) {
     EXPECT_EQ("stdout" , storage->getLoggingInfo()[0].destinations_[0].output_);
 }
 
-// Test the getVersion() redefinition
-TEST_F(DaemonTest, getVersion) {
-    EXPECT_THROW(Daemon::getVersion(false), NotImplemented);
-
-    ASSERT_NO_THROW(DaemonImpl::getVersion(false));
-
-    EXPECT_EQ(DaemonImpl::getVersion(false), "BASIC");
-
-    ASSERT_NO_THROW(DaemonImpl::getVersion(true));
-
-    EXPECT_EQ(DaemonImpl::getVersion(true), "EXTENDED");
-}
 
 
 // More tests will appear here as we develop Daemon class.

+ 100 - 0
src/lib/testutils/dhcp_test_lib.sh.in

@@ -426,6 +426,60 @@ must be a number"
     kill -${sig} ${_GET_PIDS}
 }
 
+# Verifies that a server is up running by its PID file
+# The PID file is constructed from the given config file name and
+# binary name.  If it exists and the PID it contains refers to a
+# live process it sets _SERVER_PID_FILE and _SERVER_PID to the
+# corresponding values.  Otherwise, it emits an error and exits.
+verify_server_pid() {
+    local bin_name="${1}" # binary name of the server
+    local cfg_file="${2}" # config file name
+
+    # We will construct the PID file name based on the server config
+    # and binary name
+    if [ -z ${bin_name} ]; then
+        test_lib_error "verify_server_pid requires binary name"
+        clean_exit 1
+    fi
+
+    if [ -z ${cfg_file} ]; then
+        test_lib_error "verify_server_pid requires config file name"
+        clean_exit 1
+    fi
+
+    # Only the file name portion of the config file is used, try and
+    # extract it. NOTE if this "algorithm" changes this code will need
+    # to be updated.
+    fname=`basename ${cfg_file}`
+    fname=`echo $fname | cut -f1 -d'.'`
+
+    if [ -z ${fname} ]; then
+        test_lib_error "verify_server_pid could not extract config name"
+        clean_exit 1
+    fi
+
+    # Now we can build the name:
+    pid_file="$KEA_PIDFILE_DIR/$fname.$bin_name.pid"
+
+    if [ ! -e ${pid_file} ]; then
+        printf "ERROR: PID file:[%s] does not exist\n" ${pid_file}
+        clean_exit 1
+    fi
+
+    # File exists, does its PID point to a live process?
+    pid=`cat ${pid_file}`
+    kill -0 ${pid}
+    if [ $? -ne 0 ]; then
+        printf "ERROR: PID file:[%s] exists but PID:[%d] does not\n" \
+               ${pid_file} ${pid}
+        clean_exit 1
+    fi
+
+    # Make the values accessible to the caller
+    _SERVER_PID="${pid}"
+    _SERVER_PID_FILE="${pid_file}"
+}
+
 # This test verifies that the binary is reporting its version properly.
 version_test() {
     test_name=${1}  # Test name
@@ -536,3 +590,49 @@ logger_vars_test() {
 
     test_finish 0
 }
+
+# This test verifies server PID file management
+# 1. It verifies that upon startup, the server creates a PID file
+# 2. It verifies the an attempt to start a second instance fails
+# due to pre-existing PID File/PID detection
+server_pid_file_test() {
+    local server_cfg="${1}"
+    local log_id="${2}"
+
+    # Log the start of the test and print test name.
+    test_start "${bin}.server_pid_file_test"
+    # Remove dangling DHCP4 instances and remove log files.
+    cleanup
+    # Create new configuration file.
+    create_config "${CONFIG}"
+    # Instruct server to log to the specific file.
+    set_logger
+    # Start server
+    start_kea ${bin_path}/${bin}
+    # Wait up to 20s for server to start.
+    wait_for_kea 20
+    if [ ${_WAIT_FOR_KEA} -eq 0 ]; then
+        printf "ERROR: timeout waiting for %s to start.\n" ${bin}
+        clean_exit 1
+    fi
+
+    # Verify server is still running
+    verify_server_pid ${bin} ${CFG_FILE}
+
+    printf "PID file is [%s],  PID is [%d]" ${_SERVER_PID_FILE} ${_SERVER_PID}
+
+    # Now try to start a second one
+    start_kea ${bin_path}/${bin}
+
+    wait_for_message 10 "${log_id}" 1
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: Second %s instance started? PID conflict not reported.\n" ${bin}
+        clean_exit 1
+    fi
+
+    # Verify server is still running
+    verify_server_pid ${bin} ${CFG_FILE}
+
+    # All ok. Shut down the server and exit.
+    test_finish 0
+}

+ 4 - 4
src/lib/util/pid_file.cc

@@ -28,7 +28,7 @@ PIDFile::PIDFile(const std::string& filename)
 PIDFile::~PIDFile() {
 }
 
-bool
+int
 PIDFile::check() const {
     std::ifstream fs(filename_.c_str());
     int pid;
@@ -51,13 +51,13 @@ PIDFile::check() const {
                   << filename_ << "'");
     }
 
-    // If the process is still running return true
+    // If the process is still running return its pid.
     if (kill(pid, 0) == 0) {
-        return (true);
+        return (pid);
     }
 
     // No process
-    return (false);
+    return (0);
 }
 
 void

+ 6 - 2
src/lib/util/pid_file.h

@@ -16,6 +16,7 @@
 #define PID_FILE_H
 
 #include <exceptions/exceptions.h>
+#include <boost/shared_ptr.hpp>
 #include <fstream>
 #include <ostream>
 #include <string>
@@ -62,11 +63,11 @@ public:
     /// If the file exists but can't be read or it doesn't have
     /// the proper format treat it as the process existing.
     ///
-    /// @return true if the PID is in use, false otherwise
+    /// @return returns the PID if it is in, 0 otherwise.
     ///
     /// @throw throws PIDCantReadPID if it was able to open the file
     /// but was unable to read the PID from it.
-    bool check() const;
+    int check() const;
 
     /// @brief Write the PID to the file.
     ///
@@ -95,6 +96,9 @@ private:
     std::string filename_;
 };
 
+/// @brief Defines a shared pointer to a PIDFile
+typedef boost::shared_ptr<PIDFile> PIDFilePtr;
+
 } // namespace isc::util
 } // namespace isc
 

+ 3 - 3
src/lib/util/tests/pid_file_unittest.cc

@@ -127,7 +127,7 @@ TEST_F(PIDFileTest, pidInUse) {
     pid_file.write();
 
     // Check if we think the process is running
-    EXPECT_TRUE(pid_file.check());
+    EXPECT_EQ(getpid(), pid_file.check());
 }
 
 /// @brief Test checking a PID. Write a PID that isn't in use
@@ -148,7 +148,7 @@ TEST_F(PIDFileTest, pidNotInUse) {
     pid_file.write(pid);
 
     // Check to see if we think the process is running
-    if (!pid_file.check()) {
+    if (pid_file.check() == 0) {
         return;
     }
 
@@ -159,7 +159,7 @@ TEST_F(PIDFileTest, pidNotInUse) {
     pid_file.write(pid);
 
     // Check to see if we think the process is running
-    EXPECT_FALSE(pid_file.check());
+    EXPECT_EQ(0, pid_file.check());
 }
 
 /// @brief Test checking a PID.  Write garbage to the PID file