src/dotnet/RiderPlugin.UnrealLink/PluginInstaller/UnrealPluginInstaller.cs (662 lines of code) (raw):

using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using JetBrains.Application.I18n; using JetBrains.Application.Parts; using JetBrains.Application.Settings; using JetBrains.Application.Threading; using JetBrains.Collections.Viewable; using JetBrains.DataFlow; using JetBrains.Diagnostics; using JetBrains.HabitatDetector; using JetBrains.IDE; using JetBrains.Lifetimes; using JetBrains.ProjectModel; using JetBrains.ProjectModel.DataContext; using JetBrains.ProjectModel.ProjectsHost.SolutionHost.Progress; using JetBrains.Rd.Base; using JetBrains.ReSharper.Feature.Services.Protocol; using JetBrains.ReSharper.Psi.Cpp.UE4; using JetBrains.ReSharper.Resources.Shell; using JetBrains.Rider.Model; using JetBrains.Rider.Model.Notifications; using JetBrains.Util; using Newtonsoft.Json.Linq; using RiderPlugin.UnrealLink.Model.FrontendBackend; using RiderPlugin.UnrealLink.Resources; using RiderPlugin.UnrealLink.Settings; using RiderPlugin.UnrealLink.Utils; namespace RiderPlugin.UnrealLink.PluginInstaller { [SolutionComponent(InstantiationEx.LegacyDefault)] public class UnrealPluginInstaller { public Lifetime Lifetime { get; } private readonly ILogger myLogger; private readonly PluginPathsProvider myPathsProvider; private readonly ISolution mySolution; private readonly UnrealHost myUnrealHost; private readonly NotificationsModel myNotificationsModel; private readonly BackgroundProgressManager myBackgroundProgressManager; private IContextBoundSettingsStoreLive myBoundSettingsStore; private UnrealPluginDetector myPluginDetector; private const string TMP_PREFIX = "UnrealLink"; public UnrealPluginInstaller(Lifetime lifetime, ILogger logger, UnrealPluginDetector pluginDetector, PluginPathsProvider pathsProvider, ISolution solution, ISettingsStore settingsStore, UnrealHost unrealHost, NotificationsModel notificationsModel, BackgroundProgressManager backgroundProgressManager) { Lifetime = lifetime; myLogger = logger; myPathsProvider = pathsProvider; mySolution = solution; myUnrealHost = unrealHost; myNotificationsModel = notificationsModel; myBackgroundProgressManager = backgroundProgressManager; myBoundSettingsStore = settingsStore.BindToContextLive(Lifetime, ContextRange.Smart(solution.ToDataContext())); myPluginDetector = pluginDetector; myPluginDetector.InstallInfoProperty.Change.Advise_NewNotNull(Lifetime, installInfo => { myUnrealHost.myModel.IsInstallInfoAvailable.Set(true); mySolution.Locks.ExecuteOrQueueReadLockEx(Lifetime, "UnrealPluginInstaller.CheckAllProjectsIfAutoInstallEnabled", () => { HandleAutoUpdatePlugin(installInfo.New); }); }); BindToInstallationSettingChange(); BindToNotificationFixAction(); } private void HandleAutoUpdatePlugin(UnrealPluginInstallInfo unrealPluginInstallInfo) { var status = PluginInstallStatus.NoPlugin; var outOfSync = true; if (unrealPluginInstallInfo.Location == PluginInstallLocation.Engine) { status = PluginInstallStatus.InEngine; outOfSync = !unrealPluginInstallInfo.EnginePlugin.PluginChecksum.SequenceEqual(myPathsProvider.CurrentPluginChecksum); } if (unrealPluginInstallInfo.Location == PluginInstallLocation.Game) { status = PluginInstallStatus.InGame; outOfSync = unrealPluginInstallInfo.ProjectPlugins.Any(description => !description.PluginChecksum.SequenceEqual(myPathsProvider.CurrentPluginChecksum) ); } if (!outOfSync) return; if(myBoundSettingsStore.GetValue((UnrealLinkSettings s) => s.AutoUpdateRiderLinkPlugin)) { var installLocation = unrealPluginInstallInfo.Location; if (installLocation == PluginInstallLocation.NotInstalled) { installLocation = GetInstallLocationFromSettings(); } QueueAutoUpdate(installLocation); return; } myLogger.Warn("[UnrealLink]: Plugin is out of sync"); myUnrealHost.PerformModelAction(model => { var isGameAvailable = !unrealPluginInstallInfo.ProjectPlugins.IsEmpty(); model.OnEditorPluginOutOfSync(new EditorPluginOutOfSync(status, isGameAvailable)); }); } private void QueueAutoUpdate(PluginInstallLocation installLocation) { var entry = myBoundSettingsStore.GetValue((UnrealLinkSettings s) => s.DefaultUpdateRiderLinkBehavior); var shouldBeBuilt = (entry == InstallOrExtract.Install) || (!mySolution.GetComponent<ICppUE4SolutionDetector>().UnrealContext.Value.IsBuiltFromSource && installLocation == PluginInstallLocation.Engine); mySolution.Locks.ExecuteOrQueueReadLockEx(Lifetime, "UnrealPluginInstaller.InstallPluginIfRequired", () => HandleManualInstallPlugin( new InstallPluginDescription(installLocation, ForceInstall.No, shouldBeBuilt) )); } private VirtualFileSystemPath CreateTempDirectory() { var entry = myBoundSettingsStore.GetValue((UnrealLinkSettings s) => s.IntermediateBuildFolderRoot); if (entry.IsNullOrEmpty()) { var defaultBuildPath = VirtualFileSystemDefinition.GetTempPath(InteractionContext.SolutionContext); return VirtualFileSystemDefinition.CreateTemporaryDirectory(InteractionContext.SolutionContext, defaultBuildPath, TMP_PREFIX); } return VirtualFileSystemDefinition.CreateTemporaryDirectory(InteractionContext.SolutionContext, entry.ToVirtualFileSystemPath(), TMP_PREFIX); } private void InstallPluginInGame(Lifetime lifetime, UnrealPluginInstallInfo unrealPluginInstallInfo, Property<double> progress, bool buildRequired) { myLogger.Verbose("[UnrealLink]: Installing plugin in Game"); var backupDir = CreateTempDirectory(); if(backupDir.IsNullOrEmpty()) return; using var deleteTempFolders = new DeleteTempFolders(backupDir.Directory); var backupAllPlugins = BackupAllPlugins(unrealPluginInstallInfo); var success = true; var size = unrealPluginInstallInfo.ProjectPlugins.Count; var range = 1.0 / size; for (int i = 0; i < unrealPluginInstallInfo.ProjectPlugins.Count; i++) { progress.Value = range * i; var installDescription = unrealPluginInstallInfo.ProjectPlugins[i]; myLogger.Verbose($"[UnrealLink]: Installing plugin for {installDescription.ProjectName}"); try { if (buildRequired && InstallPlugin(lifetime, installDescription, unrealPluginInstallInfo.EngineRoot, progress, range)) continue; if (!buildRequired && ExtractPlugin(lifetime, installDescription, unrealPluginInstallInfo.EngineRoot, progress, range)) continue; } catch (OperationCanceledException) { // Operation was cancelled, don't need to do anything, fallback to break case } success = false; break; } if (success) { unrealPluginInstallInfo.EnginePlugin.IsPluginAvailable = false; } else { foreach (var backupAllPlugin in backupAllPlugins) { backupAllPlugin.Restore(); } } myUnrealHost.myModel.InstallPluginFinished(success); } private List<BackupDir> BackupAllPlugins(UnrealPluginInstallInfo unrealPluginInstallInfo) { var result = new List<BackupDir>(); if (unrealPluginInstallInfo.EnginePlugin.IsPluginAvailable) { try { result.Add(new BackupDir(unrealPluginInstallInfo.EnginePlugin.UnrealPluginRootFolder, TMP_PREFIX)); } catch { var text = "Close all running instances of Unreal Editor and try again\n" + $"Path to old plugin: {unrealPluginInstallInfo.EnginePlugin.UnrealPluginRootFolder}"; myUnrealHost.myModel.RiderLinkInstallMessage( new InstallMessage("Failed to backup old RiderLink plugin", ContentType.Error)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(text, ContentType.Error)); throw; } } foreach (var installDescription in unrealPluginInstallInfo.ProjectPlugins) { try { if (installDescription.IsPluginAvailable) result.Add(new BackupDir(installDescription.UnrealPluginRootFolder, TMP_PREFIX)); } catch { var text = "Close all running instances of Unreal Editor and try again\n" + $"Path to old plugin: {installDescription.UnrealPluginRootFolder}"; myUnrealHost.myModel.RiderLinkInstallMessage( new InstallMessage("Failed to backup old RiderLink plugin", ContentType.Error)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(text, ContentType.Error)); throw; } } return result; } private void InstallPluginInEngine(Lifetime lifetime, UnrealPluginInstallInfo unrealPluginInstallInfo, IProperty<double> progress, bool buildRequired) { var backupDir = CreateTempDirectory(); if (backupDir.IsNullOrEmpty()) return; using var deleteTempFolders = new DeleteTempFolders(backupDir.Directory); var backupAllPlugins = BackupAllPlugins(unrealPluginInstallInfo); progress.Value = 0.0; bool success; try { if (buildRequired) { success = InstallPlugin(lifetime, unrealPluginInstallInfo.EnginePlugin, unrealPluginInstallInfo.EngineRoot, progress, 1.0); } else { success = ExtractPlugin(lifetime, unrealPluginInstallInfo.EnginePlugin, unrealPluginInstallInfo.EngineRoot, progress, 1.0); } } catch (OperationCanceledException) { success = false; } if (!success) { foreach (var backupAllPlugin in backupAllPlugins) { backupAllPlugin.Restore(); } } else { foreach (var installDescription in unrealPluginInstallInfo.ProjectPlugins) { installDescription.IsPluginAvailable = false; } } myUnrealHost.myModel.InstallPluginFinished(success); } private bool ExtractPlugin(Lifetime lifetime, UnrealPluginInstallInfo.InstallDescription installDescription, VirtualFileSystemPath engineRoot, IProperty<double> progressProperty, double range) { using var def = new LifetimeDefinition(); var ZIP_STEP = 0.5 * range; var PATCH_STEP = 0.5 * range; var pluginRootFolder = installDescription.UnrealPluginRootFolder; pluginRootFolder.CreateDirectory().DeleteChildren(); var editorPluginPathFile = myPathsProvider.PathToPackedPlugin; try { ZipFile.ExtractToDirectory(editorPluginPathFile.FullPath, pluginRootFolder.FullPath); progressProperty.Value += ZIP_STEP; } catch (Exception exception) { myLogger.Warn(exception, $"[UnrealLink]: Couldn't extract {editorPluginPathFile} to {pluginRootFolder}"); const string unzipFailTitle = "Failed to unzip new RiderLink plugin"; var unzipFailText = $"Failed to unzip new version of RiderLink ({editorPluginPathFile.FullPath}) to user folder ({pluginRootFolder.FullPath})\n" + "Try restarting Rider in administrative mode"; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(unzipFailTitle, ContentType.Error)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(unzipFailText, ContentType.Error)); return false; } if (!PatchUpluginFileAfterInstallation(pluginRootFolder)) { const string failedToPatch = "Failed to patch RiderLink.uplugin"; var failedPatchText = "Failed to set `EnableByDefault` to true in RiderLink.uplugin\n" + "You need to manually enable RiderLink in UnrealEditor"; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(failedToPatch, ContentType.Normal)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(failedPatchText, ContentType.Normal)); } progressProperty.Value += PATCH_STEP; lifetime.ToCancellationToken().ThrowIfCancellationRequested(); installDescription.IsPluginAvailable = true; installDescription.PluginChecksum = myPathsProvider.CurrentPluginChecksum; var title = Strings.RiderLinkPluginExtracted_Title; var text = Strings.RiderLinkPluginExtracted_Message.Format(pluginRootFolder); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(title, ContentType.Normal)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(text, ContentType.Normal)); var notification = new NotificationModel(mySolution.GetRdProjectId(), title, text, true, RdNotificationEntryType.INFO, new List<NotificationHyperlink>()); mySolution.Locks.ExecuteOrQueue(Lifetime, "UnrealLink.InstallPlugin", () => { myNotificationsModel.Notification(notification); }); var cppUe4SolutionDetector = mySolution.GetComponent<ICppUE4SolutionDetector>(); var isSln = cppUe4SolutionDetector.SupportRiderProjectModel != CppUE4ProjectModelSupportMode.UprojectOpened; if (isSln) { RefreshSlnProjectFiles(installDescription, engineRoot); } else { RefreshUprojectProjectFiles(lifetime, pluginRootFolder); } return true; } private void RefreshUprojectProjectFiles(Lifetime lifetime, VirtualFileSystemPath pluginRootFolder) { var actionTitle = "Update VirtualFileSystem after RiderLink installation"; mySolution.Locks.Queue(Lifetime, actionTitle, () => { myLogger.Verbose(actionTitle); var fileSystemModel = mySolution.GetProtocolSolution().GetFileSystemModel(); fileSystemModel.RefreshPaths.Start(lifetime, new RdFsRefreshRequest(new List<RdPath>{pluginRootFolder.ToRd()}, true)); }); } private void RefreshSlnProjectFiles(UnrealPluginInstallInfo.InstallDescription installDescription, VirtualFileSystemPath engineRoot) { const string refreshText = "Refreshing project files"; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(refreshText, ContentType.Normal)); mySolution.Locks.ExecuteOrQueue(Lifetime, "Refresh projects after RiderLink installation", () => UnrealProjectsRefresher.RefreshProjects(Lifetime, mySolution, installDescription, engineRoot)); } private bool InstallPlugin(Lifetime lifetime, UnrealPluginInstallInfo.InstallDescription installDescription, VirtualFileSystemPath engineRoot, IProperty<double> progressProperty, double range) { using var def = new LifetimeDefinition(); var ZIP_STEP = 0.1 * range; var PATCH_STEP = 0.1 * range; var BUILD_STEP = 0.7 * range; var pluginRootFolder = installDescription.UnrealPluginRootFolder; var editorPluginPathFile = myPathsProvider.PathToPackedPlugin; var pluginTmpDir = CreateTempDirectory(); if (pluginTmpDir.IsNullOrEmpty()) return false; if (PlatformUtil.RuntimePlatform == JetPlatform.Windows && pluginTmpDir.FullPath.Any(c => c >= 128)) { string nonAsciiCharactersText = Strings.NonASCIICharactersInTheBuildDirectory_Text; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(nonAsciiCharactersText, ContentType.Error)); } def.Lifetime.OnTermination(() => { pluginTmpDir.Delete(); }); try { ZipFile.ExtractToDirectory(editorPluginPathFile.FullPath, pluginTmpDir.FullPath); progressProperty.Value += ZIP_STEP; } catch (Exception exception) { myLogger.Warn(exception, $"[UnrealLink]: Couldn't extract {editorPluginPathFile} to {pluginTmpDir}"); const string unzipFailTitle = "Failed to unzip new RiderLink plugin"; var unzipFailText = $"Failed to unzip new version of RiderLink ({editorPluginPathFile.FullPath}) to user folder ({pluginTmpDir.FullPath})\n" + "Try restarting Rider in administrative mode"; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(unzipFailTitle, ContentType.Error)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(unzipFailText, ContentType.Error)); return false; } lifetime.ToCancellationToken().ThrowIfCancellationRequested(); var upluginFile = UnrealPluginDetector.GetPathToUpluginFile(pluginTmpDir); var pluginBuildOutput = CreateTempDirectory(); if (pluginBuildOutput.IsNullOrEmpty()) return false; def.Lifetime.OnTermination(() => { pluginBuildOutput.Delete(); }); var buildProgress = progressProperty.Value; var isPluginBuilt = BuildPlugin(lifetime, upluginFile, pluginBuildOutput, engineRoot, value => progressProperty.SetValue(buildProgress + value * BUILD_STEP)); if (!isPluginBuilt) { myLogger.Error($"Failed to build RiderLink for any available project"); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(Strings.FailedToBuildRiderLinkPlugin_Text, ContentType.Error)); return false; } progressProperty.Value = buildProgress + BUILD_STEP; lifetime.ToCancellationToken().ThrowIfCancellationRequested(); if (!PatchUpluginFileAfterInstallation(pluginBuildOutput)) { var failedToPatch = Strings.FailedToPatchRiderLinkUplugin_Text; var failedPatchText = Strings.FailedToPatchRiderLinkUplugin_Message; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(failedToPatch, ContentType.Normal)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(failedPatchText, ContentType.Normal)); } progressProperty.Value += PATCH_STEP; lifetime.ToCancellationToken().ThrowIfCancellationRequested(); pluginRootFolder.CreateDirectory().DeleteChildren(); pluginBuildOutput.Copy(pluginRootFolder); installDescription.IsPluginAvailable = true; installDescription.PluginChecksum = myPathsProvider.CurrentPluginChecksum; var title = Strings.RiderLinkPluginInstalled_Title; var text = Strings.RiderLinkPluginInstalled_Message.Format(pluginRootFolder); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(title, ContentType.Normal)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(text, ContentType.Normal)); var notification = new NotificationModel(mySolution.GetRdProjectId(), title, text, true, RdNotificationEntryType.INFO, new List<NotificationHyperlink>()); mySolution.Locks.ExecuteOrQueue(Lifetime, "UnrealLink.InstallPlugin", () => { myNotificationsModel.Notification(notification); }); var cppUe4SolutionDetector = mySolution.GetComponent<ICppUE4SolutionDetector>(); var isSln = cppUe4SolutionDetector.SupportRiderProjectModel != CppUE4ProjectModelSupportMode.UprojectOpened; if (isSln) { var refreshText = Strings.RefreshingProjectFiles_Text; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(refreshText, ContentType.Normal)); mySolution.Locks.ExecuteOrQueue(Lifetime, Strings.RefreshProjectsAfterRiderLinkInstallation_Text, () => UnrealProjectsRefresher.RefreshProjects(Lifetime, mySolution, installDescription, engineRoot)); } else { var actionTitle = "Update VirtualFileSystem after RiderLink installation"; mySolution.Locks.Queue(Lifetime, actionTitle, () => { myLogger.Verbose(actionTitle); var fileSystemModel = mySolution.GetProtocolSolution().GetFileSystemModel(); fileSystemModel.RefreshPaths.Start(lifetime, new RdFsRefreshRequest(new List<RdPath>() { pluginRootFolder.ToRd() }, true)); }); } return true; } private bool PatchUpluginFileAfterInstallation(VirtualFileSystemPath pluginBuildOutput) { var upluginFile = pluginBuildOutput / "RiderLink.uplugin"; if (!upluginFile.ExistsFile) return false; var jsonText = File.ReadAllText(upluginFile.FullPath); try { var jsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonText) as JObject; if (jsonObject == null) { myLogger.Warn($"[UnrealLink]: {upluginFile} is not a JSON file, couldn't patch it"); return false; } jsonObject["EnabledByDefault"] = true; jsonObject["Installed"] = false; File.WriteAllText(upluginFile.FullPath, jsonObject.ToString()); } catch (Exception e) { myLogger.Warn($"[UnrealLink]: Couldn't patch 'EnableByDefault' field of {upluginFile}", e); return false; } return true; } private void BindToInstallationSettingChange() { var entry = myBoundSettingsStore.Schema.GetScalarEntry((UnrealLinkSettings s) => s.AutoUpdateRiderLinkPlugin); myBoundSettingsStore.GetValueProperty<bool>(Lifetime, entry, null).Change.Advise_When(Lifetime, newValue => newValue, args => { InstallPluginIfInfoAvailable(); }); } private void InstallPluginIfInfoAvailable() { var unrealPluginInstallInfo = myPluginDetector.InstallInfoProperty.Value; if (unrealPluginInstallInfo != null) { HandleAutoUpdatePlugin(unrealPluginInstallInfo); } } public void HandleManualInstallPlugin(InstallPluginDescription installPluginDescription) { var unrealPluginInstallInfo = myPluginDetector.InstallInfoProperty.Value; if (unrealPluginInstallInfo == null) return; Lifetime.UsingNestedAsync(async lt => { var lifetimeDefinition = lt.CreateNested(); var lifetime = lifetimeDefinition.Lifetime; lifetime.Bracket( () => myUnrealHost.myModel.RiderLinkInstallationInProgress.Value = true, () => myUnrealHost.myModel.RiderLinkInstallationInProgress.Value = false ); var prefix = unrealPluginInstallInfo.EnginePlugin.IsPluginAvailable ? "Updating" : "Installing"; var header = $"{prefix} RiderLink plugin"; var progress = new Property<double>("UnrealLink.InstallPluginProgress", 0.0); var task = BackgroundProgressBuilder.Create() .AsCancelable(() => { myUnrealHost.myModel.RiderLinkInstallMessage( new InstallMessage(Strings.RiderLinkInstallationHasBeenCancelled_Text, ContentType.Error)); lifetimeDefinition.Terminate(); }) .WithHeader(header) .WithProgress(progress) .WithDescriptionFromProgress(); myBackgroundProgressManager.AddNewTask(lifetime, task); myUnrealHost.myModel.CancelRiderLinkInstall.AdviseOnce(lifetime, _ => { myUnrealHost.myModel.RiderLinkInstallMessage( new InstallMessage(Strings.RiderLinkInstallationHasBeenCancelled_Text, ContentType.Error)); lifetimeDefinition.Terminate(); }); myUnrealHost.myModel.RiderLinkInstallPanelInit(); await lifetime.StartBackground(() => { switch (installPluginDescription.Location) { case PluginInstallLocation.Engine: InstallPluginInEngine(lifetime, unrealPluginInstallInfo, progress, installPluginDescription.BuildRequired); break; case PluginInstallLocation.Game: InstallPluginInGame(lifetime, unrealPluginInstallInfo, progress, installPluginDescription.BuildRequired); break; case PluginInstallLocation.NotInstalled: default: myLogger.Error("UnrealLink: Invalid location set for installing RiderLink plugin"); break; } }); }); } private PluginInstallLocation GetInstallLocationFromSettings() { var entry = myBoundSettingsStore.Schema.GetScalarEntry((UnrealLinkSettings s) => s.DefaultLocationForRiderLink); return (PluginInstallLocation)myBoundSettingsStore.GetValue(entry, null); } private void BindToNotificationFixAction() { myUnrealHost.PerformModelAction(model => { model.InstallEditorPlugin.Advise(Lifetime, installPluginDescription => HandleManualInstallPlugin(installPluginDescription)); model.DeletePlugin.Advise(Lifetime, DeletePlugin); model.EnableAutoupdatePlugin.AdviseNotNull(Lifetime, unit => { myBoundSettingsStore.SetValue<UnrealLinkSettings, bool>(s => s.AutoUpdateRiderLinkPlugin, true); }); model.RefreshProjects.Advise(Lifetime, _ => UnrealProjectsRefresher.RefreshProjects(Lifetime, mySolution, myPluginDetector.InstallInfoProperty.Value)); }); } private void DeletePlugin() { var installInfo = myPluginDetector.InstallInfoProperty.Value; myLogger.Info($"[UnrealLink]: Deleting RiderLink plugins"); var cppUe4SolutionDetector = mySolution.GetComponent<ICppUE4SolutionDetector>(); var isSln = cppUe4SolutionDetector.SupportRiderProjectModel != CppUE4ProjectModelSupportMode.UprojectOpened; UnrealPluginInstallInfo.InstallDescription description = installInfo.EnginePlugin; bool result = true; result &= DeletePluginUsingDescription(description); foreach (var installInfoProjectPlugin in installInfo.ProjectPlugins) { description = installInfoProjectPlugin; result &= DeletePluginUsingDescription(installInfoProjectPlugin); } if (result) { myLogger.Info($"[UnrealLink]: RiderLink is deleted"); var title = Strings.DeletingRiderLinkPlugin_Text; var text = Strings.RiderLinkIsDeleted_Text; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(title, ContentType.Normal)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(text, ContentType.Normal)); var notification = new NotificationModel(mySolution.GetRdProjectId(), title, text, true, RdNotificationEntryType.INFO, new List<NotificationHyperlink>()); mySolution.Locks.ExecuteOrQueue(Lifetime, "UnrealLink.InstallPlugin", () => { myNotificationsModel.Notification(notification); }); } else { myLogger.Warn($"[UnrealLink]: Failed to delete RiderLink"); var title = Strings.DeletingRiderLinkPlugin_Text; var text = Strings.FailedToDeleteRiderLink_Text; myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(title, ContentType.Normal)); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(text, ContentType.Normal)); var notification = new NotificationModel(mySolution.GetRdProjectId(), title, text, true, RdNotificationEntryType.ERROR, new List<NotificationHyperlink>()); mySolution.Locks.ExecuteOrQueue(Lifetime, "UnrealLink.InstallPlugin", () => { myNotificationsModel.Notification(notification); }); } if (isSln) { RefreshSlnProjectFiles(description, installInfo.EngineRoot); } return; bool DeletePluginUsingDescription(UnrealPluginInstallInfo.InstallDescription description) { if (description.IsPluginAvailable) { try { description.UnrealPluginRootFolder.Delete(); } catch (Exception) { myLogger.Warn($"[UnrealLink]: Failed to delete RiderLink from {description.UnrealPluginRootFolder}"); return false; } description.IsPluginAvailable = false; if (!isSln) { RefreshUprojectProjectFiles(Lifetime, description.UnrealPluginRootFolder); } } return true; } } private bool BuildPlugin(Lifetime lifetime, VirtualFileSystemPath upluginPath, VirtualFileSystemPath outputDir, VirtualFileSystemPath engineRoot, Action<double> progressPump) { var runUatName = $"RunUAT.{CmdUtils.GetPlatformCmdExtension()}"; var pathToUat = engineRoot / "Engine" / "Build" / "BatchFiles" / runUatName; if (!pathToUat.ExistsFile) { myLogger.Warn($"[UnrealLink]: Failed build plugin: {runUatName} is not available"); myUnrealHost.myModel.RiderLinkInstallMessage( new InstallMessage(Strings.FailedToBuildRiderLinkPluginFor__Text.Format(engineRoot), ContentType.Error)); myUnrealHost.myModel.RiderLinkInstallMessage( new InstallMessage(Strings.Reason_UatIsNotAvailable_Text.Format(runUatName, pathToUat), ContentType.Error)); return false; } try { var pipeStreams = CreatePipeStreams("[UAT]:", progressPump); var startInfo = CmdUtils.GetProcessStartInfo(pipeStreams, pathToUat, null, "BuildPlugin", "-Unversioned", $"-Plugin=\"{upluginPath.FullPath}\"", $"-Package=\"{outputDir.FullPath}\""); myLogger.Info($"[UnrealLink]: Building UnrealLink plugin with: {startInfo.Arguments}"); myLogger.Verbose("[UnrealLink]: Start building UnrealLink"); var result = CmdUtils.RunCommandWithLock(lifetime, startInfo, myLogger); myLogger.Verbose("[UnrealLink]: Stop building UnrealLink"); lifetime.ToCancellationToken().ThrowIfCancellationRequested(); if (result != 0) { myLogger.Error($"[UnrealLink]: Failed to build plugin for {engineRoot}"); myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(Strings.FailedToBuildRiderLinkPluginFor__Text.Format(engineRoot), ContentType.Error)); return false; } } catch (OperationCanceledException) { myLogger.Verbose("[UnrealLink]: Build cancelled"); throw; } catch (Exception exception) { myLogger.Verbose("[UnrealLink]: Stop building UnrealLink"); myLogger.Error(exception, $"[UnrealLink]: Failed to build plugin for {engineRoot}"); myUnrealHost.myModel.RiderLinkInstallMessage( new InstallMessage(Strings.FailedToBuildRiderLinkPluginFor__Text.Format(engineRoot), ContentType.Error)); return false; } return true; } private InvokeChildProcess.PipeStreams CreatePipeStreams(string prefix, Action<double> progressPump = null) { return InvokeChildProcess.PipeStreams.Custom((chunk, isErr, logger) => { myUnrealHost.myModel.RiderLinkInstallMessage(new InstallMessage(chunk, isErr ? ContentType.Error : ContentType.Normal)); logger.Info(prefix + chunk); if (isErr) return; if (progressPump == null) return; var progressText = chunk.Trim(); if (!progressText.StartsWith("[")) return; var closingBracketIndex = progressText.IndexOf(']'); if (closingBracketIndex == -1) return; var progressNumberWithDivision = progressText.Substring(1, closingBracketIndex - 1); var numbers = progressNumberWithDivision.Split('/'); if (numbers.Length != 2) return; if (!int.TryParse(numbers[0], out var leftInt)) return; if (!int.TryParse(numbers[1], out var rightInt)) return; progressPump((double) leftInt / rightInt); }); } } }