Files

738 lines
32 KiB
C#

// <copyright file="CommandLine.cs" company="Google Inc.">
// Copyright (C) 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
namespace Google.Android.AppBundle.Editor.Internal.PlayServices
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
public static class CommandLine
{
// Constants used for StreamData's handle field.
public const int StandardOutputStreamDataHandle = 0;
public const int StandardErrorStreamDataHandle = 1;
/// <summary>
/// Result from Run().
/// </summary>
public class Result
{
/// String containing the standard output stream of the tool.
public string stdout;
/// String containing the standard error stream of the tool.
public string stderr;
/// Exit code returned by the tool when execution is complete.
public int exitCode;
/// String that can be used in an error message.
/// This contains:
/// * The command executed.
/// * Standard output string.
/// * Standard error string.
/// * Exit code.
public string message;
};
/// <summary>
/// Called when a RunAsync() completes.
/// </summary>
public delegate void CompletionHandler(Result result);
/// <summary>
/// Text and byte representations of an array of data.
/// </summary>
public class StreamData
{
/// <summary>
/// Handle to the stream this was read from.
/// e.g 0 for stdout, 1 for stderr.
/// </summary>
public int handle = 0;
/// <summary>
/// Text representation of "data".
/// </summary>
public string text = "";
/// <summary>
/// Array of bytes or "null" if no data is present.
/// </summary>
public byte[] data = null;
/// <summary>
/// Whether this marks the end of the stream.
/// </summary>
public bool end;
/// <summary>
/// Initialize this instance.
/// </summary>
/// <param name="handle">Stream identifier.</param>
/// <param name="text">String</param>
/// <param name="data">Bytes</param>
/// <param name="end">Whether this is the end of the stream.</param>
public StreamData(int handle, string text, byte[] data, bool end)
{
this.handle = handle;
this.text = text;
this.data = data;
this.end = end;
}
/// <summary>
/// Get an empty StreamData instance.
/// </summary>
public static StreamData Empty
{
get { return new StreamData(0, "", null, false); }
}
}
/// <summary>
/// Called when data is received from either the standard output or standard error
/// streams with a reference to the current standard input stream to enable simulated
/// interactive input.
/// </summary>
/// <param name="process">Executing process.</param>
/// <param name="stdin">Standard input stream.</param>
/// <param name="stream">Data read from the standard output or error streams.</param>
public delegate void IOHandler(Process process, StreamWriter stdin, StreamData streamData);
/// <summary>
/// Asynchronously execute a command line tool, calling the specified delegate on
/// completion.
/// </summary>
/// <param name="toolPath">Tool to execute.</param>
/// <param name="arguments">String to pass to the tools' command line.</param>
/// <param name="completionDelegate">Called when the tool completes.</param>
/// <param name="workingDirectory">Directory to execute the tool from.</param>
/// <param name="envVars">Additional environment variables to set for the command.</param>
/// <param name="ioHandler">Allows a caller to provide interactive input and also handle
/// both output and error streams from a single delegate.</param>
public static void RunAsync(
string toolPath, string arguments, CompletionHandler completionDelegate,
string workingDirectory = null,
Dictionary<string, string> envVars = null,
IOHandler ioHandler = null)
{
Thread thread = new Thread(new ThreadStart(() => {
Result result = Run(toolPath, arguments, workingDirectory, envVars: envVars,
ioHandler: ioHandler);
completionDelegate(result);
}));
thread.Start();
}
/// <summary>
/// Asynchronously reads binary data from a stream using a configurable buffer.
/// </summary>
private class AsyncStreamReader
{
/// <summary>
/// Delegate called when data is read from the stream.
/// <param name="streamData">Data read from the stream.</param>
/// </summary>
public delegate void Handler(StreamData streamData);
/// <summary>
/// Event which is signalled when data is received.
/// </summary>
public event Handler DataReceived;
// Signalled when a read completes.
private AutoResetEvent readEvent = null;
// Handle to the stream.
private int handle;
// Stream to read.
private Stream stream;
// Buffer used to read data from the stream.
private byte[] buffer;
// Whether reading is complete.
volatile bool complete = false;
/// <summary>
/// Initialize the reader.
/// </summary>
/// <param name="stream">Stream to read.</param>
/// <param name="bufferSize">Size of the buffer to read.</param>
public AsyncStreamReader(int handle, Stream stream, int bufferSize)
{
readEvent = new AutoResetEvent(false);
this.handle = handle;
this.stream = stream;
buffer = new byte[bufferSize];
}
/// <summary>
/// Get the handle of the stream associated with this reader.
/// </summary>
public int Handle
{
get
{
return handle;
}
}
/// <summary>
/// Start reading.
/// </summary>
public void Start()
{
if (!complete) (new Thread(new ThreadStart(Read))).Start();
}
/// <summary>
/// Read from the stream until the end is reached.
/// </summary>
private void Read()
{
while (!complete)
{
stream.BeginRead(
buffer, 0, buffer.Length, (asyncResult) => {
int bytesRead = stream.EndRead(asyncResult);
if (!complete)
{
complete = bytesRead == 0;
if (DataReceived != null)
{
byte[] copy = new byte[bytesRead];
Array.Copy(buffer, copy, copy.Length);
DataReceived(new StreamData(
handle, System.Text.Encoding.UTF8.GetString(copy), copy,
complete));
}
}
readEvent.Set();
}, null);
readEvent.WaitOne();
}
}
/// <summary>
/// Create a set of readers to read the specified streams, handles are assigned
/// based upon the index of each stream in the provided array.
/// </summary>
/// <param name="streams">Streams to read.</param>
/// <param name="bufferSize">Size of the buffer to use to read each stream.</param>
public static AsyncStreamReader[] CreateFromStreams(Stream[] streams, int bufferSize)
{
AsyncStreamReader[] readers = new AsyncStreamReader[streams.Length];
for (int i = 0; i < streams.Length; i++)
{
readers[i] = new AsyncStreamReader(i, streams[i], bufferSize);
}
return readers;
}
}
/// <summary>
/// Multiplexes data read from multiple AsyncStreamReaders onto a single thread.
/// </summary>
private class AsyncStreamReaderMultiplexer
{
/// Used to wait on items in the queue.
private AutoResetEvent queuedItem = null;
/// Queue of Data read from the readers.
private System.Collections.Queue queue = null;
/// Active stream handles.
private HashSet<int> activeStreams;
/// <summary>
/// Called when all streams reach the end or the reader is shut down.
/// </summary>
public delegate void CompletionHandler();
/// <summary>
/// Called when all streams reach the end or the reader is shut down.
/// </summary>
public event CompletionHandler Complete;
/// <summary>
/// Handler called from the multiplexer's thread.
/// </summary>
public event AsyncStreamReader.Handler DataReceived;
/// <summary>
/// Create the multiplexer and attach it to the specified handler.
/// </summary>
/// <param name="readers">Readers to read.</param>
/// <param name="handler">Called for queued data item.</param>
/// <param name="complete">Called when all readers complete.</param>
public AsyncStreamReaderMultiplexer(AsyncStreamReader[] readers,
AsyncStreamReader.Handler handler,
CompletionHandler complete = null)
{
queuedItem = new AutoResetEvent(false);
queue = System.Collections.Queue.Synchronized(new System.Collections.Queue());
activeStreams = new HashSet<int>();
foreach (AsyncStreamReader reader in readers)
{
reader.DataReceived += HandleRead;
activeStreams.Add(reader.Handle);
}
DataReceived += handler;
if (complete != null) Complete += complete;
(new Thread(new ThreadStart(PollQueue))).Start();
}
/// <summary>
/// Shutdown the multiplexer.
/// </summary>
public void Shutdown()
{
lock (activeStreams)
{
activeStreams.Clear();
}
queuedItem.Set();
}
// Handle stream read notification.
private void HandleRead(StreamData streamData)
{
queue.Enqueue(streamData);
queuedItem.Set();
}
// Poll the queue.
private void PollQueue()
{
while (activeStreams.Count > 0)
{
queuedItem.WaitOne();
while (queue.Count > 0)
{
StreamData data = (StreamData)queue.Dequeue();
if (data.end)
{
lock(activeStreams)
{
activeStreams.Remove(data.handle);
}
}
if (DataReceived != null) DataReceived(data);
}
}
if (Complete != null) Complete();
}
}
/// <summary>
/// Aggregates lines read by AsyncStreamReader.
/// </summary>
public class LineReader
{
// List of data keyed by stream handle.
private Dictionary<int, List<StreamData>> streamDataByHandle =
new Dictionary<int, List<StreamData>>();
/// <summary>
/// Event called with a new line of data.
/// </summary>
public event IOHandler LineHandler;
/// <summary>
/// Event called for each piece of data received.
/// </summary>
public event IOHandler DataHandler;
/// <summary>
/// Initialize the instance.
/// </summary>
/// <param name="handler">Called for each line read.</param>
public LineReader(IOHandler handler = null)
{
if (handler != null) LineHandler += handler;
}
/// <summary>
/// Retrieve the currently buffered set of data.
/// This allows the user to retrieve data before the end of a stream when
/// a newline isn't present.
/// </summary>
/// <param name="handle">Handle of the stream to query.</param>
/// <returns>List of data for the requested stream.</return>
public List<StreamData> GetBufferedData(int handle)
{
List<StreamData> handleData;
return streamDataByHandle.TryGetValue(handle, out handleData) ?
new List<StreamData>(handleData) : new List<StreamData>();
}
/// <summary>
/// Flush the internal buffer.
/// </summary>
public void Flush()
{
foreach (List<StreamData> handleData in streamDataByHandle.Values)
{
handleData.Clear();
}
}
/// <summary>
/// Aggregate the specified list of StringBytes into a single structure.
/// </summary>
/// <param name="handle">Stream handle.</param>
/// <param name="dataStream">Data to aggregate.</param>
public static StreamData Aggregate(List<StreamData> dataStream)
{
// Build a list of strings up to the newline.
List<string> stringList = new List<string>();
int byteArraySize = 0;
int handle = 0;
bool end = false;
foreach (StreamData sd in dataStream)
{
stringList.Add(sd.text);
byteArraySize += sd.data.Length;
handle = sd.handle;
end |= sd.end;
}
string concatenatedString = String.Join("", stringList.ToArray());
// Concatenate the list of bytes up to the StringBytes before the
// newline.
byte[] byteArray = new byte[byteArraySize];
int byteArrayOffset = 0;
foreach (StreamData sd in dataStream)
{
Array.Copy(sd.data, 0, byteArray, byteArrayOffset, sd.data.Length);
byteArrayOffset += sd.data.Length;
}
return new StreamData(handle, concatenatedString, byteArray, end);
}
/// <summary>
/// Delegate method which can be attached to AsyncStreamReader.DataReceived to
/// aggregate data until a new line is found before calling LineHandler.
/// </summary>
public void AggregateLine(Process process, StreamWriter stdin, StreamData data)
{
if (DataHandler != null) DataHandler(process, stdin, data);
bool linesProcessed = false;
if (data.data != null)
{
// Simplify line tracking by converting linefeeds to newlines.
data.text = data.text.Replace("\r\n", "\n").Replace("\r", "\n");
List<StreamData> aggregateList = GetBufferedData(data.handle);
aggregateList.Add(data);
bool complete = false;
while (!complete)
{
List<StreamData> newAggregateList = new List<StreamData>();
int textDataIndex = 0;
int aggregateListSize = aggregateList.Count;
complete = true;
foreach (StreamData textData in aggregateList)
{
bool end = data.end && (++textDataIndex == aggregateListSize);
newAggregateList.Add(textData);
int newlineOffset = textData.text.Length;
if (!end)
{
newlineOffset = textData.text.IndexOf("\n");
if (newlineOffset < 0) continue;
newAggregateList.Remove(textData);
}
StreamData concatenated = Aggregate(newAggregateList);
// Flush the aggregation list and store the overflow.
newAggregateList.Clear();
if (!end)
{
// Add the remaining line to the concatenated data.
concatenated.text += textData.text.Substring(0, newlineOffset + 1);
// Save the line after the newline in the buffer.
newAggregateList.Add(new StreamData(
data.handle, textData.text.Substring(newlineOffset + 1),
textData.data, false));
complete = false;
}
// Report the data.
if (LineHandler != null) LineHandler(process, stdin, concatenated);
linesProcessed = true;
}
aggregateList = newAggregateList;
}
streamDataByHandle[data.handle] = aggregateList;
}
// If no lines were processed call the handle to allow it to look ahead.
if (!linesProcessed && LineHandler != null)
{
LineHandler(process, stdin, StreamData.Empty);
}
}
}
/// <summary>
/// Execute a command line tool.
/// </summary>
/// <param name="toolPath">Tool to execute.</param>
/// <param name="arguments">String to pass to the tools' command line.</param>
/// <param name="workingDirectory">Directory to execute the tool from.</param>
/// <param name="envVars">Additional environment variables to set for the command.</param>
/// <param name="ioHandler">Allows a caller to provide interactive input and also handle
/// both output and error streams from a single delegate.</param>
/// <returns>CommandLineTool result if successful, raises an exception if it's not
/// possible to execute the tool.</returns>
public static Result Run(string toolPath, string arguments, string workingDirectory = null,
Dictionary<string, string> envVars = null,
IOHandler ioHandler = null) {
return RunViaShell(toolPath, arguments, workingDirectory: workingDirectory,
envVars: envVars, ioHandler: ioHandler, useShellExecution: false);
}
/// <summary>
/// Execute a command line tool.
/// </summary>
/// <param name="toolPath">Tool to execute. On Windows, if the path to this tool contains
/// single quotes (apostrophes) the tool will be executed via the shell.</param>
/// <param name="arguments">String to pass to the tools' command line.</param>
/// <param name="workingDirectory">Directory to execute the tool from.</param>
/// <param name="envVars">Additional environment variables to set for the command.</param>
/// <param name="ioHandler">Allows a caller to provide interactive input and also handle
/// both output and error streams from a single delegate. NOTE: This is ignored if
/// shell execution is enabled.</param>
/// <param name="useShellExecution">Execute the command via the shell. This disables
/// I/O redirection and causes a window to be popped up when the command is executed.
/// This uses file redirection to retrieve the standard output stream.
/// </param>
/// <param name="stdoutRedirectionInShellMode">Enables stdout and stderr redirection when
/// executing a command via the shell. This requires:
/// * cmd.exe (on Windows) or bash (on OSX / Linux) are in the path.
/// * Arguments containing whitespace are quoted.</param>
/// <returns>CommandLineTool result if successful, raises an exception if it's not
/// possible to execute the tool.</returns>
public static Result RunViaShell(
string toolPath, string arguments, string workingDirectory = null,
Dictionary<string, string> envVars = null,
IOHandler ioHandler = null, bool useShellExecution = false,
bool stdoutRedirectionInShellMode = true) {
var inputEncoding = Console.InputEncoding;
var outputEncoding = Console.OutputEncoding;
// Set encoderShouldEmitUTF8Identifier to false to prevent writing a Byte Order Marker (BOM).
Console.InputEncoding = new UTF8Encoding(false);
Console.OutputEncoding = new UTF8Encoding(false);
try
{
return RunViaShellInternal(toolPath, arguments, workingDirectory, envVars,
ioHandler, useShellExecution, stdoutRedirectionInShellMode);
}
finally
{
Console.InputEncoding = inputEncoding;
Console.OutputEncoding = outputEncoding;
}
}
private static Result RunViaShellInternal(
string toolPath, string arguments, string workingDirectory, Dictionary<string, string> envVars,
IOHandler ioHandler, bool useShellExecution, bool stdoutRedirectionInShellMode) {
// Mono 3.x on Windows can't execute tools with single quotes (apostrophes) in the path.
// The following checks for this condition and forces shell execution of tools in these
// paths which works fine as the shell tool should be in the system PATH.
if (UnityEngine.RuntimePlatform.WindowsEditor == UnityEngine.Application.platform &&
toolPath.Contains("\'")) {
useShellExecution = true;
stdoutRedirectionInShellMode = true;
}
string stdoutFileName = null;
string stderrFileName = null;
if (useShellExecution && stdoutRedirectionInShellMode) {
stdoutFileName = Path.GetTempFileName();
stderrFileName = Path.GetTempFileName();
string shellCmd ;
string shellArgPrefix;
string shellArgPostfix;
string escapedToolPath = toolPath;
if (UnityEngine.RuntimePlatform.WindowsEditor == UnityEngine.Application.platform) {
shellCmd = "cmd.exe";
shellArgPrefix = "/c \"";
shellArgPostfix = "\"";
} else {
shellCmd = "bash";
shellArgPrefix = "-l -c '";
shellArgPostfix = "'";
escapedToolPath = toolPath.Replace("'", "'\\''");
}
arguments = String.Format("{0}\"{1}\" {2} 1> {3} 2> {4}{5}", shellArgPrefix,
escapedToolPath, arguments, stdoutFileName,
stderrFileName, shellArgPostfix);
toolPath = shellCmd;
}
Process process = new Process();
process.StartInfo.UseShellExecute = useShellExecution;
process.StartInfo.Arguments = arguments;
if (useShellExecution) {
process.StartInfo.CreateNoWindow = false;
process.StartInfo.RedirectStandardOutput = false;
process.StartInfo.RedirectStandardError = false;
} else {
process.StartInfo.CreateNoWindow = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
if (envVars != null) {
foreach (var env in envVars) {
process.StartInfo.EnvironmentVariables[env.Key] = env.Value;
}
}
}
process.StartInfo.RedirectStandardInput = !useShellExecution && (ioHandler != null);
process.StartInfo.FileName = toolPath;
process.StartInfo.WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory;
var started = process.Start();
if (!started) {
UnityEngine.Debug.LogErrorFormat("Failed to start {0}", process);
return new Result {exitCode = -1};
}
// If an I/O handler was specified, call it with no data to provide a process and stdin
// handle before output data is sent to it.
if (ioHandler != null) ioHandler(process, process.StandardInput, StreamData.Empty);
List<string>[] stdouterr = new List<string>[] {
new List<string>(), new List<string>() };
if (useShellExecution) {
process.WaitForExit();
if (stdoutRedirectionInShellMode) {
stdouterr[0].Add(File.ReadAllText(stdoutFileName));
stdouterr[1].Add(File.ReadAllText(stderrFileName));
File.Delete(stdoutFileName);
File.Delete(stderrFileName);
}
} else {
AutoResetEvent complete = new AutoResetEvent(false);
// Read raw output from the process.
AsyncStreamReader[] readers = AsyncStreamReader.CreateFromStreams(
new Stream[] { process.StandardOutput.BaseStream,
process.StandardError.BaseStream }, 1);
new AsyncStreamReaderMultiplexer(
readers,
(StreamData data) => {
stdouterr[data.handle].Add(data.text);
if (ioHandler != null) ioHandler(process, process.StandardInput, data);
},
complete: () => { complete.Set(); });
foreach (AsyncStreamReader reader in readers) reader.Start();
process.WaitForExit();
// Wait for the reading threads to complete.
complete.WaitOne();
}
Result result = new Result();
result.stdout = String.Join("", stdouterr[StandardOutputStreamDataHandle].ToArray());
result.stderr = String.Join("", stdouterr[StandardErrorStreamDataHandle].ToArray());
result.exitCode = process.ExitCode;
result.message = FormatResultMessage(toolPath, arguments, result.stdout,
result.stderr, result.exitCode);
return result;
}
/// <summary>
/// Split a string into lines using newline, carriage return or a combination of both.
/// </summary>
/// <param name="multilineString">String to split into lines</param>
public static string[] SplitLines(string multilineString)
{
return Regex.Split(multilineString, "\r\n|\r|\n");
}
/// <summary>
/// Format a command excecution error message.
/// </summary>
/// <param name="toolPath">Tool executed.</param>
/// <param name="arguments">Arguments used to execute the tool.</param>
/// <param name="stdout">Standard output stream from tool execution.</param>
/// <param name="stderr">Standard error stream from tool execution.</param>
/// <param name="exitCode">Result of the tool.</param>
private static string FormatResultMessage(string toolPath, string arguments,
string stdout, string stderr,
int exitCode) {
return String.Format(
"{0} '{1} {2}'\n" +
"stdout:\n" +
"{3}\n" +
"stderr:\n" +
"{4}\n" +
"exit code: {5}\n",
exitCode == 0 ? "Successfully executed" : "Failed to run",
toolPath, arguments, stdout, stderr, exitCode);
}
/// <summary>
/// Returns the specified argument in double quotes.
/// </summary>
public static string QuotePath(string path)
{
return string.Format("\"{0}\"", path);
}
#if UNITY_EDITOR
/// <summary>
/// Get an executable extension.
/// </summary>
/// <returns>Platform specific extension for executables.</returns>
public static string GetExecutableExtension()
{
return (UnityEngine.RuntimePlatform.WindowsEditor ==
UnityEngine.Application.platform) ? ".exe" : "";
}
/// <summary>
/// Locate an executable in the system path.
/// </summary>
/// <param name="exeName">Executable name without a platform specific extension like
/// .exe</param>
/// <returns>A string to the executable path if it's found, null otherwise.</returns>
public static string FindExecutable(string executable)
{
string which = (UnityEngine.RuntimePlatform.WindowsEditor ==
UnityEngine.Application.platform) ? "where" : "which";
try
{
Result result = Run(which, executable, Environment.CurrentDirectory);
if (result.exitCode == 0)
{
return SplitLines(result.stdout)[0];
}
}
catch (Exception e)
{
UnityEngine.Debug.Log("'" + which + "' command is not on path. " +
"Unable to find executable '" + executable +
"' (" + e.ToString() + ")");
}
return null;
}
#endif // UNITY_EDITOR
}
}