// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace UnrealGameSyncLauncher { static partial class Program { /// /// Specifies the path to sync down the stable version of UGS from (eg. //depot/UnrealGameSync/Stable/...). This is a site-specific setting. /// static readonly string StableSyncPath; /// /// Specifies the path to sync down the unstable version of UGS from (eg. //depot/UnrealGameSync/Unstable). Site-specific setting. May be null. /// static readonly string UnstableSyncPath = null; [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; } StringWriter LogWriter = new StringWriter(); bool bResult = false; try { bResult = SyncAndRunApplication(Args, InstanceMutex, LogWriter); } catch(Exception Ex) { LogWriter.WriteLine(Ex.ToString()); } if(!bResult) { UpdateErrorWindow ErrorWindow = new UpdateErrorWindow(LogWriter.ToString()); ErrorWindow.ShowDialog(); return 2; } } return 0; } static bool SyncAndRunApplication(string[] Args, Mutex InstanceMutex, TextWriter LogWriter) { // Try to find Perforce in the path string PerforceFileName = null; foreach(string PathDirectoryName in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(new char[]{ Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries)) { try { string PossibleFileName = Path.Combine(PathDirectoryName, "p4.exe"); if(File.Exists(PossibleFileName)) { PerforceFileName = PossibleFileName; break; } } catch { } } // If it doesn't exist, don't continue if(PerforceFileName == null) { LogWriter.WriteLine("UnrealGameSync requires the Perforce command-line tools. Please download and install from http://www.perforce.com/."); return false; } // Get the path that we're syncing bool bUnstable = Args.Contains("-unstable", StringComparer.InvariantCultureIgnoreCase); if(!bUnstable && (Control.ModifierKeys & Keys.Shift) != 0 && UnstableSyncPath != null) { if(MessageBox.Show("Use the latest unstable build of UnrealGameSync?\n\n(This message was triggered by holding down the SHIFT key on startup).", "Use unstable build?", MessageBoxButtons.YesNo) == DialogResult.Yes) { bUnstable = true; } } string SyncPath = bUnstable? UnstableSyncPath : StableSyncPath; LogWriter.WriteLine("Syncing from {0}", SyncPath); // Create the target folder string ApplicationFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "UnrealGameSync", "Latest"); if(!SafeCreateDirectory(ApplicationFolder)) { LogWriter.WriteLine("Couldn't create directory: {0}", ApplicationFolder); return false; } // Find the most recent changelist List SubmittedLines = new List(); if(RunPerforceCommand(PerforceFileName, "changes -s submitted -m 1", SubmittedLines, LogWriter) != 0) { LogWriter.WriteLine("Couldn't find last changelist"); return false; } // Split into tokens string[] ChangeTokens = SubmittedLines[0].Split(' '); // Parse the changelist number int RequiredChangeNumber; if(ChangeTokens.Length < 2 || ChangeTokens[0] != "Change" || !int.TryParse(ChangeTokens[1], out RequiredChangeNumber)) { LogWriter.WriteLine("Couldn't parse last changelist number"); return false; } // Read the current version string SyncVersionFile = Path.Combine(ApplicationFolder, "SyncVersion.txt"); string RequiredSyncText = String.Format("{0}@{1}", SyncPath, RequiredChangeNumber.ToString()); // Check if the version has changed string SyncText; if(!File.Exists(SyncVersionFile) || !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) { LogWriter.WriteLine("Couldn't delete contents of {0} (retried {1} times).", ApplicationFolder, NumRetries); return false; } Thread.Sleep(500); } // Find all the files in the sync path at this changelist List FileLines = new List(); if(RunPerforceCommand(PerforceFileName, String.Format("-z tag fstat \"{0}@{1}\"", SyncPath, RequiredChangeNumber), FileLines, LogWriter) != 0) { LogWriter.WriteLine("Couldn't find matching files."); return false; } // 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(string FileLine in FileLines) { const string DepotPathTag = "... depotFile "; if(FileLine.StartsWith(DepotPathTag)) { string DepotPath = FileLine.Substring(DepotPathTag.Length).Trim(); if(!DepotPath.StartsWith(DepotPathPrefix, StringComparison.InvariantCultureIgnoreCase)) { LogWriter.WriteLine("Found file {0} which did not begin with {1}", DepotPath, DepotPathPrefix); return false; } string LocalPath = Path.Combine(ApplicationFolder, DepotPath.Substring(DepotPathPrefix.Length).Replace('/', Path.DirectorySeparatorChar)); if(!SafeCreateDirectory(Path.GetDirectoryName(LocalPath))) { LogWriter.WriteLine("Couldn't create folder {0}", Path.GetDirectoryName(LocalPath)); return false; } if(RunPerforceCommand(PerforceFileName, String.Format("print -o \"{0}\" \"{1}@{2}\"", LocalPath, DepotPath, RequiredChangeNumber), null, LogWriter) != 0) { LogWriter.WriteLine("Couldn't sync {0} to {1}", DepotPath, LocalPath); return false; } } } // Update the version if(!TryWriteAllText(SyncVersionFile, RequiredSyncText)) { LogWriter.WriteLine("Couldn't write sync text to {0}", SyncVersionFile); return false; } } LogWriter.WriteLine(); // 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) { if(Arg.Contains(' ')) { NewCommandLine.AppendFormat( "\"{0}\"", Arg); } else { NewCommandLine.AppendFormat(" {0}", Arg); } } // Release the mutex now so that the new application can start up InstanceMutex.Close(); // Check the application exists string ApplicationExe = Path.Combine(ApplicationFolder, "UnrealGameSync.exe"); if(!File.Exists(ApplicationExe)) { LogWriter.WriteLine("Application was not synced from Perforce. Check you have access to {0}", SyncPath); return false; } // Spawn the application LogWriter.WriteLine("Spawning {0} with command line: {1}", 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()) { LogWriter.WriteLine("Failed to start process"); return false; } } return true; } static int RunPerforceCommand(string PerforceFileName, string CommandLine, List Lines, TextWriter LogWriter) { LogWriter.WriteLine(); LogWriter.WriteLine("Running p4.exe {0}", CommandLine); using(Process ChildProcess = new Process()) { DataReceivedEventHandler OutputHandler = (x, y) => HandlePerforceOutput(x, y, Lines, LogWriter); ChildProcess.StartInfo.FileName = PerforceFileName; ChildProcess.StartInfo.Arguments = CommandLine; ChildProcess.StartInfo.UseShellExecute = false; ChildProcess.StartInfo.RedirectStandardOutput = true; ChildProcess.StartInfo.RedirectStandardError = true; ChildProcess.OutputDataReceived += OutputHandler; ChildProcess.ErrorDataReceived += OutputHandler; ChildProcess.StartInfo.CreateNoWindow = true; ChildProcess.StartInfo.StandardOutputEncoding = new System.Text.UTF8Encoding(false, false); ChildProcess.Start(); ChildProcess.BeginOutputReadLine(); ChildProcess.BeginErrorReadLine(); ChildProcess.WaitForExit(); LogWriter.WriteLine("Finished with exit code {0}", ChildProcess.ExitCode); return ChildProcess.ExitCode; } } static void HandlePerforceOutput(object Sender, DataReceivedEventArgs Args, List Lines, TextWriter LogWriter) { if(Args.Data != null) { lock(LogWriter) { if(Lines != null) { Lines.Add(Args.Data); } LogWriter.WriteLine("p4> {0}", Args.Data); } } } static bool TryReadAllText(string FileName, 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; } } } }