Skip to content
Merged
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
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Xamarin.Android.Tasks
Expand Down Expand Up @@ -180,25 +179,6 @@ static string GetInstallErrorCode (InstallResultKind kind)
_ => "ADB0010",
};
}

/// <summary>Quotes an argument for <see cref="System.Diagnostics.ProcessStartInfo.Arguments"/>.</summary>
static string QuoteProcessArgument (string argument)
{
if (argument == null) {
return "\"\"";
}
var sb = new StringBuilder ();
sb.Append ('"');
// The .NET process class only supports quoted arguments with escaped quotes/backslashes.
foreach (char c in argument) {
if (c == '"' || c == '\\') {
sb.Append ('\\');
}
sb.Append (c);
}
sb.Append ('"');
return sb.ToString ();
}
}

/// <summary>
Expand Down
83 changes: 20 additions & 63 deletions src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Android.Build.Tasks;
using Microsoft.Build.Framework;
using Xamarin.Android.Build.Debugging.Tasks.Properties;
using Xamarin.Android.Tools;

namespace Xamarin.Android.Tasks
{
Expand Down Expand Up @@ -732,70 +733,26 @@ async Task<AdbCommandResult> RunAdbCommand (string [] arguments, Dictionary<stri
}
adbArguments.AddRange (arguments);

var stdout = new StringBuilder ();
var stderr = new StringBuilder ();
using var stdoutCompleted = new ManualResetEvent (false);
using var stderrCompleted = new ManualResetEvent (false);
var psi = new ProcessStartInfo {
FileName = adb,
Arguments = string.Join (" ", adbArguments.Select (QuoteProcessArgument)),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
var psi = ProcessUtils.CreateProcessStartInfo (adb, adbArguments.ToArray ());
psi.WindowStyle = ProcessWindowStyle.Hidden;

// psi.Arguments holds the exact, correctly quoted command line whenever ProcessUtils
// joined the arguments itself; it is empty when it used ProcessStartInfo.ArgumentList.
string commandLine = !string.IsNullOrEmpty (psi.Arguments)
? psi.Arguments
: string.Join (" ", adbArguments.Select (a => $"[{a}]"));
LogDiagnostic ($"adb command: {psi.FileName} {commandLine}");

using var stdout = new StringWriter ();
using var stderr = new StringWriter ();
int exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, CancellationToken, environmentVariables);
var result = new AdbCommandResult {
ExitCode = exitCode,
StandardOutput = stdout.ToString ().Trim (),
StandardError = stderr.ToString ().Trim (),
};
if (environmentVariables != null) {
foreach (var kvp in environmentVariables) {
psi.EnvironmentVariables [kvp.Key] = kvp.Value;
}
}

LogDiagnostic ($"adb command: {psi.FileName} {psi.Arguments}");
using (var process = new Process ()) {
process.StartInfo = psi;
process.OutputDataReceived += (sender, e) => {
if (e.Data != null) {
lock (stdout) {
stdout.AppendLine (e.Data);
}
} else {
stdoutCompleted.Set ();
}
};
process.ErrorDataReceived += (sender, e) => {
if (e.Data != null) {
lock (stderr) {
stderr.AppendLine (e.Data);
}
} else {
stderrCompleted.Set ();
}
};

process.Start ();
process.BeginOutputReadLine ();
process.BeginErrorReadLine ();
using (CancellationToken.Register (() => {
try {
if (!process.HasExited) {
process.Kill ();
}
} catch (InvalidOperationException) {
}
})) {
await Task.Run (() => process.WaitForExit (), CancellationToken);
}
stdoutCompleted.WaitOne (TimeSpan.FromSeconds (30));
stderrCompleted.WaitOne (TimeSpan.FromSeconds (30));
var result = new AdbCommandResult {
ExitCode = process.ExitCode,
StandardOutput = stdout.ToString ().Trim (),
StandardError = stderr.ToString ().Trim (),
};
LogAdbCommandResult (result);
return result;
}
LogAdbCommandResult (result);
return result;
}

void LogAdbCommandResult (AdbCommandResult result)
Expand Down
67 changes: 62 additions & 5 deletions src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ internal static void Exec (ProcessStartInfo processStartInfo, DataReceivedEventH
/// <summary>
/// Creates a <see cref="ProcessStartInfo"/> with the given filename and arguments.
/// On .NET 5+ uses <see cref="ProcessStartInfo.ArgumentList"/> to avoid shell-escaping issues;
/// on older frameworks falls back to a single <see cref="ProcessStartInfo.Arguments"/> string.
/// on older frameworks falls back to a single <see cref="ProcessStartInfo.Arguments"/> string
/// built by <see cref="JoinArguments"/>.
/// </summary>
public static ProcessStartInfo CreateProcessStartInfo (string fileName, params string[] args)
{
Expand All @@ -204,18 +205,74 @@ public static ProcessStartInfo CreateProcessStartInfo (string fileName, params s
return psi;
}

#if !NET5_0_OR_GREATER
static string JoinArguments (string[] args)
/// <summary>
/// Joins <paramref name="args"/> into a single command line suitable for
/// <see cref="ProcessStartInfo.Arguments"/>.
/// </summary>
/// <remarks>
/// Implements the quoting rules understood by <c>CommandLineToArgvW</c>, which are also
/// the rules .NET uses when it parses <see cref="ProcessStartInfo.Arguments"/> on Unix.
/// A run of backslashes is only an escape sequence when it is immediately followed by a
/// quote, so backslashes must *not* be doubled unconditionally: doing so turns
/// <c>C:\dir\file.dll</c> into <c>C:\\dir\\file.dll</c>, which some tools (notably
/// <c>adb push</c>) reject.
/// </remarks>
internal static string JoinArguments (params string?[] args)
{
var sb = new StringBuilder ();
for (int i = 0; i < args.Length; i++) {
if (i > 0)
sb.Append (' ');
sb.Append ('"').Append (args [i]).Append ('"');
AppendArgument (sb, args [i]);
}
return sb.ToString ();
}
#endif

static void AppendArgument (StringBuilder sb, string? argument)
{
if (argument is null || argument.Length == 0) {
sb.Append ("\"\"");
return;
}

if (ContainsNoWhitespaceOrQuotes (argument)) {
sb.Append (argument);
return;
}

sb.Append ('"');
for (int i = 0; i < argument.Length; i++) {
int backslashes = 0;
while (i < argument.Length && argument [i] == '\\') {
backslashes++;
i++;
}

if (i == argument.Length) {
// Trailing backslashes precede the closing quote, so they must be doubled
// to avoid escaping it.
sb.Append ('\\', backslashes * 2);
break;
}

if (argument [i] == '"') {
sb.Append ('\\', backslashes * 2 + 1).Append ('"');
} else {
sb.Append ('\\', backslashes).Append (argument [i]);
}
}
sb.Append ('"');
}

static bool ContainsNoWhitespaceOrQuotes (string s)
{
for (int i = 0; i < s.Length; i++) {
char c = s [i];
if (char.IsWhiteSpace (c) || c == '"')
return false;
}
return true;
}

/// <summary>
/// Throws <see cref="InvalidOperationException"/> when <paramref name="exitCode"/> is non-zero.
Expand Down
Loading