Files
UnrealEngine/Engine/Extras/P4VUtils/Program.cs
2025-05-18 13:04:45 +08:00

543 lines
18 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Xml;
namespace P4VUtils
{
class CustomToolInfo
{
public string Name { get; set; }
public string Arguments { get; set; }
public bool AddToContextMenu { get; set; } = true;
public bool ShowConsole { get; set; }
public bool RefreshUI { get; set; } = true;
public string Shortcut { get; set; } = "";
public bool PromptForArgument { get; set; }
public string PromptText { get; set; } = "";
public CustomToolInfo(string Name, string Arguments)
{
this.Name = Name;
this.Arguments = Arguments;
}
}
abstract class Command
{
public abstract string Description { get; }
public abstract CustomToolInfo CustomTool { get; }
public abstract Task<int> Execute(string[] Args, IReadOnlyDictionary<string, string> ConfigValues, ILogger Logger);
}
class Program
{
public static IDictionary<string, Command> Commands = new Dictionary<string, Command>();
public static IDictionary<CommandCategory, List<string>> CommandCategories = new Dictionary<CommandCategory, List<string>>();
static void PrintHelp(ILogger Logger)
{
Logger.LogInformation("P4VUtils");
Logger.LogInformation("Provides useful shortcuts for working with P4V");
Logger.LogInformation("");
Logger.LogInformation("Usage:");
Logger.LogInformation(" P4VUtils [Command] [Arguments...]");
Logger.LogInformation("");
List<KeyValuePair<string, string>> Table = new List<KeyValuePair<string, string>>();
foreach (KeyValuePair<string, Command> Pair in Commands)
{
Table.Add(new KeyValuePair<string, string>(Pair.Key, Pair.Value.Description));
}
Logger.LogInformation("Commands:");
HelpUtils.PrintTable(Table, 2, 15, ConsoleUtils.WindowWidth - 1, Logger);
}
static async Task<int> Main(string[] Args)
{
using ILoggerFactory Factory = LoggerFactory.Create(Builder => Builder.AddEpicDefault());
ILogger Logger = Factory.CreateLogger<Program>();
Log.SetInnerLogger(Logger);
try
{
return await InnerMain(Args, Logger);
}
catch (Exception Ex)
{
Logger.LogError(Ex, "Unhandled exception: {Ex}", Ex.ToString());
return 1;
}
}
static void RegisterCommands()
{
// Grab all the commands, sorting to preserve the previous order from the explicit tabs.
var Types = from assembly in Assembly.GetExecutingAssembly().GetTypes()
where assembly.IsDefined(typeof(CommandAttribute))
let attrib = assembly.GetCustomAttributes(typeof(CommandAttribute), true).Cast<CommandAttribute>().First()
orderby attrib.Order
select new { Type = assembly, Attribute = attrib };
foreach (var Type in Types)
{
CommandAttribute attribute = Type.Attribute;
if (!CommandCategories.ContainsKey(attribute.Category))
{
CommandCategories[attribute.Category] = new List<string>();
}
Command? CreatedCommand = Activator.CreateInstance(Type.Type) as Command;
Commands.Add(attribute.CommandName, CreatedCommand!);
CommandCategories[attribute.Category].Add(attribute.CommandName);
}
}
static async Task<int> InnerMain(string[] Args, ILogger Logger)
{
RegisterCommands();
if (Args.Length == 0 || Args[0].Equals("-help", StringComparison.OrdinalIgnoreCase))
{
PrintHelp(Logger);
return 0;
}
else if (Args[0].StartsWith("-", StringComparison.Ordinal))
{
Logger.LogInformation("Missing command name");
PrintHelp(Logger);
return 1;
}
else if (Args[0].Equals("install", StringComparison.OrdinalIgnoreCase))
{
Logger.LogInformation("Adding custom tools...");
return await UpdateCustomToolRegistration(true, Args.Any(x => x.Equals("-inplace", StringComparison.OrdinalIgnoreCase)), Logger);
}
else if (Args[0].Equals("uninstall", StringComparison.OrdinalIgnoreCase))
{
Logger.LogInformation("Removing custom tools...");
return await UpdateCustomToolRegistration(false, Args.Any(x => x.Equals("-inplace", StringComparison.OrdinalIgnoreCase)), Logger);
}
else if (Commands.TryGetValue(Args[0], out Command? Command))
{
if (Args.Any(x => x.Equals("-help", StringComparison.OrdinalIgnoreCase)))
{
List<KeyValuePair<string, string>> Parameters = CommandLineArguments.GetParameters(Command.GetType());
Logger.LogInformation("{Title}", Args[0]);
Logger.LogInformation("{Description}", Command.GetType());
Logger.LogInformation("Parameters:");
HelpUtils.PrintTable(Parameters, 4, 24, HelpUtils.WindowWidth - 1, Logger);
return 0;
}
Dictionary<string, string> ConfigValues = ReadConfig();
return await Command.Execute(Args, ConfigValues, Logger);
}
else
{
Logger.LogError("Unknown command: {Command}", Args[0]);
PrintHelp(Logger);
return 1;
}
}
static Dictionary<string, string> ReadConfig()
{
Dictionary<string, string> ConfigValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string BasePath = System.AppContext.BaseDirectory;
AppendConfig(Path.Combine(BasePath, "P4VUtils.ini"), ConfigValues);
AppendConfig(Path.Combine(BasePath, "NotForLicensees", "P4VUtils.ini"), ConfigValues);
return ConfigValues;
}
static void AppendConfig(string SourcePath, Dictionary<string, string> ConfigValues)
{
if (File.Exists(SourcePath))
{
string[] Lines = File.ReadAllLines(SourcePath);
foreach (string Line in Lines)
{
int EqualsIdx = Line.IndexOf('=', StringComparison.Ordinal);
if (EqualsIdx != -1)
{
string Key = Line.Substring(0, EqualsIdx).Trim();
string Value = Line.Substring(EqualsIdx + 1).Trim();
ConfigValues[Key] = Value;
}
}
}
}
public static bool TryLoadXmlDocument(FileReference Location, XmlDocument Document)
{
if (FileReference.Exists(Location))
{
try
{
Document.Load(Location.FullName);
return true;
}
catch
{
}
}
return false;
}
static string GetToolName(XmlElement ToolNode)
{
return ToolNode.SelectSingleNode("Definition")?.SelectSingleNode("Name")?.InnerText ?? string.Empty;
}
static bool ShouldBeRemoved(XmlElement ChildElement, FileReference ExecutableLocation)
{
XmlElement? CommandElement = ChildElement.SelectSingleNode("Definition/Command") as XmlElement;
// leave any missing entries alone
if (CommandElement == null || CommandElement.InnerText == null)
{
return false;
}
// In a recent change we started to output the Command element as a quoted argument if the path contains spaces.
// FileReference does not resolve quoted string properly which was causing the comparisons here to fail.
// We can strip the quotes before creating a FileReference to compare with DotNetLocation to ensure that the comparsion
// is correct.
FileReference ExistingCommandPath = new FileReference(CommandElement.InnerText.StripQuoteArgument());
// if the executables matches this, then we remove it
if (ExistingCommandPath.GetFileName() == ExecutableLocation.GetFileName())
{
return true;
}
// if it's an old style command that ran "dotnet P4VUtils.dll", remove it
if (ExistingCommandPath.GetFileNameWithoutAnyExtensions().Equals("dotnet", StringComparison.OrdinalIgnoreCase))
{
XmlElement? ArgumentsElement = ChildElement.SelectSingleNode("Definition/Arguments") as XmlElement;
if (ArgumentsElement != null)
{
string[] Arguments = CommandLineArguments.Split(ArgumentsElement.InnerText);
if (Arguments.Length > 0 && Path.GetFileNameWithoutExtension(Arguments[0]).Equals("P4VUtils", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
// any other case, leave it installed
return false;
}
// returns true if all tools were removed
static bool RemoveCustomToolsFromNode(XmlElement RootNode, FileReference ExecutableLocation, ILogger Logger)
{
int ToolsChecked = 0;
int ToolsRemoved = 0;
XmlNodeList? CustomToolDefList = RootNode.SelectNodes("CustomToolDef");
if (CustomToolDefList == null)
{
return false;
}
// Removes tools explicitly calling the assembly location identified above - i assume as a way to "filter" only those we explicitly added (@Ben.Marsh) - nochecking, remove this comment once verified.
foreach (XmlNode? ChildNode in CustomToolDefList)
{
XmlElement? ChildElement = ChildNode as XmlElement;
if (ChildElement != null)
{
ToolsChecked++;
if (ShouldBeRemoved(ChildElement, ExecutableLocation))
{
Logger.LogInformation("Removing Tool {ToolName}", GetToolName(ChildElement));
RootNode.RemoveChild(ChildElement);
ToolsRemoved++;
}
}
}
return ToolsChecked == ToolsRemoved;
}
static void InstallCommandsListInFolder(string FolderName, bool AddFolderToContextMenu, CommandCategory Category, XmlDocument Document, FileReference ExecutableLocation, ILogger Logger)
{
// <CustomToolDefList> // list of custom tools (top level)
// < CustomToolDef > // loose custom tool in top level
// < CustomToolFolder> // folder containing custom tools
// < Name > Test </ Name >
// < CustomToolDefList > // list of custom tools in folder
// < CustomToolDef > // definition of tool
// This is the top level node, there will also be a per folder node added of same name
XmlElement? Root = Document.SelectSingleNode("CustomToolDefList") as XmlElement;
if (Root != null)
{
XmlElement FolderDefinition = Document.CreateElement("CustomToolFolder");
XmlElement FolderDescription = Document.CreateElement("Name");
FolderDescription.InnerText = FolderName;
FolderDefinition.AppendChild(FolderDescription);
XmlElement FolderToContextMenu = Document.CreateElement("AddToContext");
FolderToContextMenu.InnerText = AddFolderToContextMenu ? "true" : "false";
FolderDefinition.AppendChild(FolderToContextMenu);
XmlElement FolderDefList = Document.CreateElement("CustomToolDefList");
List<string> CommandList = CommandCategories[Category];
IEnumerable<KeyValuePair<string, Command>> CategoryCommands = Commands.Where(Pair => CommandList.Contains(Pair.Key));
foreach (KeyValuePair<string, Command> Pair in CategoryCommands)
{
CustomToolInfo CustomTool = Pair.Value.CustomTool;
XmlElement ToolDef = Document.CreateElement("CustomToolDef");
{
XmlElement Definition = Document.CreateElement("Definition");
{
XmlElement Description = Document.CreateElement("Name");
Description.InnerText = CustomTool.Name;
Definition.AppendChild(Description);
XmlElement Command = Document.CreateElement("Command");
Command.InnerText = ExecutableLocation.FullName;
if (OperatingSystem.IsWindows())
{
Command.InnerText = Command.InnerText.QuoteArgument();
}
Definition.AppendChild(Command);
XmlElement Arguments = Document.CreateElement("Arguments");
Arguments.InnerText = $"{Pair.Key} {CustomTool.Arguments}";
Definition.AppendChild(Arguments);
if (CustomTool.Shortcut.Length > 1)
{
XmlElement Shortcut = Document.CreateElement("Shortcut");
Shortcut.InnerText = CustomTool.Shortcut;
Definition.AppendChild(Shortcut);
}
}
ToolDef.AppendChild(Definition);
if (CustomTool.ShowConsole)
{
XmlElement Console = Document.CreateElement("Console");
{
XmlElement CloseOnExit = Document.CreateElement("CloseOnExit");
CloseOnExit.InnerText = "false";
Console.AppendChild(CloseOnExit);
}
ToolDef.AppendChild(Console);
}
if (CustomTool.RefreshUI)
{
XmlElement Refresh = Document.CreateElement("Refresh");
Refresh.InnerText = CustomTool.RefreshUI ? "true" : "false";
ToolDef.AppendChild(Refresh);
}
if (CustomTool.PromptForArgument)
{
XmlElement Prompt = Document.CreateElement("Prompt");
{
XmlElement PromptText = Document.CreateElement("PromptText");
PromptText.InnerText = CustomTool.PromptText.Length > 0 ? CustomTool.PromptText : "Argument";
Prompt.AppendChild(PromptText);
}
ToolDef.AppendChild(Prompt);
}
XmlElement AddToContext = Document.CreateElement("AddToContext");
AddToContext.InnerText = CustomTool.AddToContextMenu ? "true" : "false";
ToolDef.AppendChild(AddToContext);
}
FolderDefList.AppendChild(ToolDef);
}
FolderDefinition.AppendChild(FolderDefList);
Root.AppendChild(FolderDefinition);
}
}
static void RemoveCustomToolsFromFolders(XmlElement RootNode, FileReference ExecutableLocation, ILogger Logger)
{
XmlNodeList? CustomToolFolderList = RootNode.SelectNodes("CustomToolFolder");
if(CustomToolFolderList == null)
{
return;
}
foreach (XmlNode? ChildNode in CustomToolFolderList)
{
if (ChildNode != null)
{
bool RemoveFolder = false;
XmlElement? FolderRoot = ChildNode.SelectSingleNode("CustomToolDefList") as XmlElement;
if (FolderRoot != null)
{
XmlElement? FolderNameNode = ChildNode.SelectSingleNode("Name") as XmlElement;
string FolderNameString = "";
if (FolderNameNode != null)
{
FolderNameString = FolderNameNode.InnerText;
}
Logger.LogInformation("Removing Tools from folder {Folder}", FolderNameString);
RemoveFolder = RemoveCustomToolsFromNode(FolderRoot, ExecutableLocation, Logger);
}
if (RemoveFolder)
{
// remove the folder itself.
RootNode.RemoveChild(ChildNode);
}
}
}
}
public static async Task<int> UpdateCustomToolRegistration(bool bInstall, bool bInPlace, ILogger Logger)
{
DirectoryReference? ConfigDir = DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.UserProfile);
if (ConfigDir == null)
{
Logger.LogError("Unable to find config directory.");
return 1;
}
FileReference ConfigFile;
if (OperatingSystem.IsMacOS())
{
ConfigFile = FileReference.Combine(ConfigDir, "Library", "Preferences", "com.perforce.p4v", "customtools.xml");
}
else
{
ConfigFile = FileReference.Combine(ConfigDir, ".p4qt", "customtools.xml");
}
XmlDocument Document = new XmlDocument();
if (!TryLoadXmlDocument(ConfigFile, Document))
{
DirectoryReference.CreateDirectory(ConfigFile.Directory);
using (StreamWriter Writer = new StreamWriter(ConfigFile.FullName))
{
await Writer.WriteLineAsync(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
await Writer.WriteLineAsync(@"<!--perforce-xml-version=1.0-->");
await Writer.WriteLineAsync(@"<CustomToolDefList varName=""customtooldeflist"">");
await Writer.WriteLineAsync(@"</CustomToolDefList>");
}
Document.Load(ConfigFile.FullName);
}
FileReference ExecutableLocation = new FileReference(Environment.ProcessPath!);
if (!bInPlace)
{
DirectoryReference? InstallDir = DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.LocalApplicationData);
if (InstallDir == null)
{
Logger.LogError("Unable to get app data folder location");
return 1;
}
InstallDir = DirectoryReference.Combine(InstallDir, "Epic Games", "P4VUtils");
Logger.LogInformation("Copying application files to {Dir}", InstallDir);
try
{
UpdateInstallFolder(ExecutableLocation.Directory, InstallDir, bInstall);
ExecutableLocation = FileReference.Combine(InstallDir, ExecutableLocation.GetFileName());
}
catch (Exception ex)
{
Logger.LogError(ex, "Unable to update install folder: {Message}", ex.Message);
return 1;
}
}
XmlElement? Root = Document.SelectSingleNode("CustomToolDefList") as XmlElement;
if (Root == null)
{
Logger.LogError("Unknown schema for {ConfigFile}", ConfigFile);
return 1;
}
// Remove Custom tools at the root
RemoveCustomToolsFromNode(Root, ExecutableLocation, Logger);
// Remove Custom tools in folders, and the folders
RemoveCustomToolsFromFolders(Root, ExecutableLocation, Logger);
// Insert new entries
if (bInstall)
{
InstallCommandsListInFolder("UE RootHelpers", false/*AddFolderToContextMenu*/, CommandCategory.Root, Document, ExecutableLocation, Logger);
InstallCommandsListInFolder("UE Content", true/*AddFolderToContextMenu*/, CommandCategory.Content, Document, ExecutableLocation, Logger);
InstallCommandsListInFolder("UE Toolbox", true/*AddFolderToContextMenu*/, CommandCategory.Toolbox, Document, ExecutableLocation, Logger);
InstallCommandsListInFolder("UE Integrate", true/*AddFolderToContextMenu*/, CommandCategory.Integrate, Document, ExecutableLocation, Logger);
InstallCommandsListInFolder("UE Horde", true/*AddFolderToContextMenu*/, CommandCategory.Horde, Document, ExecutableLocation, Logger);
InstallCommandsListInFolder("UE Browser", true/*AddFolderToContextMenu*/, CommandCategory.Browser, Document, ExecutableLocation, Logger);
}
// Save the new document
Document.Save(ConfigFile.FullName);
Logger.LogInformation("Written {ConfigFile}", ConfigFile.FullName);
return 0;
}
static void UpdateInstallFolder(DirectoryReference SourceDir, DirectoryReference TargetDir, bool bInstall)
{
DirectoryReference TempDir = DirectoryReference.Combine(TargetDir.ParentDirectory!, "~" + TargetDir.GetDirectoryName());
if (DirectoryReference.Exists(TempDir))
{
foreach( FileReference file in DirectoryReference.EnumerateFiles(TempDir, "*", SearchOption.AllDirectories) )
{
file.ToFileInfo().Attributes = FileAttributes.Normal;
}
DirectoryReference.Delete(TempDir, true);
}
if (DirectoryReference.Exists(TargetDir))
{
Directory.Move(TargetDir.FullName, TempDir.FullName);
foreach (FileReference file in DirectoryReference.EnumerateFiles(TempDir, "*", SearchOption.AllDirectories))
{
file.ToFileInfo().Attributes = FileAttributes.Normal;
}
DirectoryReference.Delete(TempDir, true);
}
if (bInstall)
{
DirectoryReference.CreateDirectory(TempDir);
foreach (FileReference SourceFile in DirectoryReference.EnumerateFiles(SourceDir, "*", SearchOption.AllDirectories))
{
FileReference TargetFile = FileReference.Combine(TempDir, SourceFile.MakeRelativeTo(SourceDir));
DirectoryReference.CreateDirectory(TargetFile.Directory);
FileReference.Copy(SourceFile, TargetFile, true);
}
Directory.Move(TempDir.FullName, TargetDir.FullName);
}
}
}
}