Files
UnrealEngineUWP/Engine/Source/Programs/UnrealGameSync/UnrealGameSyncLauncher/Program.cs
Ben Marsh aac595bfe5 UGS: Fix build errors.
#preflight none

[CL 20302451 by Ben Marsh in ue5-main branch]
2022-05-20 16:34:25 -04:00

300 lines
9.5 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using EpicGames.Perforce;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using UnrealGameSync;
namespace UnrealGameSyncLauncher
{
static class Program
{
[STAThread]
static int Main(string[] Args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
bool bFirstInstance;
using(Mutex InstanceMutex = new Mutex(true, "UnrealGameSyncRunning", out bFirstInstance))
{
if(!bFirstInstance)
{
using(EventWaitHandle ActivateEvent = new EventWaitHandle(false, EventResetMode.AutoReset, "ActivateUnrealGameSync"))
{
ActivateEvent.Set();
}
return 0;
}
// Figure out if we should sync the unstable build by default
bool bUnstable = Args.Contains("-unstable", StringComparer.InvariantCultureIgnoreCase);
// Read the settings
string? ServerAndPort = null;
string? UserName = null;
string? DepotPath = DeploymentSettings.DefaultDepotPath;
GlobalPerforceSettings.ReadGlobalPerforceSettings(ref ServerAndPort, ref UserName, ref DepotPath);
// If the shift key is held down, immediately show the settings window
SettingsWindow.SyncAndRunDelegate SyncAndRunWrapper = (Perforce, DepotParam, bUnstableParam, LogWriter, CancellationToken) => SyncAndRun(Perforce, DepotParam, bUnstableParam, Args, InstanceMutex, LogWriter, CancellationToken);
if ((Control.ModifierKeys & Keys.Shift) != 0)
{
// Show the settings window immediately
SettingsWindow UpdateError = new SettingsWindow(null, null, ServerAndPort, UserName, DepotPath, bUnstable, SyncAndRunWrapper);
if(UpdateError.ShowDialog() == DialogResult.OK)
{
return 0;
}
}
else
{
// Try to do a sync with the current settings first
CaptureLogger Logger = new CaptureLogger();
IPerforceSettings Settings = PerforceSettings.Default.MergeWith(newServerAndPort: ServerAndPort, newUserName: UserName);
ModalTask? Task = PerforceModalTask.Execute(null, "Updating", "Checking for updates, please wait...", Settings, (p, c) => SyncAndRun(p, DepotPath, bUnstable, Args, InstanceMutex, Logger, c), Logger);
if (Task == null)
{
Logger.LogInformation("Canceled by user");
}
else if (Task.Succeeded)
{
return 0;
}
SettingsWindow UpdateError = new SettingsWindow("Unable to update UnrealGameSync from Perforce. Verify that your connection settings are correct.", Logger.Render(Environment.NewLine), ServerAndPort, UserName, DepotPath, bUnstable, SyncAndRunWrapper);
if(UpdateError.ShowDialog() == DialogResult.OK)
{
return 0;
}
}
}
return 1;
}
public static async Task SyncAndRun(IPerforceConnection Perforce, string? BaseDepotPath, bool bUnstable, string[] Args, Mutex InstanceMutex, ILogger Logger, CancellationToken CancellationToken)
{
try
{
if (String.IsNullOrEmpty(BaseDepotPath))
{
throw new UserErrorException($"Invalid setting for sync path");
}
string SyncPath = BaseDepotPath.TrimEnd('/') + (bUnstable ? "/UnstableRelease/..." : "/Release/...");
Logger.LogInformation("Syncing from {SyncPath}", SyncPath);
// Create the target folder
string ApplicationFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "UnrealGameSync", "Latest");
if (!SafeCreateDirectory(ApplicationFolder))
{
throw new UserErrorException($"Couldn't create directory: {ApplicationFolder}");
}
// Find the most recent changelist
List<ChangesRecord> Changes = await Perforce.GetChangesAsync(ChangesOptions.None, 1, ChangeStatus.Submitted, SyncPath, CancellationToken);
int RequiredChangeNumber = Changes[0].Number;
// Read the current version
string SyncVersionFile = Path.Combine(ApplicationFolder, "SyncVersion.txt");
string RequiredSyncText = String.Format("{0}\n{1}@{2}", Perforce.Settings.ServerAndPort ?? "", SyncPath, RequiredChangeNumber);
// Check the application exists
string ApplicationExe = Path.Combine(ApplicationFolder, "UnrealGameSync.exe");
// Check if the version has changed
string? SyncText;
if (!File.Exists(SyncVersionFile) || !File.Exists(ApplicationExe) || !TryReadAllText(SyncVersionFile, out SyncText) || SyncText != RequiredSyncText)
{
// Try to delete the directory contents. Retry for a while, in case we've been spawned by an application in this folder to do an update.
for (int NumRetries = 0; !SafeDeleteDirectoryContents(ApplicationFolder); NumRetries++)
{
if (NumRetries > 20)
{
throw new UserErrorException($"Couldn't delete contents of {ApplicationFolder} (retried {NumRetries} times).");
}
Thread.Sleep(500);
}
// Find all the files in the sync path at this changelist
List<FStatRecord> FileRecords = await Perforce.FStatAsync(FStatOptions.None, $"{SyncPath}@{RequiredChangeNumber}", CancellationToken).ToListAsync(CancellationToken);
if (FileRecords.Count == 0)
{
throw new UserErrorException($"Couldn't find any matching files for {SyncPath}@{RequiredChangeNumber}");
}
// Sync all the files in this list to the same directory structure under the application folder
string DepotPathPrefix = SyncPath.Substring(0, SyncPath.LastIndexOf('/') + 1);
foreach (FStatRecord FileRecord in FileRecords)
{
if (FileRecord.DepotFile == null)
{
throw new UserErrorException("Missing depot path for returned file");
}
string LocalPath = Path.Combine(ApplicationFolder, FileRecord.DepotFile.Substring(DepotPathPrefix.Length).Replace('/', Path.DirectorySeparatorChar));
if (!SafeCreateDirectory(Path.GetDirectoryName(LocalPath)!))
{
throw new UserErrorException($"Couldn't create folder {Path.GetDirectoryName(LocalPath)}");
}
await Perforce.PrintAsync(LocalPath, FileRecord.DepotFile, CancellationToken);
}
// Check the application exists
if (!File.Exists(ApplicationExe))
{
throw new UserErrorException($"Application was not synced from Perforce. Check that UnrealGameSync exists at {SyncPath}/UnrealGameSync.exe, and you have access to it.");
}
// Update the version
if (!TryWriteAllText(SyncVersionFile, RequiredSyncText))
{
throw new UserErrorException("Couldn't write sync text to {SyncVersionFile}");
}
}
Logger.LogInformation("");
// Build the command line for the synced application, including the sync path to monitor for updates
StringBuilder NewCommandLine = new StringBuilder(String.Format("-updatepath=\"{0}@>{1}\" -updatespawn=\"{2}\"{3}", SyncPath, RequiredChangeNumber, Assembly.GetEntryAssembly()!.Location, bUnstable ? " -unstable" : ""));
foreach (string Arg in Args)
{
NewCommandLine.AppendFormat(" {0}", QuoteArgument(Arg));
}
// Release the mutex now so that the new application can start up
InstanceMutex.Close();
// Spawn the application
Logger.LogInformation("Spawning {App} with command line: {CmdLine}", ApplicationExe, NewCommandLine.ToString());
using (Process ChildProcess = new Process())
{
ChildProcess.StartInfo.FileName = ApplicationExe;
ChildProcess.StartInfo.Arguments = NewCommandLine.ToString();
ChildProcess.StartInfo.UseShellExecute = false;
ChildProcess.StartInfo.CreateNoWindow = false;
if (!ChildProcess.Start())
{
throw new UserErrorException("Failed to start process");
}
}
}
catch (UserErrorException Ex)
{
Logger.LogError("{Message}", Ex.Message);
throw;
}
catch (Exception Ex)
{
Logger.LogError(Ex, "Error while syncing application.");
foreach (string Line in Ex.ToString().Split('\n'))
{
Logger.LogError("{Line}", Line);
}
throw;
}
}
static string QuoteArgument(string Arg)
{
if(Arg.IndexOf(' ') != -1 && !Arg.StartsWith("\""))
{
return String.Format("\"{0}\"", Arg);
}
else
{
return Arg;
}
}
static bool TryReadAllText(string FileName, [NotNullWhen(true)] out string? Text)
{
try
{
Text = File.ReadAllText(FileName);
return true;
}
catch(Exception)
{
Text = null;
return false;
}
}
static bool TryWriteAllText(string FileName, string Text)
{
try
{
File.WriteAllText(FileName, Text);
return true;
}
catch(Exception)
{
return false;
}
}
static bool SafeCreateDirectory(string DirectoryName)
{
try
{
Directory.CreateDirectory(DirectoryName);
return true;
}
catch(Exception)
{
return false;
}
}
static bool SafeDeleteDirectory(string DirectoryName)
{
try
{
Directory.Delete(DirectoryName, true);
return true;
}
catch(Exception)
{
return false;
}
}
static bool SafeDeleteDirectoryContents(string DirectoryName)
{
try
{
DirectoryInfo Directory = new DirectoryInfo(DirectoryName);
foreach(FileInfo ChildFile in Directory.EnumerateFiles("*", SearchOption.AllDirectories))
{
ChildFile.Attributes = ChildFile.Attributes & ~FileAttributes.ReadOnly;
ChildFile.Delete();
}
foreach(DirectoryInfo ChildDirectory in Directory.EnumerateDirectories())
{
SafeDeleteDirectory(ChildDirectory.FullName);
}
return true;
}
catch(Exception)
{
return false;
}
}
}
}