Add server scripts to execute on game start/stop, refactor how play sessions are recorded

This commit is contained in:
Pat Hartl 2025-02-23 15:39:06 -06:00
parent 369f856a5c
commit d38432f675
8 changed files with 269 additions and 35 deletions

View file

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

View file

@ -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")]

View file

@ -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<Task> 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<T> RetryOnExceptionAsync<T>(int maxAttempts, TimeSpan delay, T @default, Func<Task<T>> action)
{

View file

@ -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<object>($"/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<object>($"/api/PlaySessions/End/{id}");
await RetryHelper.RetryOnExceptionAsync(10, TimeSpan.FromMilliseconds(500), async () =>
{
await Client.PostRequestAsync<object>($"/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<object>($"/api/Game/{id}/Stopped");
});
}
catch (Exception ex)
{
Logger?.LogError(ex, "Failed sending stop request to server");
}
}

View file

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

View file

@ -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<DatabaseContext> contextFactory) : BaseDatabaseService<Data.Models.Server>(logger, cache, mapper, httpContextAccessor, contextFactory)
IDbContextFactory<DatabaseContext> contextFactory,
UserService userService) : BaseDatabaseService<Data.Models.Server>(logger, cache, mapper, httpContextAccessor, contextFactory)
{
public override async Task<Data.Models.Server> 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<SDK.Models.Server>(server));
scriptContext.AddVariable("Game", mapper.Map<SDK.Models.Game>(server.Game));
scriptContext.AddVariable("User", mapper.Map<SDK.Models.User>(user));
scriptContext.UseWorkingDirectory(server.WorkingDirectory);
scriptContext.UseInline(script.Contents);
scriptContext.UseShellExecute();
_logger?.LogInformation("Executing script \"{ScriptName}\"", script.Name);
await scriptContext.ExecuteAsync<int>();
}
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<SDK.Models.Server>(server));
scriptContext.AddVariable("Game", mapper.Map<SDK.Models.Game>(server.Game));
scriptContext.AddVariable("User", mapper.Map<SDK.Models.User>(user));
scriptContext.UseWorkingDirectory(server.WorkingDirectory);
scriptContext.UseInline(script.Contents);
scriptContext.UseShellExecute();
_logger?.LogInformation("Executing script \"{ScriptName}\"", script.Name);
await scriptContext.ExecuteAsync<int>();
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error running script \"{ScriptName}\" for server \"{ServerName}\"", script.Name, server.Name);
}
}
}
}
}

View file

@ -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<IActionResult> 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<IActionResult> 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<bool> CheckForUpdateAsync(Guid id, string version)
{

View file

@ -26,7 +26,7 @@
</TitleExtraTemplate>
<ChildContent>
<ScriptEditor ServerId="@context.Id" ArchiveId="@context.Game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault().Id" AllowedTypes="new[] { ScriptType.BeforeStart, ScriptType.AfterStop }" />
<ScriptEditor ServerId="@context.Id" ArchiveId="@context.Game.Archives.OrderByDescending(a => a.CreatedOn).FirstOrDefault().Id" AllowedTypes="new[] { ScriptType.BeforeStart, ScriptType.AfterStop, ScriptType.GameStarted, ScriptType.GameStopped }" />
</ChildContent>
</ServerEditView>