Multi-Platform Package Manager for Stable Diffusion
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

468 lines
14 KiB

using System.Diagnostics;
using System.Text;
using NLog;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Core.Processes;
public static class ProcessRunner
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// Opens the given URL in the default browser.
/// </summary>
/// <param name="url">URL as string</param>
public static void OpenUrl(string url)
{
Logger.Debug($"Opening URL '{url}'");
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
/// <summary>
/// Opens the given URL in the default browser.
/// </summary>
/// <param name="url">URI, using AbsoluteUri component</param>
public static void OpenUrl(Uri url)
{
OpenUrl(url.AbsoluteUri);
}
/// <summary>
/// Start an executable or .app on macOS.
/// </summary>
public static Process StartApp(string path, ProcessArgs args)
{
var startInfo = new ProcessStartInfo();
if (Compat.IsMacOS)
{
startInfo.FileName = "open";
startInfo.Arguments = args.Prepend([path, "--args"]).ToString();
startInfo.UseShellExecute = true;
}
else
{
startInfo.FileName = path;
startInfo.Arguments = args;
}
return Process.Start(startInfo) ?? throw new NullReferenceException("Process.Start returned null");
}
/// <summary>
/// Opens the given folder in the system file explorer.
/// </summary>
public static async Task OpenFolderBrowser(string directoryPath)
{
if (Compat.IsWindows)
{
using var process = new Process();
process.StartInfo.FileName = "explorer.exe";
process.StartInfo.Arguments = Quote(directoryPath);
process.Start();
await process.WaitForExitAsync().ConfigureAwait(false);
}
else if (Compat.IsLinux)
{
using var process = new Process();
process.StartInfo.FileName = directoryPath;
process.StartInfo.UseShellExecute = true;
process.Start();
await process.WaitForExitAsync().ConfigureAwait(false);
}
else if (Compat.IsMacOS)
{
using var process = new Process();
process.StartInfo.FileName = "open";
process.StartInfo.Arguments = Quote(directoryPath);
process.Start();
await process.WaitForExitAsync().ConfigureAwait(false);
}
else
{
throw new PlatformNotSupportedException();
}
}
/// <summary>
/// Opens the given file within its folder in the system file explorer.
/// </summary>
public static async Task OpenFileBrowser(string filePath)
{
if (Compat.IsWindows)
{
using var process = new Process();
process.StartInfo.FileName = "explorer.exe";
process.StartInfo.Arguments = $"/select, {Quote(filePath)}";
process.Start();
await process.WaitForExitAsync().ConfigureAwait(false);
}
else if (Compat.IsLinux)
{
using var process = new Process();
process.StartInfo.FileName = "dbus-send";
process.StartInfo.Arguments =
"--print-reply --dest=org.freedesktop.FileManager1 "
+ "/org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems "
+ $"array:string:\"file://{filePath}\" string:\"\"";
process.StartInfo.UseShellExecute = true;
process.Start();
await process.WaitForExitAsync().ConfigureAwait(false);
}
else if (Compat.IsMacOS)
{
using var process = new Process();
process.StartInfo.FileName = "open";
process.StartInfo.Arguments = $"-R {Quote(filePath)}";
process.Start();
await process.WaitForExitAsync().ConfigureAwait(false);
}
else
{
throw new PlatformNotSupportedException();
}
}
/// <summary>
/// Starts and tracks a process.
/// </summary>
private static Process StartTrackedProcess(Process process)
{
process.Start();
// Currently only supported on Windows
if (Compat.IsWindows)
{
// Supress errors here since the process may have already exited
try
{
ProcessTracker.AddProcess(process);
}
catch (InvalidOperationException) { }
}
return process;
}
public static async Task<string> GetProcessOutputAsync(
string fileName,
string arguments,
string? workingDirectory = null,
Dictionary<string, string>? environmentVariables = null
)
{
Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'");
var info = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
if (environmentVariables != null)
{
foreach (var (key, value) in environmentVariables)
{
info.EnvironmentVariables[key] = value;
}
}
if (workingDirectory != null)
{
info.WorkingDirectory = workingDirectory;
}
using var process = new Process();
process.StartInfo = info;
StartTrackedProcess(process);
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return output;
}
public static async Task<ProcessResult> GetProcessResultAsync(
string fileName,
ProcessArgs arguments,
string? workingDirectory = null,
IReadOnlyDictionary<string, string>? environmentVariables = null
)
{
Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'");
var info = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
if (environmentVariables != null)
{
foreach (var (key, value) in environmentVariables)
{
info.EnvironmentVariables[key] = value;
}
}
if (workingDirectory != null)
{
info.WorkingDirectory = workingDirectory;
}
using var process = new Process();
process.StartInfo = info;
StartTrackedProcess(process);
var stdout = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
var stderr = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
await process.WaitForExitAsync().ConfigureAwait(false);
string? processName = null;
TimeSpan elapsed = default;
// Accessing these properties may throw an exception if the process has already exited
try
{
processName = process.ProcessName;
}
catch (SystemException) { }
try
{
elapsed = process.ExitTime - process.StartTime;
}
catch (SystemException) { }
return new ProcessResult
{
ExitCode = process.ExitCode,
StandardOutput = stdout,
StandardError = stderr,
ProcessName = processName,
Elapsed = elapsed
};
}
public static Process StartProcess(
string fileName,
string arguments,
string? workingDirectory = null,
Action<string?>? outputDataReceived = null,
IReadOnlyDictionary<string, string>? environmentVariables = null
)
{
Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'");
var info = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
if (environmentVariables != null)
{
foreach (var (key, value) in environmentVariables)
{
info.EnvironmentVariables[key] = value;
}
}
if (workingDirectory != null)
{
info.WorkingDirectory = workingDirectory;
}
var process = new Process { StartInfo = info };
StartTrackedProcess(process);
if (outputDataReceived == null)
return process;
process.OutputDataReceived += (sender, args) => outputDataReceived(args.Data);
process.ErrorDataReceived += (sender, args) => outputDataReceived(args.Data);
process.BeginOutputReadLine();
process.BeginErrorReadLine();
return process;
}
public static AnsiProcess StartAnsiProcess(
string fileName,
string arguments,
string? workingDirectory = null,
Action<ProcessOutput>? outputDataReceived = null,
IReadOnlyDictionary<string, string>? environmentVariables = null
)
{
Logger.Debug(
$"Starting process '{fileName}' with arguments '{arguments}' in working directory '{workingDirectory}'"
);
var info = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
if (environmentVariables != null)
{
foreach (var (key, value) in environmentVariables)
{
info.EnvironmentVariables[key] = value;
}
}
if (workingDirectory != null)
{
info.WorkingDirectory = workingDirectory;
}
var process = new AnsiProcess(info);
StartTrackedProcess(process);
if (outputDataReceived != null)
{
process.BeginAnsiRead(outputDataReceived);
}
return process;
}
public static AnsiProcess StartAnsiProcess(
string fileName,
IEnumerable<string> arguments,
string? workingDirectory = null,
Action<ProcessOutput>? outputDataReceived = null,
Dictionary<string, string>? environmentVariables = null
)
{
// Quote arguments containing spaces
var args = string.Join(" ", arguments.Where(s => !string.IsNullOrEmpty(s)).Select(Quote));
return StartAnsiProcess(fileName, args, workingDirectory, outputDataReceived, environmentVariables);
}
public static async Task<ProcessResult> RunBashCommand(
string command,
string workingDirectory = "",
IReadOnlyDictionary<string, string>? environmentVariables = null
)
{
// Escape any single quotes in the command
var escapedCommand = command.Replace("\"", "\\\"");
var arguments = $"-c \"{escapedCommand}\"";
Logger.Info($"Running bash command [bash {arguments}]");
var processInfo = new ProcessStartInfo("bash", arguments)
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = workingDirectory,
};
if (environmentVariables != null)
{
foreach (var (key, value) in environmentVariables)
{
processInfo.EnvironmentVariables[key] = value;
}
}
using var process = new Process();
process.StartInfo = processInfo;
var stdout = new StringBuilder();
var stderr = new StringBuilder();
process.OutputDataReceived += (_, args) => stdout.Append(args.Data);
process.ErrorDataReceived += (_, args) => stderr.Append(args.Data);
StartTrackedProcess(process);
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync().ConfigureAwait(false);
return new ProcessResult
{
ExitCode = process.ExitCode,
StandardOutput = stdout.ToString(),
StandardError = stderr.ToString()
};
}
public static Task<ProcessResult> RunBashCommand(
IEnumerable<string> commands,
string workingDirectory = "",
IReadOnlyDictionary<string, string>? environmentVariables = null
)
{
// Quote arguments containing spaces
var args = string.Join(" ", commands.Select(Quote));
return RunBashCommand(args, workingDirectory, environmentVariables);
}
/// <summary>
/// Quotes argument with double quotes if it contains spaces,
/// and does not already start and end with double quotes.
/// </summary>
public static string Quote(string argument)
{
var inner = argument.Trim('"');
return inner.Contains(' ') ? $"\"{inner}\"" : argument;
}
/// <summary>
/// Waits for process to exit, then validates exit code.
/// </summary>
/// <param name="process">Process to check.</param>
/// <param name="expectedExitCode">Expected exit code.</param>
/// <param name="cancelToken">Cancellation token.</param>
/// <exception cref="ProcessException">Thrown if exit code does not match expected value.</exception>
public static async Task WaitForExitConditionAsync(
Process process,
int expectedExitCode = 0,
CancellationToken cancelToken = default
)
{
if (!process.HasExited)
{
await process.WaitForExitAsync(cancelToken).ConfigureAwait(false);
}
if (process.ExitCode == expectedExitCode)
{
return;
}
// Accessing ProcessName may error on some platforms
string? processName = null;
try
{
processName = process.ProcessName;
}
catch (SystemException) { }
throw new ProcessException(
"Process "
+ (processName == null ? "" : processName + " ")
+ $"failed with exit-code {process.ExitCode}."
);
}
}