From ff6f9997f529ef608745f10619735e642b3c4016 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Thu, 9 Nov 2023 19:38:32 -0600 Subject: [PATCH 01/17] Added game ID to manifest --- LANCommander.SDK/Models/GameManifest.cs | 1 + LANCommander/Services/GameService.cs | 1 + 2 files changed, 2 insertions(+) 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/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, From a679fae0cb75e5812a9b3bc780972fb9040352ac Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Thu, 9 Nov 2023 19:40:38 -0600 Subject: [PATCH 02/17] Relocate crucial installation logic to SDK --- .../LANCommander.PlaynitePlugin.csproj | 5 - .../LANCommanderLibraryPlugin.cs | 30 +- .../Client.cs | 83 ++-- .../ExtractionResult.cs | 2 +- .../Helpers/RetryHelper.cs | 13 +- LANCommander.SDK/LANCommander.SDK.csproj | 7 + LANCommander.SDK/LANCommander.cs | 405 ++++++++++++++++++ LANCommander.SDK/Models/AuthToken.cs | 1 + LANCommander.SDK/Models/Game.cs | 1 + .../PowerShellRuntime.cs | 24 +- .../TrackableStream.cs | 4 +- 11 files changed, 497 insertions(+), 78 deletions(-) rename LANCommander.Playnite.Extension/LANCommanderClient.cs => LANCommander.SDK/Client.cs (77%) rename {LANCommander.Playnite.Extension => LANCommander.SDK}/ExtractionResult.cs (88%) rename {LANCommander.Playnite.Extension => LANCommander.SDK}/Helpers/RetryHelper.cs (64%) create mode 100644 LANCommander.SDK/LANCommander.cs rename {LANCommander.Playnite.Extension => LANCommander.SDK}/PowerShellRuntime.cs (86%) rename {LANCommander.Playnite.Extension => LANCommander.SDK}/TrackableStream.cs (99%) diff --git a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj index 20a9688..fe6be55 100644 --- a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj +++ b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj @@ -99,15 +99,10 @@ - - - - - diff --git a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs index 3f80ec0..659eafa 100644 --- a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs +++ b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs @@ -21,7 +21,7 @@ namespace LANCommander.PlaynitePlugin { public static readonly ILogger Logger = LogManager.GetLogger(); internal LANCommanderSettingsViewModel Settings { get; set; } - internal LANCommanderClient LANCommander { get; set; } + internal LANCommander.SDK.LANCommander LANCommander { get; set; } internal PowerShellRuntime PowerShellRuntime { get; set; } internal GameSaveService GameSaveService { get; set; } @@ -39,16 +39,14 @@ namespace LANCommander.PlaynitePlugin Settings = new LANCommanderSettingsViewModel(this); - LANCommander = new LANCommanderClient(Settings.ServerAddress); - LANCommander.Token = new SDK.Models.AuthToken() + LANCommander = new SDK.LANCommander(Settings.ServerAddress); + LANCommander.Client.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 +89,7 @@ namespace LANCommander.PlaynitePlugin public bool ValidateConnection() { - return LANCommander.ValidateToken(LANCommander.Token); + return LANCommander.Client.ValidateToken(); } public override IEnumerable GetGames(LibraryGetGamesArgs args) @@ -111,7 +109,7 @@ namespace LANCommander.PlaynitePlugin } } - var games = LANCommander + var games = LANCommander.Client .GetGames() .Where(g => g != null && g.Archives != null && g.Archives.Count() > 0); @@ -121,7 +119,7 @@ namespace LANCommander.PlaynitePlugin { Logger.Trace($"Importing/updating metadata for game \"{game.Title}\"..."); - var manifest = LANCommander.GetGameManifest(game.Id); + var manifest = LANCommander.Client.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); @@ -183,13 +181,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(LANCommander.Client.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(LANCommander.Client.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(LANCommander.Client.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Background))); gameMetadata.Add(metadata); } @@ -244,7 +242,7 @@ namespace LANCommander.PlaynitePlugin if (result.Result == true) { PowerShellRuntime.RunScript(nameChangeArgs.Games.First(), SDK.Enums.ScriptType.NameChange, $@"""{result.SelectedString}"" ""{oldName}"""); - LANCommander.ChangeAlias(result.SelectedString); + LANCommander.Client.ChangeAlias(result.SelectedString); } } }; @@ -264,7 +262,7 @@ namespace LANCommander.PlaynitePlugin if (Guid.TryParse(keyChangeArgs.Games.First().GameId, out gameId)) { // NUKIEEEE - var newKey = LANCommander.GetNewKey(gameId); + var newKey = LANCommander.Client.GetNewKey(gameId); if (String.IsNullOrEmpty(newKey)) PlayniteApi.Dialogs.ShowErrorMessage("There are no more keys available on the server.", "No Keys Available"); @@ -402,7 +400,7 @@ namespace LANCommander.PlaynitePlugin var games = PlayniteApi.Database.Games.Where(g => g.IsInstalled).ToList(); - LANCommander.ChangeAlias(result.SelectedString); + LANCommander.Client.ChangeAlias(result.SelectedString); Logger.Trace($"Running name change scripts across {games.Count} installed game(s)"); diff --git a/LANCommander.Playnite.Extension/LANCommanderClient.cs b/LANCommander.SDK/Client.cs similarity index 77% rename from LANCommander.Playnite.Extension/LANCommanderClient.cs rename to LANCommander.SDK/Client.cs index e2cdfd1..958e8ca 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,19 @@ 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 static 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); } private T PostRequest(string route, object body) @@ -32,7 +32,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 +42,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 +58,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 +72,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 +88,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: @@ -102,7 +109,7 @@ namespace LANCommander.PlaynitePlugin 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 @@ -125,19 +132,19 @@ 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) { - 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); @@ -145,13 +152,18 @@ namespace LANCommander.PlaynitePlugin return response.Data; } + 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 +172,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 +240,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 +278,7 @@ namespace LANCommander.PlaynitePlugin public string GetAllocatedKey(Guid id) { - Logger.Trace("Requesting allocated key..."); + Logger.LogTrace("Requesting allocated key..."); var macAddress = GetMacAddress(); @@ -283,7 +300,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 +322,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.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.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..94389ac 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/LANCommander.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj index 9f5c4f4..11a21d2 100644 --- a/LANCommander.SDK/LANCommander.SDK.csproj +++ b/LANCommander.SDK/LANCommander.SDK.csproj @@ -4,4 +4,11 @@ netstandard2.0 + + + + + + + diff --git a/LANCommander.SDK/LANCommander.cs b/LANCommander.SDK/LANCommander.cs new file mode 100644 index 0000000..743c727 --- /dev/null +++ b/LANCommander.SDK/LANCommander.cs @@ -0,0 +1,405 @@ +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/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.Playnite.Extension/PowerShellRuntime.cs b/LANCommander.SDK/PowerShellRuntime.cs similarity index 86% rename from LANCommander.Playnite.Extension/PowerShellRuntime.cs rename to LANCommander.SDK/PowerShellRuntime.cs index 4b17858..fc8cf54 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 readonly ILogger Logger = LogManager.GetLogger(); + public static readonly ILogger Logger; [DllImport("kernel32.dll", SetLastError = true)] static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr); @@ -26,11 +24,11 @@ namespace LANCommander.PlaynitePlugin public 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); @@ -41,7 +39,7 @@ namespace LANCommander.PlaynitePlugin public 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; @@ -95,7 +93,7 @@ namespace LANCommander.PlaynitePlugin // Concatenate scripts var sb = new StringBuilder(); - Logger.Trace("Concatenating scripts..."); + Logger.LogTrace("Concatenating scripts..."); foreach (var path in paths) { @@ -103,16 +101,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()); 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 { }; From e53709334cf04ace912ceaf8b21b20b6e1b702fe Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Thu, 9 Nov 2023 23:45:37 -0600 Subject: [PATCH 03/17] Make PowerShellRuntime static --- LANCommander.SDK/PowerShellRuntime.cs | 30 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/LANCommander.SDK/PowerShellRuntime.cs b/LANCommander.SDK/PowerShellRuntime.cs index fc8cf54..c9f4514 100644 --- a/LANCommander.SDK/PowerShellRuntime.cs +++ b/LANCommander.SDK/PowerShellRuntime.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; namespace LANCommander.SDK { - internal class PowerShellRuntime + public static class PowerShellRuntime { public static readonly ILogger Logger; @@ -22,7 +22,7 @@ namespace LANCommander.SDK [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.LogTrace($"Executing command `{command}` | Admin: {asAdmin}"); @@ -37,7 +37,7 @@ namespace LANCommander.SDK 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.LogTrace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}"); @@ -73,9 +73,14 @@ namespace LANCommander.SDK 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)) { @@ -88,7 +93,7 @@ namespace LANCommander.SDK } } - public void RunScriptsAsAdmin(IEnumerable paths, string arguments = null) + public static void RunScriptsAsAdmin(IEnumerable paths, string arguments = null) { // Concatenate scripts var sb = new StringBuilder(); @@ -118,14 +123,14 @@ namespace LANCommander.SDK } } - 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; @@ -147,6 +152,11 @@ namespace LANCommander.SDK } 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" }, @@ -157,7 +167,7 @@ namespace LANCommander.SDK var filename = filenames[type]; - return Path.Combine(game.InstallDirectory, filename); + return Path.Combine(installDirectory, filename); } } } From 39f2d4b212f41b99317b684a9272b77522f9aa7b Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 00:29:16 -0600 Subject: [PATCH 04/17] 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; + } + } +} From 73b542856a62e7515468fe3f6fc02198a1a9f063 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 01:32:30 -0600 Subject: [PATCH 05/17] Refactor GameSaveService into GameSaveManager and SaveController. Update Playnite addon authentication dialogs to use new client. --- .../LANCommander.PlaynitePlugin.csproj | 8 +- .../LANCommanderLibraryPlugin.cs | 7 +- .../SaveController.cs | 61 +++++ .../Views/Authentication.xaml.cs | 30 +-- .../Views/LANCommanderSettingsView.xaml.cs | 4 +- LANCommander.SDK/Client.cs | 22 +- .../GameSaveManager.cs | 209 ++++++++---------- LANCommander.SDK/Helpers/ManifestHelper.cs | 9 +- LANCommander.SDK/LANCommander.SDK.csproj | 4 +- 9 files changed, 201 insertions(+), 153 deletions(-) create mode 100644 LANCommander.Playnite.Extension/SaveController.cs rename LANCommander.Playnite.Extension/Services/GameSaveService.cs => LANCommander.SDK/GameSaveManager.cs (52%) diff --git a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj index bbd6a13..e8bed07 100644 --- a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj +++ b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj @@ -42,6 +42,9 @@ + + ..\packages\SharpCompress.0.34.1\lib\net462\SharpCompress.dll + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll @@ -85,10 +88,13 @@ + + ..\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 fee02ea..a7da90f 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; @@ -22,7 +21,7 @@ namespace LANCommander.PlaynitePlugin public static readonly ILogger Logger = LogManager.GetLogger(); internal LANCommanderSettingsViewModel Settings { get; set; } internal LANCommander.SDK.Client LANCommanderClient { get; set; } - internal GameSaveService GameSaveService { get; set; } + internal LANCommanderSaveController SaveController { get; set; } public override Guid Id { get; } = Guid.Parse("48e1bac7-e0a0-45d7-ba83-36f5e9e959fc"); public override string Name => "LANCommander"; @@ -333,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() 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/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.SDK/Client.cs b/LANCommander.SDK/Client.cs index 958e8ca..805b0b4 100644 --- a/LANCommander.SDK/Client.cs +++ b/LANCommander.SDK/Client.cs @@ -107,7 +107,7 @@ namespace LANCommander.SDK } } - public async Task RegisterAsync(string username, string password) + public async Task RegisterAsync(string username, string password) { var response = await ApiClient.ExecuteAsync(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest() { @@ -118,7 +118,14 @@ namespace LANCommander.SDK 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: @@ -137,7 +144,7 @@ namespace LANCommander.SDK return response.StatusCode == HttpStatusCode.OK; } - public AuthResponse RefreshToken(AuthToken token) + public AuthToken RefreshToken(AuthToken token) { Logger.LogTrace("Refreshing token..."); @@ -149,7 +156,14 @@ namespace LANCommander.SDK 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() 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 4513443..a7ffc1b 100644 --- a/LANCommander.Playnite.Extension/Services/GameSaveService.cs +++ b/LANCommander.SDK/GameSaveManager.cs @@ -1,64 +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 LANCommander.SDK.Client LANCommander; - private readonly IPlayniteAPI PlayniteApi; + private readonly Client Client; - internal GameSaveService(LANCommander.SDK.Client lanCommander, IPlayniteAPI playniteApi) + 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; + 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 { @@ -72,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")) { @@ -84,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)) { @@ -105,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); @@ -153,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 index 7e82586..f4e8ed7 100644 --- a/LANCommander.SDK/Helpers/ManifestHelper.cs +++ b/LANCommander.SDK/Helpers/ManifestHelper.cs @@ -16,7 +16,7 @@ namespace LANCommander.SDK.Helpers public static GameManifest Read(string installDirectory) { - var source = Path.Combine(installDirectory, ManifestFilename); + var source = GetPath(installDirectory); var yaml = File.ReadAllText(source); var deserializer = new DeserializerBuilder() @@ -32,7 +32,7 @@ namespace LANCommander.SDK.Helpers public static void Write(GameManifest manifest, string installDirectory) { - var destination = Path.Combine(installDirectory, ManifestFilename); + var destination = GetPath(installDirectory); Logger.LogTrace("Attempting to write manifest to path {Destination}", destination); @@ -48,5 +48,10 @@ namespace LANCommander.SDK.Helpers File.WriteAllText(destination, yaml); } + + public static string GetPath(string installDirectory) + { + return Path.Combine(installDirectory, ManifestFilename); + } } } diff --git a/LANCommander.SDK/LANCommander.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj index 11a21d2..7130409 100644 --- a/LANCommander.SDK/LANCommander.SDK.csproj +++ b/LANCommander.SDK/LANCommander.SDK.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -8,7 +8,7 @@ - + From 5237e886127e1ef3f6d4b04ba25f0bc4e7eb3329 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 20:53:28 -0600 Subject: [PATCH 06/17] Null handling for logger --- LANCommander.SDK/Client.cs | 24 +++++++++---------- LANCommander.SDK/GameManager.cs | 28 +++++++++++----------- LANCommander.SDK/Helpers/ManifestHelper.cs | 8 +++---- LANCommander.SDK/Helpers/RetryHelper.cs | 4 ++-- LANCommander.SDK/Helpers/ScriptHelper.cs | 4 ++-- LANCommander.SDK/PowerShellRuntime.cs | 14 +++++------ LANCommander.SDK/RedistributableManager.cs | 12 +++++----- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/LANCommander.SDK/Client.cs b/LANCommander.SDK/Client.cs index 805b0b4..740d828 100644 --- a/LANCommander.SDK/Client.cs +++ b/LANCommander.SDK/Client.cs @@ -146,7 +146,7 @@ namespace LANCommander.SDK public AuthToken RefreshToken(AuthToken token) { - Logger.LogTrace("Refreshing token..."); + Logger?.LogTrace("Refreshing token..."); var request = new RestRequest("/api/Auth/Refresh") .AddJsonBody(token); @@ -173,11 +173,11 @@ namespace LANCommander.SDK public bool ValidateToken(AuthToken token) { - Logger.LogTrace("Validating token..."); + Logger?.LogTrace("Validating token..."); if (token == null) { - Logger.LogTrace("Token is null!"); + Logger?.LogTrace("Token is null!"); return false; } @@ -186,7 +186,7 @@ namespace LANCommander.SDK if (String.IsNullOrEmpty(token.AccessToken) || String.IsNullOrEmpty(token.RefreshToken)) { - Logger.LogTrace("Token is empty!"); + Logger?.LogTrace("Token is empty!"); return false; } @@ -195,9 +195,9 @@ namespace LANCommander.SDK var valid = response.StatusCode == HttpStatusCode.OK; if (valid) - Logger.LogTrace("Token is valid!"); + Logger?.LogTrace("Token is valid!"); else - Logger.LogTrace("Token is invalid!"); + Logger?.LogTrace("Token is invalid!"); return response.StatusCode == HttpStatusCode.OK; } @@ -254,7 +254,7 @@ namespace LANCommander.SDK public GameSave UploadSave(string gameId, byte[] data) { - Logger.LogTrace("Uploading save..."); + Logger?.LogTrace("Uploading save..."); var request = new RestRequest($"/api/Saves/Upload/{gameId}", Method.POST) .AddHeader("Authorization", $"Bearer {Token.AccessToken}"); @@ -273,7 +273,7 @@ namespace LANCommander.SDK public string GetKey(Guid id) { - Logger.LogTrace("Requesting key allocation..."); + Logger?.LogTrace("Requesting key allocation..."); var macAddress = GetMacAddress(); @@ -292,7 +292,7 @@ namespace LANCommander.SDK public string GetAllocatedKey(Guid id) { - Logger.LogTrace("Requesting allocated key..."); + Logger?.LogTrace("Requesting allocated key..."); var macAddress = GetMacAddress(); @@ -314,7 +314,7 @@ namespace LANCommander.SDK public string GetNewKey(Guid id) { - Logger.LogTrace("Requesting new key allocation..."); + Logger?.LogTrace("Requesting new key allocation..."); var macAddress = GetMacAddress(); @@ -336,14 +336,14 @@ namespace LANCommander.SDK public User GetProfile() { - Logger.LogTrace("Requesting player's profile..."); + Logger?.LogTrace("Requesting player's profile..."); return GetRequest("/api/Profile"); } public string ChangeAlias(string alias) { - Logger.LogTrace("Requesting to change player alias..."); + Logger?.LogTrace("Requesting to change player alias..."); var response = PostRequest("/api/Profile/ChangeAlias", alias); diff --git a/LANCommander.SDK/GameManager.cs b/LANCommander.SDK/GameManager.cs index c60f1b1..20ccb8f 100644 --- a/LANCommander.SDK/GameManager.cs +++ b/LANCommander.SDK/GameManager.cs @@ -41,11 +41,11 @@ namespace LANCommander.SDK { var game = Client.GetGame(gameId); - Logger.LogTrace("Installing game {GameTitle} (GameId)", game.Title, game.Id); + 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"); + Logger?.LogTrace("Attempting to download and extract game"); return DownloadAndExtract(game); }); @@ -61,7 +61,7 @@ namespace LANCommander.SDK var writeManifestSuccess = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromSeconds(1), false, () => { - Logger.LogTrace("Attempting to get game manifest"); + Logger?.LogTrace("Attempting to get game manifest"); manifest = Client.GetGameManifest(game.Id); @@ -73,7 +73,7 @@ namespace LANCommander.SDK if (!writeManifestSuccess) throw new Exception("Could not grab the manifest file. Retry the install or check your connection"); - Logger.LogTrace("Saving scripts"); + Logger?.LogTrace("Saving scripts"); ScriptHelper.SaveScript(game, ScriptType.Install); ScriptHelper.SaveScript(game, ScriptType.Uninstall); @@ -91,7 +91,7 @@ namespace LANCommander.SDK } catch (Exception ex) { - Logger.LogError(ex, "Could not execute post-install scripts"); + Logger?.LogError(ex, "Could not execute post-install scripts"); } return result.Directory; @@ -103,27 +103,27 @@ namespace LANCommander.SDK try { - Logger.LogTrace("Running uninstall script"); + Logger?.LogTrace("Running uninstall script"); PowerShellRuntime.RunScript(installDirectory, ScriptType.Uninstall); } catch (Exception ex) { - Logger.LogError(ex, "Error running uninstall script"); + Logger?.LogError(ex, "Error running uninstall script"); } - Logger.LogTrace("Attempting to delete the install directory"); + Logger?.LogTrace("Attempting to delete the install directory"); if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true); - Logger.LogTrace("Deleted install directory {InstallDirectory}", installDirectory); + 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"); + Logger?.LogTrace("Game failed to download, no game was specified"); throw new ArgumentNullException("No game was specified"); } @@ -133,7 +133,7 @@ namespace LANCommander.SDK var destination = Path.Combine(installDirectory, game.Title.SanitizeFilename()); - Logger.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination); + Logger?.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination); try { @@ -173,11 +173,11 @@ namespace LANCommander.SDK } else { - Logger.LogError(ex, "Could not extract to path {Destination}", destination); + Logger?.LogError(ex, "Could not extract to path {Destination}", destination); if (Directory.Exists(destination)) { - Logger.LogTrace("Cleaning up orphaned install files after bad install"); + Logger?.LogTrace("Cleaning up orphaned install files after bad install"); Directory.Delete(destination, true); } @@ -196,7 +196,7 @@ namespace LANCommander.SDK extractionResult.Success = true; extractionResult.Directory = destination; - Logger.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, 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 index f4e8ed7..accda4c 100644 --- a/LANCommander.SDK/Helpers/ManifestHelper.cs +++ b/LANCommander.SDK/Helpers/ManifestHelper.cs @@ -23,7 +23,7 @@ namespace LANCommander.SDK.Helpers .WithNamingConvention(PascalCaseNamingConvention.Instance) .Build(); - Logger.LogTrace("Deserializing manifest"); + Logger?.LogTrace("Deserializing manifest"); var manifest = deserializer.Deserialize(source); @@ -34,17 +34,17 @@ namespace LANCommander.SDK.Helpers { var destination = GetPath(installDirectory); - Logger.LogTrace("Attempting to write manifest to path {Destination}", destination); + Logger?.LogTrace("Attempting to write manifest to path {Destination}", destination); var serializer = new SerializerBuilder() .WithNamingConvention(PascalCaseNamingConvention.Instance) .Build(); - Logger.LogTrace("Serializing manifest"); + Logger?.LogTrace("Serializing manifest"); var yaml = serializer.Serialize(manifest); - Logger.LogTrace("Writing manifest file"); + Logger?.LogTrace("Writing manifest file"); File.WriteAllText(destination, yaml); } diff --git a/LANCommander.SDK/Helpers/RetryHelper.cs b/LANCommander.SDK/Helpers/RetryHelper.cs index 94389ac..4a912eb 100644 --- a/LANCommander.SDK/Helpers/RetryHelper.cs +++ b/LANCommander.SDK/Helpers/RetryHelper.cs @@ -16,14 +16,14 @@ namespace LANCommander.SDK.Helpers { try { - Logger.LogTrace($"Attempt #{attempts + 1}/{maxAttempts}..."); + Logger?.LogTrace($"Attempt #{attempts + 1}/{maxAttempts}..."); attempts++; return action(); } catch (Exception ex) { - Logger.LogError(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 index 2a6e926..3232e57 100644 --- a/LANCommander.SDK/Helpers/ScriptHelper.cs +++ b/LANCommander.SDK/Helpers/ScriptHelper.cs @@ -20,7 +20,7 @@ namespace LANCommander.SDK.Helpers // 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); + Logger?.LogTrace("Writing script {Script} to {Destination}", script.Name, tempPath); File.WriteAllText(tempPath, script.Contents); @@ -42,7 +42,7 @@ namespace LANCommander.SDK.Helpers if (File.Exists(filename)) File.Delete(filename); - Logger.LogTrace("Writing {ScriptType} script to {Destination}", type, filename); + Logger?.LogTrace("Writing {ScriptType} script to {Destination}", type, filename); File.WriteAllText(filename, script.Contents); } diff --git a/LANCommander.SDK/PowerShellRuntime.cs b/LANCommander.SDK/PowerShellRuntime.cs index c9f4514..18d8f8c 100644 --- a/LANCommander.SDK/PowerShellRuntime.cs +++ b/LANCommander.SDK/PowerShellRuntime.cs @@ -24,11 +24,11 @@ namespace LANCommander.SDK public static void RunCommand(string command, bool asAdmin = false) { - Logger.LogTrace($"Executing command `{command}` | Admin: {asAdmin}"); + Logger?.LogTrace($"Executing command `{command}` | Admin: {asAdmin}"); var tempScript = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".ps1"); - Logger.LogTrace($"Creating temp script at path {tempScript}"); + Logger?.LogTrace($"Creating temp script at path {tempScript}"); File.WriteAllText(tempScript, command); @@ -39,7 +39,7 @@ namespace LANCommander.SDK public static int RunScript(string path, bool asAdmin = false, string arguments = null, string workingDirectory = null) { - Logger.LogTrace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}"); + Logger?.LogTrace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}"); var wow64Value = IntPtr.Zero; @@ -98,7 +98,7 @@ namespace LANCommander.SDK // Concatenate scripts var sb = new StringBuilder(); - Logger.LogTrace("Concatenating scripts..."); + Logger?.LogTrace("Concatenating scripts..."); foreach (var path in paths) { @@ -106,16 +106,16 @@ namespace LANCommander.SDK sb.AppendLine(contents); - Logger.LogTrace($"Added {path}!"); + Logger?.LogTrace($"Added {path}!"); } - Logger.LogTrace("Done concatenating!"); + Logger?.LogTrace("Done concatenating!"); if (sb.Length > 0) { var scriptPath = Path.GetTempFileName(); - Logger.LogTrace($"Creating temp script at path {scriptPath}"); + Logger?.LogTrace($"Creating temp script at path {scriptPath}"); File.WriteAllText(scriptPath, sb.ToString()); diff --git a/LANCommander.SDK/RedistributableManager.cs b/LANCommander.SDK/RedistributableManager.cs index bb4ce9f..7c4a371 100644 --- a/LANCommander.SDK/RedistributableManager.cs +++ b/LANCommander.SDK/RedistributableManager.cs @@ -75,7 +75,7 @@ namespace LANCommander.SDK } catch (Exception ex) { - Logger.LogError(ex, "Redistributable {Redistributable} failed to install", redistributable.Name); + Logger?.LogError(ex, "Redistributable {Redistributable} failed to install", redistributable.Name); } finally { @@ -94,13 +94,13 @@ namespace LANCommander.SDK { if (redistributable == null) { - Logger.LogTrace("Redistributable failed to download! No redistributable was specified"); + 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); + Logger?.LogTrace("Downloading and extracting {Redistributable} to path {Destination}", redistributable.Name, destination); try { @@ -134,11 +134,11 @@ namespace LANCommander.SDK } catch (Exception ex) { - Logger.LogError(ex, "Could not extract to path {Destination}", destination); + Logger?.LogError(ex, "Could not extract to path {Destination}", destination); if (Directory.Exists(destination)) { - Logger.LogTrace("Cleaning up orphaned files after bad install"); + Logger?.LogTrace("Cleaning up orphaned files after bad install"); Directory.Delete(destination, true); } @@ -155,7 +155,7 @@ namespace LANCommander.SDK { extractionResult.Success = true; extractionResult.Directory = destination; - Logger.LogTrace("Redistributable {Redistributable} successfully downloaded and extracted to {Destination}", redistributable.Name, destination); + Logger?.LogTrace("Redistributable {Redistributable} successfully downloaded and extracted to {Destination}", redistributable.Name, destination); } return extractionResult; From 20de9d6cae004ebf35849d6206843a3a44be3758 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 20:53:48 -0600 Subject: [PATCH 07/17] Allow injection of loggers --- LANCommander.SDK/Client.cs | 10 +++++++++- LANCommander.SDK/GameManager.cs | 8 +++++++- LANCommander.SDK/RedistributableManager.cs | 8 +++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/LANCommander.SDK/Client.cs b/LANCommander.SDK/Client.cs index 740d828..d7db648 100644 --- a/LANCommander.SDK/Client.cs +++ b/LANCommander.SDK/Client.cs @@ -15,7 +15,7 @@ namespace LANCommander.SDK { public class Client { - private static readonly ILogger Logger; + private readonly ILogger Logger; private readonly RestClient ApiClient; private AuthToken Token; @@ -26,6 +26,14 @@ namespace LANCommander.SDK 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) { var request = new RestRequest(route) diff --git a/LANCommander.SDK/GameManager.cs b/LANCommander.SDK/GameManager.cs index 20ccb8f..7c858ab 100644 --- a/LANCommander.SDK/GameManager.cs +++ b/LANCommander.SDK/GameManager.cs @@ -15,7 +15,7 @@ namespace LANCommander.SDK { public class GameManager { - private static readonly ILogger Logger; + private readonly ILogger Logger; private Client Client { get; set; } private string DefaultInstallDirectory { get; set; } @@ -30,6 +30,12 @@ namespace LANCommander.SDK Client = client; } + public GameManager(Client client, ILogger logger) + { + Client = client; + Logger = logger; + } + /// /// Downloads, extracts, and runs post-install scripts for the specified game /// diff --git a/LANCommander.SDK/RedistributableManager.cs b/LANCommander.SDK/RedistributableManager.cs index 7c4a371..d714599 100644 --- a/LANCommander.SDK/RedistributableManager.cs +++ b/LANCommander.SDK/RedistributableManager.cs @@ -15,7 +15,7 @@ namespace LANCommander.SDK { public class RedistributableManager { - private static readonly ILogger Logger; + private readonly ILogger Logger; private Client Client { get; set; } public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e); @@ -29,6 +29,12 @@ namespace LANCommander.SDK Client = client; } + public RedistributableManager(Client client, ILogger logger) + { + Client = client; + Logger = logger; + } + public void Install(Game game) { foreach (var redistributable in game.Redistributables) From b77e7f6e5331099fdd21d903ab7cb7aca6df62cb Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 21:36:35 -0600 Subject: [PATCH 08/17] Pass in default install directory to managers --- LANCommander.Playnite.Extension/InstallController.cs | 2 +- LANCommander.Playnite.Extension/UninstallController.cs | 2 +- LANCommander.SDK/GameManager.cs | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 3ec33f7..8eca948 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -37,7 +37,7 @@ namespace LANCommander.PlaynitePlugin var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => { - var gameManager = new GameManager(Plugin.LANCommanderClient); + var gameManager = new GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory); gameManager.OnArchiveExtractionProgress += (long pos, long len) => { diff --git a/LANCommander.Playnite.Extension/UninstallController.cs b/LANCommander.Playnite.Extension/UninstallController.cs index 93518d0..eb23735 100644 --- a/LANCommander.Playnite.Extension/UninstallController.cs +++ b/LANCommander.Playnite.Extension/UninstallController.cs @@ -23,7 +23,7 @@ namespace LANCommander.PlaynitePlugin { try { - var gameManager = new LANCommander.SDK.GameManager(Plugin.LANCommanderClient); + var gameManager = new LANCommander.SDK.GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory); gameManager.Uninstall(Game.InstallDirectory); } diff --git a/LANCommander.SDK/GameManager.cs b/LANCommander.SDK/GameManager.cs index 7c858ab..f0d1e03 100644 --- a/LANCommander.SDK/GameManager.cs +++ b/LANCommander.SDK/GameManager.cs @@ -25,14 +25,16 @@ namespace LANCommander.SDK public delegate void OnArchiveExtractionProgressHandler(long position, long length); public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress; - public GameManager(Client client) + public GameManager(Client client, string defaultInstallDirectory) { Client = client; + DefaultInstallDirectory = defaultInstallDirectory; } - public GameManager(Client client, ILogger logger) + public GameManager(Client client, string defaultInstallDirectory, ILogger logger) { Client = client; + DefaultInstallDirectory = DefaultInstallDirectory; Logger = logger; } @@ -53,7 +55,7 @@ namespace LANCommander.SDK { Logger?.LogTrace("Attempting to download and extract game"); - return DownloadAndExtract(game); + return DownloadAndExtract(game, DefaultInstallDirectory); }); if (!result.Success && !result.Canceled) From 52a5f5866f005bfeb442b4918fab2bd9f8530e95 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 21:37:02 -0600 Subject: [PATCH 09/17] Downgrade YamlDotNet --- LANCommander.Playnite.Extension/packages.config | 2 -- LANCommander.SDK/Helpers/ManifestHelper.cs | 4 ++-- LANCommander.SDK/LANCommander.SDK.csproj | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/LANCommander.Playnite.Extension/packages.config b/LANCommander.Playnite.Extension/packages.config index 48c1616..7ba8b19 100644 --- a/LANCommander.Playnite.Extension/packages.config +++ b/LANCommander.Playnite.Extension/packages.config @@ -4,7 +4,6 @@ - @@ -16,6 +15,5 @@ - \ No newline at end of file diff --git a/LANCommander.SDK/Helpers/ManifestHelper.cs b/LANCommander.SDK/Helpers/ManifestHelper.cs index accda4c..97b773c 100644 --- a/LANCommander.SDK/Helpers/ManifestHelper.cs +++ b/LANCommander.SDK/Helpers/ManifestHelper.cs @@ -20,7 +20,7 @@ namespace LANCommander.SDK.Helpers var yaml = File.ReadAllText(source); var deserializer = new DeserializerBuilder() - .WithNamingConvention(PascalCaseNamingConvention.Instance) + .WithNamingConvention(new PascalCaseNamingConvention()) .Build(); Logger?.LogTrace("Deserializing manifest"); @@ -37,7 +37,7 @@ namespace LANCommander.SDK.Helpers Logger?.LogTrace("Attempting to write manifest to path {Destination}", destination); var serializer = new SerializerBuilder() - .WithNamingConvention(PascalCaseNamingConvention.Instance) + .WithNamingConvention(new PascalCaseNamingConvention()) .Build(); Logger?.LogTrace("Serializing manifest"); diff --git a/LANCommander.SDK/LANCommander.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj index 7130409..e653767 100644 --- a/LANCommander.SDK/LANCommander.SDK.csproj +++ b/LANCommander.SDK/LANCommander.SDK.csproj @@ -8,7 +8,7 @@ - + From ea337dfea16e6ddc7cc59baa598caa8363a1925c Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 21:37:27 -0600 Subject: [PATCH 10/17] Feed actual YAML contents to deserializer --- LANCommander.SDK/Helpers/ManifestHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LANCommander.SDK/Helpers/ManifestHelper.cs b/LANCommander.SDK/Helpers/ManifestHelper.cs index 97b773c..4c09f16 100644 --- a/LANCommander.SDK/Helpers/ManifestHelper.cs +++ b/LANCommander.SDK/Helpers/ManifestHelper.cs @@ -25,7 +25,7 @@ namespace LANCommander.SDK.Helpers Logger?.LogTrace("Deserializing manifest"); - var manifest = deserializer.Deserialize(source); + var manifest = deserializer.Deserialize(yaml); return manifest; } From 47bb054fd1783b0afc86093a5be031447cfe689b Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 21:44:48 -0600 Subject: [PATCH 11/17] Restore progress bar --- LANCommander.Playnite.Extension/InstallController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 8eca948..8b7e021 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -61,7 +61,7 @@ namespace LANCommander.PlaynitePlugin }, new GlobalProgressOptions($"Downloading {Game.Name}...") { - IsIndeterminate = true, + IsIndeterminate = false, Cancelable = true, }); From bb980cc063ccda0ebb04bd1ba15179b5d704720c Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 10 Nov 2023 21:45:09 -0600 Subject: [PATCH 12/17] Avoid exception if manifest is malformed --- LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs index a7da90f..e118e19 100644 --- a/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs +++ b/LANCommander.Playnite.Extension/LANCommanderLibraryPlugin.cs @@ -441,7 +441,7 @@ namespace LANCommander.PlaynitePlugin public void UpdateGame(SDK.GameManifest manifest) { - var game = PlayniteApi.Database.Games.First(g => g.GameId == manifest.Id.ToString()); + var game = PlayniteApi.Database.Games.FirstOrDefault(g => g.GameId == manifest?.Id.ToString()); if (game == null) return; From 1ede37c0313c6192572e4b2033a84617e7963439 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 12 Nov 2023 01:04:05 -0600 Subject: [PATCH 13/17] Simplify game install cancellation. Cancels now happen silently and don't generate a dialog. --- .../InstallController.cs | 15 +++- LANCommander.SDK/EventArgs.cs | 2 - LANCommander.SDK/GameManager.cs | 74 +++++++++++++------ LANCommander.SDK/RedistributableManager.cs | 2 - 4 files changed, 62 insertions(+), 31 deletions(-) diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 8b7e021..6440365 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -49,9 +49,7 @@ namespace LANCommander.PlaynitePlugin { if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested) { - e.Reader.Cancel(); - e.Reader.Dispose(); - e.Stream.Dispose(); + gameManager.CancelInstall(); progress.IsIndeterminate = true; } @@ -78,6 +76,17 @@ namespace LANCommander.PlaynitePlugin InvokeOnInstalled(new GameInstalledEventArgs(installInfo)); } + else if (result.Canceled) + { + var game = Plugin.PlayniteApi.Database.Games.Get(Game.Id); + + game.IsInstalling = false; + game.IsInstalled = false; + + Plugin.PlayniteApi.Database.Games.Update(game); + } + else if (result.Error != null) + throw result.Error; } } } diff --git a/LANCommander.SDK/EventArgs.cs b/LANCommander.SDK/EventArgs.cs index ca85135..c0af51a 100644 --- a/LANCommander.SDK/EventArgs.cs +++ b/LANCommander.SDK/EventArgs.cs @@ -14,8 +14,6 @@ namespace LANCommander.SDK 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 index f0d1e03..bd99083 100644 --- a/LANCommander.SDK/GameManager.cs +++ b/LANCommander.SDK/GameManager.cs @@ -25,6 +25,9 @@ namespace LANCommander.SDK 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; @@ -61,7 +64,7 @@ namespace LANCommander.SDK 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"); + return ""; GameManifest manifest = null; @@ -143,41 +146,62 @@ namespace LANCommander.SDK Logger?.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination); + var extractionResult = new ExtractionResult + { + Canceled = false, + }; + try { Directory.CreateDirectory(destination); - using (var gameStream = Client.StreamGame(game.Id)) - using (var reader = ReaderFactory.Open(gameStream)) + Stream = Client.StreamGame(game.Id); + Reader = ReaderFactory.Open(Stream); + + Stream.OnProgress += (pos, len) => { - gameStream.OnProgress += (pos, len) => - { - OnArchiveExtractionProgress?.Invoke(pos, len); - }; + OnArchiveExtractionProgress?.Invoke(pos, len); + }; - reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => + Reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs e) => + { + OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs { - OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs - { - Entry = e.Item, - Progress = e.ReaderProgress, - Reader = reader, - Stream = gameStream - }); - }; + Entry = e.Item, + Progress = e.ReaderProgress, + }); + }; - reader.WriteAllToDirectory(destination, new ExtractionOptions() + while (Reader.MoveToNextEntry()) + { + if (Reader.Cancelled) + break; + + Reader.WriteEntryToDirectory(destination, new ExtractionOptions() { ExtractFullPath = true, - Overwrite = true + Overwrite = true, + PreserveFileTime = true, }); } + + Reader.Dispose(); + Stream.Dispose(); } catch (Exception ex) { - if (false) + 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 { @@ -194,11 +218,6 @@ namespace LANCommander.SDK } } - var extractionResult = new ExtractionResult - { - Canceled = false, - }; - if (!extractionResult.Canceled) { extractionResult.Success = true; @@ -209,5 +228,12 @@ namespace LANCommander.SDK return extractionResult; } + + public void CancelInstall() + { + Reader?.Cancel(); + // Reader?.Dispose(); + // Stream?.Dispose(); + } } } diff --git a/LANCommander.SDK/RedistributableManager.cs b/LANCommander.SDK/RedistributableManager.cs index d714599..fcc87d8 100644 --- a/LANCommander.SDK/RedistributableManager.cs +++ b/LANCommander.SDK/RedistributableManager.cs @@ -126,8 +126,6 @@ namespace LANCommander.SDK { Entry = e.Item, Progress = e.ReaderProgress, - Reader = reader, - Stream = redistributableStream }); }; From 81e4848407118bc1e0cff8704eb41a728b60b81c Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 12 Nov 2023 01:26:51 -0600 Subject: [PATCH 14/17] Include download percentage in dialog --- LANCommander.Playnite.Extension/InstallController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 6440365..6ea34e3 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -41,8 +41,11 @@ namespace LANCommander.PlaynitePlugin gameManager.OnArchiveExtractionProgress += (long pos, long len) => { + var percent = Math.Ceiling((pos / (decimal)len) * 100); + progress.ProgressMaxValue = len; progress.CurrentProgressValue = pos; + progress.Text = $"Downloading {Game.Name} ({percent}%)"; }; gameManager.OnArchiveEntryExtractionProgress += (object sender, ArchiveEntryExtractionProgressArgs e) => @@ -57,7 +60,7 @@ namespace LANCommander.PlaynitePlugin installDirectory = gameManager.Install(gameId); }, - new GlobalProgressOptions($"Downloading {Game.Name}...") + new GlobalProgressOptions($"Preparing to download {Game.Name}") { IsIndeterminate = false, Cancelable = true, From 6f7c17493cc9abb84c6a1e17e346d563a5e13a19 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 12 Nov 2023 01:27:15 -0600 Subject: [PATCH 15/17] Install redistributables after game is installed --- .../InstallController.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 6ea34e3..954b9e3 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -5,6 +5,7 @@ using Playnite.SDK; using Playnite.SDK.Models; using Playnite.SDK.Plugins; using System; +using System.Linq; namespace LANCommander.PlaynitePlugin { @@ -66,6 +67,24 @@ namespace LANCommander.PlaynitePlugin Cancelable = true, }); + // Install any redistributables + var game = Plugin.LANCommanderClient.GetGame(gameId); + + if (game.Redistributables != null && game.Redistributables.Count() > 0) + { + Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress => + { + var redistributableManager = new RedistributableManager(Plugin.LANCommanderClient); + + redistributableManager.Install(game); + }, + new GlobalProgressOptions("Installing redistributables...") + { + IsIndeterminate = true, + Cancelable = false, + }); + } + if (!result.Canceled && result.Error == null && !String.IsNullOrWhiteSpace(installDirectory)) { var manifest = ManifestHelper.Read(installDirectory); @@ -81,12 +100,12 @@ namespace LANCommander.PlaynitePlugin } else if (result.Canceled) { - var game = Plugin.PlayniteApi.Database.Games.Get(Game.Id); + var dbGame = Plugin.PlayniteApi.Database.Games.Get(Game.Id); - game.IsInstalling = false; - game.IsInstalled = false; + dbGame.IsInstalling = false; + dbGame.IsInstalled = false; - Plugin.PlayniteApi.Database.Games.Update(game); + Plugin.PlayniteApi.Database.Games.Update(dbGame); } else if (result.Error != null) throw result.Error; From 7c97a3db5768c7ee29416674188b536a308ebd3c Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 12 Nov 2023 01:50:34 -0600 Subject: [PATCH 16/17] Include download speed in progress dialog --- .../InstallController.cs | 28 ++++++++++++++++--- .../LANCommander.PlaynitePlugin.csproj | 3 ++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 954b9e3..7c7bb86 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -5,6 +5,7 @@ using Playnite.SDK; using Playnite.SDK.Models; using Playnite.SDK.Plugins; using System; +using System.Diagnostics; using System.Linq; namespace LANCommander.PlaynitePlugin @@ -40,13 +41,30 @@ namespace LANCommander.PlaynitePlugin { 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) => { - var percent = Math.Ceiling((pos / (decimal)len) * 100); + if (stopwatch.ElapsedMilliseconds > 500) + { + var percent = Math.Ceiling((pos / (decimal)len) * 100); - progress.ProgressMaxValue = len; - progress.CurrentProgressValue = pos; - progress.Text = $"Downloading {Game.Name} ({percent}%)"; + 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) => @@ -60,6 +78,8 @@ namespace LANCommander.PlaynitePlugin }; installDirectory = gameManager.Install(gameId); + + stopwatch.Stop(); }, new GlobalProgressOptions($"Preparing to download {Game.Name}") { diff --git a/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj b/LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj index e8bed07..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 From 227411a558fe1c512cd31e886048e93ffe75df0a Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 12 Nov 2023 01:52:55 -0600 Subject: [PATCH 17/17] Include ByteSize --- LANCommander.Playnite.Extension/packages.config | 1 + 1 file changed, 1 insertion(+) diff --git a/LANCommander.Playnite.Extension/packages.config b/LANCommander.Playnite.Extension/packages.config index 7ba8b19..5a9ca11 100644 --- a/LANCommander.Playnite.Extension/packages.config +++ b/LANCommander.Playnite.Extension/packages.config @@ -1,5 +1,6 @@  +