mongo/buildscripts/resmokelib/powercycle/lib/services.py

255 lines
9.1 KiB
Python

"""Wrapper for OS Service Wrappers."""
import importlib
import os
import sys
import time
from buildscripts.resmokelib.powercycle.lib import execute_cmd
from buildscripts.resmokelib.powercycle.lib.process_control import ProcessControl
_IS_WINDOWS = sys.platform in ["win32", "cygwin"]
def _try_import(module, name=None):
"""Attempt to import a module and add it as a global variable.
If the import fails, then this function doesn't trigger an exception.
"""
try:
module_name = module if not name else name
globals()[module_name] = importlib.import_module(module)
except ImportError:
pass
if _IS_WINDOWS:
# These modules are used on both sides for dumping python stacks.
# These modules are used on the 'server' side.
_try_import("ntsecuritycon")
_try_import("pywintypes")
_try_import("winerror")
_try_import("win32file")
_try_import("win32security")
_try_import("win32service")
_try_import("win32serviceutil")
class WindowsService(object):
"""Windows service control class."""
def __init__(self, name, bin_path, bin_options, db_path):
"""Initialize WindowsService."""
self.name = name
self.bin_name = os.path.basename(bin_path)
self.bin_path = bin_path
self.bin_options = bin_options
self.db_path = db_path
self.start_type = win32service.SERVICE_DEMAND_START
self.pids = []
self._states = {
win32service.SERVICE_CONTINUE_PENDING: "continue pending",
win32service.SERVICE_PAUSE_PENDING: "pause pending",
win32service.SERVICE_PAUSED: "paused",
win32service.SERVICE_RUNNING: "running",
win32service.SERVICE_START_PENDING: "start pending",
win32service.SERVICE_STOPPED: "stopped",
win32service.SERVICE_STOP_PENDING: "stop pending",
}
def create(self):
"""Create service, if not installed. Return (code, output) tuple."""
if self.status() in list(self._states.values()):
return 1, "Service '{}' already installed, status: {}".format(self.name, self.status())
try:
win32serviceutil.InstallService(
pythonClassString="Service.{}".format(self.name),
serviceName=self.name,
displayName=self.name,
startType=self.start_type,
exeName=self.bin_path,
exeArgs=self.bin_options,
)
ret = 0
output = "Service '{}' created".format(self.name)
except pywintypes.error as err:
ret = err.winerror
output = f"{err.args[1]}: {err.args[2]}"
return ret, output
def update(self):
"""Update installed service. Return (code, output) tuple."""
if self.status() not in self._states.values():
return 1, "Service update '{}' status: {}".format(self.name, self.status())
try:
win32serviceutil.ChangeServiceConfig(
pythonClassString="Service.{}".format(self.name),
serviceName=self.name,
displayName=self.name,
startType=self.start_type,
exeName=self.bin_path,
exeArgs=self.bin_options,
)
ret = 0
output = "Service '{}' updated".format(self.name)
except pywintypes.error as err:
ret = err.winerror
output = f"{err.args[1]}: {err.args[2]}"
return ret, output
def delete(self):
"""Delete service. Return (code, output) tuple."""
if self.status() not in self._states.values():
return 1, "Service delete '{}' status: {}".format(self.name, self.status())
try:
win32serviceutil.RemoveService(serviceName=self.name)
ret = 0
output = "Service '{}' deleted".format(self.name)
except pywintypes.error as err:
ret = err.winerror
output = f"{err.args[1]}: {err.args[2]}"
return ret, output
def start(self):
"""Start service. Return (code, output) tuple."""
if self.status() not in self._states.values():
return 1, "Service start '{}' status: {}".format(self.name, self.status())
try:
win32serviceutil.StartService(serviceName=self.name)
ret = 0
output = "Service '{}' started".format(self.name)
except pywintypes.error as err:
ret = err.winerror
output = f"{err.args[1]}: {err.args[2]}"
proc = ProcessControl(name=self.bin_name)
self.pids = proc.get_pids()
return ret, output
def stop(self, timeout):
"""Stop service, waiting for 'timeout' seconds. Return (code, output) tuple."""
self.pids = []
if self.status() not in self._states.values():
return 1, "Service '{}' status: {}".format(self.name, self.status())
try:
win32serviceutil.StopService(serviceName=self.name)
start = time.time()
status = self.status()
while status == "stop pending":
if time.time() - start >= timeout:
ret = 1
output = "Service '{}' status is '{}'".format(self.name, status)
break
time.sleep(3)
status = self.status()
ret = 0
output = "Service '{}' stopped".format(self.name)
except pywintypes.error as err:
ret = err.winerror
output = f"{err.args[1]}: {err.args[2]}"
if ret == winerror.ERROR_BROKEN_PIPE:
# win32serviceutil.StopService() returns a "The pipe has been ended" error message
# (winerror=109) when stopping the "mongod-powercycle-test" service on
# Windows Server 2016 and the underlying mongod process has already exited.
ret = 0
output = f"Assuming service '{self.name}' stopped despite error: {output}"
return ret, output
def status(self):
"""Return state of the service as a string."""
try:
# QueryServiceStatus returns a tuple:
# (scvType, svcState, svcControls, err, svcErr, svcCP, svcWH)
# See https://msdn.microsoft.com/en-us/library/windows/desktop/ms685996(v=vs.85).aspx
scv_type, svc_state, svc_controls, err, svc_err, svc_cp, svc_wh = (
win32serviceutil.QueryServiceStatus(serviceName=self.name)
)
if svc_state in self._states:
return self._states[svc_state]
return "unknown"
except pywintypes.error:
return "not installed"
def get_pids(self):
"""Return list of pids for service."""
return self.pids
class PosixService(object):
"""Service control on POSIX systems.
Simulates service control for background processes which fork themselves,
i.e., mongod with '--fork'.
"""
def __init__(self, name, bin_path, bin_options, db_path):
"""Initialize PosixService."""
self.name = name
self.bin_path = bin_path
self.bin_name = os.path.basename(bin_path)
self.bin_options = bin_options
self.db_path = db_path
self.pids = []
def create(self):
"""Simulate create service. Returns (code, output) tuple."""
return 0, None
def update(self):
"""Simulate update service. Returns (code, output) tuple."""
return 0, None
def delete(self):
"""Simulate delete service. Returns (code, output) tuple."""
return 0, None
def start(self):
"""Start process. Returns (code, output) tuple."""
cmd = "{} {}".format(self.bin_path, self.bin_options)
ret, output = execute_cmd(cmd)
if not ret:
proc = ProcessControl(name=self.bin_name)
self.pids = proc.get_pids()
return ret, output
def stop(self, timeout):
"""Crash the posix process process. Empty "pids" to signal to `status` the process was terminated. Returns (code, output) tuple."""
proc = ProcessControl(name=self.bin_name)
proc.kill()
self.pids = []
return 0, None
def status(self):
"""Return status of service. If "pids" is empty due to a `stop` call, return that the process is stopped. Otherwise only return `stopped` when the lock file is removed."""
if not self.get_pids():
return "stopped"
# Wait for the lock file to be deleted which concludes a clean shutdown.
lock_file = os.path.join(self.db_path, "mongod.lock")
if not os.path.exists(lock_file):
self.pids = []
return "stopped"
try:
if os.stat(lock_file).st_size == 0:
self.pids = []
return "stopped"
except OSError:
# The lock file was probably removed. Instead of being omnipotent with exception
# interpretation, have a follow-up call observe the file does not exist.
return "running"
return "running"
def get_pids(self):
"""Return list of pids for process."""
return self.pids