@@ -13,9 +13,7 @@
# limitations under the License.
import errno
-import fcntl
import os
-import resource
import signal
import sys
import time
@@ -28,6 +26,20 @@ import ovs.timeval
import ovs.util
import ovs.vlog
+if sys.platform == "win32":
+ import ovs.fcntl_win as fcntl
+ import threading
+ import win32file
+ import win32pipe
+ import win32api
+ import win32security
+ import win32con
+ import pywintypes
+ import subprocess
+else:
+ import fcntl
+ import resource
+
vlog = ovs.vlog.Vlog("daemon")
# --detach: Should we run in the background?
@@ -53,6 +65,10 @@ _monitor = False
# File descriptor used by daemonize_start() and daemonize_complete().
_daemonize_fd = None
+if sys.platform == "win32":
+ # Running as the child process - Windows only.
+ _detached = False
+
RESTART_EXIT_CODE = 5
@@ -110,6 +126,16 @@ def set_monitor():
_monitor = True
+if sys.platform == "win32":
+ 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)
@@ -123,8 +149,12 @@ def _make_pidfile():
pid = os.getpid()
# Create a temporary pidfile.
- tmpfile = "%s.tmp%d" % (_pidfile, pid)
- ovs.fatal_signal.add_file_to_unlink(tmpfile)
+ if sys.platform == "win32":
+ tmpfile = _pidfile
+ else:
+ tmpfile = "%s.tmp%d" % (_pidfile, pid)
+ ovs.fatal_signal.add_file_to_unlink(tmpfile)
+
try:
# This is global to keep Python from garbage-collecting and
# therefore closing our file after this function exits. That would
@@ -147,40 +177,48 @@ def _make_pidfile():
_fatal("%s: write failed: %s" % (tmpfile, e.strerror))
try:
- fcntl.lockf(file_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ if sys.platform != "win32":
+ fcntl.lockf(file_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ else:
+ fcntl.lockf(file_handle, fcntl.LOCK_SH | fcntl.LOCK_NB)
except IOError as e:
_fatal("%s: fcntl failed: %s" % (tmpfile, e.strerror))
- # Rename or link it to the correct name.
- if _overwrite_pidfile:
- try:
- os.rename(tmpfile, _pidfile)
- except OSError as e:
- _fatal("failed to rename \"%s\" to \"%s\" (%s)"
- % (tmpfile, _pidfile, e.strerror))
- else:
- while True:
+ if sys.platform != "win32":
+ # Rename or link it to the correct name.
+ if _overwrite_pidfile:
try:
- os.link(tmpfile, _pidfile)
- error = 0
+ os.rename(tmpfile, _pidfile)
except OSError as e:
- error = e.errno
- if error == errno.EEXIST:
- _check_already_running()
- elif error != errno.EINTR:
- break
- if error:
- _fatal("failed to link \"%s\" as \"%s\" (%s)"
- % (tmpfile, _pidfile, os.strerror(error)))
-
- # Ensure that the pidfile will get deleted on exit.
- ovs.fatal_signal.add_file_to_unlink(_pidfile)
-
- # Delete the temporary pidfile if it still exists.
- if not _overwrite_pidfile:
- error = ovs.fatal_signal.unlink_file_now(tmpfile)
- if error:
- _fatal("%s: unlink failed (%s)" % (tmpfile, os.strerror(error)))
+ _fatal("failed to rename \"%s\" to \"%s\" (%s)"
+ % (tmpfile, _pidfile, e.strerror))
+ else:
+ while True:
+ try:
+ os.link(tmpfile, _pidfile)
+ error = 0
+ except OSError as e:
+ error = e.errno
+ if error == errno.EEXIST:
+ _check_already_running()
+ elif error != errno.EINTR:
+ break
+ if error:
+ _fatal("failed to link \"%s\" as \"%s\" (%s)"
+ % (tmpfile, _pidfile, os.strerror(error)))
+
+ # Ensure that the pidfile will get deleted on exit.
+ ovs.fatal_signal.add_file_to_unlink(_pidfile)
+
+ # Delete the temporary pidfile if it still exists.
+ if not _overwrite_pidfile:
+ error = ovs.fatal_signal.unlink_file_now(tmpfile)
+ if error:
+ _fatal("%s: unlink failed (%s)"
+ % (tmpfile, os.strerror(error)))
+ else:
+ # 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
@@ -205,6 +243,105 @@ def _waitpid(pid, options):
return -e.errno, 0
+def _windows_create_pipe():
+ sAttrs = win32security.SECURITY_ATTRIBUTES()
+ sAttrs.bInheritHandle = 1
+
+ (read_pipe, write_pipe) = win32pipe.CreatePipe(sAttrs, 0)
+ pid = win32api.GetCurrentProcess()
+ write_pipe2 = win32api.DuplicateHandle(pid, write_pipe, pid, 0, 1,
+ win32con.DUPLICATE_SAME_ACCESS)
+ win32file.CloseHandle(write_pipe)
+
+ return (read_pipe, write_pipe2)
+
+
+def __windows_fork_notify_startup(fd):
+ if fd is not None:
+ try:
+ win32file.WriteFile(fd, "0", None)
+ except:
+ win32file.WriteFile(fd, bytes("0", 'UTF-8'), None)
+
+
+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(errno.EIO, "", "")
+
+
+def _windows_detach(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 _windows_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_detach, args=(proc, wfd))
+ thread.daemon = True
+ thread.start()
+ except pywintypes.error 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_and_wait_for_startup():
try:
rfd, wfd = os.pipe()
@@ -250,7 +387,7 @@ def _fork_and_wait_for_startup():
os.close(rfd)
else:
- # Running in parent process.
+ # Running in child process.
os.close(rfd)
ovs.timeval.postfork()
@@ -259,7 +396,14 @@ def _fork_and_wait_for_startup():
return pid
-def _fork_notify_startup(fd):
+def fork_and_wait_for_startup():
+ if sys.platform == "win32":
+ return _windows_fork_and_wait_for_startup()
+ else:
+ return _fork_and_wait_for_startup()
+
+
+def __fork_notify_startup(fd):
if fd is not None:
error, bytes_written = ovs.socket_util.write_fully(fd, "0")
if error:
@@ -268,6 +412,13 @@ def _fork_notify_startup(fd):
os.close(fd)
+def _fork_notify_startup(fd):
+ if sys.platform == "win32":
+ return __windows_fork_notify_startup(fd)
+ else:
+ return __fork_notify_startup(fd)
+
+
def _should_restart(status):
global RESTART_EXIT_CODE
@@ -296,7 +447,7 @@ def _monitor_daemon(daemon_pid):
% (daemon_pid, ovs.process.status_msg(status)))
if _should_restart(status):
- if os.WCOREDUMP(status):
+ if os.WCOREDUMP(status) and sys.platform != "win32":
# Disable further core dumps to save disk space.
try:
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
@@ -319,7 +470,7 @@ def _monitor_daemon(daemon_pid):
last_restart = ovs.timeval.msec()
vlog.err("%s, restarting" % status_msg)
- daemon_pid = _fork_and_wait_for_startup()
+ daemon_pid = fork_and_wait_for_startup()
if not daemon_pid:
break
else:
@@ -332,6 +483,9 @@ def _monitor_daemon(daemon_pid):
def _close_standard_fds():
"""Close stdin, stdout, stderr. If we're started from e.g. an SSH session,
then this keeps us from holding that session open artificially."""
+ if sys.platform == "win32":
+ return
+
null_fd = ovs.socket_util.get_null_fd()
if null_fd >= 0:
os.dup2(null_fd, 0)
@@ -347,16 +501,17 @@ def daemonize_start():
nonzero exit code)."""
if _detach:
- if _fork_and_wait_for_startup() > 0:
+ if fork_and_wait_for_startup() > 0:
# Running in parent process.
sys.exit(0)
# Running in daemon or monitor process.
- os.setsid()
+ if sys.platform != "win32":
+ os.setsid()
if _monitor:
saved_daemonize_fd = _daemonize_fd
- daemon_pid = _fork_and_wait_for_startup()
+ daemon_pid = fork_and_wait_for_startup()
if daemon_pid > 0:
# Running in monitor process.
_fork_notify_startup(saved_daemonize_fd)
@@ -502,6 +657,9 @@ def add_args(parser):
help="Create pidfile (default %s)." % pidfile)
group.add_argument("--overwrite-pidfile", action="store_true",
help="With --pidfile, start even if already running.")
+ if sys.platform == "win32":
+ group.add_argument("--pipe-handle",
+ help="With --pidfile, start even if already running.")
def handle_args(args):
@@ -510,6 +668,9 @@ def handle_args(args):
parent ArgumentParser should have been prepared by add_args() before
calling parse_args()."""
+ if sys.platform == "win32" and args.pipe_handle:
+ set_detached(args.pipe_handle)
+
if args.detach:
set_detach()
@@ -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_)
@@ -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
@@ -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.")
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@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 --- python/ovs/daemon.py | 241 +++++++++++++++++++++++++++++++++++++-------- python/ovs/fatal_signal.py | 13 +++ python/ovs/vlog.py | 12 +++ tests/test-daemon.py | 4 +- 4 files changed, 228 insertions(+), 42 deletions(-)