| /* | |
| * Rewritten Python launcher for Windows | |
| * | |
| * This new rewrite properly handles PEP 514 and allows any registered Python | |
| * runtime to be launched. It also enables auto-install of versions when they | |
| * are requested but no installation can be found. | |
| */ | |
| #define __STDC_WANT_LIB_EXT1__ 1 | |
| #include <windows.h> | |
| #include <pathcch.h> | |
| #include <fcntl.h> | |
| #include <io.h> | |
| #include <shlobj.h> | |
| #include <stdio.h> | |
| #include <stdbool.h> | |
| #include <tchar.h> | |
| #include <assert.h> | |
| #define MS_WINDOWS | |
| #include "patchlevel.h" | |
| #define MAXLEN PATHCCH_MAX_CCH | |
| #define MSGSIZE 1024 | |
| #define RC_NO_STD_HANDLES 100 | |
| #define RC_CREATE_PROCESS 101 | |
| #define RC_BAD_VIRTUAL_PATH 102 | |
| #define RC_NO_PYTHON 103 | |
| #define RC_NO_MEMORY 104 | |
| #define RC_NO_SCRIPT 105 | |
| #define RC_NO_VENV_CFG 106 | |
| #define RC_BAD_VENV_CFG 107 | |
| #define RC_NO_COMMANDLINE 108 | |
| #define RC_INTERNAL_ERROR 109 | |
| #define RC_DUPLICATE_ITEM 110 | |
| #define RC_INSTALLING 111 | |
| #define RC_NO_PYTHON_AT_ALL 112 | |
| #define RC_NO_SHEBANG 113 | |
| #define RC_RECURSIVE_SHEBANG 114 | |
| static FILE * log_fp = NULL; | |
| void | |
| debug(wchar_t * format, ...) | |
| { | |
| va_list va; | |
| if (log_fp != NULL) { | |
| wchar_t buffer[MAXLEN]; | |
| int r = 0; | |
| va_start(va, format); | |
| r = vswprintf_s(buffer, MAXLEN, format, va); | |
| va_end(va); | |
| if (r <= 0) { | |
| return; | |
| } | |
| fputws(buffer, log_fp); | |
| while (r && isspace(buffer[r])) { | |
| buffer[r--] = L'\0'; | |
| } | |
| if (buffer[0]) { | |
| OutputDebugStringW(buffer); | |
| } | |
| } | |
| } | |
| void | |
| formatWinerror(int rc, wchar_t * message, int size) | |
| { | |
| FormatMessageW( | |
| FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, | |
| NULL, rc, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), | |
| message, size, NULL); | |
| } | |
| void | |
| winerror(int err, wchar_t * format, ... ) | |
| { | |
| va_list va; | |
| wchar_t message[MSGSIZE]; | |
| wchar_t win_message[MSGSIZE]; | |
| int len; | |
| if (err == 0) { | |
| err = GetLastError(); | |
| } | |
| va_start(va, format); | |
| len = _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va); | |
| va_end(va); | |
| formatWinerror(err, win_message, MSGSIZE); | |
| if (len >= 0) { | |
| _snwprintf_s(&message[len], MSGSIZE - len, _TRUNCATE, L": %s", | |
| win_message); | |
| } | |
| #if !defined(_WINDOWS) | |
| fwprintf(stderr, L"%s\n", message); | |
| #else | |
| MessageBoxW(NULL, message, L"Python Launcher is sorry to say ...", | |
| MB_OK); | |
| #endif | |
| } | |
| void | |
| error(wchar_t * format, ... ) | |
| { | |
| va_list va; | |
| wchar_t message[MSGSIZE]; | |
| va_start(va, format); | |
| _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va); | |
| va_end(va); | |
| #if !defined(_WINDOWS) | |
| fwprintf(stderr, L"%s\n", message); | |
| #else | |
| MessageBoxW(NULL, message, L"Python Launcher is sorry to say ...", | |
| MB_OK); | |
| #endif | |
| } | |
| typedef BOOL (*PIsWow64Process2)(HANDLE, USHORT*, USHORT*); | |
| USHORT | |
| _getNativeMachine(void) | |
| { | |
| static USHORT _nativeMachine = IMAGE_FILE_MACHINE_UNKNOWN; | |
| if (_nativeMachine == IMAGE_FILE_MACHINE_UNKNOWN) { | |
| USHORT processMachine; | |
| HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll"); | |
| PIsWow64Process2 IsWow64Process2 = kernel32 ? | |
| (PIsWow64Process2)GetProcAddress(kernel32, "IsWow64Process2") : | |
| NULL; | |
| if (!IsWow64Process2) { | |
| BOOL wow64Process; | |
| if (!IsWow64Process(NULL, &wow64Process)) { | |
| winerror(0, L"Checking process type"); | |
| } else if (wow64Process) { | |
| // We should always be a 32-bit executable, so if running | |
| // under emulation, it must be a 64-bit host. | |
| _nativeMachine = IMAGE_FILE_MACHINE_AMD64; | |
| } else { | |
| // Not running under emulation, and an old enough OS to not | |
| // have IsWow64Process2, so assume it's x86. | |
| _nativeMachine = IMAGE_FILE_MACHINE_I386; | |
| } | |
| } else if (!IsWow64Process2(NULL, &processMachine, &_nativeMachine)) { | |
| winerror(0, L"Checking process type"); | |
| } | |
| } | |
| return _nativeMachine; | |
| } | |
| bool | |
| isAMD64Host(void) | |
| { | |
| return _getNativeMachine() == IMAGE_FILE_MACHINE_AMD64; | |
| } | |
| bool | |
| isARM64Host(void) | |
| { | |
| return _getNativeMachine() == IMAGE_FILE_MACHINE_ARM64; | |
| } | |
| bool | |
| isEnvVarSet(const wchar_t *name) | |
| { | |
| /* only looking for non-empty, which means at least one character | |
| and the null terminator */ | |
| return GetEnvironmentVariableW(name, NULL, 0) >= 2; | |
| } | |
| bool | |
| join(wchar_t *buffer, size_t bufferLength, const wchar_t *fragment) | |
| { | |
| if (SUCCEEDED(PathCchCombineEx(buffer, bufferLength, buffer, fragment, PATHCCH_ALLOW_LONG_PATHS))) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| bool | |
| split_parent(wchar_t *buffer, size_t bufferLength) | |
| { | |
| return SUCCEEDED(PathCchRemoveFileSpec(buffer, bufferLength)); | |
| } | |
| int | |
| _compare(const wchar_t *x, int xLen, const wchar_t *y, int yLen) | |
| { | |
| // Empty strings sort first | |
| if (!x || !xLen) { | |
| return (!y || !yLen) ? 0 : -1; | |
| } else if (!y || !yLen) { | |
| return 1; | |
| } | |
| switch (CompareStringEx( | |
| LOCALE_NAME_INVARIANT, NORM_IGNORECASE | SORT_DIGITSASNUMBERS, | |
| x, xLen, y, yLen, | |
| NULL, NULL, 0 | |
| )) { | |
| case CSTR_LESS_THAN: | |
| return -1; | |
| case CSTR_EQUAL: | |
| return 0; | |
| case CSTR_GREATER_THAN: | |
| return 1; | |
| default: | |
| winerror(0, L"Error comparing '%.*s' and '%.*s' (compare)", xLen, x, yLen, y); | |
| return -1; | |
| } | |
| } | |
| int | |
| _compareArgument(const wchar_t *x, int xLen, const wchar_t *y, int yLen) | |
| { | |
| // Empty strings sort first | |
| if (!x || !xLen) { | |
| return (!y || !yLen) ? 0 : -1; | |
| } else if (!y || !yLen) { | |
| return 1; | |
| } | |
| switch (CompareStringEx( | |
| LOCALE_NAME_INVARIANT, 0, | |
| x, xLen, y, yLen, | |
| NULL, NULL, 0 | |
| )) { | |
| case CSTR_LESS_THAN: | |
| return -1; | |
| case CSTR_EQUAL: | |
| return 0; | |
| case CSTR_GREATER_THAN: | |
| return 1; | |
| default: | |
| winerror(0, L"Error comparing '%.*s' and '%.*s' (compareArgument)", xLen, x, yLen, y); | |
| return -1; | |
| } | |
| } | |
| int | |
| _comparePath(const wchar_t *x, int xLen, const wchar_t *y, int yLen) | |
| { | |
| // Empty strings sort first | |
| if (!x || !xLen) { | |
| return !y || !yLen ? 0 : -1; | |
| } else if (!y || !yLen) { | |
| return 1; | |
| } | |
| switch (CompareStringOrdinal(x, xLen, y, yLen, TRUE)) { | |
| case CSTR_LESS_THAN: | |
| return -1; | |
| case CSTR_EQUAL: | |
| return 0; | |
| case CSTR_GREATER_THAN: | |
| return 1; | |
| default: | |
| winerror(0, L"Error comparing '%.*s' and '%.*s' (comparePath)", xLen, x, yLen, y); | |
| return -1; | |
| } | |
| } | |
| bool | |
| _startsWith(const wchar_t *x, int xLen, const wchar_t *y, int yLen) | |
| { | |
| if (!x || !y) { | |
| return false; | |
| } | |
| yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen; | |
| xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen; | |
| return xLen >= yLen && 0 == _compare(x, yLen, y, yLen); | |
| } | |
| bool | |
| _startsWithArgument(const wchar_t *x, int xLen, const wchar_t *y, int yLen) | |
| { | |
| if (!x || !y) { | |
| return false; | |
| } | |
| yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen; | |
| xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen; | |
| return xLen >= yLen && 0 == _compareArgument(x, yLen, y, yLen); | |
| } | |
| // Unlike regular startsWith, this function requires that the following | |
| // character is either NULL (that is, the entire string matches) or is one of | |
| // the characters in 'separators'. | |
| bool | |
| _startsWithSeparated(const wchar_t *x, int xLen, const wchar_t *y, int yLen, const wchar_t *separators) | |
| { | |
| if (!x || !y) { | |
| return false; | |
| } | |
| yLen = yLen < 0 ? (int)wcsnlen_s(y, MAXLEN) : yLen; | |
| xLen = xLen < 0 ? (int)wcsnlen_s(x, MAXLEN) : xLen; | |
| if (xLen < yLen) { | |
| return false; | |
| } | |
| if (xLen == yLen) { | |
| return 0 == _compare(x, xLen, y, yLen); | |
| } | |
| return separators && | |
| 0 == _compare(x, yLen, y, yLen) && | |
| wcschr(separators, x[yLen]) != NULL; | |
| } | |
| /******************************************************************************\ | |
| *** HELP TEXT *** | |
| \******************************************************************************/ | |
| int | |
| showHelpText(wchar_t ** argv) | |
| { | |
| // The help text is stored in launcher-usage.txt, which is compiled into | |
| // the launcher and loaded at runtime if needed. | |
| // | |
| // The file must be UTF-8. There are two substitutions: | |
| // %ls - PY_VERSION (as wchar_t*) | |
| // %ls - argv[0] (as wchar_t*) | |
| HRSRC res = FindResourceExW(NULL, L"USAGE", MAKEINTRESOURCE(1), MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL)); | |
| HGLOBAL resData = res ? LoadResource(NULL, res) : NULL; | |
| const char *usage = resData ? (const char*)LockResource(resData) : NULL; | |
| if (usage == NULL) { | |
| winerror(0, L"Unable to load usage text"); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| DWORD cbData = SizeofResource(NULL, res); | |
| DWORD cchUsage = MultiByteToWideChar(CP_UTF8, 0, usage, cbData, NULL, 0); | |
| if (!cchUsage) { | |
| winerror(0, L"Unable to preprocess usage text"); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| cchUsage += 1; | |
| wchar_t *wUsage = (wchar_t*)malloc(cchUsage * sizeof(wchar_t)); | |
| cchUsage = MultiByteToWideChar(CP_UTF8, 0, usage, cbData, wUsage, cchUsage); | |
| if (!cchUsage) { | |
| winerror(0, L"Unable to preprocess usage text"); | |
| free((void *)wUsage); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| // Ensure null termination | |
| wUsage[cchUsage] = L'\0'; | |
| fwprintf(stdout, wUsage, (L"" PY_VERSION), argv[0]); | |
| fflush(stdout); | |
| free((void *)wUsage); | |
| return 0; | |
| } | |
| /******************************************************************************\ | |
| *** SEARCH INFO *** | |
| \******************************************************************************/ | |
| struct _SearchInfoBuffer { | |
| struct _SearchInfoBuffer *next; | |
| wchar_t buffer[0]; | |
| }; | |
| typedef struct { | |
| // the original string, managed by the OS | |
| const wchar_t *originalCmdLine; | |
| // pointer into the cmdline to mark what we've consumed | |
| const wchar_t *restOfCmdLine; | |
| // if known/discovered, the full executable path of our runtime | |
| const wchar_t *executablePath; | |
| // pointer and length into cmdline for the file to check for a | |
| // shebang line, if any. Length can be -1 if the string is null | |
| // terminated. | |
| const wchar_t *scriptFile; | |
| int scriptFileLength; | |
| // pointer and length into cmdline or a static string with the | |
| // name of the target executable. Length can be -1 if the string | |
| // is null terminated. | |
| const wchar_t *executable; | |
| int executableLength; | |
| // pointer and length into a string with additional interpreter | |
| // arguments to include before restOfCmdLine. Length can be -1 if | |
| // the string is null terminated. | |
| const wchar_t *executableArgs; | |
| int executableArgsLength; | |
| // pointer and length into cmdline or a static string with the | |
| // company name for PEP 514 lookup. Length can be -1 if the string | |
| // is null terminated. | |
| const wchar_t *company; | |
| int companyLength; | |
| // pointer and length into cmdline or a static string with the | |
| // tag for PEP 514 lookup. Length can be -1 if the string is | |
| // null terminated. | |
| const wchar_t *tag; | |
| int tagLength; | |
| // if true, treats 'tag' as a non-PEP 514 filter | |
| bool oldStyleTag; | |
| // if true, ignores 'tag' when a high priority environment is found | |
| // gh-92817: This is currently set when a tag is read from configuration, | |
| // the environment, or a shebang, rather than the command line, and the | |
| // only currently possible high priority environment is an active virtual | |
| // environment | |
| bool lowPriorityTag; | |
| // if true, allow PEP 514 lookup to override 'executable' | |
| bool allowExecutableOverride; | |
| // if true, allow a nearby pyvenv.cfg to locate the executable | |
| bool allowPyvenvCfg; | |
| // if true, allow defaults (env/py.ini) to clarify/override tags | |
| bool allowDefaults; | |
| // if true, prefer windowed (console-less) executable | |
| bool windowed; | |
| // if true, only list detected runtimes without launching | |
| bool list; | |
| // if true, only list detected runtimes with paths without launching | |
| bool listPaths; | |
| // if true, display help message before continuing | |
| bool help; | |
| // if set, limits search to registry keys with the specified Company | |
| // This is intended for debugging and testing only | |
| const wchar_t *limitToCompany; | |
| // dynamically allocated buffers to free later | |
| struct _SearchInfoBuffer *_buffer; | |
| } SearchInfo; | |
| wchar_t * | |
| allocSearchInfoBuffer(SearchInfo *search, int wcharCount) | |
| { | |
| struct _SearchInfoBuffer *buffer = (struct _SearchInfoBuffer*)malloc( | |
| sizeof(struct _SearchInfoBuffer) + | |
| wcharCount * sizeof(wchar_t) | |
| ); | |
| if (!buffer) { | |
| return NULL; | |
| } | |
| buffer->next = search->_buffer; | |
| search->_buffer = buffer; | |
| return buffer->buffer; | |
| } | |
| void | |
| freeSearchInfo(SearchInfo *search) | |
| { | |
| struct _SearchInfoBuffer *b = search->_buffer; | |
| search->_buffer = NULL; | |
| while (b) { | |
| struct _SearchInfoBuffer *nextB = b->next; | |
| free((void *)b); | |
| b = nextB; | |
| } | |
| } | |
| void | |
| _debugStringAndLength(const wchar_t *s, int len, const wchar_t *name) | |
| { | |
| if (!s) { | |
| debug(L"%s: (null)\n", name); | |
| } else if (len == 0) { | |
| debug(L"%s: (empty)\n", name); | |
| } else if (len < 0) { | |
| debug(L"%s: %s\n", name, s); | |
| } else { | |
| debug(L"%s: %.*ls\n", name, len, s); | |
| } | |
| } | |
| void | |
| dumpSearchInfo(SearchInfo *search) | |
| { | |
| if (!log_fp) { | |
| return; | |
| } | |
| #ifdef __clang__ | |
| #define DEBUGNAME(s) L # s | |
| #else | |
| #define DEBUGNAME(s) # s | |
| #endif | |
| #define DEBUG(s) debug(L"SearchInfo." DEBUGNAME(s) L": %s\n", (search->s) ? (search->s) : L"(null)") | |
| #define DEBUG_2(s, sl) _debugStringAndLength((search->s), (search->sl), L"SearchInfo." DEBUGNAME(s)) | |
| #define DEBUG_BOOL(s) debug(L"SearchInfo." DEBUGNAME(s) L": %s\n", (search->s) ? L"True" : L"False") | |
| DEBUG(originalCmdLine); | |
| DEBUG(restOfCmdLine); | |
| DEBUG(executablePath); | |
| DEBUG_2(scriptFile, scriptFileLength); | |
| DEBUG_2(executable, executableLength); | |
| DEBUG_2(executableArgs, executableArgsLength); | |
| DEBUG_2(company, companyLength); | |
| DEBUG_2(tag, tagLength); | |
| DEBUG_BOOL(oldStyleTag); | |
| DEBUG_BOOL(lowPriorityTag); | |
| DEBUG_BOOL(allowDefaults); | |
| DEBUG_BOOL(allowExecutableOverride); | |
| DEBUG_BOOL(windowed); | |
| DEBUG_BOOL(list); | |
| DEBUG_BOOL(listPaths); | |
| DEBUG_BOOL(help); | |
| DEBUG(limitToCompany); | |
| #undef DEBUG_BOOL | |
| #undef DEBUG_2 | |
| #undef DEBUG | |
| #undef DEBUGNAME | |
| } | |
| int | |
| findArgv0Length(const wchar_t *buffer, int bufferLength) | |
| { | |
| // Note: this implements semantics that are only valid for argv0. | |
| // Specifically, there is no escaping of quotes, and quotes within | |
| // the argument have no effect. A quoted argv0 must start and end | |
| // with a double quote character; otherwise, it ends at the first | |
| // ' ' or '\t'. | |
| int quoted = buffer[0] == L'"'; | |
| for (int i = 1; bufferLength < 0 || i < bufferLength; ++i) { | |
| switch (buffer[i]) { | |
| case L'\0': | |
| return i; | |
| case L' ': | |
| case L'\t': | |
| if (!quoted) { | |
| return i; | |
| } | |
| break; | |
| case L'"': | |
| if (quoted) { | |
| return i + 1; | |
| } | |
| break; | |
| } | |
| } | |
| return bufferLength; | |
| } | |
| const wchar_t * | |
| findArgv0End(const wchar_t *buffer, int bufferLength) | |
| { | |
| return &buffer[findArgv0Length(buffer, bufferLength)]; | |
| } | |
| /******************************************************************************\ | |
| *** COMMAND-LINE PARSING *** | |
| \******************************************************************************/ | |
| // Adapted from https://stackoverflow.com/a/65583702 | |
| typedef struct AppExecLinkFile { // For tag IO_REPARSE_TAG_APPEXECLINK | |
| DWORD reparseTag; | |
| WORD reparseDataLength; | |
| WORD reserved; | |
| ULONG version; | |
| wchar_t stringList[MAX_PATH * 4]; // Multistring (Consecutive UTF-16 strings each ending with a NUL) | |
| /* There are normally 4 strings here. Ex: | |
| Package ID: L"Microsoft.DesktopAppInstaller_8wekyb3d8bbwe" | |
| Entry Point: L"Microsoft.DesktopAppInstaller_8wekyb3d8bbwe!PythonRedirector" | |
| Executable: L"C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.17.106910_x64__8wekyb3d8bbwe\AppInstallerPythonRedirector.exe" | |
| Applic. Type: L"0" // Integer as ASCII. "0" = Desktop bridge application; Else sandboxed UWP application | |
| */ | |
| } AppExecLinkFile; | |
| int | |
| parseCommandLine(SearchInfo *search) | |
| { | |
| if (!search || !search->originalCmdLine) { | |
| return RC_NO_COMMANDLINE; | |
| } | |
| const wchar_t *argv0End = findArgv0End(search->originalCmdLine, -1); | |
| const wchar_t *tail = argv0End; // will be start of the executable name | |
| const wchar_t *end = argv0End; // will be end of the executable name | |
| search->restOfCmdLine = argv0End; // will be first space after argv0 | |
| while (--tail != search->originalCmdLine) { | |
| if (*tail == L'"' && end == argv0End) { | |
| // Move the "end" up to the quote, so we also allow moving for | |
| // a period later on. | |
| end = argv0End = tail; | |
| } else if (*tail == L'.' && end == argv0End) { | |
| end = tail; | |
| } else if (*tail == L'\\' || *tail == L'/') { | |
| ++tail; | |
| break; | |
| } | |
| } | |
| if (tail == search->originalCmdLine && tail[0] == L'"') { | |
| ++tail; | |
| } | |
| // Without special cases, we can now fill in the search struct | |
| int tailLen = (int)(end ? (end - tail) : wcsnlen_s(tail, MAXLEN)); | |
| search->executableLength = -1; | |
| // Our special cases are as follows | |
| #define MATCHES(s) (0 == _comparePath(tail, tailLen, (s), -1)) | |
| #define STARTSWITH(s) _startsWith(tail, tailLen, (s), -1) | |
| if (MATCHES(L"py")) { | |
| search->executable = L"python.exe"; | |
| search->allowExecutableOverride = true; | |
| search->allowDefaults = true; | |
| } else if (MATCHES(L"pyw")) { | |
| search->executable = L"pythonw.exe"; | |
| search->allowExecutableOverride = true; | |
| search->allowDefaults = true; | |
| search->windowed = true; | |
| } else if (MATCHES(L"py_d")) { | |
| search->executable = L"python_d.exe"; | |
| search->allowExecutableOverride = true; | |
| search->allowDefaults = true; | |
| } else if (MATCHES(L"pyw_d")) { | |
| search->executable = L"pythonw_d.exe"; | |
| search->allowExecutableOverride = true; | |
| search->allowDefaults = true; | |
| search->windowed = true; | |
| } else if (STARTSWITH(L"python3")) { | |
| search->executable = L"python.exe"; | |
| search->tag = &tail[6]; | |
| search->tagLength = tailLen - 6; | |
| search->allowExecutableOverride = true; | |
| search->oldStyleTag = true; | |
| search->allowPyvenvCfg = true; | |
| } else if (STARTSWITH(L"pythonw3")) { | |
| search->executable = L"pythonw.exe"; | |
| search->tag = &tail[7]; | |
| search->tagLength = tailLen - 7; | |
| search->allowExecutableOverride = true; | |
| search->oldStyleTag = true; | |
| search->allowPyvenvCfg = true; | |
| search->windowed = true; | |
| } else { | |
| search->executable = tail; | |
| search->executableLength = tailLen; | |
| search->allowPyvenvCfg = true; | |
| } | |
| #undef STARTSWITH | |
| #undef MATCHES | |
| // First argument might be one of our options. If so, consume it, | |
| // update flags and then set restOfCmdLine. | |
| const wchar_t *arg = search->restOfCmdLine; | |
| while(*arg && isspace(*arg)) { ++arg; } | |
| #define MATCHES(s) (0 == _compareArgument(arg, argLen, (s), -1)) | |
| #define STARTSWITH(s) _startsWithArgument(arg, argLen, (s), -1) | |
| if (*arg && *arg == L'-' && *++arg) { | |
| tail = arg; | |
| while (*tail && !isspace(*tail)) { ++tail; } | |
| int argLen = (int)(tail - arg); | |
| if (argLen > 0) { | |
| if (STARTSWITH(L"2") || STARTSWITH(L"3")) { | |
| // All arguments starting with 2 or 3 are assumed to be version tags | |
| search->tag = arg; | |
| search->tagLength = argLen; | |
| search->oldStyleTag = true; | |
| search->restOfCmdLine = tail; | |
| } else if (STARTSWITH(L"V:") || STARTSWITH(L"-version:")) { | |
| // Arguments starting with 'V:' specify company and/or tag | |
| const wchar_t *argStart = wcschr(arg, L':') + 1; | |
| const wchar_t *tagStart = wcschr(argStart, L'/') ; | |
| if (tagStart) { | |
| search->company = argStart; | |
| search->companyLength = (int)(tagStart - argStart); | |
| search->tag = tagStart + 1; | |
| } else { | |
| search->tag = argStart; | |
| } | |
| search->tagLength = (int)(tail - search->tag); | |
| search->allowDefaults = false; | |
| search->restOfCmdLine = tail; | |
| } else if (MATCHES(L"0") || MATCHES(L"-list")) { | |
| search->list = true; | |
| search->restOfCmdLine = tail; | |
| } else if (MATCHES(L"0p") || MATCHES(L"-list-paths")) { | |
| search->listPaths = true; | |
| search->restOfCmdLine = tail; | |
| } else if (MATCHES(L"h") || MATCHES(L"-help")) { | |
| search->help = true; | |
| // Do not update restOfCmdLine so that we trigger the help | |
| // message from whichever interpreter we select | |
| } | |
| } | |
| } | |
| #undef STARTSWITH | |
| #undef MATCHES | |
| // Might have a script filename. If it looks like a filename, add | |
| // it to the SearchInfo struct for later reference. | |
| arg = search->restOfCmdLine; | |
| while(*arg && isspace(*arg)) { ++arg; } | |
| if (*arg && *arg != L'-') { | |
| search->scriptFile = arg; | |
| if (*arg == L'"') { | |
| ++search->scriptFile; | |
| while (*++arg && *arg != L'"') { } | |
| } else { | |
| while (*arg && !isspace(*arg)) { ++arg; } | |
| } | |
| search->scriptFileLength = (int)(arg - search->scriptFile); | |
| } | |
| return 0; | |
| } | |
| int | |
| _decodeShebang(SearchInfo *search, const char *buffer, int bufferLength, bool onlyUtf8, wchar_t **decoded, int *decodedLength) | |
| { | |
| DWORD cp = CP_UTF8; | |
| int wideLen = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, buffer, bufferLength, NULL, 0); | |
| if (!wideLen) { | |
| cp = CP_ACP; | |
| wideLen = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, buffer, bufferLength, NULL, 0); | |
| if (!wideLen) { | |
| debug(L"# Failed to decode shebang line (0x%08X)\n", GetLastError()); | |
| return RC_BAD_VIRTUAL_PATH; | |
| } | |
| } | |
| wchar_t *b = allocSearchInfoBuffer(search, wideLen + 1); | |
| if (!b) { | |
| return RC_NO_MEMORY; | |
| } | |
| wideLen = MultiByteToWideChar(cp, 0, buffer, bufferLength, b, wideLen + 1); | |
| if (!wideLen) { | |
| debug(L"# Failed to decode shebang line (0x%08X)\n", GetLastError()); | |
| return RC_BAD_VIRTUAL_PATH; | |
| } | |
| b[wideLen] = L'\0'; | |
| *decoded = b; | |
| *decodedLength = wideLen; | |
| return 0; | |
| } | |
| bool | |
| _shebangStartsWith(const wchar_t *buffer, int bufferLength, const wchar_t *prefix, const wchar_t **rest, int *firstArgumentLength) | |
| { | |
| int prefixLength = (int)wcsnlen_s(prefix, MAXLEN); | |
| if (bufferLength < prefixLength || !_startsWithArgument(buffer, bufferLength, prefix, prefixLength)) { | |
| return false; | |
| } | |
| if (rest) { | |
| *rest = &buffer[prefixLength]; | |
| } | |
| if (firstArgumentLength) { | |
| int i = prefixLength; | |
| while (i < bufferLength && !isspace(buffer[i])) { | |
| i += 1; | |
| } | |
| *firstArgumentLength = i - prefixLength; | |
| } | |
| return true; | |
| } | |
| int | |
| ensure_no_redirector_stub(wchar_t* filename, wchar_t* buffer) | |
| { | |
| // Make sure we didn't find a reparse point that will open the Microsoft Store | |
| // If we did, pretend there was no shebang and let normal handling take over | |
| WIN32_FIND_DATAW findData; | |
| HANDLE hFind = FindFirstFileW(buffer, &findData); | |
| if (!hFind) { | |
| // Let normal handling take over | |
| debug(L"# Did not find %s on PATH\n", filename); | |
| return RC_NO_SHEBANG; | |
| } | |
| FindClose(hFind); | |
| if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && | |
| findData.dwReserved0 & IO_REPARSE_TAG_APPEXECLINK)) { | |
| return 0; | |
| } | |
| HANDLE hReparsePoint = CreateFileW(buffer, 0, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT, NULL); | |
| if (!hReparsePoint) { | |
| // Let normal handling take over | |
| debug(L"# Did not find %s on PATH\n", filename); | |
| return RC_NO_SHEBANG; | |
| } | |
| AppExecLinkFile appExecLink; | |
| if (!DeviceIoControl(hReparsePoint, FSCTL_GET_REPARSE_POINT, NULL, 0, &appExecLink, sizeof(appExecLink), NULL, NULL)) { | |
| // Let normal handling take over | |
| debug(L"# Did not find %s on PATH\n", filename); | |
| CloseHandle(hReparsePoint); | |
| return RC_NO_SHEBANG; | |
| } | |
| CloseHandle(hReparsePoint); | |
| const wchar_t* redirectorPackageId = L"Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"; | |
| if (0 == wcscmp(appExecLink.stringList, redirectorPackageId)) { | |
| debug(L"# ignoring redirector that would launch store\n"); | |
| return RC_NO_SHEBANG; | |
| } | |
| return 0; | |
| } | |
| int | |
| searchPath(SearchInfo *search, const wchar_t *shebang, int shebangLength) | |
| { | |
| if (isEnvVarSet(L"PYLAUNCHER_NO_SEARCH_PATH")) { | |
| return RC_NO_SHEBANG; | |
| } | |
| wchar_t *command; | |
| int commandLength; | |
| if (!_shebangStartsWith(shebang, shebangLength, L"/usr/bin/env ", &command, &commandLength)) { | |
| return RC_NO_SHEBANG; | |
| } | |
| if (!commandLength || commandLength == MAXLEN) { | |
| return RC_BAD_VIRTUAL_PATH; | |
| } | |
| int lastDot = commandLength; | |
| while (lastDot > 0 && command[lastDot] != L'.') { | |
| lastDot -= 1; | |
| } | |
| if (!lastDot) { | |
| lastDot = commandLength; | |
| } | |
| wchar_t filename[MAXLEN]; | |
| if (wcsncpy_s(filename, MAXLEN, command, commandLength)) { | |
| return RC_BAD_VIRTUAL_PATH; | |
| } | |
| const wchar_t *ext = L".exe"; | |
| // If the command already has an extension, we do not want to add it again | |
| if (!lastDot || _comparePath(&filename[lastDot], -1, ext, -1)) { | |
| if (wcscat_s(filename, MAXLEN, L".exe")) { | |
| return RC_BAD_VIRTUAL_PATH; | |
| } | |
| } | |
| debug(L"# Search PATH for %s\n", filename); | |
| wchar_t pathVariable[MAXLEN]; | |
| int n = GetEnvironmentVariableW(L"PATH", pathVariable, MAXLEN); | |
| if (!n) { | |
| if (GetLastError() == ERROR_ENVVAR_NOT_FOUND) { | |
| return RC_NO_SHEBANG; | |
| } | |
| winerror(0, L"Failed to read PATH\n", filename); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| wchar_t buffer[MAXLEN]; | |
| n = SearchPathW(pathVariable, filename, NULL, MAXLEN, buffer, NULL); | |
| if (!n) { | |
| if (GetLastError() == ERROR_FILE_NOT_FOUND) { | |
| debug(L"# Did not find %s on PATH\n", filename); | |
| // If we didn't find it on PATH, let normal handling take over | |
| return RC_NO_SHEBANG; | |
| } | |
| // Other errors should cause us to break | |
| winerror(0, L"Failed to find %s on PATH\n", filename); | |
| return RC_BAD_VIRTUAL_PATH; | |
| } | |
| int result = ensure_no_redirector_stub(filename, buffer); | |
| if (result) { | |
| return result; | |
| } | |
| // Check that we aren't going to call ourselves again | |
| // If we are, pretend there was no shebang and let normal handling take over | |
| if (GetModuleFileNameW(NULL, filename, MAXLEN) && | |
| 0 == _comparePath(filename, -1, buffer, -1)) { | |
| debug(L"# ignoring recursive shebang command\n"); | |
| return RC_RECURSIVE_SHEBANG; | |
| } | |
| wchar_t *buf = allocSearchInfoBuffer(search, n + 1); | |
| if (!buf || wcscpy_s(buf, n + 1, buffer)) { | |
| return RC_NO_MEMORY; | |
| } | |
| search->executablePath = buf; | |
| search->executableArgs = &command[commandLength]; | |
| search->executableArgsLength = shebangLength - commandLength; | |
| debug(L"# Found %s on PATH\n", buf); | |
| return 0; | |
| } | |
| int | |
| _readIni(const wchar_t *section, const wchar_t *settingName, wchar_t *buffer, int bufferLength) | |
| { | |
| wchar_t iniPath[MAXLEN]; | |
| int n; | |
| if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, iniPath)) && | |
| join(iniPath, MAXLEN, L"py.ini")) { | |
| debug(L"# Reading from %s for %s/%s\n", iniPath, section, settingName); | |
| n = GetPrivateProfileStringW(section, settingName, NULL, buffer, bufferLength, iniPath); | |
| if (n) { | |
| debug(L"# Found %s in %s\n", settingName, iniPath); | |
| return n; | |
| } else if (GetLastError() == ERROR_FILE_NOT_FOUND) { | |
| debug(L"# Did not find file %s\n", iniPath); | |
| } else { | |
| winerror(0, L"Failed to read from %s\n", iniPath); | |
| } | |
| } | |
| if (GetModuleFileNameW(NULL, iniPath, MAXLEN) && | |
| SUCCEEDED(PathCchRemoveFileSpec(iniPath, MAXLEN)) && | |
| join(iniPath, MAXLEN, L"py.ini")) { | |
| debug(L"# Reading from %s for %s/%s\n", iniPath, section, settingName); | |
| n = GetPrivateProfileStringW(section, settingName, NULL, buffer, MAXLEN, iniPath); | |
| if (n) { | |
| debug(L"# Found %s in %s\n", settingName, iniPath); | |
| return n; | |
| } else if (GetLastError() == ERROR_FILE_NOT_FOUND) { | |
| debug(L"# Did not find file %s\n", iniPath); | |
| } else { | |
| winerror(0, L"Failed to read from %s\n", iniPath); | |
| } | |
| } | |
| return 0; | |
| } | |
| bool | |
| _findCommand(SearchInfo *search, const wchar_t *command, int commandLength) | |
| { | |
| wchar_t commandBuffer[MAXLEN]; | |
| wchar_t buffer[MAXLEN]; | |
| wcsncpy_s(commandBuffer, MAXLEN, command, commandLength); | |
| int n = _readIni(L"commands", commandBuffer, buffer, MAXLEN); | |
| if (!n) { | |
| return false; | |
| } | |
| wchar_t *path = allocSearchInfoBuffer(search, n + 1); | |
| if (!path) { | |
| return false; | |
| } | |
| wcscpy_s(path, n + 1, buffer); | |
| search->executablePath = path; | |
| return true; | |
| } | |
| int | |
| _useShebangAsExecutable(SearchInfo *search, const wchar_t *shebang, int shebangLength) | |
| { | |
| wchar_t buffer[MAXLEN]; | |
| wchar_t script[MAXLEN]; | |
| wchar_t command[MAXLEN]; | |
| int commandLength = 0; | |
| int inQuote = 0; | |
| if (!shebang || !shebangLength) { | |
| return 0; | |
| } | |
| wchar_t *pC = command; | |
| for (int i = 0; i < shebangLength; ++i) { | |
| wchar_t c = shebang[i]; | |
| if (isspace(c) && !inQuote) { | |
| commandLength = i; | |
| break; | |
| } else if (c == L'"') { | |
| inQuote = !inQuote; | |
| } else if (c == L'/' || c == L'\\') { | |
| *pC++ = L'\\'; | |
| } else { | |
| *pC++ = c; | |
| } | |
| } | |
| *pC = L'\0'; | |
| if (!GetCurrentDirectoryW(MAXLEN, buffer) || | |
| wcsncpy_s(script, MAXLEN, search->scriptFile, search->scriptFileLength) || | |
| FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, script, | |
| PATHCCH_ALLOW_LONG_PATHS)) || | |
| FAILED(PathCchRemoveFileSpec(buffer, MAXLEN)) || | |
| FAILED(PathCchCombineEx(buffer, MAXLEN, buffer, command, | |
| PATHCCH_ALLOW_LONG_PATHS)) | |
| ) { | |
| return RC_NO_MEMORY; | |
| } | |
| int n = (int)wcsnlen(buffer, MAXLEN); | |
| wchar_t *path = allocSearchInfoBuffer(search, n + 1); | |
| if (!path) { | |
| return RC_NO_MEMORY; | |
| } | |
| wcscpy_s(path, n + 1, buffer); | |
| search->executablePath = path; | |
| if (commandLength) { | |
| search->executableArgs = &shebang[commandLength]; | |
| search->executableArgsLength = shebangLength - commandLength; | |
| } | |
| return 0; | |
| } | |
| int | |
| checkShebang(SearchInfo *search) | |
| { | |
| // Do not check shebang if a tag was provided or if no script file | |
| // was found on the command line. | |
| if (search->tag || !search->scriptFile) { | |
| return 0; | |
| } | |
| if (search->scriptFileLength < 0) { | |
| search->scriptFileLength = (int)wcsnlen_s(search->scriptFile, MAXLEN); | |
| } | |
| wchar_t *scriptFile = (wchar_t*)malloc(sizeof(wchar_t) * (search->scriptFileLength + 1)); | |
| if (!scriptFile) { | |
| return RC_NO_MEMORY; | |
| } | |
| wcsncpy_s(scriptFile, search->scriptFileLength + 1, | |
| search->scriptFile, search->scriptFileLength); | |
| HANDLE hFile = CreateFileW(scriptFile, GENERIC_READ, | |
| FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | |
| NULL, OPEN_EXISTING, 0, NULL); | |
| if (hFile == INVALID_HANDLE_VALUE) { | |
| debug(L"# Failed to open %s for shebang parsing (0x%08X)\n", | |
| scriptFile, GetLastError()); | |
| free(scriptFile); | |
| return 0; | |
| } | |
| DWORD bytesRead = 0; | |
| char buffer[4096]; | |
| if (!ReadFile(hFile, buffer, sizeof(buffer), &bytesRead, NULL)) { | |
| debug(L"# Failed to read %s for shebang parsing (0x%08X)\n", | |
| scriptFile, GetLastError()); | |
| free(scriptFile); | |
| return 0; | |
| } | |
| CloseHandle(hFile); | |
| debug(L"# Read %d bytes from %s to find shebang line\n", bytesRead, scriptFile); | |
| free(scriptFile); | |
| char *b = buffer; | |
| bool onlyUtf8 = false; | |
| if (bytesRead > 3 && *b == 0xEF) { | |
| if (*++b == 0xBB && *++b == 0xBF) { | |
| // Allow a UTF-8 BOM | |
| ++b; | |
| bytesRead -= 3; | |
| onlyUtf8 = true; | |
| } else { | |
| debug(L"# Invalid BOM in shebang line"); | |
| return 0; | |
| } | |
| } | |
| if (bytesRead <= 2 || b[0] != '#' || b[1] != '!') { | |
| // No shebang (#!) at start of line | |
| debug(L"# No valid shebang line"); | |
| return 0; | |
| } | |
| ++b; | |
| --bytesRead; | |
| while (--bytesRead > 0 && isspace(*++b)) { } | |
| char *start = b; | |
| while (--bytesRead > 0 && *++b != '\r' && *b != '\n') { } | |
| wchar_t *shebang; | |
| int shebangLength; | |
| // We add 1 when bytesRead==0, as in that case we hit EOF and b points | |
| // to the last character in the file, not the newline | |
| int exitCode = _decodeShebang(search, start, (int)(b - start + (bytesRead == 0)), onlyUtf8, &shebang, &shebangLength); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| debug(L"Shebang: %s\n", shebang); | |
| // Handle shebangs that we should search PATH for | |
| int executablePathWasSetByUsrBinEnv = 0; | |
| exitCode = searchPath(search, shebang, shebangLength); | |
| if (exitCode == 0) { | |
| executablePathWasSetByUsrBinEnv = 1; | |
| } else if (exitCode != RC_NO_SHEBANG) { | |
| return exitCode; | |
| } | |
| // Handle some known, case-sensitive shebangs | |
| const wchar_t *command; | |
| int commandLength; | |
| // Each template must end with "python" | |
| static const wchar_t *shebangTemplates[] = { | |
| L"/usr/bin/env python", | |
| L"/usr/bin/python", | |
| L"/usr/local/bin/python", | |
| L"python", | |
| NULL | |
| }; | |
| for (const wchar_t **tmpl = shebangTemplates; *tmpl; ++tmpl) { | |
| // Just to make sure we don't mess this up in the future | |
| assert(0 == wcscmp(L"python", (*tmpl) + wcslen(*tmpl) - 6)); | |
| if (_shebangStartsWith(shebang, shebangLength, *tmpl, &command, &commandLength)) { | |
| // Search for "python{command}" overrides. All templates end with | |
| // "python", so we prepend it by jumping back 6 characters | |
| if (_findCommand(search, &command[-6], commandLength + 6)) { | |
| search->executableArgs = &command[commandLength]; | |
| search->executableArgsLength = shebangLength - commandLength; | |
| debug(L"# Treating shebang command '%.*s' as %s\n", | |
| commandLength + 6, &command[-6], search->executablePath); | |
| return 0; | |
| } | |
| search->tag = command; | |
| search->tagLength = commandLength; | |
| // If we had 'python3.12.exe' then we want to strip the suffix | |
| // off of the tag | |
| if (search->tagLength >= 4) { | |
| const wchar_t *suffix = &search->tag[search->tagLength - 4]; | |
| if (0 == _comparePath(suffix, 4, L".exe", -1)) { | |
| search->tagLength -= 4; | |
| } | |
| } | |
| // If we had 'python3_d' then we want to strip the '_d' (any | |
| // '.exe' is already gone) | |
| if (search->tagLength >= 2) { | |
| const wchar_t *suffix = &search->tag[search->tagLength - 2]; | |
| if (0 == _comparePath(suffix, 2, L"_d", -1)) { | |
| search->tagLength -= 2; | |
| } | |
| } | |
| search->oldStyleTag = true; | |
| search->lowPriorityTag = true; | |
| search->executableArgs = &command[commandLength]; | |
| search->executableArgsLength = shebangLength - commandLength; | |
| if (search->tag && search->tagLength) { | |
| debug(L"# Treating shebang command '%.*s' as 'py -%.*s'\n", | |
| commandLength, command, search->tagLength, search->tag); | |
| } else { | |
| debug(L"# Treating shebang command '%.*s' as 'py'\n", | |
| commandLength, command); | |
| } | |
| return 0; | |
| } | |
| } | |
| // Didn't match a template, but we found it on PATH | |
| if (executablePathWasSetByUsrBinEnv) { | |
| return 0; | |
| } | |
| // Unrecognised executables are first tried as command aliases | |
| commandLength = 0; | |
| while (commandLength < shebangLength && !isspace(shebang[commandLength])) { | |
| commandLength += 1; | |
| } | |
| if (_findCommand(search, shebang, commandLength)) { | |
| search->executableArgs = &shebang[commandLength]; | |
| search->executableArgsLength = shebangLength - commandLength; | |
| debug(L"# Treating shebang command '%.*s' as %s\n", | |
| commandLength, shebang, search->executablePath); | |
| return 0; | |
| } | |
| // Unrecognised commands are joined to the script's directory and treated | |
| // as the executable path | |
| return _useShebangAsExecutable(search, shebang, shebangLength); | |
| } | |
| int | |
| checkDefaults(SearchInfo *search) | |
| { | |
| if (!search->allowDefaults) { | |
| return 0; | |
| } | |
| // Only resolve old-style (or absent) tags to defaults | |
| if (search->tag && search->tagLength && !search->oldStyleTag) { | |
| return 0; | |
| } | |
| // If tag is only a major version number, expand it from the environment | |
| // or an ini file | |
| const wchar_t *iniSettingName = NULL; | |
| const wchar_t *envSettingName = NULL; | |
| if (!search->tag || !search->tagLength) { | |
| iniSettingName = L"python"; | |
| envSettingName = L"py_python"; | |
| } else if (0 == wcsncmp(search->tag, L"3", search->tagLength)) { | |
| iniSettingName = L"python3"; | |
| envSettingName = L"py_python3"; | |
| } else if (0 == wcsncmp(search->tag, L"2", search->tagLength)) { | |
| iniSettingName = L"python2"; | |
| envSettingName = L"py_python2"; | |
| } else { | |
| debug(L"# Cannot select defaults for tag '%.*s'\n", search->tagLength, search->tag); | |
| return 0; | |
| } | |
| // First, try to read an environment variable | |
| wchar_t buffer[MAXLEN]; | |
| int n = GetEnvironmentVariableW(envSettingName, buffer, MAXLEN); | |
| // If none found, check in our two .ini files instead | |
| if (!n) { | |
| n = _readIni(L"defaults", iniSettingName, buffer, MAXLEN); | |
| } | |
| if (n) { | |
| wchar_t *tag = allocSearchInfoBuffer(search, n + 1); | |
| if (!tag) { | |
| return RC_NO_MEMORY; | |
| } | |
| wcscpy_s(tag, n + 1, buffer); | |
| wchar_t *slash = wcschr(tag, L'/'); | |
| if (!slash) { | |
| search->tag = tag; | |
| search->tagLength = n; | |
| search->oldStyleTag = true; | |
| } else { | |
| search->company = tag; | |
| search->companyLength = (int)(slash - tag); | |
| search->tag = slash + 1; | |
| search->tagLength = n - (search->companyLength + 1); | |
| search->oldStyleTag = false; | |
| } | |
| // gh-92817: allow a high priority env to be selected even if it | |
| // doesn't match the tag | |
| search->lowPriorityTag = true; | |
| } | |
| return 0; | |
| } | |
| /******************************************************************************\ | |
| *** ENVIRONMENT SEARCH *** | |
| \******************************************************************************/ | |
| typedef struct EnvironmentInfo { | |
| /* We use a binary tree and sort on insert */ | |
| struct EnvironmentInfo *prev; | |
| struct EnvironmentInfo *next; | |
| /* parent is only used when constructing */ | |
| struct EnvironmentInfo *parent; | |
| const wchar_t *company; | |
| const wchar_t *tag; | |
| int internalSortKey; | |
| const wchar_t *installDir; | |
| const wchar_t *executablePath; | |
| const wchar_t *executableArgs; | |
| const wchar_t *architecture; | |
| const wchar_t *displayName; | |
| bool highPriority; | |
| } EnvironmentInfo; | |
| int | |
| copyWstr(const wchar_t **dest, const wchar_t *src) | |
| { | |
| if (!dest) { | |
| return RC_NO_MEMORY; | |
| } | |
| if (!src) { | |
| *dest = NULL; | |
| return 0; | |
| } | |
| size_t n = wcsnlen_s(src, MAXLEN - 1) + 1; | |
| wchar_t *buffer = (wchar_t*)malloc(n * sizeof(wchar_t)); | |
| if (!buffer) { | |
| return RC_NO_MEMORY; | |
| } | |
| wcsncpy_s(buffer, n, src, n - 1); | |
| *dest = (const wchar_t*)buffer; | |
| return 0; | |
| } | |
| EnvironmentInfo * | |
| newEnvironmentInfo(const wchar_t *company, const wchar_t *tag) | |
| { | |
| EnvironmentInfo *env = (EnvironmentInfo *)malloc(sizeof(EnvironmentInfo)); | |
| if (!env) { | |
| return NULL; | |
| } | |
| memset(env, 0, sizeof(EnvironmentInfo)); | |
| int exitCode = copyWstr(&env->company, company); | |
| if (exitCode) { | |
| free((void *)env); | |
| return NULL; | |
| } | |
| exitCode = copyWstr(&env->tag, tag); | |
| if (exitCode) { | |
| free((void *)env->company); | |
| free((void *)env); | |
| return NULL; | |
| } | |
| return env; | |
| } | |
| void | |
| freeEnvironmentInfo(EnvironmentInfo *env) | |
| { | |
| if (env) { | |
| free((void *)env->company); | |
| free((void *)env->tag); | |
| free((void *)env->installDir); | |
| free((void *)env->executablePath); | |
| free((void *)env->executableArgs); | |
| free((void *)env->displayName); | |
| freeEnvironmentInfo(env->prev); | |
| env->prev = NULL; | |
| freeEnvironmentInfo(env->next); | |
| env->next = NULL; | |
| free((void *)env); | |
| } | |
| } | |
| /* Specific string comparisons for sorting the tree */ | |
| int | |
| _compareCompany(const wchar_t *x, const wchar_t *y) | |
| { | |
| if (!x && !y) { | |
| return 0; | |
| } else if (!x) { | |
| return -1; | |
| } else if (!y) { | |
| return 1; | |
| } | |
| bool coreX = 0 == _compare(x, -1, L"PythonCore", -1); | |
| bool coreY = 0 == _compare(y, -1, L"PythonCore", -1); | |
| if (coreX) { | |
| return coreY ? 0 : -1; | |
| } else if (coreY) { | |
| return 1; | |
| } | |
| return _compare(x, -1, y, -1); | |
| } | |
| int | |
| _compareTag(const wchar_t *x, const wchar_t *y) | |
| { | |
| if (!x && !y) { | |
| return 0; | |
| } else if (!x) { | |
| return -1; | |
| } else if (!y) { | |
| return 1; | |
| } | |
| // Compare up to the first dash. If not equal, that's our sort order | |
| const wchar_t *xDash = wcschr(x, L'-'); | |
| const wchar_t *yDash = wcschr(y, L'-'); | |
| int xToDash = xDash ? (int)(xDash - x) : -1; | |
| int yToDash = yDash ? (int)(yDash - y) : -1; | |
| int r = _compare(x, xToDash, y, yToDash); | |
| if (r) { | |
| return r; | |
| } | |
| // If we're equal up to the first dash, we want to sort one with | |
| // no dash *after* one with a dash. Otherwise, a reversed compare. | |
| // This works out because environments are sorted in descending tag | |
| // order, so that higher versions (probably) come first. | |
| // For PythonCore, our "X.Y" structure ensures that higher versions | |
| // come first. Everyone else will just have to deal with it. | |
| if (xDash && yDash) { | |
| return _compare(yDash, -1, xDash, -1); | |
| } else if (xDash) { | |
| return -1; | |
| } else if (yDash) { | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| int | |
| addEnvironmentInfo(EnvironmentInfo **root, EnvironmentInfo* parent, EnvironmentInfo *node) | |
| { | |
| EnvironmentInfo *r = *root; | |
| if (!r) { | |
| *root = node; | |
| node->parent = parent; | |
| return 0; | |
| } | |
| // Sort by company name | |
| switch (_compareCompany(node->company, r->company)) { | |
| case -1: | |
| return addEnvironmentInfo(&r->prev, r, node); | |
| case 1: | |
| return addEnvironmentInfo(&r->next, r, node); | |
| case 0: | |
| break; | |
| } | |
| // Then by tag (descending) | |
| switch (_compareTag(node->tag, r->tag)) { | |
| case -1: | |
| return addEnvironmentInfo(&r->next, r, node); | |
| case 1: | |
| return addEnvironmentInfo(&r->prev, r, node); | |
| case 0: | |
| break; | |
| } | |
| // Then keep the one with the lowest internal sort key | |
| if (node->internalSortKey < r->internalSortKey) { | |
| // Replace the current node | |
| node->parent = r->parent; | |
| if (node->parent) { | |
| if (node->parent->prev == r) { | |
| node->parent->prev = node; | |
| } else if (node->parent->next == r) { | |
| node->parent->next = node; | |
| } else { | |
| debug(L"# Inconsistent parent value in tree\n"); | |
| freeEnvironmentInfo(node); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| } else { | |
| // If node has no parent, then it is the root. | |
| *root = node; | |
| } | |
| node->next = r->next; | |
| node->prev = r->prev; | |
| debug(L"# replaced %s/%s/%i in tree\n", node->company, node->tag, node->internalSortKey); | |
| freeEnvironmentInfo(r); | |
| } else { | |
| debug(L"# not adding %s/%s/%i to tree\n", node->company, node->tag, node->internalSortKey); | |
| return RC_DUPLICATE_ITEM; | |
| } | |
| return 0; | |
| } | |
| /******************************************************************************\ | |
| *** REGISTRY SEARCH *** | |
| \******************************************************************************/ | |
| int | |
| _registryReadString(const wchar_t **dest, HKEY root, const wchar_t *subkey, const wchar_t *value) | |
| { | |
| // Note that this is bytes (hence 'cb'), not characters ('cch') | |
| DWORD cbData = 0; | |
| DWORD flags = RRF_RT_REG_SZ | RRF_RT_REG_EXPAND_SZ; | |
| if (ERROR_SUCCESS != RegGetValueW(root, subkey, value, flags, NULL, NULL, &cbData)) { | |
| return 0; | |
| } | |
| wchar_t *buffer = (wchar_t*)malloc(cbData); | |
| if (!buffer) { | |
| return RC_NO_MEMORY; | |
| } | |
| if (ERROR_SUCCESS == RegGetValueW(root, subkey, value, flags, NULL, buffer, &cbData)) { | |
| *dest = buffer; | |
| } else { | |
| free((void *)buffer); | |
| } | |
| return 0; | |
| } | |
| int | |
| _combineWithInstallDir(const wchar_t **dest, const wchar_t *installDir, const wchar_t *fragment, int fragmentLength) | |
| { | |
| wchar_t buffer[MAXLEN]; | |
| wchar_t fragmentBuffer[MAXLEN]; | |
| if (wcsncpy_s(fragmentBuffer, MAXLEN, fragment, fragmentLength)) { | |
| return RC_NO_MEMORY; | |
| } | |
| if (FAILED(PathCchCombineEx(buffer, MAXLEN, installDir, fragmentBuffer, PATHCCH_ALLOW_LONG_PATHS))) { | |
| return RC_NO_MEMORY; | |
| } | |
| return copyWstr(dest, buffer); | |
| } | |
| bool | |
| _isLegacyVersion(EnvironmentInfo *env) | |
| { | |
| // Check if backwards-compatibility is required. | |
| // Specifically PythonCore versions 2.X and 3.0 - 3.5 do not implement PEP 514. | |
| if (0 != _compare(env->company, -1, L"PythonCore", -1)) { | |
| return false; | |
| } | |
| int versionMajor, versionMinor; | |
| int n = swscanf_s(env->tag, L"%d.%d", &versionMajor, &versionMinor); | |
| if (n != 2) { | |
| debug(L"# %s/%s has an invalid version tag\n", env->company, env->tag); | |
| return false; | |
| } | |
| return versionMajor == 2 | |
| || (versionMajor == 3 && versionMinor >= 0 && versionMinor <= 5); | |
| } | |
| int | |
| _registryReadLegacyEnvironment(const SearchInfo *search, HKEY root, EnvironmentInfo *env, const wchar_t *fallbackArch) | |
| { | |
| // Backwards-compatibility for PythonCore versions which do not implement PEP 514. | |
| int exitCode = _combineWithInstallDir( | |
| &env->executablePath, | |
| env->installDir, | |
| search->executable, | |
| search->executableLength | |
| ); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| if (search->windowed) { | |
| exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"WindowedExecutableArguments"); | |
| } | |
| else { | |
| exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"ExecutableArguments"); | |
| } | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| if (fallbackArch) { | |
| copyWstr(&env->architecture, fallbackArch); | |
| } else { | |
| DWORD binaryType; | |
| BOOL success = GetBinaryTypeW(env->executablePath, &binaryType); | |
| if (!success) { | |
| return RC_NO_PYTHON; | |
| } | |
| switch (binaryType) { | |
| case SCS_32BIT_BINARY: | |
| copyWstr(&env->architecture, L"32bit"); | |
| break; | |
| case SCS_64BIT_BINARY: | |
| copyWstr(&env->architecture, L"64bit"); | |
| break; | |
| default: | |
| return RC_NO_PYTHON; | |
| } | |
| } | |
| if (0 == _compare(env->architecture, -1, L"32bit", -1)) { | |
| size_t tagLength = wcslen(env->tag); | |
| if (tagLength <= 3 || 0 != _compare(&env->tag[tagLength - 3], 3, L"-32", 3)) { | |
| const wchar_t *rawTag = env->tag; | |
| wchar_t *realTag = (wchar_t*) malloc(sizeof(wchar_t) * (tagLength + 4)); | |
| if (!realTag) { | |
| return RC_NO_MEMORY; | |
| } | |
| int count = swprintf_s(realTag, tagLength + 4, L"%s-32", env->tag); | |
| if (count == -1) { | |
| debug(L"# Failed to generate 32bit tag\n"); | |
| free(realTag); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| env->tag = realTag; | |
| free((void*)rawTag); | |
| } | |
| } | |
| wchar_t buffer[MAXLEN]; | |
| if (swprintf_s(buffer, MAXLEN, L"Python %s", env->tag)) { | |
| copyWstr(&env->displayName, buffer); | |
| } | |
| return 0; | |
| } | |
| int | |
| _registryReadEnvironment(const SearchInfo *search, HKEY root, EnvironmentInfo *env, const wchar_t *fallbackArch) | |
| { | |
| int exitCode = _registryReadString(&env->installDir, root, L"InstallPath", NULL); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| if (!env->installDir) { | |
| return RC_NO_PYTHON; | |
| } | |
| if (_isLegacyVersion(env)) { | |
| return _registryReadLegacyEnvironment(search, root, env, fallbackArch); | |
| } | |
| // If pythonw.exe requested, check specific value | |
| if (search->windowed) { | |
| exitCode = _registryReadString(&env->executablePath, root, L"InstallPath", L"WindowedExecutablePath"); | |
| if (!exitCode && env->executablePath) { | |
| exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"WindowedExecutableArguments"); | |
| } | |
| } | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| // Missing windowed path or non-windowed request means we use ExecutablePath | |
| if (!env->executablePath) { | |
| exitCode = _registryReadString(&env->executablePath, root, L"InstallPath", L"ExecutablePath"); | |
| if (!exitCode && env->executablePath) { | |
| exitCode = _registryReadString(&env->executableArgs, root, L"InstallPath", L"ExecutableArguments"); | |
| } | |
| } | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| if (!env->executablePath) { | |
| debug(L"# %s/%s has no executable path\n", env->company, env->tag); | |
| return RC_NO_PYTHON; | |
| } | |
| exitCode = _registryReadString(&env->architecture, root, NULL, L"SysArchitecture"); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| exitCode = _registryReadString(&env->displayName, root, NULL, L"DisplayName"); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| return 0; | |
| } | |
| int | |
| _registrySearchTags(const SearchInfo *search, EnvironmentInfo **result, HKEY root, int sortKey, const wchar_t *company, const wchar_t *fallbackArch) | |
| { | |
| wchar_t buffer[256]; | |
| int err = 0; | |
| int exitCode = 0; | |
| for (int i = 0; exitCode == 0; ++i) { | |
| DWORD cchBuffer = sizeof(buffer) / sizeof(buffer[0]); | |
| err = RegEnumKeyExW(root, i, buffer, &cchBuffer, NULL, NULL, NULL, NULL); | |
| if (err) { | |
| if (err != ERROR_NO_MORE_ITEMS) { | |
| winerror(0, L"Failed to read installs (tags) from the registry"); | |
| } | |
| break; | |
| } | |
| HKEY subkey; | |
| if (ERROR_SUCCESS == RegOpenKeyExW(root, buffer, 0, KEY_READ, &subkey)) { | |
| EnvironmentInfo *env = newEnvironmentInfo(company, buffer); | |
| env->internalSortKey = sortKey; | |
| exitCode = _registryReadEnvironment(search, subkey, env, fallbackArch); | |
| RegCloseKey(subkey); | |
| if (exitCode == RC_NO_PYTHON) { | |
| freeEnvironmentInfo(env); | |
| exitCode = 0; | |
| } else if (!exitCode) { | |
| exitCode = addEnvironmentInfo(result, NULL, env); | |
| if (exitCode) { | |
| freeEnvironmentInfo(env); | |
| if (exitCode == RC_DUPLICATE_ITEM) { | |
| exitCode = 0; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return exitCode; | |
| } | |
| int | |
| registrySearch(const SearchInfo *search, EnvironmentInfo **result, HKEY root, int sortKey, const wchar_t *fallbackArch) | |
| { | |
| wchar_t buffer[256]; | |
| int err = 0; | |
| int exitCode = 0; | |
| for (int i = 0; exitCode == 0; ++i) { | |
| DWORD cchBuffer = sizeof(buffer) / sizeof(buffer[0]); | |
| err = RegEnumKeyExW(root, i, buffer, &cchBuffer, NULL, NULL, NULL, NULL); | |
| if (err) { | |
| if (err != ERROR_NO_MORE_ITEMS) { | |
| winerror(0, L"Failed to read distributors (company) from the registry"); | |
| } | |
| break; | |
| } | |
| if (search->limitToCompany && 0 != _compare(search->limitToCompany, -1, buffer, cchBuffer)) { | |
| debug(L"# Skipping %s due to PYLAUNCHER_LIMIT_TO_COMPANY\n", buffer); | |
| continue; | |
| } | |
| HKEY subkey; | |
| if (ERROR_SUCCESS == RegOpenKeyExW(root, buffer, 0, KEY_READ, &subkey)) { | |
| exitCode = _registrySearchTags(search, result, subkey, sortKey, buffer, fallbackArch); | |
| RegCloseKey(subkey); | |
| } | |
| } | |
| return exitCode; | |
| } | |
| /******************************************************************************\ | |
| *** APP PACKAGE SEARCH *** | |
| \******************************************************************************/ | |
| int | |
| appxSearch(const SearchInfo *search, EnvironmentInfo **result, const wchar_t *packageFamilyName, const wchar_t *tag, int sortKey) | |
| { | |
| wchar_t realTag[32]; | |
| wchar_t buffer[MAXLEN]; | |
| const wchar_t *exeName = search->executable; | |
| if (!exeName || search->allowExecutableOverride) { | |
| exeName = search->windowed ? L"pythonw.exe" : L"python.exe"; | |
| } | |
| // Failure to get LocalAppData may just mean we're running as a user who | |
| // doesn't have a profile directory. | |
| // In this case, return "not found", but don't fail. | |
| // Chances are they can't launch Store installs anyway. | |
| if (FAILED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, buffer))) { | |
| return RC_NO_PYTHON; | |
| } | |
| if (!join(buffer, MAXLEN, L"Microsoft\\WindowsApps") || | |
| !join(buffer, MAXLEN, packageFamilyName) || | |
| !join(buffer, MAXLEN, exeName)) { | |
| debug(L"# Failed to construct App Execution Alias path\n"); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(buffer)) { | |
| return RC_NO_PYTHON; | |
| } | |
| // Assume packages are native architecture, which means we need to append | |
| // the '-arm64' on ARM64 host. | |
| wcscpy_s(realTag, 32, tag); | |
| if (isARM64Host()) { | |
| wcscat_s(realTag, 32, L"-arm64"); | |
| } | |
| EnvironmentInfo *env = newEnvironmentInfo(L"PythonCore", realTag); | |
| if (!env) { | |
| return RC_NO_MEMORY; | |
| } | |
| env->internalSortKey = sortKey; | |
| if (isAMD64Host()) { | |
| copyWstr(&env->architecture, L"64bit"); | |
| } else if (isARM64Host()) { | |
| copyWstr(&env->architecture, L"ARM64"); | |
| } | |
| copyWstr(&env->executablePath, buffer); | |
| if (swprintf_s(buffer, MAXLEN, L"Python %s (Store)", tag)) { | |
| copyWstr(&env->displayName, buffer); | |
| } | |
| int exitCode = addEnvironmentInfo(result, NULL, env); | |
| if (exitCode) { | |
| freeEnvironmentInfo(env); | |
| if (exitCode == RC_DUPLICATE_ITEM) { | |
| exitCode = 0; | |
| } | |
| } | |
| return exitCode; | |
| } | |
| /******************************************************************************\ | |
| *** OVERRIDDEN EXECUTABLE PATH *** | |
| \******************************************************************************/ | |
| int | |
| explicitOverrideSearch(const SearchInfo *search, EnvironmentInfo **result) | |
| { | |
| if (!search->executablePath) { | |
| return 0; | |
| } | |
| EnvironmentInfo *env = newEnvironmentInfo(NULL, NULL); | |
| if (!env) { | |
| return RC_NO_MEMORY; | |
| } | |
| env->internalSortKey = 10; | |
| int exitCode = copyWstr(&env->executablePath, search->executablePath); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| exitCode = copyWstr(&env->displayName, L"Explicit override"); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| exitCode = addEnvironmentInfo(result, NULL, env); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| return 0; | |
| abort: | |
| freeEnvironmentInfo(env); | |
| if (exitCode == RC_DUPLICATE_ITEM) { | |
| exitCode = 0; | |
| } | |
| return exitCode; | |
| } | |
| /******************************************************************************\ | |
| *** ACTIVE VIRTUAL ENVIRONMENT SEARCH *** | |
| \******************************************************************************/ | |
| int | |
| virtualenvSearch(const SearchInfo *search, EnvironmentInfo **result) | |
| { | |
| int exitCode = 0; | |
| EnvironmentInfo *env = NULL; | |
| wchar_t buffer[MAXLEN]; | |
| int n = GetEnvironmentVariableW(L"VIRTUAL_ENV", buffer, MAXLEN); | |
| if (!n || !join(buffer, MAXLEN, L"Scripts") || !join(buffer, MAXLEN, search->executable)) { | |
| return 0; | |
| } | |
| DWORD attr = GetFileAttributesW(buffer); | |
| if (INVALID_FILE_ATTRIBUTES == attr && search->lowPriorityTag) { | |
| if (!split_parent(buffer, MAXLEN) || !join(buffer, MAXLEN, L"python.exe")) { | |
| return 0; | |
| } | |
| attr = GetFileAttributesW(buffer); | |
| } | |
| if (INVALID_FILE_ATTRIBUTES == attr) { | |
| debug(L"Python executable %s missing from virtual env\n", buffer); | |
| return 0; | |
| } | |
| env = newEnvironmentInfo(NULL, NULL); | |
| if (!env) { | |
| return RC_NO_MEMORY; | |
| } | |
| env->highPriority = true; | |
| env->internalSortKey = 20; | |
| exitCode = copyWstr(&env->displayName, L"Active venv"); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| exitCode = copyWstr(&env->executablePath, buffer); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| exitCode = addEnvironmentInfo(result, NULL, env); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| return 0; | |
| abort: | |
| freeEnvironmentInfo(env); | |
| if (exitCode == RC_DUPLICATE_ITEM) { | |
| return 0; | |
| } | |
| return exitCode; | |
| } | |
| /******************************************************************************\ | |
| *** COLLECT ENVIRONMENTS *** | |
| \******************************************************************************/ | |
| struct RegistrySearchInfo { | |
| // Registry subkey to search | |
| const wchar_t *subkey; | |
| // Registry hive to search | |
| HKEY hive; | |
| // Flags to use when opening the subkey | |
| DWORD flags; | |
| // Internal sort key to select between "identical" environments discovered | |
| // through different methods | |
| int sortKey; | |
| // Fallback value to assume for PythonCore entries missing a SysArchitecture value | |
| const wchar_t *fallbackArch; | |
| }; | |
| struct RegistrySearchInfo REGISTRY_SEARCH[] = { | |
| { | |
| L"Software\\Python", | |
| HKEY_CURRENT_USER, | |
| KEY_READ, | |
| 1, | |
| NULL | |
| }, | |
| { | |
| L"Software\\Python", | |
| HKEY_LOCAL_MACHINE, | |
| KEY_READ | KEY_WOW64_64KEY, | |
| 3, | |
| L"64bit" | |
| }, | |
| { | |
| L"Software\\Python", | |
| HKEY_LOCAL_MACHINE, | |
| KEY_READ | KEY_WOW64_32KEY, | |
| 4, | |
| L"32bit" | |
| }, | |
| { NULL, 0, 0, 0, NULL } | |
| }; | |
| struct AppxSearchInfo { | |
| // The package family name. Can be found for an installed package using the | |
| // Powershell "Get-AppxPackage" cmdlet | |
| const wchar_t *familyName; | |
| // The tag to treat the installation as | |
| const wchar_t *tag; | |
| // Internal sort key to select between "identical" environments discovered | |
| // through different methods | |
| int sortKey; | |
| }; | |
| struct AppxSearchInfo APPX_SEARCH[] = { | |
| // Releases made through the Store | |
| { L"PythonSoftwareFoundation.Python.3.14_qbz5n2kfra8p0", L"3.14", 10 }, | |
| { L"PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0", L"3.13", 10 }, | |
| { L"PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0", L"3.12", 10 }, | |
| { L"PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0", L"3.11", 10 }, | |
| { L"PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0", L"3.10", 10 }, | |
| { L"PythonSoftwareFoundation.Python.3.9_qbz5n2kfra8p0", L"3.9", 10 }, | |
| { L"PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0", L"3.8", 10 }, | |
| // Side-loadable releases. Note that the publisher ID changes whenever we | |
| // change our code signing certificate subject, so the newer IDs have higher | |
| // priorities (lower sortKey) | |
| { L"PythonSoftwareFoundation.Python.3.14_3847v3x7pw1km", L"3.14", 11 }, | |
| { L"PythonSoftwareFoundation.Python.3.13_3847v3x7pw1km", L"3.13", 11 }, | |
| { L"PythonSoftwareFoundation.Python.3.12_3847v3x7pw1km", L"3.12", 11 }, | |
| { L"PythonSoftwareFoundation.Python.3.11_3847v3x7pw1km", L"3.11", 11 }, | |
| { L"PythonSoftwareFoundation.Python.3.11_hd69rhyc2wevp", L"3.11", 12 }, | |
| { L"PythonSoftwareFoundation.Python.3.10_3847v3x7pw1km", L"3.10", 11 }, | |
| { L"PythonSoftwareFoundation.Python.3.10_hd69rhyc2wevp", L"3.10", 12 }, | |
| { L"PythonSoftwareFoundation.Python.3.9_3847v3x7pw1km", L"3.9", 11 }, | |
| { L"PythonSoftwareFoundation.Python.3.9_hd69rhyc2wevp", L"3.9", 12 }, | |
| { L"PythonSoftwareFoundation.Python.3.8_hd69rhyc2wevp", L"3.8", 12 }, | |
| { NULL, NULL, 0 } | |
| }; | |
| int | |
| collectEnvironments(const SearchInfo *search, EnvironmentInfo **result) | |
| { | |
| int exitCode = 0; | |
| HKEY root; | |
| EnvironmentInfo *env = NULL; | |
| if (!result) { | |
| debug(L"# collectEnvironments() was passed a NULL result\n"); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| *result = NULL; | |
| exitCode = explicitOverrideSearch(search, result); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| exitCode = virtualenvSearch(search, result); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| // If we aren't collecting all items to list them, we can exit now. | |
| if (env && !(search->list || search->listPaths)) { | |
| return 0; | |
| } | |
| for (struct RegistrySearchInfo *info = REGISTRY_SEARCH; info->subkey; ++info) { | |
| if (ERROR_SUCCESS == RegOpenKeyExW(info->hive, info->subkey, 0, info->flags, &root)) { | |
| exitCode = registrySearch(search, result, root, info->sortKey, info->fallbackArch); | |
| RegCloseKey(root); | |
| } | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| } | |
| if (search->limitToCompany) { | |
| debug(L"# Skipping APPX search due to PYLAUNCHER_LIMIT_TO_COMPANY\n"); | |
| return 0; | |
| } | |
| for (struct AppxSearchInfo *info = APPX_SEARCH; info->familyName; ++info) { | |
| exitCode = appxSearch(search, result, info->familyName, info->tag, info->sortKey); | |
| if (exitCode && exitCode != RC_NO_PYTHON) { | |
| return exitCode; | |
| } | |
| } | |
| return 0; | |
| } | |
| /******************************************************************************\ | |
| *** INSTALL ON DEMAND *** | |
| \******************************************************************************/ | |
| struct StoreSearchInfo { | |
| // The tag a user is looking for | |
| const wchar_t *tag; | |
| // The Store ID for a package if it can be installed from the Microsoft | |
| // Store. These are obtained from the dashboard at | |
| // https://partner.microsoft.com/dashboard | |
| const wchar_t *storeId; | |
| }; | |
| struct StoreSearchInfo STORE_SEARCH[] = { | |
| { L"3", /* 3.13 */ L"9PNRBTZXMB4Z" }, | |
| { L"3.14", L"9NTRHQCBBPR8" }, | |
| { L"3.13", L"9PNRBTZXMB4Z" }, | |
| { L"3.12", L"9NCVDN91XZQP" }, | |
| { L"3.11", L"9NRWMJP3717K" }, | |
| { L"3.10", L"9PJPW5LDXLZ5" }, | |
| { L"3.9", L"9P7QFQMJRFP7" }, | |
| { L"3.8", L"9MSSZTT1N39L" }, | |
| { NULL, NULL } | |
| }; | |
| int | |
| _installEnvironment(const wchar_t *command, const wchar_t *arguments) | |
| { | |
| SHELLEXECUTEINFOW siw = { | |
| sizeof(SHELLEXECUTEINFOW), | |
| SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE, | |
| NULL, NULL, | |
| command, arguments, NULL, | |
| SW_SHOWNORMAL | |
| }; | |
| debug(L"# Installing with %s %s\n", command, arguments); | |
| if (isEnvVarSet(L"PYLAUNCHER_DRYRUN")) { | |
| debug(L"# Exiting due to PYLAUNCHER_DRYRUN\n"); | |
| fflush(stdout); | |
| int mode = _setmode(_fileno(stdout), _O_U8TEXT); | |
| if (arguments) { | |
| fwprintf_s(stdout, L"\"%s\" %s\n", command, arguments); | |
| } else { | |
| fwprintf_s(stdout, L"\"%s\"\n", command); | |
| } | |
| fflush(stdout); | |
| if (mode >= 0) { | |
| _setmode(_fileno(stdout), mode); | |
| } | |
| return RC_INSTALLING; | |
| } | |
| if (!ShellExecuteExW(&siw)) { | |
| return RC_NO_PYTHON; | |
| } | |
| if (!siw.hProcess) { | |
| return RC_INSTALLING; | |
| } | |
| WaitForSingleObjectEx(siw.hProcess, INFINITE, FALSE); | |
| DWORD exitCode = 0; | |
| if (GetExitCodeProcess(siw.hProcess, &exitCode) && exitCode == 0) { | |
| return 0; | |
| } | |
| return RC_INSTALLING; | |
| } | |
| const wchar_t *WINGET_COMMAND = L"Microsoft\\WindowsApps\\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\\winget.exe"; | |
| const wchar_t *WINGET_ARGUMENTS = L"install -q %s --exact --accept-package-agreements --source msstore"; | |
| const wchar_t *MSSTORE_COMMAND = L"ms-windows-store://pdp/?productid=%s"; | |
| int | |
| installEnvironment(const SearchInfo *search) | |
| { | |
| // No tag? No installing | |
| if (!search->tag || !search->tagLength) { | |
| debug(L"# Cannot install Python with no tag specified\n"); | |
| return RC_NO_PYTHON; | |
| } | |
| // PEP 514 tag but not PythonCore? No installing | |
| if (!search->oldStyleTag && | |
| search->company && search->companyLength && | |
| 0 != _compare(search->company, search->companyLength, L"PythonCore", -1)) { | |
| debug(L"# Cannot install for company %.*s\n", search->companyLength, search->company); | |
| return RC_NO_PYTHON; | |
| } | |
| const wchar_t *storeId = NULL; | |
| for (struct StoreSearchInfo *info = STORE_SEARCH; info->tag; ++info) { | |
| if (0 == _compare(search->tag, search->tagLength, info->tag, -1)) { | |
| storeId = info->storeId; | |
| break; | |
| } | |
| } | |
| if (!storeId) { | |
| return RC_NO_PYTHON; | |
| } | |
| int exitCode; | |
| wchar_t command[MAXLEN]; | |
| wchar_t arguments[MAXLEN]; | |
| if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, command)) && | |
| join(command, MAXLEN, WINGET_COMMAND) && | |
| swprintf_s(arguments, MAXLEN, WINGET_ARGUMENTS, storeId)) { | |
| if (INVALID_FILE_ATTRIBUTES == GetFileAttributesW(command)) { | |
| formatWinerror(GetLastError(), arguments, MAXLEN); | |
| debug(L"# Skipping %s: %s\n", command, arguments); | |
| } else { | |
| fputws(L"Launching winget to install Python. The following output is from the install process\n\ | |
| ***********************************************************************\n", stdout); | |
| exitCode = _installEnvironment(command, arguments); | |
| if (exitCode == RC_INSTALLING) { | |
| fputws(L"***********************************************************************\n\ | |
| Please check the install status and run your command again.", stderr); | |
| return exitCode; | |
| } else if (exitCode) { | |
| return exitCode; | |
| } | |
| fputws(L"***********************************************************************\n\ | |
| Install appears to have succeeded. Searching for new matching installs.\n", stdout); | |
| return 0; | |
| } | |
| } | |
| if (swprintf_s(command, MAXLEN, MSSTORE_COMMAND, storeId)) { | |
| fputws(L"Opening the Microsoft Store to install Python. After installation, " | |
| L"please run your command again.\n", stderr); | |
| exitCode = _installEnvironment(command, NULL); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| return 0; | |
| } | |
| return RC_NO_PYTHON; | |
| } | |
| /******************************************************************************\ | |
| *** ENVIRONMENT SELECT *** | |
| \******************************************************************************/ | |
| bool | |
| _companyMatches(const SearchInfo *search, const EnvironmentInfo *env) | |
| { | |
| if (!search->company || !search->companyLength) { | |
| return true; | |
| } | |
| return 0 == _compare(env->company, -1, search->company, search->companyLength); | |
| } | |
| bool | |
| _tagMatches(const SearchInfo *search, const EnvironmentInfo *env, int searchTagLength) | |
| { | |
| if (searchTagLength < 0) { | |
| searchTagLength = search->tagLength; | |
| } | |
| if (!search->tag || !searchTagLength) { | |
| return true; | |
| } | |
| return _startsWithSeparated(env->tag, -1, search->tag, searchTagLength, L".-"); | |
| } | |
| bool | |
| _is32Bit(const EnvironmentInfo *env) | |
| { | |
| if (env->architecture) { | |
| return 0 == _compare(env->architecture, -1, L"32bit", -1); | |
| } | |
| return false; | |
| } | |
| int | |
| _selectEnvironment(const SearchInfo *search, EnvironmentInfo *env, EnvironmentInfo **best) | |
| { | |
| int exitCode = 0; | |
| while (env) { | |
| exitCode = _selectEnvironment(search, env->prev, best); | |
| if (exitCode && exitCode != RC_NO_PYTHON) { | |
| return exitCode; | |
| } else if (!exitCode && *best) { | |
| return 0; | |
| } | |
| if (env->highPriority && search->lowPriorityTag) { | |
| // This environment is marked high priority, and the search allows | |
| // it to be selected even though a tag is specified, so select it | |
| // gh-92817: this allows an active venv to be selected even when a | |
| // default tag has been found in py.ini or the environment | |
| *best = env; | |
| return 0; | |
| } | |
| if (!search->oldStyleTag) { | |
| if (_companyMatches(search, env) && _tagMatches(search, env, -1)) { | |
| // Because of how our sort tree is set up, we will walk up the | |
| // "prev" side and implicitly select the "best" best. By | |
| // returning straight after a match, we skip the entire "next" | |
| // branch and won't ever select a "worse" best. | |
| *best = env; | |
| return 0; | |
| } | |
| } else if (0 == _compare(env->company, -1, L"PythonCore", -1)) { | |
| // Old-style tags can only match PythonCore entries | |
| // If the tag ends with -64, we want to exclude 32-bit runtimes | |
| // (If the tag ends with -32, it will be filtered later) | |
| int tagLength = search->tagLength; | |
| bool exclude32Bit = false, only32Bit = false; | |
| if (tagLength > 3) { | |
| if (0 == _compareArgument(&search->tag[tagLength - 3], 3, L"-64", 3)) { | |
| tagLength -= 3; | |
| exclude32Bit = true; | |
| } else if (0 == _compareArgument(&search->tag[tagLength - 3], 3, L"-32", 3)) { | |
| tagLength -= 3; | |
| only32Bit = true; | |
| } | |
| } | |
| if (_tagMatches(search, env, tagLength)) { | |
| if (exclude32Bit && _is32Bit(env)) { | |
| debug(L"# Excluding %s/%s because it looks like 32bit\n", env->company, env->tag); | |
| } else if (only32Bit && !_is32Bit(env)) { | |
| debug(L"# Excluding %s/%s because it doesn't look 32bit\n", env->company, env->tag); | |
| } else { | |
| *best = env; | |
| return 0; | |
| } | |
| } | |
| } | |
| env = env->next; | |
| } | |
| return RC_NO_PYTHON; | |
| } | |
| int | |
| selectEnvironment(const SearchInfo *search, EnvironmentInfo *root, EnvironmentInfo **best) | |
| { | |
| if (!best) { | |
| debug(L"# selectEnvironment() was passed a NULL best\n"); | |
| return RC_INTERNAL_ERROR; | |
| } | |
| if (!root) { | |
| *best = NULL; | |
| return RC_NO_PYTHON_AT_ALL; | |
| } | |
| EnvironmentInfo *result = NULL; | |
| int exitCode = _selectEnvironment(search, root, &result); | |
| if (!exitCode) { | |
| *best = result; | |
| } | |
| return exitCode; | |
| } | |
| /******************************************************************************\ | |
| *** LIST ENVIRONMENTS *** | |
| \******************************************************************************/ | |
| #define TAGWIDTH 16 | |
| int | |
| _printEnvironment(const EnvironmentInfo *env, FILE *out, bool showPath, const wchar_t *argument) | |
| { | |
| if (showPath) { | |
| if (env->executablePath && env->executablePath[0]) { | |
| if (env->executableArgs && env->executableArgs[0]) { | |
| fwprintf(out, L" %-*s %s %s\n", TAGWIDTH, argument, env->executablePath, env->executableArgs); | |
| } else { | |
| fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->executablePath); | |
| } | |
| } else if (env->installDir && env->installDir[0]) { | |
| fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->installDir); | |
| } else { | |
| fwprintf(out, L" %s\n", argument); | |
| } | |
| } else if (env->displayName) { | |
| fwprintf(out, L" %-*s %s\n", TAGWIDTH, argument, env->displayName); | |
| } else { | |
| fwprintf(out, L" %s\n", argument); | |
| } | |
| return 0; | |
| } | |
| int | |
| _listAllEnvironments(EnvironmentInfo *env, FILE * out, bool showPath, EnvironmentInfo *defaultEnv) | |
| { | |
| wchar_t buffer[256]; | |
| const int bufferSize = 256; | |
| while (env) { | |
| int exitCode = _listAllEnvironments(env->prev, out, showPath, defaultEnv); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| if (!env->company || !env->tag) { | |
| buffer[0] = L'\0'; | |
| } else if (0 == _compare(env->company, -1, L"PythonCore", -1)) { | |
| swprintf_s(buffer, bufferSize, L"-V:%s", env->tag); | |
| } else { | |
| swprintf_s(buffer, bufferSize, L"-V:%s/%s", env->company, env->tag); | |
| } | |
| if (env == defaultEnv) { | |
| wcscat_s(buffer, bufferSize, L" *"); | |
| } | |
| if (buffer[0]) { | |
| exitCode = _printEnvironment(env, out, showPath, buffer); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| } | |
| env = env->next; | |
| } | |
| return 0; | |
| } | |
| int | |
| listEnvironments(EnvironmentInfo *env, FILE * out, bool showPath, EnvironmentInfo *defaultEnv) | |
| { | |
| if (!env) { | |
| fwprintf_s(stdout, L"No installed Pythons found!\n"); | |
| return 0; | |
| } | |
| /* TODO: Do we want to display these? | |
| In favour, helps users see that '-3' is a good option | |
| Against, repeats the next line of output | |
| SearchInfo majorSearch; | |
| EnvironmentInfo *major; | |
| int exitCode; | |
| if (showPath) { | |
| memset(&majorSearch, 0, sizeof(majorSearch)); | |
| majorSearch.company = L"PythonCore"; | |
| majorSearch.companyLength = -1; | |
| majorSearch.tag = L"3"; | |
| majorSearch.tagLength = -1; | |
| majorSearch.oldStyleTag = true; | |
| major = NULL; | |
| exitCode = selectEnvironment(&majorSearch, env, &major); | |
| if (!exitCode && major) { | |
| exitCode = _printEnvironment(major, out, showPath, L"-3 *"); | |
| isDefault = false; | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| } | |
| majorSearch.tag = L"2"; | |
| major = NULL; | |
| exitCode = selectEnvironment(&majorSearch, env, &major); | |
| if (!exitCode && major) { | |
| exitCode = _printEnvironment(major, out, showPath, L"-2"); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| } | |
| } | |
| */ | |
| int mode = _setmode(_fileno(out), _O_U8TEXT); | |
| int exitCode = _listAllEnvironments(env, out, showPath, defaultEnv); | |
| fflush(out); | |
| if (mode >= 0) { | |
| _setmode(_fileno(out), mode); | |
| } | |
| return exitCode; | |
| } | |
| /******************************************************************************\ | |
| *** INTERPRETER LAUNCH *** | |
| \******************************************************************************/ | |
| int | |
| calculateCommandLine(const SearchInfo *search, const EnvironmentInfo *launch, wchar_t *buffer, int bufferLength) | |
| { | |
| int exitCode = 0; | |
| const wchar_t *executablePath = NULL; | |
| // Construct command line from a search override, or else the selected | |
| // environment's executablePath | |
| if (search->executablePath) { | |
| executablePath = search->executablePath; | |
| } else if (launch && launch->executablePath) { | |
| executablePath = launch->executablePath; | |
| } | |
| // If we have an executable path, put it at the start of the command, but | |
| // only if the search allowed an override. | |
| // Otherwise, use the environment's installDir and the search's default | |
| // executable name. | |
| if (executablePath && search->allowExecutableOverride) { | |
| if (wcschr(executablePath, L' ') && executablePath[0] != L'"') { | |
| buffer[0] = L'"'; | |
| exitCode = wcscpy_s(&buffer[1], bufferLength - 1, executablePath); | |
| if (!exitCode) { | |
| exitCode = wcscat_s(buffer, bufferLength, L"\""); | |
| } | |
| } else { | |
| exitCode = wcscpy_s(buffer, bufferLength, executablePath); | |
| } | |
| } else if (launch) { | |
| if (!launch->installDir) { | |
| fwprintf_s(stderr, L"Cannot launch %s %s because no install directory was specified", | |
| launch->company, launch->tag); | |
| exitCode = RC_NO_PYTHON; | |
| } else if (!search->executable || !search->executableLength) { | |
| fwprintf_s(stderr, L"Cannot launch %s %s because no executable name is available", | |
| launch->company, launch->tag); | |
| exitCode = RC_NO_PYTHON; | |
| } else { | |
| wchar_t executable[256]; | |
| wcsncpy_s(executable, 256, search->executable, search->executableLength); | |
| if ((wcschr(launch->installDir, L' ') && launch->installDir[0] != L'"') || | |
| (wcschr(executable, L' ') && executable[0] != L'"')) { | |
| buffer[0] = L'"'; | |
| exitCode = wcscpy_s(&buffer[1], bufferLength - 1, launch->installDir); | |
| if (!exitCode) { | |
| exitCode = join(buffer, bufferLength, executable) ? 0 : RC_NO_MEMORY; | |
| } | |
| if (!exitCode) { | |
| exitCode = wcscat_s(buffer, bufferLength, L"\""); | |
| } | |
| } else { | |
| exitCode = wcscpy_s(buffer, bufferLength, launch->installDir); | |
| if (!exitCode) { | |
| exitCode = join(buffer, bufferLength, executable) ? 0 : RC_NO_MEMORY; | |
| } | |
| } | |
| } | |
| } else { | |
| exitCode = RC_NO_PYTHON; | |
| } | |
| if (!exitCode && launch && launch->executableArgs) { | |
| exitCode = wcscat_s(buffer, bufferLength, L" "); | |
| if (!exitCode) { | |
| exitCode = wcscat_s(buffer, bufferLength, launch->executableArgs); | |
| } | |
| } | |
| if (!exitCode && search->executableArgs) { | |
| if (search->executableArgsLength < 0) { | |
| exitCode = wcscat_s(buffer, bufferLength, search->executableArgs); | |
| } else if (search->executableArgsLength > 0) { | |
| int end = (int)wcsnlen_s(buffer, MAXLEN); | |
| if (end < bufferLength - (search->executableArgsLength + 1)) { | |
| exitCode = wcsncpy_s(&buffer[end], bufferLength - end, | |
| search->executableArgs, search->executableArgsLength); | |
| } | |
| } | |
| } | |
| if (!exitCode && search->restOfCmdLine) { | |
| exitCode = wcscat_s(buffer, bufferLength, search->restOfCmdLine); | |
| } | |
| return exitCode; | |
| } | |
| BOOL | |
| _safeDuplicateHandle(HANDLE in, HANDLE * pout, const wchar_t *nameForError) | |
| { | |
| BOOL ok; | |
| HANDLE process = GetCurrentProcess(); | |
| DWORD rc; | |
| *pout = NULL; | |
| ok = DuplicateHandle(process, in, process, pout, 0, TRUE, | |
| DUPLICATE_SAME_ACCESS); | |
| if (!ok) { | |
| rc = GetLastError(); | |
| if (rc == ERROR_INVALID_HANDLE) { | |
| debug(L"DuplicateHandle returned ERROR_INVALID_HANDLE\n"); | |
| ok = TRUE; | |
| } | |
| else { | |
| winerror(0, L"Failed to duplicate %s handle", nameForError); | |
| } | |
| } | |
| return ok; | |
| } | |
| BOOL WINAPI | |
| ctrl_c_handler(DWORD code) | |
| { | |
| return TRUE; /* We just ignore all control events. */ | |
| } | |
| int | |
| launchEnvironment(const SearchInfo *search, const EnvironmentInfo *launch, wchar_t *launchCommand) | |
| { | |
| HANDLE job; | |
| JOBOBJECT_EXTENDED_LIMIT_INFORMATION info; | |
| DWORD rc; | |
| BOOL ok; | |
| STARTUPINFOW si; | |
| PROCESS_INFORMATION pi; | |
| // If this is a dryrun, do not actually launch | |
| if (isEnvVarSet(L"PYLAUNCHER_DRYRUN")) { | |
| debug(L"LaunchCommand: %s\n", launchCommand); | |
| debug(L"# Exiting due to PYLAUNCHER_DRYRUN variable\n"); | |
| fflush(stdout); | |
| int mode = _setmode(_fileno(stdout), _O_U8TEXT); | |
| fwprintf(stdout, L"%s\n", launchCommand); | |
| fflush(stdout); | |
| if (mode >= 0) { | |
| _setmode(_fileno(stdout), mode); | |
| } | |
| return 0; | |
| } | |
| #if defined(_WINDOWS) | |
| /* | |
| When explorer launches a Windows (GUI) application, it displays | |
| the "app starting" (the "pointer + hourglass") cursor for a number | |
| of seconds, or until the app does something UI-ish (eg, creating a | |
| window, or fetching a message). As this launcher doesn't do this | |
| directly, that cursor remains even after the child process does these | |
| things. We avoid that by doing a simple post+get message. | |
| See http://bugs.python.org/issue17290 | |
| */ | |
| MSG msg; | |
| PostMessage(0, 0, 0, 0); | |
| GetMessage(&msg, 0, 0, 0); | |
| #endif | |
| debug(L"# about to run: %s\n", launchCommand); | |
| job = CreateJobObject(NULL, NULL); | |
| ok = QueryInformationJobObject(job, JobObjectExtendedLimitInformation, | |
| &info, sizeof(info), &rc); | |
| if (!ok || (rc != sizeof(info)) || !job) { | |
| winerror(0, L"Failed to query job information"); | |
| return RC_CREATE_PROCESS; | |
| } | |
| info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | | |
| JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; | |
| ok = SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info, | |
| sizeof(info)); | |
| if (!ok) { | |
| winerror(0, L"Failed to update job information"); | |
| return RC_CREATE_PROCESS; | |
| } | |
| memset(&si, 0, sizeof(si)); | |
| GetStartupInfoW(&si); | |
| if (!_safeDuplicateHandle(GetStdHandle(STD_INPUT_HANDLE), &si.hStdInput, L"stdin") || | |
| !_safeDuplicateHandle(GetStdHandle(STD_OUTPUT_HANDLE), &si.hStdOutput, L"stdout") || | |
| !_safeDuplicateHandle(GetStdHandle(STD_ERROR_HANDLE), &si.hStdError, L"stderr")) { | |
| return RC_NO_STD_HANDLES; | |
| } | |
| ok = SetConsoleCtrlHandler(ctrl_c_handler, TRUE); | |
| if (!ok) { | |
| winerror(0, L"Failed to update Control-C handler"); | |
| return RC_NO_STD_HANDLES; | |
| } | |
| si.dwFlags = STARTF_USESTDHANDLES; | |
| ok = CreateProcessW(NULL, launchCommand, NULL, NULL, TRUE, | |
| 0, NULL, NULL, &si, &pi); | |
| if (!ok) { | |
| winerror(0, L"Unable to create process using '%s'", launchCommand); | |
| return RC_CREATE_PROCESS; | |
| } | |
| AssignProcessToJobObject(job, pi.hProcess); | |
| CloseHandle(pi.hThread); | |
| WaitForSingleObjectEx(pi.hProcess, INFINITE, FALSE); | |
| ok = GetExitCodeProcess(pi.hProcess, &rc); | |
| if (!ok) { | |
| winerror(0, L"Failed to get exit code of process"); | |
| return RC_CREATE_PROCESS; | |
| } | |
| debug(L"child process exit code: %d\n", rc); | |
| return rc; | |
| } | |
| /******************************************************************************\ | |
| *** PROCESS CONTROLLER *** | |
| \******************************************************************************/ | |
| int | |
| performSearch(SearchInfo *search, EnvironmentInfo **envs) | |
| { | |
| // First parse the command line for options | |
| int exitCode = parseCommandLine(search); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| // Check for a shebang line in our script file | |
| // (or return quickly if no script file was specified) | |
| exitCode = checkShebang(search); | |
| switch (exitCode) { | |
| case 0: | |
| case RC_NO_SHEBANG: | |
| case RC_RECURSIVE_SHEBANG: | |
| break; | |
| default: | |
| return exitCode; | |
| } | |
| // Resolve old-style tags (possibly from a shebang) against py.ini entries | |
| // and environment variables. | |
| exitCode = checkDefaults(search); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| // If debugging is enabled, list our search criteria | |
| dumpSearchInfo(search); | |
| // Find all matching environments | |
| exitCode = collectEnvironments(search, envs); | |
| if (exitCode) { | |
| return exitCode; | |
| } | |
| return 0; | |
| } | |
| int | |
| process(int argc, wchar_t ** argv) | |
| { | |
| int exitCode = 0; | |
| int searchExitCode = 0; | |
| SearchInfo search = {0}; | |
| EnvironmentInfo *envs = NULL; | |
| EnvironmentInfo *env = NULL; | |
| wchar_t launchCommand[MAXLEN]; | |
| memset(launchCommand, 0, sizeof(launchCommand)); | |
| if (isEnvVarSet(L"PYLAUNCHER_DEBUG")) { | |
| setvbuf(stderr, (char *)NULL, _IONBF, 0); | |
| log_fp = stderr; | |
| debug(L"argv0: %s\nversion: %S\n", argv[0], PY_VERSION); | |
| } | |
| DWORD len = GetEnvironmentVariableW(L"PYLAUNCHER_LIMIT_TO_COMPANY", NULL, 0); | |
| if (len > 1) { | |
| wchar_t *limitToCompany = allocSearchInfoBuffer(&search, len); | |
| if (!limitToCompany) { | |
| exitCode = RC_NO_MEMORY; | |
| winerror(0, L"Failed to allocate internal buffer"); | |
| goto abort; | |
| } | |
| search.limitToCompany = limitToCompany; | |
| if (0 == GetEnvironmentVariableW(L"PYLAUNCHER_LIMIT_TO_COMPANY", limitToCompany, len)) { | |
| exitCode = RC_INTERNAL_ERROR; | |
| winerror(0, L"Failed to read PYLAUNCHER_LIMIT_TO_COMPANY variable"); | |
| goto abort; | |
| } | |
| } | |
| search.originalCmdLine = GetCommandLineW(); | |
| exitCode = performSearch(&search, &envs); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| // Display the help text, but only exit on error | |
| if (search.help) { | |
| exitCode = showHelpText(argv); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| } | |
| // Select best environment | |
| // This is early so that we can show the default when listing, but all | |
| // responses to any errors occur later. | |
| searchExitCode = selectEnvironment(&search, envs, &env); | |
| // List all environments, then exit | |
| if (search.list || search.listPaths) { | |
| exitCode = listEnvironments(envs, stdout, search.listPaths, env); | |
| goto abort; | |
| } | |
| // When debugging, list all discovered environments anyway | |
| if (log_fp) { | |
| exitCode = listEnvironments(envs, log_fp, true, NULL); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| } | |
| // We searched earlier, so if we didn't find anything, now we react | |
| exitCode = searchExitCode; | |
| // If none found, and if permitted, install it | |
| if (exitCode == RC_NO_PYTHON && isEnvVarSet(L"PYLAUNCHER_ALLOW_INSTALL") || | |
| isEnvVarSet(L"PYLAUNCHER_ALWAYS_INSTALL")) { | |
| exitCode = installEnvironment(&search); | |
| if (!exitCode) { | |
| // Successful install, so we need to re-scan and select again | |
| env = NULL; | |
| exitCode = performSearch(&search, &envs); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| exitCode = selectEnvironment(&search, envs, &env); | |
| } | |
| } | |
| if (exitCode == RC_NO_PYTHON) { | |
| fputws(L"No suitable Python runtime found\n", stderr); | |
| fputws(L"Pass --list (-0) to see all detected environments on your machine\n", stderr); | |
| if (!isEnvVarSet(L"PYLAUNCHER_ALLOW_INSTALL") && search.oldStyleTag) { | |
| fputws(L"or set environment variable PYLAUNCHER_ALLOW_INSTALL to use winget\n" | |
| L"or open the Microsoft Store to the requested version.\n", stderr); | |
| } | |
| goto abort; | |
| } | |
| if (exitCode == RC_NO_PYTHON_AT_ALL) { | |
| fputws(L"No installed Python found!\n", stderr); | |
| goto abort; | |
| } | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| if (env) { | |
| debug(L"env.company: %s\nenv.tag: %s\n", env->company, env->tag); | |
| } else { | |
| debug(L"env.company: (null)\nenv.tag: (null)\n"); | |
| } | |
| exitCode = calculateCommandLine(&search, env, launchCommand, sizeof(launchCommand) / sizeof(launchCommand[0])); | |
| if (exitCode) { | |
| goto abort; | |
| } | |
| // Launch selected runtime | |
| exitCode = launchEnvironment(&search, env, launchCommand); | |
| abort: | |
| freeSearchInfo(&search); | |
| freeEnvironmentInfo(envs); | |
| return exitCode; | |
| } | |
| #if defined(_WINDOWS) | |
| int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, | |
| LPWSTR lpstrCmd, int nShow) | |
| { | |
| return process(__argc, __wargv); | |
| } | |
| #else | |
| int cdecl wmain(int argc, wchar_t ** argv) | |
| { | |
| return process(argc, argv); | |
| } | |
| #endif |