Implement RunWrapper script execution for redistributables
RunWrapper scripts were defined as a script type but never executed. This adds full execution support during game launch, with proper game running state tracking and cancellation/stop support including child process cleanup.
This commit is contained in:
parent
5b3278685e
commit
68b112d6ce
5 changed files with 196 additions and 7 deletions
|
|
@ -2085,11 +2085,55 @@ namespace LANCommander.SDK.Services
|
|||
try
|
||||
{
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var task = context.ExecuteGameActionAsync(installDirectory, gameId, action, "", cancellationTokenSource.Token);
|
||||
|
||||
_running[gameId] = cancellationTokenSource;
|
||||
|
||||
await task;
|
||||
#region Run Wrapper Scripts
|
||||
bool runWrapperHandled = false;
|
||||
|
||||
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
|
||||
var resolvedAction = action ?? gameManifest.Actions.FirstOrDefault(a => a.IsPrimaryAction);
|
||||
|
||||
if (resolvedAction != null && gameManifest.Redistributables != null)
|
||||
{
|
||||
var wrapperRedistributables = gameManifest.Redistributables
|
||||
.Where(r => r.Scripts != null && r.Scripts.Any(s => s.Type == Enums.ScriptType.RunWrapper))
|
||||
.ToList();
|
||||
|
||||
if (wrapperRedistributables.Any())
|
||||
{
|
||||
if (gameManifest.CustomFields != null && gameManifest.CustomFields.Any())
|
||||
{
|
||||
foreach (var customField in gameManifest.CustomFields)
|
||||
{
|
||||
context.AddVariable(customField.Name, customField.Value);
|
||||
}
|
||||
}
|
||||
|
||||
var executablePath = context.ExpandVariables(resolvedAction.Path, installDirectory);
|
||||
var arguments = context.ExpandVariables(resolvedAction.Arguments, installDirectory, skipSlashes: true);
|
||||
var workingDirectory = context.ExpandVariables(resolvedAction.WorkingDirectory, installDirectory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(workingDirectory))
|
||||
workingDirectory = installDirectory;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(args))
|
||||
arguments = string.IsNullOrWhiteSpace(arguments) ? args : arguments + " " + args;
|
||||
|
||||
foreach (var redistributable in wrapperRedistributables)
|
||||
{
|
||||
runWrapperHandled = await scriptClient.Redistributable_RunRunWrapperScriptAsync(installDirectory, gameId, redistributable.Id, executablePath, arguments, workingDirectory, cancellationTokenSource.Token);
|
||||
|
||||
if (runWrapperHandled)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
if (!runWrapperHandled)
|
||||
{
|
||||
await context.ExecuteGameActionAsync(installDirectory, gameId, action, args, cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
_running.Remove(gameId);
|
||||
cancellationTokenSource.Dispose();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using LANCommander.SDK.Models;
|
|||
using LANCommander.SDK.PowerShell;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
|
@ -425,6 +427,126 @@ public partial class ScriptClient
|
|||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> Redistributable_RunRunWrapperScriptAsync(string installDirectory, Guid gameId, Guid redistributableId, string executablePath, string arguments, string workingDirectory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
try
|
||||
{
|
||||
var gameManifest = await ManifestHelper.ReadAsync<SDK.Models.Manifest.Game>(installDirectory, gameId);
|
||||
var redistributableManifest = await ManifestHelper.ReadAsync<Redistributable>(installDirectory, redistributableId);
|
||||
|
||||
var path = ScriptHelper.GetScriptFilePath(installDirectory, redistributableId, Enums.ScriptType.RunWrapper);
|
||||
|
||||
using (var op = logger.BeginOperation("Executing run wrapper script"))
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var script = powerShellScriptFactory.Create(Enums.ScriptType.RunWrapper);
|
||||
|
||||
script.AddVariable("InstallDirectory", installDirectory);
|
||||
script.AddVariable("GameManifest", gameManifest);
|
||||
script.AddVariable("RedistributableManifest", redistributableManifest);
|
||||
script.AddVariable("DefaultInstallDirectory", settingsProvider.CurrentValue.Games.InstallDirectories.FirstOrDefault());
|
||||
script.AddVariable("ServerAddress", connectionClient.GetServerAddress());
|
||||
script.AddVariable("ExecutablePath", executablePath);
|
||||
script.AddVariable("Arguments", arguments);
|
||||
script.AddVariable("WorkingDirectory", workingDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
op
|
||||
.Enrich("InstallDirectory", installDirectory)
|
||||
.Enrich("GameManifestPath", ManifestHelper.GetPath(installDirectory, gameId))
|
||||
.Enrich("RedistributableManifestPath", ManifestHelper.GetPath(installDirectory, redistributableId))
|
||||
.Enrich("ScriptPath", path)
|
||||
.Enrich("ExecutablePath", executablePath)
|
||||
.Enrich("GameTitle", gameManifest.Title)
|
||||
.Enrich("GameId", gameManifest.Id)
|
||||
.Enrich("RedistributableName", redistributableManifest.Name)
|
||||
.Enrich("RedistributableId", redistributableManifest.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
script.UseWorkingDirectory(Path.Combine(GameClient.GetMetadataDirectoryPath(installDirectory, redistributableId)));
|
||||
script.UseFile(path);
|
||||
|
||||
if (Debug)
|
||||
script.EnableDebug();
|
||||
|
||||
var handled = await RunScriptExternallyAsync(script);
|
||||
|
||||
if (!handled)
|
||||
{
|
||||
// Snapshot existing process IDs so we can identify child processes on stop
|
||||
var existingPids = new HashSet<int>(Process.GetProcesses().Select(p => p.Id));
|
||||
|
||||
using (var registration = cancellationToken.Register(() =>
|
||||
{
|
||||
logger?.LogTrace("Stopping run wrapper script due to cancellation");
|
||||
script.Stop();
|
||||
|
||||
// Kill any processes spawned during script execution
|
||||
try
|
||||
{
|
||||
var currentProcesses = Process.GetProcesses();
|
||||
|
||||
foreach (var proc in currentProcesses)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!existingPids.Contains(proc.Id) && !proc.HasExited)
|
||||
{
|
||||
logger?.LogTrace("Killing child process {ProcessId} ({ProcessName})", proc.Id, proc.ProcessName);
|
||||
proc.Kill(true);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
proc.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogWarning(ex, "Error killing child processes after run wrapper cancellation");
|
||||
}
|
||||
}))
|
||||
{
|
||||
await script.ExecuteAsync<int>();
|
||||
}
|
||||
}
|
||||
|
||||
result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger?.LogTrace("No run wrapper script found");
|
||||
}
|
||||
|
||||
op.Complete();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Ran into an unexpected error when attempting to run a Run Wrapper script");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<Package> Redistributable_RunPackageScriptAsync(Script packageScript, Redistributable redistributable)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -144,6 +144,18 @@ namespace LANCommander.SDK.PowerShell
|
|||
return this;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
Context?.Stop();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogWarning(ex, "Error stopping PowerShell pipeline");
|
||||
}
|
||||
}
|
||||
|
||||
public PowerShellScript AsAdmin()
|
||||
{
|
||||
RunAsAdmin = true;
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@
|
|||
(Options.ToolId.HasValue && Options.ToolId != Guid.Empty) ||
|
||||
(Options.RedistributableId.HasValue && Options.RedistributableId != Guid.Empty);
|
||||
|
||||
static readonly string[] GameClientScriptTypes = ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall"];
|
||||
static readonly string[] GameClientScriptTypes = ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall", "RunWrapper"];
|
||||
|
||||
static readonly Dictionary<string, string[]> VariablesByScriptType = new()
|
||||
{
|
||||
|
|
@ -154,6 +154,7 @@
|
|||
["GameStopped"] = ["$WorkingDirectory", "$Server", "$Game", "$User", "$ServerId", "$ServerName", "$ServerHost", "$ServerPort", "$GameTitle", "$GameId"],
|
||||
["UserRegistration"] = ["$WorkingDirectory", "$User"],
|
||||
["UserLogin"] = ["$WorkingDirectory", "$User"],
|
||||
["RunWrapper"] = ["$InstallDirectory", "$WorkingDirectory", "$ServerAddress", "$DefaultInstallDirectory", "$GameManifest", "$RedistributableManifest", "$ExecutablePath", "$Arguments"],
|
||||
};
|
||||
|
||||
void UpdateVariables()
|
||||
|
|
|
|||
|
|
@ -16,14 +16,14 @@ interface VariableDefinition {
|
|||
}
|
||||
|
||||
// Variables available across all client-side game script types
|
||||
const gameClientScriptTypes = ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall"];
|
||||
const gameClientScriptTypes = ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall", "RunWrapper"];
|
||||
|
||||
const variables: VariableDefinition[] = [
|
||||
{ name: "$InstallDirectory", type: "string", description: "Root install directory for the game", scriptTypes: gameClientScriptTypes },
|
||||
{ name: "$WorkingDirectory", type: "string", description: "Current working directory for the script" },
|
||||
{ name: "$ServerAddress", type: "string", description: "Address of the LANCommander server", scriptTypes: gameClientScriptTypes },
|
||||
{ name: "$DefaultInstallDirectory", type: "string", description: "Default installation directory", scriptTypes: gameClientScriptTypes },
|
||||
{ name: "$GameManifest", type: "GameManifest", description: "The game's manifest object containing metadata such as title, ID, sort title, description, notes, and related collections", scriptTypes: ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall"] },
|
||||
{ name: "$GameManifest", type: "GameManifest", description: "The game's manifest object containing metadata such as title, ID, sort title, description, notes, and related collections", scriptTypes: ["Install", "Uninstall", "BeforeStart", "AfterStop", "NameChange", "KeyChange", "SaveUpload", "SaveDownload", "DetectInstall", "RunWrapper"] },
|
||||
{ name: "$PlayerAlias", type: "string", description: "Current player's alias/display name", scriptTypes: ["BeforeStart", "AfterStop"] },
|
||||
{ name: "$NewPlayerAlias", type: "string", description: "New player alias (name change scripts)", scriptTypes: ["NameChange"] },
|
||||
{ name: "$OldPlayerAlias", type: "string", description: "Previous player alias (name change scripts)", scriptTypes: ["NameChange"] },
|
||||
|
|
@ -34,7 +34,9 @@ const variables: VariableDefinition[] = [
|
|||
{ name: "$User", type: "User", description: "User model object with properties: Id, UserName, and Alias", scriptTypes: ["GameStarted", "GameStopped", "UserRegistration", "UserLogin"] },
|
||||
{ name: "$ToolManifest", type: "ToolManifest", description: "Tool manifest object containing metadata for the current tool", scriptTypes: ["Install", "BeforeStart", "AfterStop", "DetectInstall"] },
|
||||
{ name: "$Tool", type: "Tool", description: "Tool model object with properties: Id, Name, and Description", scriptTypes: ["Package"] },
|
||||
{ name: "$RedistributableManifest", type: "RedistributableManifest", description: "Redistributable manifest object containing metadata for the current redistributable", scriptTypes: ["Install", "BeforeStart", "AfterStop", "NameChange", "DetectInstall"] },
|
||||
{ name: "$RedistributableManifest", type: "RedistributableManifest", description: "Redistributable manifest object containing metadata for the current redistributable", scriptTypes: ["Install", "BeforeStart", "AfterStop", "NameChange", "DetectInstall", "RunWrapper"] },
|
||||
{ name: "$ExecutablePath", type: "string", description: "Resolved path to the game executable", scriptTypes: ["RunWrapper"] },
|
||||
{ name: "$Arguments", type: "string", description: "Resolved command-line arguments for the executable", scriptTypes: ["RunWrapper"] },
|
||||
{ name: "$Redistributable", type: "Redistributable", description: "Redistributable model object with properties: Id, Name, and Description", scriptTypes: ["Package"] },
|
||||
{ name: "$DisplayWidth", type: "string", description: "Primary display width in pixels", scriptTypes: gameClientScriptTypes },
|
||||
{ name: "$DisplayHeight", type: "string", description: "Primary display height in pixels", scriptTypes: gameClientScriptTypes },
|
||||
|
|
@ -455,6 +457,14 @@ const scriptTemplates: Record<string, string> = {
|
|||
"# Runs after the game process stops.",
|
||||
"",
|
||||
].join("\n"),
|
||||
RunWrapper: [
|
||||
"# Run Wrapper Script",
|
||||
"# Controls how the game executable is launched.",
|
||||
"# This script runs instead of the normal process launch.",
|
||||
"",
|
||||
"# Launch the game",
|
||||
"Start-Process -FilePath $ExecutablePath -ArgumentList $Arguments -WorkingDirectory $WorkingDirectory -Wait",
|
||||
].join("\n"),
|
||||
};
|
||||
|
||||
export function getScriptTemplate(scriptType: string): string | null {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue