Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions source/gui/installerGui.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,25 @@
# This file may be used under the terms of the GNU General Public License, version 2 or later, as modified by the NVDA license.
# For full terms and any additional permissions, see the NVDA license file: https://github.com/nvaccess/nvda/blob/master/copying.txt

from ctypes import FormatError, GetLastError, byref
from ctypes.wintypes import HANDLE
import os
import subprocess
import sys

from utils.security import isRunningElevated
from winBindings.user32 import EWX, SHTDN_REASON, ExitWindowsEx
from winAPI.constants import SystemErrorCodes
from winBindings.kernel32 import GetCurrentProcess
from winBindings.advapi32 import (
SE_PRIVILEGE,
TOKEN_PRIVILEGES,
AdjustTokenPrivileges,
LookupPrivilegeValue,
OpenProcessToken,
PrivilegeName,
TokenAccessRight,
)
import winUser
import wx
import config
Expand Down Expand Up @@ -57,8 +71,50 @@ def _canPortableConfigBeCopied() -> bool:

def _restartWindows() -> bool:
"""Issue a Windows restart command. Returns True if the command succeeded."""
result = subprocess.run(["shutdown", "/r", "/t", "0"], creationflags=subprocess.CREATE_NO_WINDOW)
return result.returncode == 0
# In order to request system restart,
# we must have the SE_SHUTDOWN_NAME privilege.
# Get a token for this process.
hToken = HANDLE()
if not OpenProcessToken(
GetCurrentProcess(),
TokenAccessRight.ADJUST_PRIVILEGES | TokenAccessRight.QUERY,
byref(hToken),
):
log.error(
f"Failed to open the current process token with the ADJUST_PRIVILEGES access right. {GetLastError()}: {FormatError()}",
)
return False
privileges = TOKEN_PRIVILEGES()
# Get the LUID for the shutdown privilege.
if LookupPrivilegeValue(None, PrivilegeName.SHUTDOWN, byref(privileges.Privileges[0].Luid)) == 0:
log.error(
f"Failed to retrieve the LUID for the shutdown privilege. {GetLastError()}: {FormatError()}",
)
return False
privileges.Privileges[0].Attributes = SE_PRIVILEGE.ENABLED
privileges.PrivilegeCount = 1
# Set the shutdown privilege for this process.
# The return value can indicate success even if not all of the privileges were modified as requested.
# But the last error code will be set to ERROR_SUCCESS if and only if all of the privileges were modified as requested.
# So ignore the return value, and just check GetLastError.
AdjustTokenPrivileges(hToken, False, byref(privileges), 0, None, None)
if GetLastError() != SystemErrorCodes.SUCCESS:
log.error(
f"Failed to add the shutdown privilege to the current process. {GetLastError()}: {FormatError()}",
)
return False
# Shut down the system and force all applications to close.
if (
ExitWindowsEx(
EWX.RESTARTAPPS,
SHTDN_REASON.MAJOR_APPLICATION | SHTDN_REASON.MINOR_INSTALLATION | SHTDN_REASON.FLAG_PLANNED,
)
== 0
):
log.error(f"Failed to trigger system restart. {GetLastError()}: {FormatError()}")
return False
# shutdown was successful
return True


def _showPostInstallDialog(isUpdate: bool, startAfterInstall: bool) -> None:
Expand Down Expand Up @@ -113,10 +169,9 @@ def _showPostInstallDialog(isUpdate: bool, startAfterInstall: bool) -> None:
if not core.triggerNVDAExit(newNVDA):
log.error("NVDA already in process of exiting, this indicates a logic error.")
case ReturnCode.CUSTOM_2:
if _restartWindows():
if not core.triggerNVDAExit(None):
log.error("NVDA already in process of exiting, this indicates a logic error.")
else:
# If we successfully request a system restart, we will be terminated by the shutdown sequence.
# Other apps may require input before they can exit, so we should not exit just yet.
if not _restartWindows():
# Restart failed — inform the user.
# Only exit if a new copy can be started so the user keeps a screen reader.
gui.messageBox(
Expand Down Expand Up @@ -254,12 +309,15 @@ def doSilentInstall(
startOnLogon = globalVars.appArgs.enableStartOnLogon
if startOnLogon is None:
startOnLogon = config.getStartOnLogonScreen() if not freshInstall else False
# Currently, this function is only called by ``core.main`` when ``--install`` or ``--install-silent`` are provided at the command line.
# The only use of the ``silent`` parameter to ``doInstall`` is to surpress the post-installation restart dialog.
# Since that dialog should be shown unless this genuinely is a silent installation, use presence of ``--install-silent`` as the actual value of the ``silent`` argument.
doInstall(
createDesktopShortcut=installer.isDesktopShortcutInstalled() if not freshInstall else True,
startOnLogon=startOnLogon,
isUpdate=not freshInstall,
copyPortableConfig=copyPortableConfig,
silent=True,
silent=globalVars.appArgs.installSilent,
startAfterInstall=startAfterInstall,
)

Expand Down
107 changes: 105 additions & 2 deletions source/winBindings/advapi32.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ctypes.wintypes import (
BOOL,
DWORD,
PDWORD,
WORD,
HANDLE,
HKEY,
Expand All @@ -25,7 +26,7 @@
LPWSTR,
LPVOID,
)
from enum import IntEnum
from enum import IntEnum, IntFlag, StrEnum

__all__ = (
"OpenProcessToken",
Expand All @@ -49,8 +50,10 @@ class TokenAccessRight(IntEnum):
https://learn.microsoft.com/en-us/windows/win32/secauthz/access-rights-for-access-token-objects
"""

QUERY = 8
QUERY = 0x0008
"""TOKEN_QUERY: Required to query an access token."""
ADJUST_PRIVILEGES = 0x0020
"""TOKEN_ADJUST_PRIVILEGES: Required to enable or disable the privileges in an access token."""


OpenProcessToken = WINFUNCTYPE(None)(("OpenProcessToken", dll))
Expand Down Expand Up @@ -259,3 +262,103 @@ class TOKEN_ELEVATION_TYPE(IntEnum):
POINTER(DWORD), # ReturnLength
)
GetTokenInformation.restype = BOOL


class SE_PRIVILEGE(IntFlag):
"""Possible attributes of privilege in a TOKEN_PRIVILEGES structure."""

ENABLED_BY_DEFAULT = 0x00000001
"""SE_PRIVILEGE_ENABLED_BY_DEFAULT: The privilege is enabled by default."""
ENABLED = 0x00000002
"""SE_PRIVILEGE_ENABLED: The privilege is enabled."""
REMOVED = 0x00000004
"""SE_PRIVILEGE_REMOVED: Used to remove a privilege."""
USED_FOR_ACCESS = 0x80000000
"""SE_PRIVILEGE_USED_FOR_ACCESS: The privilege was used to gain access to an object or service."""


class LUID(Structure):
"""Describes a local identifier for an adapter.

.. seealso::
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-luid
"""

_fields_ = (
("LowPart", DWORD),
("HighPart", LONG),
)


PLUID = POINTER(LUID)


class LUID_AND_ATTRIBUTES(Structure):
"""Represents a locally unique identifier (LUID) and its attributes.

.. seealso::
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-luid_and_attributes
"""

_fields_ = (
("Luid", LUID),
("Attributes", DWORD),
)


class TOKEN_PRIVILEGES(Structure):
"""Contains information about a set of privileges for an access token.

.. warning::
To create this array with more than one element, you must allocate sufficient memory for the structure to take into account additional elements.

.. seealso::
https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_privileges
"""

_fields_ = (
("PrivilegeCount", DWORD),
("Privileges", LUID_AND_ATTRIBUTES * 1),
)


PTOKEN_PRIVILEGES = POINTER(TOKEN_PRIVILEGES)

AdjustTokenPrivileges = WINFUNCTYPE(None)(("AdjustTokenPrivileges", dll))
"""Enables, disables or removes privileges in an access token.

.. note::
Enabling or disabling privileges in an access token requires TOKEN_ADJUST_PRIVILEGES access.

.. seealso::
https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-adjusttokenprivileges
"""
AdjustTokenPrivileges.restype = BOOL
AdjustTokenPrivileges.argtypes = (
HANDLE, # TokenHandle: A handle to the access token that contains the privileges to be modified.
BOOL, # DisableAllPrivileges: Specifies whether to disable all of the token's privileges, or modify them based on the NewState parameter.
PTOKEN_PRIVILEGES, # NewState: Specifies an array of privileges and their attributes. Only used if DisableAllPrivileges is FALSE.
DWORD, # BufferLength: The size, in bytes, of the buffer pointed to by the PreviousState parameter.
PTOKEN_PRIVILEGES, # PreviousState: Optional pointer to a buffer to be filled with the the previous state of any privileges that were modified.
PDWORD, # ReturnLength: Optional pointer to a variable that receives the required size, in bytes, of the buffer pointed to by the PreviousState parameter.
)


class PrivilegeName(StrEnum):
"""Privilege constants for use with the LookupPrivilegeValue function.

.. seealso::
https://learn.microsoft.com/en-us/windows/win32/secauthz/privilege-constants
"""

SHUTDOWN = "SeShutdownPrivilege"
"""SE_SHUTDOWN_NAME: Required to shut down a local system."""


LookupPrivilegeValue = WINFUNCTYPE(None)(("LookupPrivilegeValueW", dll))
LookupPrivilegeValue.restype = BOOL
LookupPrivilegeValue.argtypes = (
LPCWSTR, # lpSystemName: The name of the system on which the privilege name is retrieved, or null to find the privilege name on the local system.
LPCWSTR, # lpName: The name of the privilege, as defined in Winnt.h.
PLUID, # lpLuid: Receives the LUID by which the privilege is known on the specified system.
)
35 changes: 35 additions & 0 deletions source/winBindings/user32.py
Original file line number Diff line number Diff line change
Expand Up @@ -1683,3 +1683,38 @@ class NMHDR(Structure):
("idFrom", UINT_PTR),
("code", UINT),
)


class EWX(IntFlag):
"""The shutdown type requested by a call to ExitWindowsEx."""

RESTARTAPPS = 0x00000040
"""EWX_RESTARTAPPS: Shuts down and restarts the system, as well as any applications that have been registered for restart using the RegisterApplicationRestart function."""


class SHTDN_REASON(IntFlag):
"""Possible values of the dwReason parameter of the ExitWindowsEx function.

.. seealso::
https://learn.microsoft.com/en-us/windows/win32/shutdown/system-shutdown-reason-codes
"""

MAJOR_APPLICATION = 0x00040000
"""SHTDN_REASON_MAJOR_APPLICATION: Application issue."""
MINOR_INSTALLATION = 0x00000002
"""SHTDN_REASON_MINOR_INSTALLATION: Installation."""
FLAG_PLANNED = 0x80000000
"""SHTDN_REASON_FLAG_PLANNED: The shutdown was planned, so the system generates a System State Data (SSD) file containing information such as the processes, threads, memory usage, and configuration."""


ExitWindowsEx = WINFUNCTYPE(None)(("ExitWindowsEx", dll))
"""Logs off the interactive user, shuts down the system, or shuts down and restarts the system.

.. seealso::
https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-exitwindowsex
"""
ExitWindowsEx.restype = BOOL
ExitWindowsEx.argtypes = (
UINT, # uFlags: The shutdown type.
DWORD, # dwReason: The reason for initiating the shutdown.
)
Loading