/* * Copyright (C) 2011-2013 Vinay Sajip. * Licensed to PSF under a contributor agreement. * * Based on the work of: * * Mark Hammond (original author of Python version) * Curt Hagenlocher (job management) */ #include #include #include #include #define BUFSIZE 256 #define MSGSIZE 1024 /* Build options. */ #define SKIP_PREFIX #define SEARCH_PATH /* Error codes */ #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 /* * SCRIPT_WRAPPER is used to choose between two variants of an executable built * from this source file. If not defined, the PEP 397 Python launcher is built; * if defined, a script launcher of the type used by setuptools is built, which * looks for a script name related to the executable name and runs that script * with the appropriate Python interpreter. * * SCRIPT_WRAPPER should be undefined in the source, and defined in a VS project * which builds the setuptools-style launcher. */ #if defined(SCRIPT_WRAPPER) #define RC_NO_SCRIPT 105 #endif /* Just for now - static definition */ static FILE * log_fp = NULL; static wchar_t * skip_whitespace(wchar_t * p) { while (*p && isspace(*p)) ++p; return p; } static void debug(wchar_t * format, ...) { va_list va; if (log_fp != NULL) { va_start(va, format); vfwprintf_s(log_fp, format, va); va_end(va); } } static void winerror(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); } static void error(int rc, wchar_t * format, ... ) { va_list va; wchar_t message[MSGSIZE]; wchar_t win_message[MSGSIZE]; int len; va_start(va, format); len = _vsnwprintf_s(message, MSGSIZE, _TRUNCATE, format, va); va_end(va); if (rc == 0) { /* a Windows error */ winerror(GetLastError(), 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 MessageBox(NULL, message, TEXT("Python Launcher is sorry to say ..."), MB_OK); #endif exit(rc); } /* * This function is here to simplify memory management * and to treat blank values as if they are absent. */ static wchar_t * get_env(wchar_t * key) { /* This is not thread-safe, just like getenv */ static wchar_t buf[BUFSIZE]; DWORD result = GetEnvironmentVariableW(key, buf, BUFSIZE); if (result >= BUFSIZE) { /* Large environment variable. Accept some leakage */ wchar_t *buf2 = (wchar_t*)malloc(sizeof(wchar_t) * (result+1)); if (buf2 == NULL) { error(RC_NO_MEMORY, L"Could not allocate environment buffer"); } GetEnvironmentVariableW(key, buf2, result); return buf2; } if (result == 0) /* Either some error, e.g. ERROR_ENVVAR_NOT_FOUND, or an empty environment variable. */ return NULL; return buf; } #if defined(_WINDOWS) #define PYTHON_EXECUTABLE L"pythonw.exe" #else #define PYTHON_EXECUTABLE L"python.exe" #endif #define MAX_VERSION_SIZE 4 typedef struct { wchar_t version[MAX_VERSION_SIZE]; /* m.n */ int bits; /* 32 or 64 */ wchar_t executable[MAX_PATH]; } INSTALLED_PYTHON; /* * To avoid messing about with heap allocations, just assume we can allocate * statically and never have to deal with more versions than this. */ #define MAX_INSTALLED_PYTHONS 100 static INSTALLED_PYTHON installed_pythons[MAX_INSTALLED_PYTHONS]; static size_t num_installed_pythons = 0; /* * To hold SOFTWARE\Python\PythonCore\X.Y...\InstallPath * The version name can be longer than MAX_VERSION_SIZE, but will be * truncated to just X.Y for comparisons. */ #define IP_BASE_SIZE 40 #define IP_VERSION_SIZE 8 #define IP_SIZE (IP_BASE_SIZE + IP_VERSION_SIZE) #define CORE_PATH L"SOFTWARE\\Python\\PythonCore" static wchar_t * location_checks[] = { L"\\", L"\\PCbuild\\win32\\", L"\\PCbuild\\amd64\\", /* To support early 32bit versions of Python that stuck the build binaries * directly in PCbuild... */ L"\\PCbuild\\", NULL }; static INSTALLED_PYTHON * find_existing_python(wchar_t * path) { INSTALLED_PYTHON * result = NULL; size_t i; INSTALLED_PYTHON * ip; for (i = 0, ip = installed_pythons; i < num_installed_pythons; i++, ip++) { if (_wcsicmp(path, ip->executable) == 0) { result = ip; break; } } return result; } static void locate_pythons_for_key(HKEY root, REGSAM flags) { HKEY core_root, ip_key; LSTATUS status = RegOpenKeyExW(root, CORE_PATH, 0, flags, &core_root); wchar_t message[MSGSIZE]; DWORD i; size_t n; BOOL ok; DWORD type, data_size, attrs; INSTALLED_PYTHON * ip, * pip; wchar_t ip_version[IP_VERSION_SIZE]; wchar_t ip_path[IP_SIZE]; wchar_t * check; wchar_t ** checkp; wchar_t *key_name = (root == HKEY_LOCAL_MACHINE) ? L"HKLM" : L"HKCU"; if (status != ERROR_SUCCESS) debug(L"locate_pythons_for_key: unable to open PythonCore key in %ls\n", key_name); else { ip = &installed_pythons[num_installed_pythons]; for (i = 0; num_installed_pythons < MAX_INSTALLED_PYTHONS; i++) { status = RegEnumKeyW(core_root, i, ip_version, IP_VERSION_SIZE); if (status != ERROR_SUCCESS) { if (status != ERROR_NO_MORE_ITEMS) { /* unexpected error */ winerror(status, message, MSGSIZE); debug(L"Can't enumerate registry key for version %ls: %ls\n", ip_version, message); } break; } else { wcsncpy_s(ip->version, MAX_VERSION_SIZE, ip_version, MAX_VERSION_SIZE-1); _snwprintf_s(ip_path, IP_SIZE, _TRUNCATE, L"%ls\\%ls\\InstallPath", CORE_PATH, ip_version); status = RegOpenKeyExW(root, ip_path, 0, flags, &ip_key); if (status != ERROR_SUCCESS) { winerror(status, message, MSGSIZE); /* Note: 'message' already has a trailing \n*/ debug(L"%ls\\%ls: %ls", key_name, ip_path, message); continue; } data_size = sizeof(ip->executable) - 1; status = RegQueryValueExW(ip_key, NULL, NULL, &type, (LPBYTE)ip->executable, &data_size); RegCloseKey(ip_key); if (status != ERROR_SUCCESS) { winerror(status, message, MSGSIZE); debug(L"%ls\\%ls: %ls\n", key_name, ip_path, message); continue; } if (type == REG_SZ) { data_size = data_size / sizeof(wchar_t) - 1; /* for NUL */ if (ip->executable[data_size - 1] == L'\\') --data_size; /* reg value ended in a backslash */ /* ip->executable is data_size long */ for (checkp = location_checks; *checkp; ++checkp) { check = *checkp; _snwprintf_s(&ip->executable[data_size], MAX_PATH - data_size, MAX_PATH - data_size, L"%ls%ls", check, PYTHON_EXECUTABLE); attrs = GetFileAttributesW(ip->executable); if (attrs == INVALID_FILE_ATTRIBUTES) { winerror(GetLastError(), message, MSGSIZE); debug(L"locate_pythons_for_key: %ls: %ls", ip->executable, message); } else if (attrs & FILE_ATTRIBUTE_DIRECTORY) { debug(L"locate_pythons_for_key: '%ls' is a \ directory\n", ip->executable, attrs); } else if (find_existing_python(ip->executable)) { debug(L"locate_pythons_for_key: %ls: already \ found\n", ip->executable); } else { /* check the executable type. */ ok = GetBinaryTypeW(ip->executable, &attrs); if (!ok) { debug(L"Failure getting binary type: %ls\n", ip->executable); } else { if (attrs == SCS_64BIT_BINARY) ip->bits = 64; else if (attrs == SCS_32BIT_BINARY) ip->bits = 32; else ip->bits = 0; if (ip->bits == 0) { debug(L"locate_pythons_for_key: %ls: \ invalid binary type: %X\n", ip->executable, attrs); } else { if (wcschr(ip->executable, L' ') != NULL) { /* has spaces, so quote */ n = wcslen(ip->executable); memmove(&ip->executable[1], ip->executable, n * sizeof(wchar_t)); ip->executable[0] = L'\"'; ip->executable[n + 1] = L'\"'; ip->executable[n + 2] = L'\0'; } debug(L"locate_pythons_for_key: %ls \ is a %dbit executable\n", ip->executable, ip->bits); ++num_installed_pythons; pip = ip++; if (num_installed_pythons >= MAX_INSTALLED_PYTHONS) break; /* Copy over the attributes for the next */ *ip = *pip; } } } } } } } RegCloseKey(core_root); } } static int compare_pythons(const void * p1, const void * p2) { INSTALLED_PYTHON * ip1 = (INSTALLED_PYTHON *) p1; INSTALLED_PYTHON * ip2 = (INSTALLED_PYTHON *) p2; /* note reverse sorting on version */ int result = wcscmp(ip2->version, ip1->version); if (result == 0) result = ip2->bits - ip1->bits; /* 64 before 32 */ return result; } static void locate_all_pythons() { #if defined(_M_X64) /* If we are a 64bit process, first hit the 32bit keys. */ debug(L"locating Pythons in 32bit registry\n"); locate_pythons_for_key(HKEY_CURRENT_USER, KEY_READ | KEY_WOW64_32KEY); locate_pythons_for_key(HKEY_LOCAL_MACHINE, KEY_READ | KEY_WOW64_32KEY); #else /* If we are a 32bit process on a 64bit Windows, first hit the 64bit keys.*/ BOOL f64 = FALSE; if (IsWow64Process(GetCurrentProcess(), &f64) && f64) { debug(L"locating Pythons in 64bit registry\n"); locate_pythons_for_key(HKEY_CURRENT_USER, KEY_READ | KEY_WOW64_64KEY); locate_pythons_for_key(HKEY_LOCAL_MACHINE, KEY_READ | KEY_WOW64_64KEY); } #endif /* now hit the "native" key for this process bittedness. */ debug(L"locating Pythons in native registry\n"); locate_pythons_for_key(HKEY_CURRENT_USER, KEY_READ); locate_pythons_for_key(HKEY_LOCAL_MACHINE, KEY_READ); qsort(installed_pythons, num_installed_pythons, sizeof(INSTALLED_PYTHON), compare_pythons); } static INSTALLED_PYTHON * find_python_by_version(wchar_t const * wanted_ver) { INSTALLED_PYTHON * result = NULL; INSTALLED_PYTHON * ip = installed_pythons; size_t i, n; size_t wlen = wcslen(wanted_ver); int bits = 0; if (wcsstr(wanted_ver, L"-32")) { bits = 32; wlen -= wcslen(L"-32"); } else if (wcsstr(wanted_ver, L"-64")) { /* Added option to select 64 bit explicitly */ bits = 64; wlen -= wcslen(L"-64"); } for (i = 0; i < num_installed_pythons; i++, ip++) { n = wcslen(ip->version); if (n > wlen) n = wlen; if ((wcsncmp(ip->version, wanted_ver, n) == 0) && /* bits == 0 => don't care */ ((bits == 0) || (ip->bits == bits))) { result = ip; break; } } return result; } static wchar_t * find_python_by_venv() { static wchar_t venv_python[MAX_PATH]; wchar_t *virtual_env = get_env(L"VIRTUAL_ENV"); DWORD attrs; /* Check for VIRTUAL_ENV environment variable */ if (virtual_env == NULL || virtual_env[0] == L'\0') { return NULL; } /* Check for a python executable in the venv */ debug(L"Checking for Python executable in virtual env '%ls'\n", virtual_env); _snwprintf_s(venv_python, MAX_PATH, _TRUNCATE, L"%ls\\Scripts\\%ls", virtual_env, PYTHON_EXECUTABLE); attrs = GetFileAttributesW(venv_python); if (attrs == INVALID_FILE_ATTRIBUTES) { debug(L"Python executable %ls missing from virtual env\n", venv_python); return NULL; } return venv_python; } static wchar_t appdata_ini_path[MAX_PATH]; static wchar_t launcher_ini_path[MAX_PATH]; /* * Get a value either from the environment or a configuration file. * The key passed in will either be "python", "python2" or "python3". */ static wchar_t * get_configured_value(wchar_t * key) { /* * Note: this static value is used to return a configured value * obtained either from the environment or configuration file. * This should be OK since there wouldn't be any concurrent calls. */ static wchar_t configured_value[MSGSIZE]; wchar_t * result = NULL; wchar_t * found_in = L"environment"; DWORD size; /* First, search the environment. */ _snwprintf_s(configured_value, MSGSIZE, _TRUNCATE, L"py_%ls", key); result = get_env(configured_value); if (result == NULL && appdata_ini_path[0]) { /* Not in environment: check local configuration. */ size = GetPrivateProfileStringW(L"defaults", key, NULL, configured_value, MSGSIZE, appdata_ini_path); if (size > 0) { result = configured_value; found_in = appdata_ini_path; } } if (result == NULL && launcher_ini_path[0]) { /* Not in environment or local: check global configuration. */ size = GetPrivateProfileStringW(L"defaults", key, NULL, configured_value, MSGSIZE, launcher_ini_path); if (size > 0) { result = configured_value; found_in = launcher_ini_path; } } if (result) { debug(L"found configured value '%ls=%ls' in %ls\n", key, result, found_in ? found_in : L"(unknown)"); } else { debug(L"found no configured value for '%ls'\n", key); } return result; } static INSTALLED_PYTHON * locate_python(wchar_t * wanted_ver, BOOL from_shebang) { static wchar_t config_key [] = { L"pythonX" }; static wchar_t * last_char = &config_key[sizeof(config_key) / sizeof(wchar_t) - 2]; INSTALLED_PYTHON * result = NULL; size_t n = wcslen(wanted_ver); wchar_t * configured_value; if (num_installed_pythons == 0) locate_all_pythons(); if (n == 1) { /* just major version specified */ *last_char = *wanted_ver; configured_value = get_configured_value(config_key); if (configured_value != NULL) wanted_ver = configured_value; } if (*wanted_ver) { result = find_python_by_version(wanted_ver); debug(L"search for Python version '%ls' found ", wanted_ver); if (result) { debug(L"'%ls'\n", result->executable); } else { debug(L"no interpreter\n"); } } else { *last_char = L'\0'; /* look for an overall default */ configured_value = get_configured_value(config_key); if (configured_value) result = find_python_by_version(configured_value); /* Not found a value yet - try by major version. * If we're looking for an interpreter specified in a shebang line, * we want to try Python 2 first, then Python 3 (for Unix and backward * compatibility). If we're being called interactively, assume the user * wants the latest version available, so try Python 3 first, then * Python 2. */ if (result == NULL) result = find_python_by_version(from_shebang ? L"2" : L"3"); if (result == NULL) result = find_python_by_version(from_shebang ? L"3" : L"2"); debug(L"search for default Python found "); if (result) { debug(L"version %ls at '%ls'\n", result->version, result->executable); } else { debug(L"no interpreter\n"); } } return result; } #if defined(SCRIPT_WRAPPER) /* * Check for a script located alongside the executable */ #if defined(_WINDOWS) #define SCRIPT_SUFFIX L"-script.pyw" #else #define SCRIPT_SUFFIX L"-script.py" #endif static wchar_t wrapped_script_path[MAX_PATH]; /* Locate the script being wrapped. * * This code should store the name of the wrapped script in * wrapped_script_path, or terminate the program with an error if there is no * valid wrapped script file. */ static void locate_wrapped_script() { wchar_t * p; size_t plen; DWORD attrs; plen = GetModuleFileNameW(NULL, wrapped_script_path, MAX_PATH); p = wcsrchr(wrapped_script_path, L'.'); if (p == NULL) { debug(L"GetModuleFileNameW returned value has no extension: %ls\n", wrapped_script_path); error(RC_NO_SCRIPT, L"Wrapper name '%ls' is not valid.", wrapped_script_path); } wcsncpy_s(p, MAX_PATH - (p - wrapped_script_path) + 1, SCRIPT_SUFFIX, _TRUNCATE); attrs = GetFileAttributesW(wrapped_script_path); if (attrs == INVALID_FILE_ATTRIBUTES) { debug(L"File '%ls' non-existent\n", wrapped_script_path); error(RC_NO_SCRIPT, L"Script file '%ls' is not present.", wrapped_script_path); } debug(L"Using wrapped script file '%ls'\n", wrapped_script_path); } #endif /* * Process creation code */ static BOOL safe_duplicate_handle(HANDLE in, HANDLE * pout) { 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 { debug(L"DuplicateHandle returned %d\n", rc); } } return ok; } static BOOL WINAPI ctrl_c_handler(DWORD code) { return TRUE; /* We just ignore all control events. */ } static void run_child(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 and https://bitbucket.org/vinay.sajip/pylauncher/issue/20/busy-cursor-for-a-long-time-when-running */ MSG msg; PostMessage(0, 0, 0, 0); GetMessage(&msg, 0, 0, 0); #endif debug(L"run_child: about to run '%ls'\n", cmdline); job = CreateJobObject(NULL, NULL); ok = QueryInformationJobObject(job, JobObjectExtendedLimitInformation, &info, sizeof(info), &rc); if (!ok || (rc != sizeof(info)) || !job) error(RC_CREATE_PROCESS, L"Job information querying failed"); 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) error(RC_CREATE_PROCESS, L"Job information setting failed"); memset(&si, 0, sizeof(si)); si.cb = sizeof(si); ok = safe_duplicate_handle(GetStdHandle(STD_INPUT_HANDLE), &si.hStdInput); if (!ok) error(RC_NO_STD_HANDLES, L"stdin duplication failed"); ok = safe_duplicate_handle(GetStdHandle(STD_OUTPUT_HANDLE), &si.hStdOutput); if (!ok) error(RC_NO_STD_HANDLES, L"stdout duplication failed"); ok = safe_duplicate_handle(GetStdHandle(STD_ERROR_HANDLE), &si.hStdError); if (!ok) error(RC_NO_STD_HANDLES, L"stderr duplication failed"); ok = SetConsoleCtrlHandler(ctrl_c_handler, TRUE); if (!ok) error(RC_CREATE_PROCESS, L"control handler setting failed"); si.dwFlags = STARTF_USESTDHANDLES; ok = CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); if (!ok) error(RC_CREATE_PROCESS, L"Unable to create process using '%ls'", cmdline); AssignProcessToJobObject(job, pi.hProcess); CloseHandle(pi.hThread); WaitForSingleObjectEx(pi.hProcess, INFINITE, FALSE); ok = GetExitCodeProcess(pi.hProcess, &rc); if (!ok) error(RC_CREATE_PROCESS, L"Failed to get exit code of process"); debug(L"child process exit code: %d\n", rc); exit(rc); } static void invoke_child(wchar_t * executable, wchar_t * suffix, wchar_t * cmdline) { wchar_t * child_command; size_t child_command_size; BOOL no_suffix = (suffix == NULL) || (*suffix == L'\0'); BOOL no_cmdline = (*cmdline == L'\0'); if (no_suffix && no_cmdline) run_child(executable); else { if (no_suffix) { /* add 2 for space separator + terminating NUL. */ child_command_size = wcslen(executable) + wcslen(cmdline) + 2; } else { /* add 3 for 2 space separators + terminating NUL. */ child_command_size = wcslen(executable) + wcslen(suffix) + wcslen(cmdline) + 3; } child_command = calloc(child_command_size, sizeof(wchar_t)); if (child_command == NULL) error(RC_CREATE_PROCESS, L"unable to allocate %d bytes for child command.", child_command_size); if (no_suffix) _snwprintf_s(child_command, child_command_size, child_command_size - 1, L"%ls %ls", executable, cmdline); else _snwprintf_s(child_command, child_command_size, child_command_size - 1, L"%ls %ls %ls", executable, suffix, cmdline); run_child(child_command); free(child_command); } } typedef struct { wchar_t *shebang; BOOL search; } SHEBANG; static SHEBANG builtin_virtual_paths [] = { { L"/usr/bin/env python", TRUE }, { L"/usr/bin/python", FALSE }, { L"/usr/local/bin/python", FALSE }, { L"python", FALSE }, { NULL, FALSE }, }; /* For now, a static array of commands. */ #define MAX_COMMANDS 100 typedef struct { wchar_t key[MAX_PATH]; wchar_t value[MSGSIZE]; } COMMAND; static COMMAND commands[MAX_COMMANDS]; static int num_commands = 0; #if defined(SKIP_PREFIX) static wchar_t * builtin_prefixes [] = { /* These must be in an order that the longest matches should be found, * i.e. if the prefix is "/usr/bin/env ", it should match that entry * *before* matching "/usr/bin/". */ L"/usr/bin/env ", L"/usr/bin/", L"/usr/local/bin/", NULL }; static wchar_t * skip_prefix(wchar_t * name) { wchar_t ** pp = builtin_prefixes; wchar_t * result = name; wchar_t * p; size_t n; for (; p = *pp; pp++) { n = wcslen(p); if (_wcsnicmp(p, name, n) == 0) { result += n; /* skip the prefix */ if (p[n - 1] == L' ') /* No empty strings in table, so n > 1 */ result = skip_whitespace(result); break; } } return result; } #endif #if defined(SEARCH_PATH) static COMMAND path_command; static COMMAND * find_on_path(wchar_t * name) { wchar_t * pathext; size_t varsize; wchar_t * context = NULL; wchar_t * extension; COMMAND * result = NULL; DWORD len; errno_t rc; wcscpy_s(path_command.key, MAX_PATH, name); if (wcschr(name, L'.') != NULL) { /* assume it has an extension. */ len = SearchPathW(NULL, name, NULL, MSGSIZE, path_command.value, NULL); if (len) { result = &path_command; } } else { /* No extension - search using registered extensions. */ rc = _wdupenv_s(&pathext, &varsize, L"PATHEXT"); if (rc == 0) { extension = wcstok_s(pathext, L";", &context); while (extension) { len = SearchPathW(NULL, name, extension, MSGSIZE, path_command.value, NULL); if (len) { result = &path_command; break; } extension = wcstok_s(NULL, L";", &context); } free(pathext); } } return result; } #endif static COMMAND * find_command(wchar_t * name) { COMMAND * result = NULL; COMMAND * cp = commands; int i; for (i = 0; i < num_commands; i++, cp++) { if (_wcsicmp(cp->key, name) == 0) { result = cp; break; } } #if defined(SEARCH_PATH) if (result == NULL) result = find_on_path(name); #endif return result; } static void update_command(COMMAND * cp, wchar_t * name, wchar_t * cmdline) { wcsncpy_s(cp->key, MAX_PATH, name, _TRUNCATE); wcsncpy_s(cp->value, MSGSIZE, cmdline, _TRUNCATE); } static void add_command(wchar_t * name, wchar_t * cmdline) { if (num_commands >= MAX_COMMANDS) { debug(L"can't add %ls = '%ls': no room\n", name, cmdline); } else { COMMAND * cp = &commands[num_commands++]; update_command(cp, name, cmdline); } } static void read_config_file(wchar_t * config_path) { wchar_t keynames[MSGSIZE]; wchar_t value[MSGSIZE]; DWORD read; wchar_t * key; COMMAND * cp; wchar_t * cmdp; read = GetPrivateProfileStringW(L"commands", NULL, NULL, keynames, MSGSIZE, config_path); if (read == MSGSIZE - 1) { debug(L"read_commands: %ls: not enough space for names\n", config_path); } key = keynames; while (*key) { read = GetPrivateProfileStringW(L"commands", key, NULL, value, MSGSIZE, config_path); if (read == MSGSIZE - 1) { debug(L"read_commands: %ls: not enough space for %ls\n", config_path, key); } cmdp = skip_whitespace(value); if (*cmdp) { cp = find_command(key); if (cp == NULL) add_command(key, value); else update_command(cp, key, value); } key += wcslen(key) + 1; } } static void read_commands() { if (launcher_ini_path[0]) read_config_file(launcher_ini_path); if (appdata_ini_path[0]) read_config_file(appdata_ini_path); } static BOOL parse_shebang(wchar_t * shebang_line, int nchars, wchar_t ** command, wchar_t ** suffix, BOOL *search) { BOOL rc = FALSE; SHEBANG * vpp; size_t plen; wchar_t * p; wchar_t zapped; wchar_t * endp = shebang_line + nchars - 1; COMMAND * cp; wchar_t * skipped; *command = NULL; /* failure return */ *suffix = NULL; *search = FALSE; if ((*shebang_line++ == L'#') && (*shebang_line++ == L'!')) { shebang_line = skip_whitespace(shebang_line); if (*shebang_line) { *command = shebang_line; for (vpp = builtin_virtual_paths; vpp->shebang; ++vpp) { plen = wcslen(vpp->shebang); if (wcsncmp(shebang_line, vpp->shebang, plen) == 0) { rc = TRUE; *search = vpp->search; /* We can do this because all builtin commands contain * "python". */ *command = wcsstr(shebang_line, L"python"); break; } } if (vpp->shebang == NULL) { /* * Not found in builtins - look in customized commands. * * We can't permanently modify the shebang line in case * it's not a customized command, but we can temporarily * stick a NUL after the command while searching for it, * then put back the char we zapped. */ #if defined(SKIP_PREFIX) skipped = skip_prefix(shebang_line); #else skipped = shebang_line; #endif p = wcspbrk(skipped, L" \t\r\n"); if (p != NULL) { zapped = *p; *p = L'\0'; } cp = find_command(skipped); if (p != NULL) *p = zapped; if (cp != NULL) { *command = cp->value; if (p != NULL) *suffix = skip_whitespace(p); } } /* remove trailing whitespace */ while ((endp > shebang_line) && isspace(*endp)) --endp; if (endp > shebang_line) endp[1] = L'\0'; } } return rc; } /* #define CP_UTF8 65001 defined in winnls.h */ #define CP_UTF16LE 1200 #define CP_UTF16BE 1201 #define CP_UTF32LE 12000 #define CP_UTF32BE 12001 typedef struct { int length; char sequence[4]; UINT code_page; } BOM; /* * Strictly, we don't need to handle UTF-16 and UTF-32, since Python itself * doesn't. Never mind, one day it might - there's no harm leaving it in. */ static BOM BOMs[] = { { 3, { 0xEF, 0xBB, 0xBF }, CP_UTF8 }, /* UTF-8 - keep first */ /* Test UTF-32LE before UTF-16LE since UTF-16LE BOM is a prefix * of UTF-32LE BOM. */ { 4, { 0xFF, 0xFE, 0x00, 0x00 }, CP_UTF32LE }, /* UTF-32LE */ { 4, { 0x00, 0x00, 0xFE, 0xFF }, CP_UTF32BE }, /* UTF-32BE */ { 2, { 0xFF, 0xFE }, CP_UTF16LE }, /* UTF-16LE */ { 2, { 0xFE, 0xFF }, CP_UTF16BE }, /* UTF-16BE */ { 0 } /* sentinel */ }; static BOM * find_BOM(char * buffer) { /* * Look for a BOM in the input and return a pointer to the * corresponding structure, or NULL if not found. */ BOM * result = NULL; BOM *bom; for (bom = BOMs; bom->length; bom++) { if (strncmp(bom->sequence, buffer, bom->length) == 0) { result = bom; break; } } return result; } static char * find_terminator(char * buffer, int len, BOM *bom) { char * result = NULL; char * end = buffer + len; char * p; char c; int cp; for (p = buffer; p < end; p++) { c = *p; if (c == '\r') { result = p; break; } if (c == '\n') { result = p; break; } } if (result != NULL) { cp = bom->code_page; /* adjustments to include all bytes of the char */ /* no adjustment needed for UTF-8 or big endian */ if (cp == CP_UTF16LE) ++result; else if (cp == CP_UTF32LE) result += 3; ++result; /* point just past terminator */ } return result; } static BOOL validate_version(wchar_t * p) { /* Version information should start with one of 2 or 3, Optionally followed by a period and a minor version, Optionally followed by a minus and one of 32 or 64. Valid examples: 2 3 2.7 3.6 2.7-32 The intent is to add to the valid patterns: 3.10 3-32 3.6-64 3-64 */ BOOL result = (p != NULL); /* Default to False if null pointer. */ result = result && iswdigit(*p); /* Result = False if fist string element is not a digit. */ while (result && iswdigit(*p)) /* Require a major version */ ++p; /* Skip all leading digit(s) */ if (result && (*p == L'.')) /* Allow . for major minor separator.*/ { result = iswdigit(*++p); /* Must be at least one digit */ while (result && iswdigit(*++p)) ; /* Skip any more Digits */ } if (result && (*p == L'-')) { /* Allow - for Bits Separator */ switch(*++p){ case L'3': /* 3 is OK */ result = (*++p == L'2') && !*++p; /* only if followed by 2 and ended.*/ break; case L'6': /* 6 is OK */ result = (*++p == L'4') && !*++p; /* only if followed by 4 and ended.*/ break; default: result = FALSE; break; } } result = result && !*p; /* Must have reached EOS */ return result; } typedef struct { unsigned short min; unsigned short max; wchar_t version[MAX_VERSION_SIZE]; } PYC_MAGIC; static PYC_MAGIC magic_values[] = { { 50823, 50823, L"2.0" }, { 60202, 60202, L"2.1" }, { 60717, 60717, L"2.2" }, { 62011, 62021, L"2.3" }, { 62041, 62061, L"2.4" }, { 62071, 62131, L"2.5" }, { 62151, 62161, L"2.6" }, { 62171, 62211, L"2.7" }, { 3000, 3131, L"3.0" }, { 3141, 3151, L"3.1" }, { 3160, 3180, L"3.2" }, { 3190, 3230, L"3.3" }, { 3250, 3310, L"3.4" }, { 3320, 3351, L"3.5" }, { 3360, 3379, L"3.6" }, { 3390, 3399, L"3.7" }, { 0 } }; static INSTALLED_PYTHON * find_by_magic(unsigned short magic) { INSTALLED_PYTHON * result = NULL; PYC_MAGIC * mp; for (mp = magic_values; mp->min; mp++) { if ((magic >= mp->min) && (magic <= mp->max)) { result = locate_python(mp->version, FALSE); if (result != NULL) break; } } return result; } static void maybe_handle_shebang(wchar_t ** argv, wchar_t * cmdline) { /* * Look for a shebang line in the first argument. If found * and we spawn a child process, this never returns. If it * does return then we process the args "normally". * * argv[0] might be a filename with a shebang. */ FILE * fp; errno_t rc = _wfopen_s(&fp, *argv, L"rb"); char buffer[BUFSIZE]; wchar_t shebang_line[BUFSIZE + 1]; size_t read; char *p; char * start; char * shebang_alias = (char *) shebang_line; BOM* bom; int i, j, nchars = 0; int header_len; BOOL is_virt; BOOL search; wchar_t * command; wchar_t * suffix; COMMAND *cmd = NULL; INSTALLED_PYTHON * ip; if (rc == 0) { read = fread(buffer, sizeof(char), BUFSIZE, fp); debug(L"maybe_handle_shebang: read %d bytes\n", read); fclose(fp); if ((read >= 4) && (buffer[3] == '\n') && (buffer[2] == '\r')) { ip = find_by_magic((((unsigned char)buffer[1]) << 8 | (unsigned char)buffer[0]) & 0xFFFF); if (ip != NULL) { debug(L"script file is compiled against Python %ls\n", ip->version); invoke_child(ip->executable, NULL, cmdline); } } /* Look for BOM */ bom = find_BOM(buffer); if (bom == NULL) { start = buffer; debug(L"maybe_handle_shebang: BOM not found, using UTF-8\n"); bom = BOMs; /* points to UTF-8 entry - the default */ } else { debug(L"maybe_handle_shebang: BOM found, code page %d\n", bom->code_page); start = &buffer[bom->length]; } p = find_terminator(start, BUFSIZE, bom); /* * If no CR or LF was found in the heading, * we assume it's not a shebang file. */ if (p == NULL) { debug(L"maybe_handle_shebang: No line terminator found\n"); } else { /* * Found line terminator - parse the shebang. * * Strictly, we don't need to handle UTF-16 anf UTF-32, * since Python itself doesn't. * Never mind, one day it might. */ header_len = (int) (p - start); switch(bom->code_page) { case CP_UTF8: nchars = MultiByteToWideChar(bom->code_page, 0, start, header_len, shebang_line, BUFSIZE); break; case CP_UTF16BE: if (header_len % 2 != 0) { debug(L"maybe_handle_shebang: UTF-16BE, but an odd number \ of bytes: %d\n", header_len); /* nchars = 0; Not needed - initialised to 0. */ } else { for (i = header_len; i > 0; i -= 2) { shebang_alias[i - 1] = start[i - 2]; shebang_alias[i - 2] = start[i - 1]; } nchars = header_len / sizeof(wchar_t); } break; case CP_UTF16LE: if ((header_len % 2) != 0) { debug(L"UTF-16LE, but an odd number of bytes: %d\n", header_len); /* nchars = 0; Not needed - initialised to 0. */ } else { /* no actual conversion needed. */ memcpy(shebang_line, start, header_len); nchars = header_len / sizeof(wchar_t); } break; case CP_UTF32BE: if (header_len % 4 != 0) { debug(L"UTF-32BE, but not divisible by 4: %d\n", header_len); /* nchars = 0; Not needed - initialised to 0. */ } else { for (i = header_len, j = header_len / 2; i > 0; i -= 4, j -= 2) { shebang_alias[j - 1] = start[i - 2]; shebang_alias[j - 2] = start[i - 1]; } nchars = header_len / sizeof(wchar_t); } break; case CP_UTF32LE: if (header_len % 4 != 0) { debug(L"UTF-32LE, but not divisible by 4: %d\n", header_len); /* nchars = 0; Not needed - initialised to 0. */ } else { for (i = header_len, j = header_len / 2; i > 0; i -= 4, j -= 2) { shebang_alias[j - 1] = start[i - 3]; shebang_alias[j - 2] = start[i - 4]; } nchars = header_len / sizeof(wchar_t); } break; } if (nchars > 0) { shebang_line[--nchars] = L'\0'; is_virt = parse_shebang(shebang_line, nchars, &command, &suffix, &search); if (command != NULL) { debug(L"parse_shebang: found command: %ls\n", command); if (!is_virt) { invoke_child(command, suffix, cmdline); } else { suffix = wcschr(command, L' '); if (suffix != NULL) { *suffix++ = L'\0'; suffix = skip_whitespace(suffix); } if (wcsncmp(command, L"python", 6)) error(RC_BAD_VIRTUAL_PATH, L"Unknown virtual \ path '%ls'", command); command += 6; /* skip past "python" */ if (search && ((*command == L'\0') || isspace(*command))) { /* Command is eligible for path search, and there * is no version specification. */ debug(L"searching PATH for python executable\n"); cmd = find_on_path(PYTHON_EXECUTABLE); debug(L"Python on path: %ls\n", cmd ? cmd->value : L""); if (cmd) { debug(L"located python on PATH: %ls\n", cmd->value); invoke_child(cmd->value, suffix, cmdline); /* Exit here, as we have found the command */ return; } /* FALL THROUGH: No python found on PATH, so fall * back to locating the correct installed python. */ } if (*command && !validate_version(command)) error(RC_BAD_VIRTUAL_PATH, L"Invalid version \ specification: '%ls'.\nIn the first line of the script, 'python' needs to be \ followed by a valid version specifier.\nPlease check the documentation.", command); /* TODO could call validate_version(command) */ ip = locate_python(command, TRUE); if (ip == NULL) { error(RC_NO_PYTHON, L"Requested Python version \ (%ls) is not installed", command); } else { invoke_child(ip->executable, suffix, cmdline); } } } } } } } static wchar_t * skip_me(wchar_t * cmdline) { BOOL quoted; wchar_t c; wchar_t * result = cmdline; quoted = cmdline[0] == L'\"'; if (!quoted) c = L' '; else { c = L'\"'; ++result; } result = wcschr(result, c); if (result == NULL) /* when, for example, just exe name on command line */ result = L""; else { ++result; /* skip past space or closing quote */ result = skip_whitespace(result); } return result; } static DWORD version_high = 0; static DWORD version_low = 0; static void get_version_info(wchar_t * version_text, size_t size) { WORD maj, min, rel, bld; if (!version_high && !version_low) wcsncpy_s(version_text, size, L"0.1", _TRUNCATE); /* fallback */ else { maj = HIWORD(version_high); min = LOWORD(version_high); rel = HIWORD(version_low); bld = LOWORD(version_low); _snwprintf_s(version_text, size, _TRUNCATE, L"%d.%d.%d.%d", maj, min, rel, bld); } } static void show_help_text(wchar_t ** argv) { wchar_t version_text [MAX_PATH]; #if defined(_M_X64) BOOL canDo64bit = TRUE; #else /* If we are a 32bit process on a 64bit Windows, first hit the 64bit keys. */ BOOL canDo64bit = FALSE; IsWow64Process(GetCurrentProcess(), &canDo64bit); #endif get_version_info(version_text, MAX_PATH); fwprintf(stdout, L"\ Python Launcher for Windows Version %ls\n\n", version_text); fwprintf(stdout, L"\ usage:\n\ %ls [launcher-args] [python-args] script [script-args]\n\n", argv[0]); fputws(L"\ Launcher arguments:\n\n\ -2 : Launch the latest Python 2.x version\n\ -3 : Launch the latest Python 3.x version\n\ -X.Y : Launch the specified Python version\n", stdout); if (canDo64bit) { fputws(L"\ The above all default to 64 bit if a matching 64 bit python is present.\n\ -X.Y-32: Launch the specified 32bit Python version\n\ -X-32 : Launch the latest 32bit Python X version\n\ -X.Y-64: Launch the specified 64bit Python version\n\ -X-64 : Launch the latest 64bit Python X version", stdout); } fputws(L"\n-0 --list : List the available pythons", stdout); fputws(L"\n-0p --list-paths : List with paths", stdout); fputws(L"\n\nThe following help text is from Python:\n\n", stdout); fflush(stdout); } static BOOL show_python_list(wchar_t ** argv) { /* * Display options -0 */ INSTALLED_PYTHON * result = NULL; INSTALLED_PYTHON * ip = installed_pythons; /* List of installed pythons */ INSTALLED_PYTHON * defpy = locate_python(L"", FALSE); size_t i = 0; wchar_t *p = argv[1]; wchar_t *fmt = L"\n -%ls-%d"; /* print VER-BITS */ wchar_t *defind = L" *"; /* Default indicator */ /* * Output informational messages to stderr to keep output * clean for use in pipes, etc. */ fwprintf(stderr, L"Installed Pythons found by %s Launcher for Windows", argv[0]); if (!_wcsicmp(p, L"-0p") || !_wcsicmp(p, L"--list-paths")) /* Show path? */ fmt = L"\n -%ls-%d\t%ls"; /* print VER-BITS path */ if (num_installed_pythons == 0) /* We have somehow got here without searching for pythons */ locate_all_pythons(); /* Find them, Populates installed_pythons */ if (num_installed_pythons == 0) /* No pythons found */ fwprintf(stderr, L"\nNo Installed Pythons Found!"); else { for (i = 0; i < num_installed_pythons; i++, ip++) { fwprintf(stdout, fmt, ip->version, ip->bits, ip->executable); /* If there is a default indicate it */ if ((defpy != NULL) && !_wcsicmp(ip->executable, defpy->executable)) fwprintf(stderr, defind); } } if ((defpy == NULL) && (num_installed_pythons > 0)) /* We have pythons but none is the default */ fwprintf(stderr, L"\n\nCan't find a Default Python.\n\n"); else fwprintf(stderr, L"\n\n"); /* End with a blank line */ return(FALSE); /* If this has been called we cannot continue */ } static int process(int argc, wchar_t ** argv) { wchar_t * wp; wchar_t * command; wchar_t * executable; wchar_t * p; int rc = 0; size_t plen; INSTALLED_PYTHON * ip; BOOL valid; DWORD size, attrs; HRESULT hr; wchar_t message[MSGSIZE]; void * version_data; VS_FIXEDFILEINFO * file_info; UINT block_size; int index; #if defined(SCRIPT_WRAPPER) int newlen; wchar_t * newcommand; wchar_t * av[2]; #endif setvbuf(stderr, (char *)NULL, _IONBF, 0); wp = get_env(L"PYLAUNCH_DEBUG"); if ((wp != NULL) && (*wp != L'\0')) log_fp = stderr; #if defined(_M_X64) debug(L"launcher build: 64bit\n"); #else debug(L"launcher build: 32bit\n"); #endif #if defined(_WINDOWS) debug(L"launcher executable: Windows\n"); #else debug(L"launcher executable: Console\n"); #endif /* Get the local appdata folder (non-roaming) */ hr = SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, appdata_ini_path); if (hr != S_OK) { debug(L"SHGetFolderPath failed: %X\n", hr); appdata_ini_path[0] = L'\0'; } else { plen = wcslen(appdata_ini_path); p = &appdata_ini_path[plen]; wcsncpy_s(p, MAX_PATH - plen, L"\\py.ini", _TRUNCATE); attrs = GetFileAttributesW(appdata_ini_path); if (attrs == INVALID_FILE_ATTRIBUTES) { debug(L"File '%ls' non-existent\n", appdata_ini_path); appdata_ini_path[0] = L'\0'; } else { debug(L"Using local configuration file '%ls'\n", appdata_ini_path); } } plen = GetModuleFileNameW(NULL, launcher_ini_path, MAX_PATH); size = GetFileVersionInfoSizeW(launcher_ini_path, &size); if (size == 0) { winerror(GetLastError(), message, MSGSIZE); debug(L"GetFileVersionInfoSize failed: %ls\n", message); } else { version_data = malloc(size); if (version_data) { valid = GetFileVersionInfoW(launcher_ini_path, 0, size, version_data); if (!valid) debug(L"GetFileVersionInfo failed: %X\n", GetLastError()); else { valid = VerQueryValueW(version_data, L"\\", (LPVOID *) &file_info, &block_size); if (!valid) debug(L"VerQueryValue failed: %X\n", GetLastError()); else { version_high = file_info->dwFileVersionMS; version_low = file_info->dwFileVersionLS; } } free(version_data); } } p = wcsrchr(launcher_ini_path, L'\\'); if (p == NULL) { debug(L"GetModuleFileNameW returned value has no backslash: %ls\n", launcher_ini_path); launcher_ini_path[0] = L'\0'; } else { wcsncpy_s(p, MAX_PATH - (p - launcher_ini_path), L"\\py.ini", _TRUNCATE); attrs = GetFileAttributesW(launcher_ini_path); if (attrs == INVALID_FILE_ATTRIBUTES) { debug(L"File '%ls' non-existent\n", launcher_ini_path); launcher_ini_path[0] = L'\0'; } else { debug(L"Using global configuration file '%ls'\n", launcher_ini_path); } } command = skip_me(GetCommandLineW()); debug(L"Called with command line: %ls\n", command); #if defined(SCRIPT_WRAPPER) /* The launcher is being used in "script wrapper" mode. * There should therefore be a Python script named -script.py in * the same directory as the launcher executable. * Put the script name into argv as the first (script name) argument. */ /* Get the wrapped script name - if the script is not present, this will * terminate the program with an error. */ locate_wrapped_script(); /* Add the wrapped script to the start of command */ newlen = wcslen(wrapped_script_path) + wcslen(command) + 2; /* ' ' + NUL */ newcommand = malloc(sizeof(wchar_t) * newlen); if (!newcommand) { error(RC_NO_MEMORY, L"Could not allocate new command line"); } else { wcscpy_s(newcommand, newlen, wrapped_script_path); wcscat_s(newcommand, newlen, L" "); wcscat_s(newcommand, newlen, command); debug(L"Running wrapped script with command line '%ls'\n", newcommand); read_commands(); av[0] = wrapped_script_path; av[1] = NULL; maybe_handle_shebang(av, newcommand); /* Returns if no shebang line - pass to default processing */ command = newcommand; valid = FALSE; } #else if (argc <= 1) { valid = FALSE; p = NULL; } else { p = argv[1]; plen = wcslen(p); if ((argc == 2) && (!wcsncmp(p, L"-0", wcslen(L"-0")) || /* Starts with -0 or --list */ !wcsncmp(p, L"--list", wcslen(L"--list")))) { valid = show_python_list(argv); /* Check for -0 or --list FIRST */ } valid = valid && (*p == L'-') && validate_version(&p[1]); if (valid) { ip = locate_python(&p[1], FALSE); if (ip == NULL) { fwprintf(stdout, \ L"Python %ls not found!\n", &p[1]); valid = show_python_list(argv); error(RC_NO_PYTHON, L"Requested Python version (%ls) not \ installed, use -0 for available pythons", &p[1]); } executable = ip->executable; command += wcslen(p); command = skip_whitespace(command); } else { for (index = 1; index < argc; ++index) { if (*argv[index] != L'-') break; } if (index < argc) { read_commands(); maybe_handle_shebang(&argv[index], command); } } } #endif if (!valid) { if ((argc == 2) && (!_wcsicmp(p, L"-h") || !_wcsicmp(p, L"--help"))) show_help_text(argv); if ((argc == 2) && (!_wcsicmp(p, L"-0") || !_wcsicmp(p, L"--list") || !_wcsicmp(p, L"-0p") || !_wcsicmp(p, L"--list-paths"))) { executable = NULL; /* Info call only */ } else { /* Look for an active virtualenv */ executable = find_python_by_venv(); /* If we didn't find one, look for the default Python */ if (executable == NULL) { ip = locate_python(L"", FALSE); if (ip == NULL) error(RC_NO_PYTHON, L"Can't find a default Python."); executable = ip->executable; } } } if (executable != NULL) invoke_child(executable, NULL, command); else rc = RC_NO_PYTHON; return rc; } #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