Skip to content

Commit 39b2311

Browse files
committed
Overhaul and vastly simplify service registration process.
1 parent 8ecbaa5 commit 39b2311

5 files changed

Lines changed: 62 additions & 177 deletions

File tree

CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ Note that build 228 was the last version supporting Python 2.
88

99
Since build 304:
1010
----------------
11+
* service registration had an overhaul, avoiding a complicated, and ultimately
12+
unnecessary "single globally registered service runner" concept.
13+
Now, when registering a service, the host pythonservice.exe runner will be
14+
copied to `sys.exec_prefix`, along with possibly `pywintypesXX.dll` and run
15+
from there. (#XXXX)
1116

1217
Since build 303:
1318
----------------

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ def finalize_options(self, build_ext):
437437
else:
438438
self.extra_link_args.append("/SUBSYSTEM:CONSOLE")
439439

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

win32/Demos/service/nativePipeTestService.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ class NativeTestPipeService(TestPipeService):
3333
def main():
3434
if len(sys.argv) == 1:
3535
# service must be starting...
36+
print("service is starting...")
37+
print("(execute this script with '--help' if that isn't what you want)")
38+
3639
# for the sake of debugging etc, we use win32traceutil to see
3740
# any unhandled exceptions and print statements.
3841
import win32traceutil
3942

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

4345
servicemanager.Initialize()
4446
servicemanager.PrepareToHostSingle(NativeTestPipeService)

win32/Lib/win32serviceutil.py

Lines changed: 51 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -2,135 +2,65 @@
22
# and for for Python programs which run as services...
33
#
44
# Note that most utility functions here will raise win32api.error's
5-
# (which is == win32service.error, pywintypes.error, etc)
5+
# (which is win32service.error, pywintypes.error, etc)
66
# when things go wrong - eg, not enough permissions to hit the
77
# registry etc.
88

99
import win32service, win32api, win32con, winerror
1010
import sys, pywintypes, os, warnings
1111
import importlib
1212

13+
_d = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else ""
1314
error = RuntimeError
1415

15-
# We need to find a `pythonservice.exe` to register as a service. Now that the
16-
# 90's have passed, we really can't assume that pythonXX.dll/pywintypesXX.dll
17-
# etc are all in SYSTEM32. pythonservice.exe isn't, by default, in a place where
18-
# these DLLs are likely to be found in a service context.
19-
20-
# We could try and have the postinstall script copy the .exe? But I guess the
21-
# number of users is tiny and everyone else doesn't need the extra .exe hanging
22-
# around on the PATH.
23-
# So - just make noise.
24-
noise = """
25-
**** WARNING ****
26-
The executable at "{exe}" is being used as a service.
27-
28-
The service will need to find "{py_dll}" and "{pyw_dll}" in it's environment,
29-
but we were unable to find these in locations we can safely predict.
30-
We searched in:
31-
{dirs}
32-
and found:
33-
{fpaths}
34-
Note that this warning will appear despite any files found in "{exec_prefix}"
35-
as this may not be in the PATH that the service will see.
36-
If the service does fail to run, not finding these files will be the reason.
37-
38-
You should consider copying this executable to the directory where these
39-
DLLs live - "{good}" might be a good place.
40-
****
41-
"""
42-
43-
44-
def LocatePythonServiceExe(exeName=None):
45-
found = _LocatePythonServiceExe(exeName)
46-
where = os.path.dirname(found)
47-
under_d = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else ""
48-
suffix = "%s%s%s.dll" % (
49-
sys.version_info[0],
50-
sys.version_info[1],
51-
under_d,
52-
)
53-
# If someone isn't using python.exe to register pythonservice.exe we assume they know what they
54-
# are doing.
55-
if found == sys.executable:
56-
return found
57-
58-
py_dll = "python{}".format(suffix)
59-
pyw_dll = "pywintypes{}".format(suffix)
60-
system_dir = win32api.GetSystemDirectory()
61-
exec_prefix = sys.exec_prefix
62-
63-
dirs = [system_dir, exec_prefix, where]
64-
fpaths = [os.path.join(p, dll) for dll in [py_dll, pyw_dll] for p in dirs]
65-
fpaths = [f for f in fpaths if os.path.exists(f)]
66-
fpaths = fpaths and fpaths or ["nothing"]
67-
68-
ok = (
69-
os.path.exists(os.path.join(where, py_dll))
70-
or os.path.exists(os.path.join(system_dir, py_dll))
71-
) and (
72-
os.path.exists(os.path.join(where, pyw_dll))
73-
or os.path.exists(os.path.join(system_dir, pyw_dll))
74-
)
75-
if not ok:
76-
print(
77-
noise.format(
78-
exe=found,
79-
good=os.path.dirname(pywintypes.__file__),
80-
py_dll=py_dll,
81-
pyw_dll=pyw_dll,
82-
dirs="\n ".join(dirs),
83-
fpaths="\n ".join(fpaths),
84-
exec_prefix=exec_prefix,
85-
),
86-
file=sys.stderr,
87-
)
88-
89-
return found
90-
91-
92-
def _LocatePythonServiceExe(exeName=None):
93-
if not exeName and hasattr(sys, "frozen"):
94-
# If py2exe etc calls this with no exeName, default is current exe.
16+
# Returns the full path to an executable for hosting a Python service - typically
17+
# 'pythonservice.exe'
18+
# * If you pass a param and it exists as a file, you'll get the abs path back
19+
# * Otherwise we'll use the param instead of 'pythonservice.exe', and we will
20+
# look for it.
21+
def LocatePythonServiceExe(exe=None):
22+
if not exe and hasattr(sys, "frozen"):
23+
# If py2exe etc calls this with no exe, default is current exe,
24+
# and all setup is their problem :)
9525
return sys.executable
9626

97-
# Try and find the specified EXE somewhere. If specifically registered,
98-
# use it. Otherwise look down sys.path, and the global PATH environment.
99-
if exeName is None:
100-
if os.path.splitext(win32service.__file__)[0].endswith("_d"):
101-
exeName = "PythonService_d.exe"
102-
else:
103-
exeName = "PythonService.exe"
104-
# See if it exists as specified
105-
if os.path.isfile(exeName):
106-
return win32api.GetFullPathName(exeName)
107-
baseName = os.path.splitext(os.path.basename(exeName))[0]
108-
try:
109-
exeName = win32api.RegQueryValue(
110-
win32con.HKEY_LOCAL_MACHINE,
111-
"Software\\Python\\%s\\%s" % (baseName, sys.winver),
112-
)
113-
if os.path.isfile(exeName):
114-
return exeName
115-
raise RuntimeError(
116-
"The executable '%s' is registered as the Python "
117-
"service exe, but it does not exist as specified" % exeName
118-
)
119-
except win32api.error:
120-
# OK - not there - lets go a-searchin'
121-
for path in [sys.prefix] + sys.path:
122-
look = os.path.join(path, exeName)
123-
if os.path.isfile(look):
124-
return win32api.GetFullPathName(look)
125-
# Try the global Path.
126-
try:
127-
return win32api.SearchPath(None, exeName)[0]
128-
except win32api.error:
129-
msg = (
130-
"%s is not correctly registered\nPlease locate and run %s, and it will self-register\nThen run this service registration process again."
131-
% (exeName, exeName)
132-
)
133-
raise error(msg)
27+
if exe and os.path.isfile(exe):
28+
return win32api.GetFullPathName(exe)
29+
30+
# We are confused if we aren't now looking for our default. But if that
31+
# exists as specified we assume it's good.
32+
exe = f"pythonservice{_d}.exe"
33+
if os.path.isfile(exe):
34+
return win32api.GetFullPathName(exe)
35+
36+
# Now we are searching for the .exe
37+
# We are going to want it here.
38+
correct = os.path.join(sys.exec_prefix, exe)
39+
# If that doesn't exist, we might find it where pywin32 installed it,
40+
# next to win32service.pyd.
41+
maybe = os.path.join(os.path.dirname(win32service.__file__), exe)
42+
if os.path.exists(maybe):
43+
# Welp, copy it to exec_prefix
44+
print(f"copying host exe '{maybe}' -> '{correct}'")
45+
win32api.CopyFile(maybe, correct)
46+
correct = maybe
47+
48+
if not os.path.exists(correct):
49+
raise error(f"Can't find '{correct}'")
50+
51+
# If pywintypes.dll isn't next to us, or at least next to pythonXX.dll,
52+
# there's a good chance the service will not run. That's usually copied by
53+
# `pywin32_postinstall`, but putting it next to the python DLL seems
54+
# reasonable.
55+
python_dll = win32api.GetModuleFileName(sys.dllhandle)
56+
pyw = f"pywintypes{sys.version_info[0]}{sys.version_info[1]}{_d}.dll"
57+
correct_pyw = os.path.join(os.path.dirname(python_dll), pyw)
58+
59+
if not os.path.exists(correct_pyw):
60+
print(f"copying helper dll '{pywintypes.__file__}' -> '{correct_pyw}'")
61+
win32api.CopyFile(pywintypes.__file__, correct_pyw)
62+
63+
return correct
13464

13565

13666
def _GetServiceShortName(longName):
@@ -176,8 +106,7 @@ def SmartOpenService(hscm, name, access):
176106

177107

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

286-
exeName = '"%s"' % LocatePythonServiceExe(
287-
exeName
288-
) # None here means use default PythonService.exe
215+
exeName = '"%s"' % LocatePythonServiceExe(exeName)
289216
commandLine = _GetCommandLine(exeName, exeArgs)
290217
hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
291218
try:
@@ -320,8 +247,7 @@ def InstallService(
320247
)
321248
except (win32service.error, NotImplementedError):
322249
## delayed start only exists on Vista and later - warn only when trying to set delayed to True
323-
if delayedstart:
324-
warnings.warn("Delayed Start not available on this system")
250+
warnings.warn("Delayed Start not available on this system")
325251
win32service.CloseServiceHandle(hs)
326252
finally:
327253
win32service.CloseServiceHandle(hscm)

win32/src/PythonService.cpp

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ BOOL WINAPI DebugControlHandler(DWORD dwCtrlType);
105105
DWORD WINAPI service_ctrl_ex(DWORD, DWORD, LPVOID, LPVOID);
106106
VOID WINAPI service_ctrl(DWORD);
107107

108-
BOOL RegisterPythonServiceExe(void);
109-
110108
static PY_SERVICE_TABLE_ENTRY *FindPythonServiceEntry(LPCTSTR svcName);
111109

112110
static PyObject *LoadPythonServiceClass(TCHAR *svcInitString);
@@ -1077,12 +1075,6 @@ int PythonService_main(int argc, TCHAR **argv)
10771075
}
10781076
// Process the args
10791077
if ((argc > 1) && ((*argv[1] == '-') || (*argv[1] == '/'))) {
1080-
#ifndef BUILD_FREEZE
1081-
if (_tcsicmp(_T("register"), argv[1] + 1) == 0 || _tcsicmp(_T("install"), argv[1] + 1) == 0) {
1082-
// Get out of here.
1083-
return RegisterPythonServiceExe() ? 0 : 1;
1084-
}
1085-
#endif
10861078
if (_tcsicmp(_T("debug"), argv[1] + 1) == 0) {
10871079
/* Debugging the service. If this EXE has a service name
10881080
embedded in it, use it, otherwise insist one is passed on the
@@ -1122,9 +1114,6 @@ int PythonService_main(int argc, TCHAR **argv)
11221114
// We are not being run by the SCM - print a debug message.
11231115
_tprintf(_T("%s - Python Service Manager\n"), argv[0]);
11241116
printf("Options:\n");
1125-
#ifndef BUILD_FREEZE
1126-
printf(" -register - register the EXE - this should generally not be necessary.\n");
1127-
#endif
11281117
printf(" -debug servicename [parms] - debug the Python service.\n");
11291118
printf("\nNOTE: You do not start the service using this program - start the\n");
11301119
printf("service using Control Panel, or 'net start service_name'\n");
@@ -1133,10 +1122,6 @@ int PythonService_main(int argc, TCHAR **argv)
11331122
// Some other nasty error - log it.
11341123
ReportAPIError(PYS_E_API_CANT_START_SERVICE, errCode);
11351124
printf("Could not start the service - error %d\n", errCode);
1136-
// Just incase the error was caused by this EXE not being registered
1137-
#ifndef BUILD_FREEZE
1138-
RegisterPythonServiceExe();
1139-
#endif
11401125
}
11411126
return 2;
11421127
}
@@ -1285,41 +1270,6 @@ BOOL LocatePythonServiceClassString(TCHAR *svcName, TCHAR *buf, int cchBuf)
12851270
return ok;
12861271
}
12871272

1288-
// Register the EXE.
1289-
// This writes an entry to the Python registry and also
1290-
// to the EventLog so I can stick in messages.
1291-
static BOOL RegisterPythonServiceExe(void)
1292-
{
1293-
printf("Registering the Python Service Manager...\n");
1294-
const int fnameBufSize = MAX_PATH + 1;
1295-
TCHAR fnameBuf[fnameBufSize];
1296-
if (GetModuleFileName(NULL, fnameBuf, fnameBufSize) == 0) {
1297-
printf("Registration failed due to GetModuleFileName() failing (error %d)\n", GetLastError());
1298-
return FALSE;
1299-
}
1300-
assert(Py_IsInitialized());
1301-
CEnterLeavePython _celp;
1302-
// Register this specific EXE against this specific DLL version
1303-
PyObject *obVerString = PySys_GetObject("winver");
1304-
if (obVerString == NULL || !PyBytes_Check(obVerString)) {
1305-
Py_XDECREF(obVerString);
1306-
printf("Registration failed as sys.winver is not available or not a string\n");
1307-
return FALSE;
1308-
}
1309-
char *szVerString = PyBytes_AsString(obVerString);
1310-
Py_DECREF(obVerString);
1311-
// note wsprintf allows %hs to be "char *" even when UNICODE!
1312-
TCHAR keyBuf[256];
1313-
wsprintf(keyBuf, _T("Software\\Python\\PythonService\\%hs"), szVerString);
1314-
DWORD rc;
1315-
if ((rc = RegSetValue(HKEY_LOCAL_MACHINE, keyBuf, REG_SZ, fnameBuf, _tcslen(fnameBuf))) != ERROR_SUCCESS) {
1316-
printf("Registration failed due to RegSetValue() of service EXE - error %d\n", rc);
1317-
return FALSE;
1318-
}
1319-
// don't bother registering in the event log - do it when we write a log entry.
1320-
return TRUE;
1321-
}
1322-
13231273
#endif // PYSERVICE_BUILD_DLL
13241274

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

0 commit comments

Comments
 (0)