diff --git a/LANCommander.Playnite.Extension/InstallController.cs b/LANCommander.Playnite.Extension/InstallController.cs index 165ff26..3921f5d 100644 --- a/LANCommander.Playnite.Extension/InstallController.cs +++ b/LANCommander.Playnite.Extension/InstallController.cs @@ -50,7 +50,7 @@ namespace LANCommander.PlaynitePlugin var result = RetryHelper.RetryOnException(10, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () => { Logger.Trace("Attempting to download and extract game..."); - return DownloadAndExtract(game); + return DownloadAndExtractGame(game); }); if (!result.Success && !result.Canceled) @@ -88,6 +88,12 @@ namespace LANCommander.PlaynitePlugin 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); @@ -106,7 +112,7 @@ namespace LANCommander.PlaynitePlugin InvokeOnInstalled(new GameInstalledEventArgs(installInfo)); } - private ExtractionResult DownloadAndExtract(LANCommander.SDK.Models.Game game) + private ExtractionResult DownloadAndExtractGame(LANCommander.SDK.Models.Game game) { if (game == null) { @@ -204,6 +210,152 @@ namespace LANCommander.PlaynitePlugin 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); + + if (!detectionResult == 0) + { + var extractionResult = DownloadAndExtractRedistributable(redistributable); + + if (extractionResult.Success) + { + extractTempPath = extractionResult.Directory; + + 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; @@ -294,6 +446,17 @@ namespace LANCommander.PlaynitePlugin File.WriteAllText(destination, yaml); } + private string SaveTempScript(LANCommander.SDK.Models.Script script) + { + var tempPath = Path.GetTempFileName(); + + 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); diff --git a/LANCommander.Playnite.Extension/LANCommanderClient.cs b/LANCommander.Playnite.Extension/LANCommanderClient.cs index 3a4fa47..b5ecce6 100644 --- a/LANCommander.Playnite.Extension/LANCommanderClient.cs +++ b/LANCommander.Playnite.Extension/LANCommanderClient.cs @@ -206,6 +206,11 @@ namespace LANCommander.PlaynitePlugin return DownloadRequest($"/api/Archives/Download/{id}", progressHandler, completeHandler); } + public TrackableStream StreamRedistributable(Guid id) + { + return StreamRequest($"/api/Redistributables/{id}/Download"); + } + public string DownloadSave(Guid id, Action progressHandler, Action completeHandler) { return DownloadRequest($"/api/Saves/Download/{id}", progressHandler, completeHandler); diff --git a/LANCommander.Playnite.Extension/PowerShellRuntime.cs b/LANCommander.Playnite.Extension/PowerShellRuntime.cs index 082a264..4b17858 100644 --- a/LANCommander.Playnite.Extension/PowerShellRuntime.cs +++ b/LANCommander.Playnite.Extension/PowerShellRuntime.cs @@ -39,7 +39,7 @@ namespace LANCommander.PlaynitePlugin File.Delete(tempScript); } - public void RunScript(string path, bool asAdmin = false, string arguments = null) + 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}"); @@ -58,6 +58,9 @@ namespace LANCommander.PlaynitePlugin if (arguments != null) process.StartInfo.Arguments += " " + arguments; + if (workingDirectory != null) + process.StartInfo.WorkingDirectory = workingDirectory; + if (asAdmin) { process.StartInfo.Verb = "runas"; @@ -68,6 +71,8 @@ namespace LANCommander.PlaynitePlugin process.WaitForExit(); Wow64RevertWow64FsRedirection(ref wow64Value); + + return process.ExitCode; } public void RunScript(Game game, ScriptType type, string arguments = null) diff --git a/LANCommander.SDK/Enums/ScriptType.cs b/LANCommander.SDK/Enums/ScriptType.cs index e1f9b60..9c3f429 100644 --- a/LANCommander.SDK/Enums/ScriptType.cs +++ b/LANCommander.SDK/Enums/ScriptType.cs @@ -4,7 +4,15 @@ { Install, Uninstall, + [Display(Name = "Name Change")] NameChange, - KeyChange + [Display(Name = "Key Change")] + KeyChange, + [Display(Name = "Save Upload")] + SaveUpload, + [Display(Name = "Save Download")] + SaveDownload, + [Display(Name = "Detect Install")] + DetectInstall } } diff --git a/LANCommander.SDK/Models/Game.cs b/LANCommander.SDK/Models/Game.cs index d0dfe55..39e102e 100644 --- a/LANCommander.SDK/Models/Game.cs +++ b/LANCommander.SDK/Models/Game.cs @@ -16,5 +16,6 @@ namespace LANCommander.SDK.Models public virtual Company Developer { get; set; } public virtual IEnumerable Archives { get; set; } public virtual IEnumerable