From 39f2d4b212f41b99317b684a9272b77522f9aa7b Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 00:29:16 -0600 Subject: [PATCH] Move methods that should be static to ManifestHelper and ScriptHelper. Move install logic to GameManager and RedistributableManager. Update InstallController and UninstallController --- .../InstallController.cs | 465 ++---------------- .../LANCommander.PlaynitePlugin.csproj | 12 - .../LANCommanderLibraryPlugin.cs | 47 +- .../Services/GameSaveService.cs | 6 +- .../UninstallController.cs | 16 +- LANCommander.SDK/EventArgs.cs | 22 + LANCommander.SDK/GameManager.cs | 205 ++++++++ LANCommander.SDK/Helpers/ManifestHelper.cs | 52 ++ LANCommander.SDK/Helpers/ScriptHelper.cs | 50 ++ LANCommander.SDK/LANCommander.cs | 405 --------------- LANCommander.SDK/RedistributableManager.cs | 164 ++++++ 11 files changed, 553 insertions(+), 891 deletions(-) create mode 100644 LANCommander.SDK/EventArgs.cs create mode 100644 LANCommander.SDK/GameManager.cs create mode 100644 LANCommander.SDK/Helpers/ManifestHelper.cs create mode 100644 LANCommander.SDK/Helpers/ScriptHelper.cs delete mode 100644 LANCommander.SDK/LANCommander.cs create mode 100644 LANCommander.SDK/RedistributableManager.cs diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 084b406..3ec33f7 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -1,17 +1,10 @@ -using LANCommander.PlaynitePlugin.Helpers; -using LANCommander.SDK.Enums; -using LANCommander.SDK.Extensions; +using LANCommander.SDK; +using LANCommander.SDK.Helpers; using LANCommander.SDK.Models; using Playnite.SDK; using Playnite.SDK.Models; using Playnite.SDK.Plugins; -using SharpCompress.Common; -using SharpCompress.Readers; using System; -using System.IO; -using System.Linq; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace LANCommander.PlaynitePlugin { @@ -20,15 +13,11 @@ namespace LANCommander.PlaynitePlugin public static readonly ILogger Logger = LogManager.GetLogger(); private LANCommanderLibraryPlugin Plugin; - private PowerShellRuntime PowerShellRuntime; - private Playnite.SDK.Models.Game PlayniteGame; public LANCommanderInstallController(LANCommanderLibraryPlugin plugin, Playnite.SDK.Models.Game game) : base(game) { Name = "Install using LANCommander"; Plugin = plugin; - PlayniteGame = game; - PowerShellRuntime = new PowerShellRuntime(); } public override void Install(InstallActionArgs args) @@ -43,450 +32,52 @@ namespace LANCommander.PlaynitePlugin } var gameId = Guid.Parse(Game.GameId); - var game = Plugin.LANCommander.GetGame(gameId); - Logger.Trace($"Installing game {game.Title} ({game.Id})..."); + string installDirectory = null; - var result = RetryHelper.RetryOnException(10, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () => - { - Logger.Trace("Attempting to download and extract game..."); - return DownloadAndExtractGame(game); - }); - - if (!result.Success && !result.Canceled) - throw new Exception("Could not extract the install archive. Retry the install or check your connection."); - else if (result.Canceled) - throw new Exception("Install was canceled"); - - var installInfo = new GameInstallationData() - { - InstallDirectory = result.Directory - }; - - PlayniteGame.InstallDirectory = result.Directory; - - SDK.GameManifest manifest = null; - - var writeManifestSuccess = RetryHelper.RetryOnException(10, TimeSpan.FromSeconds(1), false, () => - { - Logger.Trace("Attempting to get game manifest..."); - - manifest = Plugin.LANCommander.GetGameManifest(gameId); - - WriteManifest(manifest, result.Directory); - - return true; - }); - - if (!writeManifestSuccess) - throw new Exception("Could not get or write the manifest file. Retry the install or check your connection."); - - Logger.Trace("Saving scripts..."); - - SaveScript(game, result.Directory, ScriptType.Install); - SaveScript(game, result.Directory, ScriptType.Uninstall); - SaveScript(game, result.Directory, ScriptType.NameChange); - SaveScript(game, result.Directory, ScriptType.KeyChange); - - if (game.Redistributables != null && game.Redistributables.Count() > 0) - { - Logger.Trace("Installing required redistributables..."); - InstallRedistributables(game); - } - - try - { - PowerShellRuntime.RunScript(PlayniteGame, ScriptType.Install); - PowerShellRuntime.RunScript(PlayniteGame, ScriptType.NameChange, Plugin.Settings.PlayerName); - - var key = Plugin.LANCommander.GetAllocatedKey(game.Id); - - PowerShellRuntime.RunScript(PlayniteGame, ScriptType.KeyChange, $"\"{key}\""); - } - catch { } - - Plugin.UpdateGame(manifest, gameId); - - Plugin.DownloadCache.Remove(gameId); - - InvokeOnInstalled(new GameInstalledEventArgs(installInfo)); - } - - private ExtractionResult DownloadAndExtractGame(LANCommander.SDK.Models.Game game) - { - if (game == null) - { - Logger.Trace("Game failed to download! No game was specified!"); - - throw new Exception("Game failed to download!"); - } - - var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename()); - - Logger.Trace($"Downloading and extracting \"{game.Title}\" to path {destination}"); var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => { - try + var gameManager = new GameManager(Plugin.LANCommanderClient); + + gameManager.OnArchiveExtractionProgress += (long pos, long len) => { - Directory.CreateDirectory(destination); - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; + progress.ProgressMaxValue = len; + progress.CurrentProgressValue = pos; + }; - using (var gameStream = Plugin.LANCommander.StreamGame(game.Id)) - using (var reader = ReaderFactory.Open(gameStream)) - { - progress.ProgressMaxValue = gameStream.Length; - - gameStream.OnProgress += (pos, len) => - { - progress.CurrentProgressValue = pos; - }; - - reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => - { - if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) - { - reader.Cancel(); - progress.IsIndeterminate = true; - - reader.Dispose(); - gameStream.Dispose(); - } - }; - - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - } - catch (Exception ex) + gameManager.OnArchiveEntryExtractionProgress += (object sender, ArchiveEntryExtractionProgressArgs e) => { if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) { - Logger.Trace("User cancelled the download"); + e.Reader.Cancel(); + e.Reader.Dispose(); + e.Stream.Dispose(); - if (Directory.Exists(destination)) - { - Logger.Trace("Cleaning up orphaned install files after cancelled install..."); - - Directory.Delete(destination, true); - } + progress.IsIndeterminate = true; } - else - { - Logger.Error(ex, $"Could not extract to path {destination}"); + }; - if (Directory.Exists(destination)) - { - Logger.Trace("Cleaning up orphaned install files after bad install..."); - - Directory.Delete(destination, true); - } - - throw new Exception("The game archive could not be extracted. Please try again or fix the archive!"); - } - } + installDirectory = gameManager.Install(gameId); }, - new GlobalProgressOptions($"Downloading {game.Title}...") + new GlobalProgressOptions($"Downloading {Game.Name}...") { - IsIndeterminate = false, + IsIndeterminate = true, Cancelable = true, }); - var extractionResult = new ExtractionResult + if (!result.Canceled && result.Error == null && !String.IsNullOrWhiteSpace(installDirectory)) { - Canceled = result.Canceled - }; + var manifest = ManifestHelper.Read(installDirectory); - if (!result.Canceled) - { - extractionResult.Success = true; - extractionResult.Directory = destination; - Logger.Trace($"Game successfully downloaded and extracted to {destination}"); + Plugin.UpdateGame(manifest); + + var installInfo = new GameInstallationData + { + InstallDirectory = installDirectory, + }; + + InvokeOnInstalled(new GameInstalledEventArgs(installInfo)); } - - return extractionResult; - } - - private void InstallRedistributables(LANCommander.SDK.Models.Game game) - { - foreach (var redistributable in game.Redistributables) - { - string installScriptTempFile = null; - string detectionScriptTempFile = null; - string extractTempPath = null; - - try - { - var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install); - installScriptTempFile = SaveTempScript(installScript); - - var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall); - detectionScriptTempFile = SaveTempScript(detectionScript); - - var detectionResult = PowerShellRuntime.RunScript(detectionScriptTempFile, detectionScript.RequiresAdmin); - - // Redistributable is not installed - if (detectionResult == 0) - { - if (redistributable.Archives.Count() > 0) - { - var extractionResult = DownloadAndExtractRedistributable(redistributable); - - if (extractionResult.Success) - { - extractTempPath = extractionResult.Directory; - - PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath); - } - } - else - { - PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath); - } - } - } - catch (Exception ex) - { - Logger.Error(ex, $"Redistributable {redistributable.Name} failed to install"); - } - finally - { - if (File.Exists(installScriptTempFile)) - File.Delete(installScriptTempFile); - - if (File.Exists(detectionScriptTempFile)) - File.Delete(detectionScriptTempFile); - - if (Directory.Exists(extractTempPath)) - Directory.Delete(extractTempPath); - } - } - } - - private ExtractionResult DownloadAndExtractRedistributable(LANCommander.SDK.Models.Redistributable redistributable) - { - if (redistributable == null) - { - Logger.Trace("Redistributable failed to download! No redistributable was specified!"); - - throw new Exception("Redistributable failed to download!"); - } - - var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename()); - - Logger.Trace($"Downloading and extracting \"{redistributable.Name}\" to path {destination}"); - var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => - { - try - { - Directory.CreateDirectory(destination); - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; - - using (var redistributableStream = Plugin.LANCommander.StreamRedistributable(redistributable.Id)) - using (var reader = ReaderFactory.Open(redistributableStream)) - { - progress.ProgressMaxValue = redistributableStream.Length; - - redistributableStream.OnProgress += (pos, len) => - { - progress.CurrentProgressValue = pos; - }; - - reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => - { - if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) - { - reader.Cancel(); - progress.IsIndeterminate = true; - - reader.Dispose(); - redistributableStream.Dispose(); - } - }; - - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - } - catch (Exception ex) - { - if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) - { - Logger.Trace("User cancelled the download"); - - if (Directory.Exists(destination)) - { - Logger.Trace("Cleaning up orphaned install files after cancelled install..."); - - Directory.Delete(destination, true); - } - } - else - { - Logger.Error(ex, $"Could not extract to path {destination}"); - - if (Directory.Exists(destination)) - { - Logger.Trace("Cleaning up orphaned install files after bad install..."); - - Directory.Delete(destination, true); - } - - throw new Exception("The redistributable archive could not be extracted. Please try again or fix the archive!"); - } - } - }, - new GlobalProgressOptions($"Downloading {redistributable.Name}...") - { - IsIndeterminate = false, - Cancelable = true, - }); - - var extractionResult = new ExtractionResult - { - Canceled = result.Canceled - }; - - if (!result.Canceled) - { - extractionResult.Success = true; - extractionResult.Directory = destination; - Logger.Trace($"Redistributable successfully downloaded and extracted to {destination}"); - } - - return extractionResult; - } - - private string Download(LANCommander.SDK.Models.Game game) - { - string tempFile = String.Empty; - - if (game != null) - { - Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => - { - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; - - var destination = Plugin.LANCommander.DownloadGame(game.Id, (changed) => - { - progress.CurrentProgressValue = changed.ProgressPercentage; - }, (complete) => - { - progress.CurrentProgressValue = 100; - }); - - // Lock the thread until download is done - while (progress.CurrentProgressValue != 100) - { - - } - - tempFile = destination; - }, - new GlobalProgressOptions($"Downloading {game.Title}...") - { - IsIndeterminate = false, - Cancelable = false, - }); - - return tempFile; - } - else - throw new Exception("Game failed to download!"); - } - - private string Extract(LANCommander.SDK.Models.Game game, string archivePath) - { - var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename()); - - Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => - { - Directory.CreateDirectory(destination); - - using (var fs = File.OpenRead(archivePath)) - using (var ts = new TrackableStream(fs)) - using (var reader = ReaderFactory.Open(ts)) - { - progress.ProgressMaxValue = ts.Length; - ts.OnProgress += (pos, len) => - { - progress.CurrentProgressValue = pos; - }; - - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - }, - new GlobalProgressOptions($"Extracting {game.Title}...") - { - IsIndeterminate = false, - Cancelable = false, - }); - - return destination; - } - - private void WriteManifest(SDK.GameManifest manifest, string installDirectory) - { - var destination = Path.Combine(installDirectory, "_manifest.yml"); - - Logger.Trace($"Attempting to write manifest to path {destination}"); - - var serializer = new SerializerBuilder() - .WithNamingConvention(new PascalCaseNamingConvention()) - .Build(); - - Logger.Trace("Serializing manifest..."); - var yaml = serializer.Serialize(manifest); - - Logger.Trace("Writing manifest file..."); - File.WriteAllText(destination, yaml); - } - - private string SaveTempScript(LANCommander.SDK.Models.Script script) - { - var tempPath = Path.GetTempFileName(); - - File.Move(tempPath, tempPath + ".ps1"); - - tempPath = tempPath + ".ps1"; - - Logger.Trace($"Writing script {script.Name} to {tempPath}"); - - File.WriteAllText(tempPath, script.Contents); - - return tempPath; - } - - private void SaveScript(LANCommander.SDK.Models.Game game, string installationDirectory, ScriptType type) - { - var script = game.Scripts.FirstOrDefault(s => s.Type == type); - - if (script == null) - return; - - if (script.RequiresAdmin) - script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents; - - var filename = PowerShellRuntime.GetScriptFilePath(PlayniteGame, type); - - if (File.Exists(filename)) - File.Delete(filename); - - Logger.Trace($"Writing {type} script to {filename}"); - - File.WriteAllText(filename, script.Contents); } } } diff --git a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj index fe6be55..bbd6a13 100644 --- a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj +++ b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj @@ -42,12 +42,6 @@ - - ..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll - - - ..\packages\SharpCompress.0.34.1\lib\net462\SharpCompress.dll - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll @@ -91,12 +85,6 @@ - - ..\packages\YamlDotNet.5.4.0\lib\net45\YamlDotNet.dll - - - ..\packages\ZstdSharp.Port.0.7.2\lib\net461\ZstdSharp.dll - diff --git a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs index 659eafa..fee02ea 100644 --- a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs +++ b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs @@ -21,8 +21,7 @@ namespace LANCommander.PlaynitePlugin { public static readonly ILogger Logger = LogManager.GetLogger(); internal LANCommanderSettingsViewModel Settings { get; set; } - internal LANCommander.SDK.LANCommander LANCommander { get; set; } - internal PowerShellRuntime PowerShellRuntime { get; set; } + internal LANCommander.SDK.Client LANCommanderClient { get; set; } internal GameSaveService GameSaveService { get; set; } public override Guid Id { get; } = Guid.Parse("48e1bac7-e0a0-45d7-ba83-36f5e9e959fc"); @@ -39,8 +38,8 @@ namespace LANCommander.PlaynitePlugin Settings = new LANCommanderSettingsViewModel(this); - LANCommander = new SDK.LANCommander(Settings.ServerAddress); - LANCommander.Client.UseToken(new SDK.Models.AuthToken() + LANCommanderClient = new SDK.Client(Settings.ServerAddress); + LANCommanderClient.UseToken(new SDK.Models.AuthToken() { AccessToken = Settings.AccessToken, RefreshToken = Settings.RefreshToken, @@ -89,7 +88,7 @@ namespace LANCommander.PlaynitePlugin public bool ValidateConnection() { - return LANCommander.Client.ValidateToken(); + return LANCommanderClient.ValidateToken(); } public override IEnumerable GetGames(LibraryGetGamesArgs args) @@ -109,7 +108,7 @@ namespace LANCommander.PlaynitePlugin } } - var games = LANCommander.Client + var games = LANCommanderClient .GetGames() .Where(g => g != null && g.Archives != null && g.Archives.Count() > 0); @@ -119,7 +118,7 @@ namespace LANCommander.PlaynitePlugin { Logger.Trace($"Importing/updating metadata for game \"{game.Title}\"..."); - var manifest = LANCommander.Client.GetGameManifest(game.Id); + var manifest = LANCommanderClient.GetGameManifest(game.Id); Logger.Trace("Successfully grabbed game manifest"); var existingGame = PlayniteApi.Database.Games.FirstOrDefault(g => g.GameId == game.Id.ToString() && g.PluginId == Id && g.IsInstalled); @@ -128,7 +127,7 @@ namespace LANCommander.PlaynitePlugin { Logger.Trace("Game already exists in library, updating metadata..."); - UpdateGame(manifest, game.Id); + UpdateGame(manifest); continue; } @@ -181,13 +180,13 @@ namespace LANCommander.PlaynitePlugin metadata.Features.Add(new MetadataNameProperty($"Online Multiplayer {manifest.OnlineMultiplayer.GetPlayerCount()}".Trim())); if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Icon)) - metadata.Icon = new MetadataFile(LANCommander.Client.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Icon))); + metadata.Icon = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Icon))); if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Cover)) - metadata.CoverImage = new MetadataFile(LANCommander.Client.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Cover))); + metadata.CoverImage = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Cover))); if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Background)) - metadata.BackgroundImage = new MetadataFile(LANCommander.Client.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Background))); + metadata.BackgroundImage = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Background))); gameMetadata.Add(metadata); } @@ -222,9 +221,9 @@ namespace LANCommander.PlaynitePlugin if (args.Games.Count == 1 && args.Games.First().IsInstalled && !String.IsNullOrWhiteSpace(args.Games.First().InstallDirectory)) { - var nameChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.NameChange); - var keyChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.KeyChange); - var installScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.Install); + var nameChangeScriptPath = LANCommander.SDK.PowerShellRuntime.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.NameChange); + var keyChangeScriptPath = LANCommander.SDK.PowerShellRuntime.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.KeyChange); + var installScriptPath = LANCommander.SDK.PowerShellRuntime.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.Install); if (File.Exists(nameChangeScriptPath)) { @@ -241,8 +240,10 @@ namespace LANCommander.PlaynitePlugin if (result.Result == true) { - PowerShellRuntime.RunScript(nameChangeArgs.Games.First(), SDK.Enums.ScriptType.NameChange, $@"""{result.SelectedString}"" ""{oldName}"""); - LANCommander.Client.ChangeAlias(result.SelectedString); + var game = nameChangeArgs.Games.First(); + + LANCommander.SDK.PowerShellRuntime.RunScript(game.InstallDirectory, SDK.Enums.ScriptType.NameChange, $@"""{result.SelectedString}"" ""{oldName}"""); + LANCommanderClient.ChangeAlias(result.SelectedString); } } }; @@ -262,12 +263,12 @@ namespace LANCommander.PlaynitePlugin if (Guid.TryParse(keyChangeArgs.Games.First().GameId, out gameId)) { // NUKIEEEE - var newKey = LANCommander.Client.GetNewKey(gameId); + var newKey = LANCommanderClient.GetNewKey(gameId); if (String.IsNullOrEmpty(newKey)) PlayniteApi.Dialogs.ShowErrorMessage("There are no more keys available on the server.", "No Keys Available"); else - PowerShellRuntime.RunScript(keyChangeArgs.Games.First(), SDK.Enums.ScriptType.KeyChange, $@"""{newKey}"""); + LANCommander.SDK.PowerShellRuntime.RunScript(keyChangeArgs.Games.First().InstallDirectory, SDK.Enums.ScriptType.KeyChange, $@"""{newKey}"""); } else { @@ -290,7 +291,7 @@ namespace LANCommander.PlaynitePlugin if (Guid.TryParse(installArgs.Games.First().GameId, out gameId)) { - PowerShellRuntime.RunScript(installArgs.Games.First(), SDK.Enums.ScriptType.Install); + LANCommander.SDK.PowerShellRuntime.RunScript(installArgs.Games.First().InstallDirectory, SDK.Enums.ScriptType.Install); } else { @@ -400,11 +401,11 @@ namespace LANCommander.PlaynitePlugin var games = PlayniteApi.Database.Games.Where(g => g.IsInstalled).ToList(); - LANCommander.Client.ChangeAlias(result.SelectedString); + LANCommanderClient.ChangeAlias(result.SelectedString); Logger.Trace($"Running name change scripts across {games.Count} installed game(s)"); - PowerShellRuntime.RunScripts(games, SDK.Enums.ScriptType.NameChange, Settings.PlayerName); + LANCommander.SDK.PowerShellRuntime.RunScripts(games.Select(g => g.InstallDirectory), SDK.Enums.ScriptType.NameChange, Settings.PlayerName); } } else @@ -439,9 +440,9 @@ namespace LANCommander.PlaynitePlugin return window; } - public void UpdateGame(SDK.GameManifest manifest, Guid gameId) + public void UpdateGame(SDK.GameManifest manifest) { - var game = PlayniteApi.Database.Games.First(g => g.GameId == gameId.ToString()); + var game = PlayniteApi.Database.Games.First(g => g.GameId == manifest.Id.ToString()); if (game == null) return; diff --git a/LANCommander.Playnite.Extension/Services/GameSaveService.cs b/LANCommander.Playnite.Extension/Services/GameSaveService.cs index a650e5c..4513443 100644 --- a/LANCommander.Playnite.Extension/Services/GameSaveService.cs +++ b/LANCommander.Playnite.Extension/Services/GameSaveService.cs @@ -17,15 +17,13 @@ namespace LANCommander.PlaynitePlugin.Services { internal class GameSaveService { - private readonly LANCommanderClient LANCommander; + private readonly LANCommander.SDK.Client LANCommander; private readonly IPlayniteAPI PlayniteApi; - private readonly PowerShellRuntime PowerShellRuntime; - internal GameSaveService(LANCommanderClient lanCommander, IPlayniteAPI playniteApi, PowerShellRuntime powerShellRuntime) + internal GameSaveService(LANCommander.SDK.Client lanCommander, IPlayniteAPI playniteApi) { LANCommander = lanCommander; PlayniteApi = playniteApi; - PowerShellRuntime = powerShellRuntime; } internal void DownloadSave(Game game) diff --git a/LANCommander.Playnite.Extension/UninstallController.cs b/LANCommander.Playnite.Extension/UninstallController.cs index 3a77ee6..93518d0 100644 --- a/LANCommander.Playnite.Extension/UninstallController.cs +++ b/LANCommander.Playnite.Extension/UninstallController.cs @@ -12,29 +12,25 @@ namespace LANCommander.PlaynitePlugin public static readonly ILogger Logger = LogManager.GetLogger(); private LANCommanderLibraryPlugin Plugin; - private PowerShellRuntime PowerShellRuntime; public LANCommanderUninstallController(LANCommanderLibraryPlugin plugin, Game game) : base(game) { Name = "Uninstall LANCommander Game"; Plugin = plugin; - PowerShellRuntime = new PowerShellRuntime(); } public override void Uninstall(UninstallActionArgs args) { try { - PowerShellRuntime.RunScript(Game, ScriptType.Uninstall); + var gameManager = new LANCommander.SDK.GameManager(Plugin.LANCommanderClient); + + gameManager.Uninstall(Game.InstallDirectory); } - catch { } + catch (Exception ex) + { - Logger.Trace("Attempting to delete install directory..."); - - if (!String.IsNullOrWhiteSpace(Game.InstallDirectory) && Directory.Exists(Game.InstallDirectory)) - Directory.Delete(Game.InstallDirectory, true); - - Logger.Trace("Deleted!"); + } InvokeOnUninstalled(new GameUninstalledEventArgs()); } diff --git a/LANCommander.SDK/EventArgs.cs b/LANCommander.SDK/EventArgs.cs new file mode 100644 index 0000000..ca85135 --- /dev/null +++ b/LANCommander.SDK/EventArgs.cs @@ -0,0 +1,22 @@ +using SharpCompress.Common; +using SharpCompress.Readers; +using System; +using System.Collections.Generic; +using System.Text; + +namespace LANCommander.SDK +{ + public class ArchiveExtractionProgressArgs : EventArgs + { + public long Position { get; set; } + public long Length { get; set; } + } + + public class ArchiveEntryExtractionProgressArgs : EventArgs + { + public IReader Reader { get; set; } + public TrackableStream Stream { get; set; } + public ReaderProgress Progress { get; set; } + public IEntry Entry { get; set; } + } +} diff --git a/LANCommander.SDK/GameManager.cs b/LANCommander.SDK/GameManager.cs new file mode 100644 index 0000000..c60f1b1 --- /dev/null +++ b/LANCommander.SDK/GameManager.cs @@ -0,0 +1,205 @@ +using LANCommander.SDK.Enums; +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.Models; +using Microsoft.Extensions.Logging; +using SharpCompress.Common; +using SharpCompress.Readers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LANCommander.SDK +{ + public class GameManager + { + private static readonly ILogger Logger; + private Client Client { get; set; } + private string DefaultInstallDirectory { get; set; } + + public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); + public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress; + + public delegate void OnArchiveExtractionProgressHandler(long position, long length); + public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress; + + public GameManager(Client client) + { + Client = client; + } + + /// + /// Downloads, extracts, and runs post-install scripts for the specified game + /// + /// Game to install + /// Maximum attempts in case of transmission error + /// Final install path + /// + public string Install(Guid gameId, int maxAttempts = 10) + { + var game = Client.GetGame(gameId); + + Logger.LogTrace("Installing game {GameTitle} (GameId)", game.Title, game.Id); + + var result = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () => + { + Logger.LogTrace("Attempting to download and extract game"); + + return DownloadAndExtract(game); + }); + + if (!result.Success && !result.Canceled) + throw new Exception("Could not extract the installer. Retry the install or check your connection"); + else if (result.Canceled) + throw new Exception("Game install was canceled"); + + GameManifest manifest = null; + + game.InstallDirectory = result.Directory; + + var writeManifestSuccess = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromSeconds(1), false, () => + { + Logger.LogTrace("Attempting to get game manifest"); + + manifest = Client.GetGameManifest(game.Id); + + ManifestHelper.Write(manifest, game.InstallDirectory); + + return true; + }); + + if (!writeManifestSuccess) + throw new Exception("Could not grab the manifest file. Retry the install or check your connection"); + + Logger.LogTrace("Saving scripts"); + + ScriptHelper.SaveScript(game, ScriptType.Install); + ScriptHelper.SaveScript(game, ScriptType.Uninstall); + ScriptHelper.SaveScript(game, ScriptType.NameChange); + ScriptHelper.SaveScript(game, ScriptType.KeyChange); + + try + { + PowerShellRuntime.RunScript(game, ScriptType.Install); + PowerShellRuntime.RunScript(game, ScriptType.NameChange, /* Plugin.Settings.PlayerName */ ""); + + var key = Client.GetAllocatedKey(game.Id); + + PowerShellRuntime.RunScript(game, ScriptType.KeyChange, $"\"{key}\""); + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not execute post-install scripts"); + } + + return result.Directory; + } + + public void Uninstall(string installDirectory) + { + var manifest = ManifestHelper.Read(installDirectory); + + try + { + Logger.LogTrace("Running uninstall script"); + PowerShellRuntime.RunScript(installDirectory, ScriptType.Uninstall); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error running uninstall script"); + } + + Logger.LogTrace("Attempting to delete the install directory"); + + if (Directory.Exists(installDirectory)) + Directory.Delete(installDirectory, true); + + Logger.LogTrace("Deleted install directory {InstallDirectory}", installDirectory); + } + + private ExtractionResult DownloadAndExtract(Game game, string installDirectory = "") + { + if (game == null) + { + Logger.LogTrace("Game failed to download, no game was specified"); + + throw new ArgumentNullException("No game was specified"); + } + + if (String.IsNullOrWhiteSpace(installDirectory)) + installDirectory = DefaultInstallDirectory; + + var destination = Path.Combine(installDirectory, game.Title.SanitizeFilename()); + + Logger.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination); + + try + { + Directory.CreateDirectory(destination); + + using (var gameStream = Client.StreamGame(game.Id)) + using (var reader = ReaderFactory.Open(gameStream)) + { + gameStream.OnProgress += (pos, len) => + { + OnArchiveExtractionProgress?.Invoke(pos, len); + }; + + reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => + { + OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs + { + Entry = e.Item, + Progress = e.ReaderProgress, + Reader = reader, + Stream = gameStream + }); + }; + + reader.WriteAllToDirectory(destination, new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + catch (Exception ex) + { + if (false) + { + + } + else + { + Logger.LogError(ex, "Could not extract to path {Destination}", destination); + + if (Directory.Exists(destination)) + { + Logger.LogTrace("Cleaning up orphaned install files after bad install"); + + Directory.Delete(destination, true); + } + + throw new Exception("The game archive could not be extracted, is it corrupted? Please try again"); + } + } + + var extractionResult = new ExtractionResult + { + Canceled = false, + }; + + if (!extractionResult.Canceled) + { + extractionResult.Success = true; + extractionResult.Directory = destination; + + Logger.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination); + } + + return extractionResult; + } + } +} diff --git a/LANCommander.SDK/Helpers/ManifestHelper.cs b/LANCommander.SDK/Helpers/ManifestHelper.cs new file mode 100644 index 0000000..7e82586 --- /dev/null +++ b/LANCommander.SDK/Helpers/ManifestHelper.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization; + +namespace LANCommander.SDK.Helpers +{ + public static class ManifestHelper + { + public static readonly ILogger Logger; + + public const string ManifestFilename = "_manifest.yml"; + + public static GameManifest Read(string installDirectory) + { + var source = Path.Combine(installDirectory, ManifestFilename); + var yaml = File.ReadAllText(source); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .Build(); + + Logger.LogTrace("Deserializing manifest"); + + var manifest = deserializer.Deserialize(source); + + return manifest; + } + + public static void Write(GameManifest manifest, string installDirectory) + { + var destination = Path.Combine(installDirectory, ManifestFilename); + + Logger.LogTrace("Attempting to write manifest to path {Destination}", destination); + + var serializer = new SerializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .Build(); + + Logger.LogTrace("Serializing manifest"); + + var yaml = serializer.Serialize(manifest); + + Logger.LogTrace("Writing manifest file"); + + File.WriteAllText(destination, yaml); + } + } +} diff --git a/LANCommander.SDK/Helpers/ScriptHelper.cs b/LANCommander.SDK/Helpers/ScriptHelper.cs new file mode 100644 index 0000000..2a6e926 --- /dev/null +++ b/LANCommander.SDK/Helpers/ScriptHelper.cs @@ -0,0 +1,50 @@ +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LANCommander.SDK.Helpers +{ + public static class ScriptHelper + { + public static readonly ILogger Logger; + + public static string SaveTempScript(Script script) + { + var tempPath = Path.GetTempFileName(); + + // PowerShell will only run scripts with the .ps1 file extension + File.Move(tempPath, tempPath + ".ps1"); + + Logger.LogTrace("Writing script {Script} to {Destination}", script.Name, tempPath); + + File.WriteAllText(tempPath, script.Contents); + + return tempPath; + } + + public static void SaveScript(Game game, ScriptType type) + { + var script = game.Scripts.FirstOrDefault(s => s.Type == type); + + if (script == null) + return; + + if (script.RequiresAdmin) + script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents; + + var filename = PowerShellRuntime.GetScriptFilePath(game, type); + + if (File.Exists(filename)) + File.Delete(filename); + + Logger.LogTrace("Writing {ScriptType} script to {Destination}", type, filename); + + File.WriteAllText(filename, script.Contents); + } + } +} diff --git a/LANCommander.SDK/LANCommander.cs b/LANCommander.SDK/LANCommander.cs deleted file mode 100644 index 743c727..0000000 --- a/LANCommander.SDK/LANCommander.cs +++ /dev/null @@ -1,405 +0,0 @@ -using LANCommander.SDK.Enums; -using LANCommander.SDK.Extensions; -using LANCommander.SDK.Helpers; -using LANCommander.SDK.Models; -using Microsoft.Extensions.Logging; -using SharpCompress.Common; -using SharpCompress.Readers; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using YamlDotNet.RepresentationModel; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace LANCommander.SDK -{ - public class ArchiveExtractionProgressArgs : EventArgs - { - public long Position { get; set; } - public long Length { get; set; } - } - - public class ArchiveEntryExtractionProgressArgs : EventArgs - { - public IReader Reader { get; set; } - public TrackableStream Stream { get; set; } - public ReaderProgress Progress { get; set; } - public IEntry Entry { get; set; } - } - - public class LANCommander - { - public static readonly ILogger Logger; - - private const string ManifestFilename = "_manifest.yml"; - - private string DefaultInstallDirectory { get; set; } - public Client Client { get; set; } - private PowerShellRuntime PowerShellRuntime; - - public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); - public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress; - - public delegate void OnArchiveExtractionProgressHandler(long position, long length); - public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress; - - public LANCommander(string baseUrl) - { - Client = new Client(baseUrl); - } - - /// - /// Downloads, extracts, and runs post-install scripts for the specified game - /// - /// Game to install - /// Maximum attempts in case of transmission error - /// Final install path - /// - public string InstallGame(Guid gameId, int maxAttempts = 10) - { - var game = Client.GetGame(gameId); - - Logger.LogTrace("Installing game {GameTitle} (GameId)", game.Title, game.Id); - - var result = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () => - { - Logger.LogTrace("Attempting to download and extract game"); - - return DownloadAndExtractGame(game); - }); - - if (!result.Success && !result.Canceled) - throw new Exception("Could not extract the installer. Retry the install or check your connection"); - else if (result.Canceled) - throw new Exception("Game install was canceled"); - - GameManifest manifest = null; - - game.InstallDirectory = result.Directory; - - var writeManifestSuccess = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromSeconds(1), false, () => - { - Logger.LogTrace("Attempting to get game manifest"); - - manifest = Client.GetGameManifest(game.Id); - - WriteManifest(manifest, game.InstallDirectory); - - return true; - }); - - if (!writeManifestSuccess) - throw new Exception("Could not grab the manifest file. Retry the install or check your connection"); - - Logger.LogTrace("Saving scripts"); - - SaveScript(game, ScriptType.Install); - SaveScript(game, ScriptType.Uninstall); - SaveScript(game, ScriptType.NameChange); - SaveScript(game, ScriptType.KeyChange); - - if (game.Redistributables != null && game.Redistributables.Count() > 0) - { - Logger.LogTrace("Installing required redistributables"); - InstallRedistributables(game); - } - - try - { - PowerShellRuntime.RunScript(game, ScriptType.Install); - PowerShellRuntime.RunScript(game, ScriptType.NameChange, /* Plugin.Settings.PlayerName */ ""); - - var key = Client.GetAllocatedKey(game.Id); - - PowerShellRuntime.RunScript(game, ScriptType.KeyChange, $"\"{key}\""); - } - catch (Exception ex) - { - Logger.LogError(ex, "Could not execute post-install scripts"); - } - - // Plugin.UpdateGame(manifest, gameId) - - // Plugin.DownloadCache.Remove(gameId); - - return result.Directory; - } - - private ExtractionResult DownloadAndExtractGame(Game game, string installDirectory = "") - { - if (game == null) - { - Logger.LogTrace("Game failed to download, no game was specified"); - - throw new ArgumentNullException("No game was specified"); - } - - if (String.IsNullOrWhiteSpace(installDirectory)) - installDirectory = DefaultInstallDirectory; - - var destination = Path.Combine(installDirectory, game.Title.SanitizeFilename()); - - Logger.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination); - - try - { - Directory.CreateDirectory(destination); - - using (var gameStream = Client.StreamGame(game.Id)) - using (var reader = ReaderFactory.Open(gameStream)) - { - gameStream.OnProgress += (pos, len) => - { - OnArchiveExtractionProgress?.Invoke(pos, len); - }; - - reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => - { - OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs - { - Entry = e.Item, - Progress = e.ReaderProgress, - Reader = reader, - Stream = gameStream - }); - }; - - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - } - catch (Exception ex) - { - if (false) - { - - } - else - { - Logger.LogError(ex, "Could not extract to path {Destination}", destination); - - if (Directory.Exists(destination)) - { - Logger.LogTrace("Cleaning up orphaned install files after bad install"); - - Directory.Delete(destination, true); - } - - throw new Exception("The game archive could not be extracted, is it corrupted? Please try again"); - } - } - - var extractionResult = new ExtractionResult - { - Canceled = false, - }; - - if (!extractionResult.Canceled) - { - extractionResult.Success = true; - extractionResult.Directory = destination; - - Logger.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination); - } - - return extractionResult; - } - - private void InstallRedistributables(Game game) - { - foreach (var redistributable in game.Redistributables) - { - InstallRedistributable(redistributable); - } - } - - private void InstallRedistributable(Redistributable redistributable) - { - string installScriptTempFile = null; - string detectionScriptTempFile = null; - string extractTempPath = null; - - try - { - var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install); - installScriptTempFile = SaveTempScript(installScript); - - var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall); - detectionScriptTempFile = SaveTempScript(detectionScript); - - var detectionResult = PowerShellRuntime.RunScript(detectionScriptTempFile, detectionScript.RequiresAdmin); - - // Redistributable is not installed - if (detectionResult == 0) - { - if (redistributable.Archives.Count() > 0) - { - var extractionResult = DownloadAndExtractRedistributable(redistributable); - - if (extractionResult.Success) - { - extractTempPath = extractionResult.Directory; - - PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath); - } - } - else - { - PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath); - } - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Redistributable {Redistributable} failed to install", redistributable.Name); - } - finally - { - if (File.Exists(installScriptTempFile)) - File.Delete(installScriptTempFile); - - if (File.Exists(detectionScriptTempFile)) - File.Delete(detectionScriptTempFile); - - if (Directory.Exists(extractTempPath)) - Directory.Delete(extractTempPath); - } - } - - private ExtractionResult DownloadAndExtractRedistributable(Redistributable redistributable) - { - if (redistributable == null) - { - Logger.LogTrace("Redistributable failed to download! No redistributable was specified"); - throw new ArgumentNullException("No redistributable was specified"); - } - - var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename()); - - Logger.LogTrace("Downloading and extracting {Redistributable} to path {Destination}", redistributable.Name, destination); - - try - { - Directory.CreateDirectory(destination); - - using (var redistributableStream = Client.StreamRedistributable(redistributable.Id)) - using (var reader = ReaderFactory.Open(redistributableStream)) - { - redistributableStream.OnProgress += (pos, len) => - { - OnArchiveExtractionProgress?.Invoke(pos, len); - }; - - reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => - { - OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs - { - Entry = e.Item, - Progress = e.ReaderProgress, - Reader = reader, - Stream = redistributableStream - }); - }; - - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Could not extract to path {Destination}", destination); - - if (Directory.Exists(destination)) - { - Logger.LogTrace("Cleaning up orphaned files after bad install"); - - Directory.Delete(destination, true); - } - - throw new Exception("The redistributable archive could not be extracted, is it corrupted? Please try again"); - } - - var extractionResult = new ExtractionResult - { - Canceled = false - }; - - if (!extractionResult.Canceled) - { - extractionResult.Success = true; - extractionResult.Directory = destination; - Logger.LogTrace("Redistributable {Redistributable} successfully downloaded and extracted to {Destination}", redistributable.Name, destination); - } - - return extractionResult; - } - - public void WriteManifest(GameManifest manifest, string installDirectory) - { - var destination = Path.Combine(installDirectory, ManifestFilename); - - Logger.LogTrace("Attempting to write manifest to path {Destination}", destination); - - var serializer = new SerializerBuilder() - .WithNamingConvention(PascalCaseNamingConvention.Instance) - .Build(); - - Logger.LogTrace("Serializing manifest"); - - var yaml = serializer.Serialize(manifest); - - Logger.LogTrace("Writing manifest file"); - - File.WriteAllText(destination, yaml); - } - - private string SaveTempScript(Script script) - { - var tempPath = Path.GetTempFileName(); - - // PowerShell will only run scripts with the .ps1 file extension - File.Move(tempPath, tempPath + ".ps1"); - - Logger.LogTrace("Writing script {Script} to {Destination}", script.Name, tempPath); - - File.WriteAllText(tempPath, script.Contents); - - return tempPath; - } - - private void SaveScript(Game game, ScriptType type) - { - var script = game.Scripts.FirstOrDefault(s => s.Type == type); - - if (script == null) - return; - - if (script.RequiresAdmin) - script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents; - - var filename = PowerShellRuntime.GetScriptFilePath(game, type); - - if (File.Exists(filename)) - File.Delete(filename); - - Logger.LogTrace("Writing {ScriptType} script to {Destination}", type, filename); - - File.WriteAllText(filename, script.Contents); - } - - public void ChangeAlias(string alias) - { - - } - } -} diff --git a/LANCommander.SDK/RedistributableManager.cs b/LANCommander.SDK/RedistributableManager.cs new file mode 100644 index 0000000..bb4ce9f --- /dev/null +++ b/LANCommander.SDK/RedistributableManager.cs @@ -0,0 +1,164 @@ +using LANCommander.SDK.Enums; +using LANCommander.SDK.Extensions; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.Models; +using Microsoft.Extensions.Logging; +using SharpCompress.Common; +using SharpCompress.Readers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LANCommander.SDK +{ + public class RedistributableManager + { + private static readonly ILogger Logger; + private Client Client { get; set; } + + public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); + public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress; + + public delegate void OnArchiveExtractionProgressHandler(long position, long length); + public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress; + + public RedistributableManager(Client client) + { + Client = client; + } + + public void Install(Game game) + { + foreach (var redistributable in game.Redistributables) + { + Install(redistributable); + } + } + + public void Install(Redistributable redistributable) + { + string installScriptTempFile = null; + string detectionScriptTempFile = null; + string extractTempPath = null; + + try + { + var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install); + installScriptTempFile = ScriptHelper.SaveTempScript(installScript); + + var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall); + detectionScriptTempFile = ScriptHelper.SaveTempScript(detectionScript); + + var detectionResult = PowerShellRuntime.RunScript(detectionScriptTempFile, detectionScript.RequiresAdmin); + + // Redistributable is not installed + if (detectionResult == 0) + { + if (redistributable.Archives.Count() > 0) + { + var extractionResult = DownloadAndExtract(redistributable); + + if (extractionResult.Success) + { + extractTempPath = extractionResult.Directory; + + PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath); + } + } + else + { + PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath); + } + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Redistributable {Redistributable} failed to install", redistributable.Name); + } + finally + { + if (File.Exists(installScriptTempFile)) + File.Delete(installScriptTempFile); + + if (File.Exists(detectionScriptTempFile)) + File.Delete(detectionScriptTempFile); + + if (Directory.Exists(extractTempPath)) + Directory.Delete(extractTempPath); + } + } + + private ExtractionResult DownloadAndExtract(Redistributable redistributable) + { + if (redistributable == null) + { + Logger.LogTrace("Redistributable failed to download! No redistributable was specified"); + throw new ArgumentNullException("No redistributable was specified"); + } + + var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename()); + + Logger.LogTrace("Downloading and extracting {Redistributable} to path {Destination}", redistributable.Name, destination); + + try + { + Directory.CreateDirectory(destination); + + using (var redistributableStream = Client.StreamRedistributable(redistributable.Id)) + using (var reader = ReaderFactory.Open(redistributableStream)) + { + redistributableStream.OnProgress += (pos, len) => + { + OnArchiveExtractionProgress?.Invoke(pos, len); + }; + + reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => + { + OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs + { + Entry = e.Item, + Progress = e.ReaderProgress, + Reader = reader, + Stream = redistributableStream + }); + }; + + reader.WriteAllToDirectory(destination, new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true + }); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not extract to path {Destination}", destination); + + if (Directory.Exists(destination)) + { + Logger.LogTrace("Cleaning up orphaned files after bad install"); + + Directory.Delete(destination, true); + } + + throw new Exception("The redistributable archive could not be extracted, is it corrupted? Please try again"); + } + + var extractionResult = new ExtractionResult + { + Canceled = false + }; + + if (!extractionResult.Canceled) + { + extractionResult.Success = true; + extractionResult.Directory = destination; + Logger.LogTrace("Redistributable {Redistributable} successfully downloaded and extracted to {Destination}", redistributable.Name, destination); + } + + return extractionResult; + } + } +}