在本文蓝丙,筆者將帶大家從 EditorUtility.CompileCsharp API 出發(fā),看一看Unity 是怎么使用 帶參數(shù)命令行調(diào)用軟件的望拖。行文倉促內(nèi)容繁雜故而較為佛系渺尘,另筆者技術(shù)有限難免疏漏,見諒~
前言
昨天看到一篇 博客 说敏,講述了如何將Unity 編輯器之外的腳本通過 EditorUtility.CompileCsharp 在編輯器下編譯成 Dll 鸥跟。
這引起了筆者的不少興趣,于是拿起 Dnspy 沿著這個方法名 CompileCsharp
就追了進(jìn)去...
這些與腳本編譯相關(guān)的邏輯居然不是調(diào)用的 Native dll的方法,這很 nice医咨!
發(fā)現(xiàn)
經(jīng)過不停的點擊跟進(jìn)枫匾,筆者發(fā)現(xiàn)了一個 Program 類型,也就是本文的主角拟淮,它將我們常規(guī)的 軟件命令行調(diào)用 進(jìn)行了封裝干茉,感覺到了滿滿的專業(yè)級別的嚴(yán)謹(jǐn):
using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace UnityEditor.Utils
{
internal class Program : IDisposable
{
protected Program()
{
this._process = new Process();
}
public Program(ProcessStartInfo si) : this()
{
this._process.StartInfo = si;
}
public void Start()
{
this.Start(null);
}
public void Start(EventHandler exitCallback)
{
if (exitCallback != null)
{
this._process.EnableRaisingEvents = true;
this._process.Exited += exitCallback; //監(jiān)聽軟件退出事件
}
this._process.StartInfo.RedirectStandardInput = true;
this._process.StartInfo.RedirectStandardError = true;
this._process.StartInfo.RedirectStandardOutput = true;
this._process.StartInfo.UseShellExecute = false;
this._process.Start();
this._stdout = new ProcessOutputStreamReader(this._process, this._process.StandardOutput); //使用異步讀取軟件常規(guī)輸出
this._stderr = new ProcessOutputStreamReader(this._process, this._process.StandardError);//使用異步讀取軟件異常
this._stdin = this._process.StandardInput.BaseStream;
}
public ProcessStartInfo GetProcessStartInfo()
{
return this._process.StartInfo;
}
public void LogProcessStartInfo()
{
if (this._process != null)
{
Program.LogProcessStartInfo(this._process.StartInfo);
}
else
{
Console.WriteLine("Failed to retrieve process startInfo");
}
}
private static void LogProcessStartInfo(ProcessStartInfo si) //實現(xiàn)被啟動的進(jìn)程的 文件名+命令行參數(shù)的輸出
{
Console.WriteLine("Filename: " + si.FileName);
Console.WriteLine("Arguments: " + si.Arguments);
IEnumerator enumerator = si.EnvironmentVariables.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
object obj = enumerator.Current;
DictionaryEntry dictionaryEntry = (DictionaryEntry)obj;
if (dictionaryEntry.Key.ToString().StartsWith("MONO"))
{
Console.WriteLine("{0}: {1}", dictionaryEntry.Key, dictionaryEntry.Value);
}
}
}
finally
{
IDisposable disposable;
if ((disposable = (enumerator as IDisposable)) != null)
{
disposable.Dispose();
}
}
int num = si.Arguments.IndexOf("Temp/UnityTempFile");
Console.WriteLine("index: " + num);
if (num > 0)
{
string text = si.Arguments.Substring(num);
Console.WriteLine("Responsefile: " + text + " Contents: ");
Console.WriteLine(File.ReadAllText(text));
}
}
public string GetAllOutput() //實現(xiàn)進(jìn)程的所有標(biāo)準(zhǔn)輸出和異常輸出
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("stdout:");
foreach (string value in this.GetStandardOutput())
{
stringBuilder.AppendLine(value);
}
stringBuilder.AppendLine("stderr:");
foreach (string value2 in this.GetErrorOutput())
{
stringBuilder.AppendLine(value2);
}
return stringBuilder.ToString();
}
public bool HasExited
{
get
{
if (this._process == null)
{
throw new InvalidOperationException("You cannot call HasExited before calling Start");
}
bool result;
try
{
result = this._process.HasExited;
}
catch (InvalidOperationException)
{
result = true;
}
return result;
}
}
public int ExitCode
{
get
{
return this._process.ExitCode;
}
}
public int Id
{
get
{
return this._process.Id;
}
}
public void Dispose()
{
this.Kill();
this._process.Dispose();
}
public void Kill()
{
if (!this.HasExited)
{
this._process.Kill();
this._process.WaitForExit();
}
}
public Stream GetStandardInput()
{
return this._stdin;
}
public string[] GetStandardOutput()
{
return this._stdout.GetOutput();
}
public string GetStandardOutputAsString()
{
string[] standardOutput = this.GetStandardOutput();
return Program.GetOutputAsString(standardOutput);
}
public string[] GetErrorOutput()
{
return this._stderr.GetOutput();
}
public string GetErrorOutputAsString()
{
string[] errorOutput = this.GetErrorOutput();
return Program.GetOutputAsString(errorOutput);
}
private static string GetOutputAsString(string[] output)
{
StringBuilder stringBuilder = new StringBuilder();
foreach (string value in output)
{
stringBuilder.AppendLine(value);
}
return stringBuilder.ToString();
}
public void WaitForExit()
{
this._process.WaitForExit();
}
public bool WaitForExit(int milliseconds)
{
return this._process.WaitForExit(milliseconds);
}
private ProcessOutputStreamReader _stdout;
private ProcessOutputStreamReader _stderr;
private Stream _stdin;
public Process _process;
}
}
其中,它把 ProcessInfo 進(jìn)行了包裝很泊,完善了命令行執(zhí)行軟件運行的流程:Start 角虫、Kill 、Dispose 一個都不少委造。
再次戳鹅,它把 標(biāo)準(zhǔn)輸出(StandardOuput) 和 標(biāo)準(zhǔn)錯誤輸出 (StandardOuput)使用了專門封裝的 流讀寫器中,開啟線程進(jìn)行讀取昏兆,提高了性能枫虏,也避免了卡調(diào)用 Process 的線程。
最后亮垫,為了方便調(diào)試模软,實現(xiàn)了非常完善的 命令行參數(shù)調(diào)試輸出伟骨,Process 常規(guī)數(shù)據(jù)和異常輸出 的方法饮潦。
其他
- ProcessOutputStreamReader
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
namespace UnityEditor.Utils
{
internal class ProcessOutputStreamReader
{
internal ProcessOutputStreamReader(Process p, StreamReader stream) : this(() => p.HasExited, stream){}
internal ProcessOutputStreamReader(Func<bool> hostProcessExited, StreamReader stream)
{
this.hostProcessExited = hostProcessExited;
this.stream = stream;
this.lines = new List<string>();
this.thread = new Thread(new ThreadStart(this.ThreadFunc));
this.thread.Start();
}
private void ThreadFunc()
{
if (!this.hostProcessExited())
{
try
{
while (this.stream.BaseStream != null)
{
string text = this.stream.ReadLine();
if (text == null)
{
break;
}
object obj = this.lines;
lock (obj)
{
this.lines.Add(text);
}
}
}
catch (ObjectDisposedException)
{
object obj2 = this.lines;
lock (obj2)
{
this.lines.Add("Could not read output because an ObjectDisposedException was thrown.");
}
}
catch (IOException)
{
}
}
}
internal string[] GetOutput()
{
if (this.hostProcessExited())
{
this.thread.Join();
}
object obj = this.lines;
string[] result;
lock (obj)
{
result = this.lines.ToArray();
}
return result;
}
private readonly Func<bool> hostProcessExited;
private readonly StreamReader stream;
internal List<string> lines;
private Thread thread;
}
}
當(dāng)然了,里面還有很多令人印象深刻的工具類携狭,比如對Unity Mono 庫的查找輔助腳本 MonoInstallationFinder
啦继蜡,對命令行參數(shù)進(jìn)行進(jìn)行修剪/格式化的腳本:CommandLineFormatter
啦。這些都可以直接拿來用逛腿,當(dāng)然也很值得借鑒一哦稀并。
- CommandLineFormatter
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
namespace UnityEditor.Scripting.Compilers
{
internal static class CommandLineFormatter
{
public static string EscapeCharsQuote(string input)
{
string result;
if (input.IndexOf('\'') == -1)
{
result = "'" + input + "'";
}
else if (input.IndexOf('"') == -1)
{
result = "\"" + input + "\"";
}
else
{
result = null;
}
return result;
}
public static string PrepareFileName(string input)
{
input = FileUtil.ResolveSymlinks(input);
string result;
if (Application.platform == RuntimePlatform.WindowsEditor)
{
result = CommandLineFormatter.EscapeCharsWindows(input);
}
else
{
result = CommandLineFormatter.EscapeCharsQuote(input);
}
return result;
}
public static string EscapeCharsWindows(string input)
{
string result;
if (input.Length == 0)
{
result = "\"\"";
}
else if (CommandLineFormatter.UnescapeableChars.IsMatch(input))
{
Debug.LogWarning("Cannot escape control characters in string");
result = "\"\"";
}
else if (CommandLineFormatter.UnsafeCharsWindows.IsMatch(input))
{
result = "\"" + CommandLineFormatter.Quotes.Replace(input, "\"\"") + "\"";
}
else
{
result = input;
}
return result;
}
internal static string GenerateResponseFile(IEnumerable<string> arguments)
{
string uniqueTempPathInProject = FileUtil.GetUniqueTempPathInProject();
using (StreamWriter streamWriter = new StreamWriter(uniqueTempPathInProject))
{
foreach (string value in from a in arguments
where a != null
select a)
{
streamWriter.WriteLine(value);
}
}
return uniqueTempPathInProject;
}
private static readonly Regex UnsafeCharsWindows = new Regex("[^A-Za-z0-9_\\-\\.\\:\\,\\/\\@\\\\]");
private static readonly Regex UnescapeableChars = new Regex("[\\x00-\\x08\\x10-\\x1a\\x1c-\\x1f\\x7f\\xff]");
private static readonly Regex Quotes = new Regex("\"");
}
}
- 上述腳本的使用場景:ManagedProgram
using System;
using System.Diagnostics;
using System.IO;
using UnityEditor.Scripting.Compilers;
using UnityEditor.Utils;
using UnityEngine;
namespace UnityEditor.Scripting
{
internal class ManagedProgram : Program //繼承 Program
{
public ManagedProgram(string monodistribution, string profile, string executable, string arguments, Action<ProcessStartInfo> setupStartInfo) : this(monodistribution, profile, executable, arguments, true, setupStartInfo){}
public ManagedProgram(string monodistribution, string profile, string executable, string arguments, bool setMonoEnvironmentVariables, Action<ProcessStartInfo> setupStartInfo)
{
string text = ManagedProgram.PathCombine(new string[]
{
monodistribution,
"bin",
"mono"
});
if (Application.platform == RuntimePlatform.WindowsEditor)
{
text = CommandLineFormatter.PrepareFileName(text + ".exe"); //裝載 exe 程序路徑
}
ProcessStartInfo processStartInfo = new ProcessStartInfo
{
//配置帶參數(shù)啟動的命令行參數(shù)
Arguments = CommandLineFormatter.PrepareFileName(executable) + " " + arguments,
CreateNoWindow = true,
FileName = text,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = Application.dataPath + "/..", //設(shè)置工作目錄
UseShellExecute = false
};
if (setMonoEnvironmentVariables)
{
string value = ManagedProgram.PathCombine(new string[]
{
monodistribution,
"lib",
"mono",
profile
});
processStartInfo.EnvironmentVariables["MONO_PATH"] = value; //按需設(shè)置環(huán)境變量
processStartInfo.EnvironmentVariables["MONO_CFG_DIR"] = ManagedProgram.PathCombine(new string[]
{
monodistribution,
"etc"
});
}
if (setupStartInfo != null)
{
setupStartInfo(processStartInfo); // 進(jìn)程信息配置完成的回調(diào)
}
this._process.StartInfo = processStartInfo;
}
private static string PathCombine(params string[] parts)
{
string text = parts[0];
for (int i = 1; i < parts.Length; i++)
{
text = Path.Combine(text, parts[i]);
}
return text;
}
}
}
Tips : 更上層的調(diào)用見:**UnityEditor.Scripting.Compilers.ScriptCompilerBase**
雜項
-
可以發(fā)現(xiàn)在 Windows 下Unity 是調(diào)用了 mcs.exe 進(jìn)行的腳本編譯。
- 并不是所有遇到的 API 都能在官方 Manual 找到单默,譬如這個 EditorUtility.CompileCsharp
-
博客中提供的 Demo 現(xiàn)在測試時會報錯 【CS0518 】:
- 經(jīng)過谷歌碘举,找到解決方案:Compiler Error CS0518 | Microsoft Docs
亦即是:添加上對 mscorlib.dll 的引用即可。 -
經(jīng)過追源碼搁廓,這個 EditorUtility.CompileCsharp 中ApiCompatibilityLevel
固化為ApiCompatibilityLevel.NET_2_0_Subset,這一點需要留意引颈,因為筆者目前遇到的情況表明子集缺少一些API,包含但可能不限于部分 IO 和 Regex 相關(guān)的API境蜕。