From a679fae0cb75e5812a9b3bc780972fb9040352ac Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Thu, 9 Nov 2023 19:40:38 -0600 Subject: [PATCH] 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 { };