|
2 | 2 | # and for for Python programs which run as services... |
3 | 3 | # |
4 | 4 | # 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) |
6 | 6 | # when things go wrong - eg, not enough permissions to hit the |
7 | 7 | # registry etc. |
8 | 8 |
|
9 | 9 | import win32service, win32api, win32con, winerror |
10 | 10 | import sys, pywintypes, os, warnings |
11 | 11 | import importlib |
12 | 12 |
|
| 13 | +_d = "_d" if "_d.pyd" in importlib.machinery.EXTENSION_SUFFIXES else "" |
13 | 14 | error = RuntimeError |
14 | 15 |
|
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 :) |
95 | 25 | return sys.executable |
96 | 26 |
|
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 |
134 | 64 |
|
135 | 65 |
|
136 | 66 | def _GetServiceShortName(longName): |
@@ -176,8 +106,7 @@ def SmartOpenService(hscm, name, access): |
176 | 106 |
|
177 | 107 |
|
178 | 108 | 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. |
181 | 110 | hkey = win32api.RegOpenKey( |
182 | 111 | win32con.HKEY_LOCAL_MACHINE, |
183 | 112 | "SYSTEM\\CurrentControlSet\\Services\\%s" % (serviceName), |
@@ -283,9 +212,7 @@ def InstallService( |
283 | 212 | if errorControl is None: |
284 | 213 | errorControl = win32service.SERVICE_ERROR_NORMAL |
285 | 214 |
|
286 | | - exeName = '"%s"' % LocatePythonServiceExe( |
287 | | - exeName |
288 | | - ) # None here means use default PythonService.exe |
| 215 | + exeName = '"%s"' % LocatePythonServiceExe(exeName) |
289 | 216 | commandLine = _GetCommandLine(exeName, exeArgs) |
290 | 217 | hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS) |
291 | 218 | try: |
@@ -320,8 +247,7 @@ def InstallService( |
320 | 247 | ) |
321 | 248 | except (win32service.error, NotImplementedError): |
322 | 249 | ## 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") |
325 | 251 | win32service.CloseServiceHandle(hs) |
326 | 252 | finally: |
327 | 253 | win32service.CloseServiceHandle(hscm) |
|
0 commit comments