You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
542 lines
16 KiB
C#
542 lines
16 KiB
C#
// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using RPCUtility;
|
|
using System.Net;
|
|
using System.Net.NetworkInformation;
|
|
using System.Reflection;
|
|
using System.Runtime.Remoting;
|
|
using System.Threading;
|
|
using System.Net.Sockets;
|
|
using Ionic.Zip;
|
|
|
|
namespace UnrealBuildTool
|
|
{
|
|
public class RPCUtilHelper
|
|
{
|
|
/** The Mac we are compiling on */
|
|
private static string MacName;
|
|
|
|
/** A socket per command thread */
|
|
private static Hashtable CommandThreadSockets = new Hashtable();
|
|
|
|
/** Time difference between remote and local idea's of UTC time */
|
|
private static TimeSpan TimeDifferenceFromRemote = new TimeSpan(0);
|
|
|
|
/** The number of commands the remote side should be able to run at once */
|
|
private static int MaxRemoteCommandsAllowed = 0;
|
|
|
|
static RPCUtilHelper()
|
|
{
|
|
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
|
|
}
|
|
|
|
/**
|
|
* A callback function to find RPCUtility.exe
|
|
*/
|
|
static Assembly CurrentDomain_AssemblyResolve(Object sender, ResolveEventArgs args)
|
|
{
|
|
// Name is fully qualified assembly definition - e.g. "p4dn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ff968dc1933aba6f"
|
|
string[] AssemblyInfo = args.Name.Split(",".ToCharArray());
|
|
string AssemblyName = AssemblyInfo[0];
|
|
|
|
if (AssemblyName.ToLowerInvariant() == "rpcutility")
|
|
{
|
|
AssemblyName = Path.GetFullPath(@"..\Binaries\DotNET\RPCUtility.exe");
|
|
Debug.WriteLineIf(System.Diagnostics.Debugger.IsAttached, "Loading assembly: " + AssemblyName);
|
|
|
|
if (File.Exists(AssemblyName))
|
|
{
|
|
Assembly A = Assembly.LoadFile(AssemblyName);
|
|
return A;
|
|
}
|
|
}
|
|
else if (AssemblyName.ToLowerInvariant() == "ionic.zip.reduced")
|
|
{
|
|
AssemblyName = Path.GetFullPath(@"..\Binaries\DotNET\" + AssemblyName + ".dll");
|
|
|
|
Debug.WriteLineIf(System.Diagnostics.Debugger.IsAttached, "Loading assembly: " + AssemblyName);
|
|
|
|
if (File.Exists(AssemblyName))
|
|
{
|
|
Assembly A = Assembly.LoadFile(AssemblyName);
|
|
return A;
|
|
}
|
|
}
|
|
|
|
return (null);
|
|
}
|
|
|
|
private static DateTime RemoteToLocalTime(string RemoteTime)
|
|
{
|
|
try
|
|
{
|
|
// convert string to integer
|
|
int RemoteTimeSinceEpoch = int.Parse(RemoteTime);
|
|
|
|
// convert the seconds into TimeSpan
|
|
TimeSpan RemoteSpanSinceEpoch = new TimeSpan(0, 0, RemoteTimeSinceEpoch);
|
|
|
|
// put the remote time into local time (Jan 1, 1970 was the Epoch for Mac), and adjust it for time difference between machines
|
|
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) + RemoteSpanSinceEpoch + TimeDifferenceFromRemote;
|
|
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// use MinValue for any errors
|
|
return DateTime.MinValue;
|
|
}
|
|
}
|
|
|
|
static public int Initialize(string InMacName)
|
|
{
|
|
MacName = InMacName;
|
|
|
|
// when not using RPCUtil, we do NOT want to ping the host
|
|
if (!RemoteToolChain.bUseRPCUtil || CommandHelper.PingRemoteHost(MacName))
|
|
{
|
|
try
|
|
{
|
|
if (!RemoteToolChain.bUseRPCUtil)
|
|
{
|
|
// make sure we have SSH setup
|
|
if (RemoteToolChain.ResolvedSSHAuthentication == null)
|
|
{
|
|
Log.TraceError("SSH authentication required a key, but one was not found. Use Editor to setup remote authentication!");
|
|
return 100;
|
|
}
|
|
// ask for current time, free memory and num CPUs
|
|
string[] Commands = new string[]
|
|
{
|
|
"+\"%s\"",
|
|
"sysctl -a | grep hw.memsize | awk '{print $2}'",
|
|
"sysctl -a | grep hw.logicalcpu: | awk '{print $2}'",
|
|
};
|
|
Hashtable Results = Command("/", "date", string.Join(" && ", Commands), null);
|
|
if ((Int64)Results["ExitCode"] != 0)
|
|
{
|
|
Log.TraceError("Failed to run init commands on {0}. Output = {1}", MacName, Results["CommandOutput"]);
|
|
return 101;
|
|
}
|
|
|
|
string[] Lines = ((string)Results["CommandOutput"]).Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
// convert it to a time, with TimeDifferenceFromRemote as 0
|
|
DateTime RemoteTimebase = RemoteToLocalTime(Lines[0]);
|
|
if (RemoteTimebase == DateTime.MinValue)
|
|
{
|
|
throw new BuildException("Failed to parse remote time on " + MacName);
|
|
}
|
|
|
|
// calculate the difference
|
|
TimeDifferenceFromRemote = DateTime.UtcNow - RemoteTimebase;
|
|
|
|
// now figure out max number of commands to run at once
|
|
// int PageSize = int.Parse(Lines[1]);
|
|
Int64 AvailableMem = Int64.Parse(Lines[1].Replace(".", ""));// *PageSize;
|
|
int NumProcesses = (int)Math.Max(1, AvailableMem / (RemoteToolChain.MemoryPerCompileMB * 1024 * 1024));
|
|
|
|
// now, combine that with actual number of cores
|
|
int NumCores = int.Parse(Lines[2]);
|
|
MaxRemoteCommandsAllowed = Math.Min(NumProcesses, NumCores);
|
|
|
|
Console.WriteLine("Remote time is {0}, difference is {1}", RemoteTimebase.ToString(), TimeDifferenceFromRemote.ToString());
|
|
}
|
|
|
|
if (BuildConfiguration.bFlushBuildDirOnRemoteMac)
|
|
{
|
|
Command("/", "rm", "-rf /UE4/Builds/" + Environment.MachineName, null);
|
|
}
|
|
}
|
|
catch(Exception Ex)
|
|
{
|
|
Log.TraceVerbose("SSH Initialize exception {0}", Ex.ToString());
|
|
Log.TraceError("Failed to run init commands on {0}", MacName);
|
|
return 101;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.TraceError("Failed to ping Mac named {0}", MacName);
|
|
return 102;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Handle a thread ending
|
|
*/
|
|
public static void OnThreadComplete()
|
|
{
|
|
lock (CommandThreadSockets)
|
|
{
|
|
// close and remove the socket
|
|
Socket ThreadSocket = CommandThreadSockets[Thread.CurrentThread] as Socket;
|
|
if (ThreadSocket != null)
|
|
{
|
|
ThreadSocket.Close();
|
|
}
|
|
CommandThreadSockets.Remove(Thread.CurrentThread);
|
|
}
|
|
}
|
|
|
|
private static Socket GetSocket()
|
|
{
|
|
Socket ThreadSocket = null;
|
|
|
|
lock (CommandThreadSockets)
|
|
{
|
|
ThreadSocket = CommandThreadSockets[Thread.CurrentThread] as Socket;
|
|
if (ThreadSocket == null)
|
|
{
|
|
try
|
|
{
|
|
ThreadSocket = RPCUtility.CommandHelper.ConnectToUnrealRemoteTool(MacName);
|
|
}
|
|
catch (Exception Ex)
|
|
{
|
|
Log.TraceInformation("Failed to connect to UnrealRemoteTool running on {0}.", MacName);
|
|
throw new BuildException(Ex, "Failed to connect to UnrealRemoteTool running on {0}.", MacName);
|
|
}
|
|
CommandThreadSockets[Thread.CurrentThread] = ThreadSocket;
|
|
}
|
|
}
|
|
|
|
return ThreadSocket;
|
|
}
|
|
|
|
/**
|
|
* This function should be used as the ActionHandler delegate method for Actions that
|
|
* need to run over RPCUtility. It will block until the remote command completes
|
|
*/
|
|
static public void RPCActionHandler(Action Action, out int ExitCode, out string Output)
|
|
{
|
|
Hashtable Results = RPCUtilHelper.Command(Action.WorkingDirectory, Action.CommandPath, Action.CommandArguments,
|
|
Action.ProducedItems.Count > 0 ? Action.ProducedItems[0].AbsolutePath : null);
|
|
if (Results == null)
|
|
{
|
|
ExitCode = -1;
|
|
Output = null;
|
|
Log.TraceInformation("Command failed to execute! {0} {1}", Action.CommandPath, Action.CommandArguments);
|
|
}
|
|
else
|
|
{
|
|
// capture the exit code
|
|
if (Results["ExitCode"] != null)
|
|
{
|
|
ExitCode = (int)(Int64)Results["ExitCode"];
|
|
}
|
|
else
|
|
{
|
|
ExitCode = 0;
|
|
}
|
|
|
|
// pass back the string
|
|
Output = Results["CommandOutput"] as string;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return the modification time on the remote machine, accounting for rough difference in time between the two machines
|
|
*/
|
|
public static bool GetRemoteFileInfo(string RemotePath, out DateTime ModificationTime, out long Length)
|
|
{
|
|
if (RemoteToolChain.bUseRPCUtil)
|
|
{
|
|
return RPCUtility.CommandHelper.GetFileInfo(GetSocket(), RemotePath, DateTime.UtcNow, out ModificationTime, out Length);
|
|
}
|
|
else
|
|
{
|
|
string CommandArgs = string.Format("-c 'if [ -e \"{0}\" ]; then eval $(stat -s \"{0}\") && echo $st_mtime,$st_size; fi'", RemotePath);
|
|
Hashtable Results = Command("/", "bash", CommandArgs, null);
|
|
|
|
string Output = Results["CommandOutput"] as string;
|
|
string[] Tokens = Output.Split(",".ToCharArray());
|
|
if (Tokens.Length == 2)
|
|
{
|
|
ModificationTime = RemoteToLocalTime(Tokens[0]);
|
|
Length = long.Parse(Tokens[1]);
|
|
return true;
|
|
}
|
|
|
|
// any failures will fall through to here
|
|
ModificationTime = DateTime.MinValue;
|
|
Length = 0;
|
|
return false;
|
|
|
|
}
|
|
}
|
|
|
|
public static void MakeDirectory(string Directory)
|
|
{
|
|
if (RemoteToolChain.bUseRPCUtil)
|
|
{
|
|
RPCUtility.CommandHelper.MakeDirectory(GetSocket(), Directory);
|
|
}
|
|
else
|
|
{
|
|
Command("/", "bash", "-c 'mkdir \"" + Directory + "\"'", null);
|
|
}
|
|
}
|
|
|
|
[Flags]
|
|
public enum ECopyOptions
|
|
{
|
|
None = 0,
|
|
IsUpload = 1 << 0,
|
|
DoNotReplace = 1 << 1, // if used, will merge a directory
|
|
DoNotUnpack = 1 << 2
|
|
}
|
|
|
|
public static void CopyFile(string Source, string Dest, bool bIsUpload)
|
|
{
|
|
if (RemoteToolChain.bUseRPCUtil)
|
|
{
|
|
if (bIsUpload)
|
|
{
|
|
RPCUtility.CommandHelper.RPCUpload(GetSocket(), Source, Dest);
|
|
}
|
|
else
|
|
{
|
|
RPCUtility.CommandHelper.RPCDownload(GetSocket(), Source, Dest);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bIsUpload)
|
|
{
|
|
RemoteToolChain.UploadFile(Source, Dest);
|
|
}
|
|
else
|
|
{
|
|
RemoteToolChain.DownloadFile(Source, Dest);
|
|
}
|
|
}
|
|
}
|
|
|
|
// @todo: use temp, random names for zip files
|
|
public static void CopyDirectory(string Source, string Dest, ECopyOptions Options)
|
|
{
|
|
string SourceDirName = Path.GetFileName(Source);
|
|
string DestDirName = Path.GetFileName(Dest);
|
|
|
|
if (Options.HasFlag(ECopyOptions.IsUpload))
|
|
{
|
|
if (!Directory.Exists(Source))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Zip source directory
|
|
string SourceZipPath = Path.Combine(Path.GetFullPath(Path.GetDirectoryName(Source)), SourceDirName + ".zip");
|
|
File.Delete(SourceZipPath);
|
|
ZipFile Zip = new ZipFile(SourceZipPath);
|
|
Zip.CompressionLevel = Ionic.Zlib.CompressionLevel.Level9;
|
|
Zip.BufferSize = 0x10000;
|
|
Zip.AddDirectory(Source, DestDirName);
|
|
Zip.Save();
|
|
|
|
// Upload the zip file
|
|
string DestWorkingDir = Path.GetDirectoryName(Dest).Replace("\\", "/");
|
|
string DestZipName = DestDirName + ".zip";
|
|
CopyFile(SourceZipPath, DestWorkingDir + "/" + DestZipName, true);
|
|
|
|
if (!Options.HasFlag(ECopyOptions.DoNotReplace))
|
|
{
|
|
Command(DestWorkingDir, "rm -rf \"" + DestDirName + "\"", "", null);
|
|
}
|
|
|
|
if (!Options.HasFlag(ECopyOptions.DoNotUnpack))
|
|
{
|
|
// Unpack, if requested
|
|
Command(DestWorkingDir, "unzip \"" + DestZipName + "\"", "", null);
|
|
Command(DestWorkingDir, "rm \"" + DestZipName + "\"", "", null);
|
|
}
|
|
|
|
File.Delete(SourceZipPath);
|
|
}
|
|
else
|
|
{
|
|
// Zip source directory
|
|
string SourceWorkingDir = Path.GetDirectoryName(Source).Replace("\\", "/");
|
|
string ZipCommand = "zip -0 -r -y -T " + SourceDirName + ".zip " + SourceDirName;
|
|
Command(SourceWorkingDir, ZipCommand, "", null);
|
|
|
|
// Download the zip file
|
|
string SourceZipPath = Path.Combine(Path.GetDirectoryName(Source), SourceDirName + ".zip").Replace("\\", "/");
|
|
string DestZipPath = Path.Combine(Path.GetFullPath(Path.GetDirectoryName(Dest)), DestDirName + ".zip");
|
|
CopyFile(SourceZipPath, DestZipPath, false);
|
|
|
|
if (!Options.HasFlag(ECopyOptions.DoNotReplace) && Directory.Exists(Dest))
|
|
{
|
|
Directory.GetFiles(Dest, "*", SearchOption.AllDirectories).ToList().ForEach(Entry => { File.SetAttributes(Entry, FileAttributes.Normal); });
|
|
Directory.Delete(Dest, true);
|
|
}
|
|
|
|
if (!Options.HasFlag(ECopyOptions.DoNotUnpack))
|
|
{
|
|
// Unpack, if requested
|
|
using (ZipFile Zip = ZipFile.Read(DestZipPath))
|
|
{
|
|
Zip.ToList().ForEach(Entry =>
|
|
{
|
|
Entry.FileName = DestDirName + Entry.FileName.Substring(SourceDirName.Length);
|
|
Entry.Extract(Path.GetDirectoryName(Dest), ExtractExistingFileAction.OverwriteSilently);
|
|
});
|
|
}
|
|
|
|
File.Delete(DestZipPath);
|
|
}
|
|
|
|
Command(SourceWorkingDir, "rm \"" + SourceDirName + ".zip\"", "", null);
|
|
}
|
|
}
|
|
|
|
public static void BatchUpload(string[] Commands)
|
|
{
|
|
// batch upload
|
|
RPCUtility.CommandHelper.RPCBatchUpload(GetSocket(), Commands);
|
|
}
|
|
|
|
public static void BatchFileInfo(FileItem[] Files)
|
|
{
|
|
if (RemoteToolChain.bUseRPCUtil)
|
|
{
|
|
// build a list of file paths to get info about
|
|
StringBuilder FileList = new StringBuilder();
|
|
foreach (FileItem File in Files)
|
|
{
|
|
FileList.AppendFormat("{0}\n", File.AbsolutePath);
|
|
}
|
|
|
|
DateTime Now = DateTime.Now;
|
|
|
|
// execute the command!
|
|
Int64[] FileSizeAndDates = RPCUtility.CommandHelper.RPCBatchFileInfo(GetSocket(), FileList.ToString());
|
|
|
|
Console.WriteLine("BatchFileInfo version 1 took {0}", (DateTime.Now - Now).ToString());
|
|
|
|
// now update the source times
|
|
for (int Index = 0; Index < Files.Length; Index++)
|
|
{
|
|
Files[Index].Length = FileSizeAndDates[Index * 2 + 0];
|
|
Files[Index].LastWriteTime = new DateTimeOffset(RPCUtility.CommandHelper.FromRemoteTime(FileSizeAndDates[Index * 2 + 1]));
|
|
Files[Index].bExists = FileSizeAndDates[Index * 2 + 0] >= 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// build a list of file paths to get info about
|
|
StringBuilder Commands = new StringBuilder();
|
|
Commands.Append("#!/bin/bash\n");
|
|
foreach (FileItem File in Files)
|
|
{
|
|
Commands.AppendFormat("if [ -e \"{0}\" ]; then eval $(stat -s \"{0}\") && echo $st_mtime && echo $st_size; else echo 0 && echo -1; fi\n", File.AbsolutePath);
|
|
}
|
|
|
|
// write out locally
|
|
string LocalCommandsFile = Path.GetTempFileName();
|
|
System.IO.File.WriteAllText(LocalCommandsFile, Commands.ToString());
|
|
|
|
string RemoteDir = "/var/tmp/" + Environment.MachineName;
|
|
string RemoteCommandsFile = Path.GetFileName(LocalCommandsFile) + ".sh";
|
|
|
|
DateTime Now = DateTime.Now;
|
|
RemoteToolChain.UploadFile(LocalCommandsFile, RemoteDir + "/" + RemoteCommandsFile);
|
|
|
|
// execute the file, not a commandline
|
|
Hashtable Results = Command(RemoteDir, "sh", RemoteCommandsFile + " && rm " + RemoteCommandsFile, null);
|
|
|
|
Console.WriteLine("BatchFileInfo took {0}", (DateTime.Now - Now).ToString());
|
|
|
|
string[] Lines = ((string)Results["CommandOutput"]).Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
|
if (Lines.Length != Files.Length * 2)
|
|
{
|
|
throw new BuildException("Received the wrong number of results from BatchFileInfo");
|
|
}
|
|
|
|
for (int Index = 0; Index < Files.Length; Index++)
|
|
{
|
|
Files[Index].LastWriteTime = new DateTimeOffset(RemoteToLocalTime(Lines[Index * 2 + 0]));
|
|
Files[Index].Length = long.Parse(Lines[Index * 2 + 1]);
|
|
Files[Index].bExists = Files[Index].Length >= 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static int GetCommandSlots()
|
|
{
|
|
if (RemoteToolChain.bUseRPCUtil)
|
|
{
|
|
return RPCUtility.CommandHelper.GetCommandSlots(GetSocket());
|
|
}
|
|
else
|
|
{
|
|
return MaxRemoteCommandsAllowed;
|
|
}
|
|
}
|
|
|
|
public static Hashtable Command(string WorkingDirectory, string CommandWithArgs, string RemoteOutputPath)
|
|
{
|
|
int FirstSpace = CommandWithArgs.IndexOf(' ');
|
|
if (FirstSpace == -1)
|
|
{
|
|
return Command(WorkingDirectory, CommandWithArgs, " ", RemoteOutputPath);
|
|
}
|
|
|
|
return Command(WorkingDirectory, CommandWithArgs.Substring(0, FirstSpace), CommandWithArgs.Substring(FirstSpace + 1), RemoteOutputPath);
|
|
|
|
}
|
|
|
|
public static Hashtable Command(string WorkingDirectory, string Command, string CommandArgs, string RemoteOutputPath)
|
|
{
|
|
if (RemoteToolChain.bUseRPCUtil)
|
|
{
|
|
int RetriesRemaining = 6;
|
|
do
|
|
{
|
|
// a $ on the commandline will actually be converted, so we need to quote it
|
|
CommandArgs = CommandArgs.Replace("$", "\\$");
|
|
|
|
try
|
|
{
|
|
Hashtable Results = RPCUtility.CommandHelper.RPCCommand(GetSocket(), WorkingDirectory, Command, CommandArgs, RemoteOutputPath);
|
|
return Results;
|
|
}
|
|
catch (Exception Ex)
|
|
{
|
|
if (RetriesRemaining > 0)
|
|
{
|
|
Int32 RetryTimeoutMS = 1000;
|
|
Debug.WriteLine("Retrying command after sleeping for " + RetryTimeoutMS + " milliseconds. Command is:" + Command + " " + CommandArgs);
|
|
Thread.Sleep(RetryTimeoutMS);
|
|
}
|
|
else
|
|
{
|
|
Log.TraceInformation("Out of retries, too many exceptions:" + Ex.ToString());
|
|
// We've tried enough times, just throw the error
|
|
throw new Exception("Deep Exception, retries exhausted... ", Ex);
|
|
}
|
|
RetriesRemaining--;
|
|
}
|
|
}
|
|
while (RetriesRemaining > 0);
|
|
|
|
return null;
|
|
|
|
}
|
|
else
|
|
{
|
|
return RemoteToolChain.SSHCommand(WorkingDirectory, Command + " " + CommandArgs, RemoteOutputPath);
|
|
}
|
|
}
|
|
}
|
|
}
|