[ovs-dev] [PATCH 07/12] python tests: Ported Python daemon to Windows

Paul Boca pboca at cloudbasesolutions.com
Fri Aug 26 14:40:10 UTC 2016


Used subprocess.Popen instead os.fork (not implemented on windows)
and repaced of os.pipe with Windows pipes.

To be able to identify the child process I added an extra parameter
to daemon process '--pipe-handle', this parameter also contains
the parent Windows pipe handle, used by the child to signal the start.

The PID file is created directly on Windows, without using a temporary file
because the symbolic link doesn't inheriths the file lock set on temporary file.

Signed-off-by: Paul-Daniel Boca <pboca at cloudbasesolutions.com>
---
 INSTALL.Windows.md           |   1 +
 python/automake.mk           |   1 +
 python/ovs/daemon.py         |   2 +
 python/ovs/daemon_windows.py | 535 +++++++++++++++++++++++++++++++++++++++++++
 python/ovs/fatal_signal.py   |  13 ++
 python/ovs/vlog.py           |  12 +
 tests/test-daemon.py         |   4 +-
 7 files changed, 566 insertions(+), 2 deletions(-)
 create mode 100644 python/ovs/daemon_windows.py

diff --git a/INSTALL.Windows.md b/INSTALL.Windows.md
index 6b0f5d8..207fd93 100644
--- a/INSTALL.Windows.md
+++ b/INSTALL.Windows.md
@@ -27,6 +27,7 @@ the following entry in /etc/fstab - 'C:/MinGW /mingw'.
 
 * Install the latest Python 2.x from python.org and verify that its path is
 part of Windows' PATH environment variable.
+You must also have the Python six and pywin32 libraries.
 
 * You will need at least Visual Studio 2013 (update 4) to compile userspace
 binaries.  In addition to that, if you want to compile the kernel module you
diff --git a/python/automake.mk b/python/automake.mk
index 1bbe390..d6e39bb 100644
--- a/python/automake.mk
+++ b/python/automake.mk
@@ -12,6 +12,7 @@ ovs_pyfiles = \
 	python/ovs/__init__.py \
 	python/ovs/daemon.py \
 	python/ovs/daemon_unix.py \
+	python/ovs/daemon_windows.py \
 	python/ovs/fcntl_win.py \
 	python/ovs/db/__init__.py \
 	python/ovs/db/data.py \
diff --git a/python/ovs/daemon.py b/python/ovs/daemon.py
index b1d6c36..f6802fd 100644
--- a/python/ovs/daemon.py
+++ b/python/ovs/daemon.py
@@ -17,6 +17,8 @@ import sys
 # This is only a wrapper over Linux implementations
 if sys.platform != "win32":
     import ovs.daemon_unix as daemon_util
+else:
+    import ovs.daemon_windows as daemon_util
 
 RESTART_EXIT_CODE = daemon_util.RESTART_EXIT_CODE
 
diff --git a/python/ovs/daemon_windows.py b/python/ovs/daemon_windows.py
new file mode 100644
index 0000000..7ea2212
--- /dev/null
+++ b/python/ovs/daemon_windows.py
@@ -0,0 +1,535 @@
+# Copyright (c) 2016 Cloudbase Solutions Srl
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at:
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import errno
+import os
+import signal
+import sys
+import time
+
+import ovs.dirs
+import ovs.fatal_signal
+import ovs.process
+import ovs.socket_util
+import ovs.timeval
+import ovs.util
+import ovs.vlog
+
+import ovs.fcntl_win as fcntl
+import threading
+import win32file
+import win32pipe
+import win32security
+import pywintypes
+import subprocess
+
+vlog = ovs.vlog.Vlog("daemon")
+
+# --detach: Should we run in the background?
+_detach = False
+
+# --pidfile: Name of pidfile (null if none).
+_pidfile = None
+
+# Our pidfile's inode and device, if we have created one.
+_pidfile_dev = None
+_pidfile_ino = None
+
+# --overwrite-pidfile: Create pidfile even if one already exists and is locked?
+_overwrite_pidfile = False
+
+# --no-chdir: Should we chdir to "/"?
+_chdir = True
+
+# --monitor: Should a supervisory process monitor the daemon and restart it if
+# it dies due to an error signal?
+_monitor = False
+
+# File descriptor used by daemonize_start() and daemonize_complete().
+_daemonize_fd = None
+
+# Running as the child process - Windows only.
+_detached = False
+
+RESTART_EXIT_CODE = 5
+
+
+def make_pidfile_name(name):
+    """Returns the file name that would be used for a pidfile if 'name' were
+    provided to set_pidfile()."""
+    if name is None or name == "":
+        return "%s/%s.pid" % (ovs.dirs.RUNDIR, ovs.util.PROGRAM_NAME)
+    else:
+        return ovs.util.abs_file_name(ovs.dirs.RUNDIR, name)
+
+
+def set_pidfile(name):
+    """Sets up a following call to daemonize() to create a pidfile named
+    'name'.  If 'name' begins with '/', then it is treated as an absolute path.
+    Otherwise, it is taken relative to ovs.util.RUNDIR, which is
+    $(prefix)/var/run by default.
+
+    If 'name' is null, then ovs.util.PROGRAM_NAME followed by ".pid" is
+    used."""
+    global _pidfile
+    _pidfile = make_pidfile_name(name)
+
+
+def set_no_chdir():
+    """Sets that we do not chdir to "/"."""
+    global _chdir
+    _chdir = False
+
+
+def ignore_existing_pidfile():
+    """Normally, daemonize() or daemonize_start() will terminate the program
+    with a message if a locked pidfile already exists.  If this function is
+    called, an existing pidfile will be replaced, with a warning."""
+    global _overwrite_pidfile
+    _overwrite_pidfile = True
+
+
+def set_detach():
+    """Sets up a following call to daemonize() to detach from the foreground
+    session, running this process in the background."""
+    global _detach
+    _detach = True
+
+
+def get_detach():
+    """Will daemonize() really detach?"""
+    return _detach
+
+
+def set_monitor():
+    """Sets up a following call to daemonize() to fork a supervisory process to
+    monitor the daemon and restart it if it dies due to an error signal."""
+    global _monitor
+    _monitor = True
+
+
+def set_detached(wp):
+    """Sets up a following call to daemonize() to fork a supervisory process to
+    monitor the daemon and restart it if it dies due to an error signal."""
+    global _detached
+    global _daemonize_fd
+    _detached = True
+    _daemonize_fd = int(wp)
+
+
+def _fatal(msg):
+    vlog.err(msg)
+    sys.stderr.write("%s\n" % msg)
+    sys.exit(1)
+
+
+def _make_pidfile():
+    """If a pidfile has been configured, creates it and stores the running
+    process's pid in it.  Ensures that the pidfile will be deleted when the
+    process exits."""
+    pid = os.getpid()
+
+    # Create a temporary pidfile.
+    tmpfile = _pidfile
+
+    try:
+        # This is global to keep Python from garbage-collecting and
+        # therefore closing our file after this function exits.  That would
+        # unlock the lock for us, and we don't want that.
+        global file_handle
+
+        file_handle = open(tmpfile, "w")
+    except IOError as e:
+        _fatal("%s: create failed (%s)" % (tmpfile, e.strerror))
+
+    try:
+        s = os.fstat(file_handle.fileno())
+    except IOError as e:
+        _fatal("%s: fstat failed (%s)" % (tmpfile, e.strerror))
+
+    try:
+        file_handle.write("%s\n" % pid)
+        file_handle.flush()
+    except OSError as e:
+        _fatal("%s: write failed: %s" % (tmpfile, e.strerror))
+
+    try:
+        fcntl.lockf(file_handle, fcntl.LOCK_SH | fcntl.LOCK_NB)
+    except IOError as e:
+        _fatal("%s: fcntl failed: %s" % (tmpfile, e.strerror))
+
+    # Ensure that the pidfile will gets closed and deleted on exit.
+    ovs.fatal_signal.add_file_to_close_and_unlink(_pidfile, file_handle)
+
+    global _pidfile_dev
+    global _pidfile_ino
+    _pidfile_dev = s.st_dev
+    _pidfile_ino = s.st_ino
+
+
+def daemonize():
+    """If configured with set_pidfile() or set_detach(), creates the pid file
+    and detaches from the foreground session."""
+    daemonize_start()
+    daemonize_complete()
+
+
+def _waitpid(pid, options):
+    while True:
+        try:
+            return os.waitpid(pid, options)
+        except OSError as e:
+            if e.errno == errno.EINTR:
+                pass
+            return -e.errno, 0
+
+
+def _windows_create_pipe():
+    sAttrs = win32security.SECURITY_ATTRIBUTES()
+    sAttrs.bInheritHandle = 1
+
+    (read_pipe, write_pipe) = win32pipe.CreatePipe(sAttrs, 0)
+
+    return (read_pipe, write_pipe)
+
+
+def _windows_read_pipe(fd):
+    if fd is not None:
+        sAttrs = win32security.SECURITY_ATTRIBUTES()
+        sAttrs.bInheritHandle = 1
+        try:
+            (ret, data) = win32file.ReadFile(fd, 1, None)
+            return data
+        except pywintypes.error as e:
+            raise OSError(e.winerror, "", "")
+
+
+def _windows_cleanup(proc, wfd):
+    """ If the child process closes and it was detached
+    then close the communication pipe so the parent process
+    can terminate """
+    proc.wait()
+    win32file.CloseHandle(wfd)
+
+
+def _fork_and_wait_for_startup():
+    if _detached:
+        return 0
+
+    """ close the log file, on Windows cannot be moved while the parent has
+    a reference on it."""
+    vlog.close_log_file()
+
+    try:
+        (rfd, wfd) = _windows_create_pipe()
+    except pywintypes.error as e:
+        sys.stderr.write("pipe failed: %s\n" % os.strerror(e.errno))
+        sys.exit(1)
+
+    try:
+        proc = subprocess.Popen("%s %s --pipe-handle=%ld"
+                                % (sys.executable, " ".join(sys.argv),
+                                   int(wfd)), close_fds=False, shell=False)
+        pid = proc.pid
+        # Start a thread and wait the subprocess exit code
+        thread = threading.Thread(target=_windows_cleanup, args=(proc, wfd))
+        thread.daemon = True
+        thread.start()
+    except OSError as e:
+        sys.stderr.write("could not fork: %s\n" % os.strerror(e.errno))
+        sys.exit(1)
+
+    if pid > 0:
+        # Running in parent process.
+        ovs.fatal_signal.fork()
+        while True:
+            try:
+                s = _windows_read_pipe(rfd)
+                error = 0
+            except OSError as e:
+                s = ""
+                error = e.errno
+            if error != errno.EINTR:
+                break
+        if len(s) != 1:
+            retval, status = _waitpid(pid, 0)
+            if retval == pid:
+                if os.WIFEXITED(status) and os.WEXITSTATUS(status):
+                    # Child exited with an error.  Convey the same error to
+                    # our parent process as a courtesy.
+                    sys.exit(os.WEXITSTATUS(status))
+                else:
+                    sys.stderr.write("fork child failed to signal "
+                                     "startup (%s)\n"
+                                     % ovs.process.status_msg(status))
+            else:
+                assert retval < 0
+                sys.stderr.write("waitpid failed (%s)\n"
+                                 % os.strerror(-retval))
+                sys.exit(1)
+
+    return pid
+
+
+def _fork_notify_startup(fd):
+    if fd is not None:
+        try:
+            try:
+                win32file.WriteFile(fd, "0", None)
+            except:
+                win32file.WriteFile(fd, bytes("0", 'UTF-8'), None)
+        except pywintypes.error as e:
+            sys.stderr.write("could not write to pipe %s\n" %
+                             os.strerror(e.errno))
+            sys.exit(1)
+
+
+def _should_restart(status):
+    global RESTART_EXIT_CODE
+
+    if os.WIFEXITED(status) and os.WEXITSTATUS(status) == RESTART_EXIT_CODE:
+        return True
+
+    if os.WIFSIGNALED(status):
+        for signame in ("SIGABRT", "SIGFPE", "SIGILL", "SIGSEGV"):
+            if os.WTERMSIG(status) == getattr(signal, signame, None):
+                return True
+    return False
+
+
+def _monitor_daemon(daemon_pid):
+    # XXX should log daemon's stderr output at startup time
+    # XXX should use setproctitle module if available
+    last_restart = None
+    while True:
+        retval, status = _waitpid(daemon_pid, 0)
+        if retval < 0:
+            sys.stderr.write("waitpid failed\n")
+            sys.exit(1)
+        elif retval == daemon_pid:
+            status_msg = ("pid %d died, %s"
+                          % (daemon_pid, ovs.process.status_msg(status)))
+
+            if _should_restart(status):
+                # Throttle restarts to no more than once every 10 seconds.
+                if (last_restart is not None and
+                    ovs.timeval.msec() < last_restart + 10000):
+                    vlog.warn("%s, waiting until 10 seconds since last "
+                              "restart" % status_msg)
+                    while True:
+                        now = ovs.timeval.msec()
+                        wakeup = last_restart + 10000
+                        if now > wakeup:
+                            break
+                        sys.stdout.write("sleep %f\n" % (
+                            (wakeup - now) / 1000.0))
+                        time.sleep((wakeup - now) / 1000.0)
+                last_restart = ovs.timeval.msec()
+
+                vlog.err("%s, restarting" % status_msg)
+                daemon_pid = _fork_and_wait_for_startup()
+                if not daemon_pid:
+                    break
+            else:
+                vlog.info("%s, exiting" % status_msg)
+                sys.exit(0)
+
+    # Running in new daemon process.
+
+
+def daemonize_start():
+    """If daemonization is configured, then starts daemonization, by forking
+    and returning in the child process.  The parent process hangs around until
+    the child lets it know either that it completed startup successfully (by
+    calling daemon_complete()) or that it failed to start up (by exiting with a
+    nonzero exit code)."""
+
+    if _detach:
+        if _fork_and_wait_for_startup() > 0:
+            # Running in parent process.
+            sys.exit(0)
+
+    if _monitor:
+        saved_daemonize_fd = _daemonize_fd
+        daemon_pid = _fork_and_wait_for_startup()
+        if daemon_pid > 0:
+            # Running in monitor process.
+            _fork_notify_startup(saved_daemonize_fd)
+            _monitor_daemon(daemon_pid)
+        # Running in daemon process
+
+    if _pidfile:
+        _make_pidfile()
+
+
+def daemonize_complete():
+    """If daemonization is configured, then this function notifies the parent
+    process that the child process has completed startup successfully."""
+    _fork_notify_startup(_daemonize_fd)
+
+    if _detach:
+        if _chdir:
+            os.chdir("/")
+
+
+def usage():
+    sys.stdout.write("""
+Daemon options:
+   --detach                run in background as daemon
+   --no-chdir              do not chdir to '/'
+   --pidfile[=FILE]        create pidfile (default: %s/%s.pid)
+   --overwrite-pidfile     with --pidfile, start even if already running
+""" % (ovs.dirs.RUNDIR, ovs.util.PROGRAM_NAME))
+
+
+def __read_pidfile(pidfile, delete_if_stale):
+    if _pidfile_dev is not None:
+        try:
+            s = os.stat(pidfile)
+            if s.st_ino == _pidfile_ino and s.st_dev == _pidfile_dev:
+                # It's our own pidfile.  We can't afford to open it,
+                # because closing *any* fd for a file that a process
+                # has locked also releases all the locks on that file.
+                #
+                # Fortunately, we know the associated pid anyhow.
+                return os.getpid()
+        except OSError:
+            pass
+
+    try:
+        file_handle = open(pidfile, "r+")
+    except IOError as e:
+        if e.errno == errno.ENOENT and delete_if_stale:
+            return 0
+        vlog.warn("%s: open: %s" % (pidfile, e.strerror))
+        return -e.errno
+
+    # Python fcntl doesn't directly support F_GETLK so we have to just try
+    # to lock it.
+    try:
+        fcntl.lockf(file_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
+
+        # pidfile exists but wasn't locked by anyone.  Now we have the lock.
+        if not delete_if_stale:
+            file_handle.close()
+            vlog.warn("%s: pid file is stale" % pidfile)
+            return -errno.ESRCH
+
+        # Is the file we have locked still named 'pidfile'?
+        try:
+            raced = False
+            s = os.stat(pidfile)
+            s2 = os.fstat(file_handle.fileno())
+            if s.st_ino != s2.st_ino or s.st_dev != s2.st_dev:
+                raced = True
+        except IOError:
+            raced = True
+        if raced:
+            vlog.warn("%s: lost race to delete pidfile" % pidfile)
+            return -errno.EALREADY
+
+        # We won the right to delete the stale pidfile.
+        try:
+            os.unlink(pidfile)
+        except IOError as e:
+            vlog.warn("%s: failed to delete stale pidfile (%s)"
+                            % (pidfile, e.strerror))
+            return -e.errno
+        else:
+            vlog.dbg("%s: deleted stale pidfile" % pidfile)
+            file_handle.close()
+            return 0
+    except IOError as e:
+        if e.errno not in [errno.EACCES, errno.EAGAIN]:
+            vlog.warn("%s: fcntl: %s" % (pidfile, e.strerror))
+            return -e.errno
+
+    # Someone else has the pidfile locked.
+    try:
+        try:
+            error = int(file_handle.readline())
+        except IOError as e:
+            vlog.warn("%s: read: %s" % (pidfile, e.strerror))
+            error = -e.errno
+        except ValueError:
+            vlog.warn("%s does not contain a pid" % pidfile)
+            error = -errno.EINVAL
+
+        return error
+    finally:
+        try:
+            file_handle.close()
+        except IOError:
+            pass
+
+
+def read_pidfile(pidfile):
+    """Opens and reads a PID from 'pidfile'.  Returns the positive PID if
+    successful, otherwise a negative errno value."""
+    return __read_pidfile(pidfile, False)
+
+
+def _check_already_running():
+    pid = __read_pidfile(_pidfile, True)
+    if pid > 0:
+        _fatal("%s: already running as pid %d, aborting" % (_pidfile, pid))
+    elif pid < 0:
+        _fatal("%s: pidfile check failed (%s), aborting"
+               % (_pidfile, os.strerror(pid)))
+
+
+def add_args(parser):
+    """Populates 'parser', an ArgumentParser allocated using the argparse
+    module, with the command line arguments required by the daemon module."""
+
+    pidfile = make_pidfile_name(None)
+
+    group = parser.add_argument_group(title="Daemon Options")
+    group.add_argument("--detach", action="store_true",
+            help="Run in background as a daemon.")
+    group.add_argument("--no-chdir", action="store_true",
+            help="Do not chdir to '/'.")
+    group.add_argument("--monitor", action="store_true",
+            help="Monitor %s process." % ovs.util.PROGRAM_NAME)
+    group.add_argument("--pidfile", nargs="?", const=pidfile,
+            help="Create pidfile (default %s)." % pidfile)
+    group.add_argument("--overwrite-pidfile", action="store_true",
+            help="With --pidfile, start even if already running.")
+    group.add_argument("--pipe-handle",
+            help="With --pidfile, start even if already running.")
+
+
+def handle_args(args):
+    """Handles daemon module settings in 'args'.  'args' is an object
+    containing values parsed by the parse_args() method of ArgumentParser.  The
+    parent ArgumentParser should have been prepared by add_args() before
+    calling parse_args()."""
+
+    if args.pipe_handle:
+        set_detached(args.pipe_handle)
+
+    if args.detach:
+        set_detach()
+
+    if args.no_chdir:
+        set_no_chdir()
+
+    if args.pidfile:
+        set_pidfile(args.pidfile)
+
+    if args.overwrite_pidfile:
+        ignore_existing_pidfile()
+
+    if args.monitor:
+        set_monitor()
diff --git a/python/ovs/fatal_signal.py b/python/ovs/fatal_signal.py
index 73e4be6..dfc446e 100644
--- a/python/ovs/fatal_signal.py
+++ b/python/ovs/fatal_signal.py
@@ -58,6 +58,17 @@ def add_file_to_unlink(file):
     _files[file] = None
 
 
+def add_file_to_close_and_unlink(file, fd=None):
+    """Registers 'file' to be unlinked when the program terminates via
+    sys.exit() or a fatal signal and the 'fd' to be closed. On Windows a file
+    cannot be removed while it is open for writing."""
+    global _added_hook
+    if not _added_hook:
+        _added_hook = True
+        add_hook(_unlink_files, _cancel_files, True)
+    _files[file] = fd
+
+
 def remove_file_to_unlink(file):
     """Unregisters 'file' from being unlinked when the program terminates via
     sys.exit() or a fatal signal."""
@@ -77,6 +88,8 @@ def unlink_file_now(file):
 
 def _unlink_files():
     for file_ in _files:
+        if _files[file_]:
+            _files[file_].close()
         _unlink(file_)
 
 
diff --git a/python/ovs/vlog.py b/python/ovs/vlog.py
index 48d52ad..2768ce7 100644
--- a/python/ovs/vlog.py
+++ b/python/ovs/vlog.py
@@ -384,6 +384,17 @@ class Vlog(object):
             logger.addHandler(Vlog.__file_handler)
 
     @staticmethod
+    def close_log_file():
+        """Closes the current log file. (This is useful on Windows, to ensure
+        that a reference to the file is not kept by the daemon in case of
+        detach.)"""
+
+        if Vlog.__log_file:
+            logger = logging.getLogger("file")
+            logger.removeHandler(Vlog.__file_handler)
+            Vlog.__file_handler.close()
+
+    @staticmethod
     def _unixctl_vlog_reopen(conn, unused_argv, unused_aux):
         if Vlog.__log_file:
             Vlog.reopen_log_file()
@@ -396,6 +407,7 @@ class Vlog(object):
         if Vlog.__log_file:
             logger = logging.getLogger("file")
             logger.removeHandler(Vlog.__file_handler)
+            Vlog.__file_handler.close()
         conn.reply(None)
 
     @staticmethod
diff --git a/tests/test-daemon.py b/tests/test-daemon.py
index 63c1f70..a3b5751 100644
--- a/tests/test-daemon.py
+++ b/tests/test-daemon.py
@@ -26,8 +26,8 @@ def handler(signum, _):
 
 
 def main():
-
-    signal.signal(signal.SIGHUP, handler)
+    if sys.platform != "win32":
+        signal.signal(signal.SIGHUP, handler)
 
     parser = argparse.ArgumentParser(
             description="Open vSwitch daemonization test program for Python.")
-- 
2.7.2.windows.1



More information about the dev mailing list