2022-02-09 10:41:17 -05:00
// 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 ;
2022-11-12 18:42:11 -05:00
using EpicGames.Core ;
2023-01-24 12:06:09 -05:00
using static AutomationTool . ProcessResult ;
2022-02-09 10:41:17 -05:00
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 ) ;
}
2023-01-23 21:21:51 -05:00
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 ;
}
2022-02-09 10:41:17 -05:00
public virtual void CleanDeviceArtifacts ( )
{
if ( ! string . IsNullOrEmpty ( ArtifactPath ) & & Directory . Exists ( ArtifactPath ) )
{
try
{
2023-01-23 21:21:51 -05:00
Log . Info ( "Clearing device artifacts path {0} for {1}" , ArtifactPath , Device . Name ) ;
2022-02-09 10:41:17 -05:00
Directory . Delete ( ArtifactPath , true ) ;
}
catch ( Exception Ex )
{
2023-01-23 21:21:51 -05:00
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 ) ;
}
2022-02-09 10:41:17 -05:00
}
}
}
}
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 ) ;
}
}
/// <summary>
/// Linux implementation of a device to run applications
/// </summary>
public class TargetDeviceLinux : ITargetDevice
{
public string Name { get ; protected set ; }
protected string UserDir { get ; set ; }
/// <summary>
/// Our mappings of Intended directories to where they actually represent on this platform.
/// </summary>
protected Dictionary < EIntendedBaseCopyDirectory , string > LocalDirectoryMappings { get ; set ; }
public TargetDeviceLinux ( string InName , string InCacheDir )
{
Name = InName ;
LocalCachePath = InCacheDir ;
RunOptions = CommandUtils . ERunOptions . NoWaitForExit | CommandUtils . ERunOptions . NoLoggingOfRunCommand ;
2022-07-21 11:43:40 -04:00
UserDir = Path . Combine ( LocalCachePath , string . Format ( "{0}_UserDir" , Name ) ) ;
2022-02-09 10:41:17 -05:00
LocalDirectoryMappings = new Dictionary < EIntendedBaseCopyDirectory , string > ( ) ;
}
#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 ) ;
2023-01-24 12:06:09 -05:00
Result = CommandUtils . Run ( LinuxApp . ExecutablePath , CmdLine , Options : LinuxApp . RunOptions , SpewFilterCallback : new SpewFilterCallbackType ( delegate ( string M ) { return null ; } ) /* make sure stderr does not spew in the stdout */ ) ;
2022-02-09 10:41:17 -05:00
if ( Result . HasExited & & Result . ExitCode ! = 0 )
{
throw new AutomationException ( "Failed to launch {0}. Error {1}" , LinuxApp . ExecutablePath , Result . ExitCode ) ;
}
Environment . CurrentDirectory = OldWD ;
}
2023-01-24 12:06:09 -05:00
return new LinuxAppInstance ( LinuxApp , Result ) ;
2022-02-09 10:41:17 -05:00
}
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
{
2022-11-12 18:42:11 -05:00
Log . Warning ( KnownLogEvents . Gauntlet_DeviceEvent , "File to copy {File} not found" , FileToCopy ) ;
2022-02-09 10:41:17 -05:00
}
}
}
}
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 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 < EIntendedBaseCopyDirectory , string > 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 ;
}
}
}