Skip to content

Commit

Permalink
Add timeout option for IAdbCommandLineClient
Browse files Browse the repository at this point in the history
  • Loading branch information
wherewhere committed Apr 6, 2024
1 parent 33ab62c commit 97250ed
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 52 deletions.
10 changes: 3 additions & 7 deletions AdvancedSharpAdbClient.Tests/Dummys/DummyAdbCommandLineClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@ namespace AdvancedSharpAdbClient.Tests
/// <summary>
/// A mock implementation of the <see cref="IAdbCommandLineClient"/> class.
/// </summary>
internal class DummyAdbCommandLineClient : AdbCommandLineClient
internal class DummyAdbCommandLineClient() : AdbCommandLineClient(ServerName)
{
public DummyAdbCommandLineClient() : base(ServerName)
{
}

public AdbCommandLineStatus Version { get; set; }

public bool ServerStarted { get; private set; }
Expand All @@ -24,7 +20,7 @@ public DummyAdbCommandLineClient() : base(ServerName)

public override Task<bool> CheckAdbFileExistsAsync(string adbPath, CancellationToken cancellationToken = default) => Task.FromResult(true);

protected override int RunProcess(string filename, string command, ICollection<string> errorOutput, ICollection<string> standardOutput)
protected override int RunProcess(string filename, string command, ICollection<string> errorOutput, ICollection<string> standardOutput, int timeout)
{
if (filename == AdbPath)
{
Expand Down Expand Up @@ -55,7 +51,7 @@ protected override int RunProcess(string filename, string command, ICollection<s
protected override async Task<int> RunProcessAsync(string filename, string command, ICollection<string> errorOutput, ICollection<string> standardOutput, CancellationToken cancellationToken = default)
{
await Task.Yield();
return RunProcess(filename, command, errorOutput, standardOutput);
return RunProcess(filename, command, errorOutput, standardOutput, Timeout.Infinite);
}

private static string ServerName => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "adb.exe" : "adb";
Expand Down
46 changes: 19 additions & 27 deletions AdvancedSharpAdbClient/AdbCommandLineClient.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
using System.Threading;

namespace AdvancedSharpAdbClient
Expand Down Expand Up @@ -93,8 +94,7 @@ public virtual Task<bool> CheckAdbFileExistsAsync(string adbPath, CancellationTo
/// This value can be <see langword="null"/> if you are not interested in the standard output.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task"/> which represents the asynchronous operation.</returns>
/// <remarks>Use this command only for <c>adb</c> commands that return immediately, such as
/// <c>adb version</c>. This operation times out after 5 seconds.</remarks>
/// <remarks>Use this command only for <c>adb</c> commands that return immediately, such as <c>adb version</c>.</remarks>
/// <exception cref="AdbException">The process exited with an exit code other than <c>0</c>.</exception>
protected async Task RunAdbProcessAsync(string command, ICollection<string>? errorOutput, ICollection<string>? standardOutput, CancellationToken cancellationToken = default)
{
Expand All @@ -113,8 +113,7 @@ protected async Task RunAdbProcessAsync(string command, ICollection<string>? err
/// This value can be <see langword="null"/> if you are not interested in the standard output.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{Int32}"/> which returns the return code of the <c>adb</c> process.</returns>
/// <remarks>Use this command only for <c>adb</c> commands that return immediately, such as
/// <c>adb version</c>. This operation times out after 5 seconds.</remarks>
/// <remarks>Use this command only for <c>adb</c> commands that return immediately, such as <c>adb version</c>.</remarks>
protected async Task<int> RunAdbProcessInnerAsync(string command, ICollection<string>? errorOutput, ICollection<string>? standardOutput, CancellationToken cancellationToken = default)
{
ExceptionExtensions.ThrowIfNull(command);
Expand All @@ -124,7 +123,14 @@ protected async Task<int> RunAdbProcessInnerAsync(string command, ICollection<st
/// <summary>
/// Asynchronously runs process, invoking a specific command, and reads the standard output and standard error output.
/// </summary>
/// <returns>The return code of the process.</returns>
/// <param name="filename">The filename of the process to start.</param>
/// <param name="command">The command to invoke, such as <c>version</c> or <c>start-server</c>.</param>
/// <param name="errorOutput">A list in which to store the standard error output. Each line is added as a new entry.
/// This value can be <see langword="null"/> if you are not interested in the standard error.</param>
/// <param name="standardOutput">A list in which to store the standard output. Each line is added as a new entry.
/// This value can be <see langword="null"/> if you are not interested in the standard output.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{Int32}"/> which returns the return code of the process.</returns>
#if !HAS_PROCESS
[DoesNotReturn]
#endif
Expand All @@ -142,33 +148,19 @@ protected virtual async Task<int> RunProcessAsync(string filename, string comman

using Process process = Process.Start(psi) ?? throw new AdbException($"The adb process could not be started. The process returned null when starting {filename} {command}");

#if NET5_0_OR_GREATER
using (CancellationTokenSource completionSource = new(TimeSpan.FromMilliseconds(5000)))
using (CancellationTokenRegistration registration = cancellationToken.Register(process.Kill))
{
try
{
await process.WaitForExitAsync(completionSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (completionSource.IsCancellationRequested)
{
if (!process.HasExited)
{
process.Kill();
}
}
string standardErrorString = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
string standardOutputString = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);

errorOutput?.AddRange(standardErrorString.Split(separator, StringSplitOptions.RemoveEmptyEntries));
standardOutput?.AddRange(standardOutputString.Split(separator, StringSplitOptions.RemoveEmptyEntries));
}
#else
if (!process.WaitForExit(5000))

if (!process.HasExited)
{
process.Kill();
}
#endif

string standardErrorString = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
string standardOutputString = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);

errorOutput?.AddRange(standardErrorString.Split(separator, StringSplitOptions.RemoveEmptyEntries));
standardOutput?.AddRange(standardOutputString.Split(separator, StringSplitOptions.RemoveEmptyEntries));

// get the return code from the process
return process.ExitCode;
Expand Down
38 changes: 25 additions & 13 deletions AdvancedSharpAdbClient/AdbCommandLineClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;

namespace AdvancedSharpAdbClient
{
Expand Down Expand Up @@ -85,9 +86,9 @@ public AdbCommandLineStatus GetVersion()
}

/// <inheritdoc/>
public void StartServer()
public void StartServer(int timeout = Timeout.Infinite)
{
int status = RunAdbProcessInner("start-server", null, null);
int status = RunAdbProcessInner("start-server", null, null, timeout);
if (status == 0) { return; }

// Starting the adb server failed for whatever reason. This can happen if adb.exe
Expand All @@ -97,15 +98,17 @@ public void StartServer()

// Try again. This time, we don't call "Inner", and an exception will be thrown if the start operation fails
// again. We'll let that exception bubble up the stack.
RunAdbProcess("start-server", null, null);
RunAdbProcess("start-server", null, null, timeout);
}

/// <inheritdoc/>
public virtual List<string> ExecuteAdbCommand(string command)
/// <remarks>Use this command only for <c>adb</c> commands that return immediately, such as
/// <c>adb version</c>. This operation times out after 5 seconds.</remarks>
public virtual List<string> ExecuteAdbCommand(string command, int timeout = 5000)
{
List<string> errorOutput = [];
List<string> standardOutput = [];
int status = RunAdbProcessInner(command, errorOutput, standardOutput);
int status = RunAdbProcessInner(command, errorOutput, standardOutput, timeout);
if (errorOutput.Count > 0)
{
string error = StringExtensions.Join(Environment.NewLine, errorOutput!);
Expand Down Expand Up @@ -170,12 +173,13 @@ protected virtual void EnsureIsValidAdbFile(string adbPath)
/// This value can be <see langword="null"/> if you are not interested in the standard error.</param>
/// <param name="standardOutput">A list in which to store the standard output. Each line is added as a new entry.
/// This value can be <see langword="null"/> if you are not interested in the standard output.</param>
/// <param name="timeout">The timeout in milliseconds to wait for the <c>adb</c> process to exit.</param>
/// <remarks>Use this command only for <c>adb</c> commands that return immediately, such as
/// <c>adb version</c>. This operation times out after 5 seconds.</remarks>
/// <c>adb version</c>. This operation times out after 5 seconds in default.</remarks>
/// <exception cref="AdbException">The process exited with an exit code other than <c>0</c>.</exception>
protected void RunAdbProcess(string command, ICollection<string>? errorOutput, ICollection<string>? standardOutput)
protected void RunAdbProcess(string command, ICollection<string>? errorOutput, ICollection<string>? standardOutput, int timeout = 5000)
{
int status = RunAdbProcessInner(command, errorOutput, standardOutput);
int status = RunAdbProcessInner(command, errorOutput, standardOutput, timeout);
if (status != 0) { throw new AdbException($"The adb process returned error code {status} when running command {command}"); }
}

Expand All @@ -188,13 +192,14 @@ protected void RunAdbProcess(string command, ICollection<string>? errorOutput, I
/// This value can be <see langword="null"/> if you are not interested in the standard error.</param>
/// <param name="standardOutput">A list in which to store the standard output. Each line is added as a new entry.
/// This value can be <see langword="null"/> if you are not interested in the standard output.</param>
/// <param name="timeout">The timeout in milliseconds to wait for the <c>adb</c> process to exit.</param>
/// <returns>The return code of the <c>adb</c> process.</returns>
/// <remarks>Use this command only for <c>adb</c> commands that return immediately, such as
/// <c>adb version</c>. This operation times out after 5 seconds.</remarks>
protected int RunAdbProcessInner(string command, ICollection<string>? errorOutput, ICollection<string>? standardOutput)
/// <c>adb version</c>. This operation times out after 5 seconds in default.</remarks>
protected int RunAdbProcessInner(string command, ICollection<string>? errorOutput, ICollection<string>? standardOutput, int timeout = 5000)
{
ExceptionExtensions.ThrowIfNull(command);
return RunProcess(AdbPath, command, errorOutput, standardOutput);
return RunProcess(AdbPath, command, errorOutput, standardOutput, timeout);
}

/// <summary>
Expand Down Expand Up @@ -235,11 +240,18 @@ protected virtual void KillProcess(string processName)
/// <summary>
/// Runs process, invoking a specific command, and reads the standard output and standard error output.
/// </summary>
/// <param name="filename">The filename of the process to start.</param>
/// <param name="command">The command to invoke, such as <c>version</c> or <c>start-server</c>.</param>
/// <param name="errorOutput">A list in which to store the standard error output. Each line is added as a new entry.
/// This value can be <see langword="null"/> if you are not interested in the standard error.</param>
/// <param name="standardOutput">A list in which to store the standard output. Each line is added as a new entry.
/// This value can be <see langword="null"/> if you are not interested in the standard output.</param>
/// <param name="timeout">The timeout in milliseconds to wait for the process to exit.</param>
/// <returns>The return code of the process.</returns>
#if !HAS_PROCESS
[DoesNotReturn]
#endif
protected virtual int RunProcess(string filename, string command, ICollection<string>? errorOutput, ICollection<string>? standardOutput)
protected virtual int RunProcess(string filename, string command, ICollection<string>? errorOutput, ICollection<string>? standardOutput, int timeout)
{
#if HAS_PROCESS
ProcessStartInfo psi = new(filename, command)
Expand All @@ -254,7 +266,7 @@ protected virtual int RunProcess(string filename, string command, ICollection<st
using Process process = Process.Start(psi) ?? throw new AdbException($"The adb process could not be started. The process returned null when starting {filename} {command}");

// get the return code from the process
if (!process.WaitForExit(5000))
if (!process.WaitForExit(timeout))
{
process.Kill();
}
Expand Down
5 changes: 3 additions & 2 deletions AdvancedSharpAdbClient/AdbServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace AdvancedSharpAdbClient
{
Expand Down Expand Up @@ -199,7 +200,7 @@ public StartServerResult StartServer(string adbPath, bool restartServerIfNewer =
|| (restartServerIfNewer && serverStatus.Version < commandLineVersion))
{
StopServer();
commandLineClient.StartServer();
commandLineClient.StartServer(Timeout.Infinite);
return StartServerResult.RestartedOutdatedDaemon;
}
else
Expand All @@ -209,7 +210,7 @@ public StartServerResult StartServer(string adbPath, bool restartServerIfNewer =
}
else
{
commandLineClient.StartServer();
commandLineClient.StartServer(Timeout.Infinite);
return StartServerResult.Started;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public DeviceNotFoundException() : base("The device was not found.")
/// Initializes a new instance of the <see cref="DeviceNotFoundException"/> class.
/// </summary>
/// <param name="device">The device.</param>
public DeviceNotFoundException(string? device) : base("The device '" + device + "' was not found.")
public DeviceNotFoundException(string? device) : base($"The device '{device}' was not found.")
{
}

Expand Down
6 changes: 4 additions & 2 deletions AdvancedSharpAdbClient/Interfaces/IAdbCommandLineClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ public partial interface IAdbCommandLineClient
/// <summary>
/// Starts the adb server by running the <c>adb start-server</c> command.
/// </summary>
void StartServer();
/// <param name="timeout">The timeout in milliseconds to wait for the <c>adb</c> process to exit.</param>
void StartServer(int timeout);

/// <summary>
/// Runs the <c>adb.exe</c> process, invoking a specific <paramref name="command"/>, and reads the standard output.
/// </summary>
/// <param name="command">The <c>adb.exe</c> command to invoke, such as <c>version</c> or <c>start-server</c>.</param>
/// <param name="timeout">The timeout in milliseconds to wait for the <c>adb</c> process to exit.</param>
/// <return>A list in which to store the standard output. Each line is added as a new entry.</return>
List<string> ExecuteAdbCommand(string command);
List<string> ExecuteAdbCommand(string command, int timeout);

/// <summary>
/// Determines whether the <c>adb.exe</c> file exists.
Expand Down

0 comments on commit 97250ed

Please sign in to comment.