Skip to content
Merged
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 CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ or

Coming in build 305, as yet unreleased
--------------------------------------
* service registration had an overhaul, avoiding a complicated, and ultimately
unnecessary "single globally registered service runner" concept.
Now, when registering a service, the host pythonservice.exe runner will be
copied to `sys.exec_prefix`, along with possibly `pywintypesXX.dll` and run
from there. (#1908)

Build 304, released 2022-05-02
------------------------------
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ def finalize_options(self, build_ext):
else:
self.extra_link_args.append("/SUBSYSTEM:CONSOLE")

# pythonservice.exe goes in win32, where it doesn't actually work, but
# win32serviceutil manages to copy it to where it does.
def get_pywin32_dir(self):
return "win32"

Expand Down
6 changes: 4 additions & 2 deletions win32/Demos/service/nativePipeTestService.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ class NativeTestPipeService(TestPipeService):
def main():
if len(sys.argv) == 1:
# service must be starting...
print("service is starting...")
print("(execute this script with '--help' if that isn't what you want)")

# for the sake of debugging etc, we use win32traceutil to see
# any unhandled exceptions and print statements.
import win32traceutil

print("service is starting...")
print("(execute this script with '--help' if that isn't what you want)")
print("service is still starting...")

servicemanager.Initialize()
servicemanager.PrepareToHostSingle(NativeTestPipeService)
Expand Down
176 changes: 51 additions & 125 deletions win32/Lib/win32serviceutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,135 +2,65 @@
# and for for Python programs which run as services...
#
# Note that most utility functions here will raise win32api.error's
# (which is == win32service.error, pywintypes.error, etc)
# (which is win32service.error, pywintypes.error, etc)
# when things go wrong - eg, not enough permissions to hit the
# registry etc.

import win32service, win32api, win32con, winerror
import sys, pywintypes, os, warnings
import importlib

_d = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else ""
error = RuntimeError

# We need to find a `pythonservice.exe` to register as a service. Now that the
# 90's have passed, we really can't assume that pythonXX.dll/pywintypesXX.dll
# etc are all in SYSTEM32. pythonservice.exe isn't, by default, in a place where
# these DLLs are likely to be found in a service context.

# We could try and have the postinstall script copy the .exe? But I guess the
# number of users is tiny and everyone else doesn't need the extra .exe hanging
# around on the PATH.
# So - just make noise.
noise = """
**** WARNING ****
The executable at "{exe}" is being used as a service.

The service will need to find "{py_dll}" and "{pyw_dll}" in it's environment,
but we were unable to find these in locations we can safely predict.
We searched in:
{dirs}
and found:
{fpaths}
Note that this warning will appear despite any files found in "{exec_prefix}"
as this may not be in the PATH that the service will see.
If the service does fail to run, not finding these files will be the reason.

You should consider copying this executable to the directory where these
DLLs live - "{good}" might be a good place.
****
"""


def LocatePythonServiceExe(exeName=None):
found = _LocatePythonServiceExe(exeName)
where = os.path.dirname(found)
under_d = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else ""
suffix = "%s%s%s.dll" % (
sys.version_info[0],
sys.version_info[1],
under_d,
)
# If someone isn't using python.exe to register pythonservice.exe we assume they know what they
# are doing.
if found == sys.executable:
return found

py_dll = "python{}".format(suffix)
pyw_dll = "pywintypes{}".format(suffix)
system_dir = win32api.GetSystemDirectory()
exec_prefix = sys.exec_prefix

dirs = [system_dir, exec_prefix, where]
fpaths = [os.path.join(p, dll) for dll in [py_dll, pyw_dll] for p in dirs]
fpaths = [f for f in fpaths if os.path.exists(f)]
fpaths = fpaths and fpaths or ["nothing"]

ok = (
os.path.exists(os.path.join(where, py_dll))
or os.path.exists(os.path.join(system_dir, py_dll))
) and (
os.path.exists(os.path.join(where, pyw_dll))
or os.path.exists(os.path.join(system_dir, pyw_dll))
)
if not ok:
print(
noise.format(
exe=found,
good=os.path.dirname(pywintypes.__file__),
py_dll=py_dll,
pyw_dll=pyw_dll,
dirs="\n ".join(dirs),
fpaths="\n ".join(fpaths),
exec_prefix=exec_prefix,
),
file=sys.stderr,
)

return found


def _LocatePythonServiceExe(exeName=None):
if not exeName and hasattr(sys, "frozen"):
# If py2exe etc calls this with no exeName, default is current exe.
# Returns the full path to an executable for hosting a Python service - typically
# 'pythonservice.exe'
# * If you pass a param and it exists as a file, you'll get the abs path back
# * Otherwise we'll use the param instead of 'pythonservice.exe', and we will
# look for it.
def LocatePythonServiceExe(exe=None):
if not exe and hasattr(sys, "frozen"):
# If py2exe etc calls this with no exe, default is current exe,
# and all setup is their problem :)
return sys.executable

# Try and find the specified EXE somewhere. If specifically registered,
# use it. Otherwise look down sys.path, and the global PATH environment.
if exeName is None:
if os.path.splitext(win32service.__file__)[0].endswith("_d"):
exeName = "PythonService_d.exe"
else:
exeName = "PythonService.exe"
# See if it exists as specified
if os.path.isfile(exeName):
return win32api.GetFullPathName(exeName)
baseName = os.path.splitext(os.path.basename(exeName))[0]
try:
exeName = win32api.RegQueryValue(
win32con.HKEY_LOCAL_MACHINE,
"Software\\Python\\%s\\%s" % (baseName, sys.winver),
)
if os.path.isfile(exeName):
return exeName
raise RuntimeError(
"The executable '%s' is registered as the Python "
"service exe, but it does not exist as specified" % exeName
)
except win32api.error:
# OK - not there - lets go a-searchin'
for path in [sys.prefix] + sys.path:
look = os.path.join(path, exeName)
if os.path.isfile(look):
return win32api.GetFullPathName(look)
# Try the global Path.
try:
return win32api.SearchPath(None, exeName)[0]
except win32api.error:
msg = (
"%s is not correctly registered\nPlease locate and run %s, and it will self-register\nThen run this service registration process again."
% (exeName, exeName)
)
raise error(msg)
if exe and os.path.isfile(exe):
return win32api.GetFullPathName(exe)

# We are confused if we aren't now looking for our default. But if that
# exists as specified we assume it's good.
exe = f"pythonservice{_d}.exe"
if os.path.isfile(exe):
return win32api.GetFullPathName(exe)

# Now we are searching for the .exe
# We are going to want it here.
correct = os.path.join(sys.exec_prefix, exe)
# If that doesn't exist, we might find it where pywin32 installed it,
# next to win32service.pyd.
maybe = os.path.join(os.path.dirname(win32service.__file__), exe)
if os.path.exists(maybe):
# Welp, copy it to exec_prefix
print(f"copying host exe '{maybe}' -> '{correct}'")
win32api.CopyFile(maybe, correct)
correct = maybe

if not os.path.exists(correct):
raise error(f"Can't find '{correct}'")

# If pywintypes.dll isn't next to us, or at least next to pythonXX.dll,
# there's a good chance the service will not run. That's usually copied by
# `pywin32_postinstall`, but putting it next to the python DLL seems
# reasonable.
python_dll = win32api.GetModuleFileName(sys.dllhandle)
pyw = f"pywintypes{sys.version_info[0]}{sys.version_info[1]}{_d}.dll"
correct_pyw = os.path.join(os.path.dirname(python_dll), pyw)

if not os.path.exists(correct_pyw):
print(f"copying helper dll '{pywintypes.__file__}' -> '{correct_pyw}'")
win32api.CopyFile(pywintypes.__file__, correct_pyw)

return correct


def _GetServiceShortName(longName):
Expand Down Expand Up @@ -176,8 +106,7 @@ def SmartOpenService(hscm, name, access):


def LocateSpecificServiceExe(serviceName):
# Given the name of a specific service, return the .EXE name _it_ uses
# (which may or may not be the Python Service EXE
# Return the .exe name of any service.
hkey = win32api.RegOpenKey(
win32con.HKEY_LOCAL_MACHINE,
"SYSTEM\\CurrentControlSet\\Services\\%s" % (serviceName),
Expand Down Expand Up @@ -283,9 +212,7 @@ def InstallService(
if errorControl is None:
errorControl = win32service.SERVICE_ERROR_NORMAL

exeName = '"%s"' % LocatePythonServiceExe(
exeName
) # None here means use default PythonService.exe
exeName = '"%s"' % LocatePythonServiceExe(exeName)
commandLine = _GetCommandLine(exeName, exeArgs)
hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
try:
Expand Down Expand Up @@ -320,8 +247,7 @@ def InstallService(
)
except (win32service.error, NotImplementedError):
## delayed start only exists on Vista and later - warn only when trying to set delayed to True
if delayedstart:
warnings.warn("Delayed Start not available on this system")
warnings.warn("Delayed Start not available on this system")
win32service.CloseServiceHandle(hs)
finally:
win32service.CloseServiceHandle(hscm)
Expand Down
50 changes: 0 additions & 50 deletions win32/src/PythonService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ BOOL WINAPI DebugControlHandler(DWORD dwCtrlType);
DWORD WINAPI service_ctrl_ex(DWORD, DWORD, LPVOID, LPVOID);
VOID WINAPI service_ctrl(DWORD);

BOOL RegisterPythonServiceExe(void);

static PY_SERVICE_TABLE_ENTRY *FindPythonServiceEntry(LPCTSTR svcName);

static PyObject *LoadPythonServiceClass(TCHAR *svcInitString);
Expand Down Expand Up @@ -1077,12 +1075,6 @@ int PythonService_main(int argc, TCHAR **argv)
}
// Process the args
if ((argc > 1) && ((*argv[1] == '-') || (*argv[1] == '/'))) {
#ifndef BUILD_FREEZE
if (_tcsicmp(_T("register"), argv[1] + 1) == 0 || _tcsicmp(_T("install"), argv[1] + 1) == 0) {
// Get out of here.
return RegisterPythonServiceExe() ? 0 : 1;
}
#endif
if (_tcsicmp(_T("debug"), argv[1] + 1) == 0) {
/* Debugging the service. If this EXE has a service name
embedded in it, use it, otherwise insist one is passed on the
Expand Down Expand Up @@ -1122,9 +1114,6 @@ int PythonService_main(int argc, TCHAR **argv)
// We are not being run by the SCM - print a debug message.
_tprintf(_T("%s - Python Service Manager\n"), argv[0]);
printf("Options:\n");
#ifndef BUILD_FREEZE
printf(" -register - register the EXE - this should generally not be necessary.\n");
#endif
printf(" -debug servicename [parms] - debug the Python service.\n");
printf("\nNOTE: You do not start the service using this program - start the\n");
printf("service using Control Panel, or 'net start service_name'\n");
Expand All @@ -1133,10 +1122,6 @@ int PythonService_main(int argc, TCHAR **argv)
// Some other nasty error - log it.
ReportAPIError(PYS_E_API_CANT_START_SERVICE, errCode);
printf("Could not start the service - error %d\n", errCode);
// Just incase the error was caused by this EXE not being registered
#ifndef BUILD_FREEZE
RegisterPythonServiceExe();
#endif
}
return 2;
}
Expand Down Expand Up @@ -1285,41 +1270,6 @@ BOOL LocatePythonServiceClassString(TCHAR *svcName, TCHAR *buf, int cchBuf)
return ok;
}

// Register the EXE.
// This writes an entry to the Python registry and also
// to the EventLog so I can stick in messages.
static BOOL RegisterPythonServiceExe(void)
{
printf("Registering the Python Service Manager...\n");
const int fnameBufSize = MAX_PATH + 1;
TCHAR fnameBuf[fnameBufSize];
if (GetModuleFileName(NULL, fnameBuf, fnameBufSize) == 0) {
printf("Registration failed due to GetModuleFileName() failing (error %d)\n", GetLastError());
return FALSE;
}
assert(Py_IsInitialized());
CEnterLeavePython _celp;
// Register this specific EXE against this specific DLL version
PyObject *obVerString = PySys_GetObject("winver");
if (obVerString == NULL || !PyBytes_Check(obVerString)) {
Py_XDECREF(obVerString);
printf("Registration failed as sys.winver is not available or not a string\n");
return FALSE;
}
char *szVerString = PyBytes_AsString(obVerString);
Py_DECREF(obVerString);
// note wsprintf allows %hs to be "char *" even when UNICODE!
TCHAR keyBuf[256];
wsprintf(keyBuf, _T("Software\\Python\\PythonService\\%hs"), szVerString);
DWORD rc;
if ((rc = RegSetValue(HKEY_LOCAL_MACHINE, keyBuf, REG_SZ, fnameBuf, _tcslen(fnameBuf))) != ERROR_SUCCESS) {
printf("Registration failed due to RegSetValue() of service EXE - error %d\n", rc);
return FALSE;
}
// don't bother registering in the event log - do it when we write a log entry.
return TRUE;
}

#endif // PYSERVICE_BUILD_DLL

// Code that exists in both EXE and DLL - mainly error handling code.
Expand Down