Skip to content

Ctrl-C / window-close does not cleanly shut down wfweb on Windows #32

@adecarolis

Description

@adecarolis

Problem

On Windows, pressing Ctrl-C (or clicking the console window's close button) does not give wfweb a chance to run its destructors. As a result:

  • ~icomUdpBase() does not run, so the 0x05 token-removal packet is never sent to the radio.
  • The rig's LAN slot stays occupied from its point of view until an idle timeout elapses, blocking other apps (RS-BA1, wfview, another wfweb instance) from connecting.
  • Serial port handles, audio handles, and the settings .ini flush may also be skipped.

This behavior is Windows-specific. On Linux/macOS, the existing POSIX signal(SIGINT/SIGTERM, cleanup) handler fires, QCoreApplication::quit() returns from a.exec(), and the normal Qt teardown runs — LAN radios get the 0x05 disconnect cleanly.

Root cause

Our current Windows handler in src/main.cpp:

#ifdef Q_OS_WIN
bool __stdcall cleanup(DWORD sig)
#endif
{
    case SIGINT:
    case SIGTERM:
        qInfo() << \"terminate signal caught\";
        if (kb != Q_NULLPTR) kb->terminate();
        if (w != Q_NULLPTR) w->deleteLater();
        QCoreApplication::quit();
        break;
    ...
}
SetConsoleCtrlHandler((PHANDLER_ROUTINE)cleanup, TRUE);

Three Windows-specific problems:

  1. Handler runs on its own OS thread. Windows spawns a fresh thread to run the SetConsoleCtrlHandler callback; it is not the main thread. QCoreApplication::quit() does post QEvent::Quit to the main thread correctly, but the handler itself does not wait for the main thread to process it.

  2. Windows has a short deadline after the handler returns. Once the handler returns TRUE, Windows may proceed to terminate the process (via an ExitProcess-style path) without waiting for our main thread to finish cleanup. The practical deadlines are roughly:

    • CTRL_C_EVENT / CTRL_BREAK_EVENT: ~5 s
    • CTRL_CLOSE_EVENT (console X button): ~5 s
    • CTRL_LOGOFF_EVENT / CTRL_SHUTDOWN_EVENT: ~2 s
      If the main thread hasn't returned from a.exec() and torn down servermain by then, Qt destructors never run.
  3. Using switch(sig) with SIGINT / SIGTERM labels is wrong on Windows. The Windows handler receives CTRL_C_EVENT, CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, CTRL_SHUTDOWN_EVENT. These are different numeric constants from SIGINT/SIGTERM. The current case SIGINT: branch is reached only coincidentally (or not at all) — the X-button close in particular is missed entirely.

Proposed solution

Rewrite the Windows branch of cleanup() to:

  1. Dispatch on the correct CTRL_*_EVENT values.
  2. Post QCoreApplication::quit() to the main thread from the handler.
  3. Block the handler thread on an atomic flag (or a HANDLE event) set by main() immediately after a.exec() returns, so Windows does not terminate the process before Qt teardown finishes. Use a bounded wait (~4 s for C/BREAK/CLOSE, ~1.5 s for LOGOFF/SHUTDOWN) so we never exceed the platform deadline.

Sketch (src/main.cpp, Windows-only):

#ifdef Q_OS_WIN
static std::atomic<bool> g_mainExited{false};

bool __stdcall cleanup(DWORD sig)
{
    int budgetMs = 0;
    switch (sig) {
        case CTRL_C_EVENT:
        case CTRL_BREAK_EVENT:
        case CTRL_CLOSE_EVENT:     budgetMs = 4000; break;
        case CTRL_LOGOFF_EVENT:
        case CTRL_SHUTDOWN_EVENT:  budgetMs = 1500; break;
        default: return FALSE;
    }
    qInfo() << \"terminate signal caught (Windows)\" << sig;
    if (kb != Q_NULLPTR) kb->terminate();
    if (w != Q_NULLPTR) QMetaObject::invokeMethod(w, \"deleteLater\", Qt::QueuedConnection);
    QMetaObject::invokeMethod(QCoreApplication::instance(), \"quit\", Qt::QueuedConnection);

    // Block so Windows doesn't kill us before ~servermain() finishes
    // (this is what lets the 0x05 LAN disconnect packet actually go out).
    const int step = 50;
    for (int waited = 0; waited < budgetMs && !g_mainExited.load(); waited += step) {
        Sleep(step);
    }
    return TRUE;
}
#endif

And in main() after a.exec():

int rc = a.exec();
#ifdef Q_OS_WIN
g_mainExited.store(true);
#endif
return rc;

The POSIX branch is untouched — Linux/macOS continue to use signal(SIGINT/SIGTERM, cleanup) as today.

Acceptance

  • On Windows, pressing Ctrl-C while wfweb is connected to a LAN Icom radio results in the 0x05 token-removal packet being sent (visible in wfweb log as 'Sending token removal packet', and the radio's LAN slot is immediately available to another client).
  • Closing the console window (X button) triggers the same clean teardown path.
  • Linux/macOS behavior is unchanged.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions