mirror of https://github.com/python/cpython
511 lines
13 KiB
C
511 lines
13 KiB
C
/*
|
|
* 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
|