// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; namespace Tools.DotNETCommon.LaunchProcess { /// /// Enum to make the meaning of WaitForExit return code clear /// public enum EWaitResult { /// The task completed in a timely manner Ok, /// The task was abandoned, since it was taking longer than the specified time-out duration TimedOut } /// /// A class to handle spawning and monitoring of a child process /// public class LaunchProcess : IDisposable { /// A callback signature for handling logging. public delegate void CaptureMessageDelegate( string Message ); /// The current callback for logging, or null for quiet operation. CaptureMessageDelegate CaptureMessage = null; /// The process that was launched. private Process LaunchedProcess = null; /// Set to true when the launched process finishes. private bool bIsFinished = false; /// /// Implementing Dispose. /// public void Dispose() { Dispose( true ); GC.SuppressFinalize( this ); } /// /// Disposes the resources. /// /// protected virtual void Dispose( bool Disposing ) { LaunchedProcess.Dispose(); } /// /// The process exit event. /// /// Unused. /// Unused. private void ProcessExit( object Sender, EventArgs Args ) { // Flush and close any pending messages if( CaptureMessage != null ) { LaunchedProcess.CancelOutputRead(); LaunchedProcess.CancelErrorRead(); } LaunchedProcess.EnableRaisingEvents = false; bIsFinished = true; } /// /// Safely invoke the logging callback. /// /// The line of text to pass back to the calling process via the delegate. private void PrintLog( string Message ) { if( CaptureMessage != null ) { CaptureMessage( Message ); } } /// /// The event called for StdOut an StdErr redirections. /// /// Unused. /// The container for the line of text to pass through the system. private void CaptureMessageCallback( object Sender, DataReceivedEventArgs Args ) { PrintLog( Args.Data ); } /// /// Check to see if the currently running child process has finished. /// /// true if the process was successfully spawned and correctly exited. It also returns true if the process failed to spawn. public bool IsFinished() { if( LaunchedProcess != null ) { if( bIsFinished ) { return true; } // Check for timed out return false; } return true; } /// /// Wait for the launched process to exit, and return its exit code. /// /// Number of milliseconds to wait for the process to exit. Default is forever. /// The exit code of the launched process. /// false is returned if the process failed to finish before the requested timeout. public EWaitResult WaitForExit( int Timeout = Int32.MaxValue ) { if( LaunchedProcess != null ) { LaunchedProcess.WaitForExit( Timeout ); if( !bIsFinished ) { // Calling Kill() here seems to be a race condition on the process terminating after WaitForExit (TTP#315685). Catch anything it throws, and make sure the Kill() finishes. try { LaunchedProcess.Kill(); LaunchedProcess.WaitForExit(); } catch (Exception) { } } } return bIsFinished ? EWaitResult.Ok : EWaitResult.TimedOut; } /// /// Construct a class wrapper that spawns a new child process with StdOut and StdErr optionally redirected and captured. /// /// The executable to launch. /// The working directory of the process. If this is null, the current directory is used. /// The log callback function. This can be null for no logging. /// A string array of parameters passed on the command line, and delimited with spaces. /// Any errors are passed back through the capture delegate. The existence of the executable and the working directory are checked before spawning is attempted. /// An exit code of -1 is returned if there was an exception when spawning the process. public LaunchProcess( string Executable, string WorkingDirectory, CaptureMessageDelegate InCaptureMessage, params string[] Parameters ) { CaptureMessage = InCaptureMessage; // Simple check to ensure the executable exists FileInfo Info = new FileInfo( Executable ); if( !Info.Exists ) { PrintLog( "ERROR: Executable does not exist: " + Executable ); bIsFinished = true; return; } // Set the default working directory if necessary if( WorkingDirectory == null ) { WorkingDirectory = Environment.CurrentDirectory; } // Simple check to ensure the working directory exists DirectoryInfo DirInfo = new DirectoryInfo( WorkingDirectory ); if( !DirInfo.Exists ) { PrintLog( "ERROR: Working directory does not exist: " + WorkingDirectory ); bIsFinished = true; return; } // Create a new process to launch LaunchedProcess = new Process(); // Prepare a ProcessStart structure LaunchedProcess.StartInfo.FileName = Info.FullName; LaunchedProcess.StartInfo.Arguments = String.Join( " ", Parameters ); LaunchedProcess.StartInfo.WorkingDirectory = DirInfo.FullName; LaunchedProcess.StartInfo.CreateNoWindow = true; // Need this for the Exited event as well as the output capturing LaunchedProcess.EnableRaisingEvents = true; LaunchedProcess.Exited += new EventHandler(ProcessExit); // Redirect the output. if (CaptureMessage != null) { LaunchedProcess.StartInfo.UseShellExecute = false; LaunchedProcess.StartInfo.RedirectStandardOutput = true; LaunchedProcess.StartInfo.RedirectStandardError = true; LaunchedProcess.OutputDataReceived += new DataReceivedEventHandler(CaptureMessageCallback); LaunchedProcess.ErrorDataReceived += new DataReceivedEventHandler(CaptureMessageCallback); } // Spawn the process - try to start the process, handling thrown exceptions as a failure. try { PrintLog( "Launching: " + LaunchedProcess.StartInfo.FileName + " " + LaunchedProcess.StartInfo.Arguments + " (CWD: " + LaunchedProcess.StartInfo.WorkingDirectory + ")" ); LaunchedProcess.Start(); // Start the output redirection if we have a logging callback if( CaptureMessage != null ) { LaunchedProcess.BeginOutputReadLine(); LaunchedProcess.BeginErrorReadLine(); } } catch( Exception Ex ) { // Clean up should there be any exception LaunchedProcess = null; bIsFinished = true; PrintLog( "ERROR: Failed to launch with exception: " + Ex.Message ); } } } }