Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support windows services #1041

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions circus/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
OTHER = 3


DEFAULT_PROCESS = "default"
PAPA = "papa"
WIN_SERVICE = "win_service"


# psutil < 2.x compat
def get_children(proc, recursive=False):
try:
Expand Down
29 changes: 27 additions & 2 deletions circus/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@
from circus.py3compat import bytestring, is_callable, b, PY2


from circus.process import DEFAULT_PROCESS, PAPA, WIN_SERVICE
if IS_WINDOWS:
from circus.win_process import WinService
PROCESS_TYPES = {
DEFAULT_PROCESS: Process,
PAPA: PapaProcessProxy,
WIN_SERVICE: WinService
}
else:
PROCESS_TYPES = {
DEFAULT_PROCESS: Process,
PAPA: PapaProcessProxy
}


class Watcher(object):

"""
Expand Down Expand Up @@ -194,6 +209,9 @@ class Watcher(object):

- **use_papa**: If True, use the papa process kernel for this process.
default: False.

- **proc_type**: Type of the process to start. Local, win_service,
win_remote, linux_remote
"""

def __init__(self, name, cmd, args=None, numprocesses=1, warmup_delay=0.,
Expand All @@ -209,7 +227,7 @@ def __init__(self, name, cmd, args=None, numprocesses=1, warmup_delay=0.,
stdin_socket=None, close_child_stdin=True,
close_child_stdout=False,
close_child_stderr=False, virtualenv_py_ver=None,
use_papa=False, **options):
use_papa=False, proc_type=None, **options):
self.name = name
self.use_sockets = use_sockets
self.on_demand = on_demand
Expand Down Expand Up @@ -248,8 +266,14 @@ def __init__(self, name, cmd, args=None, numprocesses=1, warmup_delay=0.,
self.close_child_stdout = close_child_stdout
self.close_child_stderr = close_child_stderr
self.use_papa = use_papa and papa is not None
self.proc_type = proc_type.lower() if proc_type else DEFAULT_PROCESS
self.proc_type = PAPA if self.use_papa else self.proc_type
self.loop = loop or ioloop.IOLoop.instance()

if self.proc_type not in PROCESS_TYPES:
raise ValueError("Watcher: {}, Invalid process type supplied: {}"
.format(self.name, self.proc_type))

if singleton and self.numprocesses not in (0, 1):
raise ValueError("Cannot have %d processes with a singleton "
" watcher" % self.numprocesses)
Expand Down Expand Up @@ -340,7 +364,7 @@ def _redirector_class(self):

@property
def _process_class(self):
return PapaProcessProxy if self.use_papa else Process
return PROCESS_TYPES[self.proc_type]

def _reload_stream(self, key, val):
parts = key.split('.', 1)
Expand Down Expand Up @@ -658,6 +682,7 @@ def spawn_process(self, recovery_wid=None):
# noinspection PyPep8Naming
ProcCls = self._process_class
try:
logger.debug("Cmd: {}".format(cmd))
process = ProcCls(self.name, recovery_wid or self._nextwid,
cmd, args=self.args,
working_dir=self.working_dir,
Expand Down
178 changes: 178 additions & 0 deletions circus/win_process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import time
import signal

import psutil
import wmi

from circus import logger
from circus import process
from circus.util import debuglog


class WinProcessError(OSError):
'''
All windows process/service specific errors must derive from this one.
'''

SUCCESS = 0
START_SERVICE_ECODES = {
SUCCESS: "The request was accepted.",
1: "The request is not supported.",
2: "The user did not have the necessary access.",
3: "The service cannot be stopped because other services that are running are dependent on it.",
4: "The requested control code is not valid, or it is unacceptable to the service.",
5: "The requested control code cannot be sent to the service because the "
"the service is not supporting this request, or user doesn't have necessary access",
6: "The service has not been started.",
7: "The service did not respond to the start request in a timely fashion.",
8: "Unknown failure when starting the service.",
9: "The directory path to the service executable file was not found.",
10: "The service is already running.",
11: "The database to add a new service is locked.",
12: "A dependency this service relies on has been removed from the system.",
13: "The service failed to find the service needed from a dependent service.",
14: "The service has been disabled from the system.",
15: "The service does not have the correct authentication to run on the system.",
16: "This service is being removed from the system.",
17: "The service has no execution thread.",
18: "The service has circular dependencies when it starts.",
19: "A service is running under the same name.",
20: "The service name has invalid characters.",
21: "Invalid parameters have been passed to the service.",
22: "The account under which this service runs is either invalid or lacks the permissions to run the service.",
23: "The service exists in the database of services available from the system.",
24: "The service is currently paused in the system.",
}


STOPPED = "Stopped"
START_PENDING = "Start Pending"
STOP_PENDING = "Stop Pending"
RUNNING = "Running"
CONT_PENDING = "Continue Pending"
PAUSE_PENDING = "Pause Pending"
PAUSED = "Paused"
UNKNOWN = "Unknown"

DISABLED = "Disabled"
MANUAL = "Manual"


class WinService(process.Process):
'''
Wraps windows service.
'''
def __init__(self, *args, **kwargs):
self._server = kwargs.pop("server") if "server" in kwargs else None
self._wmi = self._make_wmi_connection(self._server)
self._service = None

# Must call super last, otherwise spawn may be called before anything
# below __init__ get initialized
super().__init__(*args, **kwargs)

def _make_wmi_connection(self, server):
'''
Create the WMI connection to start the windows process.

@param server - name of the server on which the process needs to be
started
'''
if server:
return wmi.WMI(computer=server)
else:
return wmi.WMI()

@property
def service(self):
'''
This property always returns a fresh copy of the service
'''
service = self._wmi.Win32_Service(displayname=self.cmd)\
or self._wmi.Win32_Service(name=self.cmd)

if not service:
raise WinProcessError("System can't find service: {}".format(self.cmd))
return service[0]

@debuglog
def poll(self):
return None if self.is_alive() else self.service.ExitCode

@debuglog
def is_alive(self):
return self._worker.is_running()

def returncode(self):
return self.poll()

@property
def stdout(self):
"""Return the *stdout* stream"""
return None

@property
def stderr(self):
"""Return the *stdout* stream"""
return None

@debuglog
def stop(self):
'''
Use WMI API to shutdown this service.
'''
if self.is_alive():
self._stop_service()

@debuglog
def send_signal(self, sig):
'''
Overwrite the default behavior to cleanly stop the windows service
using WMI if "sig" is SIGTERM.
'''
if sig == signal.SIGTERM:
logger.debug("sending signal %s to %s" % (sig, self.pid))
self._stop_service()
else:
super().send_signal(sig)

def spawn(self):
'''
Spawn a new instance of this process
'''
self.started = time.time()
if self.service.StartMode == DISABLED:
self._exec_service_command("ChangeStartMode", MANUAL)

self._stop_service()
self._exec_service_command("StartService")
self._worker = psutil.Process(self.service.ProcessID)

def _stop_service(self):
if self.service.State == STOPPED:
return

self.service.StopService()
if self.service.State == STOP_PENDING:
time.sleep(0.05)
if self.service.State != STOPPED:
raise WinProcessError("Unable to stop Windows Service: {}".format(self.cmd))

def _exec_service_command(self, command, *args):
'''
Helper method to execute WMI service commands

@param command - WMI method to execute
@param *args - arguments to the command method
'''
res = getattr(self.service, command)(*args)[0]
if res != SUCCESS:
raise self._make_error(res)

def _make_error(self, ecode):
'''
Helper method to make an exception and populate it with an appropriate
error.
'''
message = START_SERVICE_ECODES.get(ecode, "Unknown error occurred: {}".format(ecode))
return WinProcessError(message)
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import os
import sys
from setuptools import setup, find_packages
from circus import __version__

if not hasattr(sys, 'version_info') or sys.version_info < (2, 6, 0, 'final'):
raise SystemExit("Circus requires Python 2.6 or higher.")

IS_WINDOWS = os.name == 'nt'

install_requires = ['psutil', 'pyzmq>=13.1.0', 'tornado>=3.0', 'six']
if IS_WINDOWS:
install_requires = ['psutil', 'pyzmq>=13.1.0', 'tornado>=3.0', 'six', 'PyMI']
else:
install_requires = ['psutil', 'pyzmq>=13.1.0', 'tornado>=3.0', 'six']

try:
import argparse # NOQA
Expand Down