From patchwork Fri Aug 26 14:40:10 2016 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Paul Boca X-Patchwork-Id: 663131 X-Patchwork-Delegate: guru@ovn.org Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from archives.nicira.com (archives.nicira.com [96.126.127.54]) by ozlabs.org (Postfix) with ESMTP id 3sLNzK74FZz9s8x for ; Sat, 27 Aug 2016 00:41:49 +1000 (AEST) Received: from archives.nicira.com (localhost [127.0.0.1]) by archives.nicira.com (Postfix) with ESMTP id 6ECED109C1; Fri, 26 Aug 2016 07:41:36 -0700 (PDT) X-Original-To: dev@openvswitch.org Delivered-To: dev@openvswitch.org Received: from mx1e4.cudamail.com (mx1.cudamail.com [69.90.118.67]) by archives.nicira.com (Postfix) with ESMTPS id D12E7109BA for ; Fri, 26 Aug 2016 07:41:34 -0700 (PDT) Received: from bar5.cudamail.com (unknown [192.168.21.12]) by mx1e4.cudamail.com (Postfix) with ESMTPS id 688C61E04F8 for ; Fri, 26 Aug 2016 08:41:34 -0600 (MDT) X-ASG-Debug-ID: 1472222493-09eadd73980efb0001-byXFYA Received: from mx1-pf1.cudamail.com ([192.168.24.1]) by bar5.cudamail.com with ESMTP id LHRKy3sE1aOqFVMe (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NO) for ; Fri, 26 Aug 2016 08:41:33 -0600 (MDT) X-Barracuda-Envelope-From: pboca@cloudbasesolutions.com X-Barracuda-RBL-Trusted-Forwarder: 192.168.24.1 Received: from unknown (HELO cbssmtp1.cloudbase.local) (91.232.152.5) by mx1-pf1.cudamail.com with SMTP; 26 Aug 2016 14:41:32 -0000 Received-SPF: pass (mx1-pf1.cudamail.com: SPF record at cloudbasesolutions.com designates 91.232.152.5 as permitted sender) X-Barracuda-Apparent-Source-IP: 91.232.152.5 X-Barracuda-RBL-IP: 91.232.152.5 Received: from localhost (localhost [127.0.0.1]) by cbssmtp1.cloudbase.local (Postfix) with ESMTP id 8638C41A3B for ; Fri, 26 Aug 2016 17:41:31 +0300 (EEST) X-Virus-Scanned: amavisd-new at cloudbasesolutions.com Received: from cbssmtp1.cloudbase.local ([127.0.0.1]) by localhost (cbssmtp1.cloudbase.local [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id G1SAJDgPD-9t for ; Fri, 26 Aug 2016 17:41:10 +0300 (EEST) Received: from CBSEX1.cloudbase.local (unknown [10.77.78.3]) by cbssmtp1.cloudbase.local (Postfix) with ESMTP id C327A41A3E for ; Fri, 26 Aug 2016 17:40:11 +0300 (EEST) Received: from CBSEX1.cloudbase.local ([10.77.78.3]) by CBSEX1.cloudbase.local ([10.77.78.3]) with mapi id 14.03.0301.000; Fri, 26 Aug 2016 16:40:11 +0200 X-CudaMail-Envelope-Sender: pboca@cloudbasesolutions.com From: Paul Boca To: "dev@openvswitch.org" X-CudaMail-MID: CM-E1-825022227 X-CudaMail-DTE: 082616 X-CudaMail-Originating-IP: 91.232.152.5 Thread-Topic: [PATCH 07/12] python tests: Ported Python daemon to Windows X-ASG-Orig-Subj: [##CM-E1-825022227##][PATCH 07/12] python tests: Ported Python daemon to Windows Thread-Index: AQHR/6e/ifLAHj/GX0+lc+A1OqCqLw== Date: Fri, 26 Aug 2016 14:40:10 +0000 Message-ID: <1472222398-5780-8-git-send-email-pboca@cloudbasesolutions.com> References: <1472222398-5780-1-git-send-email-pboca@cloudbasesolutions.com> In-Reply-To: <1472222398-5780-1-git-send-email-pboca@cloudbasesolutions.com> Accept-Language: en-US, it-IT Content-Language: en-US X-MS-Has-Attach: X-MS-TNEF-Correlator: x-originating-ip: [10.77.78.1] MIME-Version: 1.0 X-Barracuda-Connect: UNKNOWN[192.168.24.1] X-Barracuda-Start-Time: 1472222493 X-Barracuda-Encrypted: ECDHE-RSA-AES256-GCM-SHA384 X-Barracuda-URL: https://web.cudamail.com:443/cgi-mod/mark.cgi X-Virus-Scanned: by bsmtpd at cudamail.com X-Barracuda-BRTS-Status: 1 X-Barracuda-Spam-Score: 1.10 X-Barracuda-Spam-Status: No, SCORE=1.10 using global scores of TAG_LEVEL=3.5 QUARANTINE_LEVEL=1000.0 KILL_LEVEL=4.0 tests=BSF_RULE7568M, BSF_RULE_7582B, INFO_TLD, RDNS_NONE X-Barracuda-Spam-Report: Code version 3.2, rules version 3.2.3.32341 Rule breakdown below pts rule name description ---- ---------------------- -------------------------------------------------- 0.00 INFO_TLD URI: Contains an URL in the INFO top-level domain 0.50 BSF_RULE7568M Custom Rule 7568M 0.50 BSF_RULE_7582B Custom Rule 7582B 0.10 RDNS_NONE Delivered to trusted network by a host with no rDNS Subject: [ovs-dev] [PATCH 07/12] python tests: Ported Python daemon to Windows X-BeenThere: dev@openvswitch.org X-Mailman-Version: 2.1.16 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: dev-bounces@openvswitch.org Sender: "dev" 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 --- 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.")