Skip to content
Draft
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 @@ -31,6 +31,7 @@ public DotNetCLI (string projectOrSolution)
/// Creates and starts a `dotnet` process with the specified arguments.
/// </summary>
/// <param name="args">command arguments</param>
/// <param name="workingDirectory">optional working directory</param>
/// <returns>A started Process instance. Caller is responsible for disposing.</returns>
protected Process ExecuteProcess (string [] args, string workingDirectory = null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using NUnit.Framework;

Expand Down Expand Up @@ -32,5 +33,29 @@ public static void SetEnvironmentVariable (this ProcessStartInfo psi, string key

Assert.Inconclusive ("Could not set ProcessStartInfo environment variable.");
}

/// <summary>
/// Sends Ctrl+C (SIGINT) to the specified process.
/// Currently only supported on Unix/macOS; throws PlatformNotSupportedException on Windows.
/// </summary>
/// <remarks>
/// See dotnet/sdk's NativeMethods.cs and GivenDotnetRunIsInterrupted.cs for the pattern used here.
/// </remarks>
public static void SendCtrlC (this Process process)
{
if (OperatingSystem.IsWindows ()) {
throw new PlatformNotSupportedException ("SendCtrlC is not yet implemented on Windows.");
}
int result = kill (process.Id, SIGINT);
if (result != 0) {
throw new InvalidOperationException (
$"kill({process.Id}, SIGINT) failed with errno {Marshal.GetLastPInvokeError ()}");
}
}

[DllImport ("libc", SetLastError = true)]
static extern int kill (int pid, int sig);

const int SIGINT = 2;
}
}
90 changes: 90 additions & 0 deletions tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,96 @@ public void DotNetRunWaitForExit ()
Assert.IsTrue (foundMessage, $"Expected message '{logcatMessage}' was not found in output. See {logPath} for details.");
}

[Test]
public void DotNetRunCtrlC ()
{
AssertCommercialBuild (); //FIXME: https://github.com/dotnet/android/issues/10832

const string logcatMessage = "DOTNET_RUN_CTRLC_TEST_99999";
var proj = new XamarinAndroidApplicationProject ();

// Enable verbose output from Microsoft.Android.Run for debugging
proj.SetProperty ("_AndroidRunExtraArgs", "--verbose");

// Add a Console.WriteLine that will appear in logcat
proj.MainActivity = proj.DefaultMainActivity.Replace (
"//${AFTER_ONCREATE}",
$"Console.WriteLine (\"{logcatMessage}\");");

using var builder = CreateApkBuilder ();
builder.Save (proj);

var dotnet = new DotNetCLI (Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath));
Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed");

// Start dotnet run with WaitForExit=true, which uses Microsoft.Android.Run
using var process = dotnet.StartRun ();

var locker = new Lock ();
var output = new StringBuilder ();
var appLaunched = new ManualResetEventSlim (false);

process.OutputDataReceived += (sender, e) => {
if (e.Data != null) {
lock (locker) {
output.AppendLine (e.Data);
if (e.Data.Contains (logcatMessage)) {
appLaunched.Set ();
}
}
}
};
process.ErrorDataReceived += (sender, e) => {
if (e.Data != null) {
lock (locker) {
output.AppendLine ($"STDERR: {e.Data}");
}
}
};

process.BeginOutputReadLine ();
process.BeginErrorReadLine ();

// Wait for the app to start and produce logcat output
bool launched = appLaunched.Wait (TimeSpan.FromSeconds (ActivityStartTimeoutInSeconds));

string logPath = Path.Combine (Root, builder.ProjectDirectory, "dotnet-run-ctrlc-output.log");
try {
Assert.IsTrue (launched, $"Expected message '{logcatMessage}' was not found in output within {ActivityStartTimeoutInSeconds}s.");

// Verify the app is running on the device
var pidOutput = RunAdbCommand ($"shell pidof {proj.PackageName}").Trim ();
Assert.IsTrue (!string.IsNullOrEmpty (pidOutput) && int.TryParse (pidOutput.Split (' ') [0], out _),
$"App should be running on the device. pidof output: '{pidOutput}'");

// Send Ctrl+C to the dotnet run process
process.SendCtrlC ();

// Wait for the process to exit gracefully
bool exited = process.WaitForExit (30_000);
Assert.IsTrue (exited, "dotnet run process should have exited 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");

// Verify the app is no longer running on the device
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
if (!process.HasExited) {
process.Kill (entireProcessTree: true);
process.WaitForExit ();
}

File.WriteAllText (logPath, output.ToString ());
TestContext.AddTestAttachment (logPath);
}
}

[Test]
public void DotNetRunWithDeviceParameter ()
{
Expand Down