diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 084b406..7c7bb86 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -1,17 +1,12 @@ -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.Diagnostics; using System.Linq; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace LANCommander.PlaynitePlugin { @@ -20,15 +15,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 +34,101 @@ 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(), () => + var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => { - Logger.Trace("Attempting to download and extract game..."); - return DownloadAndExtractGame(game); + var gameManager = new GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory); + + Stopwatch stopwatch = new Stopwatch(); + + stopwatch.Start(); + + var lastTotalSize = 0d; + var speed = 0d; + + gameManager.OnArchiveExtractionProgress += (long pos, long len) => + { + if (stopwatch.ElapsedMilliseconds > 500) + { + var percent = Math.Ceiling((pos / (decimal)len) * 100); + + progress.ProgressMaxValue = len; + progress.CurrentProgressValue = pos; + + speed = (double)(progress.CurrentProgressValue - lastTotalSize) / (stopwatch.ElapsedMilliseconds / 1000d); + + progress.Text = $"Downloading {Game.Name} ({percent}%) | {ByteSizeLib.ByteSize.FromBytes(speed).ToString("#.#")}/s"; + + lastTotalSize = pos; + + stopwatch.Restart(); + } + }; + + gameManager.OnArchiveEntryExtractionProgress += (object sender, ArchiveEntryExtractionProgressArgs e) => + { + if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) + { + gameManager.CancelInstall(); + + progress.IsIndeterminate = true; + } + }; + + installDirectory = gameManager.Install(gameId); + + stopwatch.Stop(); + }, + new GlobalProgressOptions($"Preparing to download {Game.Name}") + { + IsIndeterminate = false, + Cancelable = true, }); - 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); + // Install any redistributables + var game = Plugin.LANCommanderClient.GetGame(gameId); 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 - { - Directory.CreateDirectory(destination); - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; - - 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) - { - 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 game archive could not be extracted. Please try again or fix the archive!"); - } - } - }, - new GlobalProgressOptions($"Downloading {game.Title}...") - { - IsIndeterminate = false, - Cancelable = true, - }); - - var extractionResult = new ExtractionResult - { - Canceled = result.Canceled - }; - - if (!result.Canceled) - { - extractionResult.Success = true; - extractionResult.Directory = destination; - Logger.Trace($"Game successfully downloaded and extracted to {destination}"); - } - - 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 redistributableManager = new RedistributableManager(Plugin.LANCommanderClient); - 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; + redistributableManager.Install(game); }, - new GlobalProgressOptions($"Downloading {game.Title}...") + new GlobalProgressOptions("Installing redistributables...") { - IsIndeterminate = false, + IsIndeterminate = true, 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 => + if (!result.Canceled && result.Error == null && !String.IsNullOrWhiteSpace(installDirectory)) { - Directory.CreateDirectory(destination); + var manifest = ManifestHelper.Read(installDirectory); - using (var fs = File.OpenRead(archivePath)) - using (var ts = new TrackableStream(fs)) - using (var reader = ReaderFactory.Open(ts)) + Plugin.UpdateGame(manifest); + + var installInfo = new GameInstallationData { - progress.ProgressMaxValue = ts.Length; - ts.OnProgress += (pos, len) => - { - progress.CurrentProgressValue = pos; - }; + InstallDirectory = installDirectory, + }; - reader.WriteAllToDirectory(destination, new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } - }, - new GlobalProgressOptions($"Extracting {game.Title}...") + InvokeOnInstalled(new GameInstalledEventArgs(installInfo)); + } + else if (result.Canceled) { - IsIndeterminate = false, - Cancelable = false, - }); + var dbGame = Plugin.PlayniteApi.Database.Games.Get(Game.Id); - return destination; - } + dbGame.IsInstalling = false; + dbGame.IsInstalled = false; - 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); + Plugin.PlayniteApi.Database.Games.Update(dbGame); + } + else if (result.Error != null) + throw result.Error; } } } diff --git a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj index 20a9688..38dff6c 100644 --- a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj +++ b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj @@ -34,6 +34,9 @@ ..\packages\rix0rrr.BeaconLib.1.0.2\lib\net40\BeaconLib.dll + + ..\packages\ByteSize.2.1.1\lib\net45\ByteSize.dll + ..\packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll @@ -42,9 +45,6 @@ - - ..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll - ..\packages\SharpCompress.0.34.1\lib\net462\SharpCompress.dll @@ -91,23 +91,15 @@ - - ..\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 3f80ec0..e118e19 100644 --- a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs +++ b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs @@ -1,5 +1,4 @@ using LANCommander.PlaynitePlugin.Extensions; -using LANCommander.PlaynitePlugin.Services; using Playnite.SDK; using Playnite.SDK.Events; using Playnite.SDK.Models; @@ -21,9 +20,8 @@ namespace LANCommander.PlaynitePlugin { public static readonly ILogger Logger = LogManager.GetLogger(); internal LANCommanderSettingsViewModel Settings { get; set; } - internal LANCommanderClient LANCommander { get; set; } - internal PowerShellRuntime PowerShellRuntime { get; set; } - internal GameSaveService GameSaveService { get; set; } + internal LANCommander.SDK.Client LANCommanderClient { get; set; } + internal LANCommanderSaveController SaveController { get; set; } public override Guid Id { get; } = Guid.Parse("48e1bac7-e0a0-45d7-ba83-36f5e9e959fc"); public override string Name => "LANCommander"; @@ -39,16 +37,14 @@ namespace LANCommander.PlaynitePlugin Settings = new LANCommanderSettingsViewModel(this); - LANCommander = new LANCommanderClient(Settings.ServerAddress); - LANCommander.Token = new SDK.Models.AuthToken() + LANCommanderClient = new SDK.Client(Settings.ServerAddress); + LANCommanderClient.UseToken(new SDK.Models.AuthToken() { AccessToken = Settings.AccessToken, RefreshToken = Settings.RefreshToken, - }; + }); - PowerShellRuntime = new PowerShellRuntime(); - - GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime); + // GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime); api.UriHandler.RegisterSource("lancommander", args => { @@ -91,7 +87,7 @@ namespace LANCommander.PlaynitePlugin public bool ValidateConnection() { - return LANCommander.ValidateToken(LANCommander.Token); + return LANCommanderClient.ValidateToken(); } public override IEnumerable GetGames(LibraryGetGamesArgs args) @@ -111,7 +107,7 @@ namespace LANCommander.PlaynitePlugin } } - var games = LANCommander + var games = LANCommanderClient .GetGames() .Where(g => g != null && g.Archives != null && g.Archives.Count() > 0); @@ -121,7 +117,7 @@ namespace LANCommander.PlaynitePlugin { Logger.Trace($"Importing/updating metadata for game \"{game.Title}\"..."); - var manifest = LANCommander.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); @@ -130,7 +126,7 @@ namespace LANCommander.PlaynitePlugin { Logger.Trace("Game already exists in library, updating metadata..."); - UpdateGame(manifest, game.Id); + UpdateGame(manifest); continue; } @@ -183,13 +179,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.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.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.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); } @@ -224,9 +220,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)) { @@ -243,8 +239,10 @@ namespace LANCommander.PlaynitePlugin if (result.Result == true) { - PowerShellRuntime.RunScript(nameChangeArgs.Games.First(), SDK.Enums.ScriptType.NameChange, $@"""{result.SelectedString}"" ""{oldName}"""); - LANCommander.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); } } }; @@ -264,12 +262,12 @@ namespace LANCommander.PlaynitePlugin if (Guid.TryParse(keyChangeArgs.Games.First().GameId, out gameId)) { // NUKIEEEE - var newKey = LANCommander.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 { @@ -292,7 +290,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 { @@ -334,12 +332,12 @@ namespace LANCommander.PlaynitePlugin public override void OnGameStarting(OnGameStartingEventArgs args) { - GameSaveService.DownloadSave(args.Game); + SaveController.Download(args.Game); } public override void OnGameStopped(OnGameStoppedEventArgs args) { - GameSaveService.UploadSave(args.Game); + SaveController.Upload(args.Game); } public override IEnumerable GetTopPanelItems() @@ -402,11 +400,11 @@ namespace LANCommander.PlaynitePlugin var games = PlayniteApi.Database.Games.Where(g => g.IsInstalled).ToList(); - LANCommander.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 @@ -441,9 +439,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.FirstOrDefault(g => g.GameId == manifest?.Id.ToString()); if (game == null) return; diff --git a/LANCommander.Playnite.Extension/SaveController.cs b/LANCommander.Playnite.Extension/SaveController.cs new file mode 100644 index 0000000..557ee2e --- /dev/null +++ b/LANCommander.Playnite.Extension/SaveController.cs @@ -0,0 +1,61 @@ +using LANCommander.SDK; +using Playnite.SDK; +using Playnite.SDK.Models; +using Playnite.SDK.Plugins; + +namespace LANCommander.PlaynitePlugin +{ + public class LANCommanderSaveController : ControllerBase + { + private static readonly ILogger Logger; + + private LANCommanderLibraryPlugin Plugin; + + public LANCommanderSaveController(LANCommanderLibraryPlugin plugin, Game game) : base(game) + { + Name = "Download save using LANCommander"; + Plugin = plugin; + } + + public void Download(Game game) + { + if (game != null) + { + Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => + { + progress.ProgressMaxValue = 100; + progress.CurrentProgressValue = 0; + + var saveManager = new GameSaveManager(Plugin.LANCommanderClient); + + saveManager.OnDownloadProgress += (downloadProgress) => + { + progress.CurrentProgressValue = downloadProgress.ProgressPercentage; + }; + + saveManager.OnDownloadComplete += (downloadComplete) => + { + progress.CurrentProgressValue = 100; + }; + + saveManager.Download(game.InstallDirectory); + + // Lock the thread until the download is done + while (progress.CurrentProgressValue != 100) { } + }, + new GlobalProgressOptions("Downloading latest save...") + { + IsIndeterminate = false, + Cancelable = false + }); + } + } + + public void Upload(Game game) + { + var saveManager = new GameSaveManager(Plugin.LANCommanderClient); + + saveManager.Upload(game.InstallDirectory); + } + } +} diff --git a/LANCommander.Playnite.Extension/UninstallController.cs b/LANCommander.Playnite.Extension/UninstallController.cs index 3a77ee6..eb23735 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, Plugin.Settings.InstallDirectory); + + 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.Playnite.Extension/Views/Authentication.xaml.cs b/LANCommander.Playnite.Extension/Views/Authentication.xaml.cs index 6b3db4f..0e6869a 100644 --- a/LANCommander.Playnite.Extension/Views/Authentication.xaml.cs +++ b/LANCommander.Playnite.Extension/Views/Authentication.xaml.cs @@ -100,24 +100,16 @@ namespace LANCommander.PlaynitePlugin.Views LoginButton.Content = "Logging in..."; })); - if (Plugin.LANCommander == null || Plugin.LANCommander.Client == null) - Plugin.LANCommander = new LANCommanderClient(Context.ServerAddress); - else - Plugin.LANCommander.Client.BaseUrl = new Uri(Context.ServerAddress); + if (Plugin.LANCommanderClient == null) + Plugin.LANCommanderClient = new LANCommander.SDK.Client(Context.ServerAddress); - var response = await Plugin.LANCommander.AuthenticateAsync(Context.UserName, Context.Password); + var response = await Plugin.LANCommanderClient.AuthenticateAsync(Context.UserName, Context.Password); Plugin.Settings.ServerAddress = Context.ServerAddress; Plugin.Settings.AccessToken = response.AccessToken; Plugin.Settings.RefreshToken = response.RefreshToken; - Plugin.LANCommander.Token = new AuthToken() - { - AccessToken = response.AccessToken, - RefreshToken = response.RefreshToken, - }; - - var profile = Plugin.LANCommander.GetProfile(); + var profile = Plugin.LANCommanderClient.GetProfile(); Plugin.Settings.PlayerName = String.IsNullOrWhiteSpace(profile.Alias) ? profile.UserName : profile.Alias; @@ -148,24 +140,16 @@ namespace LANCommander.PlaynitePlugin.Views RegisterButton.IsEnabled = false; RegisterButton.Content = "Working..."; - if (Plugin.LANCommander == null || Plugin.LANCommander.Client == null) - Plugin.LANCommander = new LANCommanderClient(Context.ServerAddress); - else - Plugin.LANCommander.Client.BaseUrl = new Uri(Context.ServerAddress); + if (Plugin.LANCommanderClient == null) + Plugin.LANCommanderClient = new LANCommander.SDK.Client(Context.ServerAddress); - var response = await Plugin.LANCommander.RegisterAsync(Context.UserName, Context.Password); + var response = await Plugin.LANCommanderClient.RegisterAsync(Context.UserName, Context.Password); Plugin.Settings.ServerAddress = Context.ServerAddress; Plugin.Settings.AccessToken = response.AccessToken; Plugin.Settings.RefreshToken = response.RefreshToken; Plugin.Settings.PlayerName = Context.UserName; - Plugin.LANCommander.Token = new AuthToken() - { - AccessToken = response.AccessToken, - RefreshToken = response.RefreshToken, - }; - Context.Password = String.Empty; Plugin.SavePluginSettings(Plugin.Settings); diff --git a/LANCommander.Playnite.Extension/Views/LANCommanderSettingsView.xaml.cs b/LANCommander.Playnite.Extension/Views/LANCommanderSettingsView.xaml.cs index 2388840..44526a0 100644 --- a/LANCommander.Playnite.Extension/Views/LANCommanderSettingsView.xaml.cs +++ b/LANCommander.Playnite.Extension/Views/LANCommanderSettingsView.xaml.cs @@ -47,7 +47,7 @@ namespace LANCommander.PlaynitePlugin RefreshToken = Settings.RefreshToken, }; - var task = Task.Run(() => Plugin.LANCommander.ValidateToken(token)) + var task = Task.Run(() => Plugin.LANCommanderClient.ValidateToken(token)) .ContinueWith(antecedent => { try @@ -90,7 +90,7 @@ namespace LANCommander.PlaynitePlugin { Plugin.Settings.AccessToken = String.Empty; Plugin.Settings.RefreshToken = String.Empty; - Plugin.LANCommander.Token = null; + Plugin.LANCommanderClient.UseToken(null); Plugin.SavePluginSettings(Plugin.Settings); diff --git a/LANCommander.Playnite.Extension/packages.config b/LANCommander.Playnite.Extension/packages.config index 48c1616..5a9ca11 100644 --- a/LANCommander.Playnite.Extension/packages.config +++ b/LANCommander.Playnite.Extension/packages.config @@ -1,10 +1,10 @@  + - @@ -16,6 +16,5 @@ - \ No newline at end of file diff --git a/LANCommander.Playnite.Extension/LANCommanderClient.cs b/LANCommander.SDK/Client.cs similarity index 71% rename from LANCommander.Playnite.Extension/LANCommanderClient.cs rename to LANCommander.SDK/Client.cs index e2cdfd1..d7db648 100644 --- a/LANCommander.Playnite.Extension/LANCommanderClient.cs +++ b/LANCommander.SDK/Client.cs @@ -1,6 +1,6 @@ using LANCommander.SDK; using LANCommander.SDK.Models; -using Playnite.SDK; +using Microsoft.Extensions.Logging; using RestSharp; using System; using System.Collections.Generic; @@ -11,19 +11,27 @@ using System.Net; using System.Net.NetworkInformation; using System.Threading.Tasks; -namespace LANCommander.PlaynitePlugin +namespace LANCommander.SDK { - internal class LANCommanderClient + public class Client { - public static readonly ILogger Logger = LogManager.GetLogger(); + private readonly ILogger Logger; - public readonly RestClient Client; - public AuthToken Token; + private readonly RestClient ApiClient; + private AuthToken Token; - public LANCommanderClient(string baseUrl) + public Client(string baseUrl) { if (!String.IsNullOrWhiteSpace(baseUrl)) - Client = new RestClient(baseUrl); + ApiClient = new RestClient(baseUrl); + } + + public Client(string baseUrl, ILogger logger) + { + if (!String.IsNullOrWhiteSpace(baseUrl)) + ApiClient = new RestClient(baseUrl); + + Logger = logger; } private T PostRequest(string route, object body) @@ -32,7 +40,7 @@ namespace LANCommander.PlaynitePlugin .AddJsonBody(body) .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); - var response = Client.Post(request); + var response = ApiClient.Post(request); return response.Data; } @@ -42,7 +50,7 @@ namespace LANCommander.PlaynitePlugin var request = new RestRequest(route) .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); - var response = Client.Get(request); + var response = ApiClient.Get(request); return response.Data; } @@ -58,7 +66,7 @@ namespace LANCommander.PlaynitePlugin client.DownloadProgressChanged += (s, e) => progressHandler(e); client.DownloadFileCompleted += (s, e) => completeHandler(e); - client.DownloadFileAsync(new Uri($"{Client.BaseUrl}{route}"), tempFile); + client.DownloadFileAsync(new Uri($"{ApiClient.BaseUrl}{route}"), tempFile); return tempFile; } @@ -72,14 +80,14 @@ namespace LANCommander.PlaynitePlugin client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}"); - var ws = client.OpenRead(new Uri($"{Client.BaseUrl}{route}")); + var ws = client.OpenRead(new Uri($"{ApiClient.BaseUrl}{route}")); return new TrackableStream(ws, true, Convert.ToInt64(client.ResponseHeaders["Content-Length"])); } - public async Task AuthenticateAsync(string username, string password) + public async Task AuthenticateAsync(string username, string password) { - var response = await Client.ExecuteAsync(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest() + var response = await ApiClient.ExecuteAsync(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest() { UserName = username, Password = password @@ -88,7 +96,14 @@ namespace LANCommander.PlaynitePlugin switch (response.StatusCode) { case HttpStatusCode.OK: - return response.Data; + Token = new AuthToken + { + AccessToken = response.Data.AccessToken, + RefreshToken = response.Data.RefreshToken, + Expiration = response.Data.Expiration + }; + + return Token; case HttpStatusCode.Forbidden: case HttpStatusCode.BadRequest: @@ -100,9 +115,9 @@ namespace LANCommander.PlaynitePlugin } } - public async Task RegisterAsync(string username, string password) + public async Task RegisterAsync(string username, string password) { - var response = await Client.ExecuteAsync(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest() + var response = await ApiClient.ExecuteAsync(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest() { UserName = username, Password = password @@ -111,7 +126,14 @@ namespace LANCommander.PlaynitePlugin switch (response.StatusCode) { case HttpStatusCode.OK: - return response.Data; + Token = new AuthToken + { + AccessToken = response.Data.AccessToken, + RefreshToken = response.Data.RefreshToken, + Expiration = response.Data.Expiration + }; + + return Token; case HttpStatusCode.BadRequest: case HttpStatusCode.Forbidden: @@ -125,33 +147,45 @@ namespace LANCommander.PlaynitePlugin public async Task PingAsync() { - var response = await Client.ExecuteAsync(new RestRequest("/api/Ping", Method.GET)); + var response = await ApiClient.ExecuteAsync(new RestRequest("/api/Ping", Method.GET)); return response.StatusCode == HttpStatusCode.OK; } - public AuthResponse RefreshToken(AuthToken token) + public AuthToken RefreshToken(AuthToken token) { - Logger.Trace("Refreshing token..."); + Logger?.LogTrace("Refreshing token..."); var request = new RestRequest("/api/Auth/Refresh") .AddJsonBody(token); - var response = Client.Post(request); + var response = ApiClient.Post(request); if (response.StatusCode != HttpStatusCode.OK) throw new WebException(response.ErrorMessage); - return response.Data; + Token = new AuthToken + { + AccessToken = response.Data.AccessToken, + RefreshToken = response.Data.RefreshToken, + Expiration = response.Data.Expiration + }; + + return Token; + } + + public bool ValidateToken() + { + return ValidateToken(Token); } public bool ValidateToken(AuthToken token) { - Logger.Trace("Validating token..."); + Logger?.LogTrace("Validating token..."); if (token == null) { - Logger.Trace("Token is null!"); + Logger?.LogTrace("Token is null!"); return false; } @@ -160,22 +194,27 @@ namespace LANCommander.PlaynitePlugin if (String.IsNullOrEmpty(token.AccessToken) || String.IsNullOrEmpty(token.RefreshToken)) { - Logger.Trace("Token is empty!"); + Logger?.LogTrace("Token is empty!"); return false; } - var response = Client.Post(request); + var response = ApiClient.Post(request); var valid = response.StatusCode == HttpStatusCode.OK; if (valid) - Logger.Trace("Token is valid!"); + Logger?.LogTrace("Token is valid!"); else - Logger.Trace("Token is invalid!"); + Logger?.LogTrace("Token is invalid!"); return response.StatusCode == HttpStatusCode.OK; } + public void UseToken(AuthToken token) + { + Token = token; + } + public IEnumerable GetGames() { return GetRequest>("/api/Games"); @@ -223,26 +262,26 @@ namespace LANCommander.PlaynitePlugin public GameSave UploadSave(string gameId, byte[] data) { - Logger.Trace("Uploading save..."); + Logger?.LogTrace("Uploading save..."); var request = new RestRequest($"/api/Saves/Upload/{gameId}", Method.POST) .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); request.AddFile(gameId, data, gameId); - var response = Client.Post(request); + var response = ApiClient.Post(request); return response.Data; } public string GetMediaUrl(Media media) { - return (new Uri(Client.BaseUrl, $"/api/Media/{media.Id}/Download?fileId={media.FileId}").ToString()); + return (new Uri(ApiClient.BaseUrl, $"/api/Media/{media.Id}/Download?fileId={media.FileId}").ToString()); } public string GetKey(Guid id) { - Logger.Trace("Requesting key allocation..."); + Logger?.LogTrace("Requesting key allocation..."); var macAddress = GetMacAddress(); @@ -261,7 +300,7 @@ namespace LANCommander.PlaynitePlugin public string GetAllocatedKey(Guid id) { - Logger.Trace("Requesting allocated key..."); + Logger?.LogTrace("Requesting allocated key..."); var macAddress = GetMacAddress(); @@ -283,7 +322,7 @@ namespace LANCommander.PlaynitePlugin public string GetNewKey(Guid id) { - Logger.Trace("Requesting new key allocation..."); + Logger?.LogTrace("Requesting new key allocation..."); var macAddress = GetMacAddress(); @@ -305,14 +344,14 @@ namespace LANCommander.PlaynitePlugin public User GetProfile() { - Logger.Trace("Requesting player's profile..."); + Logger?.LogTrace("Requesting player's profile..."); return GetRequest("/api/Profile"); } public string ChangeAlias(string alias) { - Logger.Trace("Requesting to change player alias..."); + Logger?.LogTrace("Requesting to change player alias..."); var response = PostRequest("/api/Profile/ChangeAlias", alias); diff --git a/LANCommander.SDK/EventArgs.cs b/LANCommander.SDK/EventArgs.cs new file mode 100644 index 0000000..c0af51a --- /dev/null +++ b/LANCommander.SDK/EventArgs.cs @@ -0,0 +1,20 @@ +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 ReaderProgress Progress { get; set; } + public IEntry Entry { get; set; } + } +} diff --git a/LANCommander.Playnite.Extension/ExtractionResult.cs b/LANCommander.SDK/ExtractionResult.cs similarity index 88% rename from LANCommander.Playnite.Extension/ExtractionResult.cs rename to LANCommander.SDK/ExtractionResult.cs index ee4fb2c..ac9e1d8 100644 --- a/LANCommander.Playnite.Extension/ExtractionResult.cs +++ b/LANCommander.SDK/ExtractionResult.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace LANCommander.PlaynitePlugin +namespace LANCommander.SDK { internal class ExtractionResult { diff --git a/LANCommander.SDK/GameManager.cs b/LANCommander.SDK/GameManager.cs new file mode 100644 index 0000000..bd99083 --- /dev/null +++ b/LANCommander.SDK/GameManager.cs @@ -0,0 +1,239 @@ +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 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; + + private TrackableStream Stream; + private IReader Reader; + + public GameManager(Client client, string defaultInstallDirectory) + { + Client = client; + DefaultInstallDirectory = defaultInstallDirectory; + } + + public GameManager(Client client, string defaultInstallDirectory, ILogger logger) + { + Client = client; + DefaultInstallDirectory = DefaultInstallDirectory; + Logger = logger; + } + + /// + /// 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, DefaultInstallDirectory); + }); + + if (!result.Success && !result.Canceled) + throw new Exception("Could not extract the installer. Retry the install or check your connection"); + else if (result.Canceled) + return ""; + + 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); + + var extractionResult = new ExtractionResult + { + Canceled = false, + }; + + try + { + Directory.CreateDirectory(destination); + + Stream = Client.StreamGame(game.Id); + Reader = ReaderFactory.Open(Stream); + + Stream.OnProgress += (pos, len) => + { + OnArchiveExtractionProgress?.Invoke(pos, len); + }; + + Reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => + { + OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs + { + Entry = e.Item, + Progress = e.ReaderProgress, + }); + }; + + while (Reader.MoveToNextEntry()) + { + if (Reader.Cancelled) + break; + + Reader.WriteEntryToDirectory(destination, new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true, + PreserveFileTime = true, + }); + } + + Reader.Dispose(); + Stream.Dispose(); + } + catch (Exception ex) + { + if (Reader.Cancelled) + { + Logger?.LogTrace("User cancelled the download"); + + extractionResult.Canceled = true; + + if (Directory.Exists(destination)) + { + Logger?.LogTrace("Cleaning up orphaned files after cancelled install"); + + Directory.Delete(destination, true); + } + } + 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"); + } + } + + if (!extractionResult.Canceled) + { + extractionResult.Success = true; + extractionResult.Directory = destination; + + Logger?.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination); + } + + return extractionResult; + } + + public void CancelInstall() + { + Reader?.Cancel(); + // Reader?.Dispose(); + // Stream?.Dispose(); + } + } +} diff --git a/LANCommander.Playnite.Extension/Services/GameSaveService.cs b/LANCommander.SDK/GameSaveManager.cs similarity index 52% rename from LANCommander.Playnite.Extension/Services/GameSaveService.cs rename to LANCommander.SDK/GameSaveManager.cs index a650e5c..a7ffc1b 100644 --- a/LANCommander.Playnite.Extension/Services/GameSaveService.cs +++ b/LANCommander.SDK/GameSaveManager.cs @@ -1,66 +1,55 @@ using LANCommander.SDK; -using Playnite.SDK; -using Playnite.SDK.Models; +using LANCommander.SDK.Helpers; +using LANCommander.SDK.Models; using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; using SharpCompress.Readers; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; +using System.Net; using System.Text; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace LANCommander.PlaynitePlugin.Services +namespace LANCommander.SDK { - internal class GameSaveService + public class GameSaveManager { - private readonly LANCommanderClient LANCommander; - private readonly IPlayniteAPI PlayniteApi; - private readonly PowerShellRuntime PowerShellRuntime; + private readonly Client Client; - internal GameSaveService(LANCommanderClient lanCommander, IPlayniteAPI playniteApi, PowerShellRuntime powerShellRuntime) + public delegate void OnDownloadProgressHandler(DownloadProgressChangedEventArgs e); + public event OnDownloadProgressHandler OnDownloadProgress; + + public delegate void OnDownloadCompleteHandler(AsyncCompletedEventArgs e); + public event OnDownloadCompleteHandler OnDownloadComplete; + + public GameSaveManager(Client client) { - LANCommander = lanCommander; - PlayniteApi = playniteApi; - PowerShellRuntime = powerShellRuntime; + Client = client; } - internal void DownloadSave(Game game) + public void Download(string installDirectory) { + var manifest = ManifestHelper.Read(installDirectory); + string tempFile = String.Empty; - if (game != null) + if (manifest != null) { - PlayniteApi.Dialogs.ActivateGlobalProgress(progress => + var destination = Client.DownloadLatestSave(manifest.Id, (changed) => { - progress.ProgressMaxValue = 100; - progress.CurrentProgressValue = 0; - - var destination = LANCommander.DownloadLatestSave(Guid.Parse(game.GameId), (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 latest save...") + OnDownloadProgress?.Invoke(changed); + }, (complete) => { - IsIndeterminate = false, - Cancelable = false + OnDownloadComplete?.Invoke(complete); }); + tempFile = destination; + // Go into the archive and extract the files to the correct locations try { @@ -74,10 +63,6 @@ namespace LANCommander.PlaynitePlugin.Services .WithNamingConvention(new PascalCaseNamingConvention()) .Build(); - var manifestContents = File.ReadAllText(Path.Combine(tempLocation, "_manifest.yml")); - - var manifest = deserializer.Deserialize(manifestContents); - #region Move files foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) { @@ -86,7 +71,7 @@ namespace LANCommander.PlaynitePlugin.Services var tempSavePathFile = Path.Combine(tempSavePath, savePath.Path.Replace('/', '\\').Replace("{InstallDir}\\", "")); - var destination = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory)); + destination = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", installDirectory)); if (File.Exists(tempSavePathFile)) { @@ -107,7 +92,7 @@ namespace LANCommander.PlaynitePlugin.Services if (inInstallDir) { // Files are in the game's install directory. Move them there from the save path. - destination = file.Replace(tempSavePath, savePath.Path.Replace('/', '\\').TrimEnd('\\').Replace("{InstallDir}", game.InstallDirectory)); + destination = file.Replace(tempSavePath, savePath.Path.Replace('/', '\\').TrimEnd('\\').Replace("{InstallDir}", installDirectory)); if (File.Exists(destination)) File.Delete(destination); @@ -155,97 +140,89 @@ namespace LANCommander.PlaynitePlugin.Services } } - internal void UploadSave(Game game) + public void Upload(string installDirectory) { - var manifestPath = Path.Combine(game.InstallDirectory, "_manifest.yml"); + var manifest = ManifestHelper.Read(installDirectory); - if (File.Exists(manifestPath)) + var temp = Path.GetTempFileName(); + + if (manifest.SavePaths != null && manifest.SavePaths.Count() > 0) { - var deserializer = new DeserializerBuilder() - .WithNamingConvention(new PascalCaseNamingConvention()) - .Build(); - - var manifest = deserializer.Deserialize(File.ReadAllText(manifestPath)); - var temp = Path.GetTempFileName(); - - if (manifest.SavePaths != null && manifest.SavePaths.Count() > 0) + using (var archive = ZipArchive.Create()) { - using (var archive = ZipArchive.Create()) + archive.DeflateCompressionLevel = SharpCompress.Compressors.Deflate.CompressionLevel.BestCompression; + + #region Add files from defined paths + foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) { - archive.DeflateCompressionLevel = SharpCompress.Compressors.Deflate.CompressionLevel.BestCompression; + var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", installDirectory)); - #region Add files from defined paths - foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) + if (Directory.Exists(localPath)) { - var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory)); - - if (Directory.Exists(localPath)) - { - AddDirectoryToZip(archive, localPath, localPath, savePath.Id); - } - else if (File.Exists(localPath)) - { - archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath); - } + AddDirectoryToZip(archive, localPath, localPath, savePath.Id); } - #endregion - - #region Add files from defined paths - foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) + else if (File.Exists(localPath)) { - var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory)); - - if (Directory.Exists(localPath)) - { - AddDirectoryToZip(archive, localPath, localPath, savePath.Id); - } - else if (File.Exists(localPath)) - { - archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath); - } + archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath); } - #endregion + } + #endregion - #region Export registry keys - if (manifest.SavePaths.Any(sp => sp.Type == "Registry")) + #region Add files from defined paths + foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File")) + { + var localPath = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", installDirectory)); + + if (Directory.Exists(localPath)) { - List tempRegFiles = new List(); - - var exportCommand = new StringBuilder(); - - foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "Registry")) - { - var tempRegFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".reg"); - - exportCommand.AppendLine($"reg.exe export \"{savePath.Path.Replace(":\\", "\\")}\" \"{tempRegFile}\""); - tempRegFiles.Add(tempRegFile); - } - - PowerShellRuntime.RunCommand(exportCommand.ToString()); - - var exportFile = new StringBuilder(); - - foreach (var tempRegFile in tempRegFiles) - { - exportFile.AppendLine(File.ReadAllText(tempRegFile)); - File.Delete(tempRegFile); - } - - archive.AddEntry("_registry.reg", new MemoryStream(Encoding.UTF8.GetBytes(exportFile.ToString())), true); + AddDirectoryToZip(archive, localPath, localPath, savePath.Id); } - #endregion - - archive.AddEntry("_manifest.yml", manifestPath); - - using (var ms = new MemoryStream()) + else if (File.Exists(localPath)) { - archive.SaveTo(ms); - - ms.Seek(0, SeekOrigin.Begin); - - var save = LANCommander.UploadSave(game.GameId, ms.ToArray()); + archive.AddEntry(Path.Combine(savePath.Id.ToString(), savePath.Path.Replace("{InstallDir}/", "")), localPath); } } + #endregion + + #region Export registry keys + if (manifest.SavePaths.Any(sp => sp.Type == "Registry")) + { + List tempRegFiles = new List(); + + var exportCommand = new StringBuilder(); + + foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "Registry")) + { + var tempRegFile = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".reg"); + + exportCommand.AppendLine($"reg.exe export \"{savePath.Path.Replace(":\\", "\\")}\" \"{tempRegFile}\""); + tempRegFiles.Add(tempRegFile); + } + + PowerShellRuntime.RunCommand(exportCommand.ToString()); + + var exportFile = new StringBuilder(); + + foreach (var tempRegFile in tempRegFiles) + { + exportFile.AppendLine(File.ReadAllText(tempRegFile)); + File.Delete(tempRegFile); + } + + archive.AddEntry("_registry.reg", new MemoryStream(Encoding.UTF8.GetBytes(exportFile.ToString())), true); + } + #endregion + + archive.AddEntry("_manifest.yml", ManifestHelper.GetPath(installDirectory)); + + using (var ms = new MemoryStream()) + { + archive.SaveTo(ms); + + ms.Seek(0, SeekOrigin.Begin); + + var save = Client.UploadSave(manifest.Id.ToString(), ms.ToArray()); + } } } } diff --git a/LANCommander.SDK/Helpers/ManifestHelper.cs b/LANCommander.SDK/Helpers/ManifestHelper.cs new file mode 100644 index 0000000..4c09f16 --- /dev/null +++ b/LANCommander.SDK/Helpers/ManifestHelper.cs @@ -0,0 +1,57 @@ +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 = GetPath(installDirectory); + var yaml = File.ReadAllText(source); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(new PascalCaseNamingConvention()) + .Build(); + + Logger?.LogTrace("Deserializing manifest"); + + var manifest = deserializer.Deserialize(yaml); + + return manifest; + } + + public static void Write(GameManifest manifest, string installDirectory) + { + var destination = GetPath(installDirectory); + + Logger?.LogTrace("Attempting to write manifest to path {Destination}", destination); + + var serializer = new SerializerBuilder() + .WithNamingConvention(new PascalCaseNamingConvention()) + .Build(); + + Logger?.LogTrace("Serializing manifest"); + + var yaml = serializer.Serialize(manifest); + + Logger?.LogTrace("Writing manifest file"); + + File.WriteAllText(destination, yaml); + } + + public static string GetPath(string installDirectory) + { + return Path.Combine(installDirectory, ManifestFilename); + } + } +} diff --git a/LANCommander.Playnite.Extension/Helpers/RetryHelper.cs b/LANCommander.SDK/Helpers/RetryHelper.cs similarity index 64% rename from LANCommander.Playnite.Extension/Helpers/RetryHelper.cs rename to LANCommander.SDK/Helpers/RetryHelper.cs index 898a1d8..4a912eb 100644 --- a/LANCommander.Playnite.Extension/Helpers/RetryHelper.cs +++ b/LANCommander.SDK/Helpers/RetryHelper.cs @@ -1,15 +1,12 @@ -using Playnite.SDK; +using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; -namespace LANCommander.PlaynitePlugin.Helpers +namespace LANCommander.SDK.Helpers { internal static class RetryHelper { - internal static readonly ILogger Logger = LogManager.GetLogger(); + internal static readonly ILogger Logger; internal static T RetryOnException(int maxAttempts, TimeSpan delay, T @default, Func action) { @@ -19,14 +16,14 @@ namespace LANCommander.PlaynitePlugin.Helpers { try { - Logger.Trace($"Attempt #{attempts + 1}/{maxAttempts}..."); + Logger?.LogTrace($"Attempt #{attempts + 1}/{maxAttempts}..."); attempts++; return action(); } catch (Exception ex) { - Logger.Error(ex, $"Attempt failed!"); + Logger?.LogError(ex, $"Attempt failed!"); if (attempts >= maxAttempts) return @default; diff --git a/LANCommander.SDK/Helpers/ScriptHelper.cs b/LANCommander.SDK/Helpers/ScriptHelper.cs new file mode 100644 index 0000000..3232e57 --- /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.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj index 9f5c4f4..e653767 100644 --- a/LANCommander.SDK/LANCommander.SDK.csproj +++ b/LANCommander.SDK/LANCommander.SDK.csproj @@ -1,7 +1,14 @@ - + netstandard2.0 + + + + + + + diff --git a/LANCommander.SDK/Models/AuthToken.cs b/LANCommander.SDK/Models/AuthToken.cs index 4f5f97b..f8728e6 100644 --- a/LANCommander.SDK/Models/AuthToken.cs +++ b/LANCommander.SDK/Models/AuthToken.cs @@ -8,5 +8,6 @@ namespace LANCommander.SDK.Models { public string AccessToken { get; set; } public string RefreshToken { get; set; } + public DateTime Expiration { get; set; } } } diff --git a/LANCommander.SDK/Models/Game.cs b/LANCommander.SDK/Models/Game.cs index d13737b..befabf4 100644 --- a/LANCommander.SDK/Models/Game.cs +++ b/LANCommander.SDK/Models/Game.cs @@ -10,6 +10,7 @@ namespace LANCommander.SDK.Models public string DirectoryName { get; set; } public string Description { get; set; } public DateTime ReleasedOn { get; set; } + public string InstallDirectory { get; set; } public virtual IEnumerable Actions { get; set; } public virtual IEnumerable Tags { get; set; } public virtual Company Publisher { get; set; } diff --git a/LANCommander.SDK/Models/GameManifest.cs b/LANCommander.SDK/Models/GameManifest.cs index 21920d6..260bc6b 100644 --- a/LANCommander.SDK/Models/GameManifest.cs +++ b/LANCommander.SDK/Models/GameManifest.cs @@ -6,6 +6,7 @@ namespace LANCommander.SDK { public class GameManifest { + public Guid Id { get; set; } public string Title { get; set; } public string SortTitle { get; set; } public string Description { get; set; } diff --git a/LANCommander.Playnite.Extension/PowerShellRuntime.cs b/LANCommander.SDK/PowerShellRuntime.cs similarity index 69% rename from LANCommander.Playnite.Extension/PowerShellRuntime.cs rename to LANCommander.SDK/PowerShellRuntime.cs index 4b17858..18d8f8c 100644 --- a/LANCommander.Playnite.Extension/PowerShellRuntime.cs +++ b/LANCommander.SDK/PowerShellRuntime.cs @@ -1,22 +1,20 @@ using LANCommander.SDK.Enums; -using Playnite.SDK; -using Playnite.SDK.Models; +using LANCommander.SDK.Models; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Management.Automation; using System.Runtime.InteropServices; -using System.Security.RightsManagement; using System.Text; using System.Threading.Tasks; -namespace LANCommander.PlaynitePlugin +namespace LANCommander.SDK { - internal class PowerShellRuntime + public static class PowerShellRuntime { - public static readonly ILogger Logger = LogManager.GetLogger(); + public static readonly ILogger Logger; [DllImport("kernel32.dll", SetLastError = true)] static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr); @@ -24,13 +22,13 @@ namespace LANCommander.PlaynitePlugin [DllImport("kernel32.dll", SetLastError = true)] static extern bool Wow64RevertWow64FsRedirection(ref IntPtr ptr); - public void RunCommand(string command, bool asAdmin = false) + public static void RunCommand(string command, bool asAdmin = false) { - Logger.Trace($"Executing command `{command}` | Admin: {asAdmin}"); + Logger?.LogTrace($"Executing command `{command}` | Admin: {asAdmin}"); var tempScript = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".ps1"); - Logger.Trace($"Creating temp script at path {tempScript}"); + Logger?.LogTrace($"Creating temp script at path {tempScript}"); File.WriteAllText(tempScript, command); @@ -39,9 +37,9 @@ namespace LANCommander.PlaynitePlugin File.Delete(tempScript); } - public int RunScript(string path, bool asAdmin = false, string arguments = null, string workingDirectory = null) + public static int RunScript(string path, bool asAdmin = false, string arguments = null, string workingDirectory = null) { - Logger.Trace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}"); + Logger?.LogTrace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}"); var wow64Value = IntPtr.Zero; @@ -75,9 +73,14 @@ namespace LANCommander.PlaynitePlugin return process.ExitCode; } - public void RunScript(Game game, ScriptType type, string arguments = null) + public static void RunScript(Game game, ScriptType type, string arguments = null) { - var path = GetScriptFilePath(game, type); + RunScript(game.InstallDirectory, type, arguments); + } + + public static void RunScript(string installDirectory, ScriptType type, string arguments = null) + { + var path = GetScriptFilePath(installDirectory, type); if (File.Exists(path)) { @@ -90,12 +93,12 @@ namespace LANCommander.PlaynitePlugin } } - public void RunScriptsAsAdmin(IEnumerable paths, string arguments = null) + public static void RunScriptsAsAdmin(IEnumerable paths, string arguments = null) { // Concatenate scripts var sb = new StringBuilder(); - Logger.Trace("Concatenating scripts..."); + Logger?.LogTrace("Concatenating scripts..."); foreach (var path in paths) { @@ -103,16 +106,16 @@ namespace LANCommander.PlaynitePlugin sb.AppendLine(contents); - Logger.Trace($"Added {path}!"); + Logger?.LogTrace($"Added {path}!"); } - Logger.Trace("Done concatenating!"); + Logger?.LogTrace("Done concatenating!"); if (sb.Length > 0) { var scriptPath = Path.GetTempFileName(); - Logger.Trace($"Creating temp script at path {scriptPath}"); + Logger?.LogTrace($"Creating temp script at path {scriptPath}"); File.WriteAllText(scriptPath, sb.ToString()); @@ -120,14 +123,14 @@ namespace LANCommander.PlaynitePlugin } } - public void RunScripts(IEnumerable games, ScriptType type, string arguments = null) + public static void RunScripts(IEnumerable installDirectories, ScriptType type, string arguments = null) { List scripts = new List(); List adminScripts = new List(); - foreach (var game in games) + foreach (var installDirectory in installDirectories) { - var path = GetScriptFilePath(game, type); + var path = GetScriptFilePath(installDirectory, type); if (!File.Exists(path)) continue; @@ -149,6 +152,11 @@ namespace LANCommander.PlaynitePlugin } public static string GetScriptFilePath(Game game, ScriptType type) + { + return GetScriptFilePath(game.InstallDirectory, type); + } + + public static string GetScriptFilePath(string installDirectory, ScriptType type) { Dictionary filenames = new Dictionary() { { ScriptType.Install, "_install.ps1" }, @@ -159,7 +167,7 @@ namespace LANCommander.PlaynitePlugin var filename = filenames[type]; - return Path.Combine(game.InstallDirectory, filename); + return Path.Combine(installDirectory, filename); } } } diff --git a/LANCommander.SDK/RedistributableManager.cs b/LANCommander.SDK/RedistributableManager.cs new file mode 100644 index 0000000..fcc87d8 --- /dev/null +++ b/LANCommander.SDK/RedistributableManager.cs @@ -0,0 +1,168 @@ +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 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 RedistributableManager(Client client, ILogger logger) + { + Client = client; + Logger = logger; + } + + 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.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; + } + } +} diff --git a/LANCommander.Playnite.Extension/TrackableStream.cs b/LANCommander.SDK/TrackableStream.cs similarity index 99% rename from LANCommander.Playnite.Extension/TrackableStream.cs rename to LANCommander.SDK/TrackableStream.cs index cff27f8..38f8835 100644 --- a/LANCommander.Playnite.Extension/TrackableStream.cs +++ b/LANCommander.SDK/TrackableStream.cs @@ -1,9 +1,9 @@ using System; using System.IO; -namespace LANCommander.PlaynitePlugin +namespace LANCommander.SDK { - internal class TrackableStream : MemoryStream, IDisposable + public class TrackableStream : MemoryStream, IDisposable { public delegate void OnProgressDelegate(long Position, long Length); public event OnProgressDelegate OnProgress = delegate { }; diff --git a/LANCommander/Services/GameService.cs b/LANCommander/Services/GameService.cs index 179ec5a..a45c333 100644 --- a/LANCommander/Services/GameService.cs +++ b/LANCommander/Services/GameService.cs @@ -44,6 +44,7 @@ namespace LANCommander.Services var manifest = new GameManifest() { + Id = game.Id, Title = game.Title, SortTitle = game.SortTitle, Description = game.Description,