// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; using UnityEngine; namespace HoloToolkit.Unity { /// <summary> /// Helper class for launching external processes inside of the unity editor. /// </summary> public class ExternalProcess : IDisposable { [DllImport("ExternalProcessAPI", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr ExternalProcessAPI_CreateProcess([MarshalAs(UnmanagedType.LPStr)] string cmdline); [DllImport("ExternalProcessAPI", CallingConvention = CallingConvention.Cdecl)] private static extern bool ExternalProcessAPI_IsRunning(IntPtr handle); [DllImport("ExternalProcessAPI", CallingConvention = CallingConvention.Cdecl)] private static extern void ExternalProcessAPI_SendLine(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string line); [DllImport("ExternalProcessAPI", CallingConvention = CallingConvention.Cdecl)] private static extern IntPtr ExternalProcessAPI_GetLine(IntPtr handle); [DllImport("ExternalProcessAPI", CallingConvention = CallingConvention.Cdecl)] private static extern void ExternalProcessAPI_DestroyProcess(IntPtr handle); [DllImport("ExternalProcessAPI", CallingConvention = CallingConvention.Cdecl)] public static extern void ExternalProcessAPI_ConfirmOrBeginProcess([MarshalAs(UnmanagedType.LPStr)] string processName); private IntPtr mHandle; /// <summary> /// First some static utility functions, used by some other code as well. /// They are related to "external processes" so they appear here. /// </summary> private static string sAppDataPath; public static void Launch(string appName) { // Full or relative paths only. Currently unused. if (!appName.StartsWith(@"\")) { appName += @"\"; } string appPath = AppDataPath + appName; string appDir = Path.GetDirectoryName(appPath); Process pr = new Process(); pr.StartInfo.FileName = appPath; pr.StartInfo.WorkingDirectory = appDir; pr.Start(); } private static string AppDataPath { get { if (string.IsNullOrEmpty(sAppDataPath)) { sAppDataPath = Application.dataPath.Replace("/", @"\"); } return sAppDataPath; } } public static bool FindAndLaunch(string appName) { return FindAndLaunch(appName, null); } public static bool FindAndLaunch(string appName, string args) { // Start at working directory, append appName (should read "appRelativePath"), see if it exists. // If not go up to parent and try again till drive level reached. string appPath = FindPathToExecutable(appName); if (appPath == null) { return false; } string appDir = Path.GetDirectoryName(appPath); Process pr = new Process(); pr.StartInfo.FileName = appPath; pr.StartInfo.WorkingDirectory = appDir; pr.StartInfo.Arguments = args; return pr.Start(); } public static string FindPathToExecutable(string appName) { // Start at working directory, append appName (should read "appRelativePath"), see if it exists. // If not go up to parent and try again till drive level reached. if (!appName.StartsWith(@"\")) { appName = @"\" + appName; } string searchDir = AppDataPath; while (searchDir.Length > 3) { string appPath = searchDir + appName; if (File.Exists(appPath)) { return appPath; } searchDir = Path.GetDirectoryName(searchDir); } return null; } public static string MakeRelativePath(string path1, string path2) { // TBD- doesn't really belong in ExternalProcess. path1 = path1.Replace('\\', '/'); path2 = path2.Replace('\\', '/'); path1 = path1.Replace("\"", ""); path2 = path2.Replace("\"", ""); Uri uri1 = new Uri(path1); Uri uri2 = new Uri(path2); Uri relativePath = uri1.MakeRelativeUri(uri2); return relativePath.OriginalString; } /// <summary> /// The actual ExternalProcess class. /// </summary> /// <param name="appName"></param> /// <returns></returns> public static ExternalProcess CreateExternalProcess(string appName) { return CreateExternalProcess(appName, null); } public static ExternalProcess CreateExternalProcess(string appName, string args) { // Seems like it would be safer and more informative to call this static method and test for null after. try { return new ExternalProcess(appName, args); } catch (Exception ex) { UnityEngine.Debug.LogError("Unable to start process " + appName + ", " + ex.Message + "."); } return null; } private ExternalProcess(string appName, string args) { appName = appName.Replace("/", @"\"); string appPath = appName; if (!File.Exists(appPath)) { appPath = FindPathToExecutable(appName); } if (appPath == null) { throw new ArgumentException("Unable to find app " + appPath); } // This may throw, calling code should catch the exception. string launchString = args == null ? appPath : appPath + " " + args; mHandle = ExternalProcessAPI_CreateProcess(launchString); } ~ExternalProcess() { Dispose(false); } public bool IsRunning() { try { if (mHandle != IntPtr.Zero) { return ExternalProcessAPI_IsRunning(mHandle); } } catch { Terminate(); } return false; } public bool WaitForStart(float seconds) { return WaitFor(seconds, () => { return ExternalProcessAPI_IsRunning(mHandle); }); } public bool WaitForShutdown(float seconds) { return WaitFor(seconds, () => { return !ExternalProcessAPI_IsRunning(mHandle); }); } public bool WaitFor(float seconds, Func<bool> func) { if (seconds <= 0.0f) seconds = 5.0f; float end = Time.realtimeSinceStartup + seconds; bool hasHappened = false; while (Time.realtimeSinceStartup < end) { hasHappened = func(); if (hasHappened) { break; } Thread.Sleep(Math.Min(500, (int)(seconds * 1000))); } return hasHappened; } public void SendLine(string line) { try { if (mHandle != IntPtr.Zero) { ExternalProcessAPI_SendLine(mHandle, line); } } catch { Terminate(); } } public string GetLine() { try { if (mHandle != IntPtr.Zero) { return Marshal.PtrToStringAnsi(ExternalProcessAPI_GetLine(mHandle)); } } catch { Terminate(); } return null; } public void Terminate() { try { if (mHandle != IntPtr.Zero) { ExternalProcessAPI_DestroyProcess(mHandle); } } catch { // TODO: Should we be catching something here? } mHandle = IntPtr.Zero; } // IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { Terminate(); } } }