Skip to content
Merged
3 changes: 3 additions & 0 deletions _msbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ class ResourceFile(CSourceFile):
CFunction('date_as_str'),
CFunction('datetime_as_str'),
CFunction('reg_rename_key'),
CFunction('get_current_package'),
CFunction('read_alias_package'),
CFunction('broadcast_settings_change'),
source='src/_native',
RootNamespace='_native',
)
Expand Down
3 changes: 3 additions & 0 deletions _msbuild_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
CFunction('date_as_str'),
CFunction('datetime_as_str'),
CFunction('reg_rename_key'),
CFunction('get_current_package'),
CFunction('read_alias_package'),
CFunction('broadcast_settings_change'),
source='src/_native',
),
DllPackage('_shellext_test',
Expand Down
25 changes: 25 additions & 0 deletions scripts/test-firstrun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Simple script to allow running manage/firstrun.py without rebuilding.

You'll need to build the test module (_msbuild_test.py).
"""

import os
import pathlib
import sys


ROOT = pathlib.Path(__file__).absolute().parent.parent / "src"
sys.path.append(str(ROOT))


import _native
if not hasattr(_native, "coinitialize"):
import _native_test
for k in dir(_native_test):
if k[:1] not in ("", "_"):
setattr(_native, k, getattr(_native_test, k))


import manage.commands
cmd = manage.commands.FirstRun([], ROOT)
sys.exit(cmd.execute() or 0)
114 changes: 114 additions & 0 deletions src/_native/misc.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <Python.h>
#include <windows.h>
#include <appmodel.h>

#include "helpers.h"

Expand Down Expand Up @@ -119,4 +120,117 @@ PyObject *reg_rename_key(PyObject *, PyObject *args, PyObject *kwargs) {
return r;
}


PyObject *get_current_package(PyObject *, PyObject *, PyObject *) {
wchar_t package_name[256];
UINT32 cch = sizeof(package_name) / sizeof(package_name[0]);
int err = GetCurrentPackageFamilyName(&cch, package_name);
switch (err) {
case ERROR_SUCCESS:
return PyUnicode_FromWideChar(package_name, cch ? cch - 1 : 0);
case APPMODEL_ERROR_NO_PACKAGE:
return Py_GetConstant(Py_CONSTANT_NONE);
default:
PyErr_SetFromWindowsErr(err);
return NULL;
}
}


PyObject *read_alias_package(PyObject *, PyObject *args, PyObject *kwargs) {
static const char * keywords[] = {"path", NULL};
wchar_t *path = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:read_alias_package", keywords,
as_utf16, &path)) {
return NULL;
}

HANDLE h = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
PyMem_Free(path);
if (h == INVALID_HANDLE_VALUE) {
PyErr_SetFromWindowsErr(0);
return NULL;
}

struct {
DWORD tag;
DWORD _reserved1;
DWORD _reserved2;
wchar_t package_name[256];
wchar_t nul;
} buffer;
DWORD nread;

if (!DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, NULL, 0,
&buffer, sizeof(buffer), &nread, NULL)
// we expect our buffer to be too small, but we only want the package
&& GetLastError() != ERROR_MORE_DATA) {
PyErr_SetFromWindowsErr(0);
CloseHandle(h);
return NULL;
}

CloseHandle(h);

if (buffer.tag != IO_REPARSE_TAG_APPEXECLINK) {
return Py_GetConstant(Py_CONSTANT_NONE);
}

buffer.nul = 0;
return PyUnicode_FromWideChar(buffer.package_name, -1);
}


typedef LRESULT (*PSendMessageTimeoutW)(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam,
UINT fuFlags,
UINT uTimeout,
PDWORD_PTR lpdwResult
);

PyObject *broadcast_settings_change(PyObject *, PyObject *, PyObject *) {
// Avoid depending on user32 because it's so slow
HMODULE user32 = LoadLibraryExW(L"user32.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
if (!user32) {
PyErr_SetFromWindowsErr(0);
return NULL;
}
PSendMessageTimeoutW sm = (PSendMessageTimeoutW)GetProcAddress(user32, "SendMessageTimeoutW");
if (!sm) {
PyErr_SetFromWindowsErr(0);
FreeLibrary(user32);
return NULL;
}

// SendMessageTimeout needs special error handling
SetLastError(0);
LPARAM lParam = (LPARAM)L"Environment";

if (!(*sm)(
HWND_BROADCAST,
WM_SETTINGCHANGE,
NULL,
lParam,
SMTO_ABORTIFHUNG,
50,
NULL
)) {
int err = GetLastError();
if (!err) {
PyErr_SetString(PyExc_OSError, "Unspecified error");
} else {
PyErr_SetFromWindowsErr(err);
}
FreeLibrary(user32);
return NULL;
}

FreeLibrary(user32);
return Py_GetConstant(Py_CONSTANT_NONE);
}

}
87 changes: 62 additions & 25 deletions src/manage/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@
DEFAULT_TAG = "3.14"


# TODO: Remove the /dev/ for stable release
HELP_URL = "https://docs.python.org/dev/using/windows"


COPYRIGHT = f"""Python installation manager {__version__}
Copyright (c) Python Software Foundation. All Rights Reserved.
"""


if EXE_NAME.casefold() == "py-manager".casefold():
EXE_NAME = "py"

Check warning on line 38 in src/manage/commands.py

View check run for this annotation

Codecov / codecov/patch

src/manage/commands.py#L38

Added line #L38 was not covered by tests


WELCOME = f"""!B!Python install manager was successfully updated to {__version__}.!W!
"""

Expand Down Expand Up @@ -188,6 +196,7 @@
"enable-shortcut-kinds": ("enable_shortcut_kinds", _NEXT, config_split),
"disable-shortcut-kinds": ("disable_shortcut_kinds", _NEXT, config_split),
"help": ("show_help", True), # nested to avoid conflict with command
"configure": ("configure", True),
# Set when the manager is doing an automatic install.
# Generally won't be set by manual invocation
"automatic": ("automatic", True),
Expand All @@ -202,6 +211,10 @@
"force": ("confirm", False),
"help": ("show_help", True), # nested to avoid conflict with command
},

"**first_run": {
"explicit": ("explicit", True),
},
}


Expand Down Expand Up @@ -240,6 +253,16 @@
"disable_shortcut_kinds": (str, config_split_append),
},

"first_run": {
"enabled": (config_bool, None, "env"),
"explicit": (config_bool, None),
"check_app_alias": (config_bool, None, "env"),
"check_long_paths": (config_bool, None, "env"),
"check_py_on_path": (config_bool, None, "env"),
"check_any_install": (config_bool, None, "env"),
"check_global_dir": (config_bool, None, "env"),
},

# These configuration settings are intended for administrative override only
# For example, if you are managing deployments that will use your own index
# and/or your own builds.
Expand Down Expand Up @@ -419,11 +442,11 @@
# If our command has any config, load them to override anything that
# wasn't set on the command line.
try:
cmd_config = config[self.CMD]
cmd_config = config[self.CMD.lstrip("*")]
except (AttributeError, LookupError):
pass
else:
arg_names = frozenset(CONFIG_SCHEMA[self.CMD])
arg_names = frozenset(CONFIG_SCHEMA[self.CMD.lstrip("*")])

Check warning on line 449 in src/manage/commands.py

View check run for this annotation

Codecov / codecov/patch

src/manage/commands.py#L449

Added line #L449 was not covered by tests
for k, v in cmd_config.items():
if k in arg_names and k not in _set_args:
LOGGER.debug("Overriding command option %s with %r", k, v)
Expand Down Expand Up @@ -511,7 +534,7 @@
logs_dir = Path(os.getenv("TMP") or os.getenv("TEMP") or os.getcwd())
from _native import datetime_as_str
self._log_file = logs_dir / "python_{}_{}_{}.log".format(
self.CMD, datetime_as_str(), os.getpid()
self.CMD.strip("*"), datetime_as_str(), os.getpid()
)
return self._log_file

Expand Down Expand Up @@ -545,28 +568,13 @@
if usage_ljust % 4:
usage_ljust += 4 - (usage_ljust % 4)
usage_ljust = max(usage_ljust, 16) + 1
sp = " " * usage_ljust

LOGGER.print("!G!Usage:!W!")
for k, d in usage_docs:
if k.endswith("\n") and len(logging.strip_colour(k)) >= usage_ljust:
LOGGER.print(k.rstrip())
r = sp
else:
k = k.rstrip()
r = k.ljust(usage_ljust + len(k) - len(logging.strip_colour(k)))
for b in d.split(" "):
if len(r) >= logging.CONSOLE_MAX_WIDTH:
LOGGER.print(r.rstrip())
r = sp
r += b + " "
if r.rstrip():
LOGGER.print(r)

LOGGER.print()
# TODO: Remove the /dev/ for stable release
LOGGER.print("Find additional information at !B!https://docs.python.org/dev/using/windows!W!.")
LOGGER.print()
for s in logging.wrap_and_indent(d, indent=usage_ljust, hang=k.rstrip()):
LOGGER.print(s)

LOGGER.print("\nFind additional information at !B!%s!W!.\n", HELP_URL)

@classmethod
def help_text(cls):
Expand Down Expand Up @@ -746,6 +754,7 @@
-u, --update Overwrite existing install if a newer version is available.
--dry-run Choose runtime but do not install
--refresh Update shortcuts and aliases for all installed versions.
--configure Re-run the system configuration helper.
--by-id Require TAG to exactly match the install ID. (For advanced use.)
!B!<TAG> <TAG>!W! ... One or more tags to install (Company\Tag format)

Expand Down Expand Up @@ -775,6 +784,7 @@
dry_run = False
refresh = False
by_id = False
configure = False
automatic = False
from_script = None
enable_shortcut_kinds = None
Expand All @@ -801,9 +811,13 @@
self.download = Path(self.download).absolute()

def execute(self):
from .install_command import execute
self.show_welcome()
execute(self)
if self.configure:
cmd = FirstRun(["**first_run", "--explicit"], self.root)
cmd.execute()
else:
from .install_command import execute
execute(self)

Check warning on line 820 in src/manage/commands.py

View check run for this annotation

Codecov / codecov/patch

src/manage/commands.py#L819-L820

Added lines #L819 - L820 were not covered by tests


class UninstallCommand(BaseCommand):
Expand Down Expand Up @@ -867,6 +881,7 @@
"""

_create_log_file = False
commands_only = False

def __init__(self, args, root=None):
super().__init__([self.CMD], root)
Expand All @@ -891,7 +906,7 @@


class HelpWithErrorCommand(HelpCommand):
CMD = "__help_with_error"
CMD = "**help_with_error"

def __init__(self, args, root=None):
# Essentially disable argument processing for this command
Expand Down Expand Up @@ -945,6 +960,28 @@
super().__init__([], root)


class FirstRun(BaseCommand):
CMD = "**first_run"
enabled = True
explicit = False
check_app_alias = True
check_long_paths = True
check_py_on_path = True
check_any_install = True
check_global_dir = True

def execute(self):
if not self.enabled:
return

Check warning on line 975 in src/manage/commands.py

View check run for this annotation

Codecov / codecov/patch

src/manage/commands.py#L975

Added line #L975 was not covered by tests
from .firstrun import first_run
first_run(self)
if not self.explicit:
self.show_usage()
if self.confirm and not self.ask_ny("View online help?"):
import os
os.startfile(HELP_URL)

Check warning on line 982 in src/manage/commands.py

View check run for this annotation

Codecov / codecov/patch

src/manage/commands.py#L981-L982

Added lines #L981 - L982 were not covered by tests


def load_default_config(root):
return DefaultConfig(root)

Expand Down
Loading