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