From d38432f675a7ac8a4e5a386e57f27f5b7e6d594f Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 23 Feb 2025 15:39:06 -0600 Subject: [PATCH] Add server scripts to execute on game start/stop, refactor how play sessions are recorded --- .../PlaySessionService.cs | 4 +- LANCommander.SDK/Enums/ScriptType.cs | 4 + LANCommander.SDK/Helpers/RetryHelper.cs | 51 ++++++++ LANCommander.SDK/Services/GameService.cs | 35 ++++-- .../PlaySessionService.cs | 19 --- LANCommander.Server.Services/ServerService.cs | 72 ++++++++++- .../Controllers/Api/GamesController.cs | 117 +++++++++++++++++- .../UI/Pages/Servers/Edit/Scripts.razor | 2 +- 8 files changed, 269 insertions(+), 35 deletions(-) diff --git a/LANCommander.Launcher.Services/PlaySessionService.cs b/LANCommander.Launcher.Services/PlaySessionService.cs index a9dad2c0..4a36fe50 100644 --- a/LANCommander.Launcher.Services/PlaySessionService.cs +++ b/LANCommander.Launcher.Services/PlaySessionService.cs @@ -40,7 +40,7 @@ namespace LANCommander.Launcher.Services await AddAsync(session); if (Client.IsConnected()) - await Client.Games.StartPlaySessionAsync(gameId); + await Client.Games.StartedAsync(gameId); } catch (Exception ex) { @@ -67,7 +67,7 @@ namespace LANCommander.Launcher.Services } finally { - await Client.Games.EndPlaySessionAsync(gameId); + await Client.Games.StoppedAsync(gameId); } } } diff --git a/LANCommander.SDK/Enums/ScriptType.cs b/LANCommander.SDK/Enums/ScriptType.cs index 6809a18c..bd529aa7 100644 --- a/LANCommander.SDK/Enums/ScriptType.cs +++ b/LANCommander.SDK/Enums/ScriptType.cs @@ -21,6 +21,10 @@ namespace LANCommander.SDK.Enums BeforeStart, [Display(Name = "After Stop")] AfterStop, + [Display(Name = "Game Started")] + GameStarted, + [Display(Name = "Game Ended")] + GameStopped, [Display(Name = "User Registration")] UserRegistration, [Display(Name = "User Login")] diff --git a/LANCommander.SDK/Helpers/RetryHelper.cs b/LANCommander.SDK/Helpers/RetryHelper.cs index a9b44d86..b1fccbd7 100644 --- a/LANCommander.SDK/Helpers/RetryHelper.cs +++ b/LANCommander.SDK/Helpers/RetryHelper.cs @@ -33,6 +33,57 @@ namespace LANCommander.SDK.Helpers } } while (true); } + + internal static void RetryOnException(int maxAttempts, TimeSpan delay, Action action) + { + int attempts = 0; + + do + { + try + { + Logger?.LogTrace($"Attempt #{attempts + 1}/{maxAttempts}..."); + + attempts++; + action(); + } + catch (Exception ex) + { + Logger?.LogError(ex, $"Attempt failed!"); + + if (attempts >= maxAttempts) + return; + + Task.Delay(delay).Wait(); + } + } while (true); + } + + internal static async Task RetryOnExceptionAsync(int maxAttempts, TimeSpan delay, Func action) + { + int attempts = 0; + + do + { + try + { + Logger?.LogTrace($"Attempt #{attempts + 1}/{maxAttempts}..."); + + attempts++; + + await action(); + } + catch (Exception ex) + { + Logger?.LogError(ex, $"Attempt failed!"); + + if (attempts >= maxAttempts) + return; + + Task.Delay(delay).Wait(); + } + } while (true); + } internal static async Task RetryOnExceptionAsync(int maxAttempts, TimeSpan delay, T @default, Func> action) { diff --git a/LANCommander.SDK/Services/GameService.cs b/LANCommander.SDK/Services/GameService.cs index 30ef5a38..c02dc3f3 100644 --- a/LANCommander.SDK/Services/GameService.cs +++ b/LANCommander.SDK/Services/GameService.cs @@ -202,24 +202,37 @@ namespace LANCommander.SDK.Services return Client.StreamRequest($"/api/Games/{id}/Download"); } - public async Task StartPlaySessionAsync(Guid id) + public async Task StartedAsync(Guid id) { - Logger?.LogTrace("Starting a game session..."); - - await Client.PostRequestAsync($"/api/PlaySessions/Start/{id}"); - } - - public async Task EndPlaySessionAsync(Guid id) - { - Logger?.LogTrace("Ending a game session..."); + Logger?.LogTrace("Signaling to the server that we started the game..."); try { - await Client.PostRequestAsync($"/api/PlaySessions/End/{id}"); + await RetryHelper.RetryOnExceptionAsync(10, TimeSpan.FromMilliseconds(500), async () => + { + await Client.PostRequestAsync($"/api/Game/{id}/Started"); + }); } catch (Exception ex) { - Logger?.LogError(ex, "Failed sending end session request to server"); + Logger?.LogError(ex, "Failed sending start request to server"); + } + } + + public async Task StoppedAsync(Guid id) + { + Logger?.LogTrace("Signaling to the server that we stopped the game..."); + + try + { + await RetryHelper.RetryOnExceptionAsync(10, TimeSpan.FromMilliseconds(500), async () => + { + await Client.PostRequestAsync($"/api/Game/{id}/Stopped"); + }); + } + catch (Exception ex) + { + Logger?.LogError(ex, "Failed sending stop request to server"); } } diff --git a/LANCommander.Server.Services/PlaySessionService.cs b/LANCommander.Server.Services/PlaySessionService.cs index c57f971a..ac4e6a6f 100644 --- a/LANCommander.Server.Services/PlaySessionService.cs +++ b/LANCommander.Server.Services/PlaySessionService.cs @@ -51,13 +51,6 @@ namespace LANCommander.Server.Services }; await AddAsync(session); - - var servers = await serverService.GetAsync(s => s.GameId == gameId && s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnPlayerActivity); - - foreach (var server in servers) - { - serverProcessService.StartServerAsync(server.Id); - } } public async Task EndSessionAsync(Guid gameId, Guid userId) @@ -70,18 +63,6 @@ namespace LANCommander.Server.Services await UpdateAsync(existingSession); } - - var activeSessions = (await GetAsync(ps => ps.GameId == gameId && ps.End == null)).Any(); - - if (!activeSessions) - { - var servers = await serverService.GetAsync(s => s.GameId == gameId && s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnPlayerActivity); - - foreach (var server in servers) - { - serverProcessService.StopServerAsync(server.Id); - } - } } } } diff --git a/LANCommander.Server.Services/ServerService.cs b/LANCommander.Server.Services/ServerService.cs index bb18dc5e..1d5c6a0a 100644 --- a/LANCommander.Server.Services/ServerService.cs +++ b/LANCommander.Server.Services/ServerService.cs @@ -1,5 +1,8 @@ using LANCommander.Server.Data; using AutoMapper; +using LANCommander.SDK; +using LANCommander.SDK.Enums; +using LANCommander.SDK.PowerShell; using Microsoft.Extensions.Logging; using LANCommander.Server.Services.Extensions; using Microsoft.AspNetCore.Http; @@ -13,7 +16,8 @@ namespace LANCommander.Server.Services IFusionCache cache, IMapper mapper, IHttpContextAccessor httpContextAccessor, - IDbContextFactory contextFactory) : BaseDatabaseService(logger, cache, mapper, httpContextAccessor, contextFactory) + IDbContextFactory contextFactory, + UserService userService) : BaseDatabaseService(logger, cache, mapper, httpContextAccessor, contextFactory) { public override async Task AddAsync(Data.Models.Server entity) { @@ -44,5 +48,71 @@ namespace LANCommander.Server.Services await context.UpdateRelationshipAsync(s => s.ServerConsoles); }); } + + public async Task RunGameStartedScriptsAsync(Guid serverId, Guid userId) + { + var user = await userService.GetAsync(userId); + var server = await + Include(s => s.Game) + .Include(s => s.Scripts) + .FirstOrDefaultAsync(s => s.Id == serverId); + + foreach (var script in server.Scripts.Where(s => s.Type == ScriptType.GameStarted)) + { + try + { + var scriptContext = new PowerShellScript(ScriptType.GameStarted); + + scriptContext.AddVariable("Server", mapper.Map(server)); + scriptContext.AddVariable("Game", mapper.Map(server.Game)); + scriptContext.AddVariable("User", mapper.Map(user)); + + scriptContext.UseWorkingDirectory(server.WorkingDirectory); + scriptContext.UseInline(script.Contents); + scriptContext.UseShellExecute(); + + _logger?.LogInformation("Executing script \"{ScriptName}\"", script.Name); + + await scriptContext.ExecuteAsync(); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error running script \"{ScriptName}\" for server \"{ServerName}\"", script.Name, server.Name); + } + } + } + + public async Task RunGameStoppedScriptsAsync(Guid serverId, Guid userId) + { + var user = await userService.GetAsync(userId); + var server = await + Include(s => s.Game) + .Include(s => s.Scripts) + .FirstOrDefaultAsync(s => s.Id == serverId); + + foreach (var script in server.Scripts.Where(s => s.Type == ScriptType.GameStopped)) + { + try + { + var scriptContext = new PowerShellScript(ScriptType.GameStopped); + + scriptContext.AddVariable("Server", mapper.Map(server)); + scriptContext.AddVariable("Game", mapper.Map(server.Game)); + scriptContext.AddVariable("User", mapper.Map(user)); + + scriptContext.UseWorkingDirectory(server.WorkingDirectory); + scriptContext.UseInline(script.Contents); + scriptContext.UseShellExecute(); + + _logger?.LogInformation("Executing script \"{ScriptName}\"", script.Name); + + await scriptContext.ExecuteAsync(); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error running script \"{ScriptName}\" for server \"{ServerName}\"", script.Name, server.Name); + } + } + } } } diff --git a/LANCommander.Server/Controllers/Api/GamesController.cs b/LANCommander.Server/Controllers/Api/GamesController.cs index 04c16665..5f020258 100644 --- a/LANCommander.Server/Controllers/Api/GamesController.cs +++ b/LANCommander.Server/Controllers/Api/GamesController.cs @@ -24,6 +24,9 @@ namespace LANCommander.Server.Controllers.Api private readonly StorageLocationService StorageLocationService; private readonly ArchiveService ArchiveService; private readonly UserService UserService; + private readonly PlaySessionService PlaySessionService; + private readonly ServerService ServerService; + private readonly ServerProcessService ServerProcessService; private readonly IFusionCache Cache; private readonly IMapper Mapper; @@ -36,7 +39,10 @@ namespace LANCommander.Server.Controllers.Api LibraryService libraryService, StorageLocationService storageLocationService, ArchiveService archiveService, - UserService userService) : base(logger) + UserService userService, + PlaySessionService playSessionService, + ServerService serverService, + ServerProcessService serverProcessService) : base(logger) { GameService = gameService; ImportService = importService; @@ -44,6 +50,9 @@ namespace LANCommander.Server.Controllers.Api StorageLocationService = storageLocationService; ArchiveService = archiveService; UserService = userService; + PlaySessionService = playSessionService; + ServerService = serverService; + ServerProcessService = serverProcessService; Cache = cache; Mapper = mapper; } @@ -177,6 +186,112 @@ namespace LANCommander.Server.Controllers.Api return addons; } + [HttpGet("{id}/Started")] + public async Task StartedAsync(Guid id) + { + var user = await UserService.GetAsync(User?.Identity?.Name); + var game = await GameService.GetAsync(id); + + if (game == null || user == null) + return BadRequest(); + + #region Start recording play session + var activeSessions = await PlaySessionService + .Include(ps => ps.Game) + .GetAsync(ps => ps.UserId == user.Id && ps.End == null); + + foreach (var activeSession in activeSessions) + await PlaySessionService.EndSessionAsync(game.Id, activeSession.UserId); + + await PlaySessionService.StartSessionAsync(game.Id, user.Id); + #endregion + + #region Autostart Servers + try + { + var servers = await ServerService.GetAsync(s => + s.GameId == game.Id && s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnPlayerActivity); + + foreach (var server in servers) + { + ServerProcessService.StartServerAsync(server.Id); + } + } + catch (Exception ex) + { + Logger?.LogError(ex, "Servers could not be autostarted"); + } + #endregion + + #region Run server scripts + + try + { + var servers = await ServerService + .GetAsync(s => s.GameId == game.Id); + + foreach (var server in servers) + { + await ServerService.RunGameStartedScriptsAsync(server.Id, user.Id); + } + } + catch (Exception ex) + { + Logger?.LogError(ex, "Server scripts could not run"); + } + #endregion + + return Ok(); + } + + [HttpGet("{id}/Stopped")] + public async Task StoppedAsync(Guid id) + { + var user = await UserService.GetAsync(User?.Identity?.Name); + var game = await GameService.GetAsync(id); + + if (game == null || user == null) + return BadRequest(); + + await PlaySessionService.EndSessionAsync(game.Id, user.Id); + + #region Autostart Servers + try + { + var servers = await ServerService.GetAsync(s => + s.GameId == game.Id && s.Autostart && s.AutostartMethod == ServerAutostartMethod.OnPlayerActivity); + + foreach (var server in servers) + { + ServerProcessService.StartServerAsync(server.Id); + } + } + catch (Exception ex) + { + Logger?.LogError(ex, "Servers could not be autostarted"); + } + #endregion + + #region Run server scripts + try + { + var servers = await ServerService + .GetAsync(s => s.GameId == game.Id); + + foreach (var server in servers) + { + await ServerService.RunGameStoppedScriptsAsync(server.Id, user.Id); + } + } + catch (Exception ex) + { + Logger?.LogError(ex, "Server scripts could not run"); + } + #endregion + + return Ok(); + } + [HttpGet("{id}/CheckForUpdate")] public async Task CheckForUpdateAsync(Guid id, string version) { diff --git a/LANCommander.Server/UI/Pages/Servers/Edit/Scripts.razor b/LANCommander.Server/UI/Pages/Servers/Edit/Scripts.razor index 11afcc9a..8107e40f 100644 --- a/LANCommander.Server/UI/Pages/Servers/Edit/Scripts.razor +++ b/LANCommander.Server/UI/Pages/Servers/Edit/Scripts.razor @@ -26,7 +26,7 @@ - +