diff --git a/Editor/Extensions/ProcessExtensions.cs b/Editor/Extensions/ProcessExtensions.cs
new file mode 100644
index 0000000..9a1df0b
--- /dev/null
+++ b/Editor/Extensions/ProcessExtensions.cs
@@ -0,0 +1,257 @@
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+
+namespace Utilities.Extensions.Editor
+{
+ ///
+ /// Extension class.
+ ///
+ public static class ProcessExtensions
+ {
+ ///
+ /// Runs an external process.
+ ///
+ ///
+ /// The passed arguments.
+ /// The output of the process.
+ /// The Application to run through the command line. Default application is "cmd.exe"
+ /// Add the platform command switch to the arguments?
+ /// Output string.
+ /// This process will block the main thread of the editor if command takes too long to run. Use for a background process.
+ public static bool Run(this Process process, string args, out string output, string application = "", bool usePlatformArgs = true)
+ {
+ if (string.IsNullOrEmpty(args))
+ {
+ output = "You cannot pass a null or empty parameter.";
+ UnityEngine.Debug.LogError(output);
+ return false;
+ }
+
+ if (usePlatformArgs)
+ {
+ SetupPlatformArgs(ref args, ref application);
+ }
+
+ process.StartInfo = new ProcessStartInfo
+ {
+ Arguments = args,
+ CreateNoWindow = true,
+ FileName = application,
+ UseShellExecute = false,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+
+ try
+ {
+ if (!process.Start())
+ {
+ output = "Failed to start process!";
+ UnityEngine.Debug.LogError(output);
+ return false;
+ }
+
+ var error = process.StandardError.ReadToEnd();
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ output = error;
+ return false;
+ }
+
+ output = process.StandardOutput.ReadToEnd();
+
+ process.WaitForExit();
+ process.Close();
+ process.Dispose();
+ }
+ catch (Exception e)
+ {
+ output = e.Message;
+ UnityEngine.Debug.LogException(e);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Starts a process asynchronously.
+ ///
+ /// This Process.
+ /// The Process arguments.
+ /// Should output debug code to Editor Console?
+ ///
+ ///
+ public static async Task RunAsync(this Process process, string args, bool showDebug, CancellationToken cancellationToken = default)
+ => await RunAsync(process, args, string.Empty, showDebug, cancellationToken);
+
+ ///
+ /// Starts a process asynchronously.
+ ///
+ /// This Process.
+ /// The process executable to run.
+ /// The Process arguments.
+ /// Should output debug code to Editor Console?
+ ///
+ /// Add the command platform switch to the arguments?
+ ///
+ public static async Task RunAsync(this Process process, string args, string application = "", bool showDebug = false, CancellationToken cancellationToken = default, bool setPlatformArgs = true)
+ {
+ if (setPlatformArgs)
+ {
+ SetupPlatformArgs(ref args, ref application);
+ }
+
+ if (showDebug)
+ {
+ UnityEngine.Debug.Log($"\"{application}\" {args}");
+ }
+
+ return await RunAsync(process, new ProcessStartInfo
+ {
+ Arguments = args,
+ CreateNoWindow = true,
+ FileName = application,
+ UseShellExecute = false,
+ RedirectStandardError = true,
+ RedirectStandardOutput = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ }, showDebug, cancellationToken);
+ }
+
+ ///
+ /// Starts a process asynchronously.
+ ///
+ /// The provided Process Start Info must not use shell execution, and should redirect the standard output and errors.
+ /// This Process.
+ /// The Process start info.
+ /// Should output debug code to Editor Console?
+ ///
+ ///
+ public static async Task RunAsync(this Process process, ProcessStartInfo startInfo, bool showDebug = false, CancellationToken cancellationToken = default)
+ {
+ Debug.Assert(!startInfo.UseShellExecute, "Process Start Info must not use shell execution.");
+ Debug.Assert(startInfo.RedirectStandardOutput, "Process Start Info must redirect standard output.");
+ Debug.Assert(startInfo.RedirectStandardError, "Process Start Info must redirect standard errors.");
+
+ process.StartInfo = startInfo;
+ process.EnableRaisingEvents = true;
+
+ var processResult = new TaskCompletionSource();
+ var errorCodeResult = new TaskCompletionSource();
+ var errorList = new List();
+ var outputCodeResult = new TaskCompletionSource();
+ var outputList = new List();
+
+ process.Exited += OnProcessExited;
+ process.ErrorDataReceived += OnErrorDataReceived;
+ process.OutputDataReceived += OnOutputDataReceived;
+
+ async void OnProcessExited(object sender, EventArgs args)
+ {
+ processResult.TrySetResult(new ProcessResult(process.StartInfo.Arguments, process.ExitCode, await errorCodeResult.Task, await outputCodeResult.Task));
+ process.Close();
+ process.Dispose();
+ process = null;
+ }
+
+ void OnErrorDataReceived(object sender, DataReceivedEventArgs args)
+ {
+ if (!string.IsNullOrWhiteSpace(args.Data))
+ {
+ errorList.Add(args.Data);
+
+ if (!showDebug)
+ {
+ return;
+ }
+
+ UnityEngine.Debug.LogError(args.Data);
+ }
+ else
+ {
+ errorCodeResult.TrySetResult(errorList.ToArray());
+ }
+ }
+
+ void OnOutputDataReceived(object sender, DataReceivedEventArgs args)
+ {
+ if (!string.IsNullOrWhiteSpace(args.Data))
+ {
+ outputList.Add(args.Data);
+
+ if (!showDebug)
+ {
+ return;
+ }
+
+ UnityEngine.Debug.Log(args.Data);
+ }
+ else
+ {
+ outputCodeResult.TrySetResult(outputList.ToArray());
+ }
+ }
+
+ if (!process.Start())
+ {
+ if (showDebug)
+ {
+ UnityEngine.Debug.LogError("Failed to start process!");
+ }
+
+ processResult.TrySetResult(new ProcessResult(process.StartInfo.Arguments, process.ExitCode, new[] { "Failed to start process!" }, null));
+ }
+ else
+ {
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ CancellationWatcher();
+ }
+
+ async void CancellationWatcher()
+ {
+ // ReSharper disable once MethodSupportsCancellation
+ // We utilize the cancellation token in the loop
+ await Task.Run(() =>
+ {
+ while (process is { HasExited: false })
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ processResult.TrySetResult(new ProcessResult(process.StartInfo.Arguments, 1223, new[] { "The operation was canceled by the user." }, new[] { "The operation was canceled by the user." }));
+ process.Kill();
+ process.Close();
+ process.Dispose();
+ break;
+ }
+ }
+ });
+ }
+
+ return await processResult.Task;
+ }
+
+ private static void SetupPlatformArgs(ref string args, ref string application)
+ {
+ var updateApplication = string.IsNullOrWhiteSpace(application);
+
+ if (updateApplication)
+ {
+#if UNITY_EDITOR_WIN
+ application = "cmd.exe";
+ args = $"/c {args}";
+#else
+ application = "/bin/bash";
+ args = $"-c \"{args}\"";
+#endif
+ }
+ }
+ }
+}
diff --git a/Editor/Extensions/ProcessExtensions.cs.meta b/Editor/Extensions/ProcessExtensions.cs.meta
new file mode 100644
index 0000000..92be01b
--- /dev/null
+++ b/Editor/Extensions/ProcessExtensions.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e94708f93385d1d46a0416a27d938e80
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Editor/Extensions/ProcessResult.cs b/Editor/Extensions/ProcessResult.cs
new file mode 100644
index 0000000..67c0d46
--- /dev/null
+++ b/Editor/Extensions/ProcessResult.cs
@@ -0,0 +1,76 @@
+// Licensed under the MIT License. See LICENSE in the project root for license information.
+
+using System.Text;
+using System;
+
+namespace Utilities.Extensions.Editor
+{
+ ///
+ /// Result from a completed asynchronous process.
+ ///
+ public class ProcessResult
+ {
+ public string Arguments { get; }
+
+ ///
+ /// Exit code from completed process.
+ ///
+ public int ExitCode { get; }
+
+ ///
+ /// Errors from completed process.
+ ///
+ public string[] Errors { get; }
+
+ ///
+ /// Output from completed process.
+ ///
+ public string[] Output { get; }
+
+ ///
+ /// Constructor for Process Result.
+ ///
+ /// The process into arguments.
+ /// Exit code from completed process.
+ /// Errors from completed process.
+ /// Output from completed process.
+ public ProcessResult(string arguments, int exitCode, string[] errors, string[] output)
+ {
+ Arguments = arguments;
+ ExitCode = exitCode;
+ Errors = errors;
+ Output = output;
+ }
+
+ ///
+ /// Checks and throws an exception if it is not zero.
+ ///
+ ///
+ public void ThrowIfNonZeroExitCode()
+ {
+ if (ExitCode != 0)
+ {
+ var messageBuilder = new StringBuilder($"[{ExitCode}] Failed to run: \"{Arguments}\"");
+
+ if (Output != null)
+ {
+
+ foreach (var line in Output)
+ {
+ messageBuilder.Append($"\n{line}");
+ }
+ }
+
+ if (Errors != null)
+ {
+ foreach (var line in Errors)
+ {
+ messageBuilder.Append($"\n{line}");
+ }
+ }
+
+ throw new Exception(messageBuilder.ToString());
+ }
+ }
+ }
+}
diff --git a/Editor/Extensions/ProcessResult.cs.meta b/Editor/Extensions/ProcessResult.cs.meta
new file mode 100644
index 0000000..dad1fa4
--- /dev/null
+++ b/Editor/Extensions/ProcessResult.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4472c029eef8a6c46bad0f2371ee7356
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/package.json b/package.json
index 496661a..f75f480 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Utilities.Extensions",
"description": "Common extensions for Unity types (UPM)",
"keywords": [],
- "version": "1.1.15",
+ "version": "1.1.16",
"unity": "2021.3",
"documentationUrl": "https://github.com/RageAgainstThePixel/com.utilities.extensions#documentation",
"changelogUrl": "https://github.com/RageAgainstThePixel/com.utilities.extensions/releases",