Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public async Task InitializeAsync()
() =>
{
ConsoleLog(PlatformResources.CancellingTestSession);
ConsoleLog(PlatformResources.PressCtrlCAgainToForceExit);
return Task.CompletedTask;
}).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ public void StartCancelling()
{
terminal.AppendLine();
terminal.AppendLine(PlatformResources.CancellingTestSession);
terminal.AppendLine(PlatformResources.PressCtrlCAgainToForceExit);
terminal.AppendLine();
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,11 @@ public async Task InitializeAsync()
// stdout, corrupting the JSON document. Route a single-line cancellation notice
// to stderr instead so the user still gets feedback on Ctrl+C.
await _policiesService.RegisterOnAbortCallbackAsync(
() => WriteToStandardErrorAsync(PlatformResources.CancellingTestSession)).ConfigureAwait(false);
async () =>
{
await WriteToStandardErrorAsync(PlatformResources.CancellingTestSession).ConfigureAwait(false);
await WriteToStandardErrorAsync(PlatformResources.PressCtrlCAgainToForceExit).ConfigureAwait(false);
}).ConfigureAwait(false);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ Read more about Microsoft Testing Platform telemetry: https://aka.ms/testingplat
<data name="CancellingTestSession" xml:space="preserve">
<value>Canceling the test session...</value>
</data>
<data name="PressCtrlCAgainToForceExit" xml:space="preserve">
<value>Press Ctrl+C again to force exit.</value>
</data>
<data name="DiagnosticFileLevelWithAsyncFlush" xml:space="preserve">
<value>Diagnostic file (level '{0}' with async flush): {1}</value>
<comment>0 level such as verbose,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Může mít jenom jeden argument jako řetězec ve formátu &lt;value&gt;[h|m|s], kde value je hodnota datového typu float.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">Proces měl být ukončen před tím, než jsme mohli určit tuto hodnotu.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Nimmt ein Argument als Zeichenfolge im Format &lt;value&gt;[h|m|s], wobei "value" auf "float" festgelegt ist.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">Der Prozess hätte beendet werden müssen, bevor dieser Wert ermittelt werden kann</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Toma un argumento como cadena con el formato &lt;value&gt;[h|m|s] donde 'value' es float.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">El proceso debería haberse terminado para poder determinar este valor</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Prend un argument sous forme de chaîne au format &lt;value&gt;[h|m|s] où « value » est float.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">Le processus aurait dû s’arrêter avant que nous puissions déterminer cette valeur</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Acquisisce un argomento come stringa nel formato &lt;value&gt;[h|m|s] dove 'value' è float.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">Il processo dovrebbe essere terminato prima di poter determinare questo valore</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
1 つの引数を文字列として &lt;value&gt;[h|m|s] の形式で使用します。この場合、'value' は float です。</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">この値を決定する前にプロセスを終了する必要があります</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
'value'가 float인 &lt;value&gt;[h|m|s] 형식의 문자열로 인수 하나를 사용합니다.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">이 값을 결정하려면 프로세스가 종료되어야 합니다.</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Pobiera jeden argument jako ciąg w formacie &lt;value&gt;[h|m|s], gdzie element „value” ma wartość zmiennoprzecinkową.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">Proces powinien zakończyć się przed ustaleniem tej wartości</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Recebe um argumento como cadeia de caracteres no formato &lt;valor&gt;[h|m|s] em que 'valor' é float.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">O processo deve ter sido encerrado antes que possamos determinar esse valor</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Принимает один аргумент в виде строки в формате &lt;value&gt;[h|m|s], где "value" — число с плавающей точкой.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">Процесс должен быть завершен, прежде чем мы сможем определить это значение</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
Bir bağımsız değişkeni, 'value' değerinin kayan olduğu &lt;value&gt;[h|m|s] biçiminde dize olarak alır.</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">Bu değeri belirleyebilmemiz için süreçten çıkılmış olması gerekir</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
获取一个字符串形式的参数,格式为 &lt;value&gt;[h|m|s],其中 "value" 为浮点型。</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">在我们确定此值之前,流程应该已退出</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ Takes one argument as string in the format &lt;value&gt;[h|m|s] where 'value' is
將一個引數作為字串,格式為 &lt;value&gt;[h|m|s],其中 'value' 為 float。</target>
<note />
</trans-unit>
<trans-unit id="PressCtrlCAgainToForceExit">
<source>Press Ctrl+C again to force exit.</source>
<target state="new">Press Ctrl+C again to force exit.</target>
<note />
</trans-unit>
<trans-unit id="ProcessHasNotYetExitedErrorMessage">
<source>Process should have exited before we can determine this value</source>
<target state="translated">在我們確定此值之前,流程應已結束</target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@ namespace Microsoft.Testing.Platform.Services;

internal sealed class CTRLPlusCCancellationTokenSource : ITestApplicationCancellationTokenSource, IDisposable
{
private const int StateIdle = 0;
private const int StateCancelling = 1;
private const int StateForcing = 2;

private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly IEnvironment _environment;
private readonly ILogger? _logger;
private readonly IConsole? _subscribedConsole;
private int _state = StateIdle;
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
private int _disposed;

public CTRLPlusCCancellationTokenSource(IConsole? console = null, ILogger? logger = null)
public CTRLPlusCCancellationTokenSource(IConsole? console = null, ILogger? logger = null, IEnvironment? environment = null)
{
if (console is not null && !IsCancelKeyPressNotSupported())
{
console.CancelKeyPress += OnConsoleCancelKeyPressed;
_subscribedConsole = console;
}

_environment = environment ?? new SystemEnvironment();
_logger = logger;
}
Comment thread
Evangelink marked this conversation as resolved.

Expand All @@ -40,19 +50,60 @@ public CancellationToken CancellationToken

private void OnConsoleCancelKeyPressed(object? sender, ConsoleCancelEventArgs e)
{
// Suppress the runtime's default Ctrl+C handling so we control the exit code on
// both the first (cooperative) and the second (force-exit) press.
e.Cancel = true;
try

// The state machine counts user Ctrl+C presses *independently* of external cancellation
// sources (timeout, max-failed-tests, explicit Cancel()). This honors the contract
// advertised next to the "Cancelling..." message ("Press Ctrl+C again to force exit.")
// regardless of who initiated the cancellation: the user must always press Ctrl+C twice
// to force-exit.
if (Interlocked.CompareExchange(ref _state, StateCancelling, StateIdle) == StateIdle)
{
_cancellationTokenSource.Cancel();
// First user Ctrl+C: cooperative cancellation. If the token was already cancelled
// by an external source this is effectively a no-op, but we still transitioned the
// state so the next press goes to force-exit.
try
{
_cancellationTokenSource.Cancel();
}
catch (AggregateException ex)
{
_logger?.LogWarning($"Exception during CTRLPlusCCancellationTokenSource cancel:\n{ex}");
}

return;
}
Comment thread
Evangelink marked this conversation as resolved.
catch (AggregateException ex)

// Second user Ctrl+C: force-exit. We intentionally do not print an extra
// "Forcing exit..." message here because the IConsole abstraction has no stderr channel
// and writing to stdout would corrupt the JSON document produced by --list-tests json.
// The user already saw the "Press Ctrl+C again to force exit." hint, so the exit itself
// is the confirmation. Any subsequent presses are suppressed by the StateForcing guard.
if (Interlocked.CompareExchange(ref _state, StateForcing, StateCancelling) == StateCancelling)
{
_logger?.LogWarning($"Exception during CTRLPlusCCancellationTokenSource cancel:\n{ex}");
_environment.Exit((int)ExitCode.TestSessionAborted);
}
}

public void Dispose()
=> _cancellationTokenSource.Dispose();
{
if (Interlocked.Exchange(ref _disposed, 1) != 0)
{
return;
}

// We stored the console reference only when subscription was actually performed
// (i.e. when CancelKeyPress is supported on this platform), so we can safely call -=
// on the same supported-platform paths.
if (_subscribedConsole is not null && !IsCancelKeyPressNotSupported())
{
_subscribedConsole.CancelKeyPress -= OnConsoleCancelKeyPressed;
}

_cancellationTokenSource.Dispose();
}

public void Cancel()
=> _cancellationTokenSource.Cancel();
Expand Down
Loading
Loading