| /* |
| * venv redirector for Windows |
| * |
| * This launcher looks for a nearby pyvenv.cfg to find the correct home |
| * directory, and then launches the original Python executable from it. |
| * The name of this executable is passed as argv[0]. |
| */ |
| |
| #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_NO_PYTHON 103 |
| #define RC_NO_MEMORY 104 |
| #define RC_NO_VENV_CFG 106 |
| #define RC_BAD_VENV_CFG 107 |
| #define RC_NO_COMMANDLINE 108 |
| #define RC_INTERNAL_ERROR 109 |
| |
| // This should always be defined when we build for real, |
| // but it's handy to have a definition for quick testing |
| #ifndef EXENAME |
| #define EXENAME L"python.exe" |
| #endif |
| |
| #ifndef CFGNAME |
| #define CFGNAME L"pyvenv.cfg" |
| #endif |
| |
| 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; |
| } |
| fwprintf(log_fp, L"%ls\n", buffer); |
| 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": %ls", |
| win_message); |
| } |
| |
| #if !defined(_WINDOWS) |
| fwprintf(stderr, L"%ls\n", message); |
| #else |
| MessageBoxW(NULL, message, L"Python venv 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"%ls\n", message); |
| #else |
| MessageBoxW(NULL, message, L"Python venv launcher is sorry to say ...", |
| MB_OK); |
| #endif |
| } |
| |
| |
| 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)); |
| } |
| |
| |
| /* |
| * Path calculation |
| */ |
| |
| int |
| calculate_pyvenvcfg_path(wchar_t *pyvenvcfg_path, size_t maxlen) |
| { |
| if (!pyvenvcfg_path) { |
| error(L"invalid buffer provided"); |
| return RC_INTERNAL_ERROR; |
| } |
| if ((DWORD)maxlen != maxlen) { |
| error(L"path buffer is too large"); |
| return RC_INTERNAL_ERROR; |
| } |
| if (!GetModuleFileNameW(NULL, pyvenvcfg_path, (DWORD)maxlen)) { |
| winerror(GetLastError(), L"failed to read executable directory"); |
| return RC_NO_COMMANDLINE; |
| } |
| // Remove 'python.exe' from our path |
| if (!split_parent(pyvenvcfg_path, maxlen)) { |
| error(L"failed to remove segment from '%ls'", pyvenvcfg_path); |
| return RC_NO_COMMANDLINE; |
| } |
| // Replace with 'pyvenv.cfg' |
| if (!join(pyvenvcfg_path, maxlen, CFGNAME)) { |
| error(L"failed to append '%ls' to '%ls'", CFGNAME, pyvenvcfg_path); |
| return RC_NO_MEMORY; |
| } |
| // If it exists, return |
| if (GetFileAttributesW(pyvenvcfg_path) != INVALID_FILE_ATTRIBUTES) { |
| return 0; |
| } |
| // Otherwise, remove 'pyvenv.cfg' and (probably) 'Scripts' |
| if (!split_parent(pyvenvcfg_path, maxlen) || |
| !split_parent(pyvenvcfg_path, maxlen)) { |
| error(L"failed to remove segments from '%ls'", pyvenvcfg_path); |
| return RC_NO_COMMANDLINE; |
| } |
| // Replace 'pyvenv.cfg' |
| if (!join(pyvenvcfg_path, maxlen, CFGNAME)) { |
| error(L"failed to append '%ls' to '%ls'", CFGNAME, pyvenvcfg_path); |
| return RC_NO_MEMORY; |
| } |
| // If it exists, return |
| if (GetFileAttributesW(pyvenvcfg_path) != INVALID_FILE_ATTRIBUTES) { |
| return 0; |
| } |
| // Otherwise, we fail |
| winerror(GetLastError(), L"failed to locate %ls", CFGNAME); |
| return RC_NO_VENV_CFG; |
| } |
| |
| |
| /* |
| * pyvenv.cfg parsing |
| */ |
| |
| static int |
| find_home_value(const char *buffer, DWORD maxlen, const char **start, DWORD *length) |
| { |
| if (!buffer || !start || !length) { |
| error(L"invalid find_home_value parameters()"); |
| return 0; |
| } |
| for (const char *s = strstr(buffer, "home"); |
| s && ((ptrdiff_t)s - (ptrdiff_t)buffer) < maxlen; |
| s = strstr(s + 1, "\nhome") |
| ) { |
| if (*s == '\n') { |
| ++s; |
| } |
| for (int i = 4; i > 0 && *s; --i, ++s); |
| |
| while (*s && iswspace(*s)) { |
| ++s; |
| } |
| if (*s != L'=') { |
| continue; |
| } |
| |
| do { |
| ++s; |
| } while (*s && iswspace(*s)); |
| |
| *start = s; |
| char *nl = strchr(s, '\n'); |
| if (nl) { |
| while (nl != s && iswspace(nl[-1])) { |
| --nl; |
| } |
| *length = (DWORD)((ptrdiff_t)nl - (ptrdiff_t)s); |
| } else { |
| *length = (DWORD)strlen(s); |
| } |
| return 1; |
| } |
| return 0; |
| } |
| |
| |
| int |
| read_home(const wchar_t *pyvenv_cfg, wchar_t *home_path, size_t maxlen) |
| { |
| HANDLE hFile = CreateFileW(pyvenv_cfg, GENERIC_READ, |
| FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, |
| NULL, OPEN_EXISTING, 0, NULL); |
| |
| if (hFile == INVALID_HANDLE_VALUE) { |
| winerror(GetLastError(), L"failed to open '%ls'", pyvenv_cfg); |
| return RC_BAD_VENV_CFG; |
| } |
| |
| // 8192 characters ought to be enough for anyone |
| // (doubled compared to the old implementation!) |
| char buffer[8192]; |
| DWORD len; |
| if (!ReadFile(hFile, buffer, sizeof(buffer) - 1, &len, NULL)) { |
| winerror(GetLastError(), L"failed to read '%ls'", pyvenv_cfg); |
| CloseHandle(hFile); |
| return RC_BAD_VENV_CFG; |
| } |
| CloseHandle(hFile); |
| // Ensure null termination |
| buffer[len] = '\0'; |
| |
| char *home; |
| DWORD home_len; |
| if (!find_home_value(buffer, sizeof(buffer), &home, &home_len)) { |
| error(L"no home= specified in '%ls'", pyvenv_cfg); |
| return RC_BAD_VENV_CFG; |
| } |
| |
| if ((DWORD)maxlen != maxlen) { |
| maxlen = 8192; |
| } |
| len = MultiByteToWideChar(CP_UTF8, 0, home, home_len, home_path, (DWORD)maxlen); |
| if (!len) { |
| winerror(GetLastError(), L"failed to decode home setting in '%ls'", pyvenv_cfg); |
| return RC_BAD_VENV_CFG; |
| } |
| home_path[len] = L'\0'; |
| |
| return 0; |
| } |
| |
| |
| int |
| locate_python(wchar_t *path, size_t maxlen) |
| { |
| if (!join(path, maxlen, EXENAME)) { |
| error(L"failed to append %ls to '%ls'", EXENAME, path); |
| return RC_NO_MEMORY; |
| } |
| |
| if (GetFileAttributesW(path) == INVALID_FILE_ATTRIBUTES) { |
| winerror(GetLastError(), L"did not find executable at '%ls'", path); |
| return RC_NO_PYTHON; |
| } |
| |
| return 0; |
| } |
| |
| |
| int |
| smuggle_path() |
| { |
| wchar_t buffer[MAXLEN]; |
| // We could use argv[0], but that may be wrong in certain rare cases (if the |
| // user is doing something weird like symlinks to venv redirectors), and |
| // what we _really_ want is the directory of the venv. We always copy the |
| // redirectors, so if we've made the venv, this will be correct. |
| DWORD len = GetModuleFileNameW(NULL, buffer, MAXLEN); |
| if (!len) { |
| winerror(GetLastError(), L"Failed to get own executable path"); |
| return RC_INTERNAL_ERROR; |
| } |
| buffer[len] = L'\0'; |
| debug(L"Setting __PYVENV_LAUNCHER__ = '%s'", buffer); |
| |
| if (!SetEnvironmentVariableW(L"__PYVENV_LAUNCHER__", buffer)) { |
| winerror(GetLastError(), L"Failed to set launcher environment"); |
| return RC_INTERNAL_ERROR; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Process creation |
| */ |
| |
| static BOOL |
| safe_duplicate_handle(HANDLE in, HANDLE * pout, const wchar_t *name) |
| { |
| 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(%ls) returned ERROR_INVALID_HANDLE\n", name); |
| ok = TRUE; |
| } |
| else { |
| debug(L"DuplicateHandle(%ls) returned %d\n", name, rc); |
| } |
| } |
| return ok; |
| } |
| |
| static BOOL WINAPI |
| ctrl_c_handler(DWORD code) |
| { |
| return TRUE; /* We just ignore all control events. */ |
| } |
| |
| static int |
| launch(const wchar_t *executable, wchar_t *cmdline) |
| { |
| HANDLE job; |
| JOBOBJECT_EXTENDED_LIMIT_INFORMATION info; |
| DWORD rc; |
| BOOL ok; |
| STARTUPINFOW si; |
| PROCESS_INFORMATION pi; |
| |
| #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"run_child: about to run '%ls' with '%ls'\n", executable, cmdline); |
| job = CreateJobObject(NULL, NULL); |
| ok = QueryInformationJobObject(job, JobObjectExtendedLimitInformation, |
| &info, sizeof(info), &rc); |
| if (!ok || (rc != sizeof(info)) || !job) { |
| winerror(GetLastError(), L"Job information querying failed"); |
| 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(GetLastError(), L"Job information setting failed"); |
| return RC_CREATE_PROCESS; |
| } |
| memset(&si, 0, sizeof(si)); |
| GetStartupInfoW(&si); |
| ok = safe_duplicate_handle(GetStdHandle(STD_INPUT_HANDLE), &si.hStdInput, L"stdin"); |
| if (!ok) { |
| return RC_NO_STD_HANDLES; |
| } |
| ok = safe_duplicate_handle(GetStdHandle(STD_OUTPUT_HANDLE), &si.hStdOutput, L"stdout"); |
| if (!ok) { |
| return RC_NO_STD_HANDLES; |
| } |
| ok = safe_duplicate_handle(GetStdHandle(STD_ERROR_HANDLE), &si.hStdError, L"stderr"); |
| if (!ok) { |
| return RC_NO_STD_HANDLES; |
| } |
| |
| ok = SetConsoleCtrlHandler(ctrl_c_handler, TRUE); |
| if (!ok) { |
| winerror(GetLastError(), L"control handler setting failed"); |
| return RC_CREATE_PROCESS; |
| } |
| |
| si.dwFlags = STARTF_USESTDHANDLES; |
| ok = CreateProcessW(executable, cmdline, NULL, NULL, TRUE, |
| 0, NULL, NULL, &si, &pi); |
| if (!ok) { |
| winerror(GetLastError(), L"Unable to create process using '%ls'", cmdline); |
| return RC_CREATE_PROCESS; |
| } |
| AssignProcessToJobObject(job, pi.hProcess); |
| CloseHandle(pi.hThread); |
| WaitForSingleObjectEx(pi.hProcess, INFINITE, FALSE); |
| ok = GetExitCodeProcess(pi.hProcess, &rc); |
| if (!ok) { |
| winerror(GetLastError(), L"Failed to get exit code of process"); |
| return RC_CREATE_PROCESS; |
| } |
| debug(L"child process exit code: %d", rc); |
| return rc; |
| } |
| |
| |
| int |
| process(int argc, wchar_t ** argv) |
| { |
| int exitCode; |
| wchar_t pyvenvcfg_path[MAXLEN]; |
| wchar_t home_path[MAXLEN]; |
| |
| if (isEnvVarSet(L"PYLAUNCHER_DEBUG")) { |
| setvbuf(stderr, (char *)NULL, _IONBF, 0); |
| log_fp = stderr; |
| } |
| |
| exitCode = calculate_pyvenvcfg_path(pyvenvcfg_path, MAXLEN); |
| if (exitCode) return exitCode; |
| |
| exitCode = read_home(pyvenvcfg_path, home_path, MAXLEN); |
| if (exitCode) return exitCode; |
| |
| exitCode = locate_python(home_path, MAXLEN); |
| if (exitCode) return exitCode; |
| |
| // We do not update argv[0] to point at the target runtime, and so we do not |
| // pass through our original argv[0] in an environment variable. |
| //exitCode = smuggle_path(); |
| //if (exitCode) return exitCode; |
| |
| exitCode = launch(home_path, GetCommandLineW()); |
| 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 |