PromptPlus

|Adr-Plus Fields|Values Migrated | |–|–| |ADR|Guard interactive controls against redirected console input in Run()| |Version|01| |Revision|02| |Scope|| |Domain|| |Created|Proposed (2026-07-28)| |Changed|Accepted (2026-07-31)| |Superseded||

PromptPlus # PromptPlus ## **ADR0023V01R02** [![NuGet](https://img.shields.io/badge/NuGet-PromptPlus-blue)](https://www.nuget.org/packages/PromptPlus) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-512BD4)](https://dotnet.microsoft.com/)

↑ ADR Index


ADR0023V01R02 — Guard interactive controls against redirected console input in Run()

Context

BaseControlPrompt<T>.WaitKeypress — the loop every interactive control blocks on while waiting for a key — is:

while (!console.KeyAvailable && !token.IsCancellationRequested)
{
    ...
    token.WaitHandle.WaitOne(16);
}

Show() and the default Run() overload use CancellationToken.None, which never cancels. Per ConsolePlus’s ADR0015 — Redirected/headless console I/O contract, KeyAvailable fails safe under redirected input — it returns false forever instead of throwing. Combined, this means any interactive control run against a redirected console with no caller-supplied CancellationToken — the common case, since Show() and plain .Run() are the documented default usage pattern throughout docs/hangs forever, with no exception, no log, and no way to tell it apart from a control just waiting for real user input.

This was previously masked by an inconsistency on the ConsolePlus side: before its own redirected-I/O contract (ADR0015) was in place, KeyAvailable/ReadKey threw a raw, undocumented exception under redirection, which at least crashed loudly (if unpredictably). Fixing that inconsistency in ConsolePlus (correctly, per its own contract) removed the accidental crash and exposed the hang underneath — confirmed empirically: PromptPlus.Controls.Input("Name").Run() under redirected stdin did not return within 3 seconds.

Not every control is affected equally. ProgressBar, Task, MultiTasks, and Timer override WaitKeypress but complete on their own signal (progress reaching 100%, the wrapped task finishing, the countdown elapsing) — they never actually depend on a real key becoming available, and were confirmed empirically to complete normally under redirected input. Only controls that have no completion path except a real keystroke are at risk.

Decision

Guard at the top of BaseControlPrompt<T>.Run() — the single method every control and widget reaches through the IControls/IWidgets factory interfaces — rather than in the constructor or in each control individually:

if (!isWidget && !IsLiveAutoRenderControl && console.IsInputRedirected && !console.DemoModeActive)
{
    throw new InvalidOperationException(
        "Cannot run an interactive control: console input is redirected and no key presses can be read.");
}

Consequences