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
191 changes: 178 additions & 13 deletions src/Microsoft.Android.Run/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Diagnostics;
using System.Text;
using Mono.Options;
using Xamarin.Android.Tools;

const string Name = "Microsoft.Android.Run";
const string VersionsFileName = "Microsoft.Android.versions.txt";
const string DotNetRunArgumentsKey = "dotnetRunArgumentsBase64";

string? adbPath = null;
string? adbTarget = null;
Expand Down Expand Up @@ -34,6 +36,7 @@ async Task<int> RunAsync (string[] args)
{
bool showHelp = false;
bool showVersion = false;
var remaining = new List<string> ();

var options = new OptionSet {
$"Usage: {Name} [OPTIONS]",
Expand Down Expand Up @@ -79,18 +82,24 @@ async Task<int> RunAsync (string[] args)
{ "h|help|?",
"Show this help message and exit.",
v => showHelp = v != null },
// Mono.Options uses "<>" as the default handler for non-option arguments.
// `dotnet run --` consumes the `--` delimiter before this tool is launched,
// so this captures only the forwarded app arguments that follow it.
{ "<>",
v => remaining.Add (v) },
};

List<string> remaining;
try {
remaining = options.Parse (args);
options.Parse (args);
} catch (OptionException e) {
Console.Error.WriteLine ($"Error: {e.Message}");
Console.Error.WriteLine ($"Try '{Name} --help' for more information.");
return 1;
}

if (remaining.Count > 0 && !isDotnetTestMode) {
bool isInstrumentMode = !string.IsNullOrEmpty (instrumentation);

if (remaining.Count > 0 && !isDotnetTestMode && !isInstrumentMode) {
Console.Error.WriteLine ($"Error: Unexpected argument(s): {string.Join (" ", remaining)}");
Console.Error.WriteLine ($"Try '{Name} --help' for more information.");
return 1;
Expand Down Expand Up @@ -126,8 +135,6 @@ async Task<int> RunAsync (string[] args)
return 1;
}

bool isInstrumentMode = !string.IsNullOrEmpty (instrumentation);

if (!isInstrumentMode && string.IsNullOrEmpty (activity) && !isDotnetTestMode) {
Console.Error.WriteLine ("Error: --activity or --instrument is required.");
Console.Error.WriteLine ($"Try '{Name} --help' for more information.");
Expand Down Expand Up @@ -183,7 +190,7 @@ async Task<int> RunAsync (string[] args)
return await RunDotnetTestAsync (remaining);

if (isInstrumentMode)
return await RunInstrumentationAsync ();
return await RunInstrumentationAsync (remaining);

return await RunAppAsync ();
} finally {
Expand Down Expand Up @@ -214,11 +221,13 @@ void OnCancelKeyPress (object? sender, ConsoleCancelEventArgs e)
}
}

async Task<int> RunInstrumentationAsync ()
async Task<int> RunInstrumentationAsync (List<string> instrumentationArguments)
{
// Build the am instrument command
var userArg = string.IsNullOrEmpty (deviceUserId) ? "" : $" --user {deviceUserId}";
var cmdArgs = $"shell am instrument -w{userArg} {package}/{instrumentation}";
var encodedArguments = EncodeRunArguments (instrumentationArguments);
var argumentsArg = encodedArguments == null ? "" : $" -e {DotNetRunArgumentsKey} {encodedArguments}";
var cmdArgs = $"shell am instrument -w{userArg}{argumentsArg} {package}/{instrumentation}";

if (verbose)
Console.WriteLine ($"Running instrumentation: adb {cmdArgs}");
Expand All @@ -228,17 +237,25 @@ async Task<int> RunInstrumentationAsync ()
using var instrumentProcess = new Process { StartInfo = psi };

var locker = new Lock ();
var output = new StringBuilder ();
var error = new StringBuilder ();

instrumentProcess.OutputDataReceived += (s, e) => {
if (e.Data != null)
lock (locker)
if (e.Data != null) {
lock (locker) {
output.AppendLine (e.Data);
Console.WriteLine (e.Data);
}
}
};

instrumentProcess.ErrorDataReceived += (s, e) => {
if (e.Data != null)
lock (locker)
if (e.Data != null) {
lock (locker) {
error.AppendLine (e.Data);
Console.Error.WriteLine (e.Data);
}
}
};

instrumentProcess.Start ();
Expand All @@ -254,8 +271,14 @@ async Task<int> RunInstrumentationAsync ()
try {
try {
await instrumentProcess.WaitForExitAsync (cts.Token);
instrumentProcess.WaitForExit ();
} catch (OperationCanceledException) {
try { instrumentProcess.Kill (); } catch (Exception ex) {
try {
if (!instrumentProcess.HasExited) {
instrumentProcess.Kill ();
instrumentProcess.WaitForExit (1000);
}
} catch (Exception ex) {
if (verbose)
Console.Error.WriteLine ($"Cleanup: {ex.Message}");
}
Expand All @@ -280,9 +303,137 @@ async Task<int> RunInstrumentationAsync ()
return 1;
}

var result = ParseInstrumentationRunOutput (output.ToString (), error.ToString ());
if (!string.IsNullOrEmpty (result.Summary))
Console.WriteLine (result.Summary);
if (!string.IsNullOrEmpty (result.ArtifactsPath)) {
var localArtifactsPath = await PullArtifactsAsync (result.ArtifactsPath);
if (string.IsNullOrEmpty (localArtifactsPath))
return 1;
Console.WriteLine ($"Artifacts: {localArtifactsPath}");
}

if (!result.Succeeded) {
if (!string.IsNullOrEmpty (result.Error))
Console.Error.WriteLine (result.Error);
else if (result.InstrumentationCode.HasValue)
Console.Error.WriteLine ($"Instrumentation failed with code {result.InstrumentationCode.Value}.");
else
Console.Error.WriteLine ("Instrumentation failed.");
return 1;
}

return 0;
}

static string? EncodeRunArguments (List<string> arguments)
{
if (arguments.Count == 0)
return null;

// These are the app arguments forwarded after `dotnet run --`.
// `am instrument -e` accepts a single string value, so preserve argv boundaries
// with NUL separators and base64-encode the payload for adb shell transport.
var joinedArguments = string.Join ('\0', arguments);
return Convert.ToBase64String (Encoding.UTF8.GetBytes (joinedArguments));
}

InstrumentationRunResult ParseInstrumentationRunOutput (string output, string error)
{
var result = new InstrumentationRunResult ();
foreach (var rawLine in (output + "\n" + error).Split ('\n')) {
var line = rawLine.TrimEnd ('\r');
if (line.StartsWith ("INSTRUMENTATION_RESULT: ", StringComparison.Ordinal)) {
var value = line.Substring ("INSTRUMENTATION_RESULT: ".Length);
var separator = value.IndexOf ('=');
if (separator <= 0)
continue;

var key = value.Substring (0, separator).Trim ();
var keyValue = value.Substring (separator + 1).Trim ();
switch (key) {
case "artifactsPath":
result.ArtifactsPath = keyValue;
break;
case "summary":
result.Summary = keyValue;
break;
case "error":
result.Error = keyValue;
break;
}
} else if (line.StartsWith ("INSTRUMENTATION_CODE: ", StringComparison.Ordinal)) {
var code = line.Substring ("INSTRUMENTATION_CODE: ".Length).Trim ();
if (int.TryParse (code, out int instrumentationCode))
result.InstrumentationCode = instrumentationCode;
} else if (line.StartsWith ("INSTRUMENTATION_FAILED", StringComparison.Ordinal)) {
result.InstrumentationFailed = true;
if (string.IsNullOrEmpty (result.Error))
result.Error = line;
}
}

return result;
}

async Task<string?> PullArtifactsAsync (string deviceArtifactsPath)
{
if (!TryGetArtifactsDirectoryName (deviceArtifactsPath, out var artifactsDirectoryName)) {
Console.Error.WriteLine ($"Error: Invalid artifacts path reported by instrumentation: '{deviceArtifactsPath}'.");
return null;
}

var localArtifactsPath = Path.Combine (Environment.CurrentDirectory, artifactsDirectoryName);

if (verbose)
Console.WriteLine ($"Pulling artifacts: {deviceArtifactsPath} -> {localArtifactsPath}");

var (exitCode, output, error) = await AdbHelper.RunAsync (
adbPath,
adbTarget,
$"pull \"{deviceArtifactsPath}\" \"{Environment.CurrentDirectory}\"",
cts.Token,
verbose);
if (exitCode != 0) {
Console.Error.WriteLine ($"Error: Failed to pull artifacts: {error}");
if (verbose && !string.IsNullOrWhiteSpace (output))
Console.Error.WriteLine (output);
return null;
}

return localArtifactsPath;
}

static bool TryGetArtifactsDirectoryName (string deviceArtifactsPath, out string artifactsDirectoryName)
{
artifactsDirectoryName = "";
if (string.IsNullOrEmpty (deviceArtifactsPath) || deviceArtifactsPath [0] != '/')
return false;

foreach (var ch in deviceArtifactsPath) {
if (char.IsControl (ch) || char.IsWhiteSpace (ch)) {
return false;
}
switch (ch) {
case '"':
case '\'':
case ';':
case '&':
case '|':
case '`':
case '$':
case '<':
case '>':
case '\\':
return false;
}
}

var trimmedPath = deviceArtifactsPath.TrimEnd ('/');
artifactsDirectoryName = Path.GetFileName (trimmedPath);
return artifactsDirectoryName != "" && artifactsDirectoryName != "." && artifactsDirectoryName != "..";
}

async Task<int> RunDotnetTestAsync (List<string> mtpArgs)
{
if (verbose)
Expand Down Expand Up @@ -522,3 +673,17 @@ async Task StopAppAsync ()
return (null, null);
}
}

class InstrumentationRunResult
{
public string? ArtifactsPath { get; set; }
public string? Summary { get; set; }
public string? Error { get; set; }
public int? InstrumentationCode { get; set; }
public bool InstrumentationFailed { get; set; }

public bool Succeeded =>
!InstrumentationFailed &&
string.IsNullOrEmpty (Error) &&
(!InstrumentationCode.HasValue || InstrumentationCode.Value == -1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Microsoft",
"classifications": [ "Android", "Mobile", "Benchmark" ],
"identity": "Microsoft.Android.AndroidBenchmarkDotNet",
"name": "Android BenchmarkDotNet Project",
"description": "A project for creating a .NET for Android BenchmarkDotNet project",
"shortName": "android-benchmarkdotnet",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "AndroidBenchmark1",
"preferNameDirectory": true,
"primaryOutputs": [
{ "path": "AndroidBenchmark1.csproj" }
],
"symbols": {
"packageName": {
"type": "parameter",
"description": "Overrides the package name in the AndroidManifest.xml",
"datatype": "string",
"replaces": "com.companyname.AndroidBenchmark1"
},
"supportedOSVersion": {
"type": "parameter",
"description": "Overrides $(SupportedOSPlatformVersion) in the project",
"datatype": "string",
"replaces": "SUPPORTED_OS_PLATFORM_VERSION",
"defaultValue": "24"
}
},
"defaultName": "AndroidBenchmark1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net11.0-android</TargetFramework>
<SupportedOSPlatformVersion>SUPPORTED_OS_PLATFORM_VERSION</SupportedOSPlatformVersion>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">AndroidBenchmark1</RootNamespace>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<DebugType>none</DebugType>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationId>com.companyname.AndroidBenchmark1</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<UseMonoRuntime>false</UseMonoRuntime>
<RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>
<AndroidPackageFormats>apk</AndroidPackageFormats>
<AndroidInstrumentation>com.companyname.AndroidBenchmark1.BenchmarkInstrumentation</AndroidInstrumentation>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>

<ItemGroup>
<!-- BenchmarkDotNet discovers [Benchmark] methods via reflection, and
BenchmarkRunner emits per-benchmark wrapper code that references
many BenchmarkDotNet internals. Root both assemblies so trimming
keeps the metadata BenchmarkDotNet needs at runtime. -->
<TrimmerRootAssembly Include="AndroidBenchmark1" />
<TrimmerRootAssembly Include="BenchmarkDotNet" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="AndroidBenchmark1">
</application>
</manifest>
Loading