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