Skip to content
Open
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
50 changes: 29 additions & 21 deletions src/Microsoft.Android.Run/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

const string Name = "Microsoft.Android.Run";
const string VersionsFileName = "Microsoft.Android.versions.txt";
const int CtrlCExitCode = 130; // Standard Unix exit code for SIGINT: 128 + signal 2.
const int StopAppTimeoutSeconds = 10;

string? adbPath = null;
string? adbTarget = null;
Expand All @@ -17,14 +19,15 @@
int? logcatPid = null;
Process? logcatProcess = null;
CancellationTokenSource cts = new ();
int ctrlCRequested = 0;
string? logcatArgs = null;
bool isDotnetTestMode = false;
string? dotnetTestPipe = null;

try {
return await RunAsync (args);
} catch (OperationCanceledException) {
return 130; // 128 + SIGINT(2), standard Unix convention for Ctrl+C
return CtrlCExitCode;
} catch (Exception ex) {
Console.Error.WriteLine ($"Error: {ex.Message}");
if (verbose)
Expand Down Expand Up @@ -185,18 +188,24 @@ async Task<int> RunAsync (string[] args)
// Set up Ctrl+C handler
Console.CancelKeyPress += OnCancelKeyPress;

int exitCode;
bool cancellationRequested;
try {
if (isDotnetTestMode)
return await RunDotnetTestAsync (remaining);

if (isInstrumentMode)
return await RunInstrumentationAsync (remaining);

return await RunAppAsync ();
exitCode = await RunDotnetTestAsync (remaining);
else if (isInstrumentMode)
exitCode = await RunInstrumentationAsync (remaining);
else
exitCode = await RunAppAsync ();
} finally {
Console.CancelKeyPress -= OnCancelKeyPress;
cancellationRequested = Volatile.Read (ref ctrlCRequested) != 0;
if (cancellationRequested)
await StopAppAsync ();
cts.Dispose ();
}

return cancellationRequested ? CtrlCExitCode : exitCode;
}

void OnCancelKeyPress (object? sender, ConsoleCancelEventArgs e)
Expand All @@ -205,20 +214,8 @@ void OnCancelKeyPress (object? sender, ConsoleCancelEventArgs e)
Console.WriteLine ();
Console.WriteLine ("Stopping application...");

Interlocked.Exchange (ref ctrlCRequested, 1);
cts.Cancel ();

// Force-stop the app (fire-and-forget in cancel handler)
_ = StopAppAsync ();

// Kill logcat process if running
try {
if (logcatProcess != null && !logcatProcess.HasExited) {
logcatProcess.Kill ();
}
} catch (Exception ex) {
if (verbose)
Console.Error.WriteLine ($"Error killing logcat process: {ex.Message}");
}
}

async Task<int> RunInstrumentationAsync (List<string> instrumentationArgs)
Expand Down Expand Up @@ -616,7 +613,18 @@ async Task StopAppAsync ()
return;

var userArg = string.IsNullOrEmpty (deviceUserId) ? "" : $" --user {deviceUserId}";
await AdbHelper.RunAsync (adbPath, adbTarget, $"shell am force-stop{userArg} {package}", CancellationToken.None, verbose);
using var timeoutCts = new CancellationTokenSource (TimeSpan.FromSeconds (StopAppTimeoutSeconds));
try {
var (exitCode, _, error) = await AdbHelper.RunAsync (adbPath, adbTarget, $"shell am force-stop{userArg} {package}", timeoutCts.Token, verbose);
if (exitCode != 0)
Console.Error.WriteLine ($"Error: Failed to stop app: {error}");
} catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) {
Console.Error.WriteLine ($"Error: Timed out stopping app after {StopAppTimeoutSeconds} seconds.");
} catch (Exception ex) {
Console.Error.WriteLine ($"Error: Failed to stop app: {ex.Message}");
if (verbose)
Console.Error.WriteLine (ex.ToString ());
}
}

string? FindAdbPath ()
Expand Down
18 changes: 6 additions & 12 deletions tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -474,24 +474,18 @@ public void DotNetRunCtrlC ()
// Wait for the process to exit gracefully
bool exited = process.WaitForExit (30_000);
Assert.IsTrue (exited, "dotnet run process should have exited after SIGINT");
Assert.AreEqual (130, process.ExitCode, "dotnet run process should report user cancellation after SIGINT");

// Verify the output contains the "Stopping application..." message from Microsoft.Android.Run
string outputText = output.ToString ();
Assert.IsTrue (outputText.Contains ("Stopping application..."),
$"Output should contain 'Stopping application...' from Microsoft.Android.Run's Ctrl+C handler");
Assert.IsFalse (outputText.Contains ("Error: The operation was canceled."),
Comment thread
jonathanpeppers marked this conversation as resolved.
"Cancellation should not be reported as an error");

// Verify the app is no longer running on the device.
// Poll with retries since StopAppAsync is fire-and-forget in the Ctrl+C handler.
bool appStopped = false;
for (int i = 0; i < 10; i++) {
pidOutput = RunAdbCommand ($"shell pidof {proj.PackageName}").Trim ();
if (string.IsNullOrEmpty (pidOutput)) {
appStopped = true;
break;
}
Thread.Sleep (1000);
}
Assert.IsTrue (appStopped,
// Microsoft.Android.Run must not exit until force-stop has completed.
pidOutput = RunAdbCommand ($"shell pidof {proj.PackageName}").Trim ();
Assert.IsTrue (string.IsNullOrEmpty (pidOutput),
$"App should not be running on the device after Ctrl+C. pidof output: '{pidOutput}'");
} finally {
// Ensure the process is killed if it's still running
Expand Down