Files
MobileShooter/Packages/com.merry-yellow.code-assist/Editor/UnityEditorShell.cs
2025-12-26 17:56:05 -08:00

305 lines
9.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using UnityEngine;
using UnityEngine.Events;
using UnityEditor;
#pragma warning disable IDE0005
using Serilog = Meryel.Serilog;
#pragma warning restore IDE0005
#nullable enable
namespace Meryel.UnityCodeAssist.Editor.Shell
{
public class UnityEditorShell
{
public static string DefaultShellApp
{
get
{
#if UNITY_EDITOR_WIN
return "cmd.exe";
#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
//return "bash";
return System.IO.File.Exists("/bin/zsh") ? "/bin/zsh" : "/bin/bash";
#else
Serilog.Log.Error("invalid platform");
return "invalid-platform";
#endif
}
}
// we are using unity actions for posterity in case we want to inspect those in-editor someday
private static readonly List<UnityAction> ActionsQueue;
static UnityEditorShell()
{
ActionsQueue = new List<UnityAction>();
EditorApplication.update += OnUpdate;
}
// while running the Unity Editor update loop, we'll unqueue any tasks if such exist.
// actions can be
private static void OnUpdate()
{
while (ActionsQueue.Count > 0)
{
lock (ActionsQueue)
{
var action = ActionsQueue[0];
try
{
action?.Invoke();
}
catch (Exception e)
{
Serilog.Log.Error(e, "error invoking shell action");
}
finally
{
ActionsQueue.RemoveAt(0);
}
}
}
}
private static void Enqueue(UnityAction action)
{
lock (ActionsQueue)
{
ActionsQueue.Add(action);
}
}
public static ShellCommandEditorToken Execute(string cmd)
{
var shellCommandEditorToken = new ShellCommandEditorToken();
System.Threading.ThreadPool.QueueUserWorkItem(delegate (object state)
{
Process? process = null;
try
{
var processStartInfo = CreateProcessStartInfo(cmd);
// in case the command was already killed from the editor when the thread was queued
if (shellCommandEditorToken.IsKillRequested)
{
return;
}
process = Process.Start(processStartInfo);
SetupProcessCallbacks(process, processStartInfo, shellCommandEditorToken);
ReadProcessOutput(process, shellCommandEditorToken);
}
catch (Exception e)
{
Serilog.Log.Error(e, "error starting shell");
process?.Close();
Enqueue(() =>
{
shellCommandEditorToken.FeedLog(UnityShellLogType.Error, e.ToString());
shellCommandEditorToken.MarkAsDone(-1);
});
}
});
return shellCommandEditorToken;
}
private static ProcessStartInfo CreateProcessStartInfo(string cmd)
{
var processStartInfo = new ProcessStartInfo(DefaultShellApp);
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
processStartInfo.Arguments = "-c";
#elif UNITY_EDITOR_WIN
processStartInfo.Arguments = "/c";
#endif
processStartInfo.Arguments += (" \"" + cmd + " \"");
processStartInfo.CreateNoWindow = true;
processStartInfo.ErrorDialog = true;
processStartInfo.UseShellExecute = false;
//processStartInfo.WorkingDirectory = options.WorkingDirectory == null ? "./" : options.WorkingDirectory;
processStartInfo.RedirectStandardOutput = true;
processStartInfo.RedirectStandardError = true;
processStartInfo.RedirectStandardInput = true;
processStartInfo.StandardOutputEncoding = Encoding.UTF8;
processStartInfo.StandardErrorEncoding = Encoding.UTF8;
return processStartInfo;
}
private static void SetupProcessCallbacks(Process process, ProcessStartInfo processStartInfo, ShellCommandEditorToken shellCommandEditorToken)
{
shellCommandEditorToken.BindProcess(process);
process.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs e)
{
Serilog.Log.Error("error on shell.ErrorDataReceived: {data}", e.Data);
};
process.OutputDataReceived += delegate (object sender, DataReceivedEventArgs e)
{
Serilog.Log.Debug("shell.OutputDataReceived: {data}", e.Data);
};
process.Exited += delegate (object sender, System.EventArgs e)
{
Serilog.Log.Debug("shell.Exited: {data}", e.ToString());
};
}
private static void ReadProcessOutput(Process process, ShellCommandEditorToken shellCommandEditorToken)
{
do
{
var line = process.StandardOutput.ReadLine();
if (line == null)
{
break;
}
line = line.Replace("\\", "/");
Enqueue(delegate () { shellCommandEditorToken.FeedLog(UnityShellLogType.Log, line); });
} while (true);
while (true)
{
var error = process.StandardError.ReadLine();
if (string.IsNullOrEmpty(error))
{
break;
}
Enqueue(delegate () { shellCommandEditorToken.FeedLog(UnityShellLogType.Error, error); });
}
process.WaitForExit();
var exitCode = process.ExitCode;
process.Close();
Enqueue(() => { shellCommandEditorToken.MarkAsDone(exitCode); });
}
}
public class ShellCommandEditorToken
{
public event UnityAction<UnityShellLogType, string>? OnLog;
public event UnityAction<int>? OnExit;
private Process? _process;
internal void BindProcess(Process process)
{
_process = process;
}
internal void FeedLog(UnityShellLogType unityShellLogType, string log)
{
OnLog?.Invoke(unityShellLogType, log);
if (unityShellLogType == UnityShellLogType.Error)
{
HasError = true;
}
}
public bool IsKillRequested { get; private set; }
public void Kill()
{
if (IsKillRequested)
{
return;
}
IsKillRequested = true;
if (_process != null)
{
_process.Kill();
_process = null;
}
else
{
MarkAsDone(137);
}
}
public bool HasError { get; private set; }
public int ExitCode { get; private set; }
public bool IsDone { get; private set; }
internal void MarkAsDone(int exitCode)
{
ExitCode = exitCode;
IsDone = true;
OnExit?.Invoke(exitCode);
}
/// <summary>
/// This method is intended for compiler use. Don't call it in your code.
/// </summary>
public ShellCommandAwaiter GetAwaiter()
{
return new ShellCommandAwaiter(this);
}
}
public struct ShellCommandAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
{
private readonly ShellCommandEditorToken _shellCommandEditorToken;
public ShellCommandAwaiter(ShellCommandEditorToken shellCommandEditorToken)
{
_shellCommandEditorToken = shellCommandEditorToken;
}
public int GetResult()
{
return _shellCommandEditorToken.ExitCode;
}
public bool IsCompleted => _shellCommandEditorToken.IsDone;
public void OnCompleted(Action continuation)
{
UnsafeOnCompleted(continuation);
}
public void UnsafeOnCompleted(Action continuation)
{
if (IsCompleted)
{
continuation();
}
else
{
_shellCommandEditorToken.OnExit += (_) => { continuation(); };
}
}
}
public enum UnityShellLogType
{
Log,
Error
}
public class ShellCommandYieldable : CustomYieldInstruction
{
private readonly ShellCommandEditorToken _shellCommandEditorToken;
public ShellCommandYieldable(ShellCommandEditorToken shellCommandEditorToken)
{
_shellCommandEditorToken = shellCommandEditorToken;
}
public override bool keepWaiting => !_shellCommandEditorToken.IsDone;
}
}