• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * venv redirector for Windows
3  *
4  * This launcher looks for a nearby pyvenv.cfg to find the correct home
5  * directory, and then launches the original Python executable from it.
6  * The name of this executable is passed as argv[0].
7  */
8 
9 #define __STDC_WANT_LIB_EXT1__ 1
10 
11 #include <windows.h>
12 #include <pathcch.h>
13 #include <fcntl.h>
14 #include <io.h>
15 #include <shlobj.h>
16 #include <stdio.h>
17 #include <stdbool.h>
18 #include <tchar.h>
19 #include <assert.h>
20 
21 #define MS_WINDOWS
22 #include "patchlevel.h"
23 
24 #define MAXLEN PATHCCH_MAX_CCH
25 #define MSGSIZE 1024
26 
27 #define RC_NO_STD_HANDLES   100
28 #define RC_CREATE_PROCESS   101
29 #define RC_NO_PYTHON        103
30 #define RC_NO_MEMORY        104
31 #define RC_NO_VENV_CFG      106
32 #define RC_BAD_VENV_CFG     107
33 #define RC_NO_COMMANDLINE   108
34 #define RC_INTERNAL_ERROR   109
35 
36 // This should always be defined when we build for real,
37 // but it's handy to have a definition for quick testing
38 #ifndef EXENAME
39 #define EXENAME L"python.exe"
40 #endif
41 
42 #ifndef CFGNAME
43 #define CFGNAME L"pyvenv.cfg"
44 #endif
45 
46 static FILE * log_fp = NULL;
47 
48 void
debug(wchar_t * format,...)49 debug(wchar_t * format, ...)
50 {
51     va_list va;
52 
53     if (log_fp != NULL) {
54         wchar_t buffer[MAXLEN];
55         int r = 0;
56         va_start(va, format);
57         r = vswprintf_s(buffer, MAXLEN, format, va);
58         va_end(va);
59 
60         if (r <= 0) {
61             return;
62         }
63         fwprintf(log_fp, L"%ls\n", buffer);
64         while (r && isspace(buffer[r])) {
65             buffer[r--] = L'\0';
66         }
67         if (buffer[0]) {
68             OutputDebugStringW(buffer);
69         }
70     }
71 }
72 
73 
74 void
formatWinerror(int rc,wchar_t * message,int size)75 formatWinerror(int rc, wchar_t * message, int size)
76 {
77     FormatMessageW(
78         FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
79         NULL, rc, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
80         message, size, NULL);
81 }
82 
83 
84 void
winerror(int err,wchar_t * format,...)85 winerror(int err, wchar_t * format, ... )
86 {
87     va_list va;
88     wchar_t message[MSGSIZE];
89     wchar_t win_message[MSGSIZE];
90     int len;
91 
92     if (err == 0) {
93         err = GetLastError();
94     }
95 
96     va_start(va, format);
97     len = _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va);
98     va_end(va);
99 
100     formatWinerror(err, win_message, MSGSIZE);
101     if (len >= 0) {
102         _snwprintf_s(&message[len], MSGSIZE - len, _TRUNCATE, L": %ls",
103                      win_message);
104     }
105 
106 #if !defined(_WINDOWS)
107     fwprintf(stderr, L"%ls\n", message);
108 #else
109     MessageBoxW(NULL, message, L"Python venv launcher is sorry to say ...",
110                MB_OK);
111 #endif
112 }
113 
114 
115 void
error(wchar_t * format,...)116 error(wchar_t * format, ... )
117 {
118     va_list va;
119     wchar_t message[MSGSIZE];
120 
121     va_start(va, format);
122     _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va);
123     va_end(va);
124 
125 #if !defined(_WINDOWS)
126     fwprintf(stderr, L"%ls\n", message);
127 #else
128     MessageBoxW(NULL, message, L"Python venv launcher is sorry to say ...",
129                MB_OK);
130 #endif
131 }
132 
133 
134 bool
isEnvVarSet(const wchar_t * name)135 isEnvVarSet(const wchar_t *name)
136 {
137     /* only looking for non-empty, which means at least one character
138        and the null terminator */
139     return GetEnvironmentVariableW(name, NULL, 0) >= 2;
140 }
141 
142 
143 bool
join(wchar_t * buffer,size_t bufferLength,const wchar_t * fragment)144 join(wchar_t *buffer, size_t bufferLength, const wchar_t *fragment)
145 {
146     if (SUCCEEDED(PathCchCombineEx(buffer, bufferLength, buffer, fragment, PATHCCH_ALLOW_LONG_PATHS))) {
147         return true;
148     }
149     return false;
150 }
151 
152 
153 bool
split_parent(wchar_t * buffer,size_t bufferLength)154 split_parent(wchar_t *buffer, size_t bufferLength)
155 {
156     return SUCCEEDED(PathCchRemoveFileSpec(buffer, bufferLength));
157 }
158 
159 
160 /*
161  * Path calculation
162  */
163 
164 int
calculate_pyvenvcfg_path(wchar_t * pyvenvcfg_path,size_t maxlen)165 calculate_pyvenvcfg_path(wchar_t *pyvenvcfg_path, size_t maxlen)
166 {
167     if (!pyvenvcfg_path) {
168         error(L"invalid buffer provided");
169         return RC_INTERNAL_ERROR;
170     }
171     if ((DWORD)maxlen != maxlen) {
172         error(L"path buffer is too large");
173         return RC_INTERNAL_ERROR;
174     }
175     if (!GetModuleFileNameW(NULL, pyvenvcfg_path, (DWORD)maxlen)) {
176         winerror(GetLastError(), L"failed to read executable directory");
177         return RC_NO_COMMANDLINE;
178     }
179     // Remove 'python.exe' from our path
180     if (!split_parent(pyvenvcfg_path, maxlen)) {
181         error(L"failed to remove segment from '%ls'", pyvenvcfg_path);
182         return RC_NO_COMMANDLINE;
183     }
184     // Replace with 'pyvenv.cfg'
185     if (!join(pyvenvcfg_path, maxlen, CFGNAME)) {
186         error(L"failed to append '%ls' to '%ls'", CFGNAME, pyvenvcfg_path);
187         return RC_NO_MEMORY;
188     }
189     // If it exists, return
190     if (GetFileAttributesW(pyvenvcfg_path) != INVALID_FILE_ATTRIBUTES) {
191         return 0;
192     }
193     // Otherwise, remove 'pyvenv.cfg' and (probably) 'Scripts'
194     if (!split_parent(pyvenvcfg_path, maxlen) ||
195         !split_parent(pyvenvcfg_path, maxlen)) {
196         error(L"failed to remove segments from '%ls'", pyvenvcfg_path);
197         return RC_NO_COMMANDLINE;
198     }
199     // Replace 'pyvenv.cfg'
200     if (!join(pyvenvcfg_path, maxlen, CFGNAME)) {
201         error(L"failed to append '%ls' to '%ls'", CFGNAME, pyvenvcfg_path);
202         return RC_NO_MEMORY;
203     }
204     // If it exists, return
205     if (GetFileAttributesW(pyvenvcfg_path) != INVALID_FILE_ATTRIBUTES) {
206         return 0;
207     }
208     // Otherwise, we fail
209     winerror(GetLastError(), L"failed to locate %ls", CFGNAME);
210     return RC_NO_VENV_CFG;
211 }
212 
213 
214 /*
215  * pyvenv.cfg parsing
216  */
217 
218 static int
find_home_value(const char * buffer,DWORD maxlen,const char ** start,DWORD * length)219 find_home_value(const char *buffer, DWORD maxlen, const char **start, DWORD *length)
220 {
221     if (!buffer || !start || !length) {
222         error(L"invalid find_home_value parameters()");
223         return 0;
224     }
225     for (const char *s = strstr(buffer, "home");
226          s && (size_t)((ptrdiff_t)s - (ptrdiff_t)buffer) < maxlen;
227          s = strstr(s + 1, "\nhome")
228     ) {
229         if (*s == '\n') {
230             ++s;
231         }
232         for (int i = 4; i > 0 && *s; --i, ++s);
233 
234         while (*s && iswspace(*s)) {
235             ++s;
236         }
237         if (*s != L'=') {
238             continue;
239         }
240 
241         do {
242             ++s;
243         } while (*s && iswspace(*s));
244 
245         *start = s;
246         char *nl = strchr(s, '\n');
247         if (nl) {
248             while (nl != s && iswspace(nl[-1])) {
249                 --nl;
250             }
251             *length = (DWORD)((ptrdiff_t)nl - (ptrdiff_t)s);
252         } else {
253             *length = (DWORD)strlen(s);
254         }
255         return 1;
256     }
257     return 0;
258 }
259 
260 
261 int
read_home(const wchar_t * pyvenv_cfg,wchar_t * home_path,size_t maxlen)262 read_home(const wchar_t *pyvenv_cfg, wchar_t *home_path, size_t maxlen)
263 {
264     HANDLE hFile = CreateFileW(pyvenv_cfg, GENERIC_READ,
265         FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
266         NULL, OPEN_EXISTING, 0, NULL);
267 
268     if (hFile == INVALID_HANDLE_VALUE) {
269         winerror(GetLastError(), L"failed to open '%ls'", pyvenv_cfg);
270         return RC_BAD_VENV_CFG;
271     }
272 
273     // 8192 characters ought to be enough for anyone
274     // (doubled compared to the old implementation!)
275     char buffer[8192];
276     DWORD len;
277     if (!ReadFile(hFile, buffer, sizeof(buffer) - 1, &len, NULL)) {
278         winerror(GetLastError(), L"failed to read '%ls'", pyvenv_cfg);
279         CloseHandle(hFile);
280         return RC_BAD_VENV_CFG;
281     }
282     CloseHandle(hFile);
283     // Ensure null termination
284     buffer[len] = '\0';
285 
286     char *home;
287     DWORD home_len;
288     if (!find_home_value(buffer, sizeof(buffer), &home, &home_len)) {
289         error(L"no home= specified in '%ls'", pyvenv_cfg);
290         return RC_BAD_VENV_CFG;
291     }
292 
293     if ((DWORD)maxlen != maxlen) {
294         maxlen = 8192;
295     }
296     len = MultiByteToWideChar(CP_UTF8, 0, home, home_len, home_path, (DWORD)maxlen);
297     if (!len) {
298         winerror(GetLastError(), L"failed to decode home setting in '%ls'", pyvenv_cfg);
299         return RC_BAD_VENV_CFG;
300     }
301     home_path[len] = L'\0';
302 
303     return 0;
304 }
305 
306 
307 int
locate_python(wchar_t * path,size_t maxlen)308 locate_python(wchar_t *path, size_t maxlen)
309 {
310     if (!join(path, maxlen, EXENAME)) {
311         error(L"failed to append %ls to '%ls'", EXENAME, path);
312         return RC_NO_MEMORY;
313     }
314 
315     if (GetFileAttributesW(path) == INVALID_FILE_ATTRIBUTES) {
316         winerror(GetLastError(), L"did not find executable at '%ls'", path);
317         return RC_NO_PYTHON;
318     }
319 
320     return 0;
321 }
322 
323 
324 int
smuggle_path()325 smuggle_path()
326 {
327     wchar_t buffer[MAXLEN];
328     // We could use argv[0], but that may be wrong in certain rare cases (if the
329     // user is doing something weird like symlinks to venv redirectors), and
330     // what we _really_ want is the directory of the venv. We always copy the
331     // redirectors, so if we've made the venv, this will be correct.
332     DWORD len = GetModuleFileNameW(NULL, buffer, MAXLEN);
333     if (!len) {
334         winerror(GetLastError(), L"Failed to get own executable path");
335         return RC_INTERNAL_ERROR;
336     }
337     buffer[len] = L'\0';
338     debug(L"Setting __PYVENV_LAUNCHER__ = '%s'", buffer);
339 
340     if (!SetEnvironmentVariableW(L"__PYVENV_LAUNCHER__", buffer)) {
341         winerror(GetLastError(), L"Failed to set launcher environment");
342         return RC_INTERNAL_ERROR;
343     }
344 
345     return 0;
346 }
347 
348 /*
349  * Process creation
350  */
351 
352 static BOOL
safe_duplicate_handle(HANDLE in,HANDLE * pout,const wchar_t * name)353 safe_duplicate_handle(HANDLE in, HANDLE * pout, const wchar_t *name)
354 {
355     BOOL ok;
356     HANDLE process = GetCurrentProcess();
357     DWORD rc;
358 
359     *pout = NULL;
360     ok = DuplicateHandle(process, in, process, pout, 0, TRUE,
361                          DUPLICATE_SAME_ACCESS);
362     if (!ok) {
363         rc = GetLastError();
364         if (rc == ERROR_INVALID_HANDLE) {
365             debug(L"DuplicateHandle(%ls) returned ERROR_INVALID_HANDLE\n", name);
366             ok = TRUE;
367         }
368         else {
369             debug(L"DuplicateHandle(%ls) returned %d\n", name, rc);
370         }
371     }
372     return ok;
373 }
374 
375 static BOOL WINAPI
ctrl_c_handler(DWORD code)376 ctrl_c_handler(DWORD code)
377 {
378     return TRUE;    /* We just ignore all control events. */
379 }
380 
381 static int
launch(const wchar_t * executable,wchar_t * cmdline)382 launch(const wchar_t *executable, wchar_t *cmdline)
383 {
384     HANDLE job;
385     JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;
386     DWORD rc;
387     BOOL ok;
388     STARTUPINFOW si;
389     PROCESS_INFORMATION pi;
390 
391 #if defined(_WINDOWS)
392     /*
393     When explorer launches a Windows (GUI) application, it displays
394     the "app starting" (the "pointer + hourglass") cursor for a number
395     of seconds, or until the app does something UI-ish (eg, creating a
396     window, or fetching a message).  As this launcher doesn't do this
397     directly, that cursor remains even after the child process does these
398     things.  We avoid that by doing a simple post+get message.
399     See http://bugs.python.org/issue17290
400     */
401     MSG msg;
402 
403     PostMessage(0, 0, 0, 0);
404     GetMessage(&msg, 0, 0, 0);
405 #endif
406 
407     debug(L"run_child: about to run '%ls' with '%ls'\n", executable, cmdline);
408     job = CreateJobObject(NULL, NULL);
409     ok = QueryInformationJobObject(job, JobObjectExtendedLimitInformation,
410                                    &info, sizeof(info), &rc);
411     if (!ok || (rc != sizeof(info)) || !job) {
412         winerror(GetLastError(), L"Job information querying failed");
413         return RC_CREATE_PROCESS;
414     }
415     info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
416                                              JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
417     ok = SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info,
418                                  sizeof(info));
419     if (!ok) {
420         winerror(GetLastError(), L"Job information setting failed");
421         return RC_CREATE_PROCESS;
422     }
423     memset(&si, 0, sizeof(si));
424     GetStartupInfoW(&si);
425     ok = safe_duplicate_handle(GetStdHandle(STD_INPUT_HANDLE), &si.hStdInput, L"stdin");
426     if (!ok) {
427         return RC_NO_STD_HANDLES;
428     }
429     ok = safe_duplicate_handle(GetStdHandle(STD_OUTPUT_HANDLE), &si.hStdOutput, L"stdout");
430     if (!ok) {
431         return RC_NO_STD_HANDLES;
432     }
433     ok = safe_duplicate_handle(GetStdHandle(STD_ERROR_HANDLE), &si.hStdError, L"stderr");
434     if (!ok) {
435         return RC_NO_STD_HANDLES;
436     }
437 
438     ok = SetConsoleCtrlHandler(ctrl_c_handler, TRUE);
439     if (!ok) {
440         winerror(GetLastError(), L"control handler setting failed");
441         return RC_CREATE_PROCESS;
442     }
443 
444     si.dwFlags = STARTF_USESTDHANDLES;
445     ok = CreateProcessW(executable, cmdline, NULL, NULL, TRUE,
446                         0, NULL, NULL, &si, &pi);
447     if (!ok) {
448         winerror(GetLastError(), L"Unable to create process using '%ls'", cmdline);
449         return RC_CREATE_PROCESS;
450     }
451     AssignProcessToJobObject(job, pi.hProcess);
452     CloseHandle(pi.hThread);
453     WaitForSingleObjectEx(pi.hProcess, INFINITE, FALSE);
454     ok = GetExitCodeProcess(pi.hProcess, &rc);
455     if (!ok) {
456         winerror(GetLastError(), L"Failed to get exit code of process");
457         return RC_CREATE_PROCESS;
458     }
459     debug(L"child process exit code: %d", rc);
460     return rc;
461 }
462 
463 
464 int
process(int argc,wchar_t ** argv)465 process(int argc, wchar_t ** argv)
466 {
467     int exitCode;
468     wchar_t pyvenvcfg_path[MAXLEN];
469     wchar_t home_path[MAXLEN];
470 
471     if (isEnvVarSet(L"PYLAUNCHER_DEBUG")) {
472         setvbuf(stderr, (char *)NULL, _IONBF, 0);
473         log_fp = stderr;
474     }
475 
476     exitCode = calculate_pyvenvcfg_path(pyvenvcfg_path, MAXLEN);
477     if (exitCode) return exitCode;
478 
479     exitCode = read_home(pyvenvcfg_path, home_path, MAXLEN);
480     if (exitCode) return exitCode;
481 
482     exitCode = locate_python(home_path, MAXLEN);
483     if (exitCode) return exitCode;
484 
485     // We do not update argv[0] to point at the target runtime, and so we do not
486     // pass through our original argv[0] in an environment variable.
487     exitCode = smuggle_path();
488     if (exitCode) return exitCode;
489 
490     exitCode = launch(home_path, GetCommandLineW());
491     return exitCode;
492 }
493 
494 
495 #if defined(_WINDOWS)
496 
wWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPWSTR lpstrCmd,int nShow)497 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
498                    LPWSTR lpstrCmd, int nShow)
499 {
500     return process(__argc, __wargv);
501 }
502 
503 #else
504 
wmain(int argc,wchar_t ** argv)505 int cdecl wmain(int argc, wchar_t ** argv)
506 {
507     return process(argc, argv);
508 }
509 
510 #endif
511