Title: Enable handling logoff and shutdown Windows console events
Type: enhancement Stage: needs patch
Components: ctypes, Extension Modules, Library (Lib), Windows Versions: Python 3.10, Python 3.9
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: eryksun, paul.moore, steve.dower, tim.golden, zach.ware
Priority: normal Keywords:

Created on 2020-07-14 22:02 by eryksun, last changed 2020-07-14 22:02 by eryksun.

Messages (1)
msg373659 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2020-07-14 22:02
A console script should be able to handle Windows console logoff and shutdown events with relatively simple ctypes code, such as the following:

    import ctypes
    kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

    CTRL_C_EVENT = 0

    @ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_ulong)
    def console_ctrl_handler(event):
        if event == CTRL_SHUTDOWN_EVENT:
            return handle_shutdown()
        if event == CTRL_LOGOFF_EVENT:
            return handle_logoff()
        if event == CTRL_CLOSE_EVENT:
            return handle_close()
        if event == CTRL_BREAK_EVENT:
            return handle_break()
        if event == CTRL_C_EVENT:
            return handle_cancel()
        return False # chain to next handler

    if not kernel32.SetConsoleCtrlHandler(console_ctrl_handler, True):
        raise ctypes.WinError(ctypes.get_last_error())

As of 3.9, it's not possible for python.exe to receive the above logoff and shutdown events via ctypes. In these two cases, the console doesn't even get to send a close event, so a console script cannot exit gracefully.

The session server (csrss.exe) doesn't send the logoff and shutdown console events to python.exe because it's seen as a GUI process, which is expected to handle WM_QUERYENDSESSION and WM_ENDSESSION instead. That requires creating a hidden window and running a message loop, which is not nearly as simple as a console control handler.

The system registers python.exe as a GUI process because user32.dll is loaded, which means the process is ready to interact with the desktop in every way, except for the final step of actually creating UI objects. In particular, loading user32.dll causes the system to extend the process and its threads with additional kernel data structures for use by win32k.sys (e.g. a message queue for each thread). It also opens handles for and connects to the session's "WinSta0" interactive window station (a container for an atom table, clipboard, and desktops) and "Default" desktop (a container for UI objects such as windows, menus, and hooks). (The process can connect to a different desktop or window station if set in the lpDesktop field of the process startup info. Also, if the process access token is for a service or batch logon, by default it connects to a non-interactive window station that's named for the logon ID. For example, the SYSTEM logon ID is 0x3e7, so a SYSTEM service or batch process gets connected to "Service-0x0-3e7$".)

Prior to 3.9, python3x.dll loads shlwapi.dll (the lightweight shell API) to access the Windows path functions PathCanonicalizeW and PathCombineW. shlwapi.dll in turn loads user32.dll. 3.9+ is one step closer to the non-GUI goal because it no longer depends on shlwapi.dll. Instead it always uses the newer PathCchCanonicalizeEx and PathCchCombineEx functions from api-ms-win-core-path-l1-1-0.dll, which is implemented by the base API (kernelbase.dll) instead of the shell API. 

The next hurdle is extension modules, especially the _ctypes extension module, since it's needed for the console control handler. _ctypes.pyd loads ole32.dll, which in turn loads user32.dll. This is just to call ProgIDFromCLSID, which is rarely used. I see no reason that ole32.dll can't be delay loaded or just manually link to ProgIDFromCLSID on first use via GetModuleHandleW / LoadLibraryExW and GetProcAddress. I did a quick patch to implement the latter, and, since user32.dll no longer gets loaded, the console control handler is enabled for console logoff and shutdown events. So this is the minimal fix to resolve this issue in 3.9+.

Additional modules

winsound loads user32.dll for MessageBeep. The Beep and PlaySound functions don't require user32.dll, so winsound is still useful if it gets delay loaded. 

_ssl and _hashlib depend on libcrypto, which loads user32.dll for MessageBoxW, GetProcessWindowStation and GetUserObjectInformationW. The latter two are called in OPENSSL_isservice [1] in order to get the window station name. If StandardError isn't a valid file handle, OPENSSL_isservice determines whether an error should be reported as an event or interactively shown with a message box. user32.dll can be delay loaded for this, which, if I'm reading the source right, will never occur as long as StandardError is a valid file.
Date User Action Args
2020-07-14 22:02:26eryksuncreate