1 /*
2 * Rewritten Python launcher for Windows
3 *
4 * This new rewrite properly handles PEP 514 and allows any registered Python
5 * runtime to be launched. It also enables auto-install of versions when they
6 * are requested but no installation can be found.
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_BAD_VIRTUAL_PATH 102
30 #define RC_NO_PYTHON 103
31 #define RC_NO_MEMORY 104
32 #define RC_NO_SCRIPT 105
33 #define RC_NO_VENV_CFG 106
34 #define RC_BAD_VENV_CFG 107
35 #define RC_NO_COMMANDLINE 108
36 #define RC_INTERNAL_ERROR 109
37 #define RC_DUPLICATE_ITEM 110
38 #define RC_INSTALLING 111
39 #define RC_NO_PYTHON_AT_ALL 112
40 #define RC_NO_SHEBANG 113
41 #define RC_RECURSIVE_SHEBANG 114
42
43 static FILE * log_fp = NULL;
44
45 void
debug(wchar_t * format,...)46 debug(wchar_t * format, ...)
47 {
48 va_list va;
49
50 if (log_fp != NULL) {
51 wchar_t buffer[MAXLEN];
52 int r = 0;
53 va_start(va, format);
54 r = vswprintf_s(buffer, MAXLEN, format, va);
55 va_end(va);
56
57 if (r <= 0) {
58 return;
59 }
60 fputws(buffer, log_fp);
61 while (r && isspace(buffer[r])) {
62 buffer[r--] = L'\0';
63 }
64 if (buffer[0]) {
65 OutputDebugStringW(buffer);
66 }
67 }
68 }
69
70
71 void
formatWinerror(int rc,wchar_t * message,int size)72 formatWinerror(int rc, wchar_t * message, int size)
73 {
74 FormatMessageW(
75 FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
76 NULL, rc, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
77 message, size, NULL);
78 }
79
80
81 void
winerror(int err,wchar_t * format,...)82 winerror(int err, wchar_t * format, ... )
83 {
84 va_list va;
85 wchar_t message[MSGSIZE];
86 wchar_t win_message[MSGSIZE];
87 int len;
88
89 if (err == 0) {
90 err = GetLastError();
91 }
92
93 va_start(va, format);
94 len = _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va);
95 va_end(va);
96
97 formatWinerror(err, win_message, MSGSIZE);
98 if (len >= 0) {
99 _snwprintf_s(&message[len], MSGSIZE - len, _TRUNCATE, L": %s",
100 win_message);
101 }
102
103 #if !defined(_WINDOWS)
104 fwprintf(stderr, L"%s\n", message);
105 #else
106 MessageBoxW(NULL, message, L"Python Launcher is sorry to say ...",
107 MB_OK);
108 #endif
109 }
110
111
112 void
error(wchar_t * format,...)113 error(wchar_t * format, ... )
114 {
115 va_list va;
116 wchar_t message[MSGSIZE];
117
118 va_start(va, format);
119 _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va);
120 va_end(va);
121
122 #if !defined(_WINDOWS)
123 fwprintf(stderr, L"%s\n", message);
124 #else
125 MessageBoxW(NULL, message, L"Python Launcher is sorry to say ...",
126 MB_OK);
127 #endif
128 }
129
130
131 typedef BOOL (*PIsWow64Process2)(HANDLE, USHORT*, USHORT*);
132
133
134 USHORT
_getNativeMachine(void)135 _getNativeMachine(void)
136 {
137 static USHORT _nativeMachine = IMAGE_FILE_MACHINE_UNKNOWN;
138 if (_nativeMachine == IMAGE_FILE_MACHINE_UNKNOWN) {
139 USHORT processMachine;
140 HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
141 PIsWow64Process2 IsWow64Process2 = kernel32 ?
142 (PIsWow64Process2)GetProcAddress(kernel32, "IsWow64Process2") :
143 NULL;
144 if (!IsWow64Process2) {
145 BOOL wow64Process;
146 if (!IsWow64Process(NULL, &wow64Process)) {
147 winerror(0, L"Checking process type");
148 } else if (wow64Process) {
149 // We should always be a 32-bit executable, so if running
150 // under emulation, it must be a 64-bit host.
151 _nativeMachine = IMAGE_FILE_MACHINE_AMD64;
152 } else {
153 // Not running under emulation, and an old enough OS to not
154 // have IsWow64Process2, so assume it's x86.
155 _nativeMachine = IMAGE_FILE_MACHINE_I386;
156 }
157 } else if (!IsWow64Process2(NULL, &processMachine, &_nativeMachine)) {
158 winerror(0, L"Checking process type");
159 }
160 }
161 return _nativeMachine;
162 }
163
164
165 bool
isAMD64Host(void)166 isAMD64Host(void)
167 {
168 return _getNativeMachine() == IMAGE_FILE_MACHINE_AMD64;
169 }
170
171
172 bool
isARM64Host(void)173 isARM64Host(void)
174 {
175 return _getNativeMachine() == IMAGE_FILE_MACHINE_ARM64;
176 }
177
178
179 bool
isEnvVarSet(const wchar_t * name)180 isEnvVarSet(const wchar_t *name)
181 {
182 /* only looking for non-empty, which means at least one character
183 and the null terminator */
184 return GetEnvironmentVariableW(name, NULL, 0) >= 2;
185 }
186
187
188 bool
join(wchar_t * buffer,size_t bufferLength,const wchar_t * fragment)189 join(wchar_t *buffer, size_t bufferLength, const wchar_t *fragment)
190 {
191 if (SUCCEEDED(PathCchCombineEx(buffer, bufferLength, buffer, fragment, PATHCCH_ALLOW_LONG_PATHS))) {
192 return true;
193 }
194 return false;
195 }
196
197
198 bool
split_parent(wchar_t * buffer,size_t bufferLength)199 split_parent(wchar_t *buffer, size_t bufferLength)
200 {
201 return SUCCEEDED(PathCchRemoveFileSpec(buffer, bufferLength));
202 }
203
204
205 int
_compare(const wchar_t * x,int xLen,const wchar_t * y,int yLen)206 _compare(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
207 {
208 // Empty strings sort first
209 if (!x || !xLen) {
210 return (!y || !yLen) ? 0 : -1;
211 } else if (!y || !yLen) {
212 return 1;
213 }
214 switch (CompareStringEx(
215 LOCALE_NAME_INVARIANT, NORM_IGNORECASE | SORT_DIGITSASNUMBERS,
216 x, xLen, y, yLen,
217 NULL, NULL, 0
218 )) {
219 case CSTR_LESS_THAN:
220 return -1;
221 case CSTR_EQUAL:
222 return 0;
223 case CSTR_GREATER_THAN:
224 return 1;
225 default:
226 winerror(0, L"Error comparing '%.*s' and '%.*s' (compare)", xLen, x, yLen, y);
227 return -1;
228 }
229 }
230
231
232 int
_compareArgument(const wchar_t * x,int xLen,const wchar_t * y,int yLen)233 _compareArgument(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
234 {
235 // Empty strings sort first
236 if (!x || !xLen) {
237 return (!y || !yLen) ? 0 : -1;
238 } else if (!y || !yLen) {
239 return 1;
240 }
241 switch (CompareStringEx(
242 LOCALE_NAME_INVARIANT, 0,
243 x, xLen, y, yLen,
244 NULL, NULL, 0
245 )) {
246 case CSTR_LESS_THAN:
247 return -1;
248 case CSTR_EQUAL:
249 return 0;
250 case CSTR_GREATER_THAN:
251 return 1;
252 default:
253 winerror(0, L"Error comparing '%.*s' and '%.*s' (compareArgument)", xLen, x, yLen, y);
254 return -1;
255 }
256 }
257
258 int
_comparePath(const wchar_t * x,int xLen,const wchar_t * y,int yLen)259 _comparePath(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
260 {
261 // Empty strings sort first
262 if (!x || !xLen) {
263 return !y || !yLen ? 0 : -1;
264 } else if (!y || !yLen) {
265 return 1;
266 }
267 switch (CompareStringOrdinal(x, xLen, y, yLen, TRUE)) {
268 case CSTR_LESS_THAN:
269 return -1;
270 case CSTR_EQUAL:
271 return 0;
272 case CSTR_GREATER_THAN:
273 return 1;
274 default:
275 winerror(0, L"Error comparing '%.*s' and '%.*s' (comparePath)", xLen, x, yLen, y);
276 return -1;
277 }
278 }
279
280
281 bool
_startsWith(const wchar_t * x,int xLen,const wchar_t * y,int yLen)282 _startsWith(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
283 {
284 if (!x || !y) {
285 return false;
286 }
287 yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen;
288 xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen;
289 return xLen >= yLen && 0 == _compare(x, yLen, y, yLen);
290 }
291
292
293 bool
_startsWithArgument(const wchar_t * x,int xLen,const wchar_t * y,int yLen)294 _startsWithArgument(const wchar_t *x, int xLen, const wchar_t *y, int yLen)
295 {
296 if (!x || !y) {
297 return false;
298 }
299 yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen;
300 xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen;
301 return xLen >= yLen && 0 == _compareArgument(x, yLen, y, yLen);
302 }
303
304
305 // Unlike regular startsWith, this function requires that the following
306 // character is either NULL (that is, the entire string matches) or is one of
307 // the characters in 'separators'.
308 bool
_startsWithSeparated(const wchar_t * x,int xLen,const wchar_t * y,int yLen,const wchar_t * separators)309 _startsWithSeparated(const wchar_t *x, int xLen, const wchar_t *y, int yLen, const wchar_t *separators)
310 {
311 if (!x || !y) {
312 return false;
313 }
314 yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen;
315 xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen;
316 if (xLen < yLen) {
317 return false;
318 }
319 if (xLen == yLen) {
320 return 0 == _compare(x, xLen, y, yLen);
321 }
322 return separators &&
323 0 == _compare(x, yLen, y, yLen) &&
324 wcschr(separators, x[yLen]) != NULL;
325 }
326
327
328
329 /******************************************************************************\
330 *** HELP TEXT ***
331 \******************************************************************************/
332
333
334 int
showHelpText(wchar_t ** argv)335 showHelpText(wchar_t ** argv)
336 {
337 // The help text is stored in launcher-usage.txt, which is compiled into
338 // the launcher and loaded at runtime if needed.
339 //
340 // The file must be UTF-8. There are two substitutions:
341 // %ls - PY_VERSION (as wchar_t*)
342 // %ls - argv[0] (as wchar_t*)
343 HRSRC res = FindResourceExW(NULL, L"USAGE", MAKEINTRESOURCE(1), MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL));
344 HGLOBAL resData = res ? LoadResource(NULL, res) : NULL;
345 const char *usage = resData ? (const char*)LockResource(resData) : NULL;
346 if (usage == NULL) {
347 winerror(0, L"Unable to load usage text");
348 return RC_INTERNAL_ERROR;
349 }
350
351 DWORD cbData = SizeofResource(NULL, res);
352 DWORD cchUsage = MultiByteToWideChar(CP_UTF8, 0, usage, cbData, NULL, 0);
353 if (!cchUsage) {
354 winerror(0, L"Unable to preprocess usage text");
355 return RC_INTERNAL_ERROR;
356 }
357
358 cchUsage += 1;
359 wchar_t *wUsage = (wchar_t*)malloc(cchUsage * sizeof(wchar_t));
360 cchUsage = MultiByteToWideChar(CP_UTF8, 0, usage, cbData, wUsage, cchUsage);
361 if (!cchUsage) {
362 winerror(0, L"Unable to preprocess usage text");
363 free((void *)wUsage);
364 return RC_INTERNAL_ERROR;
365 }
366 // Ensure null termination
367 wUsage[cchUsage] = L'\0';
368
369 fwprintf(stdout, wUsage, (L"" PY_VERSION), argv[0]);
370 fflush(stdout);
371
372 free((void *)wUsage);
373
374 return 0;
375 }
376
377
378 /******************************************************************************\
379 *** SEARCH INFO ***
380 \******************************************************************************/
381
382
383 struct _SearchInfoBuffer {
384 struct _SearchInfoBuffer *next;
385 wchar_t buffer[0];
386 };
387
388
389 typedef struct {
390 // the original string, managed by the OS
391 const wchar_t *originalCmdLine;
392 // pointer into the cmdline to mark what we've consumed
393 const wchar_t *restOfCmdLine;
394 // if known/discovered, the full executable path of our runtime
395 const wchar_t *executablePath;
396 // pointer and length into cmdline for the file to check for a
397 // shebang line, if any. Length can be -1 if the string is null
398 // terminated.
399 const wchar_t *scriptFile;
400 int scriptFileLength;
401 // pointer and length into cmdline or a static string with the
402 // name of the target executable. Length can be -1 if the string
403 // is null terminated.
404 const wchar_t *executable;
405 int executableLength;
406 // pointer and length into a string with additional interpreter
407 // arguments to include before restOfCmdLine. Length can be -1 if
408 // the string is null terminated.
409 const wchar_t *executableArgs;
410 int executableArgsLength;
411 // pointer and length into cmdline or a static string with the
412 // company name for PEP 514 lookup. Length can be -1 if the string
413 // is null terminated.
414 const wchar_t *company;
415 int companyLength;
416 // pointer and length into cmdline or a static string with the
417 // tag for PEP 514 lookup. Length can be -1 if the string is
418 // null terminated.
419 const wchar_t *tag;
420 int tagLength;
421 // if true, treats 'tag' as a non-PEP 514 filter
422 bool oldStyleTag;
423 // if true, ignores 'tag' when a high priority environment is found
424 // gh-92817: This is currently set when a tag is read from configuration,
425 // the environment, or a shebang, rather than the command line, and the
426 // only currently possible high priority environment is an active virtual
427 // environment
428 bool lowPriorityTag;
429 // if true, allow PEP 514 lookup to override 'executable'
430 bool allowExecutableOverride;
431 // if true, allow a nearby pyvenv.cfg to locate the executable
432 bool allowPyvenvCfg;
433 // if true, allow defaults (env/py.ini) to clarify/override tags
434 bool allowDefaults;
435 // if true, prefer windowed (console-less) executable
436 bool windowed;
437 // if true, only list detected runtimes without launching
438 bool list;
439 // if true, only list detected runtimes with paths without launching
440 bool listPaths;
441 // if true, display help message before continuing
442 bool help;
443 // if set, limits search to registry keys with the specified Company
444 // This is intended for debugging and testing only
445 const wchar_t *limitToCompany;
446 // dynamically allocated buffers to free later
447 struct _SearchInfoBuffer *_buffer;
448 } SearchInfo;
449
450
451 wchar_t *
allocSearchInfoBuffer(SearchInfo * search,int wcharCount)452 allocSearchInfoBuffer(SearchInfo *search, int wcharCount)
453 {
454 struct _SearchInfoBuffer *buffer = (struct _SearchInfoBuffer*)malloc(
455 sizeof(struct _SearchInfoBuffer) +
456 wcharCount * sizeof(wchar_t)
457 );
458 if (!buffer) {
459 return NULL;
460 }
461 buffer->next = search->_buffer;
462 search->_buffer = buffer;
463 return buffer->buffer;
464 }
465
466
467 void
freeSearchInfo(SearchInfo * search)468 freeSearchInfo(SearchInfo *search)
469 {
470 struct _SearchInfoBuffer *b = search->_buffer;
471 search->_buffer = NULL;
472 while (b) {
473 struct _SearchInfoBuffer *nextB = b->next;
474 free((void *)b);
475 b = nextB;
476 }
477 }
478
479
480 void
_debugStringAndLength(const wchar_t * s,int len,const wchar_t * name)481 _debugStringAndLength(const wchar_t *s, int len, const wchar_t *name)
482 {
483 if (!s) {
484 debug(L"%s: (null)\n", name);
485 } else if (len == 0) {
486 debug(L"%s: (empty)\n", name);
487 } else if (len < 0) {
488 debug(L"%s: %s\n", name, s);
489 } else {
490 debug(L"%s: %.*ls\n", name, len, s);
491 }
492 }
493
494
495 void
dumpSearchInfo(SearchInfo * search)496 dumpSearchInfo(SearchInfo *search)
497 {
498 if (!log_fp) {
499 return;
500 }
501
502 #ifdef __clang__
503 #define DEBUGNAME(s) L # s
504 #else
505 #define DEBUGNAME(s) # s
506 #endif
507 #define DEBUG(s) debug(L"SearchInfo." DEBUGNAME(s) L": %s\n", (search->s) ? (search->s) : L"(null)")
508 #define DEBUG_2(s, sl) _debugStringAndLength((search->s), (search->sl), L"SearchInfo." DEBUGNAME(s))
509 #define DEBUG_BOOL(s) debug(L"SearchInfo." DEBUGNAME(s) L": %s\n", (search->s) ? L"True" : L"False")
510 DEBUG(originalCmdLine);
511 DEBUG(restOfCmdLine);
512 DEBUG(executablePath);
513 DEBUG_2(scriptFile, scriptFileLength);
514 DEBUG_2(executable, executableLength);
515 DEBUG_2(executableArgs, executableArgsLength);
516 DEBUG_2(company, companyLength);
517 DEBUG_2(tag, tagLength);
518 DEBUG_BOOL(oldStyleTag);
519 DEBUG_BOOL(lowPriorityTag);
520 DEBUG_BOOL(allowDefaults);
521 DEBUG_BOOL(allowExecutableOverride);
522 DEBUG_BOOL(windowed);
523 DEBUG_BOOL(list);
524 DEBUG_BOOL(listPaths);
525 DEBUG_BOOL(help);
526 DEBUG(limitToCompany);
527 #undef DEBUG_BOOL
528 #undef DEBUG_2
529 #undef DEBUG
530 #undef DEBUGNAME
531 }
532
533
534 int
findArgv0Length(const wchar_t * buffer,int bufferLength)535 findArgv0Length(const wchar_t *buffer, int bufferLength)
536 {
537 // Note: this implements semantics that are only valid for argv0.
538 // Specifically, there is no escaping of quotes, and quotes within
539 // the argument have no effect. A quoted argv0 must start and end
540 // with a double quote character; otherwise, it ends at the first
541 // ' ' or '\t'.
542 int quoted = buffer[0] == L'"';
543 for (int i = 1; bufferLength < 0 || i < bufferLength; ++i) {
544 switch (buffer[i]) {
545 case L'\0':
546 return i;
547 case L' ':
548 case L'\t':
549 if (!quoted) {
550 return i;
551 }
552 break;
553 case L'"':
554 if (quoted) {
555 return i + 1;
556 }
557 break;
558 }
559 }
560 return bufferLength;
561 }
562
563
564 const wchar_t *
findArgv0End(const wchar_t * buffer,int bufferLength)565 findArgv0End(const wchar_t *buffer, int bufferLength)
566 {
567 return &buffer[findArgv0Length(buffer, bufferLength)];
568 }
569
570
571 /******************************************************************************\
572 *** COMMAND-LINE PARSING ***
573 \******************************************************************************/
574
575 // Adapted from https://stackoverflow.com/a/65583702
576 typedef struct AppExecLinkFile { // For tag IO_REPARSE_TAG_APPEXECLINK
577 DWORD reparseTag;
578 WORD reparseDataLength;
579 WORD reserved;
580 ULONG version;
581 wchar_t stringList[MAX_PATH * 4]; // Multistring (Consecutive UTF-16 strings each ending with a NUL)
582 /* There are normally 4 strings here. Ex:
583 Package ID: L"Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"
584 Entry Point: L"Microsoft.DesktopAppInstaller_8wekyb3d8bbwe!PythonRedirector"
585 Executable: L"C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.17.106910_x64__8wekyb3d8bbwe\AppInstallerPythonRedirector.exe"
586 Applic. Type: L"0" // Integer as ASCII. "0" = Desktop bridge application; Else sandboxed UWP application
587 */
588 } AppExecLinkFile;
589
590
591 int
parseCommandLine(SearchInfo * search)592 parseCommandLine(SearchInfo *search)
593 {
594 if (!search || !search->originalCmdLine) {
595 return RC_NO_COMMANDLINE;
596 }
597
598 const wchar_t *argv0End = findArgv0End(search->originalCmdLine, -1);
599 const wchar_t *tail = argv0End; // will be start of the executable name
600 const wchar_t *end = argv0End; // will be end of the executable name
601 search->restOfCmdLine = argv0End; // will be first space after argv0
602 while (--tail != search->originalCmdLine) {
603 if (*tail == L'"' && end == argv0End) {
604 // Move the "end" up to the quote, so we also allow moving for
605 // a period later on.
606 end = argv0End = tail;
607 } else if (*tail == L'.' && end == argv0End) {
608 end = tail;
609 } else if (*tail == L'\\' || *tail == L'/') {
610 ++tail;
611 break;
612 }
613 }
614 if (tail == search->originalCmdLine && tail[0] == L'"') {
615 ++tail;
616 }
617 // Without special cases, we can now fill in the search struct
618 int tailLen = (int)(end ? (end - tail) : wcsnlen_s(tail, MAXLEN));
619 search->executableLength = -1;
620
621 // Our special cases are as follows
622 #define MATCHES(s) (0 == _comparePath(tail, tailLen, (s), -1))
623 #define STARTSWITH(s) _startsWith(tail, tailLen, (s), -1)
624 if (MATCHES(L"py")) {
625 search->executable = L"python.exe";
626 search->allowExecutableOverride = true;
627 search->allowDefaults = true;
628 } else if (MATCHES(L"pyw")) {
629 search->executable = L"pythonw.exe";
630 search->allowExecutableOverride = true;
631 search->allowDefaults = true;
632 search->windowed = true;
633 } else if (MATCHES(L"py_d")) {
634 search->executable = L"python_d.exe";
635 search->allowExecutableOverride = true;
636 search->allowDefaults = true;
637 } else if (MATCHES(L"pyw_d")) {
638 search->executable = L"pythonw_d.exe";
639 search->allowExecutableOverride = true;
640 search->allowDefaults = true;
641 search->windowed = true;
642 } else if (STARTSWITH(L"python3")) {
643 search->executable = L"python.exe";
644 search->tag = &tail[6];
645 search->tagLength = tailLen - 6;
646 search->allowExecutableOverride = true;
647 search->oldStyleTag = true;
648 search->allowPyvenvCfg = true;
649 } else if (STARTSWITH(L"pythonw3")) {
650 search->executable = L"pythonw.exe";
651 search->tag = &tail[7];
652 search->tagLength = tailLen - 7;
653 search->allowExecutableOverride = true;
654 search->oldStyleTag = true;
655 search->allowPyvenvCfg = true;
656 search->windowed = true;
657 } else {
658 search->executable = tail;
659 search->executableLength = tailLen;
660 search->allowPyvenvCfg = true;
661 }
662 #undef STARTSWITH
663 #undef MATCHES
664
665 // First argument might be one of our options. If so, consume it,
666 // update flags and then set restOfCmdLine.
667 const wchar_t *arg = search->restOfCmdLine;
668 while(*arg && isspace(*arg)) { ++arg; }
669 #define MATCHES(s) (0 == _compareArgument(arg, argLen, (s), -1))
670 #define STARTSWITH(s) _startsWithArgument(arg, argLen, (s), -1)
671 if (*arg && *arg == L'-' && *++arg) {
672 tail = arg;
673 while (*tail && !isspace(*tail)) { ++tail; }
674 int argLen = (int)(tail - arg);
675 if (argLen > 0) {
676 if (STARTSWITH(L"2") || STARTSWITH(L"3")) {
677 // All arguments starting with 2 or 3 are assumed to be version tags
678 search->tag = arg;
679 search->tagLength = argLen;
680 search->oldStyleTag = true;
681 search->restOfCmdLine = tail;
682 } else if (STARTSWITH(L"V:") || STARTSWITH(L"-version:")) {
683 // Arguments starting with 'V:' specify company and/or tag
684 const wchar_t *argStart = wcschr(arg, L':') + 1;
685 const wchar_t *tagStart = wcschr(argStart, L'/') ;
686 if (tagStart) {
687 search->company = argStart;
688 search->companyLength = (int)(tagStart - argStart);
689 search->tag = tagStart + 1;
690 } else {
691 search->tag = argStart;
692 }
693 search->tagLength = (int)(tail - search->tag);
694 search->allowDefaults = false;
695 search->restOfCmdLine = tail;
696 } else if (MATCHES(L"0") || MATCHES(L"-list")) {
697 search->list = true;
698 search->restOfCmdLine = tail;
699 } else if (MATCHES(L"0p") || MATCHES(L"-list-paths")) {
700 search->listPaths = true;
701 search->restOfCmdLine = tail;
702 } else if (MATCHES(L"h") || MATCHES(L"-help")) {
703 search->help = true;
704 // Do not update restOfCmdLine so that we trigger the help
705 // message from whichever interpreter we select
706 }
707 }
708 }
709 #undef STARTSWITH
710 #undef MATCHES
711
712 // Might have a script filename. If it looks like a filename, add
713 // it to the SearchInfo struct for later reference.
714 arg = search->restOfCmdLine;
715 while(*arg && isspace(*arg)) { ++arg; }
716 if (*arg && *arg != L'-') {
717 search->scriptFile = arg;
718 if (*arg == L'"') {
719 ++search->scriptFile;
720 while (*++arg && *arg != L'"') { }
721 } else {
722 while (*arg && !isspace(*arg)) { ++arg; }
723 }
724 search->scriptFileLength = (int)(arg - search->scriptFile);
725 }
726
727 return 0;
728 }
729
730
731 int
_decodeShebang(SearchInfo * search,const char * buffer,int bufferLength,bool onlyUtf8,wchar_t ** decoded,int * decodedLength)732 _decodeShebang(SearchInfo *search, const char *buffer, int bufferLength, bool onlyUtf8, wchar_t **decoded, int *decodedLength)
733 {
734 DWORD cp = CP_UTF8;
735 int wideLen = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, buffer, bufferLength, NULL, 0);
736 if (!wideLen) {
737 cp = CP_ACP;
738 wideLen = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, buffer, bufferLength, NULL, 0);
739 if (!wideLen) {
740 debug(L"# Failed to decode shebang line (0x%08X)\n", GetLastError());
741 return RC_BAD_VIRTUAL_PATH;
742 }
743 }
744 wchar_t *b = allocSearchInfoBuffer(search, wideLen + 1);
745 if (!b) {
746 return RC_NO_MEMORY;
747 }
748 wideLen = MultiByteToWideChar(cp, 0, buffer, bufferLength, b, wideLen + 1);
749 if (!wideLen) {
750 debug(L"# Failed to decode shebang line (0x%08X)\n", GetLastError());
751 return RC_BAD_VIRTUAL_PATH;
752 }
753 b[wideLen] = L'\0';
754 *decoded = b;
755 *decodedLength = wideLen;
756 return 0;
757 }
758
759
760 bool
_shebangStartsWith(const wchar_t * buffer,int bufferLength,const wchar_t * prefix,const wchar_t ** rest,int * firstArgumentLength)761 _shebangStartsWith(const wchar_t *buffer, int bufferLength, const wchar_t *prefix, const wchar_t **rest, int *firstArgumentLength)
762 {
763 int prefixLength = (int)wcsnlen_s(prefix, MAXLEN);
764 if (bufferLength < prefixLength || !_startsWithArgument(buffer, bufferLength, prefix, prefixLength)) {
765 return false;
766 }
767 if (rest) {
768 *rest = &buffer[prefixLength];
769 }
770 if (firstArgumentLength) {
771 int i = prefixLength;
772 while (i < bufferLength && !isspace(buffer[i])) {
773 i += 1;
774 }
775 *firstArgumentLength = i - prefixLength;
776 }
777 return true;
778 }
779
780
781 int
ensure_no_redirector_stub(wchar_t * filename,wchar_t * buffer)782 ensure_no_redirector_stub(wchar_t* filename, wchar_t* buffer)
783 {
784 // Make sure we didn't find a reparse point that will open the Microsoft Store
785 // If we did, pretend there was no shebang and let normal handling take over
786 WIN32_FIND_DATAW findData;
787 HANDLE hFind = FindFirstFileW(buffer, &findData);
788 if (!hFind) {
789 // Let normal handling take over
790 debug(L"# Did not find %s on PATH\n", filename);
791 return RC_NO_SHEBANG;
792 }
793
794 FindClose(hFind);
795
796 if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT &&
797 findData.dwReserved0 & IO_REPARSE_TAG_APPEXECLINK)) {
798 return 0;
799 }
800
801 HANDLE hReparsePoint = CreateFileW(buffer, 0, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, NULL);
802 if (!hReparsePoint) {
803 // Let normal handling take over
804 debug(L"# Did not find %s on PATH\n", filename);
805 return RC_NO_SHEBANG;
806 }
807
808 AppExecLinkFile appExecLink;
809
810 if (!DeviceIoControl(hReparsePoint, FSCTL_GET_REPARSE_POINT, NULL, 0, &appExecLink, sizeof(appExecLink), NULL, NULL)) {
811 // Let normal handling take over
812 debug(L"# Did not find %s on PATH\n", filename);
813 CloseHandle(hReparsePoint);
814 return RC_NO_SHEBANG;
815 }
816
817 CloseHandle(hReparsePoint);
818
819 const wchar_t* redirectorPackageId = L"Microsoft.DesktopAppInstaller_8wekyb3d8bbwe";
820
821 if (0 == wcscmp(appExecLink.stringList, redirectorPackageId)) {
822 debug(L"# ignoring redirector that would launch store\n");
823 return RC_NO_SHEBANG;
824 }
825
826 return 0;
827 }
828
829
830 int
searchPath(SearchInfo * search,const wchar_t * shebang,int shebangLength)831 searchPath(SearchInfo *search, const wchar_t *shebang, int shebangLength)
832 {
833 if (isEnvVarSet(L"PYLAUNCHER_NO_SEARCH_PATH")) {
834 return RC_NO_SHEBANG;
835 }
836
837 wchar_t *command;
838 int commandLength;
839 if (!_shebangStartsWith(shebang, shebangLength, L"/usr/bin/env ", &command, &commandLength)) {
840 return RC_NO_SHEBANG;
841 }
842
843 if (!commandLength || commandLength == MAXLEN) {
844 return RC_BAD_VIRTUAL_PATH;
845 }
846
847 int lastDot = commandLength;
848 while (lastDot > 0 && command[lastDot] != L'.') {
849 lastDot -= 1;
850 }
851 if (!lastDot) {
852 lastDot = commandLength;
853 }
854
855 wchar_t filename[MAXLEN];
856 if (wcsncpy_s(filename, MAXLEN, command, commandLength)) {
857 return RC_BAD_VIRTUAL_PATH;
858 }
859
860 const wchar_t *ext = L".exe";
861 // If the command already has an extension, we do not want to add it again
862 if (!lastDot || _comparePath(&filename[lastDot], -1, ext, -1)) {
863 if (wcscat_s(filename, MAXLEN, L".exe")) {
864 return RC_BAD_VIRTUAL_PATH;
865 }
866 }
867
868 debug(L"# Search PATH for %s\n", filename);
869
870 wchar_t pathVariable[MAXLEN];
871 int n = GetEnvironmentVariableW(L"PATH", pathVariable, MAXLEN);
872 if (!n) {
873 if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
874 return RC_NO_SHEBANG;
875 }
876 winerror(0, L"Failed to read PATH\n", filename);
877 return RC_INTERNAL_ERROR;
878 }
879
880 wchar_t buffer[MAXLEN];
881 n = SearchPathW(pathVariable, filename, NULL, MAXLEN, buffer, NULL);
882 if (!n) {
883 if (GetLastError() == ERROR_FILE_NOT_FOUND) {
884 debug(L"# Did not find %s on PATH\n", filename);
885 // If we didn't find it on PATH, let normal handling take over
886 return RC_NO_SHEBANG;
887 }
888 // Other errors should cause us to break
889 winerror(0, L"Failed to find %s on PATH\n", filename);
890 return RC_BAD_VIRTUAL_PATH;
891 }
892
893 int result = ensure_no_redirector_stub(filename, buffer);
894 if (result) {
895 return result;
896 }
897
898 // Check that we aren't going to call ourselves again
899 // If we are, pretend there was no shebang and let normal handling take over
900 if (GetModuleFileNameW(NULL, filename, MAXLEN) &&
901 0 == _comparePath(filename, -1, buffer, -1)) {
902 debug(L"# ignoring recursive shebang command\n");
903 return RC_RECURSIVE_SHEBANG;
904 }
905
906 wchar_t *buf = allocSearchInfoBuffer(search, n + 1);
907 if (!buf || wcscpy_s(buf, n + 1, buffer)) {
908 return RC_NO_MEMORY;
909 }
910
911 search->executablePath = buf;
912 search->executableArgs = &command[commandLength];
913 search->executableArgsLength = shebangLength - commandLength;
914 debug(L"# Found %s on PATH\n", buf);
915
916 return 0;
917 }
918
919
920 int
_readIni(const wchar_t * section,const wchar_t * settingName,wchar_t * buffer,int bufferLength)921 _readIni(const wchar_t *section, const wchar_t *settingName, wchar_t *buffer, int bufferLength)
922 {
923 wchar_t iniPath[MAXLEN];
924 int n;
925 if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, iniPath)) &&
926 join(iniPath, MAXLEN, L"py.ini")) {
927 debug(L"# Reading from %s for %s/%s\n", iniPath, section, settingName);
928 n = GetPrivateProfileStringW(section, settingName, NULL, buffer, bufferLength, iniPath);
929 if (n) {
930 debug(L"# Found %s in %s\n", settingName, iniPath);
931 return n;
932 } else if (GetLastError() == ERROR_FILE_NOT_FOUND) {
933 debug(L"# Did not find file %s\n", iniPath);
934 } else {
935 winerror(0, L"Failed to read from %s\n", iniPath);
936 }
937 }
938 if (GetModuleFileNameW(NULL, iniPath, MAXLEN) &&
939 SUCCEEDED(PathCchRemoveFileSpec(iniPath, MAXLEN)) &&
940 join(iniPath, MAXLEN, L"py.ini")) {
941 debug(L"# Reading from %s for %s/%s\n", iniPath, section, settingName);
942 n = GetPrivateProfileStringW(section, settingName, NULL, buffer, MAXLEN, iniPath);
943 if (n) {
944 debug(L"# Found %s in %s\n", settingName, iniPath);
945 return n;
946 } else if (GetLastError() == ERROR_FILE_NOT_FOUND) {
947 debug(L"# Did not find file %s\n", iniPath);
948 } else {
949 winerror(0, L"Failed to read from %s\n", iniPath);
950 }
951 }
952 return 0;
953 }
954
955
956 bool
_findCommand(SearchInfo * search,const wchar_t * command,int commandLength)957 _findCommand(SearchInfo *search, const wchar_t *command, int commandLength)
958 {
959 wchar_t commandBuffer[MAXLEN];
960 wchar_t buffer[MAXLEN];
961 wcsncpy_s(commandBuffer, MAXLEN, command, commandLength);
962 int n = _readIni(L"commands", commandBuffer, buffer, MAXLEN);
963 if (!n) {
964 return false;
965 }
966 wchar_t *path = allocSearchInfoBuffer(search, n + 1);
967 if (!path) {
968 return false;
969 }
970 wcscpy_s(path, n + 1, buffer);
971 search->executablePath = path;
972 return true;
973 }
974
975
976 int
_useShebangAsExecutable(SearchInfo * search,const wchar_t * shebang,int shebangLength)977 _useShebangAsExecutable(SearchInfo *search, const wchar_t *shebang, int shebangLength)
978 {
979 wchar_t buffer[MAXLEN];
980 wchar_t script[MAXLEN];
981 wchar_t command[MAXLEN];
982
983 int commandLength = 0;
984 int inQuote = 0;
985
986 if (!shebang || !shebangLength) {
987 return 0;
988 }
989
990 wchar_t *pC = command;
991 for (int i = 0; i < shebangLength; ++i) {
992 wchar_t c = shebang[i];
993 if (isspace(c) && !inQuote) {
994 commandLength = i;
995 break;
996 } else if (c == L'"') {
997 inQuote = !inQuote;
998 } else if (c == L'/' || c == L'\\') {
999 *pC++ = L'\\';
1000 } else {
1001 *pC++ = c;
1002 }
1003 }
1004 *pC = L'\0';
1005
1006 if (!GetCurrentDirectoryW(MAXLEN, buffer) ||
1007 wcsncpy_s(script, MAXLEN, search->scriptFile, search->scriptFileLength) ||
1008 FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, script,
1009 PATHCCH_ALLOW_LONG_PATHS)) ||
1010 FAILED(PathCchRemoveFileSpec(buffer, MAXLEN)) ||
1011 FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, command,
1012 PATHCCH_ALLOW_LONG_PATHS))
1013 ) {
1014 return RC_NO_MEMORY;
1015 }
1016
1017 int n = (int)wcsnlen(buffer, MAXLEN);
1018 wchar_t *path = allocSearchInfoBuffer(search, n + 1);
1019 if (!path) {
1020 return RC_NO_MEMORY;
1021 }
1022 wcscpy_s(path, n + 1, buffer);
1023 search->executablePath = path;
1024 if (commandLength) {
1025 search->executableArgs = &shebang[commandLength];
1026 search->executableArgsLength = shebangLength - commandLength;
1027 }
1028 return 0;
1029 }
1030
1031
1032 int
checkShebang(SearchInfo * search)1033 checkShebang(SearchInfo *search)
1034 {
1035 // Do not check shebang if a tag was provided or if no script file
1036 // was found on the command line.
1037 if (search->tag || !search->scriptFile) {
1038 return 0;
1039 }
1040
1041 if (search->scriptFileLength < 0) {
1042 search->scriptFileLength = (int)wcsnlen_s(search->scriptFile, MAXLEN);
1043 }
1044
1045 wchar_t *scriptFile = (wchar_t*)malloc(sizeof(wchar_t) * (search->scriptFileLength + 1));
1046 if (!scriptFile) {
1047 return RC_NO_MEMORY;
1048 }
1049
1050 wcsncpy_s(scriptFile, search->scriptFileLength + 1,
1051 search->scriptFile, search->scriptFileLength);
1052
1053 HANDLE hFile = CreateFileW(scriptFile, GENERIC_READ,
1054 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
1055 NULL, OPEN_EXISTING, 0, NULL);
1056
1057 if (hFile == INVALID_HANDLE_VALUE) {
1058 debug(L"# Failed to open %s for shebang parsing (0x%08X)\n",
1059 scriptFile, GetLastError());
1060 free(scriptFile);
1061 return 0;
1062 }
1063
1064 DWORD bytesRead = 0;
1065 char buffer[4096];
1066 if (!ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL)) {
1067 debug(L"# Failed to read %s for shebang parsing (0x%08X)\n",
1068 scriptFile, GetLastError());
1069 free(scriptFile);
1070 return 0;
1071 }
1072
1073 CloseHandle(hFile);
1074 debug(L"# Read %d bytes from %s to find shebang line\n", bytesRead, scriptFile);
1075 free(scriptFile);
1076
1077
1078 char *b = buffer;
1079 bool onlyUtf8 = false;
1080 if (bytesRead > 3 && *b == 0xEF) {
1081 if (*++b == 0xBB && *++b == 0xBF) {
1082 // Allow a UTF-8 BOM
1083 ++b;
1084 bytesRead -= 3;
1085 onlyUtf8 = true;
1086 } else {
1087 debug(L"# Invalid BOM in shebang line");
1088 return 0;
1089 }
1090 }
1091 if (bytesRead <= 2 || b[0] != '#' || b[1] != '!') {
1092 // No shebang (#!) at start of line
1093 debug(L"# No valid shebang line");
1094 return 0;
1095 }
1096 ++b;
1097 --bytesRead;
1098 while (--bytesRead > 0 && isspace(*++b)) { }
1099 char *start = b;
1100 while (--bytesRead > 0 && *++b != '\r' && *b != '\n') { }
1101 wchar_t *shebang;
1102 int shebangLength;
1103 // We add 1 when bytesRead==0, as in that case we hit EOF and b points
1104 // to the last character in the file, not the newline
1105 int exitCode = _decodeShebang(search, start, (int)(b - start + (bytesRead == 0)), onlyUtf8, &shebang, &shebangLength);
1106 if (exitCode) {
1107 return exitCode;
1108 }
1109 debug(L"Shebang: %s\n", shebang);
1110
1111 // Handle shebangs that we should search PATH for
1112 int executablePathWasSetByUsrBinEnv = 0;
1113 exitCode = searchPath(search, shebang, shebangLength);
1114 if (exitCode == 0) {
1115 executablePathWasSetByUsrBinEnv = 1;
1116 } else if (exitCode != RC_NO_SHEBANG) {
1117 return exitCode;
1118 }
1119
1120 // Handle some known, case-sensitive shebangs
1121 const wchar_t *command;
1122 int commandLength;
1123 // Each template must end with "python"
1124 static const wchar_t *shebangTemplates[] = {
1125 L"/usr/bin/env python",
1126 L"/usr/bin/python",
1127 L"/usr/local/bin/python",
1128 L"python",
1129 NULL
1130 };
1131
1132 for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) {
1133 // Just to make sure we don't mess this up in the future
1134 assert(0 == wcscmp(L"python", (*tmpl) + wcslen(*tmpl) - 6));
1135
1136 if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command, &commandLength)) {
1137 // Search for "python{command}" overrides. All templates end with
1138 // "python", so we prepend it by jumping back 6 characters
1139 if (_findCommand(search, &command[-6], commandLength + 6)) {
1140 search->executableArgs = &command[commandLength];
1141 search->executableArgsLength = shebangLength - commandLength;
1142 debug(L"# Treating shebang command '%.*s' as %s\n",
1143 commandLength + 6, &command[-6], search->executablePath);
1144 return 0;
1145 }
1146
1147 search->tag = command;
1148 search->tagLength = commandLength;
1149 // If we had 'python3.12.exe' then we want to strip the suffix
1150 // off of the tag
1151 if (search->tagLength >= 4) {
1152 const wchar_t *suffix = &search->tag[search->tagLength - 4];
1153 if (0 == _comparePath(suffix, 4, L".exe", -1)) {
1154 search->tagLength -= 4;
1155 }
1156 }
1157 // If we had 'python3_d' then we want to strip the '_d' (any
1158 // '.exe' is already gone)
1159 if (search->tagLength >= 2) {
1160 const wchar_t *suffix = &search->tag[search->tagLength - 2];
1161 if (0 == _comparePath(suffix, 2, L"_d", -1)) {
1162 search->tagLength -= 2;
1163 }
1164 }
1165 search->oldStyleTag = true;
1166 search->lowPriorityTag = true;
1167 search->executableArgs = &command[commandLength];
1168 search->executableArgsLength = shebangLength - commandLength;
1169 if (search->tag && search->tagLength) {
1170 debug(L"# Treating shebang command '%.*s' as 'py -%.*s'\n",
1171 commandLength, command, search->tagLength, search->tag);
1172 } else {
1173 debug(L"# Treating shebang command '%.*s' as 'py'\n",
1174 commandLength, command);
1175 }
1176 return 0;
1177 }
1178 }
1179
1180 // Didn't match a template, but we found it on PATH
1181 if (executablePathWasSetByUsrBinEnv) {
1182 return 0;
1183 }
1184
1185 // Unrecognised executables are first tried as command aliases
1186 commandLength = 0;
1187 while (commandLength < shebangLength && !isspace(shebang[commandLength])) {
1188 commandLength += 1;
1189 }
1190 if (_findCommand(search, shebang, commandLength)) {
1191 search->executableArgs = &shebang[commandLength];
1192 search->executableArgsLength = shebangLength - commandLength;
1193 debug(L"# Treating shebang command '%.*s' as %s\n",
1194 commandLength, shebang, search->executablePath);
1195 return 0;
1196 }
1197
1198 // Unrecognised commands are joined to the script's directory and treated
1199 // as the executable path
1200 return _useShebangAsExecutable(search, shebang, shebangLength);
1201 }
1202
1203
1204 int
checkDefaults(SearchInfo * search)1205 checkDefaults(SearchInfo *search)
1206 {
1207 if (!search->allowDefaults) {
1208 return 0;
1209 }
1210
1211 // Only resolve old-style (or absent) tags to defaults
1212 if (search->tag && search->tagLength && !search->oldStyleTag) {
1213 return 0;
1214 }
1215
1216 // If tag is only a major version number, expand it from the environment
1217 // or an ini file
1218 const wchar_t *iniSettingName = NULL;
1219 const wchar_t *envSettingName = NULL;
1220 if (!search->tag || !search->tagLength) {
1221 iniSettingName = L"python";
1222 envSettingName = L"py_python";
1223 } else if (0 == wcsncmp(search->tag, L"3", search->tagLength)) {
1224 iniSettingName = L"python3";
1225 envSettingName = L"py_python3";
1226 } else if (0 == wcsncmp(search->tag, L"2", search->tagLength)) {
1227 iniSettingName = L"python2";
1228 envSettingName = L"py_python2";
1229 } else {
1230 debug(L"# Cannot select defaults for tag '%.*s'\n", search->tagLength, search->tag);
1231 return 0;
1232 }
1233
1234 // First, try to read an environment variable
1235 wchar_t buffer[MAXLEN];
1236 int n = GetEnvironmentVariableW(envSettingName, buffer, MAXLEN);
1237
1238 // If none found, check in our two .ini files instead
1239 if (!n) {
1240 n = _readIni(L"defaults", iniSettingName, buffer, MAXLEN);
1241 }
1242
1243 if (n) {
1244 wchar_t *tag = allocSearchInfoBuffer(search, n + 1);
1245 if (!tag) {
1246 return RC_NO_MEMORY;
1247 }
1248 wcscpy_s(tag, n + 1, buffer);
1249 wchar_t *slash = wcschr(tag, L'/');
1250 if (!slash) {
1251 search->tag = tag;
1252 search->tagLength = n;
1253 search->oldStyleTag = true;
1254 } else {
1255 search->company = tag;
1256 search->companyLength = (int)(slash - tag);
1257 search->tag = slash + 1;
1258 search->tagLength = n - (search->companyLength + 1);
1259 search->oldStyleTag = false;
1260 }
1261 // gh-92817: allow a high priority env to be selected even if it
1262 // doesn't match the tag
1263 search->lowPriorityTag = true;
1264 }
1265
1266 return 0;
1267 }
1268
1269 /******************************************************************************\
1270 *** ENVIRONMENT SEARCH ***
1271 \******************************************************************************/
1272
1273 typedef struct EnvironmentInfo {
1274 /* We use a binary tree and sort on insert */
1275 struct EnvironmentInfo *prev;
1276 struct EnvironmentInfo *next;
1277 /* parent is only used when constructing */
1278 struct EnvironmentInfo *parent;
1279 const wchar_t *company;
1280 const wchar_t *tag;
1281 int internalSortKey;
1282 const wchar_t *installDir;
1283 const wchar_t *executablePath;
1284 const wchar_t *executableArgs;
1285 const wchar_t *architecture;
1286 const wchar_t *displayName;
1287 bool highPriority;
1288 } EnvironmentInfo;
1289
1290
1291 int
copyWstr(const wchar_t ** dest,const wchar_t * src)1292 copyWstr(const wchar_t **dest, const wchar_t *src)
1293 {
1294 if (!dest) {
1295 return RC_NO_MEMORY;
1296 }
1297 if (!src) {
1298 *dest = NULL;
1299 return 0;
1300 }
1301 size_t n = wcsnlen_s(src, MAXLEN - 1) + 1;
1302 wchar_t *buffer = (wchar_t*)malloc(n * sizeof(wchar_t));
1303 if (!buffer) {
1304 return RC_NO_MEMORY;
1305 }
1306 wcsncpy_s(buffer, n, src, n - 1);
1307 *dest = (const wchar_t*)buffer;
1308 return 0;
1309 }
1310
1311
1312 EnvironmentInfo *
newEnvironmentInfo(const wchar_t * company,const wchar_t * tag)1313 newEnvironmentInfo(const wchar_t *company, const wchar_t *tag)
1314 {
1315 EnvironmentInfo *env = (EnvironmentInfo *)malloc(sizeof(EnvironmentInfo));
1316 if (!env) {
1317 return NULL;
1318 }
1319 memset(env, 0, sizeof(EnvironmentInfo));
1320 int exitCode = copyWstr(&env->company, company);
1321 if (exitCode) {
1322 free((void *)env);
1323 return NULL;
1324 }
1325 exitCode = copyWstr(&env->tag, tag);
1326 if (exitCode) {
1327 free((void *)env->company);
1328 free((void *)env);
1329 return NULL;
1330 }
1331 return env;
1332 }
1333
1334
1335 void
freeEnvironmentInfo(EnvironmentInfo * env)1336 freeEnvironmentInfo(EnvironmentInfo *env)
1337 {
1338 if (env) {
1339 free((void *)env->company);
1340 free((void *)env->tag);
1341 free((void *)env->installDir);
1342 free((void *)env->executablePath);
1343 free((void *)env->executableArgs);
1344 free((void *)env->displayName);
1345 freeEnvironmentInfo(env->prev);
1346 env->prev = NULL;
1347 freeEnvironmentInfo(env->next);
1348 env->next = NULL;
1349 free((void *)env);
1350 }
1351 }
1352
1353
1354 /* Specific string comparisons for sorting the tree */
1355
1356 int
_compareCompany(const wchar_t * x,const wchar_t * y)1357 _compareCompany(const wchar_t *x, const wchar_t *y)
1358 {
1359 if (!x && !y) {
1360 return 0;
1361 } else if (!x) {
1362 return -1;
1363 } else if (!y) {
1364 return 1;
1365 }
1366
1367 bool coreX = 0 == _compare(x, -1, L"PythonCore", -1);
1368 bool coreY = 0 == _compare(y, -1, L"PythonCore", -1);
1369 if (coreX) {
1370 return coreY ? 0 : -1;
1371 } else if (coreY) {
1372 return 1;
1373 }
1374 return _compare(x, -1, y, -1);
1375 }
1376
1377
1378 int
_compareTag(const wchar_t * x,const wchar_t * y)1379 _compareTag(const wchar_t *x, const wchar_t *y)
1380 {
1381 if (!x && !y) {
1382 return 0;
1383 } else if (!x) {
1384 return -1;
1385 } else if (!y) {
1386 return 1;
1387 }
1388
1389 // Compare up to the first dash. If not equal, that's our sort order
1390 const wchar_t *xDash = wcschr(x, L'-');
1391 const wchar_t *yDash = wcschr(y, L'-');
1392 int xToDash = xDash ? (int)(xDash - x) : -1;
1393 int yToDash = yDash ? (int)(yDash - y) : -1;
1394 int r = _compare(x, xToDash, y, yToDash);
1395 if (r) {
1396 return r;
1397 }
1398 // If we're equal up to the first dash, we want to sort one with
1399 // no dash *after* one with a dash. Otherwise, a reversed compare.
1400 // This works out because environments are sorted in descending tag
1401 // order, so that higher versions (probably) come first.
1402 // For PythonCore, our "X.Y" structure ensures that higher versions
1403 // come first. Everyone else will just have to deal with it.
1404 if (xDash && yDash) {
1405 return _compare(yDash, -1, xDash, -1);
1406 } else if (xDash) {
1407 return -1;
1408 } else if (yDash) {
1409 return 1;
1410 }
1411 return 0;
1412 }
1413
1414
1415 int
addEnvironmentInfo(EnvironmentInfo ** root,EnvironmentInfo * parent,EnvironmentInfo * node)1416 addEnvironmentInfo(EnvironmentInfo **root, EnvironmentInfo* parent, EnvironmentInfo *node)
1417 {
1418 EnvironmentInfo *r = *root;
1419 if (!r) {
1420 *root = node;
1421 node->parent = parent;
1422 return 0;
1423 }
1424 // Sort by company name
1425 switch (_compareCompany(node->company, r->company)) {
1426 case -1:
1427 return addEnvironmentInfo(&r->prev, r, node);
1428 case 1:
1429 return addEnvironmentInfo(&r->next, r, node);
1430 case 0:
1431 break;
1432 }
1433 // Then by tag (descending)
1434 switch (_compareTag(node->tag, r->tag)) {
1435 case -1:
1436 return addEnvironmentInfo(&r->next, r, node);
1437 case 1:
1438 return addEnvironmentInfo(&r->prev, r, node);
1439 case 0:
1440 break;
1441 }
1442 // Then keep the one with the lowest internal sort key
1443 if (node->internalSortKey < r->internalSortKey) {
1444 // Replace the current node
1445 node->parent = r->parent;
1446 if (node->parent) {
1447 if (node->parent->prev == r) {
1448 node->parent->prev = node;
1449 } else if (node->parent->next == r) {
1450 node->parent->next = node;
1451 } else {
1452 debug(L"# Inconsistent parent value in tree\n");
1453 freeEnvironmentInfo(node);
1454 return RC_INTERNAL_ERROR;
1455 }
1456 } else {
1457 // If node has no parent, then it is the root.
1458 *root = node;
1459 }
1460
1461 node->next = r->next;
1462 node->prev = r->prev;
1463
1464 debug(L"# replaced %s/%s/%i in tree\n", node->company, node->tag, node->internalSortKey);
1465 freeEnvironmentInfo(r);
1466 } else {
1467 debug(L"# not adding %s/%s/%i to tree\n", node->company, node->tag, node->internalSortKey);
1468 return RC_DUPLICATE_ITEM;
1469 }
1470 return 0;
1471 }
1472
1473
1474 /******************************************************************************\
1475 *** REGISTRY SEARCH ***
1476 \******************************************************************************/
1477
1478
1479 int
_registryReadString(const wchar_t ** dest,HKEY root,const wchar_t * subkey,const wchar_t * value)1480 _registryReadString(const wchar_t **dest, HKEY root, const wchar_t *subkey, const wchar_t *value)
1481 {
1482 // Note that this is bytes (hence 'cb'), not characters ('cch')
1483 DWORD cbData = 0;
1484 DWORD flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ;
1485
1486 if (ERROR_SUCCESS != RegGetValueW(root, subkey, value, flags, NULL, NULL, &cbData)) {
1487 return 0;
1488 }
1489
1490 wchar_t *buffer = (wchar_t*)malloc(cbData);
1491 if (!buffer) {
1492 return RC_NO_MEMORY;
1493 }
1494
1495 if (ERROR_SUCCESS == RegGetValueW(root, subkey, value, flags, NULL, buffer, &cbData)) {
1496 *dest = buffer;
1497 } else {
1498 free((void *)buffer);
1499 }
1500 return 0;
1501 }
1502
1503
1504 int
_combineWithInstallDir(const wchar_t ** dest,const wchar_t * installDir,const wchar_t * fragment,int fragmentLength)1505 _combineWithInstallDir(const wchar_t **dest, const wchar_t *installDir, const wchar_t *fragment, int fragmentLength)
1506 {
1507 wchar_t buffer[MAXLEN];
1508 wchar_t fragmentBuffer[MAXLEN];
1509 if (wcsncpy_s(fragmentBuffer, MAXLEN, fragment, fragmentLength)) {
1510 return RC_NO_MEMORY;
1511 }
1512
1513 if (FAILED(PathCchCombineEx(buffer, MAXLEN, installDir, fragmentBuffer, PATHCCH_ALLOW_LONG_PATHS))) {
1514 return RC_NO_MEMORY;
1515 }
1516
1517 return copyWstr(dest, buffer);
1518 }
1519
1520
1521 bool
_isLegacyVersion(EnvironmentInfo * env)1522 _isLegacyVersion(EnvironmentInfo *env)
1523 {
1524 // Check if backwards-compatibility is required.
1525 // Specifically PythonCore versions 2.X and 3.0 - 3.5 do not implement PEP 514.
1526 if (0 != _compare(env->company, -1, L"PythonCore", -1)) {
1527 return false;
1528 }
1529
1530 int versionMajor, versionMinor;
1531 int n = swscanf_s(env->tag, L"%d.%d", &versionMajor, &versionMinor);
1532 if (n != 2) {
1533 debug(L"# %s/%s has an invalid version tag\n", env->company, env->tag);
1534 return false;
1535 }
1536
1537 return versionMajor == 2
1538 || (versionMajor == 3 && versionMinor >= 0 && versionMinor <= 5);
1539 }
1540
1541 int
_registryReadLegacyEnvironment(const SearchInfo * search,HKEY root,EnvironmentInfo * env,const wchar_t * fallbackArch)1542 _registryReadLegacyEnvironment(const SearchInfo *search, HKEY root, EnvironmentInfo *env, const wchar_t *fallbackArch)
1543 {
1544 // Backwards-compatibility for PythonCore versions which do not implement PEP 514.
1545 int exitCode = _combineWithInstallDir(
1546 &env->executablePath,
1547 env->installDir,
1548 search->executable,
1549 search->executableLength
1550 );
1551 if (exitCode) {
1552 return exitCode;
1553 }
1554
1555 if (search->windowed) {
1556 exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"WindowedExecutableArguments");
1557 }
1558 else {
1559 exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"ExecutableArguments");
1560 }
1561 if (exitCode) {
1562 return exitCode;
1563 }
1564
1565 if (fallbackArch) {
1566 copyWstr(&env->architecture, fallbackArch);
1567 } else {
1568 DWORD binaryType;
1569 BOOL success = GetBinaryTypeW(env->executablePath, &binaryType);
1570 if (!success) {
1571 return RC_NO_PYTHON;
1572 }
1573
1574 switch (binaryType) {
1575 case SCS_32BIT_BINARY:
1576 copyWstr(&env->architecture, L"32bit");
1577 break;
1578 case SCS_64BIT_BINARY:
1579 copyWstr(&env->architecture, L"64bit");
1580 break;
1581 default:
1582 return RC_NO_PYTHON;
1583 }
1584 }
1585
1586 if (0 == _compare(env->architecture, -1, L"32bit", -1)) {
1587 size_t tagLength = wcslen(env->tag);
1588 if (tagLength <= 3 || 0 != _compare(&env->tag[tagLength - 3], 3, L"-32", 3)) {
1589 const wchar_t *rawTag = env->tag;
1590 wchar_t *realTag = (wchar_t*) malloc(sizeof(wchar_t) * (tagLength + 4));
1591 if (!realTag) {
1592 return RC_NO_MEMORY;
1593 }
1594
1595 int count = swprintf_s(realTag, tagLength + 4, L"%s-32", env->tag);
1596 if (count == -1) {
1597 debug(L"# Failed to generate 32bit tag\n");
1598 free(realTag);
1599 return RC_INTERNAL_ERROR;
1600 }
1601
1602 env->tag = realTag;
1603 free((void*)rawTag);
1604 }
1605 }
1606
1607 wchar_t buffer[MAXLEN];
1608 if (swprintf_s(buffer, MAXLEN, L"Python %s", env->tag)) {
1609 copyWstr(&env->displayName, buffer);
1610 }
1611
1612 return 0;
1613 }
1614
1615
1616 int
_registryReadEnvironment(const SearchInfo * search,HKEY root,EnvironmentInfo * env,const wchar_t * fallbackArch)1617 _registryReadEnvironment(const SearchInfo *search, HKEY root, EnvironmentInfo *env, const wchar_t *fallbackArch)
1618 {
1619 int exitCode = _registryReadString(&env->installDir, root, L"InstallPath", NULL);
1620 if (exitCode) {
1621 return exitCode;
1622 }
1623 if (!env->installDir) {
1624 return RC_NO_PYTHON;
1625 }
1626
1627 if (_isLegacyVersion(env)) {
1628 return _registryReadLegacyEnvironment(search, root, env, fallbackArch);
1629 }
1630
1631 // If pythonw.exe requested, check specific value
1632 if (search->windowed) {
1633 exitCode = _registryReadString(&env->executablePath, root, L"InstallPath", L"WindowedExecutablePath");
1634 if (!exitCode && env->executablePath) {
1635 exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"WindowedExecutableArguments");
1636 }
1637 }
1638 if (exitCode) {
1639 return exitCode;
1640 }
1641
1642 // Missing windowed path or non-windowed request means we use ExecutablePath
1643 if (!env->executablePath) {
1644 exitCode = _registryReadString(&env->executablePath, root, L"InstallPath", L"ExecutablePath");
1645 if (!exitCode && env->executablePath) {
1646 exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"ExecutableArguments");
1647 }
1648 }
1649 if (exitCode) {
1650 return exitCode;
1651 }
1652
1653 if (!env->executablePath) {
1654 debug(L"# %s/%s has no executable path\n", env->company, env->tag);
1655 return RC_NO_PYTHON;
1656 }
1657
1658 exitCode = _registryReadString(&env->architecture, root, NULL, L"SysArchitecture");
1659 if (exitCode) {
1660 return exitCode;
1661 }
1662
1663 exitCode = _registryReadString(&env->displayName, root, NULL, L"DisplayName");
1664 if (exitCode) {
1665 return exitCode;
1666 }
1667
1668 return 0;
1669 }
1670
1671 int
_registrySearchTags(const SearchInfo * search,EnvironmentInfo ** result,HKEY root,int sortKey,const wchar_t * company,const wchar_t * fallbackArch)1672 _registrySearchTags(const SearchInfo *search, EnvironmentInfo **result, HKEY root, int sortKey, const wchar_t *company, const wchar_t *fallbackArch)
1673 {
1674 wchar_t buffer[256];
1675 int err = 0;
1676 int exitCode = 0;
1677 for (int i = 0; exitCode == 0; ++i) {
1678 DWORD cchBuffer = sizeof(buffer) / sizeof(buffer[0]);
1679 err = RegEnumKeyExW(root, i, buffer, &cchBuffer, NULL, NULL, NULL, NULL);
1680 if (err) {
1681 if (err != ERROR_NO_MORE_ITEMS) {
1682 winerror(0, L"Failed to read installs (tags) from the registry");
1683 }
1684 break;
1685 }
1686 HKEY subkey;
1687 if (ERROR_SUCCESS == RegOpenKeyExW(root, buffer, 0, KEY_READ, &subkey)) {
1688 EnvironmentInfo *env = newEnvironmentInfo(company, buffer);
1689 env->internalSortKey = sortKey;
1690 exitCode = _registryReadEnvironment(search, subkey, env, fallbackArch);
1691 RegCloseKey(subkey);
1692 if (exitCode == RC_NO_PYTHON) {
1693 freeEnvironmentInfo(env);
1694 exitCode = 0;
1695 } else if (!exitCode) {
1696 exitCode = addEnvironmentInfo(result, NULL, env);
1697 if (exitCode) {
1698 freeEnvironmentInfo(env);
1699 if (exitCode == RC_DUPLICATE_ITEM) {
1700 exitCode = 0;
1701 }
1702 }
1703 }
1704 }
1705 }
1706 return exitCode;
1707 }
1708
1709
1710 int
registrySearch(const SearchInfo * search,EnvironmentInfo ** result,HKEY root,int sortKey,const wchar_t * fallbackArch)1711 registrySearch(const SearchInfo *search, EnvironmentInfo **result, HKEY root, int sortKey, const wchar_t *fallbackArch)
1712 {
1713 wchar_t buffer[256];
1714 int err = 0;
1715 int exitCode = 0;
1716 for (int i = 0; exitCode == 0; ++i) {
1717 DWORD cchBuffer = sizeof(buffer) / sizeof(buffer[0]);
1718 err = RegEnumKeyExW(root, i, buffer, &cchBuffer, NULL, NULL, NULL, NULL);
1719 if (err) {
1720 if (err != ERROR_NO_MORE_ITEMS) {
1721 winerror(0, L"Failed to read distributors (company) from the registry");
1722 }
1723 break;
1724 }
1725 if (search->limitToCompany && 0 != _compare(search->limitToCompany, -1, buffer, cchBuffer)) {
1726 debug(L"# Skipping %s due to PYLAUNCHER_LIMIT_TO_COMPANY\n", buffer);
1727 continue;
1728 }
1729 HKEY subkey;
1730 if (ERROR_SUCCESS == RegOpenKeyExW(root, buffer, 0, KEY_READ, &subkey)) {
1731 exitCode = _registrySearchTags(search, result, subkey, sortKey, buffer, fallbackArch);
1732 RegCloseKey(subkey);
1733 }
1734 }
1735 return exitCode;
1736 }
1737
1738
1739 /******************************************************************************\
1740 *** APP PACKAGE SEARCH ***
1741 \******************************************************************************/
1742
1743 int
appxSearch(const SearchInfo * search,EnvironmentInfo ** result,const wchar_t * packageFamilyName,const wchar_t * tag,int sortKey)1744 appxSearch(const SearchInfo *search, EnvironmentInfo **result, const wchar_t *packageFamilyName, const wchar_t *tag, int sortKey)
1745 {
1746 wchar_t realTag[32];
1747 wchar_t buffer[MAXLEN];
1748 const wchar_t *exeName = search->executable;
1749 if (!exeName || search->allowExecutableOverride) {
1750 exeName = search->windowed ? L"pythonw.exe" : L"python.exe";
1751 }
1752
1753 // Failure to get LocalAppData may just mean we're running as a user who
1754 // doesn't have a profile directory.
1755 // In this case, return "not found", but don't fail.
1756 // Chances are they can't launch Store installs anyway.
1757 if (FAILED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, buffer))) {
1758 return RC_NO_PYTHON;
1759 }
1760
1761 if (!join(buffer, MAXLEN, L"Microsoft\\WindowsApps") ||
1762 !join(buffer, MAXLEN, packageFamilyName) ||
1763 !join(buffer, MAXLEN, exeName)) {
1764 debug(L"# Failed to construct App Execution Alias path\n");
1765 return RC_INTERNAL_ERROR;
1766 }
1767
1768 if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(buffer)) {
1769 return RC_NO_PYTHON;
1770 }
1771
1772 // Assume packages are native architecture, which means we need to append
1773 // the '-arm64' on ARM64 host.
1774 wcscpy_s(realTag, 32, tag);
1775 if (isARM64Host()) {
1776 wcscat_s(realTag, 32, L"-arm64");
1777 }
1778
1779 EnvironmentInfo *env = newEnvironmentInfo(L"PythonCore", realTag);
1780 if (!env) {
1781 return RC_NO_MEMORY;
1782 }
1783 env->internalSortKey = sortKey;
1784 if (isAMD64Host()) {
1785 copyWstr(&env->architecture, L"64bit");
1786 } else if (isARM64Host()) {
1787 copyWstr(&env->architecture, L"ARM64");
1788 }
1789
1790 copyWstr(&env->executablePath, buffer);
1791
1792 if (swprintf_s(buffer, MAXLEN, L"Python %s (Store)", tag)) {
1793 copyWstr(&env->displayName, buffer);
1794 }
1795
1796 int exitCode = addEnvironmentInfo(result, NULL, env);
1797 if (exitCode) {
1798 freeEnvironmentInfo(env);
1799 if (exitCode == RC_DUPLICATE_ITEM) {
1800 exitCode = 0;
1801 }
1802 }
1803
1804
1805 return exitCode;
1806 }
1807
1808
1809 /******************************************************************************\
1810 *** OVERRIDDEN EXECUTABLE PATH ***
1811 \******************************************************************************/
1812
1813
1814 int
explicitOverrideSearch(const SearchInfo * search,EnvironmentInfo ** result)1815 explicitOverrideSearch(const SearchInfo *search, EnvironmentInfo **result)
1816 {
1817 if (!search->executablePath) {
1818 return 0;
1819 }
1820
1821 EnvironmentInfo *env = newEnvironmentInfo(NULL, NULL);
1822 if (!env) {
1823 return RC_NO_MEMORY;
1824 }
1825 env->internalSortKey = 10;
1826 int exitCode = copyWstr(&env->executablePath, search->executablePath);
1827 if (exitCode) {
1828 goto abort;
1829 }
1830 exitCode = copyWstr(&env->displayName, L"Explicit override");
1831 if (exitCode) {
1832 goto abort;
1833 }
1834 exitCode = addEnvironmentInfo(result, NULL, env);
1835 if (exitCode) {
1836 goto abort;
1837 }
1838 return 0;
1839
1840 abort:
1841 freeEnvironmentInfo(env);
1842 if (exitCode == RC_DUPLICATE_ITEM) {
1843 exitCode = 0;
1844 }
1845 return exitCode;
1846 }
1847
1848
1849 /******************************************************************************\
1850 *** ACTIVE VIRTUAL ENVIRONMENT SEARCH ***
1851 \******************************************************************************/
1852
1853 int
virtualenvSearch(const SearchInfo * search,EnvironmentInfo ** result)1854 virtualenvSearch(const SearchInfo *search, EnvironmentInfo **result)
1855 {
1856 int exitCode = 0;
1857 EnvironmentInfo *env = NULL;
1858 wchar_t buffer[MAXLEN];
1859 int n = GetEnvironmentVariableW(L"VIRTUAL_ENV", buffer, MAXLEN);
1860 if (!n || !join(buffer, MAXLEN, L"Scripts") || !join(buffer, MAXLEN, search->executable)) {
1861 return 0;
1862 }
1863
1864 DWORD attr = GetFileAttributesW(buffer);
1865 if (INVALID_FILE_ATTRIBUTES == attr && search->lowPriorityTag) {
1866 if (!split_parent(buffer, MAXLEN) || !join(buffer, MAXLEN, L"python.exe")) {
1867 return 0;
1868 }
1869 attr = GetFileAttributesW(buffer);
1870 }
1871
1872 if (INVALID_FILE_ATTRIBUTES == attr) {
1873 debug(L"Python executable %s missing from virtual env\n", buffer);
1874 return 0;
1875 }
1876
1877 env = newEnvironmentInfo(NULL, NULL);
1878 if (!env) {
1879 return RC_NO_MEMORY;
1880 }
1881 env->highPriority = true;
1882 env->internalSortKey = 20;
1883 exitCode = copyWstr(&env->displayName, L"Active venv");
1884 if (exitCode) {
1885 goto abort;
1886 }
1887 exitCode = copyWstr(&env->executablePath, buffer);
1888 if (exitCode) {
1889 goto abort;
1890 }
1891 exitCode = addEnvironmentInfo(result, NULL, env);
1892 if (exitCode) {
1893 goto abort;
1894 }
1895 return 0;
1896
1897 abort:
1898 freeEnvironmentInfo(env);
1899 if (exitCode == RC_DUPLICATE_ITEM) {
1900 return 0;
1901 }
1902 return exitCode;
1903 }
1904
1905 /******************************************************************************\
1906 *** COLLECT ENVIRONMENTS ***
1907 \******************************************************************************/
1908
1909
1910 struct RegistrySearchInfo {
1911 // Registry subkey to search
1912 const wchar_t *subkey;
1913 // Registry hive to search
1914 HKEY hive;
1915 // Flags to use when opening the subkey
1916 DWORD flags;
1917 // Internal sort key to select between "identical" environments discovered
1918 // through different methods
1919 int sortKey;
1920 // Fallback value to assume for PythonCore entries missing a SysArchitecture value
1921 const wchar_t *fallbackArch;
1922 };
1923
1924
1925 struct RegistrySearchInfo REGISTRY_SEARCH[] = {
1926 {
1927 L"Software\\Python",
1928 HKEY_CURRENT_USER,
1929 KEY_READ,
1930 1,
1931 NULL
1932 },
1933 {
1934 L"Software\\Python",
1935 HKEY_LOCAL_MACHINE,
1936 KEY_READ | KEY_WOW64_64KEY,
1937 3,
1938 L"64bit"
1939 },
1940 {
1941 L"Software\\Python",
1942 HKEY_LOCAL_MACHINE,
1943 KEY_READ | KEY_WOW64_32KEY,
1944 4,
1945 L"32bit"
1946 },
1947 { NULL, 0, 0, 0, NULL }
1948 };
1949
1950
1951 struct AppxSearchInfo {
1952 // The package family name. Can be found for an installed package using the
1953 // Powershell "Get-AppxPackage" cmdlet
1954 const wchar_t *familyName;
1955 // The tag to treat the installation as
1956 const wchar_t *tag;
1957 // Internal sort key to select between "identical" environments discovered
1958 // through different methods
1959 int sortKey;
1960 };
1961
1962
1963 struct AppxSearchInfo APPX_SEARCH[] = {
1964 // Releases made through the Store
1965 { L"PythonSoftwareFoundation.Python.3.14_qbz5n2kfra8p0", L"3.14", 10 },
1966 { L"PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0", L"3.13", 10 },
1967 { L"PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0", L"3.12", 10 },
1968 { L"PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0", L"3.11", 10 },
1969 { L"PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0", L"3.10", 10 },
1970 { L"PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0", L"3.9", 10 },
1971 { L"PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0", L"3.8", 10 },
1972
1973 // Side-loadable releases. Note that the publisher ID changes whenever we
1974 // change our code signing certificate subject, so the newer IDs have higher
1975 // priorities (lower sortKey)
1976 { L"PythonSoftwareFoundation.Python.3.14_3847v3x7pw1km", L"3.14", 11 },
1977 { L"PythonSoftwareFoundation.Python.3.13_3847v3x7pw1km", L"3.13", 11 },
1978 { L"PythonSoftwareFoundation.Python.3.12_3847v3x7pw1km", L"3.12", 11 },
1979 { L"PythonSoftwareFoundation.Python.3.11_3847v3x7pw1km", L"3.11", 11 },
1980 { L"PythonSoftwareFoundation.Python.3.11_hd69rhyc2wevp", L"3.11", 12 },
1981 { L"PythonSoftwareFoundation.Python.3.10_3847v3x7pw1km", L"3.10", 11 },
1982 { L"PythonSoftwareFoundation.Python.3.10_hd69rhyc2wevp", L"3.10", 12 },
1983 { L"PythonSoftwareFoundation.Python.3.9_3847v3x7pw1km", L"3.9", 11 },
1984 { L"PythonSoftwareFoundation.Python.3.9_hd69rhyc2wevp", L"3.9", 12 },
1985 { L"PythonSoftwareFoundation.Python.3.8_hd69rhyc2wevp", L"3.8", 12 },
1986 { NULL, NULL, 0 }
1987 };
1988
1989
1990 int
collectEnvironments(const SearchInfo * search,EnvironmentInfo ** result)1991 collectEnvironments(const SearchInfo *search, EnvironmentInfo **result)
1992 {
1993 int exitCode = 0;
1994 HKEY root;
1995 EnvironmentInfo *env = NULL;
1996
1997 if (!result) {
1998 debug(L"# collectEnvironments() was passed a NULL result\n");
1999 return RC_INTERNAL_ERROR;
2000 }
2001 *result = NULL;
2002
2003 exitCode = explicitOverrideSearch(search, result);
2004 if (exitCode) {
2005 return exitCode;
2006 }
2007
2008 exitCode = virtualenvSearch(search, result);
2009 if (exitCode) {
2010 return exitCode;
2011 }
2012
2013 // If we aren't collecting all items to list them, we can exit now.
2014 if (env && !(search->list || search->listPaths)) {
2015 return 0;
2016 }
2017
2018 for (struct RegistrySearchInfo *info = REGISTRY_SEARCH; info->subkey; ++info) {
2019 if (ERROR_SUCCESS == RegOpenKeyExW(info->hive, info->subkey, 0, info->flags, &root)) {
2020 exitCode = registrySearch(search, result, root, info->sortKey, info->fallbackArch);
2021 RegCloseKey(root);
2022 }
2023 if (exitCode) {
2024 return exitCode;
2025 }
2026 }
2027
2028 if (search->limitToCompany) {
2029 debug(L"# Skipping APPX search due to PYLAUNCHER_LIMIT_TO_COMPANY\n");
2030 return 0;
2031 }
2032
2033 for (struct AppxSearchInfo *info = APPX_SEARCH; info->familyName; ++info) {
2034 exitCode = appxSearch(search, result, info->familyName, info->tag, info->sortKey);
2035 if (exitCode && exitCode != RC_NO_PYTHON) {
2036 return exitCode;
2037 }
2038 }
2039
2040 return 0;
2041 }
2042
2043
2044 /******************************************************************************\
2045 *** INSTALL ON DEMAND ***
2046 \******************************************************************************/
2047
2048 struct StoreSearchInfo {
2049 // The tag a user is looking for
2050 const wchar_t *tag;
2051 // The Store ID for a package if it can be installed from the Microsoft
2052 // Store. These are obtained from the dashboard at
2053 // https://partner.microsoft.com/dashboard
2054 const wchar_t *storeId;
2055 };
2056
2057
2058 struct StoreSearchInfo STORE_SEARCH[] = {
2059 { L"3", /* 3.13 */ L"9PNRBTZXMB4Z" },
2060 { L"3.14", L"9NTRHQCBBPR8" },
2061 { L"3.13", L"9PNRBTZXMB4Z" },
2062 { L"3.12", L"9NCVDN91XZQP" },
2063 { L"3.11", L"9NRWMJP3717K" },
2064 { L"3.10", L"9PJPW5LDXLZ5" },
2065 { L"3.9", L"9P7QFQMJRFP7" },
2066 { L"3.8", L"9MSSZTT1N39L" },
2067 { NULL, NULL }
2068 };
2069
2070
2071 int
_installEnvironment(const wchar_t * command,const wchar_t * arguments)2072 _installEnvironment(const wchar_t *command, const wchar_t *arguments)
2073 {
2074 SHELLEXECUTEINFOW siw = {
2075 sizeof(SHELLEXECUTEINFOW),
2076 SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE,
2077 NULL, NULL,
2078 command, arguments, NULL,
2079 SW_SHOWNORMAL
2080 };
2081
2082 debug(L"# Installing with %s %s\n", command, arguments);
2083 if (isEnvVarSet(L"PYLAUNCHER_DRYRUN")) {
2084 debug(L"# Exiting due to PYLAUNCHER_DRYRUN\n");
2085 fflush(stdout);
2086 int mode = _setmode(_fileno(stdout), _O_U8TEXT);
2087 if (arguments) {
2088 fwprintf_s(stdout, L"\"%s\" %s\n", command, arguments);
2089 } else {
2090 fwprintf_s(stdout, L"\"%s\"\n", command);
2091 }
2092 fflush(stdout);
2093 if (mode >= 0) {
2094 _setmode(_fileno(stdout), mode);
2095 }
2096 return RC_INSTALLING;
2097 }
2098
2099 if (!ShellExecuteExW(&siw)) {
2100 return RC_NO_PYTHON;
2101 }
2102
2103 if (!siw.hProcess) {
2104 return RC_INSTALLING;
2105 }
2106
2107 WaitForSingleObjectEx(siw.hProcess, INFINITE, FALSE);
2108 DWORD exitCode = 0;
2109 if (GetExitCodeProcess(siw.hProcess, &exitCode) && exitCode == 0) {
2110 return 0;
2111 }
2112 return RC_INSTALLING;
2113 }
2114
2115
2116 const wchar_t *WINGET_COMMAND = L"Microsoft\\WindowsApps\\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\\winget.exe";
2117 const wchar_t *WINGET_ARGUMENTS = L"install -q %s --exact --accept-package-agreements --source msstore";
2118
2119 const wchar_t *MSSTORE_COMMAND = L"ms-windows-store://pdp/?productid=%s";
2120
2121 int
installEnvironment(const SearchInfo * search)2122 installEnvironment(const SearchInfo *search)
2123 {
2124 // No tag? No installing
2125 if (!search->tag || !search->tagLength) {
2126 debug(L"# Cannot install Python with no tag specified\n");
2127 return RC_NO_PYTHON;
2128 }
2129
2130 // PEP 514 tag but not PythonCore? No installing
2131 if (!search->oldStyleTag &&
2132 search->company && search->companyLength &&
2133 0 != _compare(search->company, search->companyLength, L"PythonCore", -1)) {
2134 debug(L"# Cannot install for company %.*s\n", search->companyLength, search->company);
2135 return RC_NO_PYTHON;
2136 }
2137
2138 const wchar_t *storeId = NULL;
2139 for (struct StoreSearchInfo *info = STORE_SEARCH; info->tag; ++info) {
2140 if (0 == _compare(search->tag, search->tagLength, info->tag, -1)) {
2141 storeId = info->storeId;
2142 break;
2143 }
2144 }
2145
2146 if (!storeId) {
2147 return RC_NO_PYTHON;
2148 }
2149
2150 int exitCode;
2151 wchar_t command[MAXLEN];
2152 wchar_t arguments[MAXLEN];
2153 if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, command)) &&
2154 join(command, MAXLEN, WINGET_COMMAND) &&
2155 swprintf_s(arguments, MAXLEN, WINGET_ARGUMENTS, storeId)) {
2156 if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(command)) {
2157 formatWinerror(GetLastError(), arguments, MAXLEN);
2158 debug(L"# Skipping %s: %s\n", command, arguments);
2159 } else {
2160 fputws(L"Launching winget to install Python. The following output is from the install process\n\
2161 ***********************************************************************\n", stdout);
2162 exitCode = _installEnvironment(command, arguments);
2163 if (exitCode == RC_INSTALLING) {
2164 fputws(L"***********************************************************************\n\
2165 Please check the install status and run your command again.", stderr);
2166 return exitCode;
2167 } else if (exitCode) {
2168 return exitCode;
2169 }
2170 fputws(L"***********************************************************************\n\
2171 Install appears to have succeeded. Searching for new matching installs.\n", stdout);
2172 return 0;
2173 }
2174 }
2175
2176 if (swprintf_s(command, MAXLEN, MSSTORE_COMMAND, storeId)) {
2177 fputws(L"Opening the Microsoft Store to install Python. After installation, "
2178 L"please run your command again.\n", stderr);
2179 exitCode = _installEnvironment(command, NULL);
2180 if (exitCode) {
2181 return exitCode;
2182 }
2183 return 0;
2184 }
2185
2186 return RC_NO_PYTHON;
2187 }
2188
2189 /******************************************************************************\
2190 *** ENVIRONMENT SELECT ***
2191 \******************************************************************************/
2192
2193 bool
_companyMatches(const SearchInfo * search,const EnvironmentInfo * env)2194 _companyMatches(const SearchInfo *search, const EnvironmentInfo *env)
2195 {
2196 if (!search->company || !search->companyLength) {
2197 return true;
2198 }
2199 return 0 == _compare(env->company, -1, search->company, search->companyLength);
2200 }
2201
2202
2203 bool
_tagMatches(const SearchInfo * search,const EnvironmentInfo * env,int searchTagLength)2204 _tagMatches(const SearchInfo *search, const EnvironmentInfo *env, int searchTagLength)
2205 {
2206 if (searchTagLength < 0) {
2207 searchTagLength = search->tagLength;
2208 }
2209 if (!search->tag || !searchTagLength) {
2210 return true;
2211 }
2212 return _startsWithSeparated(env->tag, -1, search->tag, searchTagLength, L".-");
2213 }
2214
2215
2216 bool
_is32Bit(const EnvironmentInfo * env)2217 _is32Bit(const EnvironmentInfo *env)
2218 {
2219 if (env->architecture) {
2220 return 0 == _compare(env->architecture, -1, L"32bit", -1);
2221 }
2222 return false;
2223 }
2224
2225
2226 int
_selectEnvironment(const SearchInfo * search,EnvironmentInfo * env,EnvironmentInfo ** best)2227 _selectEnvironment(const SearchInfo *search, EnvironmentInfo *env, EnvironmentInfo **best)
2228 {
2229 int exitCode = 0;
2230 while (env) {
2231 exitCode = _selectEnvironment(search, env->prev, best);
2232
2233 if (exitCode && exitCode != RC_NO_PYTHON) {
2234 return exitCode;
2235 } else if (!exitCode && *best) {
2236 return 0;
2237 }
2238
2239 if (env->highPriority && search->lowPriorityTag) {
2240 // This environment is marked high priority, and the search allows
2241 // it to be selected even though a tag is specified, so select it
2242 // gh-92817: this allows an active venv to be selected even when a
2243 // default tag has been found in py.ini or the environment
2244 *best = env;
2245 return 0;
2246 }
2247
2248 if (!search->oldStyleTag) {
2249 if (_companyMatches(search, env) && _tagMatches(search, env, -1)) {
2250 // Because of how our sort tree is set up, we will walk up the
2251 // "prev" side and implicitly select the "best" best. By
2252 // returning straight after a match, we skip the entire "next"
2253 // branch and won't ever select a "worse" best.
2254 *best = env;
2255 return 0;
2256 }
2257 } else if (0 == _compare(env->company, -1, L"PythonCore", -1)) {
2258 // Old-style tags can only match PythonCore entries
2259
2260 // If the tag ends with -64, we want to exclude 32-bit runtimes
2261 // (If the tag ends with -32, it will be filtered later)
2262 int tagLength = search->tagLength;
2263 bool exclude32Bit = false, only32Bit = false;
2264 if (tagLength > 3) {
2265 if (0 == _compareArgument(&search->tag[tagLength - 3], 3, L"-64", 3)) {
2266 tagLength -= 3;
2267 exclude32Bit = true;
2268 } else if (0 == _compareArgument(&search->tag[tagLength - 3], 3, L"-32", 3)) {
2269 tagLength -= 3;
2270 only32Bit = true;
2271 }
2272 }
2273
2274 if (_tagMatches(search, env, tagLength)) {
2275 if (exclude32Bit && _is32Bit(env)) {
2276 debug(L"# Excluding %s/%s because it looks like 32bit\n", env->company, env->tag);
2277 } else if (only32Bit && !_is32Bit(env)) {
2278 debug(L"# Excluding %s/%s because it doesn't look 32bit\n", env->company, env->tag);
2279 } else {
2280 *best = env;
2281 return 0;
2282 }
2283 }
2284 }
2285
2286 env = env->next;
2287 }
2288 return RC_NO_PYTHON;
2289 }
2290
2291 int
selectEnvironment(const SearchInfo * search,EnvironmentInfo * root,EnvironmentInfo ** best)2292 selectEnvironment(const SearchInfo *search, EnvironmentInfo *root, EnvironmentInfo **best)
2293 {
2294 if (!best) {
2295 debug(L"# selectEnvironment() was passed a NULL best\n");
2296 return RC_INTERNAL_ERROR;
2297 }
2298 if (!root) {
2299 *best = NULL;
2300 return RC_NO_PYTHON_AT_ALL;
2301 }
2302
2303 EnvironmentInfo *result = NULL;
2304 int exitCode = _selectEnvironment(search, root, &result);
2305 if (!exitCode) {
2306 *best = result;
2307 }
2308
2309 return exitCode;
2310 }
2311
2312
2313 /******************************************************************************\
2314 *** LIST ENVIRONMENTS ***
2315 \******************************************************************************/
2316
2317 #define TAGWIDTH 16
2318
2319 int
_printEnvironment(const EnvironmentInfo * env,FILE * out,bool showPath,const wchar_t * argument)2320 _printEnvironment(const EnvironmentInfo *env, FILE *out, bool showPath, const wchar_t *argument)
2321 {
2322 if (showPath) {
2323 if (env->executablePath && env->executablePath[0]) {
2324 if (env->executableArgs && env->executableArgs[0]) {
2325 fwprintf(out, L" %-*s %s %s\n", TAGWIDTH, argument, env->executablePath, env->executableArgs);
2326 } else {
2327 fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->executablePath);
2328 }
2329 } else if (env->installDir && env->installDir[0]) {
2330 fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->installDir);
2331 } else {
2332 fwprintf(out, L" %s\n", argument);
2333 }
2334 } else if (env->displayName) {
2335 fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->displayName);
2336 } else {
2337 fwprintf(out, L" %s\n", argument);
2338 }
2339 return 0;
2340 }
2341
2342
2343 int
_listAllEnvironments(EnvironmentInfo * env,FILE * out,bool showPath,EnvironmentInfo * defaultEnv)2344 _listAllEnvironments(EnvironmentInfo *env, FILE * out, bool showPath, EnvironmentInfo *defaultEnv)
2345 {
2346 wchar_t buffer[256];
2347 const int bufferSize = 256;
2348 while (env) {
2349 int exitCode = _listAllEnvironments(env->prev, out, showPath, defaultEnv);
2350 if (exitCode) {
2351 return exitCode;
2352 }
2353
2354 if (!env->company || !env->tag) {
2355 buffer[0] = L'\0';
2356 } else if (0 == _compare(env->company, -1, L"PythonCore", -1)) {
2357 swprintf_s(buffer, bufferSize, L"-V:%s", env->tag);
2358 } else {
2359 swprintf_s(buffer, bufferSize, L"-V:%s/%s", env->company, env->tag);
2360 }
2361
2362 if (env == defaultEnv) {
2363 wcscat_s(buffer, bufferSize, L" *");
2364 }
2365
2366 if (buffer[0]) {
2367 exitCode = _printEnvironment(env, out, showPath, buffer);
2368 if (exitCode) {
2369 return exitCode;
2370 }
2371 }
2372
2373 env = env->next;
2374 }
2375 return 0;
2376 }
2377
2378
2379 int
listEnvironments(EnvironmentInfo * env,FILE * out,bool showPath,EnvironmentInfo * defaultEnv)2380 listEnvironments(EnvironmentInfo *env, FILE * out, bool showPath, EnvironmentInfo *defaultEnv)
2381 {
2382 if (!env) {
2383 fwprintf_s(stdout, L"No installed Pythons found!\n");
2384 return 0;
2385 }
2386
2387 /* TODO: Do we want to display these?
2388 In favour, helps users see that '-3' is a good option
2389 Against, repeats the next line of output
2390 SearchInfo majorSearch;
2391 EnvironmentInfo *major;
2392 int exitCode;
2393
2394 if (showPath) {
2395 memset(&majorSearch, 0, sizeof(majorSearch));
2396 majorSearch.company = L"PythonCore";
2397 majorSearch.companyLength = -1;
2398 majorSearch.tag = L"3";
2399 majorSearch.tagLength = -1;
2400 majorSearch.oldStyleTag = true;
2401 major = NULL;
2402 exitCode = selectEnvironment(&majorSearch, env, &major);
2403 if (!exitCode && major) {
2404 exitCode = _printEnvironment(major, out, showPath, L"-3 *");
2405 isDefault = false;
2406 if (exitCode) {
2407 return exitCode;
2408 }
2409 }
2410 majorSearch.tag = L"2";
2411 major = NULL;
2412 exitCode = selectEnvironment(&majorSearch, env, &major);
2413 if (!exitCode && major) {
2414 exitCode = _printEnvironment(major, out, showPath, L"-2");
2415 if (exitCode) {
2416 return exitCode;
2417 }
2418 }
2419 }
2420 */
2421
2422 int mode = _setmode(_fileno(out), _O_U8TEXT);
2423 int exitCode = _listAllEnvironments(env, out, showPath, defaultEnv);
2424 fflush(out);
2425 if (mode >= 0) {
2426 _setmode(_fileno(out), mode);
2427 }
2428 return exitCode;
2429 }
2430
2431
2432 /******************************************************************************\
2433 *** INTERPRETER LAUNCH ***
2434 \******************************************************************************/
2435
2436
2437 int
calculateCommandLine(const SearchInfo * search,const EnvironmentInfo * launch,wchar_t * buffer,int bufferLength)2438 calculateCommandLine(const SearchInfo *search, const EnvironmentInfo *launch, wchar_t *buffer, int bufferLength)
2439 {
2440 int exitCode = 0;
2441 const wchar_t *executablePath = NULL;
2442
2443 // Construct command line from a search override, or else the selected
2444 // environment's executablePath
2445 if (search->executablePath) {
2446 executablePath = search->executablePath;
2447 } else if (launch && launch->executablePath) {
2448 executablePath = launch->executablePath;
2449 }
2450
2451 // If we have an executable path, put it at the start of the command, but
2452 // only if the search allowed an override.
2453 // Otherwise, use the environment's installDir and the search's default
2454 // executable name.
2455 if (executablePath && search->allowExecutableOverride) {
2456 if (wcschr(executablePath, L' ') && executablePath[0] != L'"') {
2457 buffer[0] = L'"';
2458 exitCode = wcscpy_s(&buffer[1], bufferLength - 1, executablePath);
2459 if (!exitCode) {
2460 exitCode = wcscat_s(buffer, bufferLength, L"\"");
2461 }
2462 } else {
2463 exitCode = wcscpy_s(buffer, bufferLength, executablePath);
2464 }
2465 } else if (launch) {
2466 if (!launch->installDir) {
2467 fwprintf_s(stderr, L"Cannot launch %s %s because no install directory was specified",
2468 launch->company, launch->tag);
2469 exitCode = RC_NO_PYTHON;
2470 } else if (!search->executable || !search->executableLength) {
2471 fwprintf_s(stderr, L"Cannot launch %s %s because no executable name is available",
2472 launch->company, launch->tag);
2473 exitCode = RC_NO_PYTHON;
2474 } else {
2475 wchar_t executable[256];
2476 wcsncpy_s(executable, 256, search->executable, search->executableLength);
2477 if ((wcschr(launch->installDir, L' ') && launch->installDir[0] != L'"') ||
2478 (wcschr(executable, L' ') && executable[0] != L'"')) {
2479 buffer[0] = L'"';
2480 exitCode = wcscpy_s(&buffer[1], bufferLength - 1, launch->installDir);
2481 if (!exitCode) {
2482 exitCode = join(buffer, bufferLength, executable) ? 0 : RC_NO_MEMORY;
2483 }
2484 if (!exitCode) {
2485 exitCode = wcscat_s(buffer, bufferLength, L"\"");
2486 }
2487 } else {
2488 exitCode = wcscpy_s(buffer, bufferLength, launch->installDir);
2489 if (!exitCode) {
2490 exitCode = join(buffer, bufferLength, executable) ? 0 : RC_NO_MEMORY;
2491 }
2492 }
2493 }
2494 } else {
2495 exitCode = RC_NO_PYTHON;
2496 }
2497
2498 if (!exitCode && launch && launch->executableArgs) {
2499 exitCode = wcscat_s(buffer, bufferLength, L" ");
2500 if (!exitCode) {
2501 exitCode = wcscat_s(buffer, bufferLength, launch->executableArgs);
2502 }
2503 }
2504
2505 if (!exitCode && search->executableArgs) {
2506 if (search->executableArgsLength < 0) {
2507 exitCode = wcscat_s(buffer, bufferLength, search->executableArgs);
2508 } else if (search->executableArgsLength > 0) {
2509 int end = (int)wcsnlen_s(buffer, MAXLEN);
2510 if (end < bufferLength - (search->executableArgsLength + 1)) {
2511 exitCode = wcsncpy_s(&buffer[end], bufferLength - end,
2512 search->executableArgs, search->executableArgsLength);
2513 }
2514 }
2515 }
2516
2517 if (!exitCode && search->restOfCmdLine) {
2518 exitCode = wcscat_s(buffer, bufferLength, search->restOfCmdLine);
2519 }
2520
2521 return exitCode;
2522 }
2523
2524
2525
2526 BOOL
_safeDuplicateHandle(HANDLE in,HANDLE * pout,const wchar_t * nameForError)2527 _safeDuplicateHandle(HANDLE in, HANDLE * pout, const wchar_t *nameForError)
2528 {
2529 BOOL ok;
2530 HANDLE process = GetCurrentProcess();
2531 DWORD rc;
2532
2533 *pout = NULL;
2534 ok = DuplicateHandle(process, in, process, pout, 0, TRUE,
2535 DUPLICATE_SAME_ACCESS);
2536 if (!ok) {
2537 rc = GetLastError();
2538 if (rc == ERROR_INVALID_HANDLE) {
2539 debug(L"DuplicateHandle returned ERROR_INVALID_HANDLE\n");
2540 ok = TRUE;
2541 }
2542 else {
2543 winerror(0, L"Failed to duplicate %s handle", nameForError);
2544 }
2545 }
2546 return ok;
2547 }
2548
2549 BOOL WINAPI
ctrl_c_handler(DWORD code)2550 ctrl_c_handler(DWORD code)
2551 {
2552 return TRUE; /* We just ignore all control events. */
2553 }
2554
2555
2556 int
launchEnvironment(const SearchInfo * search,const EnvironmentInfo * launch,wchar_t * launchCommand)2557 launchEnvironment(const SearchInfo *search, const EnvironmentInfo *launch, wchar_t *launchCommand)
2558 {
2559 HANDLE job;
2560 JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;
2561 DWORD rc;
2562 BOOL ok;
2563 STARTUPINFOW si;
2564 PROCESS_INFORMATION pi;
2565
2566 // If this is a dryrun, do not actually launch
2567 if (isEnvVarSet(L"PYLAUNCHER_DRYRUN")) {
2568 debug(L"LaunchCommand: %s\n", launchCommand);
2569 debug(L"# Exiting due to PYLAUNCHER_DRYRUN variable\n");
2570 fflush(stdout);
2571 int mode = _setmode(_fileno(stdout), _O_U8TEXT);
2572 fwprintf(stdout, L"%s\n", launchCommand);
2573 fflush(stdout);
2574 if (mode >= 0) {
2575 _setmode(_fileno(stdout), mode);
2576 }
2577 return 0;
2578 }
2579
2580 #if defined(_WINDOWS)
2581 /*
2582 When explorer launches a Windows (GUI) application, it displays
2583 the "app starting" (the "pointer + hourglass") cursor for a number
2584 of seconds, or until the app does something UI-ish (eg, creating a
2585 window, or fetching a message). As this launcher doesn't do this
2586 directly, that cursor remains even after the child process does these
2587 things. We avoid that by doing a simple post+get message.
2588 See http://bugs.python.org/issue17290
2589 */
2590 MSG msg;
2591
2592 PostMessage(0, 0, 0, 0);
2593 GetMessage(&msg, 0, 0, 0);
2594 #endif
2595
2596 debug(L"# about to run: %s\n", launchCommand);
2597 job = CreateJobObject(NULL, NULL);
2598 ok = QueryInformationJobObject(job, JobObjectExtendedLimitInformation,
2599 &info, sizeof(info), &rc);
2600 if (!ok || (rc != sizeof(info)) || !job) {
2601 winerror(0, L"Failed to query job information");
2602 return RC_CREATE_PROCESS;
2603 }
2604 info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
2605 JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
2606 ok = SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info,
2607 sizeof(info));
2608 if (!ok) {
2609 winerror(0, L"Failed to update job information");
2610 return RC_CREATE_PROCESS;
2611 }
2612 memset(&si, 0, sizeof(si));
2613 GetStartupInfoW(&si);
2614 if (!_safeDuplicateHandle(GetStdHandle(STD_INPUT_HANDLE), &si.hStdInput, L"stdin") ||
2615 !_safeDuplicateHandle(GetStdHandle(STD_OUTPUT_HANDLE), &si.hStdOutput, L"stdout") ||
2616 !_safeDuplicateHandle(GetStdHandle(STD_ERROR_HANDLE), &si.hStdError, L"stderr")) {
2617 return RC_NO_STD_HANDLES;
2618 }
2619
2620 ok = SetConsoleCtrlHandler(ctrl_c_handler, TRUE);
2621 if (!ok) {
2622 winerror(0, L"Failed to update Control-C handler");
2623 return RC_NO_STD_HANDLES;
2624 }
2625
2626 si.dwFlags = STARTF_USESTDHANDLES;
2627 ok = CreateProcessW(NULL, launchCommand, NULL, NULL, TRUE,
2628 0, NULL, NULL, &si, &pi);
2629 if (!ok) {
2630 winerror(0, L"Unable to create process using '%s'", launchCommand);
2631 return RC_CREATE_PROCESS;
2632 }
2633 AssignProcessToJobObject(job, pi.hProcess);
2634 CloseHandle(pi.hThread);
2635 WaitForSingleObjectEx(pi.hProcess, INFINITE, FALSE);
2636 ok = GetExitCodeProcess(pi.hProcess, &rc);
2637 if (!ok) {
2638 winerror(0, L"Failed to get exit code of process");
2639 return RC_CREATE_PROCESS;
2640 }
2641 debug(L"child process exit code: %d\n", rc);
2642 return rc;
2643 }
2644
2645
2646 /******************************************************************************\
2647 *** PROCESS CONTROLLER ***
2648 \******************************************************************************/
2649
2650
2651 int
performSearch(SearchInfo * search,EnvironmentInfo ** envs)2652 performSearch(SearchInfo *search, EnvironmentInfo **envs)
2653 {
2654 // First parse the command line for options
2655 int exitCode = parseCommandLine(search);
2656 if (exitCode) {
2657 return exitCode;
2658 }
2659
2660 // Check for a shebang line in our script file
2661 // (or return quickly if no script file was specified)
2662 exitCode = checkShebang(search);
2663 switch (exitCode) {
2664 case 0:
2665 case RC_NO_SHEBANG:
2666 case RC_RECURSIVE_SHEBANG:
2667 break;
2668 default:
2669 return exitCode;
2670 }
2671
2672 // Resolve old-style tags (possibly from a shebang) against py.ini entries
2673 // and environment variables.
2674 exitCode = checkDefaults(search);
2675 if (exitCode) {
2676 return exitCode;
2677 }
2678
2679 // If debugging is enabled, list our search criteria
2680 dumpSearchInfo(search);
2681
2682 // Find all matching environments
2683 exitCode = collectEnvironments(search, envs);
2684 if (exitCode) {
2685 return exitCode;
2686 }
2687
2688 return 0;
2689 }
2690
2691
2692 int
process(int argc,wchar_t ** argv)2693 process(int argc, wchar_t ** argv)
2694 {
2695 int exitCode = 0;
2696 int searchExitCode = 0;
2697 SearchInfo search = {0};
2698 EnvironmentInfo *envs = NULL;
2699 EnvironmentInfo *env = NULL;
2700 wchar_t launchCommand[MAXLEN];
2701
2702 memset(launchCommand, 0, sizeof(launchCommand));
2703
2704 if (isEnvVarSet(L"PYLAUNCHER_DEBUG")) {
2705 setvbuf(stderr, (char *)NULL, _IONBF, 0);
2706 log_fp = stderr;
2707 debug(L"argv0: %s\nversion: %S\n", argv[0], PY_VERSION);
2708 }
2709
2710 DWORD len = GetEnvironmentVariableW(L"PYLAUNCHER_LIMIT_TO_COMPANY", NULL, 0);
2711 if (len > 1) {
2712 wchar_t *limitToCompany = allocSearchInfoBuffer(&search, len);
2713 if (!limitToCompany) {
2714 exitCode = RC_NO_MEMORY;
2715 winerror(0, L"Failed to allocate internal buffer");
2716 goto abort;
2717 }
2718 search.limitToCompany = limitToCompany;
2719 if (0 == GetEnvironmentVariableW(L"PYLAUNCHER_LIMIT_TO_COMPANY", limitToCompany, len)) {
2720 exitCode = RC_INTERNAL_ERROR;
2721 winerror(0, L"Failed to read PYLAUNCHER_LIMIT_TO_COMPANY variable");
2722 goto abort;
2723 }
2724 }
2725
2726 search.originalCmdLine = GetCommandLineW();
2727
2728 exitCode = performSearch(&search, &envs);
2729 if (exitCode) {
2730 goto abort;
2731 }
2732
2733 // Display the help text, but only exit on error
2734 if (search.help) {
2735 exitCode = showHelpText(argv);
2736 if (exitCode) {
2737 goto abort;
2738 }
2739 }
2740
2741 // Select best environment
2742 // This is early so that we can show the default when listing, but all
2743 // responses to any errors occur later.
2744 searchExitCode = selectEnvironment(&search, envs, &env);
2745
2746 // List all environments, then exit
2747 if (search.list || search.listPaths) {
2748 exitCode = listEnvironments(envs, stdout, search.listPaths, env);
2749 goto abort;
2750 }
2751
2752 // When debugging, list all discovered environments anyway
2753 if (log_fp) {
2754 exitCode = listEnvironments(envs, log_fp, true, NULL);
2755 if (exitCode) {
2756 goto abort;
2757 }
2758 }
2759
2760 // We searched earlier, so if we didn't find anything, now we react
2761 exitCode = searchExitCode;
2762 // If none found, and if permitted, install it
2763 if (exitCode == RC_NO_PYTHON && isEnvVarSet(L"PYLAUNCHER_ALLOW_INSTALL") ||
2764 isEnvVarSet(L"PYLAUNCHER_ALWAYS_INSTALL")) {
2765 exitCode = installEnvironment(&search);
2766 if (!exitCode) {
2767 // Successful install, so we need to re-scan and select again
2768 env = NULL;
2769 exitCode = performSearch(&search, &envs);
2770 if (exitCode) {
2771 goto abort;
2772 }
2773 exitCode = selectEnvironment(&search, envs, &env);
2774 }
2775 }
2776 if (exitCode == RC_NO_PYTHON) {
2777 fputws(L"No suitable Python runtime found\n", stderr);
2778 fputws(L"Pass --list (-0) to see all detected environments on your machine\n", stderr);
2779 if (!isEnvVarSet(L"PYLAUNCHER_ALLOW_INSTALL") && search.oldStyleTag) {
2780 fputws(L"or set environment variable PYLAUNCHER_ALLOW_INSTALL to use winget\n"
2781 L"or open the Microsoft Store to the requested version.\n", stderr);
2782 }
2783 goto abort;
2784 }
2785 if (exitCode == RC_NO_PYTHON_AT_ALL) {
2786 fputws(L"No installed Python found!\n", stderr);
2787 goto abort;
2788 }
2789 if (exitCode) {
2790 goto abort;
2791 }
2792
2793 if (env) {
2794 debug(L"env.company: %s\nenv.tag: %s\n", env->company, env->tag);
2795 } else {
2796 debug(L"env.company: (null)\nenv.tag: (null)\n");
2797 }
2798
2799 exitCode = calculateCommandLine(&search, env, launchCommand, sizeof(launchCommand) / sizeof(launchCommand[0]));
2800 if (exitCode) {
2801 goto abort;
2802 }
2803
2804 // Launch selected runtime
2805 exitCode = launchEnvironment(&search, env, launchCommand);
2806
2807 abort:
2808 freeSearchInfo(&search);
2809 freeEnvironmentInfo(envs);
2810 return exitCode;
2811 }
2812
2813
2814 #if defined(_WINDOWS)
2815
wWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPWSTR lpstrCmd,int nShow)2816 int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
2817 LPWSTR lpstrCmd, int nShow)
2818 {
2819 return process(__argc, __wargv);
2820 }
2821
2822 #else
2823
wmain(int argc,wchar_t ** argv)2824 int cdecl wmain(int argc, wchar_t ** argv)
2825 {
2826 return process(argc, argv);
2827 }
2828
2829 #endif
2830