Skip to content

Commit

Permalink
Add support for windows services
Browse files Browse the repository at this point in the history
Circus can now control windows services local to the machine it is
running on. In order to configure a windows service watcher config now
requires an additional parameter:

proc_type = win_service

The cmd parameter only needs to contain the full name of the service.
  • Loading branch information
Aleksandr Yeganov committed May 13, 2017
1 parent f8d5a84 commit f5c9986
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 2 deletions.
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


if IS_WINDOWS:
from circus.win_process import WinService
from circus.process import DEFAULT_PROCESS, PAPA, WIN_SERVICE
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)

0 comments on commit f5c9986

Please sign in to comment.