Merge pull request #32 from LANCommander/migrate-installation-to-sdk

Migrate LANCommander Client Code to SDK
pull/33/head
Pat Hartl 2023-11-12 01:53:53 -06:00 committed by GitHub
commit 1a0cff3914
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 939 additions and 701 deletions

View File

@ -1,17 +1,12 @@
using LANCommander.PlaynitePlugin.Helpers;
using LANCommander.SDK.Enums;
using LANCommander.SDK.Extensions;
using LANCommander.SDK;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using Playnite.SDK;
using Playnite.SDK.Models;
using Playnite.SDK.Plugins;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.IO;
using System.Diagnostics;
using System.Linq;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LANCommander.PlaynitePlugin
{
@ -20,15 +15,11 @@ namespace LANCommander.PlaynitePlugin
public static readonly ILogger Logger = LogManager.GetLogger();
private LANCommanderLibraryPlugin Plugin;
private PowerShellRuntime PowerShellRuntime;
private Playnite.SDK.Models.Game PlayniteGame;
public LANCommanderInstallController(LANCommanderLibraryPlugin plugin, Playnite.SDK.Models.Game game) : base(game)
{
Name = "Install using LANCommander";
Plugin = plugin;
PlayniteGame = game;
PowerShellRuntime = new PowerShellRuntime();
}
public override void Install(InstallActionArgs args)
@ -43,450 +34,101 @@ namespace LANCommander.PlaynitePlugin
}
var gameId = Guid.Parse(Game.GameId);
var game = Plugin.LANCommander.GetGame(gameId);
Logger.Trace($"Installing game {game.Title} ({game.Id})...");
string installDirectory = null;
var result = RetryHelper.RetryOnException<ExtractionResult>(10, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () =>
var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
Logger.Trace("Attempting to download and extract game...");
return DownloadAndExtractGame(game);
var gameManager = new GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory);
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var lastTotalSize = 0d;
var speed = 0d;
gameManager.OnArchiveExtractionProgress += (long pos, long len) =>
{
if (stopwatch.ElapsedMilliseconds > 500)
{
var percent = Math.Ceiling((pos / (decimal)len) * 100);
progress.ProgressMaxValue = len;
progress.CurrentProgressValue = pos;
speed = (double)(progress.CurrentProgressValue - lastTotalSize) / (stopwatch.ElapsedMilliseconds / 1000d);
progress.Text = $"Downloading {Game.Name} ({percent}%) | {ByteSizeLib.ByteSize.FromBytes(speed).ToString("#.#")}/s";
lastTotalSize = pos;
stopwatch.Restart();
}
};
gameManager.OnArchiveEntryExtractionProgress += (object sender, ArchiveEntryExtractionProgressArgs e) =>
{
if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
{
gameManager.CancelInstall();
progress.IsIndeterminate = true;
}
};
installDirectory = gameManager.Install(gameId);
stopwatch.Stop();
},
new GlobalProgressOptions($"Preparing to download {Game.Name}")
{
IsIndeterminate = false,
Cancelable = true,
});
if (!result.Success && !result.Canceled)
throw new Exception("Could not extract the install archive. Retry the install or check your connection.");
else if (result.Canceled)
throw new Exception("Install was canceled");
var installInfo = new GameInstallationData()
{
InstallDirectory = result.Directory
};
PlayniteGame.InstallDirectory = result.Directory;
SDK.GameManifest manifest = null;
var writeManifestSuccess = RetryHelper.RetryOnException(10, TimeSpan.FromSeconds(1), false, () =>
{
Logger.Trace("Attempting to get game manifest...");
manifest = Plugin.LANCommander.GetGameManifest(gameId);
WriteManifest(manifest, result.Directory);
return true;
});
if (!writeManifestSuccess)
throw new Exception("Could not get or write the manifest file. Retry the install or check your connection.");
Logger.Trace("Saving scripts...");
SaveScript(game, result.Directory, ScriptType.Install);
SaveScript(game, result.Directory, ScriptType.Uninstall);
SaveScript(game, result.Directory, ScriptType.NameChange);
SaveScript(game, result.Directory, ScriptType.KeyChange);
// Install any redistributables
var game = Plugin.LANCommanderClient.GetGame(gameId);
if (game.Redistributables != null && game.Redistributables.Count() > 0)
{
Logger.Trace("Installing required redistributables...");
InstallRedistributables(game);
}
try
{
PowerShellRuntime.RunScript(PlayniteGame, ScriptType.Install);
PowerShellRuntime.RunScript(PlayniteGame, ScriptType.NameChange, Plugin.Settings.PlayerName);
var key = Plugin.LANCommander.GetAllocatedKey(game.Id);
PowerShellRuntime.RunScript(PlayniteGame, ScriptType.KeyChange, $"\"{key}\"");
}
catch { }
Plugin.UpdateGame(manifest, gameId);
Plugin.DownloadCache.Remove(gameId);
InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
}
private ExtractionResult DownloadAndExtractGame(LANCommander.SDK.Models.Game game)
{
if (game == null)
{
Logger.Trace("Game failed to download! No game was specified!");
throw new Exception("Game failed to download!");
}
var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename());
Logger.Trace($"Downloading and extracting \"{game.Title}\" to path {destination}");
var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
try
{
Directory.CreateDirectory(destination);
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
using (var gameStream = Plugin.LANCommander.StreamGame(game.Id))
using (var reader = ReaderFactory.Open(gameStream))
{
progress.ProgressMaxValue = gameStream.Length;
gameStream.OnProgress += (pos, len) =>
{
progress.CurrentProgressValue = pos;
};
reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
{
if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
{
reader.Cancel();
progress.IsIndeterminate = true;
reader.Dispose();
gameStream.Dispose();
}
};
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
}
catch (Exception ex)
{
if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
{
Logger.Trace("User cancelled the download");
if (Directory.Exists(destination))
{
Logger.Trace("Cleaning up orphaned install files after cancelled install...");
Directory.Delete(destination, true);
}
}
else
{
Logger.Error(ex, $"Could not extract to path {destination}");
if (Directory.Exists(destination))
{
Logger.Trace("Cleaning up orphaned install files after bad install...");
Directory.Delete(destination, true);
}
throw new Exception("The game archive could not be extracted. Please try again or fix the archive!");
}
}
},
new GlobalProgressOptions($"Downloading {game.Title}...")
{
IsIndeterminate = false,
Cancelable = true,
});
var extractionResult = new ExtractionResult
{
Canceled = result.Canceled
};
if (!result.Canceled)
{
extractionResult.Success = true;
extractionResult.Directory = destination;
Logger.Trace($"Game successfully downloaded and extracted to {destination}");
}
return extractionResult;
}
private void InstallRedistributables(LANCommander.SDK.Models.Game game)
{
foreach (var redistributable in game.Redistributables)
{
string installScriptTempFile = null;
string detectionScriptTempFile = null;
string extractTempPath = null;
try
{
var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install);
installScriptTempFile = SaveTempScript(installScript);
var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall);
detectionScriptTempFile = SaveTempScript(detectionScript);
var detectionResult = PowerShellRuntime.RunScript(detectionScriptTempFile, detectionScript.RequiresAdmin);
// Redistributable is not installed
if (detectionResult == 0)
{
if (redistributable.Archives.Count() > 0)
{
var extractionResult = DownloadAndExtractRedistributable(redistributable);
if (extractionResult.Success)
{
extractTempPath = extractionResult.Directory;
PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath);
}
}
else
{
PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath);
}
}
}
catch (Exception ex)
{
Logger.Error(ex, $"Redistributable {redistributable.Name} failed to install");
}
finally
{
if (File.Exists(installScriptTempFile))
File.Delete(installScriptTempFile);
if (File.Exists(detectionScriptTempFile))
File.Delete(detectionScriptTempFile);
if (Directory.Exists(extractTempPath))
Directory.Delete(extractTempPath);
}
}
}
private ExtractionResult DownloadAndExtractRedistributable(LANCommander.SDK.Models.Redistributable redistributable)
{
if (redistributable == null)
{
Logger.Trace("Redistributable failed to download! No redistributable was specified!");
throw new Exception("Redistributable failed to download!");
}
var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename());
Logger.Trace($"Downloading and extracting \"{redistributable.Name}\" to path {destination}");
var result = Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
try
{
Directory.CreateDirectory(destination);
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
using (var redistributableStream = Plugin.LANCommander.StreamRedistributable(redistributable.Id))
using (var reader = ReaderFactory.Open(redistributableStream))
{
progress.ProgressMaxValue = redistributableStream.Length;
redistributableStream.OnProgress += (pos, len) =>
{
progress.CurrentProgressValue = pos;
};
reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
{
if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
{
reader.Cancel();
progress.IsIndeterminate = true;
reader.Dispose();
redistributableStream.Dispose();
}
};
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
}
catch (Exception ex)
{
if (progress.CancelToken != null && progress.CancelToken.IsCancellationRequested)
{
Logger.Trace("User cancelled the download");
if (Directory.Exists(destination))
{
Logger.Trace("Cleaning up orphaned install files after cancelled install...");
Directory.Delete(destination, true);
}
}
else
{
Logger.Error(ex, $"Could not extract to path {destination}");
if (Directory.Exists(destination))
{
Logger.Trace("Cleaning up orphaned install files after bad install...");
Directory.Delete(destination, true);
}
throw new Exception("The redistributable archive could not be extracted. Please try again or fix the archive!");
}
}
},
new GlobalProgressOptions($"Downloading {redistributable.Name}...")
{
IsIndeterminate = false,
Cancelable = true,
});
var extractionResult = new ExtractionResult
{
Canceled = result.Canceled
};
if (!result.Canceled)
{
extractionResult.Success = true;
extractionResult.Directory = destination;
Logger.Trace($"Redistributable successfully downloaded and extracted to {destination}");
}
return extractionResult;
}
private string Download(LANCommander.SDK.Models.Game game)
{
string tempFile = String.Empty;
if (game != null)
{
Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
{
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
var redistributableManager = new RedistributableManager(Plugin.LANCommanderClient);
var destination = Plugin.LANCommander.DownloadGame(game.Id, (changed) =>
{
progress.CurrentProgressValue = changed.ProgressPercentage;
}, (complete) =>
{
progress.CurrentProgressValue = 100;
});
// Lock the thread until download is done
while (progress.CurrentProgressValue != 100)
{
}
tempFile = destination;
redistributableManager.Install(game);
},
new GlobalProgressOptions($"Downloading {game.Title}...")
new GlobalProgressOptions("Installing redistributables...")
{
IsIndeterminate = false,
IsIndeterminate = true,
Cancelable = false,
});
return tempFile;
}
else
throw new Exception("Game failed to download!");
}
private string Extract(LANCommander.SDK.Models.Game game, string archivePath)
{
var destination = Path.Combine(Plugin.Settings.InstallDirectory, game.Title.SanitizeFilename());
Plugin.PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
if (!result.Canceled && result.Error == null && !String.IsNullOrWhiteSpace(installDirectory))
{
Directory.CreateDirectory(destination);
var manifest = ManifestHelper.Read(installDirectory);
using (var fs = File.OpenRead(archivePath))
using (var ts = new TrackableStream(fs))
using (var reader = ReaderFactory.Open(ts))
Plugin.UpdateGame(manifest);
var installInfo = new GameInstallationData
{
progress.ProgressMaxValue = ts.Length;
ts.OnProgress += (pos, len) =>
{
progress.CurrentProgressValue = pos;
};
InstallDirectory = installDirectory,
};
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
},
new GlobalProgressOptions($"Extracting {game.Title}...")
InvokeOnInstalled(new GameInstalledEventArgs(installInfo));
}
else if (result.Canceled)
{
IsIndeterminate = false,
Cancelable = false,
});
var dbGame = Plugin.PlayniteApi.Database.Games.Get(Game.Id);
return destination;
}
dbGame.IsInstalling = false;
dbGame.IsInstalled = false;
private void WriteManifest(SDK.GameManifest manifest, string installDirectory)
{
var destination = Path.Combine(installDirectory, "_manifest.yml");
Logger.Trace($"Attempting to write manifest to path {destination}");
var serializer = new SerializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
Logger.Trace("Serializing manifest...");
var yaml = serializer.Serialize(manifest);
Logger.Trace("Writing manifest file...");
File.WriteAllText(destination, yaml);
}
private string SaveTempScript(LANCommander.SDK.Models.Script script)
{
var tempPath = Path.GetTempFileName();
File.Move(tempPath, tempPath + ".ps1");
tempPath = tempPath + ".ps1";
Logger.Trace($"Writing script {script.Name} to {tempPath}");
File.WriteAllText(tempPath, script.Contents);
return tempPath;
}
private void SaveScript(LANCommander.SDK.Models.Game game, string installationDirectory, ScriptType type)
{
var script = game.Scripts.FirstOrDefault(s => s.Type == type);
if (script == null)
return;
if (script.RequiresAdmin)
script.Contents = "# Requires Admin" + "\r\n\r\n" + script.Contents;
var filename = PowerShellRuntime.GetScriptFilePath(PlayniteGame, type);
if (File.Exists(filename))
File.Delete(filename);
Logger.Trace($"Writing {type} script to {filename}");
File.WriteAllText(filename, script.Contents);
Plugin.PlayniteApi.Database.Games.Update(dbGame);
}
else if (result.Error != null)
throw result.Error;
}
}
}

View File

@ -34,6 +34,9 @@
<Reference Include="BeaconLib, Version=1.0.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\rix0rrr.BeaconLib.1.0.2\lib\net40\BeaconLib.dll</HintPath>
</Reference>
<Reference Include="ByteSize, Version=2.1.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\ByteSize.2.1.1\lib\net45\ByteSize.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Bcl.AsyncInterfaces, Version=7.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\Microsoft.Bcl.AsyncInterfaces.7.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll</HintPath>
</Reference>
@ -42,9 +45,6 @@
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="RestSharp, Version=106.15.0.0, Culture=neutral, PublicKeyToken=598062e77f915f75, processorArchitecture=MSIL">
<HintPath>..\packages\RestSharp.106.15.0\lib\net452\RestSharp.dll</HintPath>
</Reference>
<Reference Include="SharpCompress, Version=0.34.1.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\SharpCompress.0.34.1\lib\net462\SharpCompress.dll</HintPath>
</Reference>
@ -91,23 +91,15 @@
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
<Reference Include="YamlDotNet, Version=5.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\YamlDotNet.5.4.0\lib\net45\YamlDotNet.dll</HintPath>
</Reference>
<Reference Include="ZstdSharp, Version=0.7.2.0, Culture=neutral, PublicKeyToken=8d151af33a4ad5cf, processorArchitecture=MSIL">
<HintPath>..\packages\ZstdSharp.Port.0.7.2\lib\net461\ZstdSharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="ExtractionResult.cs" />
<Compile Include="TrackableStream.cs" />
<Compile Include="Extensions\MultiplayerInfoExtensions.cs" />
<Compile Include="Helpers\RetryHelper.cs" />
<Compile Include="PowerShellRuntime.cs" />
<Compile Include="Services\GameSaveService.cs" />
<Compile Include="SaveController.cs" />
<Compile Include="UninstallController.cs" />
<Compile Include="InstallController.cs" />
<Compile Include="LANCommanderClient.cs" />
<Compile Include="LANCommanderLibraryPlugin.cs" />
<Compile Include="ViewModels\LANCommanderSettingsViewModel.cs" />
<Compile Include="Views\LANCommanderSettingsView.xaml.cs">

View File

@ -1,5 +1,4 @@
using LANCommander.PlaynitePlugin.Extensions;
using LANCommander.PlaynitePlugin.Services;
using Playnite.SDK;
using Playnite.SDK.Events;
using Playnite.SDK.Models;
@ -21,9 +20,8 @@ namespace LANCommander.PlaynitePlugin
{
public static readonly ILogger Logger = LogManager.GetLogger();
internal LANCommanderSettingsViewModel Settings { get; set; }
internal LANCommanderClient LANCommander { get; set; }
internal PowerShellRuntime PowerShellRuntime { get; set; }
internal GameSaveService GameSaveService { get; set; }
internal LANCommander.SDK.Client LANCommanderClient { get; set; }
internal LANCommanderSaveController SaveController { get; set; }
public override Guid Id { get; } = Guid.Parse("48e1bac7-e0a0-45d7-ba83-36f5e9e959fc");
public override string Name => "LANCommander";
@ -39,16 +37,14 @@ namespace LANCommander.PlaynitePlugin
Settings = new LANCommanderSettingsViewModel(this);
LANCommander = new LANCommanderClient(Settings.ServerAddress);
LANCommander.Token = new SDK.Models.AuthToken()
LANCommanderClient = new SDK.Client(Settings.ServerAddress);
LANCommanderClient.UseToken(new SDK.Models.AuthToken()
{
AccessToken = Settings.AccessToken,
RefreshToken = Settings.RefreshToken,
};
});
PowerShellRuntime = new PowerShellRuntime();
GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime);
// GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime);
api.UriHandler.RegisterSource("lancommander", args =>
{
@ -91,7 +87,7 @@ namespace LANCommander.PlaynitePlugin
public bool ValidateConnection()
{
return LANCommander.ValidateToken(LANCommander.Token);
return LANCommanderClient.ValidateToken();
}
public override IEnumerable<GameMetadata> GetGames(LibraryGetGamesArgs args)
@ -111,7 +107,7 @@ namespace LANCommander.PlaynitePlugin
}
}
var games = LANCommander
var games = LANCommanderClient
.GetGames()
.Where(g => g != null && g.Archives != null && g.Archives.Count() > 0);
@ -121,7 +117,7 @@ namespace LANCommander.PlaynitePlugin
{
Logger.Trace($"Importing/updating metadata for game \"{game.Title}\"...");
var manifest = LANCommander.GetGameManifest(game.Id);
var manifest = LANCommanderClient.GetGameManifest(game.Id);
Logger.Trace("Successfully grabbed game manifest");
var existingGame = PlayniteApi.Database.Games.FirstOrDefault(g => g.GameId == game.Id.ToString() && g.PluginId == Id && g.IsInstalled);
@ -130,7 +126,7 @@ namespace LANCommander.PlaynitePlugin
{
Logger.Trace("Game already exists in library, updating metadata...");
UpdateGame(manifest, game.Id);
UpdateGame(manifest);
continue;
}
@ -183,13 +179,13 @@ namespace LANCommander.PlaynitePlugin
metadata.Features.Add(new MetadataNameProperty($"Online Multiplayer {manifest.OnlineMultiplayer.GetPlayerCount()}".Trim()));
if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Icon))
metadata.Icon = new MetadataFile(LANCommander.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Icon)));
metadata.Icon = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Icon)));
if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Cover))
metadata.CoverImage = new MetadataFile(LANCommander.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Cover)));
metadata.CoverImage = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Cover)));
if (game.Media.Any(m => m.Type == SDK.Enums.MediaType.Background))
metadata.BackgroundImage = new MetadataFile(LANCommander.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Background)));
metadata.BackgroundImage = new MetadataFile(LANCommanderClient.GetMediaUrl(game.Media.First(m => m.Type == SDK.Enums.MediaType.Background)));
gameMetadata.Add(metadata);
}
@ -224,9 +220,9 @@ namespace LANCommander.PlaynitePlugin
if (args.Games.Count == 1 && args.Games.First().IsInstalled && !String.IsNullOrWhiteSpace(args.Games.First().InstallDirectory))
{
var nameChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.NameChange);
var keyChangeScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.KeyChange);
var installScriptPath = PowerShellRuntime.GetScriptFilePath(args.Games.First(), SDK.Enums.ScriptType.Install);
var nameChangeScriptPath = LANCommander.SDK.PowerShellRuntime.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.NameChange);
var keyChangeScriptPath = LANCommander.SDK.PowerShellRuntime.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.KeyChange);
var installScriptPath = LANCommander.SDK.PowerShellRuntime.GetScriptFilePath(args.Games.First().InstallDirectory, SDK.Enums.ScriptType.Install);
if (File.Exists(nameChangeScriptPath))
{
@ -243,8 +239,10 @@ namespace LANCommander.PlaynitePlugin
if (result.Result == true)
{
PowerShellRuntime.RunScript(nameChangeArgs.Games.First(), SDK.Enums.ScriptType.NameChange, $@"""{result.SelectedString}"" ""{oldName}""");
LANCommander.ChangeAlias(result.SelectedString);
var game = nameChangeArgs.Games.First();
LANCommander.SDK.PowerShellRuntime.RunScript(game.InstallDirectory, SDK.Enums.ScriptType.NameChange, $@"""{result.SelectedString}"" ""{oldName}""");
LANCommanderClient.ChangeAlias(result.SelectedString);
}
}
};
@ -264,12 +262,12 @@ namespace LANCommander.PlaynitePlugin
if (Guid.TryParse(keyChangeArgs.Games.First().GameId, out gameId))
{
// NUKIEEEE
var newKey = LANCommander.GetNewKey(gameId);
var newKey = LANCommanderClient.GetNewKey(gameId);
if (String.IsNullOrEmpty(newKey))
PlayniteApi.Dialogs.ShowErrorMessage("There are no more keys available on the server.", "No Keys Available");
else
PowerShellRuntime.RunScript(keyChangeArgs.Games.First(), SDK.Enums.ScriptType.KeyChange, $@"""{newKey}""");
LANCommander.SDK.PowerShellRuntime.RunScript(keyChangeArgs.Games.First().InstallDirectory, SDK.Enums.ScriptType.KeyChange, $@"""{newKey}""");
}
else
{
@ -292,7 +290,7 @@ namespace LANCommander.PlaynitePlugin
if (Guid.TryParse(installArgs.Games.First().GameId, out gameId))
{
PowerShellRuntime.RunScript(installArgs.Games.First(), SDK.Enums.ScriptType.Install);
LANCommander.SDK.PowerShellRuntime.RunScript(installArgs.Games.First().InstallDirectory, SDK.Enums.ScriptType.Install);
}
else
{
@ -334,12 +332,12 @@ namespace LANCommander.PlaynitePlugin
public override void OnGameStarting(OnGameStartingEventArgs args)
{
GameSaveService.DownloadSave(args.Game);
SaveController.Download(args.Game);
}
public override void OnGameStopped(OnGameStoppedEventArgs args)
{
GameSaveService.UploadSave(args.Game);
SaveController.Upload(args.Game);
}
public override IEnumerable<TopPanelItem> GetTopPanelItems()
@ -402,11 +400,11 @@ namespace LANCommander.PlaynitePlugin
var games = PlayniteApi.Database.Games.Where(g => g.IsInstalled).ToList();
LANCommander.ChangeAlias(result.SelectedString);
LANCommanderClient.ChangeAlias(result.SelectedString);
Logger.Trace($"Running name change scripts across {games.Count} installed game(s)");
PowerShellRuntime.RunScripts(games, SDK.Enums.ScriptType.NameChange, Settings.PlayerName);
LANCommander.SDK.PowerShellRuntime.RunScripts(games.Select(g => g.InstallDirectory), SDK.Enums.ScriptType.NameChange, Settings.PlayerName);
}
}
else
@ -441,9 +439,9 @@ namespace LANCommander.PlaynitePlugin
return window;
}
public void UpdateGame(SDK.GameManifest manifest, Guid gameId)
public void UpdateGame(SDK.GameManifest manifest)
{
var game = PlayniteApi.Database.Games.First(g => g.GameId == gameId.ToString());
var game = PlayniteApi.Database.Games.FirstOrDefault(g => g.GameId == manifest?.Id.ToString());
if (game == null)
return;

View File

@ -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);
}
}
}

View File

@ -12,29 +12,25 @@ namespace LANCommander.PlaynitePlugin
public static readonly ILogger Logger = LogManager.GetLogger();
private LANCommanderLibraryPlugin Plugin;
private PowerShellRuntime PowerShellRuntime;
public LANCommanderUninstallController(LANCommanderLibraryPlugin plugin, Game game) : base(game)
{
Name = "Uninstall LANCommander Game";
Plugin = plugin;
PowerShellRuntime = new PowerShellRuntime();
}
public override void Uninstall(UninstallActionArgs args)
{
try
{
PowerShellRuntime.RunScript(Game, ScriptType.Uninstall);
var gameManager = new LANCommander.SDK.GameManager(Plugin.LANCommanderClient, Plugin.Settings.InstallDirectory);
gameManager.Uninstall(Game.InstallDirectory);
}
catch { }
catch (Exception ex)
{
Logger.Trace("Attempting to delete install directory...");
if (!String.IsNullOrWhiteSpace(Game.InstallDirectory) && Directory.Exists(Game.InstallDirectory))
Directory.Delete(Game.InstallDirectory, true);
Logger.Trace("Deleted!");
}
InvokeOnUninstalled(new GameUninstalledEventArgs());
}

View File

@ -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);

View File

@ -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);

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="ByteSize" version="2.1.1" targetFramework="net462" />
<package id="Microsoft.Bcl.AsyncInterfaces" version="7.0.0" targetFramework="net462" />
<package id="NuGet.CommandLine" version="6.7.0" targetFramework="net462" developmentDependency="true" />
<package id="PlayniteSDK" version="6.10.0" targetFramework="net462" />
<package id="PowerShellStandard.Library" version="5.1.1" targetFramework="net462" />
<package id="RestSharp" version="106.15.0" targetFramework="net462" />
<package id="rix0rrr.BeaconLib" version="1.0.2" targetFramework="net462" />
<package id="SharpCompress" version="0.34.1" targetFramework="net462" />
<package id="System.Buffers" version="4.5.1" targetFramework="net462" />
@ -16,6 +16,5 @@
<package id="System.Text.Json" version="7.0.3" targetFramework="net462" />
<package id="System.Threading.Tasks.Extensions" version="4.5.4" targetFramework="net462" />
<package id="System.ValueTuple" version="4.5.0" targetFramework="net462" />
<package id="YamlDotNet" version="5.4.0" targetFramework="net462" />
<package id="ZstdSharp.Port" version="0.7.2" targetFramework="net462" />
</packages>

View File

@ -1,6 +1,6 @@
using LANCommander.SDK;
using LANCommander.SDK.Models;
using Playnite.SDK;
using Microsoft.Extensions.Logging;
using RestSharp;
using System;
using System.Collections.Generic;
@ -11,19 +11,27 @@ using System.Net;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin
namespace LANCommander.SDK
{
internal class LANCommanderClient
public class Client
{
public static readonly ILogger Logger = LogManager.GetLogger();
private readonly ILogger Logger;
public readonly RestClient Client;
public AuthToken Token;
private readonly RestClient ApiClient;
private AuthToken Token;
public LANCommanderClient(string baseUrl)
public Client(string baseUrl)
{
if (!String.IsNullOrWhiteSpace(baseUrl))
Client = new RestClient(baseUrl);
ApiClient = new RestClient(baseUrl);
}
public Client(string baseUrl, ILogger logger)
{
if (!String.IsNullOrWhiteSpace(baseUrl))
ApiClient = new RestClient(baseUrl);
Logger = logger;
}
private T PostRequest<T>(string route, object body)
@ -32,7 +40,7 @@ namespace LANCommander.PlaynitePlugin
.AddJsonBody(body)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
var response = Client.Post<T>(request);
var response = ApiClient.Post<T>(request);
return response.Data;
}
@ -42,7 +50,7 @@ namespace LANCommander.PlaynitePlugin
var request = new RestRequest(route)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
var response = Client.Get<T>(request);
var response = ApiClient.Get<T>(request);
return response.Data;
}
@ -58,7 +66,7 @@ namespace LANCommander.PlaynitePlugin
client.DownloadProgressChanged += (s, e) => progressHandler(e);
client.DownloadFileCompleted += (s, e) => completeHandler(e);
client.DownloadFileAsync(new Uri($"{Client.BaseUrl}{route}"), tempFile);
client.DownloadFileAsync(new Uri($"{ApiClient.BaseUrl}{route}"), tempFile);
return tempFile;
}
@ -72,14 +80,14 @@ namespace LANCommander.PlaynitePlugin
client.Headers.Add("Authorization", $"Bearer {Token.AccessToken}");
var ws = client.OpenRead(new Uri($"{Client.BaseUrl}{route}"));
var ws = client.OpenRead(new Uri($"{ApiClient.BaseUrl}{route}"));
return new TrackableStream(ws, true, Convert.ToInt64(client.ResponseHeaders["Content-Length"]));
}
public async Task<AuthResponse> AuthenticateAsync(string username, string password)
public async Task<AuthToken> AuthenticateAsync(string username, string password)
{
var response = await Client.ExecuteAsync<AuthResponse>(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest()
var response = await ApiClient.ExecuteAsync<AuthResponse>(new RestRequest("/api/Auth", Method.POST).AddJsonBody(new AuthRequest()
{
UserName = username,
Password = password
@ -88,7 +96,14 @@ namespace LANCommander.PlaynitePlugin
switch (response.StatusCode)
{
case HttpStatusCode.OK:
return response.Data;
Token = new AuthToken
{
AccessToken = response.Data.AccessToken,
RefreshToken = response.Data.RefreshToken,
Expiration = response.Data.Expiration
};
return Token;
case HttpStatusCode.Forbidden:
case HttpStatusCode.BadRequest:
@ -100,9 +115,9 @@ namespace LANCommander.PlaynitePlugin
}
}
public async Task<AuthResponse> RegisterAsync(string username, string password)
public async Task<AuthToken> RegisterAsync(string username, string password)
{
var response = await Client.ExecuteAsync<AuthResponse>(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest()
var response = await ApiClient.ExecuteAsync<AuthResponse>(new RestRequest("/api/auth/register", Method.POST).AddJsonBody(new AuthRequest()
{
UserName = username,
Password = password
@ -111,7 +126,14 @@ namespace LANCommander.PlaynitePlugin
switch (response.StatusCode)
{
case HttpStatusCode.OK:
return response.Data;
Token = new AuthToken
{
AccessToken = response.Data.AccessToken,
RefreshToken = response.Data.RefreshToken,
Expiration = response.Data.Expiration
};
return Token;
case HttpStatusCode.BadRequest:
case HttpStatusCode.Forbidden:
@ -125,33 +147,45 @@ namespace LANCommander.PlaynitePlugin
public async Task<bool> PingAsync()
{
var response = await Client.ExecuteAsync(new RestRequest("/api/Ping", Method.GET));
var response = await ApiClient.ExecuteAsync(new RestRequest("/api/Ping", Method.GET));
return response.StatusCode == HttpStatusCode.OK;
}
public AuthResponse RefreshToken(AuthToken token)
public AuthToken RefreshToken(AuthToken token)
{
Logger.Trace("Refreshing token...");
Logger?.LogTrace("Refreshing token...");
var request = new RestRequest("/api/Auth/Refresh")
.AddJsonBody(token);
var response = Client.Post<AuthResponse>(request);
var response = ApiClient.Post<AuthResponse>(request);
if (response.StatusCode != HttpStatusCode.OK)
throw new WebException(response.ErrorMessage);
return response.Data;
Token = new AuthToken
{
AccessToken = response.Data.AccessToken,
RefreshToken = response.Data.RefreshToken,
Expiration = response.Data.Expiration
};
return Token;
}
public bool ValidateToken()
{
return ValidateToken(Token);
}
public bool ValidateToken(AuthToken token)
{
Logger.Trace("Validating token...");
Logger?.LogTrace("Validating token...");
if (token == null)
{
Logger.Trace("Token is null!");
Logger?.LogTrace("Token is null!");
return false;
}
@ -160,22 +194,27 @@ namespace LANCommander.PlaynitePlugin
if (String.IsNullOrEmpty(token.AccessToken) || String.IsNullOrEmpty(token.RefreshToken))
{
Logger.Trace("Token is empty!");
Logger?.LogTrace("Token is empty!");
return false;
}
var response = Client.Post(request);
var response = ApiClient.Post(request);
var valid = response.StatusCode == HttpStatusCode.OK;
if (valid)
Logger.Trace("Token is valid!");
Logger?.LogTrace("Token is valid!");
else
Logger.Trace("Token is invalid!");
Logger?.LogTrace("Token is invalid!");
return response.StatusCode == HttpStatusCode.OK;
}
public void UseToken(AuthToken token)
{
Token = token;
}
public IEnumerable<Game> GetGames()
{
return GetRequest<IEnumerable<Game>>("/api/Games");
@ -223,26 +262,26 @@ namespace LANCommander.PlaynitePlugin
public GameSave UploadSave(string gameId, byte[] data)
{
Logger.Trace("Uploading save...");
Logger?.LogTrace("Uploading save...");
var request = new RestRequest($"/api/Saves/Upload/{gameId}", Method.POST)
.AddHeader("Authorization", $"Bearer {Token.AccessToken}");
request.AddFile(gameId, data, gameId);
var response = Client.Post<GameSave>(request);
var response = ApiClient.Post<GameSave>(request);
return response.Data;
}
public string GetMediaUrl(Media media)
{
return (new Uri(Client.BaseUrl, $"/api/Media/{media.Id}/Download?fileId={media.FileId}").ToString());
return (new Uri(ApiClient.BaseUrl, $"/api/Media/{media.Id}/Download?fileId={media.FileId}").ToString());
}
public string GetKey(Guid id)
{
Logger.Trace("Requesting key allocation...");
Logger?.LogTrace("Requesting key allocation...");
var macAddress = GetMacAddress();
@ -261,7 +300,7 @@ namespace LANCommander.PlaynitePlugin
public string GetAllocatedKey(Guid id)
{
Logger.Trace("Requesting allocated key...");
Logger?.LogTrace("Requesting allocated key...");
var macAddress = GetMacAddress();
@ -283,7 +322,7 @@ namespace LANCommander.PlaynitePlugin
public string GetNewKey(Guid id)
{
Logger.Trace("Requesting new key allocation...");
Logger?.LogTrace("Requesting new key allocation...");
var macAddress = GetMacAddress();
@ -305,14 +344,14 @@ namespace LANCommander.PlaynitePlugin
public User GetProfile()
{
Logger.Trace("Requesting player's profile...");
Logger?.LogTrace("Requesting player's profile...");
return GetRequest<User>("/api/Profile");
}
public string ChangeAlias(string alias)
{
Logger.Trace("Requesting to change player alias...");
Logger?.LogTrace("Requesting to change player alias...");
var response = PostRequest<object>("/api/Profile/ChangeAlias", alias);

View File

@ -0,0 +1,20 @@
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.Text;
namespace LANCommander.SDK
{
public class ArchiveExtractionProgressArgs : EventArgs
{
public long Position { get; set; }
public long Length { get; set; }
}
public class ArchiveEntryExtractionProgressArgs : EventArgs
{
public ReaderProgress Progress { get; set; }
public IEntry Entry { get; set; }
}
}

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin
namespace LANCommander.SDK
{
internal class ExtractionResult
{

View File

@ -0,0 +1,239 @@
using LANCommander.SDK.Enums;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using Microsoft.Extensions.Logging;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace LANCommander.SDK
{
public class GameManager
{
private readonly ILogger Logger;
private Client Client { get; set; }
private string DefaultInstallDirectory { get; set; }
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
public delegate void OnArchiveExtractionProgressHandler(long position, long length);
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress;
private TrackableStream Stream;
private IReader Reader;
public GameManager(Client client, string defaultInstallDirectory)
{
Client = client;
DefaultInstallDirectory = defaultInstallDirectory;
}
public GameManager(Client client, string defaultInstallDirectory, ILogger logger)
{
Client = client;
DefaultInstallDirectory = DefaultInstallDirectory;
Logger = logger;
}
/// <summary>
/// Downloads, extracts, and runs post-install scripts for the specified game
/// </summary>
/// <param name="game">Game to install</param>
/// <param name="maxAttempts">Maximum attempts in case of transmission error</param>
/// <returns>Final install path</returns>
/// <exception cref="Exception"></exception>
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<ExtractionResult>(maxAttempts, TimeSpan.FromMilliseconds(500), new ExtractionResult(), () =>
{
Logger?.LogTrace("Attempting to download and extract game");
return DownloadAndExtract(game, DefaultInstallDirectory);
});
if (!result.Success && !result.Canceled)
throw new Exception("Could not extract the installer. Retry the install or check your connection");
else if (result.Canceled)
return "";
GameManifest manifest = null;
game.InstallDirectory = result.Directory;
var writeManifestSuccess = RetryHelper.RetryOnException(maxAttempts, TimeSpan.FromSeconds(1), false, () =>
{
Logger?.LogTrace("Attempting to get game manifest");
manifest = Client.GetGameManifest(game.Id);
ManifestHelper.Write(manifest, game.InstallDirectory);
return true;
});
if (!writeManifestSuccess)
throw new Exception("Could not grab the manifest file. Retry the install or check your connection");
Logger?.LogTrace("Saving scripts");
ScriptHelper.SaveScript(game, ScriptType.Install);
ScriptHelper.SaveScript(game, ScriptType.Uninstall);
ScriptHelper.SaveScript(game, ScriptType.NameChange);
ScriptHelper.SaveScript(game, ScriptType.KeyChange);
try
{
PowerShellRuntime.RunScript(game, ScriptType.Install);
PowerShellRuntime.RunScript(game, ScriptType.NameChange, /* Plugin.Settings.PlayerName */ "");
var key = Client.GetAllocatedKey(game.Id);
PowerShellRuntime.RunScript(game, ScriptType.KeyChange, $"\"{key}\"");
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not execute post-install scripts");
}
return result.Directory;
}
public void Uninstall(string installDirectory)
{
var manifest = ManifestHelper.Read(installDirectory);
try
{
Logger?.LogTrace("Running uninstall script");
PowerShellRuntime.RunScript(installDirectory, ScriptType.Uninstall);
}
catch (Exception ex)
{
Logger?.LogError(ex, "Error running uninstall script");
}
Logger?.LogTrace("Attempting to delete the install directory");
if (Directory.Exists(installDirectory))
Directory.Delete(installDirectory, true);
Logger?.LogTrace("Deleted install directory {InstallDirectory}", installDirectory);
}
private ExtractionResult DownloadAndExtract(Game game, string installDirectory = "")
{
if (game == null)
{
Logger?.LogTrace("Game failed to download, no game was specified");
throw new ArgumentNullException("No game was specified");
}
if (String.IsNullOrWhiteSpace(installDirectory))
installDirectory = DefaultInstallDirectory;
var destination = Path.Combine(installDirectory, game.Title.SanitizeFilename());
Logger?.LogTrace("Downloading and extracting {Game} to path {Destination}", game.Title, destination);
var extractionResult = new ExtractionResult
{
Canceled = false,
};
try
{
Directory.CreateDirectory(destination);
Stream = Client.StreamGame(game.Id);
Reader = ReaderFactory.Open(Stream);
Stream.OnProgress += (pos, len) =>
{
OnArchiveExtractionProgress?.Invoke(pos, len);
};
Reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
{
OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs
{
Entry = e.Item,
Progress = e.ReaderProgress,
});
};
while (Reader.MoveToNextEntry())
{
if (Reader.Cancelled)
break;
Reader.WriteEntryToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true,
PreserveFileTime = true,
});
}
Reader.Dispose();
Stream.Dispose();
}
catch (Exception ex)
{
if (Reader.Cancelled)
{
Logger?.LogTrace("User cancelled the download");
extractionResult.Canceled = true;
if (Directory.Exists(destination))
{
Logger?.LogTrace("Cleaning up orphaned files after cancelled install");
Directory.Delete(destination, true);
}
}
else
{
Logger?.LogError(ex, "Could not extract to path {Destination}", destination);
if (Directory.Exists(destination))
{
Logger?.LogTrace("Cleaning up orphaned install files after bad install");
Directory.Delete(destination, true);
}
throw new Exception("The game archive could not be extracted, is it corrupted? Please try again");
}
}
if (!extractionResult.Canceled)
{
extractionResult.Success = true;
extractionResult.Directory = destination;
Logger?.LogTrace("Game {Game} successfully downloaded and extracted to {Destination}", game.Title, destination);
}
return extractionResult;
}
public void CancelInstall()
{
Reader?.Cancel();
// Reader?.Dispose();
// Stream?.Dispose();
}
}
}

View File

@ -1,66 +1,55 @@
using LANCommander.SDK;
using Playnite.SDK;
using Playnite.SDK.Models;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace LANCommander.PlaynitePlugin.Services
namespace LANCommander.SDK
{
internal class GameSaveService
public class GameSaveManager
{
private readonly LANCommanderClient LANCommander;
private readonly IPlayniteAPI PlayniteApi;
private readonly PowerShellRuntime PowerShellRuntime;
private readonly Client Client;
internal GameSaveService(LANCommanderClient lanCommander, IPlayniteAPI playniteApi, PowerShellRuntime powerShellRuntime)
public delegate void OnDownloadProgressHandler(DownloadProgressChangedEventArgs e);
public event OnDownloadProgressHandler OnDownloadProgress;
public delegate void OnDownloadCompleteHandler(AsyncCompletedEventArgs e);
public event OnDownloadCompleteHandler OnDownloadComplete;
public GameSaveManager(Client client)
{
LANCommander = lanCommander;
PlayniteApi = playniteApi;
PowerShellRuntime = powerShellRuntime;
Client = client;
}
internal void DownloadSave(Game game)
public void Download(string installDirectory)
{
var manifest = ManifestHelper.Read(installDirectory);
string tempFile = String.Empty;
if (game != null)
if (manifest != null)
{
PlayniteApi.Dialogs.ActivateGlobalProgress(progress =>
var destination = Client.DownloadLatestSave(manifest.Id, (changed) =>
{
progress.ProgressMaxValue = 100;
progress.CurrentProgressValue = 0;
var destination = LANCommander.DownloadLatestSave(Guid.Parse(game.GameId), (changed) =>
{
progress.CurrentProgressValue = changed.ProgressPercentage;
}, (complete) =>
{
progress.CurrentProgressValue = 100;
});
// Lock the thread until download is done
while (progress.CurrentProgressValue != 100)
{
}
tempFile = destination;
},
new GlobalProgressOptions("Downloading latest save...")
OnDownloadProgress?.Invoke(changed);
}, (complete) =>
{
IsIndeterminate = false,
Cancelable = false
OnDownloadComplete?.Invoke(complete);
});
tempFile = destination;
// Go into the archive and extract the files to the correct locations
try
{
@ -74,10 +63,6 @@ namespace LANCommander.PlaynitePlugin.Services
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
var manifestContents = File.ReadAllText(Path.Combine(tempLocation, "_manifest.yml"));
var manifest = deserializer.Deserialize<GameManifest>(manifestContents);
#region Move files
foreach (var savePath in manifest.SavePaths.Where(sp => sp.Type == "File"))
{
@ -86,7 +71,7 @@ namespace LANCommander.PlaynitePlugin.Services
var tempSavePathFile = Path.Combine(tempSavePath, savePath.Path.Replace('/', '\\').Replace("{InstallDir}\\", ""));
var destination = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", game.InstallDirectory));
destination = Environment.ExpandEnvironmentVariables(savePath.Path.Replace('/', '\\').Replace("{InstallDir}", installDirectory));
if (File.Exists(tempSavePathFile))
{
@ -107,7 +92,7 @@ namespace LANCommander.PlaynitePlugin.Services
if (inInstallDir)
{
// Files are in the game's install directory. Move them there from the save path.
destination = file.Replace(tempSavePath, savePath.Path.Replace('/', '\\').TrimEnd('\\').Replace("{InstallDir}", game.InstallDirectory));
destination = file.Replace(tempSavePath, savePath.Path.Replace('/', '\\').TrimEnd('\\').Replace("{InstallDir}", installDirectory));
if (File.Exists(destination))
File.Delete(destination);
@ -155,97 +140,89 @@ namespace LANCommander.PlaynitePlugin.Services
}
}
internal void UploadSave(Game game)
public void Upload(string installDirectory)
{
var manifestPath = Path.Combine(game.InstallDirectory, "_manifest.yml");
var manifest = ManifestHelper.Read(installDirectory);
if (File.Exists(manifestPath))
var temp = Path.GetTempFileName();
if (manifest.SavePaths != null && manifest.SavePaths.Count() > 0)
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
var manifest = deserializer.Deserialize<GameManifest>(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<string> tempRegFiles = new List<string>();
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<string> tempRegFiles = new List<string>();
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());
}
}
}
}

View File

@ -0,0 +1,57 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization;
namespace LANCommander.SDK.Helpers
{
public static class ManifestHelper
{
public static readonly ILogger Logger;
public const string ManifestFilename = "_manifest.yml";
public static GameManifest Read(string installDirectory)
{
var source = GetPath(installDirectory);
var yaml = File.ReadAllText(source);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
Logger?.LogTrace("Deserializing manifest");
var manifest = deserializer.Deserialize<GameManifest>(yaml);
return manifest;
}
public static void Write(GameManifest manifest, string installDirectory)
{
var destination = GetPath(installDirectory);
Logger?.LogTrace("Attempting to write manifest to path {Destination}", destination);
var serializer = new SerializerBuilder()
.WithNamingConvention(new PascalCaseNamingConvention())
.Build();
Logger?.LogTrace("Serializing manifest");
var yaml = serializer.Serialize(manifest);
Logger?.LogTrace("Writing manifest file");
File.WriteAllText(destination, yaml);
}
public static string GetPath(string installDirectory)
{
return Path.Combine(installDirectory, ManifestFilename);
}
}
}

View File

@ -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<T>(int maxAttempts, TimeSpan delay, T @default, Func<T> 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;

View File

@ -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);
}
}
}

View File

@ -1,7 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageReference Include="RestSharp" Version="106.15.0" />
<PackageReference Include="SharpCompress" Version="0.34.1" />
<PackageReference Include="YamlDotNet" Version="5.4.0" />
</ItemGroup>
</Project>

View File

@ -8,5 +8,6 @@ namespace LANCommander.SDK.Models
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime Expiration { get; set; }
}
}

View File

@ -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<Action> Actions { get; set; }
public virtual IEnumerable<Tag> Tags { get; set; }
public virtual Company Publisher { get; set; }

View File

@ -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; }

View File

@ -1,22 +1,20 @@
using LANCommander.SDK.Enums;
using Playnite.SDK;
using Playnite.SDK.Models;
using LANCommander.SDK.Models;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Runtime.InteropServices;
using System.Security.RightsManagement;
using System.Text;
using System.Threading.Tasks;
namespace LANCommander.PlaynitePlugin
namespace LANCommander.SDK
{
internal class PowerShellRuntime
public static class PowerShellRuntime
{
public static readonly ILogger Logger = LogManager.GetLogger();
public static readonly ILogger Logger;
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool Wow64DisableWow64FsRedirection(ref IntPtr ptr);
@ -24,13 +22,13 @@ namespace LANCommander.PlaynitePlugin
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool Wow64RevertWow64FsRedirection(ref IntPtr ptr);
public void RunCommand(string command, bool asAdmin = false)
public static void RunCommand(string command, bool asAdmin = false)
{
Logger.Trace($"Executing command `{command}` | Admin: {asAdmin}");
Logger?.LogTrace($"Executing command `{command}` | Admin: {asAdmin}");
var tempScript = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".ps1");
Logger.Trace($"Creating temp script at path {tempScript}");
Logger?.LogTrace($"Creating temp script at path {tempScript}");
File.WriteAllText(tempScript, command);
@ -39,9 +37,9 @@ namespace LANCommander.PlaynitePlugin
File.Delete(tempScript);
}
public int RunScript(string path, bool asAdmin = false, string arguments = null, string workingDirectory = null)
public static int RunScript(string path, bool asAdmin = false, string arguments = null, string workingDirectory = null)
{
Logger.Trace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}");
Logger?.LogTrace($"Executing script at path {path} | Admin: {asAdmin} | Arguments: {arguments}");
var wow64Value = IntPtr.Zero;
@ -75,9 +73,14 @@ namespace LANCommander.PlaynitePlugin
return process.ExitCode;
}
public void RunScript(Game game, ScriptType type, string arguments = null)
public static void RunScript(Game game, ScriptType type, string arguments = null)
{
var path = GetScriptFilePath(game, type);
RunScript(game.InstallDirectory, type, arguments);
}
public static void RunScript(string installDirectory, ScriptType type, string arguments = null)
{
var path = GetScriptFilePath(installDirectory, type);
if (File.Exists(path))
{
@ -90,12 +93,12 @@ namespace LANCommander.PlaynitePlugin
}
}
public void RunScriptsAsAdmin(IEnumerable<string> paths, string arguments = null)
public static void RunScriptsAsAdmin(IEnumerable<string> paths, string arguments = null)
{
// Concatenate scripts
var sb = new StringBuilder();
Logger.Trace("Concatenating scripts...");
Logger?.LogTrace("Concatenating scripts...");
foreach (var path in paths)
{
@ -103,16 +106,16 @@ namespace LANCommander.PlaynitePlugin
sb.AppendLine(contents);
Logger.Trace($"Added {path}!");
Logger?.LogTrace($"Added {path}!");
}
Logger.Trace("Done concatenating!");
Logger?.LogTrace("Done concatenating!");
if (sb.Length > 0)
{
var scriptPath = Path.GetTempFileName();
Logger.Trace($"Creating temp script at path {scriptPath}");
Logger?.LogTrace($"Creating temp script at path {scriptPath}");
File.WriteAllText(scriptPath, sb.ToString());
@ -120,14 +123,14 @@ namespace LANCommander.PlaynitePlugin
}
}
public void RunScripts(IEnumerable<Game> games, ScriptType type, string arguments = null)
public static void RunScripts(IEnumerable<string> installDirectories, ScriptType type, string arguments = null)
{
List<string> scripts = new List<string>();
List<string> adminScripts = new List<string>();
foreach (var game in games)
foreach (var installDirectory in installDirectories)
{
var path = GetScriptFilePath(game, type);
var path = GetScriptFilePath(installDirectory, type);
if (!File.Exists(path))
continue;
@ -149,6 +152,11 @@ namespace LANCommander.PlaynitePlugin
}
public static string GetScriptFilePath(Game game, ScriptType type)
{
return GetScriptFilePath(game.InstallDirectory, type);
}
public static string GetScriptFilePath(string installDirectory, ScriptType type)
{
Dictionary<ScriptType, string> filenames = new Dictionary<ScriptType, string>() {
{ ScriptType.Install, "_install.ps1" },
@ -159,7 +167,7 @@ namespace LANCommander.PlaynitePlugin
var filename = filenames[type];
return Path.Combine(game.InstallDirectory, filename);
return Path.Combine(installDirectory, filename);
}
}
}

View File

@ -0,0 +1,168 @@
using LANCommander.SDK.Enums;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Helpers;
using LANCommander.SDK.Models;
using Microsoft.Extensions.Logging;
using SharpCompress.Common;
using SharpCompress.Readers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace LANCommander.SDK
{
public class RedistributableManager
{
private readonly ILogger Logger;
private Client Client { get; set; }
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
public delegate void OnArchiveExtractionProgressHandler(long position, long length);
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress;
public RedistributableManager(Client client)
{
Client = client;
}
public RedistributableManager(Client client, ILogger logger)
{
Client = client;
Logger = logger;
}
public void Install(Game game)
{
foreach (var redistributable in game.Redistributables)
{
Install(redistributable);
}
}
public void Install(Redistributable redistributable)
{
string installScriptTempFile = null;
string detectionScriptTempFile = null;
string extractTempPath = null;
try
{
var installScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.Install);
installScriptTempFile = ScriptHelper.SaveTempScript(installScript);
var detectionScript = redistributable.Scripts.FirstOrDefault(s => s.Type == ScriptType.DetectInstall);
detectionScriptTempFile = ScriptHelper.SaveTempScript(detectionScript);
var detectionResult = PowerShellRuntime.RunScript(detectionScriptTempFile, detectionScript.RequiresAdmin);
// Redistributable is not installed
if (detectionResult == 0)
{
if (redistributable.Archives.Count() > 0)
{
var extractionResult = DownloadAndExtract(redistributable);
if (extractionResult.Success)
{
extractTempPath = extractionResult.Directory;
PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath);
}
}
else
{
PowerShellRuntime.RunScript(installScriptTempFile, installScript.RequiresAdmin, null, extractTempPath);
}
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Redistributable {Redistributable} failed to install", redistributable.Name);
}
finally
{
if (File.Exists(installScriptTempFile))
File.Delete(installScriptTempFile);
if (File.Exists(detectionScriptTempFile))
File.Delete(detectionScriptTempFile);
if (Directory.Exists(extractTempPath))
Directory.Delete(extractTempPath);
}
}
private ExtractionResult DownloadAndExtract(Redistributable redistributable)
{
if (redistributable == null)
{
Logger?.LogTrace("Redistributable failed to download! No redistributable was specified");
throw new ArgumentNullException("No redistributable was specified");
}
var destination = Path.Combine(Path.GetTempPath(), redistributable.Name.SanitizeFilename());
Logger?.LogTrace("Downloading and extracting {Redistributable} to path {Destination}", redistributable.Name, destination);
try
{
Directory.CreateDirectory(destination);
using (var redistributableStream = Client.StreamRedistributable(redistributable.Id))
using (var reader = ReaderFactory.Open(redistributableStream))
{
redistributableStream.OnProgress += (pos, len) =>
{
OnArchiveExtractionProgress?.Invoke(pos, len);
};
reader.EntryExtractionProgress += (object sender, ReaderExtractionEventArgs<IEntry> e) =>
{
OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs
{
Entry = e.Item,
Progress = e.ReaderProgress,
});
};
reader.WriteAllToDirectory(destination, new ExtractionOptions()
{
ExtractFullPath = true,
Overwrite = true
});
}
}
catch (Exception ex)
{
Logger?.LogError(ex, "Could not extract to path {Destination}", destination);
if (Directory.Exists(destination))
{
Logger?.LogTrace("Cleaning up orphaned files after bad install");
Directory.Delete(destination, true);
}
throw new Exception("The redistributable archive could not be extracted, is it corrupted? Please try again");
}
var extractionResult = new ExtractionResult
{
Canceled = false
};
if (!extractionResult.Canceled)
{
extractionResult.Success = true;
extractionResult.Directory = destination;
Logger?.LogTrace("Redistributable {Redistributable} successfully downloaded and extracted to {Destination}", redistributable.Name, destination);
}
return extractionResult;
}
}
}

View File

@ -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 { };

View File

@ -44,6 +44,7 @@ namespace LANCommander.Services
var manifest = new GameManifest()
{
Id = game.Id,
Title = game.Title,
SortTitle = game.SortTitle,
Description = game.Description,