This commit is contained in:
Matthew Leibowitz 2020-09-29 01:24:00 +02:00
Родитель 8ec538e76c
Коммит 72a3039011
11 изменённых файлов: 352 добавлений и 170 удалений

Просмотреть файл

@ -60,17 +60,22 @@ namespace DotNetDevices.Android
{
logger?.LogInformation("Retrieving all the virtual devices...");
return await GetVirtualDeviceNoLogging(cancellationToken).ConfigureAwait(false);
var args = $"list avd -c";
var result = await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
var avd = new List<VirtualDevice>(result.OutputCount);
foreach (var output in GetListResults(result))
{
avd.Add(new VirtualDevice(output));
}
return avd;
}
public async Task DeleteVirtualDeviceAsync(string name, CancellationToken cancellationToken = default)
{
logger?.LogInformation($"Deleting virtual device '{name}'...");
var avds = await GetVirtualDeviceNoLogging(cancellationToken);
if (!avds.Any(x => x.Name.ToLowerInvariant() == name.ToLowerInvariant()))
throw new Exception($"Unable to find virtual device '{name}'.");
var args = $"delete avd --name \"{name}\"";
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
@ -80,13 +85,6 @@ namespace DotNetDevices.Android
{
logger?.LogInformation($"Creating virtual device '{name}'...");
if (options?.Overwrite != true)
{
var avds = await GetVirtualDeviceNoLogging(cancellationToken);
if (avds.Any(x => x.Name.ToLowerInvariant() == name.ToLowerInvariant()))
throw new Exception($"Virtual device '{name}' already exists.");
}
var args = $"create avd --name \"{name}\" --package \"{package}\"";
if (options?.Overwrite == true)
args += " --force";
@ -94,6 +92,24 @@ namespace DotNetDevices.Android
await processRunner.RunWithInputAsync("no", avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
public async Task RenameVirtualDeviceAsync(string name, string newName, CancellationToken cancellationToken = default)
{
logger?.LogInformation($"Renaming virtual device '{name}'...");
var args = $"move avd --name \"{name}\" --rename \"{newName}\"";
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
public async Task MoveVirtualDeviceAsync(string name, string newPath, CancellationToken cancellationToken = default)
{
logger?.LogInformation($"Moving virtual device '{name}'...");
var args = $"move avd --name \"{name}\" --path \"{newPath}\"";
await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
}
private IEnumerable<string> GetListResults(ProcessResult result)
{
foreach (var output in result.GetOutput())
@ -118,22 +134,6 @@ namespace DotNetDevices.Android
yield return o;
}
}
private async Task<IEnumerable<VirtualDevice>> GetVirtualDeviceNoLogging(CancellationToken cancellationToken = default)
{
logger?.LogDebug("Retrieving all the virtual devices...");
var args = $"list avd -c";
var result = await processRunner.RunAsync(avdmanager, args, null, cancellationToken).ConfigureAwait(false);
var avd = new List<VirtualDevice>(result.OutputCount);
foreach (var output in GetListResults(result))
{
avd.Add(new VirtualDevice(output));
}
return avd;
}
}
public class VirtualDeviceCreateOptions

Просмотреть файл

@ -0,0 +1,11 @@
namespace DotNetDevices.Android
{
public class BootVirtualDeviceOptions
{
public bool NoWindow { get; set; }
public bool NoSnapshots { get; set; }
public bool WipeData { get; set; }
}
}

Просмотреть файл

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DotNetDevices.Processes;
using Microsoft.Extensions.Logging;
namespace DotNetDevices.Android
{
public class EmulatorManager
{
private static readonly Regex consoleListeningRegex = new Regex(@"emulator: control console listening on port (\d+), ADB on port (\d+)");
private static readonly Regex adbConnectedRegex = new Regex(@"emulator: onGuestSendCommand: \[(.+)\] Adb connected, start proxing data");
private static readonly Regex alreadyBootedRegex = new Regex(@"emulator: ERROR: Running multiple emulators with the same AVD is an experimental feature\.");
private readonly ProcessRunner processRunner;
private readonly ILogger? logger;
private readonly string emulator;
public EmulatorManager(string? sdkRoot = null, ILogger? logger = null)
{
this.logger = logger;
processRunner = new ProcessRunner(logger);
emulator = AndroidSDK.FindPath(sdkRoot, Path.Combine("emulator", "emulator"), logger)
?? throw new ArgumentException($"Unable to locate the Android Emulator. Make sure that ANDROID_HOME or ANDROID_SDK_ROOT is set.");
}
public async Task<int> BootVirtualDeviceAsync(string name, BootVirtualDeviceOptions? options = null, CancellationToken cancellationToken = default)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
logger?.LogInformation($"Booting virtual device '{name}'...");
var args = $"-avd {name} -verbose";
if (options?.NoWindow == true)
args += " -no-boot-anim -no-window";
if (options?.NoSnapshots == true)
args += " -no-snapshot";
if (options?.WipeData == true)
args += " -wipe-data";
var port = -1;
try
{
await processRunner.RunAsync(emulator, args, FindComplete, cancellationToken).ConfigureAwait(false);
}
catch (ProcessResultException ex) when (IsAlreadyLaunched(ex))
{
// no-op
}
return port;
bool FindComplete(ProcessOutput output)
{
if (!output.IsError && output.Data is string o)
{
if (port <= 0)
{
// first find port
var match = consoleListeningRegex.Match(o);
if (match.Success)
port = int.Parse(match.Groups[1].Value);
}
else
{
// then wait for the boot finished
var match = adbConnectedRegex.Match(o);
if (match.Success)
return false;
}
}
return true;
}
bool IsAlreadyLaunched(ProcessResultException ex)
{
foreach (var output in ex.ProcessResult.GetOutput())
{
var match = alreadyBootedRegex.Match(output);
if (match.Success)
return true;
}
return false;
}
}
public async Task<IEnumerable<VirtualDevice>> GetVirtualDevicesAsync(CancellationToken cancellationToken = default)
{
logger?.LogInformation("Retrieving all the virtual devices...");
var args = $"-list-avds";
var result = await processRunner.RunAsync(emulator, args, null, cancellationToken).ConfigureAwait(false);
var avd = new List<VirtualDevice>(result.OutputCount);
foreach (var output in result.GetOutput())
{
avd.Add(new VirtualDevice(output));
}
return avd;
}
}
}

Просмотреть файл

@ -3,7 +3,7 @@ using DotNetDevices.Processes;
namespace DotNetDevices.Apple
{
public class SimulatorLaunchOptions
public class LaunchAppOptions
{
public bool CaptureOutput { get; set; } = false;

Просмотреть файл

@ -2,11 +2,11 @@
namespace DotNetDevices.Apple
{
public class LaunchedSimulator
public class LaunchAppResult
{
private ProcessResult result;
public LaunchedSimulator(ProcessResult result)
public LaunchAppResult(ProcessResult result)
{
this.result = result;
}

Просмотреть файл

@ -223,7 +223,7 @@ namespace DotNetDevices.Apple
await processRunner.RunAsync(xcrun, args, null, cancellationToken).ConfigureAwait(false);
}
public async Task<LaunchedSimulator> LaunchAppAsync(string udid, string appBundleId, SimulatorLaunchOptions? options = null, CancellationToken cancellationToken = default)
public async Task<LaunchAppResult> LaunchAppAsync(string udid, string appBundleId, LaunchAppOptions? options = null, CancellationToken cancellationToken = default)
{
if (udid == null)
throw new ArgumentNullException(nameof(udid));
@ -247,13 +247,25 @@ namespace DotNetDevices.Apple
try
{
var result = await processRunner.RunAsync(xcrun, args, options?.HandleOutput, cancellationToken).ConfigureAwait(false);
return new LaunchedSimulator(result);
var result = await processRunner.RunAsync(xcrun, args, Wrap(options?.HandleOutput), cancellationToken).ConfigureAwait(false);
return new LaunchAppResult(result);
}
catch (ProcessResultException ex) when (ex.InnerException is OperationCanceledException && ex.ProcessResult != null)
{
await TerminateAppAsync(udid, appBundleId, cancellationToken);
return new LaunchedSimulator(ex.ProcessResult);
return new LaunchAppResult(ex.ProcessResult);
}
static Func<ProcessOutput, bool>? Wrap(Action<ProcessOutput>? handle)
{
if (handle == null)
return null;
return o =>
{
handle(o);
return true;
};
}
}

Просмотреть файл

@ -19,9 +19,9 @@ namespace DotNetDevices.Commands
{
public static Command Create()
{
return new Command("android", "Work with Android emulators.")
return new Command("android", "Work with Android virtual devices.")
{
new Command("list", "List the emulators.")
new Command("list", "List the virtual devices.")
{
new Option<string?>(new[] { "--sdk" }, "Whether or not to only include the available simulators."),
new Option(new[] { "--available" }, "Whether or not to only include the available simulators."),
@ -33,27 +33,21 @@ namespace DotNetDevices.Commands
new Argument<string?>("TERM", "The search term to use when filtering simulators. This could be any number of properties (UDID, runtime, version, availability, or state) as well as part of the simulator name.")
{ Arity = ArgumentArity.ZeroOrOne },
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleListAsync))!)),
new Command("create", "Create a new virtual device.")
{
new Option<string?>(new[] { "--sdk" }, "The path to the Android SDK directory."),
new Option(new[] { "--replace" }, "Replace any existing virtual devices with the same name."),
CommandLine.CreateVerbosity(),
new Argument<string?>("NAME", "The name of the new virtual device."),
new Argument<string?>("PACKAGE", "The package to use for the new virtual device."),
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleCreateAsync))!)),
new Command("boot", "Boot a particular simulator.")
{
new Option<string?>(new[] { "--sdk" }, "Whether or not to only include the available simulators."),
CommandLine.CreateVerbosity(),
new Argument<string?>("UDID", ParseUdid)
{
Description = "The UDID of the simulator to boot.",
Arity = ArgumentArity.ExactlyOne
},
new Argument<string?>("NAME", "The UDID of the simulator to boot."),
}.WithHandler(CommandHandler.Create(typeof(AndroidCommand).GetMethod(nameof(HandleBootAsync))!)),
};
static string? ParseUdid(ArgumentResult result)
{
var udid = result.Tokens[0].Value;
if (Guid.TryParse(udid, out _))
return udid;
result.ErrorMessage = "The UDID must be a valid UDID.";
return null;
}
}
public static async Task HandleListAsync(
@ -94,11 +88,20 @@ namespace DotNetDevices.Commands
}
catch { }
try
{
await avdmanager.DeleteVirtualDeviceAsync("TESTED");
}
catch { }
await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64");
await avdmanager.CreateVirtualDeviceAsync("TESTING", "system-images;android-28;google_apis_playstore;x86_64", new VirtualDeviceCreateOptions { Overwrite = true });
await avdmanager.DeleteVirtualDeviceAsync("TESTING");
await avdmanager.RenameVirtualDeviceAsync("TESTING", "TESTED");
await avdmanager.MoveVirtualDeviceAsync("TESTED", "/Users/matthew/.android/avd/tested.avd");
//await avdmanager.DeleteVirtualDeviceAsync("TESTING");
//term = term?.ToLowerInvariant()?.Trim();
@ -155,27 +158,55 @@ namespace DotNetDevices.Commands
//console.Append(new StackLayoutView { table });
}
public static async Task<int> HandleBootAsync(
string udid,
public static async Task HandleCreateAsync(
string name,
string package,
bool replace = false,
string? sdk = null,
string? verbosity = null,
IConsole console = null!,
CancellationToken cancellationToken = default)
{
var logger = console.CreateLogger(verbosity);
var simctl = new SimulatorControl(logger);
var simulator = await simctl.GetSimulatorAsync(udid, cancellationToken);
var avdmanager = new AVDManager(sdk, logger);
var options = new VirtualDeviceCreateOptions
{
Overwrite = replace
};
await avdmanager.CreateVirtualDeviceAsync(name, package, options, cancellationToken);
}
if (simulator == null)
public static async Task<int> HandleBootAsync(
string name,
string? sdk = null,
string? verbosity = null,
IConsole console = null!,
CancellationToken cancellationToken = default)
{
var logger = console.CreateLogger(verbosity);
var emulator = new EmulatorManager(sdk, logger);
var avds = await emulator.GetVirtualDevicesAsync(cancellationToken);
if (avds.All(a => !a.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
logger.LogError($"No simulator with UDID {udid} was found.");
logger.LogError($"No virtual device with name {name} was found.");
return 1;
}
if (simulator.State == SimulatorState.Booted)
logger.LogInformation($"Simulator was already booted.");
var options = new BootVirtualDeviceOptions
{
NoSnapshots = false,
WipeData = true,
};
var port = await emulator.BootVirtualDeviceAsync(name, options, cancellationToken);
if (port == -1)
logger.LogInformation($"Virtual device was already booted.");
else
await simctl.BootSimulatorAsync(udid, cancellationToken);
logger.LogInformation($"device was booted to port {port}.");
return 0;
}

Просмотреть файл

@ -64,7 +64,7 @@ namespace DotNetDevices.Commands
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var launched = await simctl.LaunchAppAsync(simulator.Udid, bundleId, new SimulatorLaunchOptions
var launched = await simctl.LaunchAppAsync(simulator.Udid, bundleId, new LaunchAppOptions
{
CaptureOutput = true,
BootSimulator = true,

Просмотреть файл

@ -11,7 +11,7 @@ namespace DotNetDevices.Processes
private const int ThreadStillRunningExitCode = 259;
private const int ThreadStillRunningRetry = 3;
public static async Task<ProcessResult> RunAsync(this ProcessStartInfo processStartInfo, string? input = null, Action<ProcessOutput>? handleOutput = null, CancellationToken cancellationToken = default)
public static async Task<ProcessResult> RunAsync(this ProcessStartInfo processStartInfo, string? input = null, Func<ProcessOutput, bool>? handleOutput = null, CancellationToken cancellationToken = default)
{
// override some info in order to capture the output
processStartInfo.UseShellExecute = false;
@ -73,7 +73,7 @@ namespace DotNetDevices.Processes
// if the process is still exiting, give it a little more time
for (var retries = 0; retries < ThreadStillRunningRetry && process.ExitCode == ThreadStillRunningExitCode; retries++)
{
await Task.Delay(200);
await Task.Delay(200).ConfigureAwait(false);
}
// if it takes too long, just pretend it exited completely
@ -81,6 +81,109 @@ namespace DotNetDevices.Processes
if (exitCode == ThreadStillRunningExitCode)
exitCode = 0;
await FinalizeTask(exitCode).ConfigureAwait(false);
}
async void HandleOutputData(object? sender, DataReceivedEventArgs? e)
{
if (e?.Data == null)
{
outputTcs.TrySetResult(true);
return;
}
var o = new ProcessOutput(e.Data, stopwatch.ElapsedMilliseconds);
if (handleOutput != null)
{
try
{
if (!handleOutput.Invoke(o))
await Detatch();
}
catch (OperationCanceledException)
{
outputTcs.TrySetCanceled();
Terminate();
return;
}
catch (Exception ex)
{
outputTcs.TrySetException(ex);
Terminate();
return;
}
}
output.Enqueue(o);
}
async void HandleErrorData(object? sender, DataReceivedEventArgs? e)
{
if (e?.Data == null)
{
errorsTcs.TrySetResult(true);
return;
}
var o = new ProcessOutput(e.Data, stopwatch.ElapsedMilliseconds, true);
if (handleOutput != null)
{
try
{
if (!handleOutput.Invoke(o))
await Detatch();
}
catch (OperationCanceledException)
{
errorsTcs.TrySetCanceled();
Terminate();
return;
}
catch (Exception ex)
{
errorsTcs.TrySetException(ex);
Terminate();
return;
}
}
output.Enqueue(o);
}
void Terminate(bool cancel = false)
{
if (cancel)
tcs?.TrySetCanceled();
if (process != null)
{
try
{
if (!process.HasExited)
process.Kill();
}
catch (InvalidOperationException)
{
}
}
}
Task Detatch()
{
process.Exited -= HandleExited;
process.OutputDataReceived -= HandleOutputData;
process.ErrorDataReceived -= HandleErrorData;
HandleErrorData(null, null);
HandleOutputData(null, null);
return FinalizeTask(0);
}
async Task FinalizeTask(int exitCode)
{
try
{
startTime = process.StartTime;
@ -102,93 +205,9 @@ namespace DotNetDevices.Processes
{
var result = new ProcessResult(output.ToArray(), exitCode, startTime, stopwatch.ElapsedMilliseconds);
tcs.TrySetException(new ProcessResultException($"The process threw an exception: {ex.Message}", ex, result));
tcs.TrySetException(new ProcessResultException(result, $"The process threw an exception: {ex.Message}", ex));
}
}
void Terminate(bool cancel = false)
{
if (cancel)
tcs?.TrySetCanceled();
if (process != null)
{
try
{
if (!process.HasExited)
process.Kill();
}
catch (InvalidOperationException)
{
}
}
}
void HandleOutputData(object? sender, DataReceivedEventArgs e)
{
if (e.Data == null)
{
outputTcs.TrySetResult(true);
return;
}
var o = new ProcessOutput(e.Data, stopwatch.ElapsedMilliseconds);
if (handleOutput != null)
{
try
{
handleOutput.Invoke(o);
}
catch (OperationCanceledException)
{
outputTcs.TrySetCanceled();
Terminate();
return;
}
catch (Exception ex)
{
outputTcs.TrySetException(ex);
Terminate();
return;
}
}
output.Enqueue(o);
}
void HandleErrorData(object? sender, DataReceivedEventArgs e)
{
if (e.Data == null)
{
errorsTcs.TrySetResult(true);
return;
}
var o = new ProcessOutput(e.Data, stopwatch.ElapsedMilliseconds, true);
if (handleOutput != null)
{
try
{
handleOutput.Invoke(o);
}
catch (OperationCanceledException)
{
errorsTcs.TrySetCanceled();
Terminate();
return;
}
catch (Exception ex)
{
errorsTcs.TrySetException(ex);
Terminate();
return;
}
}
output.Enqueue(o);
}
}
}
}

Просмотреть файл

@ -4,23 +4,23 @@ namespace DotNetDevices.Processes
{
public class ProcessResultException : Exception
{
public ProcessResultException(ProcessResult? processResult = null)
public ProcessResultException(ProcessResult processResult)
{
ProcessResult = processResult;
}
public ProcessResultException(string? message, ProcessResult? processResult = null)
public ProcessResultException(ProcessResult processResult, string? message)
: base(message)
{
ProcessResult = processResult;
}
public ProcessResultException(string? message, Exception? innerException, ProcessResult? processResult = null)
public ProcessResultException(ProcessResult processResult, string? message, Exception? innerException)
: base(message, innerException)
{
ProcessResult = processResult;
}
public ProcessResult? ProcessResult { get; }
public ProcessResult ProcessResult { get; }
}
}

Просмотреть файл

@ -17,30 +17,30 @@ namespace DotNetDevices.Processes
this.logger = logger;
}
public async Task<ProcessResult> RunAsync(string path, string? arguments = null, Action<ProcessOutput>? handleOutput = null, CancellationToken cancellationToken = default)
public async Task<ProcessResult> RunAsync(string path, string? arguments = null, Func<ProcessOutput, bool>? handleOutput = null, CancellationToken cancellationToken = default)
{
var output = await RunProcessAsync(FindCommand(path), arguments, null, handleOutput, cancellationToken);
var result = await RunProcessAsync(FindCommand(path), arguments, null, handleOutput, cancellationToken);
if (output.ExitCode != 0)
throw new Exception($"Failed to execute: {path} {arguments} - exit code: {output.ExitCode}{Environment.NewLine}{output.Output}");
if (result.ExitCode != 0)
throw new ProcessResultException(result, $"Failed to execute: {path} {arguments} - exit code: {result.ExitCode}{Environment.NewLine}{result.Output}");
logger?.LogDebug(output.ToString());
logger?.LogTrace(output.Output);
logger?.LogDebug(result.ToString());
logger?.LogTrace(result.Output);
return output;
return result;
}
public async Task<ProcessResult> RunWithInputAsync(string input, string path, string? arguments = null, Action<ProcessOutput>? handleOutput = null, CancellationToken cancellationToken = default)
public async Task<ProcessResult> RunWithInputAsync(string input, string path, string? arguments = null, Func<ProcessOutput, bool>? handleOutput = null, CancellationToken cancellationToken = default)
{
var output = await RunProcessAsync(FindCommand(path), arguments, input, handleOutput, cancellationToken);
var result = await RunProcessAsync(FindCommand(path), arguments, input, handleOutput, cancellationToken);
if (output.ExitCode != 0)
throw new Exception($"Failed to execute: {path} {arguments} - exit code: {output.ExitCode}{Environment.NewLine}{output.Output}");
if (result.ExitCode != 0)
throw new ProcessResultException(result, $"Failed to execute: {path} {arguments} - exit code: {result.ExitCode}{Environment.NewLine}{result.Output}");
logger?.LogDebug(output.ToString());
logger?.LogTrace(output.Output);
logger?.LogDebug(result.ToString());
logger?.LogTrace(result.Output);
return output;
return result;
}
private string FindCommand(string path, bool allowOSFallback = true)
@ -61,7 +61,7 @@ namespace DotNetDevices.Processes
return path;
}
private Task<ProcessResult> RunProcessAsync(string path, string? arguments = null, string? input = null, Action<ProcessOutput>? handleOutput = null, CancellationToken cancellationToken = default)
private Task<ProcessResult> RunProcessAsync(string path, string? arguments = null, string? input = null, Func<ProcessOutput, bool>? handleOutput = null, CancellationToken cancellationToken = default)
{
var psi = new ProcessStartInfo
{