First pass of adding tool installation to launcher
This commit is contained in:
parent
4474164e9b
commit
46bbf85093
16 changed files with 789 additions and 205 deletions
|
|
@ -43,6 +43,7 @@ namespace LANCommander.Launcher.Data.Models
|
|||
public virtual ICollection<Company>? Developers { get; set; } = new List<Company>();
|
||||
public virtual ICollection<Platform>? Platforms { get; set; } = new List<Platform>();
|
||||
public virtual ICollection<Redistributable>? Redistributables { get; set; } = new List<Redistributable>();
|
||||
public virtual ICollection<Tool>? Tools { get; set; } = new List<Tool>();
|
||||
public virtual ICollection<Media>? Media { get; set; } = new List<Media>();
|
||||
public virtual ICollection<Collection> Collections { get; set; } = new List<Collection>();
|
||||
public virtual ICollection<Game> DependentGames { get; set; } = new List<Game>();
|
||||
|
|
|
|||
20
LANCommander.Launcher.Data/Models/Tool.cs
Normal file
20
LANCommander.Launcher.Data/Models/Tool.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace LANCommander.Launcher.Data.Models
|
||||
{
|
||||
[Table("Tools")]
|
||||
public class Tool : BaseModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public bool Installed { get; set; }
|
||||
public string? InstallDirectory { get; set; }
|
||||
public string? InstalledVersion { get; set; }
|
||||
public DateTime? InstalledOn { get; set; }
|
||||
public string? LatestVersion { get; set; }
|
||||
|
||||
public virtual ICollection<Game>? Games { get; set; } = new List<Game>();
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,6 @@ namespace LANCommander.Launcher.Models
|
|||
public interface IInstallQueueItem
|
||||
{
|
||||
Guid Id { get; set; }
|
||||
Guid[] AddonIds { get; set; }
|
||||
Dictionary<Guid, string?> AddonVersions { get; set; }
|
||||
string Title { get; set; }
|
||||
string Version { get; set; }
|
||||
string InstallDirectory { get; set; }
|
||||
|
|
|
|||
65
LANCommander.Launcher.Models/InstallQueueTool.cs
Normal file
65
LANCommander.Launcher.Models/InstallQueueTool.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using LANCommander.SDK.Enums;
|
||||
|
||||
namespace LANCommander.Launcher.Models;
|
||||
|
||||
public class InstallQueueTool : IInstallQueueItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid[] AddonIds { get; set; }
|
||||
public Dictionary<Guid, string?> AddonVersions { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string InstallDirectory { get; set; }
|
||||
public Guid CoverId { get; set; }
|
||||
public Guid IconId { get; set; }
|
||||
public DateTime QueuedOn { get; set; }
|
||||
public DateTime? CompletedOn { get; set; }
|
||||
public bool IsUpdate { get; set; }
|
||||
|
||||
public bool State {
|
||||
get
|
||||
{
|
||||
switch (Status)
|
||||
{
|
||||
case InstallStatus.Starting:
|
||||
case InstallStatus.Moving:
|
||||
case InstallStatus.Downloading:
|
||||
case InstallStatus.InstallingRedistributables:
|
||||
case InstallStatus.InstallingMods:
|
||||
case InstallStatus.InstallingExpansions:
|
||||
case InstallStatus.InstallingAddons:
|
||||
case InstallStatus.RunningScripts:
|
||||
case InstallStatus.DownloadingSaves:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InstallStatus Status { get; set; }
|
||||
public SDK.Models.Tool Tool { get; set; }
|
||||
|
||||
public float Progress {
|
||||
get
|
||||
{
|
||||
return BytesDownloaded / (float)TotalBytes;
|
||||
}
|
||||
set { }
|
||||
}
|
||||
public double TransferSpeed { get; set; }
|
||||
public long BytesDownloaded { get; set; }
|
||||
public long TotalBytes { get; set; }
|
||||
public CancellationTokenSource CancellationToken { get; set; } = new();
|
||||
|
||||
public InstallQueueTool(SDK.Models.Tool tool)
|
||||
{
|
||||
Tool = tool;
|
||||
Id = tool.Id;
|
||||
Title = tool.Name;
|
||||
Version = tool.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault()?.Version ?? "";
|
||||
QueuedOn = DateTime.Now;
|
||||
Status = InstallStatus.Queued;
|
||||
}
|
||||
}
|
||||
|
|
@ -26,10 +26,10 @@ namespace LANCommander.Launcher.Services.Extensions
|
|||
});
|
||||
|
||||
#region Register Client
|
||||
var options = new LANCommanderOptions();
|
||||
|
||||
configure?.Invoke(options);
|
||||
|
||||
var options = new LANCommanderOptions();
|
||||
|
||||
configure?.Invoke(options);
|
||||
|
||||
services.AddSingleton<MessageBusService>();
|
||||
services.AddSingleton<AuthenticationService>();
|
||||
services.AddSingleton<KeepAliveService>();
|
||||
|
|
@ -58,6 +58,7 @@ namespace LANCommander.Launcher.Services.Extensions
|
|||
services.AddScoped<PlaySessionService>();
|
||||
services.AddScoped<ProfileService>();
|
||||
services.AddScoped<RedistributableService>();
|
||||
services.AddScoped<ToolService>();
|
||||
services.AddScoped<SaveService>();
|
||||
services.AddScoped<TagService>();
|
||||
services.AddScoped<UpdateService>();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace LANCommander.Launcher.Services.Import;
|
||||
|
||||
public class ImportContext
|
||||
public class ImportContext
|
||||
{
|
||||
private int Processed;
|
||||
private int Total;
|
||||
|
|
@ -27,27 +27,28 @@ public class ImportContext
|
|||
private readonly MultiplayerModeImporter _multiplayerModes;
|
||||
private readonly PlatformImporter _platforms;
|
||||
private readonly PublisherImporter _publishers;
|
||||
private readonly TagImporter _tags;
|
||||
|
||||
private readonly ILogger<ImportContext> _logger;
|
||||
|
||||
public ImportContext(IServiceProvider serviceProvider)
|
||||
{
|
||||
_collections = serviceProvider.GetRequiredService<CollectionImporter>();
|
||||
_developers = serviceProvider.GetRequiredService<DeveloperImporter>();
|
||||
_engines = serviceProvider.GetRequiredService<EngineImporter>();
|
||||
_games = serviceProvider.GetRequiredService<GameImporter>();
|
||||
_genres = serviceProvider.GetRequiredService<GenreImporter>();
|
||||
_media = serviceProvider.GetRequiredService<MediaImporter>();
|
||||
_multiplayerModes = serviceProvider.GetRequiredService<MultiplayerModeImporter>();
|
||||
_platforms = serviceProvider.GetRequiredService<PlatformImporter>();
|
||||
_publishers = serviceProvider.GetRequiredService<PublisherImporter>();
|
||||
_tags = serviceProvider.GetRequiredService<TagImporter>();
|
||||
_logger = serviceProvider.GetRequiredService<ILogger<ImportContext>>();
|
||||
|
||||
SetupContextOnImporters();
|
||||
}
|
||||
|
||||
private readonly TagImporter _tags;
|
||||
private readonly ToolImporter _tools;
|
||||
|
||||
private readonly ILogger<ImportContext> _logger;
|
||||
|
||||
public ImportContext(IServiceProvider serviceProvider)
|
||||
{
|
||||
_collections = serviceProvider.GetRequiredService<CollectionImporter>();
|
||||
_developers = serviceProvider.GetRequiredService<DeveloperImporter>();
|
||||
_engines = serviceProvider.GetRequiredService<EngineImporter>();
|
||||
_games = serviceProvider.GetRequiredService<GameImporter>();
|
||||
_genres = serviceProvider.GetRequiredService<GenreImporter>();
|
||||
_media = serviceProvider.GetRequiredService<MediaImporter>();
|
||||
_multiplayerModes = serviceProvider.GetRequiredService<MultiplayerModeImporter>();
|
||||
_platforms = serviceProvider.GetRequiredService<PlatformImporter>();
|
||||
_publishers = serviceProvider.GetRequiredService<PublisherImporter>();
|
||||
_tags = serviceProvider.GetRequiredService<TagImporter>();
|
||||
_logger = serviceProvider.GetRequiredService<ILogger<ImportContext>>();
|
||||
|
||||
SetupContextOnImporters();
|
||||
}
|
||||
|
||||
public async Task AddAsync(Game game)
|
||||
{
|
||||
await AddAsync(game, game.Collections, _collections);
|
||||
|
|
@ -62,6 +63,11 @@ public class ImportContext
|
|||
await AddAsync(game, game, _games);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Tool tool)
|
||||
{
|
||||
await AddAsync(tool, tool, _tools);
|
||||
}
|
||||
|
||||
private async Task AddAsync<TRecord>(BaseManifest manifest, IEnumerable<TRecord> records, BaseImporter<TRecord> importer)
|
||||
where TRecord : class
|
||||
{
|
||||
|
|
@ -76,8 +82,8 @@ public class ImportContext
|
|||
{
|
||||
var importInfo = await importer.GetImportInfoAsync(record, manifest);
|
||||
|
||||
importInfo.Key = importer.GetKey(record);
|
||||
|
||||
importInfo.Key = importer.GetKey(record);
|
||||
|
||||
_logger.LogInformation("Queuing item {ItemName} for import with key {Key}", importInfo.Name, importInfo.Key);
|
||||
|
||||
Queue.Enqueue(importInfo);
|
||||
|
|
|
|||
104
LANCommander.Launcher.Services/Import/Importers/ToolImporter.cs
Normal file
104
LANCommander.Launcher.Services/Import/Importers/ToolImporter.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using LANCommander.Launcher.Services.Exceptions;
|
||||
using LANCommander.SDK.Helpers;
|
||||
using LANCommander.SDK.Models.Manifest;
|
||||
using LANCommander.SDK.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LANCommander.Launcher.Services.Import.Importers;
|
||||
|
||||
public class ToolImporter(
|
||||
ToolService toolService,
|
||||
LibraryService libraryService,
|
||||
ILogger<ToolImporter> logger) : BaseImporter<Tool>
|
||||
{
|
||||
public override async Task<ImportItemInfo<Tool>> GetImportInfoAsync(Tool record, BaseManifest manifest) =>
|
||||
new()
|
||||
{
|
||||
Key = GetKey(record),
|
||||
Name = record.Name,
|
||||
Type = nameof(Game),
|
||||
Record = record,
|
||||
};
|
||||
|
||||
public override string GetKey(Tool record) => $"{nameof(Tool)}/{record.Id}";
|
||||
|
||||
public override async Task<bool> CanImportAsync(Tool record)
|
||||
{
|
||||
var existing = await toolService.GetAsync(record.Id);
|
||||
|
||||
if (existing == null)
|
||||
return true;
|
||||
|
||||
return
|
||||
record.UpdatedOn > existing.ImportedOn
|
||||
||
|
||||
record.Actions.Any(a => a.UpdatedOn > existing.ImportedOn || a.CreatedOn > existing.ImportedOn);
|
||||
}
|
||||
|
||||
public override async Task<bool> AddAsync(ImportItemInfo<Tool> importItemInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tool = new Data.Models.Tool
|
||||
{
|
||||
Id = importItemInfo.Record.Id,
|
||||
Name = importItemInfo.Record.Name,
|
||||
Description = importItemInfo.Record.Description,
|
||||
Notes = importItemInfo.Record.Notes,
|
||||
CreatedOn = importItemInfo.Record.CreatedOn,
|
||||
UpdatedOn = importItemInfo.Record.UpdatedOn,
|
||||
ImportedOn = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
await toolService.AddAsync(tool);
|
||||
await UpdateRelationships(importItemInfo.Record);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Could not add tool | {Key}", GetKey(importItemInfo.Record));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<bool> UpdateAsync(ImportItemInfo<Tool> importItemInfo)
|
||||
{
|
||||
var existing = await toolService.GetAsync(importItemInfo.Record.Id);
|
||||
|
||||
try
|
||||
{
|
||||
existing.Name = importItemInfo.Record.Name;
|
||||
existing.Description = importItemInfo.Record.Description;
|
||||
existing.Notes = importItemInfo.Record.Notes;
|
||||
existing.CreatedOn = importItemInfo.Record.CreatedOn;
|
||||
existing.ImportedOn = DateTime.UtcNow;
|
||||
// existing.LatestVersion = importItemInfo.Record.Version;
|
||||
|
||||
await toolService.UpdateAsync(existing);
|
||||
await UpdateRelationships(importItemInfo.Record);
|
||||
|
||||
if (await libraryService.IsInstalledAsync(existing.Id) && existing.LatestVersion == existing.InstalledVersion)
|
||||
await ManifestHelper.WriteAsync(importItemInfo.Record, existing.InstallDirectory);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ImportSkippedException<Tool>(importItemInfo.Record, "An unknown error occurred while trying to update tool", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateRelationships(Tool manifest)
|
||||
{
|
||||
var tool = await toolService.GetAsync(manifest.Id);
|
||||
|
||||
await toolService.SyncRelatedCollectionAsync(
|
||||
tool,
|
||||
t => t.Games,
|
||||
manifest.Games,
|
||||
r => c => c.Title == r.Title);
|
||||
}
|
||||
|
||||
public override async Task<bool> ExistsAsync(ImportItemInfo<Tool> importItemInfo) => await toolService.ExistsAsync(importItemInfo.Record.Id);
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ namespace LANCommander.Launcher.Services
|
|||
ILogger<ImportService> logger,
|
||||
ImportContextFactory importContextFactory,
|
||||
GameClient gameClient,
|
||||
ToolClient toolClient,
|
||||
LibraryClient libraryClient) : BaseService(logger)
|
||||
{
|
||||
private ImportProgress _importProgress = new();
|
||||
|
|
@ -65,5 +66,15 @@ namespace LANCommander.Launcher.Services
|
|||
await importContext.AddAsync(manifest);
|
||||
await importContext.ImportQueueAsync();
|
||||
}
|
||||
|
||||
public async Task ImportToolAsync(Guid toolId)
|
||||
{
|
||||
var importContext = importContextFactory.Create();
|
||||
|
||||
var manifest = await toolClient.GetManifestAsync(toolId);
|
||||
|
||||
await importContext.AddAsync(manifest);
|
||||
await importContext.ImportQueueAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ namespace LANCommander.Launcher.Services
|
|||
public class InstallService : BaseService
|
||||
{
|
||||
private readonly GameService _gameService;
|
||||
private readonly ToolService _toolService;
|
||||
private readonly ImportService _importService;
|
||||
private readonly GameClient _gameClient;
|
||||
private readonly RedistributableClient _redistributableClient;
|
||||
private readonly ToolClient _toolClient;
|
||||
private readonly MediaClient _mediaClient;
|
||||
|
||||
private Stopwatch Stopwatch { get; set; }
|
||||
|
|
@ -37,13 +40,19 @@ namespace LANCommander.Launcher.Services
|
|||
public InstallService(
|
||||
ILogger<InstallService> logger,
|
||||
GameService gameService,
|
||||
ToolService toolService,
|
||||
ImportService importService,
|
||||
GameClient gameClient,
|
||||
RedistributableClient redistributableClient,
|
||||
ToolClient toolClient,
|
||||
MediaClient mediaClient) : base(logger)
|
||||
{
|
||||
_gameService = gameService;
|
||||
_toolService = toolService;
|
||||
_importService = importService;
|
||||
_gameClient = gameClient;
|
||||
_redistributableClient = redistributableClient;
|
||||
_toolClient = toolClient;
|
||||
_mediaClient = mediaClient;
|
||||
|
||||
Stopwatch = new Stopwatch();
|
||||
|
|
@ -154,6 +163,54 @@ namespace LANCommander.Launcher.Services
|
|||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Add(SDK.Models.Tool tool, string installDirectory = "")
|
||||
{
|
||||
var toolInfo = await _toolClient.GetAsync(tool.Id);
|
||||
|
||||
// TODO: Throw exception (and gracefully handle) when gameInfo == null
|
||||
// Game probably couldn't be found or deserialized from server
|
||||
|
||||
Logger?.LogTrace("Adding game {ToolName} to the queue", toolInfo.Name);
|
||||
|
||||
try
|
||||
{
|
||||
var toolCompletedQueueItems = Queue.Where(i => i.Status == InstallStatus.Complete && i.Id == tool.Id).ToList();
|
||||
|
||||
foreach (var queueItem in toolCompletedQueueItems)
|
||||
{
|
||||
Queue.Remove(queueItem);
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
if (!Queue.Any(i => i.Id == tool.Id && i.Status == InstallStatus.Queued))
|
||||
{
|
||||
var queueItem = new InstallQueueTool(toolInfo);
|
||||
|
||||
queueItem.InstallDirectory = installDirectory;
|
||||
|
||||
if (Queue.Any(i => i.State))
|
||||
Queue.Add(queueItem);
|
||||
else
|
||||
{
|
||||
Logger?.LogTrace("Download queue is empty, starting the tool download immediately");
|
||||
|
||||
queueItem.Status = InstallStatus.Starting;
|
||||
|
||||
Queue.Add(queueItem);
|
||||
|
||||
await Next();
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(Guid id)
|
||||
{
|
||||
|
|
@ -197,18 +254,27 @@ namespace LANCommander.Launcher.Services
|
|||
if (currentItem == null)
|
||||
return;
|
||||
|
||||
if (currentItem is InstallQueueGame gameQueueItem)
|
||||
await Next(gameQueueItem);
|
||||
|
||||
if (currentItem is InstallQueueTool toolQueueItem)
|
||||
await Next(toolQueueItem);
|
||||
}
|
||||
|
||||
private async Task Next(InstallQueueGame queueItem)
|
||||
{
|
||||
Game localGame = null;
|
||||
SDK.Models.Game remoteGame = null;
|
||||
|
||||
try
|
||||
{
|
||||
localGame = await _gameService.GetAsync(currentItem.Id);
|
||||
remoteGame = await _gameClient.GetAsync(currentItem.Id);
|
||||
localGame = await _gameService.GetAsync(queueItem.Id);
|
||||
remoteGame = await _gameClient.GetAsync(queueItem.Id);
|
||||
|
||||
if (localGame == null)
|
||||
{
|
||||
Logger?.LogError("Game does not exist in local database, skipping");
|
||||
Remove(currentItem);
|
||||
Remove(queueItem);
|
||||
OnQueueChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
|
@ -217,7 +283,7 @@ namespace LANCommander.Launcher.Services
|
|||
{
|
||||
Logger?.LogError("Game info could not be retrieved from the server");
|
||||
|
||||
currentItem.Status = InstallStatus.Failed;
|
||||
queueItem.Status = InstallStatus.Failed;
|
||||
OnQueueChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
|
@ -228,31 +294,31 @@ namespace LANCommander.Launcher.Services
|
|||
await _gameClient.UpdateGameInstallationAsync(localGame.InstallDirectory, remoteGame);
|
||||
|
||||
// Probably doing a modification of some sort
|
||||
if (localGame.InstallDirectory.StartsWith(currentItem.InstallDirectory))
|
||||
if (localGame.InstallDirectory.StartsWith(queueItem.InstallDirectory))
|
||||
{
|
||||
var allAddons = remoteGame.DependentGames.ToArray();
|
||||
var removeAddons = allAddons.Except(currentItem.AddonIds ?? []).ToArray();
|
||||
var addAddons = allAddons.Intersect(currentItem.AddonIds ?? []).ToArray();
|
||||
var removeAddons = allAddons.Except(queueItem.AddonIds ?? []).ToArray();
|
||||
var addAddons = allAddons.Intersect(queueItem.AddonIds ?? []).ToArray();
|
||||
|
||||
var uninstallResult = await _gameClient.UninstallAddonsAsync(localGame.InstallDirectory, localGame.Id, removeAddons);
|
||||
var installResult = await _gameClient.InstallAddonsAsync(localGame.InstallDirectory, localGame.Id, addAddons);
|
||||
await _gameClient.RestoreFilesAsync(localGame.InstallDirectory, localGame.Id, uninstallResult.FileList, installResult.FileList);
|
||||
|
||||
UpdateGameState(currentItem, localGame, localGame.InstallDirectory);
|
||||
UpdateGameState(queueItem, localGame, localGame.InstallDirectory);
|
||||
await _gameService.UpdateAsync(localGame);
|
||||
|
||||
currentItem.Status = InstallStatus.Complete;
|
||||
queueItem.Status = InstallStatus.Complete;
|
||||
OnQueueChanged?.Invoke();
|
||||
OnInstallComplete?.Invoke(localGame);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Move(currentItem, localGame, remoteGame);
|
||||
await Move(queueItem, localGame, remoteGame);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Install(currentItem, localGame, remoteGame);
|
||||
await Install(queueItem, localGame, remoteGame);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -261,7 +327,51 @@ namespace LANCommander.Launcher.Services
|
|||
}
|
||||
}
|
||||
|
||||
public async Task Install(IInstallQueueItem currentItem, Game localGame, SDK.Models.Game remoteGame)
|
||||
private async Task Next(InstallQueueTool queueItem)
|
||||
{
|
||||
Tool localTool = null;
|
||||
SDK.Models.Tool remoteTool = null;
|
||||
|
||||
try
|
||||
{
|
||||
localTool = await _toolService.GetAsync(queueItem.Id);
|
||||
remoteTool = await _toolClient.GetAsync(queueItem.Id);
|
||||
|
||||
if (remoteTool == null)
|
||||
{
|
||||
Logger?.LogError("Tool info could not be retrieved from the server");
|
||||
|
||||
queueItem.Status = InstallStatus.Failed;
|
||||
OnQueueChanged?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
if (localTool == null)
|
||||
{
|
||||
Logger?.LogError("Tool does not exist in local database, importing");
|
||||
|
||||
await _importService.ImportToolAsync(queueItem.Id);
|
||||
|
||||
await Next(queueItem);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (localTool.Installed)
|
||||
{
|
||||
// Modify
|
||||
}
|
||||
else
|
||||
{
|
||||
await Install(queueItem, localTool, remoteTool);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Install(InstallQueueGame currentItem, Game localGame, SDK.Models.Game remoteGame)
|
||||
{
|
||||
using (var operation = Logger.BeginOperation("Installing game {GameTitle} ({GameId})", localGame.Title, localGame.Id))
|
||||
{
|
||||
|
|
@ -352,7 +462,65 @@ namespace LANCommander.Launcher.Services
|
|||
await Next();
|
||||
}
|
||||
|
||||
private static void UpdateGameState(IInstallQueueItem currentItem, Game localGame, string installDirectory)
|
||||
public async Task Install(InstallQueueTool currentItem, Tool localTool, SDK.Models.Tool remoteTool)
|
||||
{
|
||||
using (var operation = Logger.BeginOperation("Installing tool {ToolName} ({ToolId})", localTool.Name, localTool.Id))
|
||||
{
|
||||
currentItem.Status = InstallStatus.Downloading;
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _toolClient.InstallAsync(remoteTool, currentItem.InstallDirectory);
|
||||
|
||||
UpdateToolState(currentItem, localTool, result.InstallDirectory);
|
||||
}
|
||||
catch (InstallCanceledException ex)
|
||||
{
|
||||
Logger?.LogError("Install canceled, removing from queue");
|
||||
Queue.Remove(currentItem);
|
||||
return;
|
||||
}
|
||||
catch (InstallException ex)
|
||||
{
|
||||
Logger?.LogError(ex, "An error occurred during install, removing from queue");
|
||||
Queue.Remove(currentItem);
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "An unknown error occurred during install, removing from queue");
|
||||
Queue.Remove(currentItem);
|
||||
return;
|
||||
}
|
||||
|
||||
currentItem.CompletedOn = DateTime.Now;
|
||||
currentItem.Status = InstallStatus.Complete;
|
||||
currentItem.Progress = 1;
|
||||
currentItem.BytesDownloaded = currentItem.TotalBytes;
|
||||
|
||||
try
|
||||
{
|
||||
await _toolService.UpdateAsync(localTool);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogError(ex, "An unknown error occurred while trying to write changes to the database after install of tool {ToolName} ({ToolId})", localTool.Name, localTool.Id);
|
||||
}
|
||||
|
||||
OnQueueChanged?.Invoke();
|
||||
|
||||
Logger?.LogTrace("Install of tool {ToolName} ({ToolId}) complete!", localTool.Name, localTool.Id);
|
||||
|
||||
// OnInstallComplete?.Invoke(localTool);
|
||||
|
||||
operation.Complete();
|
||||
}
|
||||
|
||||
await Next();
|
||||
}
|
||||
|
||||
private static void UpdateGameState(InstallQueueGame currentItem, Game localGame, string installDirectory)
|
||||
{
|
||||
localGame.InstallDirectory = installDirectory;
|
||||
localGame.Installed = true;
|
||||
|
|
@ -380,6 +548,14 @@ namespace LANCommander.Launcher.Services
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateToolState(InstallQueueTool currentItem, Tool localTool, string installDirectory)
|
||||
{
|
||||
localTool.InstallDirectory = installDirectory;
|
||||
localTool.Installed = true;
|
||||
localTool.InstalledVersion = currentItem.Version;
|
||||
localTool.InstalledOn ??= DateTime.Now;
|
||||
}
|
||||
|
||||
public async Task Move(IInstallQueueItem currentItem, Game localGame, SDK.Models.Game remoteGame)
|
||||
{
|
||||
|
|
|
|||
12
LANCommander.Launcher.Services/ToolService.cs
Normal file
12
LANCommander.Launcher.Services/ToolService.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using LANCommander.Launcher.Data;
|
||||
using LANCommander.Launcher.Data.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LANCommander.Launcher.Services
|
||||
{
|
||||
public class ToolService(
|
||||
ILogger<ToolService> logger,
|
||||
DatabaseContext dbContext) : BaseDatabaseService<Tool>(dbContext, logger)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
@namespace LANCommander.Launcher.UI
|
||||
@inherits FeedbackComponent<Models.ListItem, string>
|
||||
@inject InstallService InstallService
|
||||
@inject ToolService ToolService
|
||||
@inject LocalizationService LocalizationService
|
||||
@inject GameClient GameClient
|
||||
@inject ISettingsProvider SettingsProvider
|
||||
|
|
@ -24,6 +25,13 @@
|
|||
|
||||
<CheckboxButtonGroup @bind-Selected="SelectedAddons" DataSource="Addons" KeySelector="a => a.Id" LabelSelector="a => a.Title" Direction="SpaceDirection.Vertical" />
|
||||
}
|
||||
|
||||
@if (Tools.Any())
|
||||
{
|
||||
<Divider Text="@LocalizationService.GetString("Tools")" />
|
||||
|
||||
<CheckboxButtonGroup @bind-Selected="SelectedTools" DataSource="Tools" KeySelector="t => t.Id" LabelSelector="t => t.Name" Direction="SpaceDirection.Vertical" />
|
||||
}
|
||||
|
||||
@if (SettingsProvider.CurrentValue.Games.InstallDirectories.Length > 1)
|
||||
{
|
||||
|
|
@ -65,7 +73,8 @@
|
|||
List<SDK.Models.Game> Addons = new();
|
||||
IEnumerable<SDK.Models.Game> SelectedAddons = new List<SDK.Models.Game>();
|
||||
|
||||
|
||||
List<SDK.Models.Tool> Tools = new();
|
||||
IEnumerable<SDK.Models.Tool> SelectedTools = new List<SDK.Models.Tool>();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
|
|
@ -78,10 +87,41 @@
|
|||
SelectedDirectory = SettingsProvider.CurrentValue.Games.InstallDirectories.First();
|
||||
|
||||
RemoteGame = await GameClient.GetAsync(Options.Key);
|
||||
Addons = (await GameClient.GetAddonsAsync(Options.Key)).OrderByTitle(g => g.SortTitle ?? g.Title).ToList();
|
||||
|
||||
await LoadAddons();
|
||||
await LoadTools();
|
||||
|
||||
var localAddons = Game?.DependentGames.Where(g => g.Installed).Select(addon => addon.Id).ToArray() ?? [];
|
||||
SelectedAddons = Addons?.Where(addon => localAddons.Contains(addon.Id)).ToList() ?? [];
|
||||
|
||||
var localTools = Game?.Tools.Where(t => t.Installed).Select(tool => tool.Id).ToArray() ?? [];
|
||||
SelectedTools = Tools?.Where(tool => localTools.Contains(tool.Id)).ToList() ?? [];
|
||||
}
|
||||
|
||||
async Task LoadAddons()
|
||||
{
|
||||
try
|
||||
{
|
||||
Addons = (await GameClient.GetAddonsAsync(Options.Key)).OrderByTitle(g => g.SortTitle ?? g.Title).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: Add logging
|
||||
Addons = [];
|
||||
}
|
||||
}
|
||||
|
||||
async Task LoadTools()
|
||||
{
|
||||
try
|
||||
{
|
||||
Tools = (await GameClient.GetToolsAsync(Options.Key)).OrderByTitle(t => t.Name).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO: Add logging
|
||||
Tools = [];
|
||||
}
|
||||
}
|
||||
|
||||
async Task Close()
|
||||
|
|
@ -93,7 +133,13 @@
|
|||
{
|
||||
var game = Options.DataItem as Game;
|
||||
|
||||
InstallService.Add(game, SelectedDirectory, SelectedAddons.ToArray());
|
||||
await InstallService.Add(game, SelectedDirectory, SelectedAddons.ToArray());
|
||||
|
||||
foreach (var tool in SelectedTools)
|
||||
{
|
||||
|
||||
await InstallService.Add(tool, SelectedDirectory);
|
||||
}
|
||||
|
||||
await CloseFeedbackAsync();
|
||||
}
|
||||
|
|
@ -121,6 +167,8 @@
|
|||
size += RemoteGame.Archives.OrderByDescending(a => a.CreatedOn).First().CompressedSize;
|
||||
|
||||
size += SelectedAddons.Sum(a => a.Archives.OrderByDescending(arc => arc.CreatedOn).First().CompressedSize);
|
||||
|
||||
size += SelectedTools.Sum(t => t.Archives.OrderByDescending(arc => arc.CreatedOn).First().CompressedSize);
|
||||
}
|
||||
|
||||
return size;
|
||||
|
|
@ -135,6 +183,8 @@
|
|||
size += RemoteGame.Archives.OrderByDescending(a => a.CreatedOn).First().UncompressedSize;
|
||||
|
||||
size += SelectedAddons.Sum(a => a.Archives.OrderByDescending(arc => arc.CreatedOn).First().UncompressedSize);
|
||||
|
||||
size += SelectedTools.Sum(t => t.Archives.OrderByDescending(arc => arc.CreatedOn).First().UncompressedSize);
|
||||
}
|
||||
|
||||
return size;
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ namespace LANCommander.SDK.Services
|
|||
SaveClient saveClient,
|
||||
ScriptClient scriptClient,
|
||||
ProfileClient profileClient,
|
||||
LobbyClient lobbyClient)
|
||||
LobbyClient lobbyClient,
|
||||
ToolClient toolClient)
|
||||
{
|
||||
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
|
||||
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
|
||||
|
|
@ -236,6 +237,16 @@ namespace LANCommander.SDK.Services
|
|||
.GetAsync<IEnumerable<Game>>();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Tool>> GetToolsAsync(Guid id)
|
||||
{
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute($"/api/Games/{id}/Tools")
|
||||
.GetAsync<IEnumerable<Tool>>();
|
||||
}
|
||||
|
||||
public async Task<bool> CheckForUpdateAsync(Guid id, string currentVersion)
|
||||
{
|
||||
return await apiRequestFactory
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ namespace LANCommander.SDK.Services;
|
|||
|
||||
public partial class ScriptClient
|
||||
{
|
||||
public async Task<bool> Tool_RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Tool tool)
|
||||
public async Task<bool> Tool_RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Guid toolId)
|
||||
{
|
||||
bool result = default;
|
||||
|
||||
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
|
||||
var toolManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Tool>(installDirectory, tool.Id);
|
||||
var toolManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Tool>(installDirectory, toolId);
|
||||
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, tool.Id, Enums.ScriptType.DetectInstall);
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, toolId, Enums.ScriptType.DetectInstall);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -42,7 +42,7 @@ public partial class ScriptClient
|
|||
{
|
||||
op.Enrich("InstallDirectory", installDirectory)
|
||||
.Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId))
|
||||
.Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, tool.Id))
|
||||
.Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, toolId))
|
||||
.Enrich("ScriptPath", path)
|
||||
.Enrich("GameTitle", gameManifest.Title)
|
||||
.Enrich("GameId", gameManifest.Id)
|
||||
|
|
@ -62,7 +62,7 @@ public partial class ScriptClient
|
|||
}
|
||||
}
|
||||
|
||||
script.UseWorkingDirectory(Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, tool.Id)));
|
||||
script.UseWorkingDirectory(Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, toolId)));
|
||||
script.UseFile(path);
|
||||
|
||||
if (Debug)
|
||||
|
|
@ -102,14 +102,13 @@ public partial class ScriptClient
|
|||
return result;
|
||||
}
|
||||
|
||||
public async Task<int> Tool_RunInstallScriptAsync(string installDirectory, Guid gameId, Tool tool)
|
||||
public async Task<int> Tool_RunInstallScriptAsync(string installDirectory, Guid toolId)
|
||||
{
|
||||
int result = default;
|
||||
|
||||
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, toolId);
|
||||
|
||||
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
|
||||
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, tool.Id);
|
||||
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, tool.Id, Enums.ScriptType.Install);
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, toolId, Enums.ScriptType.Install);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -120,7 +119,6 @@ public partial class ScriptClient
|
|||
var script = powerShellScriptFactory.Create(Enums.ScriptType.Install);
|
||||
|
||||
script.AddVariable("InstallDirectory", installDirectory);
|
||||
script.AddVariable("GameManifest", gameManifest);
|
||||
script.AddVariable("ToolManifest", toolManifest);
|
||||
script.AddVariable("DefaultInstallDirectory", settingsProvider.CurrentValue.Games.InstallDirectories.FirstOrDefault());
|
||||
script.AddVariable("ServerAddress", connectionClient.GetServerAddress());
|
||||
|
|
@ -129,11 +127,8 @@ public partial class ScriptClient
|
|||
{
|
||||
op
|
||||
.Enrich("InstallDirectory", installDirectory)
|
||||
.Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId))
|
||||
.Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, tool.Id))
|
||||
.Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, toolId))
|
||||
.Enrich("ScriptPath", path)
|
||||
.Enrich("GameTitle", gameManifest.Title)
|
||||
.Enrich("GameId", gameManifest.Id)
|
||||
.Enrich("ToolName", toolManifest.Name)
|
||||
.Enrich("ToolId", toolManifest.Id);
|
||||
}
|
||||
|
|
@ -142,15 +137,7 @@ public partial class ScriptClient
|
|||
logger?.LogError(ex, "Could not enrich logs");
|
||||
}
|
||||
|
||||
if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any())
|
||||
{
|
||||
foreach (var customField in gameManifest.CustomFields)
|
||||
{
|
||||
script.AddVariable(customField.Name, customField.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, tool.Id), "Files");
|
||||
var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, toolId), "Files");
|
||||
|
||||
script.UseWorkingDirectory(extractionPath);
|
||||
script.UseFile(path);
|
||||
|
|
@ -175,41 +162,33 @@ public partial class ScriptClient
|
|||
return result;
|
||||
}
|
||||
|
||||
public async Task<int> Tool_RunBeforeStartScriptAsync(string installDirectory, Guid gameId, Tool tool)
|
||||
public async Task<int> Tool_RunBeforeStartScriptAsync(string installDirectory, Guid toolId)
|
||||
{
|
||||
int result = default;
|
||||
|
||||
try
|
||||
{
|
||||
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
|
||||
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, tool.Id);
|
||||
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, toolId);
|
||||
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, tool.Id, Enums.ScriptType.BeforeStart);
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, toolId, Enums.ScriptType.BeforeStart);
|
||||
|
||||
using (var op = logger.BeginOperation("Executing before start script"))
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var script = powerShellScriptFactory.Create(Enums.ScriptType.BeforeStart);
|
||||
var playerAlias = await GameClient.GetPlayerAliasAsync(installDirectory, gameId);
|
||||
|
||||
script.AddVariable("InstallDirectory", installDirectory);
|
||||
script.AddVariable("GameManifest", gameManifest);
|
||||
script.AddVariable("ToolManifest", toolManifest);
|
||||
script.AddVariable("DefaultInstallDirectory", settingsProvider.CurrentValue.Games.InstallDirectories.FirstOrDefault());
|
||||
script.AddVariable("ServerAddress", connectionClient.GetServerAddress());
|
||||
script.AddVariable("PlayerAlias", playerAlias);
|
||||
|
||||
try
|
||||
{
|
||||
op
|
||||
.Enrich("InstallDirectory", installDirectory)
|
||||
.Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId))
|
||||
.Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, tool.Id))
|
||||
.Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, toolId))
|
||||
.Enrich("ScriptPath", path)
|
||||
.Enrich("PlayerAlias", playerAlias)
|
||||
.Enrich("GameTitle", gameManifest.Title)
|
||||
.Enrich("GameId", gameManifest.Id)
|
||||
.Enrich("ToolName", toolManifest.Name)
|
||||
.Enrich("ToolId", toolManifest.Id);
|
||||
}
|
||||
|
|
@ -218,15 +197,7 @@ public partial class ScriptClient
|
|||
logger?.LogError(ex, "Could not enrich logs");
|
||||
}
|
||||
|
||||
if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any())
|
||||
{
|
||||
foreach (var customField in gameManifest.CustomFields)
|
||||
{
|
||||
script.AddVariable(customField.Name, customField.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, tool.Id), "Files");
|
||||
var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, toolId), "Files");
|
||||
|
||||
script.UseWorkingDirectory(extractionPath);
|
||||
script.UseFile(path);
|
||||
|
|
@ -255,16 +226,15 @@ public partial class ScriptClient
|
|||
return result;
|
||||
}
|
||||
|
||||
public async Task<int> Tool_RunAfterStopScriptAsync(string installDirectory, Guid gameId, Tool tool)
|
||||
public async Task<int> Tool_RunAfterStopScriptAsync(string installDirectory, Guid toolId)
|
||||
{
|
||||
int result = default;
|
||||
|
||||
try
|
||||
{
|
||||
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
|
||||
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, tool.Id);
|
||||
var toolManifest = await ManifestHelper.ReadAsync<Tool>(installDirectory, toolId);
|
||||
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, tool.Id, Enums.ScriptType.AfterStop);
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, toolId, Enums.ScriptType.AfterStop);
|
||||
|
||||
using (var op = logger.BeginOperation("Executing after stop script"))
|
||||
{
|
||||
|
|
@ -273,21 +243,16 @@ public partial class ScriptClient
|
|||
var script = powerShellScriptFactory.Create(Enums.ScriptType.AfterStop);
|
||||
|
||||
script.AddVariable("InstallDirectory", installDirectory);
|
||||
script.AddVariable("GameManifest", gameManifest);
|
||||
script.AddVariable("ToolManifest", toolManifest);
|
||||
script.AddVariable("DefaultInstallDirectory", settingsProvider.CurrentValue.Games.InstallDirectories.FirstOrDefault());
|
||||
script.AddVariable("ServerAddress", connectionClient.GetServerAddress());
|
||||
script.AddVariable("PlayerAlias", await GameClient.GetPlayerAliasAsync(installDirectory, gameId));
|
||||
|
||||
try
|
||||
{
|
||||
op
|
||||
.Enrich("InstallDirectory", installDirectory)
|
||||
.Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId))
|
||||
.Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, tool.Id))
|
||||
.Enrich("ToolManifestPath", ManifestHelper.GetPath(installDirectory, toolId))
|
||||
.Enrich("ScriptPath", path)
|
||||
.Enrich("GameTitle", gameManifest.Title)
|
||||
.Enrich("GameId", gameManifest.Id)
|
||||
.Enrich("ToolName", toolManifest.Name)
|
||||
.Enrich("ToolId", toolManifest.Id);
|
||||
}
|
||||
|
|
@ -296,15 +261,7 @@ public partial class ScriptClient
|
|||
logger?.LogError(ex, "Could not enrich logs");
|
||||
}
|
||||
|
||||
if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any())
|
||||
{
|
||||
foreach (var customField in gameManifest.CustomFields)
|
||||
{
|
||||
script.AddVariable(customField.Name, customField.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, tool.Id), "Files");
|
||||
var extractionPath = Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, toolId), "Files");
|
||||
|
||||
script.UseWorkingDirectory(extractionPath);
|
||||
script.UseFile(path);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Force.Crc32;
|
||||
using LANCommander.SDK.Abstractions;
|
||||
using LANCommander.SDK.Exceptions;
|
||||
using LANCommander.SDK.Factories;
|
||||
|
|
@ -16,7 +19,7 @@ using LANCommander.SDK.Factories;
|
|||
namespace LANCommander.SDK.Services
|
||||
{
|
||||
public class ToolClient(
|
||||
ILogger<ToolClient> _logger,
|
||||
ILogger<ToolClient> logger,
|
||||
ISettingsProvider settingsProvider,
|
||||
ApiRequestFactory apiRequestFactory,
|
||||
ScriptClient scriptClient,
|
||||
|
|
@ -25,13 +28,26 @@ namespace LANCommander.SDK.Services
|
|||
public delegate void OnArchiveEntryExtractionProgressHandler(object sender, ArchiveEntryExtractionProgressArgs e);
|
||||
public event OnArchiveEntryExtractionProgressHandler OnArchiveEntryExtractionProgress;
|
||||
|
||||
public delegate void OnArchiveExtractionProgressHandler(long position, long length);
|
||||
public delegate void OnArchiveExtractionProgressHandler(long position, long length, Tool tool);
|
||||
public event OnArchiveExtractionProgressHandler OnArchiveExtractionProgress;
|
||||
|
||||
public delegate void OnInstallProgressUpdateHandler(InstallProgress e);
|
||||
public event OnInstallProgressUpdateHandler OnInstallProgressUpdate;
|
||||
|
||||
private TrackableStream _transferStream;
|
||||
private IReader _reader;
|
||||
|
||||
private InstallProgress _installProgress;
|
||||
|
||||
public async Task<Tool> GetAsync(Guid id)
|
||||
{
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute($"/api/Tools/{id}")
|
||||
.GetAsync<Tool>();
|
||||
}
|
||||
|
||||
public async Task<SDK.Models.Manifest.Tool> GetManifestAsync(Guid id)
|
||||
{
|
||||
|
|
@ -57,13 +73,15 @@ namespace LANCommander.SDK.Services
|
|||
{
|
||||
foreach (var tool in game.Tools)
|
||||
{
|
||||
await InstallAsync(tool, game);
|
||||
await InstallAsync(tool, game.InstallDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InstallAsync(Tool tool, Game game, int maxAttempts = 10)
|
||||
public async Task<InstallResult> InstallAsync(Tool tool, string installDirectory, int maxAttempts = 10)
|
||||
{
|
||||
string extractTempPath = null;
|
||||
|
||||
var installResult = new InstallResult();
|
||||
|
||||
_installProgress = new InstallProgress();
|
||||
|
||||
|
|
@ -78,73 +96,99 @@ namespace LANCommander.SDK.Services
|
|||
|
||||
try
|
||||
{
|
||||
_logger?.LogTrace("Saving manifest");
|
||||
logger?.LogTrace("Saving manifest");
|
||||
|
||||
var manifest = await GetManifestAsync(tool.Id);
|
||||
|
||||
await ManifestHelper.WriteAsync(manifest, game.InstallDirectory);
|
||||
await ManifestHelper.WriteAsync(manifest, installDirectory);
|
||||
|
||||
_logger?.LogTrace("Saving scripts");
|
||||
logger?.LogTrace("Saving scripts");
|
||||
|
||||
foreach (var script in tool.Scripts)
|
||||
await ScriptHelper.SaveScriptAsync(tool, script.Type, installDirectory);
|
||||
|
||||
if (tool.Archives?.Any() ?? false)
|
||||
{
|
||||
await ScriptHelper.SaveScriptAsync(game, tool, script.Type);
|
||||
logger?.LogTrace("Archives for tool {ToolName} exist. Attempting to download...", tool.Name);
|
||||
|
||||
var result = await RetryHelper.RetryOnExceptionAsync(maxAttempts,
|
||||
TimeSpan.FromMilliseconds(500), new ExtractionResult(),
|
||||
async () =>
|
||||
{
|
||||
logger?.LogTrace("Attempting to download and extract tool");
|
||||
|
||||
return await Task.Run(async () => await DownloadAndExtractAsync(tool, installDirectory));
|
||||
});
|
||||
|
||||
if (!result.Success && !result.Canceled)
|
||||
throw new InstallException("Could not extract the tool. Retry the install or check your connection");
|
||||
else if (result.Canceled)
|
||||
throw new InstallCanceledException("Tool install canceled");
|
||||
|
||||
extractTempPath = result.Directory;
|
||||
|
||||
logger?.LogTrace("Extraction of tool successful. Extracted path is {Path}", extractTempPath);
|
||||
logger?.LogTrace("Running install script for tool {ToolName}", tool.Name);
|
||||
|
||||
await RunPostInstallScripts(installDirectory, tool);
|
||||
}
|
||||
|
||||
var installed =
|
||||
await scriptClient.RunDetectInstallScriptAsync(game.InstallDirectory, game.Id, tool.Id);
|
||||
|
||||
_logger?.LogTrace("Tool install detection returned {Result}", installed);
|
||||
|
||||
if (!installed)
|
||||
|
||||
if (tool.Archives?.Any() ?? false)
|
||||
{
|
||||
_logger?.LogTrace("Tool {ToolName} not installed", tool.Name);
|
||||
logger?.LogTrace("Archives for tool {ToolName} exist. Attempting to download...", tool.Name);
|
||||
|
||||
var result = await RetryHelper.RetryOnExceptionAsync(maxAttempts,
|
||||
TimeSpan.FromMilliseconds(500), new ExtractionResult(),
|
||||
async () =>
|
||||
{
|
||||
logger?.LogTrace("Attempting to download and extract tool");
|
||||
|
||||
return await Task.Run(async () => await DownloadAndExtractAsync(tool, installDirectory));
|
||||
});
|
||||
|
||||
if (tool.Archives?.Any() ?? false)
|
||||
{
|
||||
_logger?.LogTrace("Archives for tool {ToolName} exist. Attempting to download...", tool.Name);
|
||||
if (!result.Success && !result.Canceled)
|
||||
throw new InstallException("Could not extract the tool. Retry the install or check your connection");
|
||||
else if (result.Canceled)
|
||||
throw new InstallCanceledException("Tool install canceled");
|
||||
|
||||
var result = await RetryHelper.RetryOnExceptionAsync(maxAttempts,
|
||||
TimeSpan.FromMilliseconds(500), new ExtractionResult(),
|
||||
async () =>
|
||||
{
|
||||
_logger?.LogTrace("Attempting to download and extract tool");
|
||||
extractTempPath = result.Directory;
|
||||
|
||||
return await Task.Run(async () => await DownloadAndExtractAsync(tool, game));
|
||||
});
|
||||
|
||||
if (!result.Success && !result.Canceled)
|
||||
throw new InstallException("Could not extract the tool. Retry the install or check your connection");
|
||||
else if (result.Canceled)
|
||||
throw new InstallCanceledException("Tool install canceled");
|
||||
installResult.InstallDirectory = result.Directory;
|
||||
|
||||
extractTempPath = result.Directory;
|
||||
|
||||
_logger?.LogTrace("Extraction of tool successful. Extracted path is {Path}", extractTempPath);
|
||||
_logger?.LogTrace("Running install script for tool {ToolName}", tool.Name);
|
||||
// TODO: Verification for tool files?
|
||||
var toolFiles = result?.Files?.Where(x => !x.EntryPath.EndsWith("/")).Select(x =>
|
||||
new GameInstallationFileListEntry.FileEntry
|
||||
{
|
||||
EntryPath = x.EntryPath,
|
||||
LocalPath = x.LocalPath,
|
||||
});
|
||||
|
||||
logger?.LogTrace("Extraction of tool successful. Extracted path is {Path}", extractTempPath);
|
||||
logger?.LogTrace("Running install script for tool {ToolName}", tool.Name);
|
||||
|
||||
await RunPostInstallScripts(game, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogTrace("No archives exist for tool {ToolName}. Running install script anyway...", tool.Name);
|
||||
await RunPostInstallScripts(installDirectory, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger?.LogTrace("No archives exist for tool {ToolName}. Running install script anyway...", tool.Name);
|
||||
|
||||
await RunPostInstallScripts(game, tool);
|
||||
}
|
||||
await RunPostInstallScripts(installDirectory, tool);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Tool {Tool} failed to install", tool.Name);
|
||||
logger?.LogError(ex, "Tool {Tool} failed to install", tool.Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(extractTempPath))
|
||||
Directory.Delete(extractTempPath, true);
|
||||
}
|
||||
|
||||
return installResult;
|
||||
}
|
||||
|
||||
private async Task RunPostInstallScripts(Game game, Tool tool)
|
||||
private async Task RunPostInstallScripts(string installDirectory, Tool tool)
|
||||
{
|
||||
if (tool.Scripts != null && tool.Scripts.Any())
|
||||
{
|
||||
|
|
@ -154,104 +198,230 @@ namespace LANCommander.SDK.Services
|
|||
|
||||
try
|
||||
{
|
||||
await scriptClient.Redistributable_RunInstallScriptAsync(installDirectory, game.Id, tool.Id);
|
||||
await scriptClient.Redistributable_RunNameChangeScriptAsync(game.InstallDirectory, game.Id, tool.Id, await profileClient.GetAliasAsync());
|
||||
await scriptClient.Tool_RunInstallScriptAsync(installDirectory, tool.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Scripts failed to execute for tool {ToolName} ({GameId})", tool.Name, tool.Id);
|
||||
logger?.LogError(ex, "Scripts failed to execute for tool {ToolName} ({GameId})", tool.Name, tool.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ExtractionResult> DownloadAndExtractAsync(Tool tool, Game game)
|
||||
private async Task<ExtractionResult> DownloadAndExtractAsync(Tool tool, string destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (tool == null)
|
||||
{
|
||||
_logger?.LogTrace("Tool failed to download! No tool was specified");
|
||||
logger?.LogTrace("Tool failed to download! No tool was specified");
|
||||
throw new ArgumentNullException(nameof(tool));
|
||||
}
|
||||
|
||||
var destination = Path.Combine(GameClient.GetMetadataDirectoryPath(game.InstallDirectory, tool.Id), "Files");
|
||||
var files = new List<ExtractionResult.FileEntry>();
|
||||
logger?.LogTrace("Downloading and extracting {Tool} to path {Destination}", tool.Name, destination);
|
||||
|
||||
_logger?.LogTrace("Downloading and extracting {Tool} to path {Destination}", tool.Name, destination);
|
||||
var extractionResult = new ExtractionResult
|
||||
{
|
||||
Canceled = false,
|
||||
};
|
||||
|
||||
if (!await CanStreamLatestArchiveAsync(tool.Id))
|
||||
{
|
||||
extractionResult.Success = false;
|
||||
extractionResult.Canceled = true;
|
||||
|
||||
return extractionResult;
|
||||
}
|
||||
|
||||
var fileManifest = new StringBuilder();
|
||||
var files = new List<ExtractionResult.FileEntry>();
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(destination);
|
||||
|
||||
using (var toolStream = await Stream(tool.Id))
|
||||
using (var reader = ReaderFactory.Open(toolStream))
|
||||
using (var monitor = new FileTransferMonitor(toolStream.Length))
|
||||
var stream = await StreamLatestArchiveAsync(tool.Id);
|
||||
|
||||
_reader = ReaderFactory.Open(stream);
|
||||
|
||||
using (var monitor = new FileTransferMonitor(stream.Length))
|
||||
{
|
||||
/*(toolStream.OnProgress += (pos, len) =>
|
||||
_reader.EntryExtractionProgress += (sender, e) =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_reader.Cancel();
|
||||
|
||||
_installProgress.Status = InstallStatus.Canceled;
|
||||
|
||||
OnInstallProgressUpdate?.Invoke(_installProgress);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.CanUpdate())
|
||||
{
|
||||
monitor.Update(pos);
|
||||
monitor.Update(stream.Position);
|
||||
|
||||
_installProgress.BytesTransferred = monitor.GetBytesTransferred();
|
||||
_installProgress.TotalBytes = len;
|
||||
_installProgress.TotalBytes = stream.Length;
|
||||
_installProgress.TransferSpeed = monitor.GetSpeed();
|
||||
_installProgress.TimeRemaining = monitor.GetTimeRemaining();
|
||||
|
||||
|
||||
OnInstallProgressUpdate?.Invoke(_installProgress);
|
||||
}
|
||||
};*/
|
||||
|
||||
reader.EntryExtractionProgress += (sender, e) =>
|
||||
{
|
||||
files.Add(new ExtractionResult.FileEntry
|
||||
{
|
||||
EntryPath = e.Item.Key,
|
||||
LocalPath = Path.Combine(destination, e.Item.Key),
|
||||
});
|
||||
|
||||
OnArchiveEntryExtractionProgress?.Invoke(this, new ArchiveEntryExtractionProgressArgs
|
||||
{
|
||||
Entry = e.Item,
|
||||
Progress = e.ReaderProgress,
|
||||
Game = null,
|
||||
});
|
||||
};
|
||||
|
||||
reader.WriteAllToDirectory(destination, new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
});
|
||||
}
|
||||
|
||||
while (_reader.MoveToNextEntry())
|
||||
{
|
||||
if (_reader.Cancelled)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
var localFile = Path.Combine(destination, _reader.Entry.Key);
|
||||
|
||||
uint crc = 0;
|
||||
|
||||
if (File.Exists(localFile))
|
||||
{
|
||||
using (FileStream fs = File.Open(localFile, FileMode.Open))
|
||||
{
|
||||
var buffer = new byte[65536];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var count = fs.Read(buffer, 0, buffer.Length);
|
||||
|
||||
if (count == 0)
|
||||
break;
|
||||
|
||||
crc = Crc32Algorithm.Append(crc, buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileManifest.AppendLine($"{_reader.Entry.Key} | {_reader.Entry.Crc.ToString("X")}");
|
||||
files.Add(new ExtractionResult.FileEntry
|
||||
{
|
||||
EntryPath = _reader.Entry.Key,
|
||||
LocalPath = localFile,
|
||||
});
|
||||
|
||||
if (crc == 0 || crc != _reader.Entry.Crc)
|
||||
_reader.WriteEntryToDirectory(destination, new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true,
|
||||
PreserveFileTime = true
|
||||
});
|
||||
else // Skip to next entry
|
||||
try
|
||||
{
|
||||
_reader.OpenEntryStream().Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger?.LogError("Could not skip to next entry in archive");
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
var errorCode = ex.HResult & 0xFFFF;
|
||||
|
||||
if (errorCode == 87)
|
||||
throw ex;
|
||||
else
|
||||
logger?.LogTrace("Not replacing existing file/folder on disk: {Message}", ex.Message);
|
||||
|
||||
// Skip to next entry
|
||||
_reader.OpenEntryStream().Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
_reader.Dispose();
|
||||
await stream.DisposeAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (ReaderCancelledException ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Could not extract to path {Destination}", destination);
|
||||
logger?.LogTrace(ex, "User cancelled the download");
|
||||
|
||||
extractionResult.Canceled = true;
|
||||
|
||||
if (Directory.Exists(destination))
|
||||
{
|
||||
_logger?.LogTrace("Cleaning up orphaned files after bad install");
|
||||
logger?.LogTrace("Cleaning up ophaned files after cancelled install");
|
||||
|
||||
Directory.Delete(destination, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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 tool archive could not be extracted, is it corrupted? Please try again");
|
||||
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;
|
||||
extractionResult.Files = files;
|
||||
_logger?.LogTrace("Tool {Tool} successfully downloaded and extracted to {Destination}", tool.Name, destination);
|
||||
|
||||
var fileListDestination = Path.Combine(destination, ".lancommander", tool.Id.ToString(), "FileList.txt");
|
||||
|
||||
if (!Directory.Exists(Path.GetDirectoryName(fileListDestination)))
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fileListDestination));
|
||||
|
||||
File.WriteAllText(fileListDestination, fileManifest.ToString());
|
||||
|
||||
logger?.LogTrace("Tool {Tool} successfully downloaded and extracted to {Destination}", tool.Name, destination);
|
||||
}
|
||||
|
||||
return extractionResult;
|
||||
}
|
||||
|
||||
private async Task<bool> CanStreamLatestArchiveAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute($"/api/Tools/{id}/Download")
|
||||
.HeadAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TrackableStream> StreamLatestArchiveAsync(Guid id)
|
||||
{
|
||||
return await apiRequestFactory
|
||||
.Create()
|
||||
.UseAuthenticationToken()
|
||||
.UseVersioning()
|
||||
.UseRoute($"/api/Tools/{id}/Download")
|
||||
.StreamAsync();
|
||||
}
|
||||
|
||||
public async Task ImportAsync(string archivePath)
|
||||
{
|
||||
using (var fs = new FileStream(archivePath, FileMode.Open, FileAccess.Read))
|
||||
|
|
|
|||
|
|
@ -82,23 +82,23 @@ namespace LANCommander.SDK.Helpers
|
|||
}
|
||||
}
|
||||
|
||||
public static async Task SaveScriptAsync(Game game, Tool tool, ScriptType type)
|
||||
public static async Task SaveScriptAsync(Tool tool, ScriptType type, string installDirectory)
|
||||
{
|
||||
var scriptContents = GetScriptContents(tool, type);
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(scriptContents))
|
||||
{
|
||||
var fileName = GetScriptFilePath(game.InstallDirectory, tool.Id, type);
|
||||
var filename = GetScriptFilePath(installDirectory, tool.Id, type);
|
||||
|
||||
if (!Directory.Exists(Path.GetDirectoryName(fileName)))
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fileName));
|
||||
|
||||
if (File.Exists(fileName))
|
||||
File.Delete(fileName);
|
||||
|
||||
Logger?.LogTrace("Writing {ScriptType} script to {Destination}", type, fileName);
|
||||
if (!Directory.Exists(Path.GetDirectoryName(filename)))
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(filename));
|
||||
|
||||
await File.WriteAllTextAsync(fileName, scriptContents);
|
||||
if (File.Exists(filename))
|
||||
File.Delete(filename);
|
||||
|
||||
Logger?.LogTrace("Writing {ScriptType} script to {Destination}", type, filename);
|
||||
|
||||
await File.WriteAllTextAsync(filename, scriptContents);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ namespace LANCommander.SDK.Models.Manifest
|
|||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public DateTime ReleasedOn { get; set; }
|
||||
public virtual ICollection<Action> Actions { get; set; } = new List<Action>();
|
||||
public virtual ICollection<Archive> Archives { get; set; } = new List<Archive>();
|
||||
public virtual ICollection<Script> Scripts { get; set; } = new List<Script>();
|
||||
public virtual ICollection<Game> Games { get; set; } = new List<Game>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue