Acked-by: Alin Gabriel Serdean <[email protected]>
> -----Mesaj original----- > De la: dev [mailto:[email protected]] În numele Paul Boca > Trimis: Tuesday, July 26, 2016 3:03 PM > Către: [email protected] > 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 <[email protected]> > --- > 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 > [email protected] > http://openvswitch.org/mailman/listinfo/dev _______________________________________________ dev mailing list [email protected] http://openvswitch.org/mailman/listinfo/dev
