• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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