cpython/PC/venvlauncher.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 && (size_t)((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