What's wrong
UIApplication.RunAsync registers an interrupt handler for the whole run (TUI/Services/UIApplication.cs:145) and disposes it only in finally (:176). ConsoleInterruptSource.OnSignal (TUI/Services/ConsoleInterruptSource.cs:40-46) cancels the runtime's default termination on every signal: Ctrl+C sets e.Cancel = true, and SIGTERM sets context.Cancel = true. It then only calls Shutdown(), which cancels a token. That token is observed only by the input loop, between keys.
So if the app's own code hangs, nothing will ever act on that token. That covers an OnHandleInput or a render that deadlocks or loops, and a synchronous wait inside a handler. Every further Ctrl+C or kill <pid> is cancelled as well. The only ways out are kill -9 or closing the terminal. Before #110/#117, the runtime's default behaviour ended such a process.
Reproduction (main 95d9508, real process and real signals)
The root is a StackPanel subclass whose OnHandleInput does Thread.Sleep(Timeout.Infinite). The provider's ReadInputAsync returns a key immediately. With await app.RunAsync() running, send three signals of each kind, 1 s apart:
SIGINT x3: process STILL ALIVE (needed kill -9)
SIGTERM x3: process STILL ALIVE (needed kill -9)
Control: the same hanging handler called outside RunAsync exits after a single SIGINT or SIGTERM.
Why it matters
A bug in application code becomes a process that can't be interrupted. It also defeats supervisors that expect SIGTERM to end the process, such as systemd, docker stop and CI timeouts. Those only escalate to SIGKILL after a grace period.
Suggested fix / acceptance criteria
- Cancel only the first signal of a run and turn it into a graceful
Shutdown().
- If a second Ctrl+C or SIGTERM arrives while shutdown is still pending, don't cancel it, so the runtime terminates the process. Alternatively, restore the terminal and call
Environment.Exit.
- Optionally, force the exit after a grace timeout that starts at the first signal.
- Add a unit test through
OnSignal / FakeInterruptSource: the first signal cancels and calls onInterrupt, and a second signal is not cancelled.
What's wrong
UIApplication.RunAsyncregisters an interrupt handler for the whole run (TUI/Services/UIApplication.cs:145) and disposes it only infinally(:176).ConsoleInterruptSource.OnSignal(TUI/Services/ConsoleInterruptSource.cs:40-46) cancels the runtime's default termination on every signal: Ctrl+C setse.Cancel = true, and SIGTERM setscontext.Cancel = true. It then only callsShutdown(), which cancels a token. That token is observed only by the input loop, between keys.So if the app's own code hangs, nothing will ever act on that token. That covers an
OnHandleInputor a render that deadlocks or loops, and a synchronous wait inside a handler. Every further Ctrl+C orkill <pid>is cancelled as well. The only ways out arekill -9or closing the terminal. Before #110/#117, the runtime's default behaviour ended such a process.Reproduction (main 95d9508, real process and real signals)
The root is a
StackPanelsubclass whoseOnHandleInputdoesThread.Sleep(Timeout.Infinite). The provider'sReadInputAsyncreturns a key immediately. Withawait app.RunAsync()running, send three signals of each kind, 1 s apart:Control: the same hanging handler called outside
RunAsyncexits after a single SIGINT or SIGTERM.Why it matters
A bug in application code becomes a process that can't be interrupted. It also defeats supervisors that expect SIGTERM to end the process, such as systemd,
docker stopand CI timeouts. Those only escalate to SIGKILL after a grace period.Suggested fix / acceptance criteria
Shutdown().Environment.Exit.OnSignal/FakeInterruptSource: the first signal cancels and callsonInterrupt, and a second signal is not cancelled.