[ovs-dev] [PATCH V9 13/17] python tests: Ported Python daemon to Windows
Alin Serdean
aserdean at cloudbasesolutions.com
Tue Jul 26 12:28:16 UTC 2016
Acked-by: Alin Gabriel Serdean <aserdean at cloudbasesolutions.com>
> -----Mesaj original-----
> De la: dev [mailto:dev-bounces at openvswitch.org] În numele Paul Boca
> Trimis: Tuesday, July 26, 2016 3:03 PM
> Către: dev at openvswitch.org
> Subiect: [ovs-dev] [PATCH V9 13/17] python tests: Ported Python daemon to
> Windows
>
> 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>
> ---
> V2: Fix lockf on Linux, small error on os.link and missing pipe_handle
> parameter.
> V3: Import modules at the start of the code
> V4: Close file before trying to delete it in signal hooks.
> On Windows the PID file cannot be deleted while it's handle
> is opened for write.
> V5: No changes
> V6: Explicitly close the vlog file in detached daemon. On Windows, even if the
> daemon is detached, the primary daemon is still holding a handle to the log
> file, therefore the log cannot be moved/deleted even is the vlog/close
> command
> is sent to the detached daemon.
> V7: Fixed flake8 errors and apply requested changes
> V8: Split daemon.py in 3 files: daemon_win.py with the specific
> implementation for
> Windows; daemon_lin.py with specific implementation for Linux; and
> daemon.py
> that imports the right file.
> V9: No changes
> ---
> 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
> 4d3fcb6..cde7ba5 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
> f45f757..42af67c 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
>
>
> def make_pidfile_name(name):
> diff --git a/python/ovs/daemon_windows.py
> b/python/ovs/daemon_windows.py new file mode 100644 index
> 0000000..5d0dda1
> --- /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.errno, "", "")
> +
> +
> +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
> _______________________________________________
> dev mailing list
> dev at openvswitch.org
> http://openvswitch.org/mailman/listinfo/dev
More information about the dev
mailing list