// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.IO;
using AutomationTool;
using UnrealBuildTool;
using Gauntlet.Utils;
using static AutomationTool.ProcessResult;
using EpicGames.Core;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using UnrealBuildBase;
namespace Gauntlet
{
///
/// Win32/64 implementation of a device to run applications
///
public class TargetDeviceWindows : TargetDeviceDesktopCommon
{
///
/// Optional executable that bootstraps the build
///
[AutoParam]
public string Bootstrap { get; protected set; }
public TargetDeviceWindows(string InName, string InCacheDir)
: base(InName, InCacheDir)
{
Platform = UnrealTargetPlatform.Win64;
RunOptions = CommandUtils.ERunOptions.NoWaitForExit | CommandUtils.ERunOptions.NoLoggingOfRunCommand;
}
public override void InstallBuild(UnrealAppConfig AppConfig)
{
if (AppConfig.Build is IWindowsSelfInstallingBuild SelfInstallingBuild)
{
SelfInstallingBuild.Install(AppConfig);
return;
}
base.InstallBuild(AppConfig);
}
public override IAppInstall CreateAppInstall(UnrealAppConfig AppConfig)
{
IAppInstall Install;
IBuild Build = AppConfig.Build;
switch (AppConfig.Build)
{
case NativeStagedBuild:
Install = CreateNativeStagedInstall(AppConfig, Build as NativeStagedBuild);
break;
case StagedBuild:
Install = CreateStagedInstall(AppConfig, Build as StagedBuild);
break;
case EditorBuild:
Install = CreateEditorInstall(AppConfig, Build as EditorBuild);
break;
case IWindowsSelfInstallingBuild:
Install = CreateSelfInstall(AppConfig, Build as IWindowsSelfInstallingBuild);
break;
default:
throw new AutomationException("{0} is an invalid build type for {1}!", Build.GetType().Name, Platform);
}
return Install;
}
public override IAppInstance Run(IAppInstall App)
{
WindowsAppInstall WinApp = App as WindowsAppInstall;
if (WinApp == null)
{
throw new AutomationException("AppInstance is of incorrect type!");
}
if (File.Exists(WinApp.ExecutablePath) == false)
{
throw new AutomationException("Specified path {0} not found!", WinApp.ExecutablePath);
}
ILongProcessResult Result = null;
lock (Globals.MainLock)
{
string ExePath = Path.GetDirectoryName(WinApp.ExecutablePath);
string NewWorkingDir = string.IsNullOrEmpty(WinApp.WorkingDirectory) ? ExePath : WinApp.WorkingDirectory;
string OldWD = Environment.CurrentDirectory;
Environment.CurrentDirectory = NewWorkingDir;
Log.Info("Launching {0} on {1}", App.Name, ToString());
string CmdLine = WinApp.CommandArguments;
Log.Verbose("\t{0}", CmdLine);
Result = new LongProcessResult(
WinApp.ExecutablePath,
CmdLine,
WinApp.RunOptions,
OutputCallback: WinApp.FilterLoggingDelegate,
WorkingDir: WinApp.WorkingDirectory,
LocalCache: WinApp.Device.LocalCachePath
);
if (Result.HasExited && Result.ExitCode != 0)
{
throw new AutomationException("Failed to launch {0}. Error {1}", WinApp.ExecutablePath, Result.ExitCode);
}
Environment.CurrentDirectory = OldWD;
}
return new WindowsAppInstance(WinApp, Result, WinApp.LogFile);
}
protected override IAppInstall CreateNativeStagedInstall(UnrealAppConfig AppConfig, NativeStagedBuild Build)
{
PopulateDirectoryMappings(Path.Combine(Build.BuildPath, AppConfig.ProjectName));
WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, AppConfig.ProjectName, this)
{
ExecutablePath = Path.Combine(Build.BuildPath, Build.ExecutablePath),
WorkingDirectory = Build.BuildPath
};
WinApp.SetDefaultCommandLineArguments(AppConfig, RunOptions, Build.BuildPath);
return WinApp;
}
protected override IAppInstall CreateStagedInstall(UnrealAppConfig AppConfig, StagedBuild Build)
{
string BuildDir = GetStagedBuildDirectory(AppConfig, Build);
PopulateDirectoryMappings(Path.Combine(BuildDir, AppConfig.ProjectName));
WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, AppConfig.ProjectName, this)
{
ExecutablePath = Path.IsPathRooted(Build.ExecutablePath)
? Build.ExecutablePath
: Path.Combine(BuildDir, Build.ExecutablePath)
};
WinApp.SetDefaultCommandLineArguments(AppConfig, RunOptions, BuildDir);
return WinApp;
}
protected override IAppInstall CreateEditorInstall(UnrealAppConfig AppConfig, EditorBuild Build)
{
PopulateDirectoryMappings(AppConfig.ProjectFile.Directory.FullName);
WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, AppConfig.ProjectName, this)
{
ExecutablePath = Build.ExecutablePath,
WorkingDirectory = Path.GetDirectoryName(Build.ExecutablePath)
};
WinApp.SetDefaultCommandLineArguments(AppConfig, RunOptions, WinApp.WorkingDirectory);
return WinApp;
}
protected IAppInstall CreateSelfInstall(UnrealAppConfig AppConfig, IWindowsSelfInstallingBuild Build)
{
WindowsAppInstall WinApp = Build.CreateAppInstall(this, AppConfig, out string BasePath);
PopulateDirectoryMappings(Path.Combine(BasePath, AppConfig.ProjectName));
return WinApp;
}
#region Legacy Implementations
public override IAppInstall InstallApplication(UnrealAppConfig AppConfig)
{
switch (AppConfig.Build)
{
case NativeStagedBuild:
return InstallNativeStagedBuild(AppConfig, AppConfig.Build as NativeStagedBuild);
case StagedBuild:
return InstallStagedBuild(AppConfig, AppConfig.Build as StagedBuild);
case EditorBuild:
return InstallEditorBuild(AppConfig, AppConfig.Build as EditorBuild);
case IWindowsSelfInstallingBuild:
return InstallSelfInstallingBuild(AppConfig, AppConfig.Build as IWindowsSelfInstallingBuild);
default:
throw new AutomationException("{0} is an invalid build type!", AppConfig.Build.ToString());
}
}
protected void BootstrapInstall(WindowsAppInstall Install)
{
if (string.IsNullOrEmpty(Bootstrap))
{
return;
}
string[] SearchPaths =
[
Path.GetDirectoryName(Install.ExecutablePath),
Path.Combine(Unreal.RootDirectory.FullName, "Engine", "Binaries", Platform.ToString())
];
if (SearchPaths.Select(x => Path.Combine(x, Bootstrap)).FirstOrDefault(File.Exists) is not { } BootstrapCandidate)
{
Log.Error("Failed to find bootstrapper '{0}'", Bootstrap);
return;
}
Install.CommandArguments += " -BootstrapTarget=\"" + Install.ExecutablePath + "\"";
Install.ExecutablePath = BootstrapCandidate;
}
protected override IAppInstall InstallNativeStagedBuild(UnrealAppConfig AppConfig, NativeStagedBuild InBuild)
{
WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, AppConfig.ProjectName, this);
WinApp.WorkingDirectory = InBuild.BuildPath;
WinApp.SetDefaultCommandLineArguments(AppConfig, RunOptions, InBuild.BuildPath);
WinApp.ExecutablePath = Path.Combine(InBuild.BuildPath, InBuild.ExecutablePath);
BootstrapInstall(WinApp);
CopyAdditionalFiles(AppConfig.FilesToCopy);
return WinApp;
}
protected override IAppInstall InstallStagedBuild(UnrealAppConfig AppConfig, StagedBuild InBuild)
{
string BuildDir = InBuild.BuildPath;
if (Utils.SystemHelpers.IsNetworkPath(BuildDir))
{
string SubDir = string.IsNullOrEmpty(AppConfig.Sandbox) ? AppConfig.ProjectName : AppConfig.Sandbox;
string InstallDir = Path.Combine(InstallRoot, SubDir, AppConfig.ProcessType.ToString());
if (!AppConfig.SkipInstall)
{
InstallDir = StagedBuild.InstallBuildParallel(AppConfig, InBuild, BuildDir, InstallDir, ToString());
}
else
{
Log.Info("Skipping install of {0} (-SkipInstall)", BuildDir);
}
BuildDir = InstallDir;
Utils.SystemHelpers.MarkDirectoryForCleanup(InstallDir);
}
WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, AppConfig.ProjectName, this);
WinApp.SetDefaultCommandLineArguments(AppConfig, RunOptions, BuildDir);
if (LocalDirectoryMappings.Count == 0)
{
PopulateDirectoryMappings(Path.Combine(BuildDir, AppConfig.ProjectName));
}
CopyAdditionalFiles(AppConfig.FilesToCopy);
if (Path.IsPathRooted(InBuild.ExecutablePath))
{
WinApp.ExecutablePath = InBuild.ExecutablePath;
}
else
{
// TODO - this check should be at a higher level....
string BinaryPath = Path.Combine(BuildDir, InBuild.ExecutablePath);
// check for a local newer executable
if (Globals.Params.ParseParam("dev") && AppConfig.ProcessType.UsesEditor() == false)
{
string LocalBinary = Path.Combine(Environment.CurrentDirectory, InBuild.ExecutablePath);
bool LocalFileExists = File.Exists(LocalBinary);
bool LocalFileNewer = LocalFileExists && File.GetLastWriteTime(LocalBinary) > File.GetLastWriteTime(BinaryPath);
Log.Verbose("Checking for newer binary at {0}", LocalBinary);
Log.Verbose("LocalFile exists: {0}. Newer: {1}", LocalFileExists, LocalFileNewer);
if (LocalFileExists && LocalFileNewer)
{
// need to -basedir to have our exe load content from the path
WinApp.CommandArguments += $" -basedir=\"{Path.GetDirectoryName(BinaryPath)}\"";
BinaryPath = LocalBinary;
}
}
WinApp.ExecutablePath = BinaryPath;
}
BootstrapInstall(WinApp);
return WinApp;
}
protected override IAppInstall InstallEditorBuild(UnrealAppConfig AppConfig, EditorBuild Build)
{
WindowsAppInstall WinApp = new WindowsAppInstall(AppConfig.Name, AppConfig.ProjectName, this);
WinApp.WorkingDirectory = Path.GetDirectoryName(Build.ExecutablePath);
WinApp.SetDefaultCommandLineArguments(AppConfig, RunOptions, WinApp.WorkingDirectory);
WinApp.ExecutablePath = Build.ExecutablePath;
if (LocalDirectoryMappings.Count == 0)
{
PopulateDirectoryMappings(AppConfig.ProjectFile.Directory.FullName);
}
BootstrapInstall(WinApp);
CopyAdditionalFiles(AppConfig.FilesToCopy);
return WinApp;
}
protected IAppInstall InstallSelfInstallingBuild(UnrealAppConfig AppConfig, IWindowsSelfInstallingBuild Build)
{
Build.Install(AppConfig);
WindowsAppInstall WinApp = Build.CreateAppInstall(this, AppConfig, out string BasePath);
PopulateDirectoryMappings(Path.Combine(BasePath, AppConfig.ProjectName));
BootstrapInstall(WinApp);
CopyAdditionalFiles(AppConfig.FilesToCopy);
return WinApp;
}
#endregion
}
public class WindowsAppInstall : DesktopCommonAppInstall, IAppInstall.IDynamicCommandLine
{
/// Obsolete! Will be removed in a future release.
/// Use 'DesktopDevice' instead
public TargetDeviceWindows WinDevice => DesktopDevice;
public WindowsAppInstall(string InName, string InProjectName, TargetDeviceWindows InDevice)
: base(InName, InProjectName, InDevice)
{ }
}
public class WindowsAppInstance : DesktopCommonAppInstance
{
[DllImport("dbghelp", SetLastError = true)]
private static extern bool MiniDumpWriteDump(SafeHandle hProcess, uint ProcessId, SafeHandle hFile, MINIDUMP_TYPE DumpType, IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam);
private enum MINIDUMP_TYPE
{
MiniDumpNormal = 0x00000000,
MiniDumpWithDataSegs = 0x00000001,
MiniDumpWithFullMemory = 0x00000002,
MiniDumpWithHandleData = 0x00000004,
MiniDumpFilterMemory = 0x00000008,
MiniDumpScanMemory = 0x00000010,
MiniDumpWithUnloadedModules = 0x00000020,
MiniDumpWithIndirectlyReferencedMemory = 0x00000040,
MiniDumpFilterModulePaths = 0x00000080,
MiniDumpWithProcessThreadData = 0x00000100,
MiniDumpWithPrivateReadWriteMemory = 0x00000200,
MiniDumpWithoutOptionalData = 0x00000400,
MiniDumpWithFullMemoryInfo = 0x00000800,
MiniDumpWithThreadInfo = 0x00001000,
MiniDumpWithCodeSegs = 0x00002000,
MiniDumpWithoutAuxiliaryState = 0x00004000,
MiniDumpWithFullAuxiliaryState = 0x00008000,
MiniDumpWithPrivateWriteCopyMemory = 0x00010000,
MiniDumpIgnoreInaccessibleMemory = 0x00020000,
MiniDumpWithTokenInformation = 0x00040000,
MiniDumpWithModuleHeaders = 0x00080000,
MiniDumpFilterTriage = 0x00100000,
MiniDumpWithAvxXStateContext = 0x00200000,
MiniDumpWithIptTrace = 0x00400000,
MiniDumpScanInaccessiblePartialPages = 0x00800000,
MiniDumpFilterWriteCombinedMemory,
MiniDumpValidTypeFlags = 0x01ffffff
}
public WindowsAppInstance(WindowsAppInstall InInstall, ILongProcessResult InProcess, string InProcessLogFile = null)
: base(InInstall, InProcess, InProcessLogFile)
{ }
protected override void GenerateDump()
{
string CrashDumpPath = Path.Combine(Install.DesktopDevice.UserDir, "Saved", "Crashes");
bool WroteDump = false;
if (!Directory.Exists(CrashDumpPath))
{
Directory.CreateDirectory(CrashDumpPath);
}
string DumpName = Path.Combine(CrashDumpPath, Path.GetFileNameWithoutExtension(ProcessResult.ProcessObject.ProcessName) + ".dmp");
using (FileStream CrashDumpStream = File.Create(DumpName))
{
WroteDump = MiniDumpWriteDump(ProcessResult.ProcessObject.SafeHandle, (uint)ProcessResult.ProcessObject.Id,
CrashDumpStream.SafeFileHandle, MINIDUMP_TYPE.MiniDumpNormal, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
if (!WroteDump)
{
Int32 Error = Marshal.GetLastWin32Error();
string ErrorMessage = new Win32Exception(Error).Message;
Log.Error(KnownLogEvents.Gauntlet, "Failed to write minidump. Error: {Error} (GetLastError={LastError})", ErrorMessage, Error);
}
}
if (!WroteDump)
{
File.Delete(DumpName);
}
else
{
Log.Error(KnownLogEvents.Gauntlet_TestEvent, "Wrote minidump to {FileName}", DumpName);
if (CommandUtils.IsBuildMachine)
{
// Produce a stack analysis on buildmachine.
FileReference CDB = GetCDBExe();
if (CDB != null)
{
string Args = $"-z \"{DumpName}\" -c \"!analyze -v -hang; q\"";
string Output = UnrealBuildTool.Utils.RunLocalProcessAndReturnStdOut(CDB.FullName, Args);
List Stack = new List();
Match M = Regex.Match(Output, @"STACK_TEXT:.*", RegexOptions.IgnoreCase);
if (M.Success)
{
Output = Output.Substring(M.Index + M.Length + 1);
foreach (string Line in Output.Split('\n').Select(L => L.Trim()))
{
if (string.IsNullOrEmpty(Line)) break;
// A callstack line looks like this from cdb:
// 000000ef`8877ad20 00007ff8`71c18cde : 00000523`3b3cd580 00007722`b1765a01 00007722`b1765a01 00000216`6e3bfc00 : UnrealEditor_Core!LowLevelTasks::Private::FWaitingQueue::PrepareWait+0x89a
// We are removing the first 115 characters from the each line as they are not going to add meaningful information to the report.
Stack.Add(Line.Substring(Line.Length > 115 ? 115 : 0));
}
}
if (Stack.Any())
{
Log.Error(KnownLogEvents.Gauntlet_TestEvent, "Stack analysis produced with 'cdb {Args}':\n{StackTrace}", Args, string.Join('\n', Stack));
}
else
{
Log.Warning("Could not find stack analysis from cdb command.");
}
}
}
}
}
private static FileReference GetCDBExe()
{
// Trying to look for auto sdk latest WindowsKits debugger tools
DirectoryReference HostAutoSdkDir = null;
if (UEBuildPlatformSDK.TryGetHostPlatformAutoSDKDir(out HostAutoSdkDir))
{
DirectoryReference WindowsKitsDebuggersDirAutoSdk = DirectoryReference.Combine(HostAutoSdkDir, "Win64", "Windows Kits", "Debuggers");
if (DirectoryReference.Exists(WindowsKitsDebuggersDirAutoSdk))
{
FileReference CDBExe64 = FileReference.Combine(WindowsKitsDebuggersDirAutoSdk, "x64", "cdb.exe");
if (FileReference.Exists(CDBExe64))
{
return CDBExe64;
}
}
}
return null;
}
}
public class Win64DeviceFactory : IDeviceFactory
{
public bool CanSupportPlatform(UnrealTargetPlatform? Platform)
{
return Platform == UnrealTargetPlatform.Win64;
}
public ITargetDevice CreateDevice(string InRef, string InCachePath, string InParam = null)
{
return new TargetDeviceWindows(InRef, InCachePath);
}
}
public class WindowsPlatformSupport : TargetPlatformSupportBase
{
public override UnrealTargetPlatform? Platform => UnrealTargetPlatform.Win64;
public override bool IsHostMountingSupported() => true;
public override string GetHostMountedPath(string Path) => Path;
}
}