Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/AutomationUtils/ProcessUtils.cs
Ben Marsh 3dbefdf14d Copying //UE4/Dev-Build to //UE4/Dev-Main (Source: //UE4/Dev-Build @ 3047776)
#lockdown Nick.Penwarden
#rb none

==========================
MAJOR FEATURES + CHANGES
==========================

Change 3021930 on 2016/06/21 by Ben.Marsh

	BuildGraph: Better diagnostic message if the source directory for copies does not exist.

Change 3022391 on 2016/06/21 by Ben.Marsh

	Rework copy task slightly so that all code paths result in files being tagged.

Change 3026592 on 2016/06/24 by Ben.Marsh

	BuildGraph: Add a ForEach element, which will assign a local property to each of a semicolon separated list of values, and expand the elements within it. Added an example in Properties.xml.

Change 3028708 on 2016/06/27 by Matthew.Griffin

	Converting Engine build process to BuildGraph script
	Added Tag Receipts task to retrieve list of build products/dependencies from *.target files.
	Changed Pak File task so that you can specify an existing response file, rather than creating one from the file list.
	Changed base task so that you can resolve filespec from a list of file patterns if you already have them separated, which was the case with wildcards in runtime dependencies.
	Added EngineMajorVersion, EngineMinorVersion and EnginePatchVersion as default properties available to BuildGraph
	Added FinalizeInstalledBuild command to write out InstalledBuild.txt file and config entries for installed platforms
	Included .exe.config and exe.mdb files to build products of CsCompile task if they exist
	Added TagReferences option to CsCompile so that you can get any external references projects have that need to be included when staging
	Added RunOptions parameter to SpawnTask, so that you can specify these for the exe you want to run
	Added missing Runtime Dependency for ICU on Mac

Change 3030209 on 2016/06/28 by Matthew.Griffin

	Renamed EngineBuild.xml to InstalledEngineBuild.xml to make its purpose more clear.
	Removed reference to xcodeunlock.sh from Mac Installed build dependencies as the file itself has been deleted.
	Added myself to list of notifiers for failures in the UE4 Binary build.

Change 3034068 on 2016/06/30 by Ben.Marsh

	BuildGraph: Change scoping rules for properties. Local properties can no longer shadow global properties with the same name (or vice versa), and local properties are always modified in the scope that they were first declared, rather than being re-declared in a narrower scope.

Change 3034070 on 2016/06/30 by Ben.Marsh

	BuildGraph: Warn when referencing a property which is not defined, and add new attributes to the <Property> element to set the default value for a property if it's not already set, and validating that it's one of a list of valid values if it is (eg. <Property Name="WithWin64" Restrict="true;false" Default="false"/>).

Change 3034110 on 2016/06/30 by Matthew.Griffin

	Updated Installed Build so that properties are consistently named Exceptions and that the right versions are used
	Added Filter and Exception properties for each target platform to add any files that can't be figured out via dependencies
	Added Default values for various properties used across Engine build scripts - IsReleaseBranch, IsPreflight, OutputDir, BuildLabel, WithWin64 etc.
	Tagged Generated Includes from each target so that they can be included in Installed Build
	Added additional Android architectures to Shipping build
	Changed SwarmCoordinator to build for Any CPU
	Removed Local HostPlatform property from DDC nodes
	Changed Installed Build target platforms to use Do blocks so that we only have to check With... property once
	Reordered stripping and signing process so that we use the Exception check in less places

Change 3035499 on 2016/07/01 by Ben.Marsh

	BuildGraph: Remove the <Local> element, and just make all <Property> declarations scoped. Also add an error if a property is later declared in a parent scope, since the earlier assignment won't be visible to the later one.

Change 3035520 on 2016/07/01 by Ben.Marsh

	BuildGraph: Add support for <, <=, >, >= operators in condition expressions.

Change 3035666 on 2016/07/01 by Matthew.Griffin

	Added more parameters to Chunk and Label Build tasks
	Updated all remaining uses of Local to Property in Installed Build script
	Made sure Feature Packs use paths compatible with Mac and also changed the node to use a ForEach element

Change 3037020 on 2016/07/04 by Matthew.Griffin

	Ensured that TempStorageFileList uses forward slashes as its path separators so that it's easily used on Mac and Windows
	Was causing the results of the Make Feature Packs node to be tagged using Windows style paths, meaning they would throw an error if you tried to copy them on Mac

Change 3037052 on 2016/07/04 by Ben.Marsh

	Move FJsonValue::ErrorMessage into cpp file, since it depends on the log class defined in Json.h (which includes it).

Change 3037283 on 2016/07/05 by Matthew.Griffin

	Removed EnterScope and LeaveScope from ReadGraphBody so that included files are treated as being in the same scope (allows use of properties across files)

Change 3037547 on 2016/07/05 by Ben.Marsh

	UAT: Allow CommandUtils.Run() to check directories listed in the PATH environment variable for the executable before failing.

Change 3037552 on 2016/07/05 by Ben.Marsh

	BuildGraph: Add an <Unzip> task, which extracts a zip file to an output directory.

Change 3039109 on 2016/07/06 by Matthew.Griffin

	Moved tagging of UAT build products to the Installed Build step as it's the only thing that needs them
	Moved Strip and Sign filters to the filters file, made sure they're used for all operations and added stripping back to UE4Editor nodes
	Changed BuildPatchTool to be built in shipping mode
	Changed all C# projects to be compiled for AnyCPU as they ended up in different output folders otherwise
	Added all files referenced by C# projects to avoid having to filter them manually
	Changed filters to get files included for Linux closer to the old pattern
	Changed Build DDC command to ignore empty entries in FeaturePacks list, don't want to fail the process if a list begins with a ;
	Changed UE4Game to use shipping PhysX libs for Shipping builds
	Added glut32.dll as a Runtime Dependency for PhysX
	Added libsteam_api.so as a Runtime Dependency for Steamworks on Linux

Change 3039676 on 2016/07/06 by Ben.Marsh

	Core: Move definitions for FORCEINLINE'd FMath functions into UnrealMathUtility. Prevents link errors if including one without the other.

Change 3039681 on 2016/07/06 by Ben.Marsh

	Core: Move implementation of GetTypeHash(FTimespan) into CPP file, to remove implicit dependency on the inline implementation of GetTypeHash(int64) being included.

Change 3039735 on 2016/07/06 by Ben.Marsh

	Core: Move USE_DELEGATE_TRYGETBOUNDFUNCTIONNAME into a separate header, so delegate headers can be included separately.

Change 3039878 on 2016/07/06 by Ben.Marsh

	Core: Move FOperatorFunctionID out of TOperatorJumpTable to allow MSVC to compile it and catch errors before the template is instantiated.

Change 3040156 on 2016/07/06 by Ben.Marsh

	Core: Move FDateTime::GetTypeHash() into cpp file to eliminate dependency on TypeHash.h being included before it.

Change 3041009 on 2016/07/07 by Matthew.Griffin

	Changed UE4Game to only use shipping PhysX libraries on Windows

Change 3041015 on 2016/07/07 by Leigh.Swift

	UBT: Support creating C# programs that will be included in the UE4.sln Programs list.
	To have your program listed, remove the sln file that may have been created for you, and add a file named "UE4CSharp.prog" next to your csproj file.

Change 3041234 on 2016/07/07 by Matthew.Griffin

	Added building of Launcher Samples to BuildGraph system
	Added Command to Build Sample projects, which distills to temp directory, builds DDC if needed and then chunks/posts to MCP

Change 3041244 on 2016/07/07 by Ben.Marsh

	Core: Change PlatformIncludes.h to include all the individual PlatformMemory.h, PlatformTime.h, etc... headers rather than including separate per-platform headers which include them all. Makes it much easier to optimize header file usage, and eliminates redundant typedefs in the individual Platform*.h files. Also fixes some headers that previously didn't compile.

Change 3042518 on 2016/07/08 by Matthew.Griffin

	Added content modifiers to those notified about Sample failures
	Throw exception if RocketPromoteBuild tries to promote all samples
	Throw exceptions for missing parameters in BuildLauncherSample command, corrected EngineDir parameter name.

Change 3042545 on 2016/07/08 by Ben.Marsh

	Core: Push/Pop defines for MAX_uint8, MAX_uint16, MAX_uint32, MAX_int32 around Windows.h includes, so we don't need to be careful about the order in which we include NumericLimits.h.

Change 3042546 on 2016/07/08 by Ben.Marsh

	Core: Put standard CRT includes into their own header, so we can include it without taking all of PlatformIncludes.h (and make any platform-specific additions as needed)

Change 3042548 on 2016/07/08 by Ben.Marsh

	Core: Include PlatformCompilerSetup headers from Platform.h, as well as all the defaults for non-platform overriden defines. Allows including Platform.h to get all the basic types, defines and compile environment set up without having to include a large number of system headers or unnecessary functionality.

Change 3044424 on 2016/07/11 by Ben.Marsh

	Merge fixes for QFE installer (CL 3044412) from 4.11 branch.

Change 3044584 on 2016/07/11 by Ben.Marsh

	Core: Move FMath::FormatIntToHumanReadable() to UnrealMath.cpp, since it's a very large/expensive function to try to inline (and introduce a FString dependency for)

Change 3044603 on 2016/07/11 by Matthew.Griffin

	Added PS4 and XboxOne to installed build as options that will always be disabled by default
	Standardised some of the agent names
	Removed logging from the Installed Build nodes as it takes a huge amount of time to write out the list for little reward

Change 3044608 on 2016/07/11 by Ben.Marsh

	Core: Split out definition of SIMD VectorRegister class into its own header, so it's not forcibly included with UnrealMathUtility.

Change 3044638 on 2016/07/11 by Matthew.Griffin

	Added internal build jobs for all games with compile, cook and package nodes.
	Added Documentation, Localization and NonUnity steps.

Change 3045959 on 2016/07/12 by Matthew.Griffin

	Removed Aggregates from Installed Build script as they weren't used/necessary.

Change 3045961 on 2016/07/12 by Matthew.Griffin

	Fixed various issues with Full Build
	Switch to build non-client/server configurations for some games
	Included PS4 and Xbox game targets in our internal monolithics aggregate
	Added Requirements for steps that need UHT, SCW etc.
	Added list of Packaged Game Nodes that we can build up as they're defined
	Added targets that were previously in the Internal Tools nodes
	Changed APIDocTool to build Release as that's what the solution uses and made use of the path created for it
	Removed -clean from the NonUnity targets as that doesn't actually build anything
	Changed mail notifications so that individual nodes are used for content modifiers, not every preceeding node too

Change 3047068 on 2016/07/12 by Ben.Marsh

	BuildGraph: Reduce the amount of log output when compiling a C# project; use /verbosity:minimal and /nolog, as Visual Studio does.

Change 3047298 on 2016/07/12 by Ben.Marsh

	EC: Add a workspace setting specifying that it should be synced incrementally.

Change 3047626 on 2016/07/13 by Matthew.Griffin

	Added PackageToNetwork property, which will default to false, which determines whether to put staged builds on the P: drive or within the LocalBuilds folder of the root dir
	Also changed WorldExplorers to use P:/Builds/Friday instead of WEX, as no one is now clearing up the WEX folder regularly

Change 3047762 on 2016/07/13 by Matthew.Griffin

	Added -nodebuginfo to all compile tasks with -precompile to reduce the size of libs produced
	Added plugin intermediates to list of files excluded from installed build

[CL 3047809 by Ben Marsh in Main branch]
2016-07-13 09:16:28 -04:00

1058 lines
33 KiB
C#

// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Threading;
using System.Diagnostics;
using System.Management;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
namespace AutomationTool
{
public enum CtrlTypes
{
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT,
CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT
}
public interface IProcess
{
void StopProcess(bool KillDescendants = true);
bool HasExited { get; }
string GetProcessName();
}
/// <summary>
/// Tracks all active processes.
/// </summary>
public sealed class ProcessManager
{
public delegate bool CtrlHandlerDelegate(CtrlTypes EventType);
// @todo: Add mono support
[DllImport("Kernel32")]
public static extern bool SetConsoleCtrlHandler(CtrlHandlerDelegate Handler, bool Add);
/// <summary>
/// List of active (running) processes.
/// </summary>
private static List<IProcess> ActiveProcesses = new List<IProcess>();
/// <summary>
/// Synchronization object
/// </summary>
private static object SyncObject = new object();
/// <summary>
/// Creates a new process and adds it to the tracking list.
/// </summary>
/// <returns>New Process objects</returns>
public static ProcessResult CreateProcess(string AppName, bool bAllowSpew, string LogName, Dictionary<string, string> Env = null, UnrealBuildTool.LogEventType SpewVerbosity = UnrealBuildTool.LogEventType.Console)
{
var NewProcess = HostPlatform.Current.CreateProcess(LogName);
if (Env != null)
{
foreach (var EnvPair in Env)
{
if (NewProcess.StartInfo.EnvironmentVariables.ContainsKey(EnvPair.Key))
{
NewProcess.StartInfo.EnvironmentVariables.Remove(EnvPair.Key);
}
if (!String.IsNullOrEmpty(EnvPair.Value))
{
NewProcess.StartInfo.EnvironmentVariables.Add(EnvPair.Key, EnvPair.Value);
}
}
}
var Result = new ProcessResult(AppName, NewProcess, bAllowSpew, LogName, SpewVerbosity: SpewVerbosity);
AddProcess(Result);
return Result;
}
public static void AddProcess(IProcess Proc)
{
lock (SyncObject)
{
ActiveProcesses.Add(Proc);
}
}
public static void RemoveProcess(IProcess Proc)
{
lock (SyncObject)
{
ActiveProcesses.Remove(Proc);
}
}
public static bool CanBeKilled(string ProcessName)
{
return !HostPlatform.Current.DontKillProcessList.Contains(ProcessName, StringComparer.InvariantCultureIgnoreCase);
}
/// <summary>
/// Kills all running processes.
/// </summary>
public static void KillAll()
{
List<IProcess> ProcessesToKill = new List<IProcess>();
lock (SyncObject)
{
foreach (var ProcResult in ActiveProcesses)
{
if (!ProcResult.HasExited)
{
ProcessesToKill.Add(ProcResult);
}
}
ActiveProcesses.Clear();
}
// Remove processes that can't be killed
for (int ProcessIndex = ProcessesToKill.Count - 1; ProcessIndex >= 0; --ProcessIndex )
{
var ProcessName = ProcessesToKill[ProcessIndex].GetProcessName();
if (!String.IsNullOrEmpty(ProcessName) && !CanBeKilled(ProcessName))
{
CommandUtils.LogLog("Ignoring process \"{0}\" because it can't be killed.", ProcessName);
ProcessesToKill.RemoveAt(ProcessIndex);
}
}
if(ProcessesToKill.Count > 0)
{
CommandUtils.LogLog("Trying to kill {0} spawned processes.", ProcessesToKill.Count);
foreach (var Proc in ProcessesToKill)
{
CommandUtils.LogLog(" {0}", Proc.GetProcessName());
}
if (CommandUtils.IsBuildMachine)
{
for (int Cnt = 0; Cnt < 9; Cnt++)
{
bool AllDone = true;
foreach (var Proc in ProcessesToKill)
{
try
{
if (!Proc.HasExited)
{
AllDone = false;
CommandUtils.LogLog("Waiting for process: {0}", Proc.GetProcessName());
}
}
catch (Exception)
{
CommandUtils.LogWarning("Exception Waiting for process");
AllDone = false;
}
}
try
{
if (ProcessResult.HasAnyDescendants(Process.GetCurrentProcess()))
{
AllDone = false;
CommandUtils.Log("Waiting for descendants of main process...");
}
}
catch (Exception Ex)
{
CommandUtils.LogWarning("Exception Waiting for descendants of main process. " + Ex);
AllDone = false;
}
if (AllDone)
{
break;
}
Thread.Sleep(10000);
}
}
foreach (var Proc in ProcessesToKill)
{
var ProcName = Proc.GetProcessName();
try
{
if (!Proc.HasExited)
{
CommandUtils.LogLog("Killing process: {0}", ProcName);
Proc.StopProcess(false);
}
}
catch (Exception Ex)
{
CommandUtils.LogWarning("Exception while trying to kill process {0}:", ProcName);
CommandUtils.LogWarning(LogUtils.FormatException(Ex));
}
}
try
{
if (CommandUtils.IsBuildMachine && ProcessResult.HasAnyDescendants(Process.GetCurrentProcess()))
{
CommandUtils.LogLog("current process still has descendants, trying to kill them...");
ProcessResult.KillAllDescendants(Process.GetCurrentProcess());
}
}
catch (Exception)
{
CommandUtils.LogWarning("Exception killing descendants of main process");
}
}
}
}
#region ProcessResult Helper Class
/// <summary>
/// Class containing the result of process execution.
/// </summary>
public class ProcessResult : IProcess
{
private string Source = "";
private int ProcessExitCode = -1;
private StringBuilder ProcessOutput = new StringBuilder();
private bool AllowSpew = true;
private UnrealBuildTool.LogEventType SpewVerbosity = UnrealBuildTool.LogEventType.Console;
private string AppName = String.Empty;
private Process Proc = null;
private AutoResetEvent OutputWaitHandle = new AutoResetEvent(false);
private AutoResetEvent ErrorWaitHandle = new AutoResetEvent(false);
private object ProcSyncObject;
public ProcessResult(string InAppName, Process InProc, bool bAllowSpew, string LogName, UnrealBuildTool.LogEventType SpewVerbosity = UnrealBuildTool.LogEventType.Console)
{
AppName = InAppName;
ProcSyncObject = new object();
Proc = InProc;
Source = LogName;
AllowSpew = bAllowSpew;
this.SpewVerbosity = SpewVerbosity;
if (Proc != null)
{
Proc.EnableRaisingEvents = false;
}
}
~ProcessResult()
{
if(Proc != null)
{
Proc.Dispose();
}
}
/// <summary>
/// Removes a process from the list of tracked processes.
/// </summary>
public void OnProcessExited()
{
ProcessManager.RemoveProcess(this);
}
/// <summary>
/// Log output of a remote process at a given severity.
/// To pretty up the output, we use a custom source so it will say the source of the process instead of this method name.
/// </summary>
/// <param name="Verbosity"></param>
/// <param name="Message"></param>
[MethodImplAttribute(MethodImplOptions.NoInlining)]
private void LogOutput(UnrealBuildTool.LogEventType Verbosity, string Message)
{
UnrealBuildTool.Log.WriteLine(1, Source, Verbosity, Message);
}
/// <summary>
/// Manually dispose of Proc and set it to null.
/// </summary>
public void DisposeProcess()
{
if(Proc != null)
{
Proc.Dispose();
Proc = null;
}
}
/// <summary>
/// Process.OutputDataReceived event handler.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event args</param>
public void StdOut(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
if (AllowSpew)
{
LogOutput(SpewVerbosity, e.Data);
}
ProcessOutput.Append(e.Data);
ProcessOutput.Append(Environment.NewLine);
}
else
{
OutputWaitHandle.Set();
}
}
/// <summary>
/// Process.ErrorDataReceived event handler.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event args</param>
public void StdErr(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
if (AllowSpew)
{
LogOutput(SpewVerbosity, e.Data);
}
ProcessOutput.Append(e.Data);
ProcessOutput.Append(Environment.NewLine);
}
else
{
ErrorWaitHandle.Set();
}
}
/// <summary>
/// Convenience operator for getting the exit code value.
/// </summary>
/// <param name="Result"></param>
/// <returns>Process exit code.</returns>
public static implicit operator int(ProcessResult Result)
{
return Result.ExitCode;
}
/// <summary>
/// Gets or sets the process exit code.
/// </summary>
public int ExitCode
{
get { return ProcessExitCode; }
set { ProcessExitCode = value; }
}
/// <summary>
/// Gets all std output the process generated.
/// </summary>
public string Output
{
get { return ProcessOutput.ToString(); }
}
public bool HasExited
{
get
{
bool bHasExited = true;
lock (ProcSyncObject)
{
if (Proc != null)
{
bHasExited = Proc.HasExited;
if (bHasExited)
{
ExitCode = Proc.ExitCode;
}
}
}
return bHasExited;
}
}
public Process ProcessObject
{
get { return Proc; }
}
/// <summary>
/// Thread-safe way of getting the process name
/// </summary>
/// <returns>Name of the process this object represents</returns>
public string GetProcessName()
{
string Name = null;
lock (ProcSyncObject)
{
try
{
if (Proc != null && !Proc.HasExited)
{
Name = Proc.ProcessName;
}
}
catch
{
// Ignore all exceptions
}
}
if (String.IsNullOrEmpty(Name))
{
Name = "[EXITED] " + AppName;
}
return Name;
}
/// <summary>
/// Object iterface.
/// </summary>
/// <returns>String representation of this object.</returns>
public override string ToString()
{
return ExitCode.ToString();
}
public void WaitForExit()
{
bool bProcTerminated = false;
bool bStdOutSignalReceived = false;
bool bStdErrSignalReceived = false;
// Make sure the process objeect is valid.
lock (ProcSyncObject)
{
bProcTerminated = (Proc == null);
}
// Keep checking if we got all output messages until the process terminates.
if (!bProcTerminated)
{
// Check messages
int MaxWaitUntilMessagesReceived = 120;
while (MaxWaitUntilMessagesReceived > 0 && !(bStdOutSignalReceived && bStdErrSignalReceived))
{
if (!bStdOutSignalReceived)
{
bStdOutSignalReceived = OutputWaitHandle.WaitOne(500);
}
if (!bStdErrSignalReceived)
{
bStdErrSignalReceived = ErrorWaitHandle.WaitOne(500);
}
// Check if the process terminated
lock (ProcSyncObject)
{
bProcTerminated = (Proc == null) || Proc.HasExited;
}
if (bProcTerminated)
{
// Process terminated but make sure we got all messages, don't wait forever though
MaxWaitUntilMessagesReceived--;
}
}
if (!(bStdOutSignalReceived && bStdErrSignalReceived))
{
CommandUtils.LogLog("Waited for a long time for output of {0}, some output may be missing; we gave up.", AppName);
}
// Double-check if the process terminated
lock (ProcSyncObject)
{
bProcTerminated = (Proc == null) || Proc.HasExited;
}
if (!bProcTerminated)
{
// The process did not terminate yet but we've read all output messages, wait until the process terminates
Proc.WaitForExit();
}
}
}
/// <summary>
/// Finds child processes of the current process.
/// </summary>
/// <param name="ProcessId"></param>
/// <param name="PossiblyRelatedId"></param>
/// <param name="VisitedPids"></param>
/// <returns></returns>
private static bool IsOurDescendant(Process ProcessToKill, int PossiblyRelatedId, HashSet<int> VisitedPids)
{
// check if we're the parent of it or its parent is our descendant
try
{
VisitedPids.Add(PossiblyRelatedId);
Process Parent = null;
using (ManagementObject ManObj = new ManagementObject(string.Format("win32_process.handle='{0}'", PossiblyRelatedId)))
{
ManObj.Get();
int ParentId = Convert.ToInt32(ManObj["ParentProcessId"]);
if (ParentId == 0 || VisitedPids.Contains(ParentId))
{
return false;
}
Parent = Process.GetProcessById(ParentId); // will throw an exception if not spawned by us or not running
}
if (Parent != null)
{
return Parent.Id == ProcessToKill.Id || IsOurDescendant(ProcessToKill, Parent.Id, VisitedPids); // could use ParentId, but left it to make the above var used
}
else
{
return false;
}
}
catch (Exception)
{
// This can happen if the pid is no longer valid which is ok.
return false;
}
}
/// <summary>
/// Kills all child processes of the specified process.
/// </summary>
/// <param name="ProcessId">Process id</param>
public static void KillAllDescendants(Process ProcessToKill)
{
bool bKilledAChild;
do
{
bKilledAChild = false;
// For some reason Process.GetProcesses() sometimes returns the same process twice
// So keep track of all processes we already tried to kill
var KilledPids = new HashSet<int>();
var AllProcs = Process.GetProcesses();
foreach (Process KillCandidate in AllProcs)
{
var VisitedPids = new HashSet<int>();
if (ProcessManager.CanBeKilled(KillCandidate.ProcessName) &&
!KilledPids.Contains(KillCandidate.Id) &&
IsOurDescendant(ProcessToKill, KillCandidate.Id, VisitedPids))
{
KilledPids.Add(KillCandidate.Id);
CommandUtils.LogLog("Trying to kill descendant pid={0}, name={1}", KillCandidate.Id, KillCandidate.ProcessName);
try
{
KillCandidate.Kill();
bKilledAChild = true;
}
catch (Exception Ex)
{
if(!KillCandidate.HasExited)
{
CommandUtils.LogWarning("Failed to kill descendant:");
CommandUtils.LogWarning(LogUtils.FormatException(Ex));
}
}
break; // exit the loop as who knows what else died, so let's get processes anew
}
}
} while (bKilledAChild);
}
/// <summary>
/// returns true if this process has any descendants
/// </summary>
/// <param name="ProcessToCheck">Process to check</param>
public static bool HasAnyDescendants(Process ProcessToCheck)
{
Process[] AllProcs = Process.GetProcesses();
foreach (Process KillCandidate in AllProcs)
{
// Silently skip InvalidOperationExceptions here, because it depends on the process still running. It may have terminated.
string ProcessName;
try
{
ProcessName = KillCandidate.ProcessName;
}
catch(InvalidOperationException)
{
continue;
}
// Check if it's still running
HashSet<int> VisitedPids = new HashSet<int>();
if (ProcessManager.CanBeKilled(ProcessName) && IsOurDescendant(ProcessToCheck, KillCandidate.Id, VisitedPids))
{
CommandUtils.LogLog("Descendant pid={0}, name={1}", KillCandidate.Id, ProcessName);
return true;
}
}
return false;
}
public void StopProcess(bool KillDescendants = true)
{
if (Proc != null)
{
Process ProcToKill = null;
// At this point any call to Proc memebers will result in an exception
// so null the object.
var ProcToKillName = GetProcessName();
lock (ProcSyncObject)
{
ProcToKill = Proc;
Proc = null;
}
// Now actually kill the process and all its descendants if requested
if (KillDescendants)
{
KillAllDescendants(ProcToKill);
}
try
{
ProcToKill.Kill();
ProcToKill.WaitForExit(60000);
if (!ProcToKill.HasExited)
{
CommandUtils.LogLog("Process {0} failed to exit.", ProcToKillName);
}
else
{
CommandUtils.LogLog("Process {0} successfully exited.", ProcToKillName);
OnProcessExited();
}
ProcToKill.Close();
}
catch (Exception Ex)
{
CommandUtils.LogWarning("Exception while trying to kill process {0}:", ProcToKillName);
CommandUtils.LogWarning(LogUtils.FormatException(Ex));
}
}
}
}
#endregion
public partial class CommandUtils
{
#region Statistics
private static Dictionary<string, int> ExeToTimeInMs = new Dictionary<string, int>();
public static void AddRunTime(string Exe, int TimeInMs)
{
string Base = Path.GetFileName(Exe);
if (!ExeToTimeInMs.ContainsKey(Base))
{
ExeToTimeInMs.Add(Base, TimeInMs);
}
else
{
ExeToTimeInMs[Base] += TimeInMs;
}
}
public static void PrintRunTime()
{
foreach (var Item in ExeToTimeInMs)
{
LogVerbose("Run: Total {0}s to run " + Item.Key, Item.Value / 1000);
}
ExeToTimeInMs.Clear();
}
#endregion
[Flags]
public enum ERunOptions
{
None = 0,
AllowSpew = 1 << 0,
AppMustExist = 1 << 1,
NoWaitForExit = 1 << 2,
NoStdOutRedirect = 1 << 3,
NoLoggingOfRunCommand = 1 << 4,
UTF8Output = 1 << 5,
/// When specified with AllowSpew, the output will be TraceEventType.Verbose instead of TraceEventType.Information
SpewIsVerbose = 1 << 6,
/// <summary>
/// If NoLoggingOfRunCommand is set, it normally suppresses the run duration output. This turns it back on.
/// </summary>
LoggingOfRunDuration = 1 << 7,
Default = AllowSpew | AppMustExist,
}
/// <summary>
/// Runs external program.
/// </summary>
/// <param name="App">Program filename.</param>
/// <param name="CommandLine">Commandline</param>
/// <param name="Input">Optional Input for the program (will be provided as stdin)</param>
/// <param name="Options">Defines the options how to run. See ERunOptions.</param>
/// <param name="Env">Environment to pass to program.</param>
/// <returns>Object containing the exit code of the program as well as it's stdout output.</returns>
public static ProcessResult Run(string App, string CommandLine = null, string Input = null, ERunOptions Options = ERunOptions.Default, Dictionary<string, string> Env = null)
{
App = ConvertSeparators(PathSeparator.Default, App);
// Get the log name before allowing the platform to modify the app/command-line. We want mono apps to be written to a log named after the application and appear with the application prefix rather than "mono:".
string LogName = Path.GetFileNameWithoutExtension(App);
HostPlatform.Current.SetupOptionsForRun(ref App, ref Options, ref CommandLine);
if (App == "ectool" || App == "zip" || App == "xcodebuild")
{
Options &= ~ERunOptions.AppMustExist;
}
// Check if the application exists, including the PATH directories.
if (Options.HasFlag(ERunOptions.AppMustExist) && !FileExists(Options.HasFlag(ERunOptions.NoLoggingOfRunCommand) ? true : false, App))
{
bool bExistsInPath = false;
if(!App.Contains(Path.DirectorySeparatorChar) && !App.Contains(Path.AltDirectorySeparatorChar))
{
string[] PathDirectories = Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator);
foreach(string PathDirectory in PathDirectories)
{
string TryApp = Path.Combine(PathDirectory, App);
if(FileExists(Options.HasFlag(ERunOptions.NoLoggingOfRunCommand), TryApp))
{
App = TryApp;
bExistsInPath = true;
break;
}
}
}
if(!bExistsInPath)
{
throw new AutomationException("BUILD FAILED: Couldn't find the executable to Run: {0}", App);
}
}
var StartTime = DateTime.UtcNow;
UnrealBuildTool.LogEventType SpewVerbosity = Options.HasFlag(ERunOptions.SpewIsVerbose) ? UnrealBuildTool.LogEventType.Verbose : UnrealBuildTool.LogEventType.Console;
if (!Options.HasFlag(ERunOptions.NoLoggingOfRunCommand))
{
LogWithVerbosity(SpewVerbosity,"Run: " + App + " " + (String.IsNullOrEmpty(CommandLine) ? "" : CommandLine));
}
ProcessResult Result = ProcessManager.CreateProcess(App, Options.HasFlag(ERunOptions.AllowSpew), LogName, Env, SpewVerbosity:SpewVerbosity);
Process Proc = Result.ProcessObject;
bool bRedirectStdOut = (Options & ERunOptions.NoStdOutRedirect) != ERunOptions.NoStdOutRedirect;
Proc.StartInfo.FileName = App;
Proc.StartInfo.Arguments = String.IsNullOrEmpty(CommandLine) ? "" : CommandLine;
Proc.StartInfo.UseShellExecute = false;
if (bRedirectStdOut)
{
Proc.StartInfo.RedirectStandardOutput = true;
Proc.StartInfo.RedirectStandardError = true;
Proc.OutputDataReceived += Result.StdOut;
Proc.ErrorDataReceived += Result.StdErr;
}
Proc.StartInfo.RedirectStandardInput = Input != null;
Proc.StartInfo.CreateNoWindow = true;
if ((Options & ERunOptions.UTF8Output) == ERunOptions.UTF8Output)
{
Proc.StartInfo.StandardOutputEncoding = new System.Text.UTF8Encoding(false, false);
}
Proc.Start();
if (bRedirectStdOut)
{
Proc.BeginOutputReadLine();
Proc.BeginErrorReadLine();
}
if (String.IsNullOrEmpty(Input) == false)
{
Proc.StandardInput.WriteLine(Input);
Proc.StandardInput.Close();
}
if (!Options.HasFlag(ERunOptions.NoWaitForExit))
{
Result.WaitForExit();
var BuildDuration = (DateTime.UtcNow - StartTime).TotalMilliseconds;
AddRunTime(App, (int)(BuildDuration));
Result.ExitCode = Proc.ExitCode;
if (!Options.HasFlag(ERunOptions.NoLoggingOfRunCommand) || Options.HasFlag(ERunOptions.LoggingOfRunDuration))
{
LogWithVerbosity(SpewVerbosity,"Run: Took {0}s to run {1}, ExitCode={2}", BuildDuration / 1000, Path.GetFileName(App), Result.ExitCode);
}
Result.OnProcessExited();
Result.DisposeProcess();
}
else
{
Result.ExitCode = -1;
}
return Result;
}
/// <summary>
/// Gets a logfile name for a RunAndLog call
/// </summary>
/// <param name="Env">Environment to use.</param>
/// <param name="App">Executable to run</param>
/// <param name="LogName">Name of the logfile ( if null, executable name is used )</param>
/// <returns>The log file name.</returns>
public static string GetRunAndLogOnlyName(CommandEnvironment Env, string App, string LogName = null)
{
if (LogName == null)
{
LogName = Path.GetFileNameWithoutExtension(App);
}
return LogUtils.GetUniqueLogName(CombinePaths(Env.LogFolder, LogName));
}
/// <summary>
/// Runs external program and writes the output to a logfile.
/// </summary>
/// <param name="Env">Environment to use.</param>
/// <param name="App">Executable to run</param>
/// <param name="CommandLine">Commandline to pass on to the executable</param>
/// <param name="LogName">Name of the logfile ( if null, executable name is used )</param>
/// <param name="Input">Optional Input for the program (will be provided as stdin)</param>
/// <param name="Options">Defines the options how to run. See ERunOptions.</param>
public static void RunAndLog(CommandEnvironment Env, string App, string CommandLine, string LogName = null, int MaxSuccessCode = 0, string Input = null, ERunOptions Options = ERunOptions.Default, Dictionary<string, string> EnvVars = null)
{
RunAndLog(App, CommandLine, GetRunAndLogOnlyName(Env, App, LogName), MaxSuccessCode, Input, Options, EnvVars);
}
/// <summary>
/// Exception class for child process commands failing
/// </summary>
public class CommandFailedException : AutomationException
{
public CommandFailedException(string Message) : base(Message)
{
}
public CommandFailedException(ExitCode ExitCode, string Message) : base(ExitCode, Message)
{
}
}
/// <summary>
/// Runs external program and writes the output to a logfile.
/// </summary>
/// <param name="App">Executable to run</param>
/// <param name="CommandLine">Commandline to pass on to the executable</param>
/// <param name="Logfile">Full path to the logfile, where the application output should be written to.</param>
/// <param name="Input">Optional Input for the program (will be provided as stdin)</param>
/// <param name="Options">Defines the options how to run. See ERunOptions.</param>
public static string RunAndLog(string App, string CommandLine, string Logfile = null, int MaxSuccessCode = 0, string Input = null, ERunOptions Options = ERunOptions.Default, Dictionary<string, string> EnvVars = null)
{
ProcessResult Result = Run(App, CommandLine, Input, Options, EnvVars);
if (Result.Output.Length > 0 && Logfile != null)
{
WriteToFile(Logfile, Result.Output);
}
else if (Logfile == null)
{
Logfile = "[No logfile specified]";
}
else
{
Logfile = "[None!, no output produced]";
}
if (Result > MaxSuccessCode || Result < 0)
{
throw new CommandFailedException((ExitCode)Result.ExitCode, String.Format("Command failed (Result:{3}): {0} {1}. See logfile for details: '{2}' ",
App, CommandLine, Path.GetFileName(Logfile), Result.ExitCode));
}
if (Result.Output.Length > 0)
{
return Result.Output;
}
return "";
}
/// <summary>
/// Runs external program and writes the output to a logfile.
/// </summary>
/// <param name="App">Executable to run</param>
/// <param name="CommandLine">Commandline to pass on to the executable</param>
/// <param name="Logfile">Full path to the logfile, where the application output should be written to.</param>
/// <returns>Whether the program executed successfully or not.</returns>
public static string RunAndLog(string App, string CommandLine, out int SuccessCode, string Logfile = null, Dictionary<string, string> EnvVars = null)
{
ProcessResult Result = Run(App, CommandLine, Env: EnvVars);
SuccessCode = Result.ExitCode;
if (Result.Output.Length > 0 && Logfile != null)
{
WriteToFile(Logfile, Result.Output);
}
if (!String.IsNullOrEmpty(Result.Output))
{
return Result.Output;
}
return "";
}
/// <summary>
/// Runs external program and writes the output to a logfile.
/// </summary>
/// <param name="Env">Environment to use.</param>
/// <param name="App">Executable to run</param>
/// <param name="CommandLine">Commandline to pass on to the executable</param>
/// <param name="LogName">Name of the logfile ( if null, executable name is used )</param>
/// <returns>Whether the program executed successfully or not.</returns>
public static string RunAndLog(CommandEnvironment Env, string App, string CommandLine, out int SuccessCode, string LogName = null, Dictionary<string, string> EnvVars = null)
{
return RunAndLog(App, CommandLine, out SuccessCode, GetRunAndLogOnlyName(Env, App, LogName), EnvVars);
}
/// <summary>
/// Runs UAT recursively
/// </summary>
/// <param name="Env">Environment to use.</param>
/// <param name="CommandLine">Commandline to pass on to the executable</param>
public static string RunUAT(CommandEnvironment Env, string CommandLine)
{
// We want to redirect the output from recursive UAT calls into our normal log folder, but prefix everything with a unique identifier. To do so, we set the EnvVarNames.LogFolder environment
// variable to a subfolder of it, then copy its contents into the main folder with a prefix after it's finished. Start by finding a base name we can use to identify the output of this run.
string BaseLogSubdir = "Recur";
if (!String.IsNullOrEmpty(CommandLine))
{
int Space = CommandLine.IndexOf(" ");
if (Space > 0)
{
BaseLogSubdir = BaseLogSubdir + "_" + CommandLine.Substring(0, Space);
}
else if (CommandLine.Contains("-profile"))
{
string PathToProfile = CommandLine.Substring(CommandLine.IndexOf('=') + 1);
BaseLogSubdir = BaseLogSubdir + "_" + (Path.GetFileNameWithoutExtension(PathToProfile));
}
else
{
BaseLogSubdir = BaseLogSubdir + "_" + CommandLine;
}
}
BaseLogSubdir = BaseLogSubdir.Trim();
// Check if there are already log files which start with this prefix, and try to uniquify it if until there aren't.
int Index = 0;
string DirOnlyName = BaseLogSubdir;
string LogSubdir = CombinePaths(CmdEnv.LogFolder, DirOnlyName, "");
while (true)
{
var ExistingFiles = FindFiles(DirOnlyName + "*", false, CmdEnv.LogFolder);
if (ExistingFiles.Length == 0)
{
break;
}
Index++;
if (Index == 1000)
{
throw new AutomationException("Couldn't seem to create a log subdir {0}", LogSubdir);
}
DirOnlyName = String.Format("{0}_{1}_", BaseLogSubdir, Index);
LogSubdir = CombinePaths(CmdEnv.LogFolder, DirOnlyName, "");
}
// Get the stdout log file for this run, and create the subdirectory for all the other log output
string LogFile = CombinePaths(CmdEnv.LogFolder, DirOnlyName + ".log");
LogVerbose("Recursive UAT Run, in log folder {0}, main log file {1}", LogSubdir, LogFile);
CreateDirectory(LogSubdir);
// Run UAT with the log folder redirected through the environment
string App = CmdEnv.UATExe;
Log("Running {0} {1}", App, CommandLine);
var OSEnv = new Dictionary<string, string>();
OSEnv.Add(AutomationTool.EnvVarNames.LogFolder, LogSubdir);
OSEnv.Add("uebp_UATMutexNoWait", "1");
if (!IsBuildMachine)
{
OSEnv.Add(AutomationTool.EnvVarNames.LocalRoot, ""); // if we don't clear this out, it will think it is a build machine; it will rederive everything
}
ProcessResult Result = Run(App, CommandLine, null, ERunOptions.Default, OSEnv);
if (Result.Output.Length > 0)
{
WriteToFile(LogFile, Result.Output);
}
else
{
WriteToFile(LogFile, "[None!, no output produced]");
}
// Copy everything into the main log folder, using the prefix we decided on earlier.
LogVerbose("Flattening log folder {0}", LogSubdir);
var Files = FindFiles("*", true, LogSubdir);
string MyLogFolder = CombinePaths(CmdEnv.LogFolder, "");
foreach (var ThisFile in Files)
{
if (!ThisFile.StartsWith(MyLogFolder, StringComparison.InvariantCultureIgnoreCase))
{
throw new AutomationException("Can't rebase {0} because it doesn't start with {1}", ThisFile, MyLogFolder);
}
string NewFilename = ThisFile.Substring(MyLogFolder.Length).Replace("/", "_").Replace("\\", "_");
NewFilename = CombinePaths(CmdEnv.LogFolder, NewFilename);
if (FileExists_NoExceptions(NewFilename))
{
throw new AutomationException("Destination log file already exists? {0}", NewFilename);
}
CopyFile(ThisFile, NewFilename);
if (!FileExists_NoExceptions(NewFilename))
{
throw new AutomationException("Destination log file could not be copied {0}", NewFilename);
}
DeleteFile_NoExceptions(ThisFile);
}
DeleteDirectory_NoExceptions(LogSubdir);
if (Result != 0)
{
throw new CommandFailedException(String.Format("Recursive UAT Command failed (Result:{3}): {0} {1}. See logfile for details: '{2}' ",
App, CommandLine, Path.GetFileName(LogFile), Result.ExitCode));
}
return LogFile;
}
protected delegate bool ProcessLog(string LogText);
/// <summary>
/// Keeps reading a log file as it's being written to by another process until it exits.
/// </summary>
/// <param name="LogFilename">Name of the log file.</param>
/// <param name="LogProcess">Process that writes to the log file.</param>
/// <param name="OnLogRead">Callback used to process the recently read log contents.</param>
protected static void LogFileReaderProcess(string LogFilename, ProcessResult LogProcess, ProcessLog OnLogRead = null)
{
while (!FileExists(LogFilename) && !LogProcess.HasExited)
{
Log("Waiting for logging process to start...");
Thread.Sleep(2000);
}
Thread.Sleep(1000);
using (FileStream ProcessLog = File.Open(LogFilename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
StreamReader LogReader = new StreamReader(ProcessLog);
bool bKeepReading = true;
// Read until the process has exited.
while (!LogProcess.HasExited && bKeepReading)
{
while (!LogReader.EndOfStream && bKeepReading)
{
string Output = LogReader.ReadToEnd();
if (Output != null && OnLogRead != null)
{
bKeepReading = OnLogRead(Output);
}
}
while (LogReader.EndOfStream && !LogProcess.HasExited && bKeepReading)
{
Thread.Sleep(250);
// Tick the callback so that it can respond to external events
if (OnLogRead != null)
{
bKeepReading = OnLogRead(null);
}
}
}
}
}
}
}