Skip to content
Open
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
110 changes: 110 additions & 0 deletions src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Xamarin.Android.Tools
{
/// <summary>
/// Runs Android Emulator commands.
/// </summary>
public class EmulatorRunner
{
readonly Func<string?> getSdkPath;
Comment on lines +14 to +18
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description/New API lists StartEmulator/StartEmulatorAsync, WaitForBootAsync, and ListRunningEmulatorsAsync, but this class currently only implements StartAvd and ListAvdNamesAsync (and the signature differs). Either update the implementation to match the proposed API surface or adjust the PR description/issue link expectations so consumers aren’t broken/misled.

Copilot uses AI. Check for mistakes.
readonly Func<string?>? getJdkPath;

public EmulatorRunner (Func<string?> getSdkPath)
: this (getSdkPath, null)
{
}

public EmulatorRunner (Func<string?> getSdkPath, Func<string?>? getJdkPath)
{
this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath));
this.getJdkPath = getJdkPath;
}

public string? EmulatorPath {
get {
var sdkPath = getSdkPath ();
if (string.IsNullOrEmpty (sdkPath))
return null;

var ext = OS.IsWindows ? ".exe" : "";
var path = Path.Combine (sdkPath, "emulator", "emulator" + ext);

return File.Exists (path) ? path : null;
}
}

public bool IsAvailable => EmulatorPath is not null;

void ConfigureEnvironment (ProcessStartInfo psi)
{
var sdkPath = getSdkPath ();
if (!string.IsNullOrEmpty (sdkPath))
psi.EnvironmentVariables ["ANDROID_HOME"] = sdkPath;

var jdkPath = getJdkPath?.Invoke ();
if (!string.IsNullOrEmpty (jdkPath))
psi.EnvironmentVariables ["JAVA_HOME"] = jdkPath;
}

public Process StartAvd (string avdName, bool coldBoot = false, string? additionalArgs = null)
{
if (!IsAvailable)
throw new InvalidOperationException ("Android Emulator not found.");

var args = $"-avd \"{avdName}\"";
if (coldBoot)
args += " -no-snapshot-load";
if (!string.IsNullOrEmpty (additionalArgs))
args += " " + additionalArgs;
Comment on lines +63 to +67
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StartAvd builds a single argument string by interpolating avdName and appending additionalArgs verbatim. If avdName contains quotes/whitespace (or additionalArgs comes from untrusted input), this can break parsing or allow argument injection. Consider validating/escaping avdName (e.g., reject quotes) and taking additional args as a structured list (e.g., IEnumerable) with proper quoting/escaping when constructing Arguments.

Copilot uses AI. Check for mistakes.

var psi = new ProcessStartInfo {
FileName = EmulatorPath!,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true
};
ConfigureEnvironment (psi);

var process = new Process { StartInfo = psi };
process.Start ();

return process;
}

public async Task<List<string>> ListAvdNamesAsync (CancellationToken cancellationToken = default)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public API returns Task<List>, which exposes a mutable concrete collection. Consider returning Task<IReadOnlyList> (or IReadOnlyList) to avoid leaking mutability and to align with the API shape described in the PR metadata.

Suggested change
public async Task<List<string>> ListAvdNamesAsync (CancellationToken cancellationToken = default)
public async Task<IReadOnlyList<string>> ListAvdNamesAsync (CancellationToken cancellationToken = default)

Copilot uses AI. Check for mistakes.
{
if (!IsAvailable)
throw new InvalidOperationException ("Android Emulator not found.");

var stdout = new StringWriter ();
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ListAvdNamesAsync allocates a StringWriter for stdout but never disposes it. Use a using/using var to dispose the writer (and to keep the pattern consistent with other code that disposes disposable resources).

Suggested change
var stdout = new StringWriter ();
using var stdout = new StringWriter ();

Copilot uses AI. Check for mistakes.
var psi = new ProcessStartInfo {
FileName = EmulatorPath!,
Arguments = "-list-avds",
UseShellExecute = false,
CreateNoWindow = true
};
ConfigureEnvironment (psi);

await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false);

var avds = new List<string> ();
foreach (var line in stdout.ToString ().Split ('\n')) {
var trimmed = line.Trim ();
if (!string.IsNullOrEmpty (trimmed))
avds.Add (trimmed);
}

return avds;
}
}
}