// Copyright 1998-2016 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();
// Protect against multiple calls
CaptureMessage = null;
}
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 TimeoutMilliseconds = Int32.MaxValue )
{
if( LaunchedProcess != null )
{
LaunchedProcess.WaitForExit(TimeoutMilliseconds);
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 );
}
}
}
}