#if UNITY_STANDALONE || UNITY_EDITOR using UnityEngine; namespace Crosstales.Common.Util { #region CTProcess /// Native process class for standalone IL2CPP-builds (mimicking the missing "System.Diagnostics.Process"-class with the most important properties, methods and events). public class CTProcess : System.IDisposable { #region Variables private uint _exitCode = 123456; private CTProcessStartInfo _startInfo = new CTProcessStartInfo(); private static readonly System.Reflection.FieldInfo[] EVENT_FIELDS = typeof(System.Diagnostics.DataReceivedEventArgs).GetFields( System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly); #endregion #region Properties /// Gets the native handle of the associated process. public System.IntPtr Handle { get; private set; } /// Gets the unique identifier for the associated process. public int Id { get; private set; } /// Gets or sets the properties to pass to the Start() method of the Process. public CTProcessStartInfo StartInfo { get => _startInfo; set { if (value != null) _startInfo = value; } } /// Gets a value indicating whether the associated process has been terminated. public bool HasExited { get; private set; } /// Gets the value that the associated process specified when it terminated. public uint ExitCode => _exitCode; /// Gets the time that the associated process was started. public System.DateTime StartTime { get; private set; } /// Gets the time that the associated process exited. public System.DateTime ExitTime { get; private set; } /// Gets a stream used to read the textual output of the application. public System.IO.StreamReader StandardOutput { get; private set; } /// Gets a stream used to read the error output of the application. public System.IO.StreamReader StandardError { get; private set; } /// Gets a value indicating whether the associated process has been busy. public bool isBusy { get; private set; } #endregion #region Events public event System.EventHandler Exited; //#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || ENABLE_IL2CPP public event System.Diagnostics.DataReceivedEventHandler OutputDataReceived; public event System.Diagnostics.DataReceivedEventHandler ErrorDataReceived; //#endif #region Event-trigger methods private void onExited() { if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG) Debug.Log($"onExited: {ExitCode}"); Exited?.Invoke(this, System.EventArgs.Empty); } #endregion #endregion #region Public methods public void BeginOutputReadLine() { //System.Threading.Thread.Sleep(100); new System.Threading.Thread(watchStdOut).Start(); } public void BeginErrorReadLine() { //System.Threading.Thread.Sleep(100); new System.Threading.Thread(watchStdErr).Start(); } /// Starts the process resource that is specified by the parameter containing process start information (for example, the file name of the process to start) and associates the resource with a new Process component.. public void Start(CTProcessStartInfo info) { if (info != null) StartInfo = info; Start(); } #endregion #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN #region Windows #region Variables private System.IntPtr _threadHandle = System.IntPtr.Zero; private const uint INFINITE = 0xffffffff; // Creation flags private const uint CREATE_NO_WINDOW = 0x08000000; #endregion #region Public methods /// Starts (or reuses) the process resource that is specified by the StartInfo property of this Process component and associates it with the component. public void Start() { cleanup(); isBusy = true; HasExited = false; if (StartInfo.UseThread) { new System.Threading.Thread(createProcess).Start(); System.Threading.Thread.Sleep(200); } else { createProcess(); } } /// Immediately stops the associated process. public void Kill() { if (Handle != System.IntPtr.Zero) { uint _exitCode = 99999; //killed NativeMethods.TerminateProcess(Handle, ref _exitCode); Dispose(); } } public void WaitForExit(int milliseconds = 0) { if (milliseconds > 0) { NativeMethods.WaitForSingleObject(Handle, (uint)milliseconds); } else { NativeMethods.WaitForSingleObject(Handle, INFINITE); } } public void Dispose() { if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG) Debug.LogWarning("Dispose called!"); if (Handle != System.IntPtr.Zero) NativeMethods.CloseHandle(Handle); if (_threadHandle != System.IntPtr.Zero) NativeMethods.CloseHandle(_threadHandle); Handle = System.IntPtr.Zero; _threadHandle = System.IntPtr.Zero; Id = 0; isBusy = false; HasExited = true; StandardOutput?.Dispose(); StandardError?.Dispose(); } #endregion #region Private methods private void createProcess() { StartTime = System.DateTime.Now; string app = StartInfo.FileName; string args = StartInfo.Arguments; if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG) Debug.Log($"createProcess: {StartTime}"); //isBusy = true; //HasExited = false; NativeMethods.STARTUPINFOEX startupInfo = new NativeMethods.STARTUPINFOEX(); try { if ((StartInfo.RedirectStandardOutput || StartInfo.RedirectStandardError || StartInfo.UseCmdExecute) && !StartInfo.FileName.CTContains("cmd")) { app = Crosstales.Common.Util.BaseConstants.CMD_WINDOWS_PATH; args = $"/c call \"{StartInfo.FileName}\" {StartInfo.Arguments}"; } if (StartInfo.RedirectStandardOutput) { string tempStdFile = FileHelper.TempFile; args += $" > \"{tempStdFile}\""; if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG) Debug.Log($"tempStdFile: {tempStdFile}"); StandardOutput = new System.IO.StreamReader(new System.IO.FileStream(tempStdFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding); } else { StandardOutput = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding); } if (StartInfo.RedirectStandardError) { string tempErrFile = FileHelper.TempFile; args += $" 2> \"{tempErrFile}\""; if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG) Debug.Log($"tempErrFile: {tempErrFile}"); StandardError = new System.IO.StreamReader(new System.IO.FileStream(tempErrFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding); } else { StandardError = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding); } NativeMethods.SECURITY_ATTRIBUTES pSec = new NativeMethods.SECURITY_ATTRIBUTES(); NativeMethods.SECURITY_ATTRIBUTES tSec = new NativeMethods.SECURITY_ATTRIBUTES(); pSec.nLength = System.Runtime.InteropServices.Marshal.SizeOf(pSec); tSec.nLength = System.Runtime.InteropServices.Marshal.SizeOf(tSec); if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG) Debug.Log($"application: {app}{System.Environment.NewLine}arguments: {args}"); bool retValue = NativeMethods.CreateProcess(app, $" {args}", ref pSec, ref tSec, true, StartInfo.CreateNoWindow ? CREATE_NO_WINDOW : 0x00000000, System.IntPtr.Zero, StartInfo.WorkingDirectory, ref startupInfo, out NativeMethods.PROCESS_INFORMATION processInfo); if (retValue) { Handle = processInfo.hProcess; _threadHandle = processInfo.hThread; Id = processInfo.dwProcessId; WaitForExit(); } else { Debug.LogError($"Could not start process: '{StartInfo.FileName}'{System.Environment.NewLine}Arguments: '{StartInfo.Arguments}'{System.Environment.NewLine}Working dir: '{StartInfo.WorkingDirectory}'{System.Environment.NewLine}Last error: {NativeMethods.GetLastError()}"); } } catch (System.Exception ex) { Debug.LogError($"Process threw an error: {ex}"); Dispose(); } finally { System.Threading.Thread.Sleep(200); //give the streams the chance to write out all events NativeMethods.GetExitCodeProcess(Handle, ref _exitCode); ExitTime = System.DateTime.Now; if (Handle != System.IntPtr.Zero) NativeMethods.CloseHandle(Handle); if (_threadHandle != System.IntPtr.Zero) NativeMethods.CloseHandle(_threadHandle); Handle = System.IntPtr.Zero; _threadHandle = System.IntPtr.Zero; Id = 0; if (!HasExited) onExited(); isBusy = false; HasExited = true; } } private void cleanup() { Kill(); Dispose(); } #endregion #endregion #else #region Unix #region Variables private System.Threading.Thread _worker; #endregion #region Public methods /// Starts (or reuses) the process resource that is specified by the StartInfo property of this Process component and associates it with the component. public void Start() { isBusy = true; HasExited = false; if (StartInfo.UseThread) { _worker = new System.Threading.Thread(() => createProcess()); _worker.Start(); System.Threading.Thread.Sleep(200); } else { createProcess(); } } /// Immediately stops the associated process. public void Kill() { _worker.CTAbort(); Dispose(); } public void WaitForExit(int milliseconds = 0) { //Debug.Log("Not implemented!"); } public void Dispose() { if (BaseConstants.DEV_DEBUG) Debug.LogWarning("Dispose called!"); Id = 0; isBusy = false; HasExited = true; if (StandardOutput != null) StandardOutput.Dispose(); if (StandardError != null) StandardError.Dispose(); } #endregion #region Private methods private void createProcess() { StartTime = System.DateTime.Now; string app = StartInfo.FileName; string args = StartInfo.Arguments; if (BaseConstants.DEV_DEBUG) Debug.LogWarning($"createProcess: {StartTime}"); try { #if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX //#if false if (StartInfo.RedirectStandardOutput) { StandardOutput = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding); } if (StartInfo.RedirectStandardError) { string tempErrFile = FileHelper.TempFile; args += $" 2> \"{tempErrFile}\""; if (BaseConstants.DEV_DEBUG) Debug.Log($"tempErrFile: {tempErrFile}"); StandardError = new System.IO.StreamReader(new System.IO.FileStream(tempErrFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding); } else { StandardError = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding); } string result = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(NativeMethods.RunCommand($"{app} {args}")); if (StartInfo.RedirectStandardOutput && !string.IsNullOrEmpty(result)) { byte[] byteArray = StartInfo.StandardOutputEncoding.GetBytes(result); System.IO.MemoryStream stream = new System.IO.MemoryStream(byteArray); StandardOutput = new System.IO.StreamReader(stream); } _exitCode = 0; #else if (StartInfo.RedirectStandardOutput) { string tempStdFile = FileHelper.TempFile; args += $" > \"{tempStdFile}\""; if (BaseConstants.DEV_DEBUG) Debug.Log($"tempStdFile: {tempStdFile}"); StandardOutput = new System.IO.StreamReader(new System.IO.FileStream(tempStdFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding); } else { StandardOutput = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding); } if (StartInfo.RedirectStandardError) { string tempErrFile = FileHelper.TempFile; args += $" 2> \"{tempErrFile}\""; if (BaseConstants.DEV_DEBUG) Debug.Log($"tempErrFile: {tempErrFile}"); StandardError = new System.IO.StreamReader(new System.IO.FileStream(tempErrFile, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), StartInfo.StandardOutputEncoding); } else { StandardError = new System.IO.StreamReader(new System.IO.MemoryStream(), StartInfo.StandardOutputEncoding); } _exitCode = (uint)NativeMethods.RunCommand($"{app} {args}"); #endif } catch (System.Threading.ThreadAbortException) { //Debug.LogWarning("Process killed!"); _exitCode = 123456; } catch (System.Exception ex) { Debug.LogError($"Process threw an error: {ex}"); _exitCode = 99; } finally { ExitTime = System.DateTime.Now; Id = 0; if (!HasExited) onExited(); isBusy = false; HasExited = true; } } #endregion #endregion #endif private void watchStdOut() { using (System.IO.StreamReader streamReader = StandardOutput) { while (!streamReader.EndOfStream) { string reply = streamReader.ReadLine(); if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG) Debug.Log($"watchStdOut: {reply}"); OutputDataReceived?.Invoke(this, createMockDataReceivedEventArgs(reply)); } } } private void watchStdErr() { using (System.IO.StreamReader streamReader = StandardError) { while (!streamReader.EndOfStream) { string reply = streamReader.ReadLine(); if (Crosstales.Common.Util.BaseConstants.DEV_DEBUG) Debug.Log($"watchStdErr: {reply}"); ErrorDataReceived?.Invoke(this, createMockDataReceivedEventArgs(reply)); } } } private static System.Diagnostics.DataReceivedEventArgs createMockDataReceivedEventArgs(string data) { if (string.IsNullOrEmpty(data)) throw new System.ArgumentException("Data is null or empty.", nameof(data)); System.Diagnostics.DataReceivedEventArgs mockEventArgs = (System.Diagnostics.DataReceivedEventArgs)System.Runtime.Serialization.FormatterServices .GetUninitializedObject(typeof(System.Diagnostics.DataReceivedEventArgs)); if (EVENT_FIELDS.Length > 0) { EVENT_FIELDS[0].SetValue(mockEventArgs, data); } else { Debug.LogError("Could not create 'DataReceivedEventArgs'!"); } return mockEventArgs; } } #endregion #region Native methods /// Native methods (bridge to Windows). internal static class NativeMethods { #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN //#if false #region Windows [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)] [return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)] internal static extern bool CreateProcess( string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, System.IntPtr lpEnvironment, string lpCurrentDirectory, [System.Runtime.InteropServices.In] ref STARTUPINFOEX lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)] [System.Runtime.ConstrainedExecution.ReliabilityContract( System.Runtime.ConstrainedExecution.Consistency.WillNotCorruptState, System.Runtime.ConstrainedExecution.Cer.MayFail)] internal static extern bool CloseHandle(System.IntPtr hObject); [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)] internal static extern bool GetExitCodeProcess(System.IntPtr process, ref uint exitCode); [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)] internal static extern uint WaitForSingleObject(System.IntPtr handle, uint milliseconds); [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)] internal static extern bool TerminateProcess(System.IntPtr hProcess, ref uint exitCode); [System.Runtime.InteropServices.DllImport("Kernel32.dll")] internal static extern uint GetLastError(); [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Unicode)] internal struct STARTUPINFOEX { public STARTUPINFO StartupInfo; public System.IntPtr lpAttributeList; } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Unicode)] internal struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public System.IntPtr lpReserved2; public System.IntPtr hStdInput; public System.IntPtr hStdOutput; public System.IntPtr hStdError; } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] internal struct PROCESS_INFORMATION { public System.IntPtr hProcess; public System.IntPtr hThread; public int dwProcessId; public int dwThreadId; } [System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)] internal struct SECURITY_ATTRIBUTES { public int nLength; public System.IntPtr lpSecurityDescriptor; public int bInheritHandle; } #endregion #elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX //#elif false #region macOS [System.Runtime.InteropServices.DllImport("ProcessStart")] internal static extern System.IntPtr RunCommand(string command); #endregion #elif UNITY_STANDALONE_LINUX || UNITY_EDITOR_LINUX //#elif true #region Linux [System.Runtime.InteropServices.DllImport("libProcessStart")] internal static extern int RunCommand(string command); #endregion #endif } #endregion #region CTProcessStartInfo /// Specifies a set of values that are used when you start a process (mimicking the "System.Diagnostics.ProcessStartInfo"-class with the most important properties). public class CTProcessStartInfo { public CTProcessStartInfo() { StandardErrorEncoding = StandardOutputEncoding = System.Text.Encoding.UTF8; UseThread = true; } /// Gets or sets the application to be threaded. public bool UseThread { get; set; } /// Gets or sets the application to be started in cmd (command prompt). public bool UseCmdExecute { get; set; } /// Gets or sets the application or document to start. public string FileName { get; set; } /// Gets or sets the set of command-line arguments to use when starting the application. public string Arguments { get; set; } /// Gets or sets a value indicating whether to start the process in a new window. public bool CreateNoWindow { get; set; } /// Gets or sets the working directory for the process to be started. public string WorkingDirectory { get; set; } /// Gets or sets a value that indicates whether the textual output of an application is written to the StandardOutput stream. public bool RedirectStandardOutput { get; set; } /// Gets or sets a value that indicates whether the error output of an application is written to the StandardError stream. public bool RedirectStandardError { get; set; } /// Gets or sets the preferred encoding for standard output (UTF8 per default). public System.Text.Encoding StandardOutputEncoding { get; set; } /// Gets or sets the preferred encoding for error output (UTF8 per default). public System.Text.Encoding StandardErrorEncoding { get; set; } /// Gets or sets a value indicating whether to use the operating system shell to start the process (ignored, always false). public bool UseShellExecute { get; set; } } #endregion } #endif // © 2019-2023 crosstales LLC (https://www.crosstales.com)