// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using AutomationTool; using UnrealBuildTool; using System.Text.RegularExpressions; using EpicGames.Core; using static AutomationTool.ProcessResult; namespace Gauntlet { class LinuxAppInstance : LocalAppProcess { protected LinuxAppInstall Install; public LinuxAppInstance(LinuxAppInstall InInstall, IProcessResult InProcess, string ProcessLogFile = null) : base(InProcess, InInstall.CommandArguments, ProcessLogFile) { Install = InInstall; } public override string ArtifactPath { get { return Install.ArtifactPath; } } public override ITargetDevice Device { get { return Install.Device; } } } class LinuxAppInstall : IAppInstall { public string Name { get; private set; } public string WorkingDirectory; public string ExecutablePath; public string CommandArguments; public string ArtifactPath; public string ProjectName; public TargetDeviceLinux LinuxDevice { get; private set; } public ITargetDevice Device { get { return LinuxDevice; } } public CommandUtils.ERunOptions RunOptions { get; set; } public LinuxAppInstall(string InName, string InProjectName, TargetDeviceLinux InDevice) { Name = InName; ProjectName = InProjectName; LinuxDevice = InDevice; CommandArguments = ""; this.RunOptions = CommandUtils.ERunOptions.NoWaitForExit; } public IAppInstance Run() { return Device.Run(this); } public bool ForceCleanDeviceArtifacts() { DirectoryInfo ClientTempDirInfo = new DirectoryInfo(ArtifactPath) { Attributes = FileAttributes.Normal }; Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "Setting files in device artifacts {0} to have normal attributes (no longer read-only).", ArtifactPath); foreach (FileSystemInfo info in ClientTempDirInfo.GetFileSystemInfos("*", SearchOption.AllDirectories)) { info.Attributes = FileAttributes.Normal; } try { Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "Clearing device artifact path {0} (force)", ArtifactPath); Directory.Delete(ArtifactPath, true); } catch (Exception Ex) { Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "Failed to force delete artifact path {File}. {Exception}", ArtifactPath, Ex.Message); return false; } return true; } public virtual void CleanDeviceArtifacts() { if (!string.IsNullOrEmpty(ArtifactPath) && Directory.Exists(ArtifactPath)) { try { Log.Info("Clearing device artifacts path {0} for {1}", ArtifactPath, Device.Name); Directory.Delete(ArtifactPath, true); } catch (Exception Ex) { Log.Info(KnownLogEvents.Gauntlet_DeviceEvent, "First attempt at clearing artifact path {0} failed - trying again", ArtifactPath); if (!ForceCleanDeviceArtifacts()) { Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "Failed to delete {File}. {Exception}", ArtifactPath, Ex.Message); } } } } } public class LinuxDeviceFactory : IDeviceFactory { public bool CanSupportPlatform(UnrealTargetPlatform? Platform) { return Platform == UnrealTargetPlatform.Linux; } public ITargetDevice CreateDevice(string InRef, string InCachePath, string InParam = null) { return new TargetDeviceLinux(InRef, InCachePath); } } /// /// Linux implementation of a device to run applications /// public class TargetDeviceLinux : ITargetDevice { public string Name { get; protected set; } protected string UserDir { get; set; } /// /// Our mappings of Intended directories to where they actually represent on this platform. /// protected Dictionary LocalDirectoryMappings { get; set; } public TargetDeviceLinux(string InName, string InCacheDir) { Name = InName; LocalCachePath = InCacheDir; RunOptions = CommandUtils.ERunOptions.NoWaitForExit | CommandUtils.ERunOptions.NoLoggingOfRunCommand; UserDir = Path.Combine(LocalCachePath, string.Format("{0}_UserDir", Name)); LocalDirectoryMappings = new Dictionary(); } #region IDisposable Support private bool disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects). } // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. disposedValue = true; } } // This code added to correctly implement the disposable pattern. public void Dispose() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(true); // TODO: uncomment the following line if the finalizer is overridden above. // GC.SuppressFinalize(this); } #endregion public CommandUtils.ERunOptions RunOptions { get; set; } // We care about UserDir in Linux as some of the roles may require files going into user instead of build dir. public void PopulateDirectoryMappings(string BasePath, string UserDir) { LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Build, Path.Combine(BasePath, "Build")); LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Binaries, Path.Combine(BasePath, "Binaries")); LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Config, Path.Combine(BasePath, "Saved", "Config")); LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Content, Path.Combine(BasePath, "Content")); LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Demos, Path.Combine(UserDir, "Saved", "Demos")); LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.PersistentDownloadDir, Path.Combine(BasePath, "Saved", "PersistentDownloadDir")); LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Profiling, Path.Combine(BasePath, "Saved", "Profiling")); LocalDirectoryMappings.Add(EIntendedBaseCopyDirectory.Saved, Path.Combine(BasePath, "Saved")); } public IAppInstance Run(IAppInstall App) { LinuxAppInstall LinuxApp = App as LinuxAppInstall; if (LinuxApp == null) { throw new DeviceException("AppInstance is of incorrect type!"); } if (File.Exists(LinuxApp.ExecutablePath) == false) { throw new DeviceException("Specified path {0} not found!", LinuxApp.ExecutablePath); } IProcessResult Result = null; lock (Globals.MainLock) { string ExePath = Path.GetDirectoryName(LinuxApp.ExecutablePath); string NewWorkingDir = string.IsNullOrEmpty(LinuxApp.WorkingDirectory) ? ExePath : LinuxApp.WorkingDirectory; string OldWD = Environment.CurrentDirectory; Environment.CurrentDirectory = NewWorkingDir; Log.Info("Launching {0} on {1}", App.Name, ToString()); string CmdLine = LinuxApp.CommandArguments; Log.Verbose("\t{0}", CmdLine); bool bAllowSpew = LinuxApp.RunOptions.HasFlag(CommandUtils.ERunOptions.AllowSpew); Result = CommandUtils.Run(LinuxApp.ExecutablePath, CmdLine, Options: LinuxApp.RunOptions, SpewFilterCallback: new SpewFilterCallbackType(delegate(string M) { return bAllowSpew ? M : null; }) /* make sure stderr does not spew in the stdout */, WorkingDir: LinuxApp.WorkingDirectory); if (Result.HasExited && Result.ExitCode != 0) { throw new AutomationException("Failed to launch {0}. Error {1}", LinuxApp.ExecutablePath, Result.ExitCode); } Environment.CurrentDirectory = OldWD; } return new LinuxAppInstance(LinuxApp, Result); } private void CopyAdditionalFiles(UnrealAppConfig AppConfig) { if (AppConfig.FilesToCopy != null) { foreach (UnrealFileToCopy FileToCopy in AppConfig.FilesToCopy) { string PathToCopyTo = Path.Combine(LocalDirectoryMappings[FileToCopy.TargetBaseDirectory], FileToCopy.TargetRelativeLocation); if (File.Exists(FileToCopy.SourceFileLocation)) { FileInfo SrcInfo = new FileInfo(FileToCopy.SourceFileLocation); SrcInfo.IsReadOnly = false; string DirectoryToCopyTo = Path.GetDirectoryName(PathToCopyTo); if (!Directory.Exists(DirectoryToCopyTo)) { Directory.CreateDirectory(DirectoryToCopyTo); } if (File.Exists(PathToCopyTo)) { FileInfo ExistingFile = new FileInfo(PathToCopyTo); ExistingFile.IsReadOnly = false; } SrcInfo.CopyTo(PathToCopyTo, true); Log.Info("Copying {0} to {1}", FileToCopy.SourceFileLocation, PathToCopyTo); } else { Log.Warning(KnownLogEvents.Gauntlet_DeviceEvent, "File to copy {File} not found", FileToCopy); } } } } protected IAppInstall InstallNativeStagedBuild(UnrealAppConfig AppConfig, NativeStagedBuild InBuild) { LinuxAppInstall LinuxApp = new LinuxAppInstall(AppConfig.Name, AppConfig.ProjectName, this); LinuxApp.RunOptions = RunOptions; if (Log.IsVeryVerbose) { LinuxApp.RunOptions |= CommandUtils.ERunOptions.AllowSpew; } LinuxApp.CommandArguments = AppConfig.CommandLine; LinuxApp.ArtifactPath = Path.Combine(InBuild.BuildPath, AppConfig.ProjectName, @"Saved"); LinuxApp.CleanDeviceArtifacts(); CopyAdditionalFiles(AppConfig); LinuxApp.ExecutablePath = Path.Combine(InBuild.BuildPath, InBuild.ExecutablePath); LinuxApp.WorkingDirectory = InBuild.BuildPath; return LinuxApp; } protected IAppInstall InstallStagedBuild(UnrealAppConfig AppConfig, StagedBuild InBuild) { bool SkipDeploy = Globals.Params.ParseParam("SkipDeploy"); string BuildPath = InBuild.BuildPath; if (CanRunFromPath(BuildPath) == false) { string SubDir = string.IsNullOrEmpty(AppConfig.Sandbox) ? AppConfig.ProjectName : AppConfig.Sandbox; string DestPath = Path.Combine(this.LocalCachePath, SubDir, AppConfig.ProcessType.ToString()); if (!SkipDeploy) { DestPath = StagedBuild.InstallBuildParallel(AppConfig, InBuild, BuildPath, DestPath, ToString()); } else { Log.Info("Skipping install of {0} (-skipdeploy)", BuildPath); } Utils.SystemHelpers.MarkDirectoryForCleanup(DestPath); BuildPath = DestPath; } LinuxAppInstall LinuxApp = new LinuxAppInstall(AppConfig.Name, AppConfig.ProjectName, this); LinuxApp.RunOptions = RunOptions; // Set commandline replace any InstallPath arguments with the path we use LinuxApp.CommandArguments = Regex.Replace(AppConfig.CommandLine, @"\$\(InstallPath\)", BuildPath, RegexOptions.IgnoreCase); if (string.IsNullOrEmpty(UserDir) == false) { LinuxApp.CommandArguments += string.Format(" -userdir=\"{0}\"", UserDir); LinuxApp.ArtifactPath = Path.Combine(UserDir, @"Saved"); Utils.SystemHelpers.MarkDirectoryForCleanup(UserDir); } else { // e.g d:\Unreal\GameName\Saved LinuxApp.ArtifactPath = Path.Combine(BuildPath, AppConfig.ProjectName, @"Saved"); } // clear artifact path LinuxApp.CleanDeviceArtifacts(); if (LocalDirectoryMappings.Count == 0) { PopulateDirectoryMappings(Path.Combine(BuildPath, AppConfig.ProjectName), UserDir); } CopyAdditionalFiles(AppConfig); if (Path.IsPathRooted(InBuild.ExecutablePath)) { LinuxApp.ExecutablePath = InBuild.ExecutablePath; } else { // TODO - this check should be at a higher level.... string BinaryPath = Path.Combine(BuildPath, 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 LinuxApp.CommandArguments += string.Format(" -basedir={0}", Path.GetDirectoryName(BinaryPath)); BinaryPath = LocalBinary; } } LinuxApp.ExecutablePath = BinaryPath; } return LinuxApp; } public IAppInstall InstallApplication(UnrealAppConfig AppConfig) { if (AppConfig.Build is NativeStagedBuild) { return InstallNativeStagedBuild(AppConfig, AppConfig.Build as NativeStagedBuild); } else if (AppConfig.Build is StagedBuild) { return InstallStagedBuild(AppConfig, AppConfig.Build as StagedBuild); } EditorBuild EditorBuild = AppConfig.Build as EditorBuild; if (EditorBuild == null) { throw new AutomationException("Invalid build type!"); } LinuxAppInstall LinuxApp = new LinuxAppInstall(AppConfig.Name, AppConfig.ProjectName, this); LinuxApp.WorkingDirectory = Path.GetDirectoryName(EditorBuild.ExecutablePath); LinuxApp.RunOptions = RunOptions; // Force this to stop logs and other artifacts going to different places LinuxApp.CommandArguments = AppConfig.CommandLine + string.Format(" -userdir=\"{0}\"", UserDir); LinuxApp.ArtifactPath = Path.Combine(UserDir, @"Saved"); LinuxApp.ExecutablePath = EditorBuild.ExecutablePath; if (LocalDirectoryMappings.Count == 0) { PopulateDirectoryMappings(AppConfig.ProjectFile.Directory.FullName, AppConfig.ProjectFile.Directory.FullName); } CopyAdditionalFiles(AppConfig); return LinuxApp; } public bool CanRunFromPath(string InPath) { return !Utils.SystemHelpers.IsNetworkPath(InPath); } public UnrealTargetPlatform? Platform { get { return UnrealTargetPlatform.Linux; } } public string LocalCachePath { get; private set; } public bool IsAvailable { get { return true; } } public bool IsConnected { get { return true; } } public bool IsOn { get { return true; } } public bool PowerOn() { return true; } public bool PowerOff() { return true; } public bool Reboot() { return true; } public bool Connect() { return true; } public bool Disconnect(bool bForce = false) { return true; } public override string ToString() { return Name; } public Dictionary GetPlatformDirectoryMappings() { if (LocalDirectoryMappings.Count == 0) { Log.Warning("Platform directory mappings have not been populated for this platform! This should be done within InstallApplication()"); } return LocalDirectoryMappings; } } }