Skip to content

Use atexit/signal callbacks to catch windows events and log them - #210

Draft
ader1990 wants to merge 1 commit into
cloudbase:masterfrom
ader1990:use_at_exit_to_catch_windows_reboots
Draft

Use atexit/signal callbacks to catch windows events and log them#210
ader1990 wants to merge 1 commit into
cloudbase:masterfrom
ader1990:use_at_exit_to_catch_windows_reboots

Conversation

@ader1990

@ader1990 ader1990 commented Jul 30, 2026

Copy link
Copy Markdown
Member

There are some corner case scenarios when Windows starts - then cloudbase-init runs and tries to execute its plugins. For example, userdata or local scripts that are running. During the execution of the scripts, there is the chance that a Windows reboot is triggered by, for example, a Windows update process. This triggers a cascade of WM_ENDSESSION events sent to the processes that cloudbase-init is a parent of - the userdata scripts that are running. At the same time, WM_ENDSESSION is also sent to the cloudbase-init process.

The documentation states that if the WM_ENDSESSION is not handled and the process has exited, a SIGTERM signal is sent to the process, killing it. If the userdata process is killed, it will exit with exit code 1, and then cloudbase-init can have time, in theory, to set the UserDataPlugin registry key state as executed. Then, after the reboot, the UserDataPlugin will not be executed anymore. In the case that the userdata script code had leveraged the exit codes to reboot, like 1001 or 1003, this corner case behaviour would override this.

This change is to just log events, and not try to fix the behaviour, to ensure in the future that we have more information on the timeline of what is happening and in what order.

@ader1990

Copy link
Copy Markdown
Member Author

A handling of the Windows event will not solve the problem though, as there is still the problem of handling the userdata process as well. The approach is very complicated, and will end up in a complex ping-pong scenario between threads (which we need to create and then maintain). I think a more simple and elegant solution, that can also be helpful in other scenarios would be to add a separate config option to cloudbase-init:

[DEFAULT]
enabled_plugins_at_every_boot = <list of plugins that will run at every boot>

Still, will leave here a possible implementation of such a handler:

import ctypes
from ctypes import wintypes
import logging
import os
import threading
import time

WM_QUERYENDSESSION = 0x0011
WM_ENDSESSION = 0x0016
GWL_WNDPROC = -4

user32 = ctypes.WinDLL("user32", use_last_error=True)
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

WNDPROC = ctypes.WINFUNCTYPE(
    wintypes.LPARAM, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM
)

shutdown_event = threading.Event()
window_ready = threading.Event()
old_wnd_proc = None


@WNDPROC
def custom_wnd_proc(hwnd, msg, w_param, l_param):
    if msg == WM_QUERYENDSESSION:
        return 1

    if msg == WM_ENDSESSION:
        if w_param:
            shutdown_event.set()
            return 0

    return user32.CallWindowProcW(old_wnd_proc, hwnd, msg, w_param, l_param)


def create_hidden_window():
    global old_wnd_proc
    hwnd = user32.CreateWindowExW(
        0,
        "STATIC",
        "CtypesShutdownListener",
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        kernel32.GetModuleHandleW(None),
        None,
    )

    if not hwnd:
        window_ready.set()
        return

    if ctypes.sizeof(ctypes.c_void_p) == 8:
        user32.SetWindowLongPtrW.restype = ctypes.c_void_p
        user32.SetWindowLongPtrW.argtypes = [wintypes.HWND, ctypes.c_int, ctypes.c_void_p]
        old_wnd_proc = user32.SetWindowLongPtrW(hwnd, GWL_WNDPROC, custom_wnd_proc)
    else:
        user32.SetWindowLongW.restype = ctypes.c_void_p
        user32.SetWindowLongW.argtypes = [wintypes.HWND, ctypes.c_int, ctypes.c_void_p]
        old_wnd_proc = user32.SetWindowLongW(hwnd, GWL_WNDPROC, custom_wnd_proc)

    window_ready.set()

    msg = wintypes.MSG()
    while user32.GetMessageW(ctypes.byref(msg), 0, 0, 0) != 0:
        user32.TranslateMessage(ctypes.byref(msg))
        user32.DispatchMessageW(ctypes.byref(msg))


def background_worker():
    while not shutdown_event.is_set():
        time.sleep(1)

    # here we should communicate to the plugin execution to not write the registry key values
    # that the plugin execution might have ended in failure because of the WM_ENDSESSION event
    time.sleep(1)


if __name__ == "__main__":

    worker = threading.Thread(target=background_worker)
    worker.start()

    pump_thread = threading.Thread(target=create_hidden_window, daemon=True)
    pump_thread.start()
    window_ready.wait()

    while worker.is_alive():
        worker.join(timeout=1.0)

@ader1990

Copy link
Copy Markdown
Member Author

Linking #204 for visibility.

Change-Id: Ifbfeed6b23a9cbce46cd4cb1724e5c769646653b
Signed-off-by: Adrian Vladu <avladu@cloudbasesolutions.com>
@ader1990
ader1990 force-pushed the use_at_exit_to_catch_windows_reboots branch from d7b6f77 to 8378dc5 Compare July 30, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant