2117 lines
72 KiB
C#
2117 lines
72 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Json;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using EpicGames.Core;
|
|
using EpicGames.Horde;
|
|
using EpicGames.OIDC;
|
|
using EpicGames.Perforce;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Serilog;
|
|
using UnrealGameSync;
|
|
|
|
namespace UnrealGameSyncCmd
|
|
{
|
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
|
|
|
public sealed class UserErrorException : Exception
|
|
{
|
|
public LogEvent Event { get; }
|
|
public int Code { get; }
|
|
|
|
public UserErrorException(LogEvent evt)
|
|
: base(evt.ToString())
|
|
{
|
|
Event = evt;
|
|
Code = 1;
|
|
}
|
|
|
|
public UserErrorException(string message, params object[] args)
|
|
: this(LogEvent.Create(LogLevel.Error, message, args))
|
|
{
|
|
}
|
|
}
|
|
|
|
public static class Program
|
|
{
|
|
static BuildConfig EditorConfig => BuildConfig.Development;
|
|
|
|
class CommandInfo
|
|
{
|
|
public string Name { get; }
|
|
public Type Type { get; }
|
|
public Type? OptionsType { get; }
|
|
public string? Usage { get; }
|
|
public string? Brief { get; }
|
|
|
|
public CommandInfo(string name, Type type, Type? optionsType, string? usage, string? brief)
|
|
{
|
|
Name = name;
|
|
Type = type;
|
|
OptionsType = optionsType;
|
|
Usage = usage;
|
|
Brief = brief;
|
|
}
|
|
}
|
|
|
|
static readonly CommandInfo[] _commands =
|
|
{
|
|
new CommandInfo("init", typeof(InitCommand), typeof(InitCommandOptions),
|
|
"ugs init [stream-path]",
|
|
"Create a client for the given stream, or initializes an existing client for use by UGS."
|
|
),
|
|
new CommandInfo("switch", typeof(SwitchCommand), typeof(SwitchCommandOptions),
|
|
"ugs switch [project name|project path|stream]",
|
|
"Changes the active project to the one in the workspace with the given name, or switches to a new stream."
|
|
),
|
|
new CommandInfo("changes", typeof(ChangesCommand), typeof(ChangesCommandOptions),
|
|
"ugs changes",
|
|
"List recently submitted changes to the current branch."
|
|
),
|
|
new CommandInfo("config", typeof(ConfigCommand), typeof(ConfigCommandOptions),
|
|
"ugs config",
|
|
"Updates the configuration for the current workspace."
|
|
),
|
|
new CommandInfo("filter", typeof(FilterCommand), typeof(FilterCommandOptions),
|
|
"ugs filter",
|
|
"Displays or updates the workspace or global sync filter"
|
|
),
|
|
new CommandInfo("sync", typeof(SyncCommand), typeof(SyncCommandOptions),
|
|
"ugs sync [change|'latest']",
|
|
"Syncs the current workspace to the given changelist, optionally removing all local state."
|
|
),
|
|
new CommandInfo("clients", typeof(ClientsCommand), typeof(ClientsCommandOptions),
|
|
"ugs clients",
|
|
"Lists all clients suitable for use on the current machine."
|
|
),
|
|
new CommandInfo("run", typeof(RunCommand), null,
|
|
"ugs run",
|
|
"Runs the editor for the current branch."
|
|
),
|
|
new CommandInfo("build", typeof(BuildCommand), typeof(BuildCommandOptions),
|
|
"ugs build [id]",
|
|
"Runs the default build steps for the current project, or a particular step referenced by id."
|
|
),
|
|
new CommandInfo("status", typeof(StatusCommand), null,
|
|
"ugs status [-update]",
|
|
"Shows the status of the currently synced branch."
|
|
),
|
|
new CommandInfo("login", typeof(LoginCommand), null,
|
|
"ugs login",
|
|
"Starts a interactive login flow against the configured Identity Provider"
|
|
),
|
|
new CommandInfo("version", typeof(VersionCommand), null,
|
|
"ugs version",
|
|
"Prints the current application version"
|
|
),
|
|
new CommandInfo("install", typeof(InstallCommand), null,
|
|
null,
|
|
null
|
|
),
|
|
new CommandInfo("uninstall", typeof(UninstallCommand), null,
|
|
null,
|
|
null
|
|
),
|
|
new CommandInfo("upgrade", typeof(UpgradeCommand), typeof(UpgradeCommandOptions),
|
|
"ugs upgrade",
|
|
"Upgrades the current installation with the latest build of UGS."
|
|
),
|
|
new CommandInfo("tools", typeof(ToolsCommand), typeof(ToolsCommandCommandOptions),
|
|
"ugs tools",
|
|
"Install a Custom Tool. Only available for windows."
|
|
),
|
|
};
|
|
|
|
class CommandContext
|
|
{
|
|
public CommandLineArguments Arguments { get; }
|
|
public ILogger Logger { get; }
|
|
public ILoggerFactory LoggerFactory { get; }
|
|
|
|
public UserSettings? GlobalSettings { get; }
|
|
|
|
public GlobalSettingsFile UserSettings { get; }
|
|
|
|
public IHordeClient? HordeClient { get; }
|
|
public ICloudStorage? CloudStorage { get; }
|
|
|
|
public CommandContext(CommandLineArguments arguments, ILogger logger, ILoggerFactory loggerFactory, GlobalSettingsFile userSettings, UserSettings? globalSettings, IHordeClient? hordeClient, ICloudStorage? cloudStorage)
|
|
{
|
|
Arguments = arguments;
|
|
Logger = logger;
|
|
LoggerFactory = loggerFactory;
|
|
GlobalSettings = globalSettings;
|
|
UserSettings = userSettings;
|
|
HordeClient = hordeClient;
|
|
CloudStorage = cloudStorage;
|
|
}
|
|
}
|
|
|
|
class ServerOptions
|
|
{
|
|
[CommandLine("-Server=")]
|
|
public string? ServerAndPort { get; set; }
|
|
|
|
[CommandLine("-User=")]
|
|
public string? UserName { get; set; }
|
|
}
|
|
|
|
class ConfigCommandOptions : ServerOptions
|
|
{
|
|
public void ApplyTo(UserWorkspaceSettings settings)
|
|
{
|
|
if (ServerAndPort != null)
|
|
{
|
|
settings.ServerAndPort = (ServerAndPort.Length == 0) ? null : ServerAndPort;
|
|
}
|
|
if (UserName != null)
|
|
{
|
|
settings.UserName = (UserName.Length == 0) ? null : UserName;
|
|
}
|
|
}
|
|
}
|
|
|
|
class InitCommandOptions : ConfigCommandOptions
|
|
{
|
|
[CommandLine("-Client=")]
|
|
public string? ClientName { get; set; }
|
|
|
|
[CommandLine("-Branch=")]
|
|
public string? BranchPath { get; set; }
|
|
|
|
[CommandLine("-Project=")]
|
|
public string? ProjectName { get; set; }
|
|
|
|
[CommandLine("-ClientRoot=")]
|
|
public string? ClientRoot { get; set; }
|
|
|
|
[CommandLine("-IgnoreExistingClients")]
|
|
public bool IgnoreExistingClients { get; set; }
|
|
}
|
|
|
|
class UpdateState
|
|
{
|
|
public string? LatestVersion { get; set; }
|
|
public DateTime LastVersionCheck { get; set; }
|
|
}
|
|
|
|
public static async Task<int> Main(string[] rawArgs)
|
|
{
|
|
DirectoryReference globalConfigFolder;
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
globalConfigFolder = DirectoryReference.Combine(DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.LocalApplicationData)!, "UnrealGameSync");
|
|
}
|
|
else
|
|
{
|
|
globalConfigFolder = DirectoryReference.Combine(DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.UserProfile)!, ".config", "UnrealGameSync");
|
|
}
|
|
DirectoryReference.CreateDirectory(globalConfigFolder);
|
|
|
|
string logName;
|
|
DirectoryReference logFolder;
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
{
|
|
logFolder = DirectoryReference.Combine(DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.UserProfile)!, "Library", "Logs", "Unreal Engine", "UnrealGameSync");
|
|
logName = "UnrealGameSync-.log";
|
|
}
|
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
{
|
|
logFolder = DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.UserProfile)!;
|
|
logName = ".ugs-.log";
|
|
}
|
|
else
|
|
{
|
|
logFolder = globalConfigFolder;
|
|
logName = "UnrealGameSyncCmd-.log";
|
|
}
|
|
|
|
Serilog.ILogger serilogLogger = new LoggerConfiguration()
|
|
.Enrich.FromLogContext()
|
|
.WriteTo.Console(Serilog.Events.LogEventLevel.Information, outputTemplate: "{Message:lj}{NewLine}")
|
|
.WriteTo.File(FileReference.Combine(logFolder, logName).FullName, Serilog.Events.LogEventLevel.Debug, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 20 * 1024 * 1024, retainedFileCountLimit: 10)
|
|
.CreateLogger();
|
|
|
|
using ILoggerFactory loggerFactory = new Serilog.Extensions.Logging.SerilogLoggerFactory(serilogLogger, true);
|
|
ILogger logger = loggerFactory.CreateLogger("Main");
|
|
try
|
|
{
|
|
LauncherSettings launcherSettings = new LauncherSettings();
|
|
launcherSettings.Read();
|
|
|
|
ServiceCollection services = new ServiceCollection();
|
|
|
|
if (launcherSettings.HordeServer != null)
|
|
{
|
|
services.AddHorde(options => options.ServerUrl = new Uri(launcherSettings.HordeServer));
|
|
}
|
|
|
|
services.AddCloudStorage();
|
|
|
|
ServiceProvider serviceProvider = services.BuildServiceProvider();
|
|
IHordeClient? hordeClient = serviceProvider.GetService<IHordeClient>();
|
|
ICloudStorage? cloudStorage = serviceProvider.GetService<ICloudStorage>();
|
|
|
|
UserSettings? globalSettings = null;
|
|
GlobalSettingsFile settings;
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
globalSettings = UserSettings.Create(globalConfigFolder, logger);
|
|
settings = globalSettings as GlobalSettingsFile;
|
|
}
|
|
else
|
|
{
|
|
settings = GlobalSettingsFile.Create(FileReference.Combine(globalConfigFolder, "Global.json"));
|
|
}
|
|
|
|
CommandLineArguments args = new CommandLineArguments(rawArgs);
|
|
|
|
string? commandName;
|
|
if (!args.TryGetPositionalArgument(out commandName))
|
|
{
|
|
PrintHelp();
|
|
return 0;
|
|
}
|
|
|
|
CommandInfo? command = _commands.FirstOrDefault(x => x.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
|
|
if (command == null)
|
|
{
|
|
logger.LogError("Unknown command '{Command}'", commandName);
|
|
Console.WriteLine();
|
|
PrintHelp();
|
|
return 1;
|
|
}
|
|
|
|
// On Windows this is distributed with the GUI client, so we don't need to check for upgrades
|
|
if (command.Type != typeof(InstallCommand) && command.Type != typeof(UpgradeCommand) && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
string version = GetVersion();
|
|
|
|
DateTime utcNow = DateTime.UtcNow;
|
|
if (settings.Global.LastVersionCheck < utcNow - TimeSpan.FromDays(1.0) || IsUpgradeAvailable(settings, version))
|
|
{
|
|
using (CancellationTokenSource cancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10.0)))
|
|
{
|
|
Task<string?> latestVersionTask = GetLatestVersionAsync(null, cancellationSource.Token);
|
|
|
|
Task delay = Task.Delay(TimeSpan.FromSeconds(2.0));
|
|
await Task.WhenAny(latestVersionTask, delay);
|
|
|
|
if (!latestVersionTask.IsCompleted)
|
|
{
|
|
logger.LogInformation("Checking for UGS updates...");
|
|
}
|
|
|
|
try
|
|
{
|
|
settings.Global.LatestVersion = await latestVersionTask;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
logger.LogInformation("Request timed out.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogInformation(ex, "Upgrade check failed: {Message}", ex.Message);
|
|
}
|
|
}
|
|
|
|
settings.Global.LastVersionCheck = utcNow;
|
|
settings.Save(logger);
|
|
}
|
|
|
|
if (IsUpgradeAvailable(settings, version))
|
|
{
|
|
logger.LogWarning("A newer version of UGS is available ({LatestVersion} > {Version}). Run {Command} to update.", settings.Global.LatestVersion, version, "ugs upgrade");
|
|
logger.LogInformation("");
|
|
}
|
|
}
|
|
|
|
Command instance = (Command)Activator.CreateInstance(command.Type)!;
|
|
await instance.ExecuteAsync(new CommandContext(args, logger, loggerFactory, settings, globalSettings, hordeClient, cloudStorage));
|
|
return 0;
|
|
}
|
|
catch (UserErrorException ex)
|
|
{
|
|
logger.Log(ex.Event.Level, "{Message}", ex.Event.ToString());
|
|
return ex.Code;
|
|
}
|
|
catch (PerforceException ex)
|
|
{
|
|
logger.LogError(ex, "{Message}", ex.Message);
|
|
return 1;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError(ex, "Unhandled exception.\n{Str}", ex);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
static bool IsUpgradeAvailable(GlobalSettingsFile settings, string version)
|
|
{
|
|
return settings.Global.LatestVersion != null && !settings.Global.LatestVersion.Equals(version, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
static void PrintHelp()
|
|
{
|
|
string appName = "UnrealGameSync Command-Line Tool";
|
|
|
|
string? productVersion = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion;
|
|
if (productVersion != null)
|
|
{
|
|
appName = $"{appName} ({productVersion})";
|
|
}
|
|
|
|
Console.WriteLine(appName);
|
|
Console.WriteLine("");
|
|
Console.WriteLine("Usage:");
|
|
foreach (CommandInfo command in _commands)
|
|
{
|
|
if (command.Usage != null && command.Brief != null)
|
|
{
|
|
Console.WriteLine();
|
|
ConsoleUtils.WriteLineWithWordWrap(GetUsage(command), 2, 8);
|
|
ConsoleUtils.WriteLineWithWordWrap(command.Brief, 4, 4);
|
|
}
|
|
}
|
|
}
|
|
|
|
static string GetUsage(CommandInfo commandInfo)
|
|
{
|
|
StringBuilder result = new StringBuilder(commandInfo.Usage);
|
|
if (commandInfo.OptionsType != null)
|
|
{
|
|
foreach (PropertyInfo propertyInfo in commandInfo.OptionsType.GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
|
{
|
|
List<string> names = new List<string>();
|
|
foreach (CommandLineAttribute attribute in propertyInfo.GetCustomAttributes<CommandLineAttribute>())
|
|
{
|
|
string name = (attribute.Prefix ?? propertyInfo.Name).ToLower(CultureInfo.InvariantCulture);
|
|
if (propertyInfo.PropertyType == typeof(bool) || propertyInfo.PropertyType == typeof(bool?))
|
|
{
|
|
names.Add(name);
|
|
}
|
|
else
|
|
{
|
|
names.Add($"{name}..");
|
|
}
|
|
}
|
|
if (names.Count > 0)
|
|
{
|
|
result.Append($" [{String.Join('|', names)}]");
|
|
}
|
|
}
|
|
}
|
|
return result.ToString();
|
|
}
|
|
|
|
static string GetVersion()
|
|
{
|
|
AssemblyInformationalVersionAttribute? version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
|
return version?.InformationalVersion ?? "Unknown";
|
|
}
|
|
|
|
class DeploymentInfo
|
|
{
|
|
public string Version { get; set; } = String.Empty;
|
|
}
|
|
|
|
static string? GetUpgradeToolName()
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
{
|
|
if (RuntimeInformation.OSArchitecture == Architecture.Arm64)
|
|
{
|
|
return "ugs-mac-arm64";
|
|
}
|
|
else
|
|
{
|
|
return "ugs-mac";
|
|
}
|
|
}
|
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
{
|
|
return "ugs-linux";
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static async Task<string?> GetLatestVersionAsync(ILogger? logger, CancellationToken cancellationToken)
|
|
{
|
|
string? hordeUrl = DeploymentSettings.Instance.HordeUrl;
|
|
if (hordeUrl == null)
|
|
{
|
|
logger?.LogError("Horde URL is not set in deployment config file. Cannot upgrade.");
|
|
return null;
|
|
}
|
|
|
|
string? toolName = GetUpgradeToolName();
|
|
if (toolName == null)
|
|
{
|
|
logger?.LogError("Command-line upgrades are not supported on this platform.");
|
|
return null;
|
|
}
|
|
|
|
using (HttpClient httpClient = new HttpClient())
|
|
{
|
|
Uri baseUrl = new Uri(hordeUrl);
|
|
|
|
DeploymentInfo? deploymentInfo;
|
|
try
|
|
{
|
|
deploymentInfo = await httpClient.GetFromJsonAsync<DeploymentInfo>(new Uri(baseUrl, $"api/v1/tools/{toolName}/deployments"), cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger?.LogError(ex, "Failed to query for deployment info: {Message}", ex.Message);
|
|
return null;
|
|
}
|
|
if (deploymentInfo == null)
|
|
{
|
|
logger?.LogError("Failed to query for deployment info.");
|
|
return null;
|
|
}
|
|
return deploymentInfo.Version;
|
|
}
|
|
}
|
|
|
|
public static UserWorkspaceSettings? ReadOptionalUserWorkspaceSettings()
|
|
{
|
|
DirectoryReference? dir = DirectoryReference.GetCurrentDirectory();
|
|
for (; dir != null; dir = dir.ParentDirectory)
|
|
{
|
|
try
|
|
{
|
|
UserWorkspaceSettings? settings;
|
|
if (UserWorkspaceSettings.TryLoad(dir, out settings))
|
|
{
|
|
return settings;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Guard against directories we can't access, eg. /Users/.ugs
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static UserWorkspaceSettings ReadRequiredUserWorkspaceSettings()
|
|
{
|
|
UserWorkspaceSettings? settings = ReadOptionalUserWorkspaceSettings();
|
|
if (settings == null)
|
|
{
|
|
throw new UserErrorException("Unable to find UGS workspace in current directory.");
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
public static async Task<WorkspaceStateWrapper> ReadWorkspaceState(IPerforceConnection perforceClient, UserWorkspaceSettings settings, GlobalSettingsFile userSettings, ILogger logger)
|
|
{
|
|
WorkspaceStateWrapper state = userSettings.FindOrAddWorkspaceState(settings);
|
|
if (state.Current.SettingsTimeUtc != settings.LastModifiedTimeUtc)
|
|
{
|
|
logger.LogDebug("Updating state due to modified settings timestamp");
|
|
ProjectInfo info = await ProjectInfo.CreateAsync(perforceClient, settings, CancellationToken.None);
|
|
state.Modify(x => x.UpdateCachedProjectInfo(info, settings.LastModifiedTimeUtc));
|
|
}
|
|
return state;
|
|
}
|
|
|
|
public static Task<IPerforceConnection> ConnectAsync(string? serverAndPort, string? userName, string? clientName, ILoggerFactory loggerFactory)
|
|
{
|
|
PerforceSettings settings = new PerforceSettings(PerforceSettings.Default);
|
|
settings.ClientName = clientName;
|
|
settings.PreferNativeClient = true;
|
|
if (!String.IsNullOrEmpty(serverAndPort))
|
|
{
|
|
settings.ServerAndPort = serverAndPort;
|
|
}
|
|
if (!String.IsNullOrEmpty(userName))
|
|
{
|
|
settings.UserName = userName;
|
|
}
|
|
|
|
return PerforceConnection.CreateAsync(settings, loggerFactory.CreateLogger("Perforce"));
|
|
}
|
|
|
|
public static Task<IPerforceConnection> ConnectAsync(UserWorkspaceSettings settings, ILoggerFactory loggerFactory)
|
|
{
|
|
return ConnectAsync(settings.ServerAndPort, settings.UserName, settings.ClientName, loggerFactory);
|
|
}
|
|
|
|
static string[] ReadSyncFilter(UserWorkspaceSettings workspaceSettings, GlobalSettingsFile userSettings, ConfigFile projectConfig, string projectIdentifier)
|
|
{
|
|
Dictionary<Guid, WorkspaceSyncCategory> syncCategories = ConfigUtils.GetSyncCategories(projectConfig);
|
|
|
|
// check if any category is from the role
|
|
IDictionary<string, Preset> roles = ConfigUtils.GetPresets(projectConfig, projectIdentifier);
|
|
if (roles.TryGetValue(workspaceSettings.Preset, out Preset? role))
|
|
{
|
|
foreach (RoleCategory roleCategory in role.Categories.Values)
|
|
{
|
|
if (syncCategories.TryGetValue(roleCategory.Id, out WorkspaceSyncCategory? category))
|
|
{
|
|
category.Enable = roleCategory.Enabled;
|
|
}
|
|
}
|
|
}
|
|
|
|
ConfigSection? perforceSection = projectConfig.FindSection("Perforce");
|
|
|
|
string[] combinedSyncFilter = GlobalSettingsFile.GetCombinedSyncFilter(syncCategories, workspaceSettings.Preset, roles, userSettings.Global.Filter, workspaceSettings.Filter, perforceSection);
|
|
|
|
return combinedSyncFilter;
|
|
}
|
|
|
|
static async Task<string> FindProjectPathAsync(IPerforceConnection perforce, string clientName, string branchPath, string? projectName)
|
|
{
|
|
using IPerforceConnection perforceClient = await PerforceConnection.CreateAsync(new PerforceSettings(perforce.Settings) { ClientName = clientName }, perforce.Logger);
|
|
|
|
// Find or validate the selected project
|
|
string searchPath;
|
|
if (projectName == null)
|
|
{
|
|
searchPath = $"//{clientName}{branchPath}/*.uprojectdirs";
|
|
}
|
|
else if (projectName.Contains('.', StringComparison.Ordinal))
|
|
{
|
|
searchPath = $"//{clientName}{branchPath}/{projectName.TrimStart('/')}";
|
|
}
|
|
else
|
|
{
|
|
searchPath = $"//{clientName}{branchPath}/.../{projectName}.uproject";
|
|
}
|
|
|
|
List<FStatRecord> projectFileRecords = await perforceClient.FStatAsync(FStatOptions.ClientFileInPerforceSyntax, searchPath).ToListAsync();
|
|
projectFileRecords.RemoveAll(x => x.HeadAction == FileAction.Delete || x.HeadAction == FileAction.MoveDelete);
|
|
projectFileRecords.RemoveAll(x => !x.IsMapped);
|
|
|
|
List<string> paths = projectFileRecords.Select(x => PerforceUtils.GetClientRelativePath(x.ClientFile!)).Distinct(StringComparer.Ordinal).ToList();
|
|
if (paths.Count == 0)
|
|
{
|
|
throw new UserErrorException("No project file found matching {SearchPath}", searchPath);
|
|
}
|
|
if (paths.Count > 1)
|
|
{
|
|
throw new UserErrorException("Multiple projects found matching {SearchPath}: {Paths}", searchPath, String.Join(", ", paths));
|
|
}
|
|
|
|
return "/" + paths[0];
|
|
}
|
|
|
|
abstract class Command
|
|
{
|
|
public abstract Task ExecuteAsync(CommandContext context);
|
|
}
|
|
|
|
class InitCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
// Get the positional argument indicating the file to look for
|
|
string? initName;
|
|
context.Arguments.TryGetPositionalArgument(out initName);
|
|
|
|
// Get the config settings from the command line
|
|
InitCommandOptions options = new InitCommandOptions();
|
|
context.Arguments.ApplyTo(options);
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
// Get the host name
|
|
using IPerforceConnection perforce = await ConnectAsync(options.ServerAndPort, options.UserName, null, context.LoggerFactory);
|
|
InfoRecord perforceInfo = await perforce.GetInfoAsync(InfoOptions.ShortOutput);
|
|
string hostName = perforceInfo.ClientHost ?? Dns.GetHostName();
|
|
|
|
// Create the perforce connection
|
|
if (initName != null)
|
|
{
|
|
await InitNewClientAsync(perforce, initName, hostName, options, context.GlobalSettings, logger);
|
|
}
|
|
else
|
|
{
|
|
await InitExistingClientAsync(perforce, hostName, options, context.GlobalSettings, logger);
|
|
}
|
|
}
|
|
|
|
static async Task InitNewClientAsync(IPerforceConnection perforce, string streamName, string hostName, InitCommandOptions options, UserSettings? globalSettings, ILogger logger)
|
|
{
|
|
logger.LogInformation("Checking stream...");
|
|
|
|
// Get the given stream
|
|
PerforceResponse<StreamRecord> streamResponse = await perforce.TryGetStreamAsync(streamName, true);
|
|
if (!streamResponse.Succeeded)
|
|
{
|
|
throw new UserErrorException($"Unable to find stream '{streamName}'");
|
|
}
|
|
StreamRecord stream = streamResponse.Data;
|
|
|
|
// Get the new directory for the client
|
|
DirectoryReference clientDir = DirectoryReference.FromString(options.ClientRoot) ?? DirectoryReference.Combine(DirectoryReference.GetCurrentDirectory(), stream.Stream.Replace('/', '+'));
|
|
DirectoryReference.CreateDirectory(clientDir);
|
|
|
|
// Make up a new client name
|
|
string clientName = options.ClientName ?? Regex.Replace($"{perforce.Settings.UserName}_{hostName}_{stream.Stream.Trim('/')}", "[^0-9a-zA-Z_.-]", "+");
|
|
|
|
if (!options.IgnoreExistingClients)
|
|
{
|
|
// Check there are no existing clients under the current path
|
|
List<ClientsRecord> clients = await FindExistingClients(perforce, hostName, clientDir);
|
|
if (clients.Count > 0)
|
|
{
|
|
if (clients.Count == 1 && clientName.Equals(clients[0].Name, StringComparison.OrdinalIgnoreCase) && clientDir == TryParseRoot(clients[0].Root))
|
|
{
|
|
logger.LogInformation("Reusing existing client for {ClientDir} ({ClientName})", clientDir, options.ClientName);
|
|
}
|
|
else
|
|
{
|
|
throw new UserErrorException("Current directory is already within a Perforce workspace ({ClientName})", clients[0].Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create the new client
|
|
ClientRecord client = new ClientRecord(clientName, perforce.Settings.UserName, clientDir.FullName);
|
|
client.Host = hostName;
|
|
client.Stream = stream.Stream;
|
|
client.Options = ClientOptions.Rmdir;
|
|
await perforce.CreateClientAsync(client);
|
|
|
|
// Branch root is currently hard-coded at the root
|
|
string branchPath = options.BranchPath ?? String.Empty;
|
|
string projectPath = await FindProjectPathAsync(perforce, clientName, branchPath, options.ProjectName);
|
|
|
|
// Create the settings object
|
|
UserWorkspaceSettings settings = new UserWorkspaceSettings();
|
|
settings.RootDir = clientDir;
|
|
settings.Init(perforce.Settings.ServerAndPort, perforce.Settings.UserName, clientName, branchPath, projectPath);
|
|
options.ApplyTo(settings);
|
|
settings.Save(logger);
|
|
|
|
if (globalSettings != null)
|
|
{
|
|
UserSelectedProjectSettings selectedProjectSettings =
|
|
new UserSelectedProjectSettings(
|
|
null,
|
|
null,
|
|
UserSelectedProjectType.Client,
|
|
$"//{client.Name}{projectPath}",
|
|
$"{client.Root}{projectPath}".Replace("/", @"\", StringComparison.Ordinal));
|
|
globalSettings.OpenProjects.Add(selectedProjectSettings);
|
|
globalSettings.RecentProjects.Add(selectedProjectSettings);
|
|
globalSettings.Save(logger);
|
|
}
|
|
|
|
logger.LogInformation("Initialized {ClientName} with root at {RootDir}", clientName, clientDir);
|
|
}
|
|
|
|
static DirectoryReference? TryParseRoot(string root)
|
|
{
|
|
try
|
|
{
|
|
return new DirectoryReference(root);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static async Task InitExistingClientAsync(IPerforceConnection perforce, string hostName, InitCommandOptions options, UserSettings? globalSettings, ILogger logger)
|
|
{
|
|
DirectoryReference currentDir = DirectoryReference.GetCurrentDirectory();
|
|
|
|
// Make sure the client name is set
|
|
string? clientName = options.ClientName;
|
|
if (clientName == null)
|
|
{
|
|
List<ClientsRecord> clients = await FindExistingClients(perforce, hostName, currentDir);
|
|
if (clients.Count == 0)
|
|
{
|
|
throw new UserErrorException("Unable to find client for {HostName} under {ClientDir}", hostName, currentDir);
|
|
}
|
|
if (clients.Count > 1)
|
|
{
|
|
throw new UserErrorException("Multiple clients found for {HostName} under {ClientDir}: {ClientList}", hostName, currentDir, String.Join(", ", clients.Select(x => x.Name)));
|
|
}
|
|
|
|
clientName = clients[0].Name;
|
|
logger.LogInformation("Found client {ClientName}", clientName);
|
|
}
|
|
|
|
// Get the client info
|
|
ClientRecord client = await perforce.GetClientAsync(clientName);
|
|
DirectoryReference clientDir = new DirectoryReference(client.Root);
|
|
|
|
// If a project path was specified in local syntax, try to convert it to client-relative syntax
|
|
string? projectName = options.ProjectName;
|
|
if (options.ProjectName != null && options.ProjectName.Contains('.', StringComparison.Ordinal))
|
|
{
|
|
options.ProjectName = FileReference.Combine(currentDir, options.ProjectName).MakeRelativeTo(clientDir).Replace('\\', '/');
|
|
}
|
|
|
|
// Branch root is currently hard-coded at the root
|
|
string branchPath = options.BranchPath ?? String.Empty;
|
|
string projectPath = await FindProjectPathAsync(perforce, clientName, branchPath, projectName);
|
|
|
|
// Create the settings object
|
|
UserWorkspaceSettings settings = new UserWorkspaceSettings();
|
|
settings.RootDir = clientDir;
|
|
settings.Init(perforce.Settings.ServerAndPort, perforce.Settings.UserName, clientName, branchPath, projectPath);
|
|
options.ApplyTo(settings);
|
|
settings.Save(logger);
|
|
|
|
if (globalSettings != null)
|
|
{
|
|
UserSelectedProjectSettings selectedProjectSettings =
|
|
new UserSelectedProjectSettings(
|
|
null,
|
|
null,
|
|
UserSelectedProjectType.Client,
|
|
$"//{client.Name}{projectPath}",
|
|
$"{client.Root}{projectPath}".Replace("/", @"\", StringComparison.Ordinal));
|
|
globalSettings.OpenProjects.Add(selectedProjectSettings);
|
|
globalSettings.RecentProjects.Add(selectedProjectSettings);
|
|
globalSettings.Save(logger);
|
|
}
|
|
|
|
logger.LogInformation("Initialized workspace at {RootDir} for {ClientProject}", clientDir, settings.ClientProjectPath);
|
|
}
|
|
|
|
static async Task<List<ClientsRecord>> FindExistingClients(IPerforceConnection perforce, string hostName, DirectoryReference clientDir)
|
|
{
|
|
List<ClientsRecord> matchingClients = new List<ClientsRecord>();
|
|
|
|
List<ClientsRecord> clients = await perforce.GetClientsAsync(ClientsOptions.None, perforce.Settings.UserName);
|
|
foreach (ClientsRecord client in clients)
|
|
{
|
|
if (!String.IsNullOrEmpty(client.Root) && !String.IsNullOrEmpty(client.Host) && String.Equals(hostName, client.Host, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
DirectoryReference? rootDir;
|
|
try
|
|
{
|
|
rootDir = new DirectoryReference(client.Root);
|
|
}
|
|
catch
|
|
{
|
|
rootDir = null;
|
|
}
|
|
|
|
if (rootDir != null && clientDir.IsUnderDirectory(rootDir))
|
|
{
|
|
matchingClients.Add(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
return matchingClients;
|
|
}
|
|
}
|
|
|
|
class SyncCommandOptions
|
|
{
|
|
[CommandLine("-Clean")]
|
|
public bool Clean { get; set; }
|
|
|
|
[CommandLine("-Build")]
|
|
public bool Build { get; set; }
|
|
|
|
[CommandLine("-Binaries")]
|
|
public bool Binaries { get; set; }
|
|
|
|
[CommandLine("-NoGPF", Value = "false")]
|
|
[CommandLine("-NoProjectFiles", Value = "false")]
|
|
public bool ProjectFiles { get; set; } = true;
|
|
|
|
[CommandLine("-Clobber")]
|
|
public bool Clobber { get; set; }
|
|
|
|
[CommandLine("-Refilter")]
|
|
public bool Refilter { get; set; }
|
|
|
|
[CommandLine("-Only")]
|
|
public bool SingleChange { get; set; }
|
|
}
|
|
|
|
class SyncCommand : Command
|
|
{
|
|
static async Task<bool> IsCodeChangeAsync(IPerforceConnection perforce, int change)
|
|
{
|
|
DescribeRecord describeRecord = await perforce.DescribeAsync(change);
|
|
return IsCodeChange(describeRecord);
|
|
}
|
|
|
|
static bool IsCodeChange(DescribeRecord describeRecord)
|
|
{
|
|
foreach (DescribeFileRecord file in describeRecord.Files)
|
|
{
|
|
if (PerforceUtils.CodeExtensions.Any(extension => file.DepotFile.EndsWith(extension, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
context.Arguments.TryGetPositionalArgument(out string? changeString);
|
|
|
|
SyncCommandOptions syncOptions = new SyncCommandOptions();
|
|
context.Arguments.ApplyTo(syncOptions);
|
|
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
using IPerforceConnection perforceClient = await ConnectAsync(settings, context.LoggerFactory);
|
|
WorkspaceStateWrapper state = await ReadWorkspaceState(perforceClient, settings, context.UserSettings, logger);
|
|
|
|
changeString ??= "latest";
|
|
|
|
ProjectInfo projectInfo = new ProjectInfo(settings.RootDir, state.Current);
|
|
UserProjectSettings projectSettings = context.UserSettings.FindOrAddProjectSettings(projectInfo, settings, logger);
|
|
|
|
ConfigFile projectConfig = await ConfigUtils.ReadProjectConfigFileAsync(perforceClient, projectInfo, logger, CancellationToken.None);
|
|
|
|
bool syncLatest = String.Equals(changeString, "latest", StringComparison.OrdinalIgnoreCase);
|
|
|
|
int change;
|
|
if (!Int32.TryParse(changeString, out change))
|
|
{
|
|
if (syncLatest)
|
|
{
|
|
List<ChangesRecord> changes = await perforceClient.GetChangesAsync(ChangesOptions.None, 1, ChangeStatus.Submitted, $"//{settings.ClientName}/...");
|
|
change = changes[0].Number;
|
|
}
|
|
else
|
|
{
|
|
throw new UserErrorException("Unknown change type for sync '{Change}'", changeString);
|
|
}
|
|
}
|
|
|
|
WorkspaceUpdateOptions options = syncOptions.SingleChange ? WorkspaceUpdateOptions.SyncSingleChange : WorkspaceUpdateOptions.Sync;
|
|
if (syncOptions.Clean)
|
|
{
|
|
options |= WorkspaceUpdateOptions.Clean;
|
|
}
|
|
if (syncOptions.Build)
|
|
{
|
|
options |= WorkspaceUpdateOptions.Build;
|
|
}
|
|
if (syncOptions.ProjectFiles)
|
|
{
|
|
options |= WorkspaceUpdateOptions.GenerateProjectFiles;
|
|
}
|
|
if (syncOptions.Clobber)
|
|
{
|
|
options |= WorkspaceUpdateOptions.Clobber;
|
|
}
|
|
if (syncOptions.Refilter)
|
|
{
|
|
options |= WorkspaceUpdateOptions.Refilter;
|
|
}
|
|
options |= WorkspaceUpdateContext.GetOptionsFromConfig(context.UserSettings.Global, settings);
|
|
options |= WorkspaceUpdateOptions.RemoveFilteredFiles;
|
|
|
|
string[] syncFilter = ReadSyncFilter(settings, context.UserSettings, projectConfig, state.Current.ProjectIdentifier);
|
|
|
|
using WorkspaceLock? workspaceLock = CreateWorkspaceLock(settings.RootDir);
|
|
if (workspaceLock != null && !await workspaceLock.TryAcquireAsync())
|
|
{
|
|
logger.LogError("Another process is already syncing this workspace.");
|
|
return;
|
|
}
|
|
|
|
WorkspaceUpdateContext updateContext = new WorkspaceUpdateContext(change, options, BuildConfig.Development, syncFilter, projectSettings.BuildSteps, null);
|
|
updateContext.PerforceSyncOptions = context.UserSettings.Global.Perforce;
|
|
|
|
if (syncOptions.Binaries)
|
|
{
|
|
List<BaseArchiveChannel> archives = await BaseArchive.EnumerateChannelsAsync(perforceClient, context.HordeClient, context.CloudStorage, projectConfig, state.Current.ProjectIdentifier, CancellationToken.None);
|
|
|
|
BaseArchiveChannel? editorArchiveInfo = archives.FirstOrDefault(x => x.Type == IArchiveChannel.EditorArchiveType);
|
|
if (editorArchiveInfo == null)
|
|
{
|
|
throw new UserErrorException("No editor archives found for project");
|
|
}
|
|
|
|
KeyValuePair<int, IArchive> revision = editorArchiveInfo.ChangeNumberToArchive.LastOrDefault(x => x.Key <= change);
|
|
if (revision.Key == 0)
|
|
{
|
|
throw new UserErrorException($"No editor archives found for CL {change}");
|
|
}
|
|
|
|
if (revision.Key < change)
|
|
{
|
|
int lastChange = revision.Key;
|
|
|
|
List<ChangesRecord> changeRecords = await perforceClient.GetChangesAsync(ChangesOptions.None, 1, ChangeStatus.Submitted, $"//{settings.ClientName}/...@{revision.Key + 1},{change}");
|
|
foreach (ChangesRecord changeRecord in changeRecords.OrderBy(x => x.Number))
|
|
{
|
|
if (await IsCodeChangeAsync(perforceClient, changeRecord.Number))
|
|
{
|
|
if (syncLatest)
|
|
{
|
|
updateContext.ChangeNumber = lastChange;
|
|
}
|
|
else
|
|
{
|
|
throw new UserErrorException($"No editor binaries found for CL {change} (last archive at CL {revision.Key}, but CL {changeRecord.Number} is a code change)");
|
|
}
|
|
break;
|
|
}
|
|
change = changeRecord.Number;
|
|
}
|
|
}
|
|
|
|
updateContext.Options |= WorkspaceUpdateOptions.SyncArchives;
|
|
updateContext.ArchiveTypeToArchive[IArchiveChannel.EditorArchiveType] = revision.Value;
|
|
}
|
|
|
|
WorkspaceUpdate update = new WorkspaceUpdate(updateContext);
|
|
(WorkspaceUpdateResult result, string message) = await update.ExecuteAsync(perforceClient.Settings, projectInfo, state, context.Logger, CancellationToken.None);
|
|
if (result == WorkspaceUpdateResult.FilesToClobber)
|
|
{
|
|
logger.LogWarning("The following files are modified in your workspace:");
|
|
foreach (string file in updateContext.ClobberFiles.Keys.OrderBy(x => x))
|
|
{
|
|
logger.LogWarning(" {File}", file);
|
|
}
|
|
logger.LogWarning("Use -Clobber to overwrite");
|
|
}
|
|
|
|
state.Modify(x => x.SetLastSyncState(result, updateContext, message));
|
|
|
|
if (result != WorkspaceUpdateResult.Success)
|
|
{
|
|
throw new UserErrorException("{Message} (Result: {Result})", message, result);
|
|
}
|
|
}
|
|
|
|
static WorkspaceLock? CreateWorkspaceLock(DirectoryReference rootDir)
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
return new WorkspaceLock(rootDir);
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class ClientsCommandOptions : ServerOptions
|
|
{
|
|
}
|
|
|
|
class ClientsCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
ClientsCommandOptions options = context.Arguments.ApplyTo<ClientsCommandOptions>(logger);
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
using IPerforceConnection perforceClient = await ConnectAsync(options.ServerAndPort, options.UserName, null, context.LoggerFactory);
|
|
InfoRecord info = await perforceClient.GetInfoAsync(InfoOptions.ShortOutput);
|
|
|
|
List<ClientsRecord> clients = await perforceClient.GetClientsAsync(EpicGames.Perforce.ClientsOptions.None, perforceClient.Settings.UserName);
|
|
foreach (ClientsRecord client in clients)
|
|
{
|
|
if (String.Equals(info.ClientHost, client.Host, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
logger.LogInformation("{Client,-50} {Root}", client.Name, client.Root);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class RunCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
using IPerforceConnection perforceClient = await ConnectAsync(settings, context.LoggerFactory);
|
|
WorkspaceStateWrapper state = await ReadWorkspaceState(perforceClient, settings, context.UserSettings, logger);
|
|
|
|
ProjectInfo projectInfo = new ProjectInfo(settings.RootDir, state.Current);
|
|
ConfigFile projectConfig = await ConfigUtils.ReadProjectConfigFileAsync(perforceClient, projectInfo, logger, CancellationToken.None);
|
|
|
|
FileReference receiptFile = ConfigUtils.GetEditorReceiptFile(projectInfo, projectConfig, EditorConfig);
|
|
logger.LogDebug("Receipt file: {Receipt}", receiptFile);
|
|
|
|
if (!ConfigUtils.TryReadEditorReceipt(projectInfo, receiptFile, out TargetReceipt? receipt) || String.IsNullOrEmpty(receipt.Launch))
|
|
{
|
|
throw new UserErrorException("The editor needs to be built before you can run it. (Missing {ReceiptFile}).", receiptFile);
|
|
}
|
|
if (!File.Exists(receipt.Launch))
|
|
{
|
|
throw new UserErrorException("The editor needs to be built before you can run it. (Missing {LaunchFile}).", receipt.Launch);
|
|
}
|
|
|
|
List<string> launchArguments = new List<string>();
|
|
if (settings.LocalProjectPath.HasExtension(".uproject"))
|
|
{
|
|
launchArguments.Add($"\"{settings.LocalProjectPath}\"");
|
|
}
|
|
if (EditorConfig == BuildConfig.Debug || EditorConfig == BuildConfig.DebugGame)
|
|
{
|
|
launchArguments.Append(" -debug");
|
|
}
|
|
for (int idx = 0; idx < context.Arguments.Count; idx++)
|
|
{
|
|
if (!context.Arguments.HasBeenUsed(idx))
|
|
{
|
|
launchArguments.Add(context.Arguments[idx]);
|
|
}
|
|
}
|
|
|
|
string commandLine = CommandLineArguments.Join(launchArguments);
|
|
logger.LogInformation("Spawning: {LaunchFile} {CommandLine}", CommandLineArguments.Quote(receipt.Launch), commandLine);
|
|
|
|
if (!Utility.SpawnProcess(receipt.Launch, commandLine))
|
|
{
|
|
logger.LogError("Unable to spawn {App} {Args}", receipt.Launch, launchArguments.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
class ChangesCommandOptions
|
|
{
|
|
[CommandLine("-Count=")]
|
|
public int Count { get; set; } = 10;
|
|
|
|
[CommandLine("-Lines=")]
|
|
public int LineCount { get; set; } = 3;
|
|
}
|
|
|
|
class ChangesCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
ChangesCommandOptions options = new ChangesCommandOptions();
|
|
context.Arguments.ApplyTo(options);
|
|
context.Arguments.CheckAllArgumentsUsed(context.Logger);
|
|
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
using IPerforceConnection perforceClient = await ConnectAsync(settings, context.LoggerFactory);
|
|
|
|
List<ChangesRecord> changes = await perforceClient.GetChangesAsync(EpicGames.Perforce.ChangesOptions.None, options.Count, ChangeStatus.Submitted, $"//{settings.ClientName}/...");
|
|
foreach (IEnumerable<ChangesRecord> changesBatch in changes.Batch(10))
|
|
{
|
|
List<DescribeRecord> describeRecords = await perforceClient.DescribeAsync(changesBatch.Select(x => x.Number).ToArray());
|
|
|
|
logger.LogInformation(" Change Type Author Description");
|
|
foreach (DescribeRecord describeRecord in describeRecords)
|
|
{
|
|
PerforceChangeDetails details = new PerforceChangeDetails(describeRecord);
|
|
|
|
string type;
|
|
if (details.ContainsCode)
|
|
{
|
|
if (details.ContainsContent)
|
|
{
|
|
type = "Both";
|
|
}
|
|
else
|
|
{
|
|
type = "Code";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (details.ContainsContent)
|
|
{
|
|
type = "Content";
|
|
}
|
|
else
|
|
{
|
|
type = "None";
|
|
}
|
|
}
|
|
|
|
string author = StringUtils.Truncate(describeRecord.User, 15);
|
|
|
|
List<string> lines = StringUtils.WordWrap(details.Description, Math.Max(ConsoleUtils.WindowWidth - 40, 10)).ToList();
|
|
if (lines.Count == 0)
|
|
{
|
|
lines.Add(String.Empty);
|
|
}
|
|
|
|
int lineCount = Math.Min(options.LineCount, lines.Count);
|
|
|
|
logger.LogInformation(" {Change,-9} {Type,-8} {Author,-15} {Description}", describeRecord.Number, type, author, lines[0]);
|
|
for (int lineIndex = 1; lineIndex < lineCount; lineIndex++)
|
|
{
|
|
logger.LogInformation(" {Description}", lines[lineIndex]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ConfigCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
if (!context.Arguments.GetUnusedArguments().Any())
|
|
{
|
|
ProcessStartInfo startInfo = new ProcessStartInfo();
|
|
startInfo.FileName = settings.ConfigFile.FullName;
|
|
startInfo.UseShellExecute = true;
|
|
using (Process? editor = Process.Start(startInfo))
|
|
{
|
|
if (editor != null)
|
|
{
|
|
await editor.WaitForExitAsync();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ConfigCommandOptions options = new ConfigCommandOptions();
|
|
context.Arguments.ApplyTo(options);
|
|
context.Arguments.CheckAllArgumentsUsed(context.Logger);
|
|
|
|
options.ApplyTo(settings);
|
|
settings.Save(logger);
|
|
|
|
logger.LogInformation("Updated {ConfigFile}", settings.ConfigFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
class FilterCommandOptions
|
|
{
|
|
[CommandLine("-Reset")]
|
|
public bool Reset { get; set; } = false;
|
|
|
|
[CommandLine("-Include=")]
|
|
public List<string> Include { get; set; } = new List<string>();
|
|
|
|
[CommandLine("-Exclude=")]
|
|
public List<string> Exclude { get; set; } = new List<string>();
|
|
|
|
[CommandLine("-View=", ListSeparator = ';')]
|
|
public List<string>? View { get; set; }
|
|
|
|
[CommandLine("-AddView=", ListSeparator = ';')]
|
|
public List<string> AddView { get; set; } = new List<string>();
|
|
|
|
[CommandLine("-RemoveView=", ListSeparator = ';')]
|
|
public List<string> RemoveView { get; set; } = new List<string>();
|
|
|
|
[CommandLine("-AllProjects", Value = "true")]
|
|
[CommandLine("-OnlyCurrent", Value = "false")]
|
|
public bool? AllProjects { get; set; } = null;
|
|
|
|
[CommandLine("-GpfAllProjects", Value = "true")]
|
|
[CommandLine("-GpfOnlyCurrent", Value = "false")]
|
|
public bool? AllProjectsInSln { get; set; } = null;
|
|
|
|
[CommandLine("-GpfMinimalSln", Value = "true")]
|
|
[CommandLine("-GpfFullSln", Value = "false")]
|
|
public bool? UprojectSpecificSln { get; set; } = null;
|
|
|
|
[CommandLine("-Global")]
|
|
public bool Global { get; set; }
|
|
}
|
|
|
|
class FilterCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
UserWorkspaceSettings workspaceSettings = ReadRequiredUserWorkspaceSettings();
|
|
using IPerforceConnection perforceClient = await ConnectAsync(workspaceSettings, context.LoggerFactory);
|
|
WorkspaceStateWrapper workspaceState = await ReadWorkspaceState(perforceClient, workspaceSettings, context.UserSettings, logger);
|
|
ProjectInfo projectInfo = new ProjectInfo(workspaceSettings.RootDir, workspaceState.Current);
|
|
|
|
ConfigFile projectConfig = await ConfigUtils.ReadProjectConfigFileAsync(perforceClient, projectInfo, logger, CancellationToken.None);
|
|
Dictionary<Guid, WorkspaceSyncCategory> syncCategories = ConfigUtils.GetSyncCategories(projectConfig);
|
|
|
|
FilterSettings globalFilter = context.UserSettings.Global.Filter;
|
|
FilterSettings workspaceFilter = workspaceSettings.Filter;
|
|
|
|
IDictionary<string, Preset> roles = ConfigUtils.GetPresets(projectConfig, workspaceState.Current.ProjectIdentifier);
|
|
|
|
roles.TryGetValue(workspaceSettings.Preset, out Preset? role);
|
|
|
|
FilterCommandOptions options = context.Arguments.ApplyTo<FilterCommandOptions>(logger);
|
|
context.Arguments.CheckAllArgumentsUsed(context.Logger);
|
|
|
|
if (options.Global)
|
|
{
|
|
ApplyCommandOptions(context.UserSettings.Global.Filter, options, syncCategories.Values, logger);
|
|
context.UserSettings.Save(logger);
|
|
}
|
|
else
|
|
{
|
|
ApplyCommandOptions(workspaceSettings.Filter, options, syncCategories.Values, logger);
|
|
workspaceSettings.Save(logger);
|
|
}
|
|
|
|
Dictionary<Guid, bool> globalCategories = globalFilter.GetCategories();
|
|
Dictionary<Guid, bool> workspaceCategories = workspaceFilter.GetCategories();
|
|
|
|
logger.LogInformation("Categories:");
|
|
foreach (WorkspaceSyncCategory syncCategory in syncCategories.Values)
|
|
{
|
|
bool enabled;
|
|
|
|
string scope = "(Default)";
|
|
if (globalCategories.TryGetValue(syncCategory.UniqueId, out enabled))
|
|
{
|
|
scope = "(Global)";
|
|
}
|
|
else if (workspaceCategories.TryGetValue(syncCategory.UniqueId, out enabled))
|
|
{
|
|
scope = "(Workspace)";
|
|
}
|
|
else
|
|
{
|
|
enabled = syncCategory.Enable;
|
|
}
|
|
|
|
if (role != null && role.Categories.TryGetValue(syncCategory.UniqueId, out RoleCategory? roleCategory))
|
|
{
|
|
scope = $"(Preset: {role.Name})";
|
|
enabled = roleCategory.Enabled;
|
|
}
|
|
|
|
logger.LogInformation(" {Id,30} {Enabled,3} {Scope,-9} {Name}", syncCategory.UniqueId, enabled ? "Yes" : "No", scope, syncCategory.Name);
|
|
}
|
|
|
|
if (globalFilter.View.Count > 0)
|
|
{
|
|
logger.LogInformation("");
|
|
logger.LogInformation("Global View:");
|
|
foreach (string line in globalFilter.View)
|
|
{
|
|
logger.LogInformation(" {Line}", line);
|
|
}
|
|
}
|
|
if (workspaceFilter.View.Count > 0)
|
|
{
|
|
logger.LogInformation("");
|
|
logger.LogInformation("Workspace View:");
|
|
foreach (string line in workspaceFilter.View)
|
|
{
|
|
logger.LogInformation(" {Line}", line);
|
|
}
|
|
}
|
|
if (role != null && role.Views.Count > 0)
|
|
{
|
|
logger.LogInformation("");
|
|
logger.LogInformation("Preset View:");
|
|
foreach (string line in role.Views)
|
|
{
|
|
logger.LogInformation(" {Line}", line);
|
|
}
|
|
}
|
|
|
|
string[] filter = ReadSyncFilter(workspaceSettings, context.UserSettings, projectConfig, workspaceState.Current.ProjectIdentifier);
|
|
|
|
logger.LogInformation("");
|
|
logger.LogInformation("Combined view:");
|
|
foreach (string filterLine in filter)
|
|
{
|
|
logger.LogInformation(" {FilterLine}", filterLine);
|
|
}
|
|
}
|
|
|
|
static void ApplyCommandOptions(FilterSettings settings, FilterCommandOptions commandOptions, IEnumerable<WorkspaceSyncCategory> syncCategories, ILogger logger)
|
|
{
|
|
if (commandOptions.Reset)
|
|
{
|
|
logger.LogInformation("Resetting settings...");
|
|
settings.Reset();
|
|
}
|
|
|
|
HashSet<Guid> includeCategories = new HashSet<Guid>(commandOptions.Include.Select(x => GetCategoryId(x, syncCategories)));
|
|
HashSet<Guid> excludeCategories = new HashSet<Guid>(commandOptions.Exclude.Select(x => GetCategoryId(x, syncCategories)));
|
|
|
|
Guid id = includeCategories.FirstOrDefault(x => excludeCategories.Contains(x));
|
|
if (id != Guid.Empty)
|
|
{
|
|
throw new UserErrorException("Category {Id} cannot be both included and excluded", id);
|
|
}
|
|
|
|
includeCategories.ExceptWith(settings.IncludeCategories);
|
|
settings.IncludeCategories.AddRange(includeCategories);
|
|
|
|
excludeCategories.ExceptWith(settings.ExcludeCategories);
|
|
settings.ExcludeCategories.AddRange(excludeCategories);
|
|
|
|
if (commandOptions.View != null)
|
|
{
|
|
settings.View.Clear();
|
|
settings.View.AddRange(commandOptions.View);
|
|
}
|
|
if (commandOptions.RemoveView.Count > 0)
|
|
{
|
|
HashSet<string> viewRemove = new HashSet<string>(commandOptions.RemoveView, StringComparer.OrdinalIgnoreCase);
|
|
settings.View.RemoveAll(x => viewRemove.Contains(x));
|
|
}
|
|
if (commandOptions.AddView.Count > 0)
|
|
{
|
|
HashSet<string> viewLines = new HashSet<string>(settings.View, StringComparer.OrdinalIgnoreCase);
|
|
settings.View.AddRange(commandOptions.AddView.Where(x => !viewLines.Contains(x)));
|
|
}
|
|
|
|
settings.AllProjects = commandOptions.AllProjects ?? settings.AllProjects;
|
|
settings.AllProjectsInSln = commandOptions.AllProjectsInSln ?? settings.AllProjectsInSln;
|
|
settings.UprojectSpecificSln = commandOptions.UprojectSpecificSln ?? settings.UprojectSpecificSln;
|
|
}
|
|
|
|
static Guid GetCategoryId(string text, IEnumerable<WorkspaceSyncCategory> syncCategories)
|
|
{
|
|
Guid id;
|
|
if (Guid.TryParse(text, out id))
|
|
{
|
|
return id;
|
|
}
|
|
|
|
WorkspaceSyncCategory? category = syncCategories.FirstOrDefault(x => x.Name.Equals(text, StringComparison.OrdinalIgnoreCase));
|
|
if (category != null)
|
|
{
|
|
return category.UniqueId;
|
|
}
|
|
|
|
throw new UserErrorException("Unable to find category '{Category}'", text);
|
|
}
|
|
}
|
|
|
|
class BuildCommandOptions
|
|
{
|
|
[CommandLine("-List")]
|
|
public bool ListOnly { get; set; }
|
|
}
|
|
|
|
class BuildCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
context.Arguments.TryGetPositionalArgument(out string? target);
|
|
|
|
BuildCommandOptions options = new BuildCommandOptions();
|
|
context.Arguments.ApplyTo(options);
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
using IPerforceConnection perforceClient = await ConnectAsync(settings, context.LoggerFactory);
|
|
WorkspaceStateWrapper state = await ReadWorkspaceState(perforceClient, settings, context.UserSettings, logger);
|
|
|
|
ProjectInfo projectInfo = new ProjectInfo(settings.RootDir, state.Current);
|
|
|
|
UserProjectSettings projectSettings = context.UserSettings.FindOrAddProjectSettings(projectInfo, settings, logger);
|
|
|
|
ConfigFile projectConfig = await ConfigUtils.ReadProjectConfigFileAsync(perforceClient, projectInfo, logger, CancellationToken.None);
|
|
FileReference editorTarget = ConfigUtils.GetEditorTargetFile(projectInfo, projectConfig);
|
|
|
|
Dictionary<Guid, ConfigObject> buildStepObjects = ConfigUtils.GetDefaultBuildStepObjects(projectInfo, editorTarget.GetFileNameWithoutAnyExtensions(), EditorConfig, projectConfig, false);
|
|
|
|
if (context.GlobalSettings != null)
|
|
{
|
|
BuildStep.MergeBuildStepObjects(buildStepObjects, projectConfig.GetValues("Build.Step", Array.Empty<string>()).Select(x => new ConfigObject(x)));
|
|
BuildStep.MergeBuildStepObjects(buildStepObjects, projectSettings.BuildSteps);
|
|
}
|
|
|
|
if (options.ListOnly)
|
|
{
|
|
List<BuildStep> buildSteps = buildStepObjects.Values.Select(x => new BuildStep(x)).OrderBy(x => x.NormalSync).ToList();
|
|
|
|
int longestDescription = buildSteps.Max(b => String.IsNullOrWhiteSpace(b.Description) ? 0 : b.Description.Length);
|
|
string descriptionHeader = "Description";
|
|
string descriptionSpace = new string(Enumerable.Repeat(' ', longestDescription + 1 - descriptionHeader.Length).ToArray());
|
|
string descriptionDashes = new string(Enumerable.Repeat('-', longestDescription + 2).ToArray());
|
|
string format = $" {{Id,-36}} | {{Name,-{longestDescription}}} | {{Type,-10}} | {{Enabled,-8}}";
|
|
|
|
logger.LogInformation("Available build steps:");
|
|
logger.LogInformation("");
|
|
logger.LogInformation(" Id | {descriptionHeader}{descriptionSpace}| Type | Enabled", descriptionHeader, descriptionSpace);
|
|
logger.LogInformation(" -------------------------------------|{descriptionDashes}|------------|-----------------", descriptionDashes);
|
|
foreach (BuildStep buildStep in buildStepObjects.Values.Select(x => new BuildStep(x)).OrderBy(x => x.NormalSync))
|
|
{
|
|
logger.LogInformation(format, buildStep.UniqueId, buildStep.Description, buildStep.Type, buildStep.NormalSync);
|
|
}
|
|
return;
|
|
}
|
|
|
|
HashSet<Guid>? steps = new HashSet<Guid>();
|
|
if (target != null)
|
|
{
|
|
if (!Guid.TryParse(target, out Guid id))
|
|
{
|
|
logger.LogError("Unable to parse '{Target}' as a GUID. Pass -List to show all available build steps and their identifiers.", target);
|
|
return;
|
|
}
|
|
|
|
steps.Add(id);
|
|
}
|
|
|
|
// check that the tools are installed
|
|
List<Guid> uninstalledTools = new List<Guid>();
|
|
if (context.GlobalSettings != null)
|
|
{
|
|
using ToolUpdateMonitor? toolsMonitor = await GetToolsMonitor(logger, context, perforceClient);
|
|
|
|
if (toolsMonitor != null)
|
|
{
|
|
foreach (Guid step in steps)
|
|
{
|
|
if (buildStepObjects.TryGetValue(step, out ConfigObject? config))
|
|
{
|
|
BuildStep buildStep = new BuildStep(config);
|
|
if (CanRunStep(buildStep, toolsMonitor))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
logger.LogWarning($"Build step '{buildStep.Description}' cannot be run as the tool is not installed.");
|
|
uninstalledTools.Add(step);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (Guid uninstalledTool in uninstalledTools)
|
|
{
|
|
steps.Remove(uninstalledTool);
|
|
}
|
|
|
|
WorkspaceUpdateContext updateContext = new WorkspaceUpdateContext(
|
|
state.Current.CurrentChangeNumber,
|
|
WorkspaceUpdateOptions.Build,
|
|
BuildConfig.Development,
|
|
null,
|
|
projectSettings.BuildSteps, steps);
|
|
|
|
WorkspaceUpdate update = new WorkspaceUpdate(updateContext);
|
|
(WorkspaceUpdateResult result, string message) = await update.ExecuteAsync(perforceClient.Settings, projectInfo, state, context.Logger, CancellationToken.None);
|
|
|
|
if (result != WorkspaceUpdateResult.Success)
|
|
{
|
|
throw new UserErrorException("{Message}", message);
|
|
}
|
|
}
|
|
|
|
private async Task<ToolUpdateMonitor?> GetToolsMonitor(ILogger logger, CommandContext context, IPerforceConnection perforceClient)
|
|
{
|
|
ToolsCommandCommandOptions options = context.Arguments.ApplyTo<ToolsCommandCommandOptions>(logger);
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
DirectoryReference dataFolder = DirectoryReference.Combine(DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.LocalApplicationData)!, "UnrealGameSync");
|
|
DirectoryReference.CreateDirectory(dataFolder);
|
|
|
|
// create a temporary service provider for the tool update monitor
|
|
ServiceCollection services = new ServiceCollection();
|
|
services.AddSingleton<IAsyncDisposer, AsyncDisposer>();
|
|
services.AddSingleton(sp => TokenStoreFactory.CreateTokenStore());
|
|
services.AddSingleton<OidcTokenManager>();
|
|
|
|
LauncherSettings launcherSettings = new LauncherSettings();
|
|
launcherSettings.Read();
|
|
|
|
if (launcherSettings.HordeServer != null)
|
|
{
|
|
services.AddHorde(options =>
|
|
{
|
|
options.ServerUrl = new Uri(launcherSettings.HordeServer);
|
|
options.AllowAuthPrompt = false;
|
|
});
|
|
}
|
|
|
|
ServiceProvider serviceProvider = services.BuildServiceProvider();
|
|
|
|
ToolUpdateMonitor toolUpdateMonitor =
|
|
new ToolUpdateMonitor(perforceClient.Settings, dataFolder, context.GlobalSettings!, logger, serviceProvider);
|
|
|
|
// get the list of tools available
|
|
logger.LogInformation("Retrieving tools information, please wait.");
|
|
await toolUpdateMonitor.GetDataFromBackendAsync();
|
|
|
|
return toolUpdateMonitor;
|
|
}
|
|
|
|
private bool CanRunStep(BuildStep step, ToolUpdateMonitor toolsMonitor)
|
|
{
|
|
if (step.ToolId != Guid.Empty)
|
|
{
|
|
string? toolName = toolsMonitor.GetToolName(step.ToolId);
|
|
if (toolName == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (toolsMonitor.GetToolPath(toolName) == null)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class StatusCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
bool update = context.Arguments.HasOption("-Update");
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
logger.LogInformation("User: {UserName}", settings.UserName);
|
|
logger.LogInformation("Server: {ServerAndPort}", settings.ServerAndPort);
|
|
logger.LogInformation("Project: {ClientProjectPath}", settings.ClientProjectPath);
|
|
|
|
using IPerforceConnection perforceClient = await ConnectAsync(settings, context.LoggerFactory);
|
|
|
|
WorkspaceStateWrapper state = await ReadWorkspaceState(perforceClient, settings, context.UserSettings, logger);
|
|
if (update)
|
|
{
|
|
ProjectInfo newProjectInfo = await ProjectInfo.CreateAsync(perforceClient, settings, CancellationToken.None);
|
|
state.Modify(x => x.UpdateCachedProjectInfo(newProjectInfo, settings.LastModifiedTimeUtc));
|
|
}
|
|
|
|
string streamOrBranchName = state.Current.StreamName ?? settings.BranchPath.TrimStart('/');
|
|
if (state.Current.LastSyncResultMessage == null)
|
|
{
|
|
logger.LogInformation("Not currently synced to {Stream}", streamOrBranchName);
|
|
}
|
|
else if (state.Current.LastSyncResult == WorkspaceUpdateResult.Success)
|
|
{
|
|
logger.LogInformation("Synced to {Stream} CL {Change}", streamOrBranchName, state.Current.LastSyncChangeNumber);
|
|
}
|
|
else
|
|
{
|
|
logger.LogWarning("Last sync to {Stream} CL {Change} failed: {Result}", streamOrBranchName, state.Current.LastSyncChangeNumber, state.Current.LastSyncResultMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
class LoginCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
// Get the positional argument indicating the file to look for
|
|
if (!context.Arguments.TryGetPositionalArgument(out string? providerIdentifier))
|
|
{
|
|
throw new UserErrorException("Missing provider identifier to login to.");
|
|
}
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
|
|
// Find the valid config file paths
|
|
DirectoryInfo engineDir = DirectoryReference.Combine(settings.RootDir, "Engine").ToDirectoryInfo();
|
|
DirectoryInfo gameDir = new DirectoryInfo(settings.ProjectPath);
|
|
using ITokenStore tokenStore = TokenStoreFactory.CreateTokenStore();
|
|
IConfiguration providerConfiguration = ProviderConfigurationFactory.ReadConfiguration(engineDir, gameDir);
|
|
OidcTokenManager oidcTokenManager = OidcTokenManager.CreateTokenManager(providerConfiguration, tokenStore, new List<string>() { providerIdentifier });
|
|
OidcTokenInfo result = await oidcTokenManager.LoginAsync(providerIdentifier);
|
|
|
|
logger.LogInformation("Logged in to provider {ProviderIdentifier}", providerIdentifier);
|
|
}
|
|
}
|
|
|
|
class SwitchCommandOptions
|
|
{
|
|
[CommandLine("-Force")]
|
|
public bool Force { get; set; }
|
|
}
|
|
|
|
class SwitchCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
// Get the positional argument indicating the file to look for
|
|
string? targetName;
|
|
if (!context.Arguments.TryGetPositionalArgument(out targetName))
|
|
{
|
|
throw new UserErrorException("Missing stream or project name to switch to.");
|
|
}
|
|
|
|
// Finish argument parsing
|
|
SwitchCommandOptions options = new SwitchCommandOptions();
|
|
context.Arguments.ApplyTo(options);
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
if (targetName.StartsWith("//", StringComparison.Ordinal))
|
|
{
|
|
options.Force = true;
|
|
}
|
|
|
|
// Get a connection to the client for this workspace
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
using IPerforceConnection perforceClient = await ConnectAsync(settings, context.LoggerFactory);
|
|
|
|
// Check whether we're switching stream or project
|
|
if (targetName.StartsWith("//", StringComparison.Ordinal))
|
|
{
|
|
await SwitchStreamAsync(perforceClient, targetName, options.Force, context.Logger);
|
|
}
|
|
else
|
|
{
|
|
await SwitchProjectAsync(perforceClient, settings, targetName, context.Logger);
|
|
}
|
|
}
|
|
|
|
public static async Task SwitchStreamAsync(IPerforceConnection perforceClient, string streamName, bool force, ILogger logger)
|
|
{
|
|
if (!force && await perforceClient.OpenedAsync(OpenedOptions.None, FileSpecList.Any).AnyAsync())
|
|
{
|
|
throw new UserErrorException("Client {ClientName} has files opened. Use -Force to switch anyway.", perforceClient.Settings.ClientName!);
|
|
}
|
|
|
|
await perforceClient.SwitchClientToStreamAsync(streamName, SwitchClientOptions.IgnoreOpenFiles);
|
|
|
|
logger.LogInformation("Switched to stream {StreamName}", streamName);
|
|
}
|
|
|
|
public static async Task SwitchProjectAsync(IPerforceConnection perforceClient, UserWorkspaceSettings settings, string projectName, ILogger logger)
|
|
{
|
|
settings.ProjectPath = await FindProjectPathAsync(perforceClient, settings.ClientName, settings.BranchPath, projectName);
|
|
settings.Save(logger);
|
|
logger.LogInformation("Switched to project {ProjectPath}", settings.ClientProjectPath);
|
|
}
|
|
}
|
|
|
|
class VersionCommand : Command
|
|
{
|
|
public override Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
string version = GetVersion();
|
|
logger.LogInformation("UnrealGameSync {Version}", version);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
|
|
class InstallCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
await UpdateInstallAsync(true, context.Logger);
|
|
}
|
|
}
|
|
|
|
class UninstallCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
await UpdateInstallAsync(false, context.Logger);
|
|
}
|
|
}
|
|
|
|
static async Task UpdateInstallAsync(bool install, ILogger logger)
|
|
{
|
|
DirectoryReference? installDir = GetInstallFolder();
|
|
if (installDir != null)
|
|
{
|
|
UpdateInstalledFiles(install, installDir, logger);
|
|
}
|
|
else
|
|
{
|
|
installDir = new FileReference(Assembly.GetExecutingAssembly().GetOriginalLocation()).Directory;
|
|
}
|
|
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
const string EnvVarName = "PATH";
|
|
|
|
string? pathVar = Environment.GetEnvironmentVariable(EnvVarName, EnvironmentVariableTarget.User);
|
|
pathVar ??= String.Empty;
|
|
|
|
List<string> paths = new List<string>(pathVar.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries));
|
|
|
|
int changes = paths.RemoveAll(x => x.Equals(installDir.FullName, StringComparison.OrdinalIgnoreCase));
|
|
if (install)
|
|
{
|
|
paths.Add(installDir.FullName);
|
|
changes++;
|
|
}
|
|
if (changes > 0)
|
|
{
|
|
pathVar = String.Join(Path.PathSeparator, paths);
|
|
Environment.SetEnvironmentVariable(EnvVarName, pathVar, EnvironmentVariableTarget.User);
|
|
}
|
|
|
|
if (install)
|
|
{
|
|
logger.LogInformation("Added {Path} to PATH environment variable", installDir);
|
|
}
|
|
else
|
|
{
|
|
logger.LogInformation("Removed {Path} from PATH environment variable", installDir);
|
|
}
|
|
}
|
|
else if (OperatingSystem.IsMacOS())
|
|
{
|
|
DirectoryReference? userDir = DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.UserProfile);
|
|
if (userDir != null)
|
|
{
|
|
FileReference configFile = FileReference.Combine(userDir, ".zshrc");
|
|
await UpdateAliasAsync(configFile, install, installDir, logger);
|
|
}
|
|
}
|
|
else if (OperatingSystem.IsLinux())
|
|
{
|
|
DirectoryReference? userDir = DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.UserProfile);
|
|
if (userDir != null)
|
|
{
|
|
FileReference configFile = FileReference.Combine(userDir, ".bashrc");
|
|
await UpdateAliasAsync(configFile, install, installDir, logger);
|
|
}
|
|
}
|
|
}
|
|
|
|
static DirectoryReference? GetInstallFolder()
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
{
|
|
DirectoryReference? installDir = DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.LocalApplicationData);
|
|
if (installDir != null)
|
|
{
|
|
return DirectoryReference.Combine(installDir, "Epic Games", "UgsCmd");
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static void UpdateInstalledFiles(bool install, DirectoryReference installDir, ILogger logger)
|
|
{
|
|
FileReference assemblyFile = new FileReference(Assembly.GetExecutingAssembly().GetOriginalLocation());
|
|
DirectoryReference sourceDir = assemblyFile.Directory;
|
|
|
|
DirectoryReference TempDir = DirectoryReference.Combine(installDir.ParentDirectory!, "~" + installDir.GetDirectoryName());
|
|
if (DirectoryReference.Exists(TempDir))
|
|
{
|
|
DirectoryReference.Delete(TempDir, true);
|
|
}
|
|
|
|
if (DirectoryReference.Exists(installDir))
|
|
{
|
|
logger.LogInformation("Removing application files from {Dir}", installDir);
|
|
|
|
Directory.Move(installDir.FullName, TempDir.FullName);
|
|
DirectoryReference.Delete(TempDir, true);
|
|
}
|
|
|
|
if (install)
|
|
{
|
|
logger.LogInformation("Copying application files to {Dir}", installDir);
|
|
|
|
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, installDir.FullName);
|
|
}
|
|
}
|
|
|
|
static async Task UpdateAliasAsync(FileReference configFile, bool install, DirectoryReference installDir, ILogger logger)
|
|
{
|
|
List<string> lines = new List<string>();
|
|
if (FileReference.Exists(configFile))
|
|
{
|
|
lines.AddRange(await FileReference.ReadAllLinesAsync(configFile));
|
|
lines.RemoveAll(x => Regex.IsMatch(x, @"^\s*alias\s+ugs\s*="));
|
|
}
|
|
if (install)
|
|
{
|
|
lines.Add($"alias ugs={FileReference.Combine(installDir, "ugs")}");
|
|
}
|
|
|
|
await FileReference.WriteAllLinesAsync(configFile, lines);
|
|
|
|
if (install)
|
|
{
|
|
logger.LogInformation("Added 'ugs' alias to {ConfigFile}", configFile);
|
|
}
|
|
else
|
|
{
|
|
logger.LogInformation("Removed 'ugs' alias from {ConfigFile}", configFile);
|
|
}
|
|
}
|
|
|
|
class UpgradeCommandOptions
|
|
{
|
|
[CommandLine("-Check")]
|
|
public bool Check { get; set; }
|
|
|
|
[CommandLine("-Force")]
|
|
public bool Force { get; set; }
|
|
}
|
|
|
|
class UpgradeCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
|
|
UpgradeCommandOptions options = new UpgradeCommandOptions();
|
|
context.Arguments.ApplyTo(options);
|
|
string? targetDirStr = context.Arguments.GetStringOrDefault("-TargetDir=", null);
|
|
context.Arguments.CheckAllArgumentsUsed(logger);
|
|
|
|
string currentVersion = GetVersion();
|
|
|
|
string? latestVersion = await GetLatestVersionAsync(logger, CancellationToken.None);
|
|
if (latestVersion == null)
|
|
{
|
|
return;
|
|
}
|
|
if (latestVersion.Equals(currentVersion, StringComparison.OrdinalIgnoreCase) && !options.Force)
|
|
{
|
|
logger.LogInformation("You are running the latest version ({Version})", currentVersion);
|
|
return;
|
|
}
|
|
if (options.Check)
|
|
{
|
|
logger.LogWarning("A newer version of UGS is available ({NewVersion})", latestVersion);
|
|
return;
|
|
}
|
|
|
|
using (HttpClient httpClient = new HttpClient())
|
|
{
|
|
Uri baseUrl = new Uri(DeploymentSettings.Instance.HordeUrl ?? String.Empty);
|
|
|
|
DirectoryReference currentDir = new FileReference(Assembly.GetExecutingAssembly().Location).Directory;
|
|
|
|
DirectoryReference targetDir = (targetDirStr == null) ? currentDir : DirectoryReference.Combine(currentDir, targetDirStr);
|
|
DirectoryReference.CreateDirectory(targetDir);
|
|
|
|
FileReference tempFile = FileReference.Combine(targetDir, "update.zip");
|
|
using (Stream requestStream = await httpClient.GetStreamAsync(new Uri(baseUrl, $"api/v1/tools/{GetUpgradeToolName()}?action=download")))
|
|
{
|
|
using (Stream tempFileStream = FileReference.Open(tempFile, FileMode.Create, FileAccess.Write, FileShare.None))
|
|
{
|
|
await requestStream.CopyToAsync(tempFileStream);
|
|
}
|
|
}
|
|
|
|
using (FileStream stream = FileReference.Open(tempFile, FileMode.Open, FileAccess.Read, FileShare.Read))
|
|
{
|
|
using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Read, true))
|
|
{
|
|
foreach (ZipArchiveEntry entry in archive.Entries)
|
|
{
|
|
FileReference targetFile = FileReference.Combine(targetDir, entry.Name);
|
|
if (!targetFile.IsUnderDirectory(targetDir))
|
|
{
|
|
throw new InvalidDataException("Attempt to extract file outside source directory");
|
|
}
|
|
entry.ExtractToFile_CrossPlatform(targetFile.FullName, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ToolsCommandCommandOptions
|
|
{
|
|
[CommandLine("-Install=", ListSeparator = ',')]
|
|
public List<string> Install { get; set; } = new List<string>();
|
|
|
|
[CommandLine("-Uninstall=", ListSeparator = ',')]
|
|
public List<string> Uninstall { get; set; } = new List<string>();
|
|
|
|
[CommandLine("-List")]
|
|
public bool List { get; set; }
|
|
|
|
[CommandLine("-Update")]
|
|
public bool Update { get; set; }
|
|
}
|
|
|
|
class ToolsCommand : Command
|
|
{
|
|
public override async Task ExecuteAsync(CommandContext context)
|
|
{
|
|
ILogger logger = context.Logger;
|
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
logger.LogInformation("This command is only available for Windows.");
|
|
return;
|
|
}
|
|
|
|
context.Arguments.TryGetPositionalArgument(out string? toolId);
|
|
|
|
ToolsCommandCommandOptions options = context.Arguments.ApplyTo<ToolsCommandCommandOptions>(logger);
|
|
context.Arguments.CheckAllArgumentsUsed();
|
|
|
|
UserWorkspaceSettings settings = ReadRequiredUserWorkspaceSettings();
|
|
using IPerforceConnection perforceClient = await ConnectAsync(settings, context.LoggerFactory);
|
|
|
|
if (context.GlobalSettings == null)
|
|
{
|
|
logger.LogError("Could not retrieve user settings.");
|
|
return;
|
|
}
|
|
|
|
DirectoryReference dataFolder = DirectoryReference.Combine(DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.LocalApplicationData)!, "UnrealGameSync");
|
|
DirectoryReference.CreateDirectory(dataFolder);
|
|
|
|
// create a temporary service provider for the tool update monitor
|
|
ServiceCollection services = new ServiceCollection();
|
|
services.AddSingleton<IAsyncDisposer, AsyncDisposer>();
|
|
services.AddSingleton(sp => TokenStoreFactory.CreateTokenStore());
|
|
services.AddSingleton<OidcTokenManager>();
|
|
|
|
LauncherSettings launcherSettings = new LauncherSettings();
|
|
launcherSettings.Read();
|
|
|
|
if (launcherSettings.HordeServer != null)
|
|
{
|
|
services.AddHorde(options =>
|
|
{
|
|
options.ServerUrl = new Uri(launcherSettings.HordeServer);
|
|
options.AllowAuthPrompt = false;
|
|
});
|
|
}
|
|
|
|
ServiceProvider serviceProvider = services.BuildServiceProvider();
|
|
|
|
using ToolUpdateMonitor toolUpdateMonitor =
|
|
new ToolUpdateMonitor(perforceClient.Settings, dataFolder, context.GlobalSettings, logger, serviceProvider);
|
|
|
|
// get the list of tools available
|
|
logger.LogInformation("Retrieving tools information, please wait.");
|
|
await toolUpdateMonitor.GetDataFromBackendAsync();
|
|
|
|
// only list the tools
|
|
if (options.List)
|
|
{
|
|
logger.LogInformation("Available Tools:");
|
|
logger.LogInformation("");
|
|
logger.LogInformation(" Id | Name | Enabled | Description ");
|
|
logger.LogInformation(" -------------------------------------| -------------------------------------| ----------------| -------------------------------------------------");
|
|
|
|
IReadOnlyList<ToolInfo> enabled = toolUpdateMonitor.GetEnabledTools();
|
|
|
|
foreach (ToolInfo toolInfo in toolUpdateMonitor.GetTools().OrderBy(t => enabled.All( e => t.Id != e.Id)))
|
|
{
|
|
logger.LogInformation(" {Id,-36} | {Name,-36} | {Enabled,-16}| {Description,-48} ", toolInfo.Id, toolInfo.Name, enabled.Any( t => t.Id == toolInfo.Id), toolInfo.Description);
|
|
}
|
|
}
|
|
|
|
// update the list of enabled tools and update (this will update all the tools)
|
|
if (options.Install.Any() || options.Uninstall.Any())
|
|
{
|
|
IReadOnlyList<ToolInfo> enabled = toolUpdateMonitor.GetEnabledTools();
|
|
HashSet<Guid> newEnabledTools = new HashSet<Guid>(enabled.Select(t => t.Id));
|
|
|
|
foreach (string id in options.Install.Select(id => id.Trim()))
|
|
{
|
|
if (Guid.TryParse(id, out Guid guid))
|
|
{
|
|
foreach (ToolInfo toolInfo in toolUpdateMonitor.GetTools())
|
|
{
|
|
if (toolInfo.Id == guid)
|
|
{
|
|
logger.LogInformation("Adding tool '{Id}' - '{Name}' to the list of enabled tools", id, toolInfo.Name);
|
|
newEnabledTools.Add(guid);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!newEnabledTools.Contains(guid))
|
|
{
|
|
logger.LogError("Could not install tool '{Id}'", id);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.LogError("'{Id}' is not a valid guid", id);
|
|
}
|
|
}
|
|
|
|
foreach (string id in options.Uninstall.Select(id => id.Trim()))
|
|
{
|
|
if (Guid.TryParse(id, out Guid guid))
|
|
{
|
|
foreach (ToolInfo toolInfo in toolUpdateMonitor.GetTools())
|
|
{
|
|
if (toolInfo.Id == guid)
|
|
{
|
|
logger.LogInformation("Removing tool '{Id}' - '{Name}' from the list of enabled tools", id, toolInfo.Name);
|
|
newEnabledTools.Remove(guid);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newEnabledTools.Contains(guid))
|
|
{
|
|
logger.LogError("Could not uninstall tool '{Id}'", id);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.LogError("'{Id}' is not a valid guid", id);
|
|
}
|
|
}
|
|
|
|
if (!newEnabledTools.SequenceEqual(context.GlobalSettings.EnabledTools))
|
|
{
|
|
context.GlobalSettings.EnabledTools.Clear();
|
|
context.GlobalSettings.EnabledTools.UnionWith(newEnabledTools);
|
|
context.GlobalSettings.Save(logger);
|
|
|
|
logger.LogInformation("Updating tools");
|
|
await toolUpdateMonitor.UpdateToolsAsync();
|
|
}
|
|
}
|
|
|
|
// update the all the enabled tools
|
|
if (options.Update)
|
|
{
|
|
await toolUpdateMonitor.UpdateToolsAsync();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|