diff --git a/LANCommander.Launcher.Services/Extensions/IServiceProviderExtensions.cs b/LANCommander.Launcher.Services/Extensions/IServiceProviderExtensions.cs index f7cba313..3f91824f 100644 --- a/LANCommander.Launcher.Services/Extensions/IServiceProviderExtensions.cs +++ b/LANCommander.Launcher.Services/Extensions/IServiceProviderExtensions.cs @@ -7,6 +7,7 @@ using System.Data.Common; using System.Diagnostics; using System.Runtime.InteropServices; using LANCommander.SDK; +using LANCommander.SDK.PowerShell; namespace LANCommander.Launcher.Services.Extensions { @@ -24,6 +25,10 @@ namespace LANCommander.Launcher.Services.Extensions var logger = scope.ServiceProvider.GetService(); var authenticationService = scope.ServiceProvider.GetService(); var keepAliveService = scope.ServiceProvider.GetService(); + var client = scope.ServiceProvider.GetService(); + var debugHandler = scope.ServiceProvider.GetService(); + + client.Scripts.RegisterDebugHandler(debugHandler); #region Scaffold Required Directories try diff --git a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs index 04cd0b56..5e9ed91d 100644 --- a/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs +++ b/LANCommander.Launcher.Services/Extensions/ServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; using System.Threading.Tasks; +using LANCommander.SDK.PowerShell; namespace LANCommander.Launcher.Services.Extensions { @@ -81,6 +82,13 @@ namespace LANCommander.Launcher.Services.Extensions services.AddScoped(); services.AddScoped(); + var debugHandler = new PowerShellDebugHandler + { + SessionId = Guid.Empty, + }; + + services.AddSingleton(debugHandler); + return services; } diff --git a/LANCommander.Launcher/UI/Components/LibraryItemContextMenu.razor b/LANCommander.Launcher/UI/Components/LibraryItemContextMenu.razor index a5d8ae40..1eab244c 100644 --- a/LANCommander.Launcher/UI/Components/LibraryItemContextMenu.razor +++ b/LANCommander.Launcher/UI/Components/LibraryItemContextMenu.razor @@ -3,6 +3,7 @@ @using System.Diagnostics @using LANCommander.SDK @using LANCommander.SDK.Helpers +@using LANCommander.SDK.PowerShell @inject GameService GameService @inject UserService UserService @inject LibraryService LibraryService @@ -10,6 +11,7 @@ @inject ModalService ModalService @inject MessageBusService MessageBusService @inject SDK.Client Client +@inject PowerShellDebugHandler DebugHandler @inject LocalizationService LocalizationService @if (GameActions != null && GameActions.Count() > 0) @@ -278,7 +280,7 @@ else foreach (var manifest in manifests) { - await Client.Scripts.RunInstallScriptAsync(Game.InstallDirectory, manifest.Id); + await Client.Scripts.RunInstallScriptAsync(Game.InstallDirectory, manifest.Id, DebugHandler); } } @@ -288,7 +290,7 @@ else foreach (var manifest in manifests) { - await Client.Scripts.RunUninstallScriptAsync(Game.InstallDirectory, manifest.Id); + await Client.Scripts.RunUninstallScriptAsync(Game.InstallDirectory, manifest.Id, DebugHandler); } } @@ -299,7 +301,7 @@ else foreach (var manifest in manifests) { - await Client.Scripts.RunNameChangeScriptAsync(Game.InstallDirectory, Game.Id, user.GetUserNameSafe ?? Settings.DEFAULT_GAME_USERNAME); + await Client.Scripts.RunNameChangeScriptAsync(Game.InstallDirectory, Game.Id, user.GetUserNameSafe ?? Settings.DEFAULT_GAME_USERNAME, DebugHandler); } } @@ -311,7 +313,7 @@ else { var key = Client.Games.GetAllocatedKey(manifest.Id); - await Client.Scripts.RunKeyChangeScriptAsync(Game.InstallDirectory, Game.Id, key); + await Client.Scripts.RunKeyChangeScriptAsync(Game.InstallDirectory, Game.Id, key, DebugHandler); } } diff --git a/LANCommander.Launcher/UI/Components/PowerShellConsole.razor b/LANCommander.Launcher/UI/Components/PowerShellConsole.razor deleted file mode 100644 index c4e29809..00000000 --- a/LANCommander.Launcher/UI/Components/PowerShellConsole.razor +++ /dev/null @@ -1,95 +0,0 @@ -@using XtermBlazor -@using LogLevel = Microsoft.Extensions.Logging.LogLevel -@inject SDK.Client Client - -
- -
- -@code { - private bool Visible { get; set; } = false; - private Xterm Terminal; - private TaskCompletionSource InputTaskCompletionSource; - - private TerminalOptions _options = new TerminalOptions - { - CursorBlink = true, - CursorStyle = CursorStyle.Bar, - }; - - private HashSet Addons = new HashSet() - { - "readline", - "addon-fit" - }; - - protected override async Task OnInitializedAsync() - { - Client.Scripts.OnDebugStart = async (ps) => - { - Terminal?.Clear(); - Visible = true; - await InvokeAsync(StateHasChanged); - - await Terminal.Addon("addon-fit").InvokeVoidAsync("fit"); - }; - - Client.Scripts.OnOutput = async (level, message) => - { - switch (level) - { - case LogLevel.Error: - await Terminal.WriteLine($"\x1b[0;31m{message}"); - break; - - case LogLevel.Warning: - await Terminal.WriteLine($"\x1b[0;33m{message}"); - break; - - case LogLevel.Debug: - await Terminal.WriteLine($"\x1b[0;37m{message}"); - break; - - case LogLevel.Trace: - await Terminal.WriteLine($"\x1b[0;36m{message}"); - break; - - case LogLevel.Information: - await Terminal.WriteLine($"\x1b[0;37m{message}"); - break; - } - }; - - Client.Scripts.OnDebugBreak = async (ps) => - { - while (true) - { - var input = await ReadLine(); - - if (input.Trim().Equals("exit", StringComparison.OrdinalIgnoreCase)) - break; - - if (input.StartsWith('$')) - input = "Write-Host " + input; - - ps.Commands.Clear(); - ps.AddScript(input); - - await ps.InvokeAsync(); - } - - Visible = false; - await InvokeAsync(StateHasChanged); - }; - } - - private async Task OnFirstRender() - { - await Terminal.Addon("addon-fit").InvokeVoidAsync("fit"); - } - - private async Task ReadLine() - { - return await Terminal.Addon("readline").InvokeAsync("read", "> "); - } -} \ No newline at end of file diff --git a/LANCommander.Launcher/UI/MainLayout.razor b/LANCommander.Launcher/UI/MainLayout.razor index bfcfb1f8..14baa83b 100644 --- a/LANCommander.Launcher/UI/MainLayout.razor +++ b/LANCommander.Launcher/UI/MainLayout.razor @@ -74,7 +74,7 @@ @if (Settings.Debug.EnableScriptDebugging) { - + } diff --git a/LANCommander.SDK/Factories/ScriptFactory.cs b/LANCommander.SDK/Factories/ScriptFactory.cs new file mode 100644 index 00000000..3329a6b9 --- /dev/null +++ b/LANCommander.SDK/Factories/ScriptFactory.cs @@ -0,0 +1,13 @@ +using LANCommander.SDK.Enums; +using LANCommander.SDK.PowerShell; +using LANCommander.SDK.Services; + +namespace LANCommander.SDK.Factories; + +public class ScriptFactory(ScriptService scriptService) +{ + public PowerShellScript Create(ScriptType type) + { + return new PowerShellScript(type, scriptService); + } +} \ No newline at end of file diff --git a/LANCommander.SDK/PowerShell/PowerShellDebugHandler.cs b/LANCommander.SDK/PowerShell/PowerShellDebugHandler.cs index 1413f9ce..9b091b7b 100644 --- a/LANCommander.SDK/PowerShell/PowerShellDebugHandler.cs +++ b/LANCommander.SDK/PowerShell/PowerShellDebugHandler.cs @@ -6,7 +6,86 @@ namespace LANCommander.SDK.PowerShell; public class PowerShellDebugHandler { - public Func OnDebugStart; - public Func OnDebugBreak; - public Func OnOutput; + public Guid SessionId { get; set; } = Guid.NewGuid(); + + public event EventHandler OnDebugStart; + public event EventHandler OnDebugBreak; + public event EventHandler OnDebugOutput; + public event EventHandler OnDebugStop; + + private System.Management.Automation.PowerShell _powerShell; + + internal void Start(System.Management.Automation.PowerShell ps = null) + { + if (_powerShell == null) + _powerShell = ps; + + OnDebugStart?.Invoke(this, new OnDebugStartEventArgs + { + PowerShell = _powerShell, + }); + } + + internal void Break(System.Management.Automation.PowerShell ps = null) + { + if (_powerShell == null) + _powerShell = ps; + + OnDebugStart?.Invoke(this, new OnDebugStartEventArgs + { + PowerShell = _powerShell, + }); + } + + internal void Output(LogLevel level, string message) + { + OnDebugOutput?.Invoke(this, new OnDebugOutputEventArgs + { + LogLevel = level, + Message = message + }); + } + + internal void Stop(System.Management.Automation.PowerShell ps = null) + { + if (_powerShell == null) + _powerShell = ps; + + OnDebugStop?.Invoke(this, new OnDebugStopEventArgs + { + PowerShell = _powerShell, + }); + } + + public async Task ExecuteAsync(string input) + { + if (input.StartsWith('$')) + input = $"Write-Host {input}"; + + _powerShell.Commands.Clear(); + _powerShell.AddScript(input); + + await _powerShell.InvokeAsync(); + } +} + +public class OnDebugStartEventArgs : EventArgs +{ + public System.Management.Automation.PowerShell PowerShell { get; set; } +} + +public class OnDebugBreakEventArgs : EventArgs +{ + public System.Management.Automation.PowerShell PowerShell { get; set; } +} + +public class OnDebugOutputEventArgs : EventArgs +{ + public LogLevel LogLevel { get; set; } + public string Message { get; set; } +} + +public class OnDebugStopEventArgs : EventArgs +{ + public System.Management.Automation.PowerShell PowerShell { get; set; } } \ No newline at end of file diff --git a/LANCommander.SDK/PowerShell/PowerShellScript.cs b/LANCommander.SDK/PowerShell/PowerShellScript.cs index 83db98b3..4c6af9f5 100644 --- a/LANCommander.SDK/PowerShell/PowerShellScript.cs +++ b/LANCommander.SDK/PowerShell/PowerShellScript.cs @@ -13,6 +13,7 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using LANCommander.SDK.Services; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -35,6 +36,8 @@ namespace LANCommander.SDK.PowerShell private TaskCompletionSource Input { get; set; } public PowerShellDebugHandler DebugHandler { get; private set; } + + private ScriptService ScriptService { get; set; } private const string Logo = @" __ ___ _ _______ __ @@ -44,9 +47,11 @@ namespace LANCommander.SDK.PowerShell "; - public PowerShellScript(ScriptType type) + public PowerShellScript(ScriptType type, ScriptService scriptService) { Type = type; + ScriptService = scriptService; + Variables = new PowerShellVariableList(); Arguments = new Dictionary(); DebugHandler = new PowerShellDebugHandler(); @@ -144,9 +149,13 @@ namespace LANCommander.SDK.PowerShell public PowerShellScript EnableDebug(PowerShellDebugHandler debugHandler = null) { Debug = true; - + if (debugHandler != null) + { DebugHandler = debugHandler; + + ScriptService.RegisterDebugHandler(debugHandler); + } return this; } @@ -184,9 +193,9 @@ namespace LANCommander.SDK.PowerShell using (var ps = System.Management.Automation.PowerShell.Create()) { ps.Runspace = runspace; - + if (Debug) - await (DebugHandler.OnDebugStart?.Invoke(ps) ?? Task.CompletedTask); + DebugHandler.Start(ps); ps.AddScript("Write-Host $Logo"); @@ -206,21 +215,18 @@ namespace LANCommander.SDK.PowerShell ps.AddScript("Write-Host ''"); ps.AddScript("Write-Host 'Enter \"exit\" to continue'"); - - if (DebugHandler.OnOutput != null) - { - ps.Streams.Information.DataAdded += Information_DataAdded; - ps.Streams.Verbose.DataAdded += Verbose_DataAdded; - ps.Streams.Debug.DataAdded += Debug_DataAdded; - ps.Streams.Warning.DataAdded += Warning_DataAdded; - ps.Streams.Error.DataAdded += Error_DataAdded; - } + + ps.Streams.Information.DataAdded += Information_DataAdded; + ps.Streams.Verbose.DataAdded += Verbose_DataAdded; + ps.Streams.Debug.DataAdded += Debug_DataAdded; + ps.Streams.Warning.DataAdded += Warning_DataAdded; + ps.Streams.Error.DataAdded += Error_DataAdded; } var results = await ps.InvokeAsync(); if (Debug) - await (DebugHandler.OnDebugBreak?.Invoke(ps) ?? Task.CompletedTask); + DebugHandler.Break(ps); try { @@ -239,6 +245,9 @@ namespace LANCommander.SDK.PowerShell if (IgnoreWow64) Wow64RevertWow64FsRedirection(ref wow64Value); + if (DebugHandler != null) + ScriptService.DeregisterDebugHandler(DebugHandler); + return result; } @@ -246,29 +255,29 @@ namespace LANCommander.SDK.PowerShell { var record = ((PSDataCollection)sender)[e.Index]; - DebugHandler.OnOutput?.Invoke(LogLevel.Error, $"{record.InvocationInfo.InvocationName} : {record.Exception.Message}"); - DebugHandler.OnOutput?.Invoke(LogLevel.Error, record.InvocationInfo.PositionMessage); + DebugHandler.Output(LogLevel.Error, $"{record.InvocationInfo.InvocationName} : {record.Exception.Message}"); + DebugHandler.Output(LogLevel.Error, record.InvocationInfo.PositionMessage); } private void Warning_DataAdded(object sender, DataAddedEventArgs e) { var record = ((PSDataCollection)sender)[e.Index]; - DebugHandler.OnOutput?.Invoke(LogLevel.Warning, record.Message); + DebugHandler.Output(LogLevel.Warning, record.Message); } private void Debug_DataAdded(object sender, DataAddedEventArgs e) { var record = ((PSDataCollection)sender)[e.Index]; - DebugHandler.OnOutput?.Invoke(LogLevel.Debug, record.Message); + DebugHandler.Output(LogLevel.Debug, record.Message); } private void Verbose_DataAdded(object sender, DataAddedEventArgs e) { var record = ((PSDataCollection)sender)[e.Index]; - DebugHandler.OnOutput?.Invoke(LogLevel.Trace, record.Message); + DebugHandler.Output(LogLevel.Trace, record.Message); } private void Information_DataAdded(object sender, DataAddedEventArgs e) @@ -276,7 +285,7 @@ namespace LANCommander.SDK.PowerShell var record = ((PSDataCollection)sender)[e.Index]; if (record.MessageData != null && record.MessageData is HostInformationMessage) - DebugHandler.OnOutput?.Invoke(LogLevel.Information, (record.MessageData as HostInformationMessage).Message); + DebugHandler.Output(LogLevel.Information, (record.MessageData as HostInformationMessage).Message); } public static string Serialize(T input) diff --git a/LANCommander.SDK/Rpc/Interfaces/Client/Log.cs b/LANCommander.SDK/Rpc/Interfaces/Client/Log.cs new file mode 100644 index 00000000..b3c2eaa3 --- /dev/null +++ b/LANCommander.SDK/Rpc/Interfaces/Client/Log.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +namespace LANCommander.SDK.Rpc.Client; + +public partial interface IRpcClient +{ + Task Log_ConsoleOutputAsync(Guid sessionId, string content); +} \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/Interfaces/Client/Script.cs b/LANCommander.SDK/Rpc/Interfaces/Client/Script.cs new file mode 100644 index 00000000..bcbf188f --- /dev/null +++ b/LANCommander.SDK/Rpc/Interfaces/Client/Script.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace LANCommander.SDK.Rpc.Client; + +public partial interface IRpcClient +{ + Task Script_DebugOutputAsync(Guid sessionId, LogLevel level, string message); + Task Script_DebugStartAsync(Guid sessionId); + Task Script_DebugBreakAsync(Guid sessionId); +} \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/Interfaces/Server/Script.cs b/LANCommander.SDK/Rpc/Interfaces/Server/Script.cs new file mode 100644 index 00000000..58e699f5 --- /dev/null +++ b/LANCommander.SDK/Rpc/Interfaces/Server/Script.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace LANCommander.SDK.Rpc.Server; + +public partial interface IRpcHub +{ + Task Script_ExecuteAsync(Guid scriptId); + Task Script_ConsoleInputAsync(Guid sessionId, string input); +} \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/Log.cs b/LANCommander.SDK/Rpc/Log.cs new file mode 100644 index 00000000..4fae36c3 --- /dev/null +++ b/LANCommander.SDK/Rpc/Log.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; + +namespace LANCommander.SDK.Rpc; + +public partial class RpcClient +{ + public async Task Log_ConsoleOutputAsync(Guid sessionId, string content) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Rpc/Script.cs b/LANCommander.SDK/Rpc/Script.cs new file mode 100644 index 00000000..72d56e88 --- /dev/null +++ b/LANCommander.SDK/Rpc/Script.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using LANCommander.SDK.PowerShell; +using Microsoft.Extensions.Logging; + +namespace LANCommander.SDK.Rpc; + +public partial class RpcClient +{ + private Dictionary _debugHandlers = new(); + + public void AddDebugHandler(Guid sessionId, PowerShellDebugHandler handler) + { + _debugHandlers[sessionId] = handler; + } + + public async Task Script_DebugOutputAsync(Guid sessionId, LogLevel level, string message) + { + if (_debugHandlers.ContainsKey(sessionId)) + _debugHandlers[sessionId].Output(level, message); + } + + public async Task Script_DebugStartAsync(Guid sessionId) + { + if (_debugHandlers.ContainsKey(sessionId)) + _debugHandlers[sessionId].Start(); + } + + public async Task Script_DebugBreakAsync(Guid sessionId) + { + if (_debugHandlers.ContainsKey(sessionId)) + _debugHandlers[sessionId].Break(); + } +} \ No newline at end of file diff --git a/LANCommander.SDK/Services/SaveService.cs b/LANCommander.SDK/Services/SaveService.cs index 658e2bdb..8cbde974 100644 --- a/LANCommander.SDK/Services/SaveService.cs +++ b/LANCommander.SDK/Services/SaveService.cs @@ -198,7 +198,7 @@ namespace LANCommander.SDK.Services { var registryImportFileContents = File.ReadAllText(registryImportFilePath); - var script = new PowerShellScript(Enums.ScriptType.SaveDownload); + var script = new PowerShellScript(Enums.ScriptType.SaveDownload, _client.Scripts); string adminArgument = string.Empty; if (registryImportFileContents.Contains("HKEY_LOCAL_MACHINE")) @@ -208,14 +208,9 @@ namespace LANCommander.SDK.Services } script.UseInline($"Start-Process regedit.exe {adminArgument} -ArgumentList \"/s\", \"{registryImportFilePath}\""); - + if (_client.Scripts.Debug) - { script.EnableDebug(); - script.DebugHandler.OnDebugStart = _client.Scripts.OnDebugStart; - script.DebugHandler.OnDebugBreak = _client.Scripts.OnDebugBreak; - script.DebugHandler.OnOutput = _client.Scripts.OnOutput; - } await script.ExecuteAsync(); } diff --git a/LANCommander.SDK/Services/ScriptService.cs b/LANCommander.SDK/Services/ScriptService.cs index 5f986973..2540d271 100644 --- a/LANCommander.SDK/Services/ScriptService.cs +++ b/LANCommander.SDK/Services/ScriptService.cs @@ -4,6 +4,7 @@ using LANCommander.SDK.Models; using LANCommander.SDK.PowerShell; using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -17,15 +18,13 @@ namespace LANCommander.SDK.Services private readonly Client _client; + private List _debugHandlers = new(); + public delegate Task ExternalScriptRunnerHandler(PowerShellScript script); public event ExternalScriptRunnerHandler ExternalScriptRunner; public bool Debug { get; set; } = false; - public Func OnDebugStart; - public Func OnDebugBreak; - public Func OnOutput; - public ScriptService(Client client) { _client = client; @@ -36,18 +35,36 @@ namespace LANCommander.SDK.Services _client = client; _logger = logger; } + + public void RegisterDebugHandler(PowerShellDebugHandler debugHandler) + { + _debugHandlers.Add(debugHandler); + } + + public void DeregisterDebugHandler(PowerShellDebugHandler debugHandler) + { + _debugHandlers.Remove(debugHandler); + } + + public PowerShellDebugHandler GetDebugHandler(Guid sessionId) + { + return _debugHandlers.FirstOrDefault(h => h.SessionId == sessionId); + } #region Authentication Scripts - public async Task RunUserLoginScript(Script loginScript, User user) + public async Task RunUserLoginScript(Script loginScript, User user, PowerShellDebugHandler debugHandler = null) { try { using (var op = _logger.BeginOperation("Executing user login script")) { - var script = new PowerShellScript(Enums.ScriptType.UserLogin); + var script = new PowerShellScript(Enums.ScriptType.UserLogin, _client.Scripts); script.AddVariable("User", user); + if (Debug) + script.EnableDebug(debugHandler); + script.UseInline(loginScript.Contents); try @@ -72,15 +89,18 @@ namespace LANCommander.SDK.Services } } - public async Task RunUserRegistrationScript(Script registrationScript, User user) + public async Task RunUserRegistrationScript(Script registrationScript, User user, PowerShellDebugHandler debugHandler = null) { try { using (var op = _logger.BeginOperation("Executing user registration script")) { - var script = new PowerShellScript(Enums.ScriptType.UserRegistration); + var script = new PowerShellScript(Enums.ScriptType.UserRegistration, _client.Scripts); script.AddVariable("User", user); + + if (Debug) + script.EnableDebug(debugHandler); script.UseInline(registrationScript.Contents); @@ -108,7 +128,7 @@ namespace LANCommander.SDK.Services #endregion #region Redistributables - public async Task RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Guid redistributableId) + public async Task RunDetectInstallScriptAsync(string installDirectory, Guid gameId, Guid redistributableId, PowerShellDebugHandler debugHandler = null) { bool result = default; @@ -123,10 +143,10 @@ namespace LANCommander.SDK.Services { using (var op = _logger.BeginOperation("Executing install detection script")) { - var script = new PowerShellScript(Enums.ScriptType.DetectInstall); + var script = new PowerShellScript(Enums.ScriptType.DetectInstall, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); @@ -161,20 +181,6 @@ namespace LANCommander.SDK.Services script.UseWorkingDirectory(Path.Combine(GameService.GetMetadataDirectoryPath(installDirectory, redistributableId))); script.UseFile(path); - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -194,7 +200,8 @@ namespace LANCommander.SDK.Services } } } - result = await script.ExecuteAsync(); + + result = await script.ExecuteAsync(); op.Complete(); } @@ -208,7 +215,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunInstallScriptAsync(string installDirectory, Guid gameId, Guid redistributableId) + public async Task RunInstallScriptAsync(string installDirectory, Guid gameId, Guid redistributableId, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -223,10 +230,10 @@ namespace LANCommander.SDK.Services { using (var op = _logger.BeginOperation("Executing install detection script")) { - var script = new PowerShellScript(Enums.ScriptType.Install); + var script = new PowerShellScript(Enums.ScriptType.Install, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); @@ -264,20 +271,6 @@ namespace LANCommander.SDK.Services script.UseWorkingDirectory(extractionPath); script.UseFile(path); - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -298,7 +291,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunBeforeStartScriptAsync(string installDirectory, Guid gameId, Guid redistributableId) + public async Task RunBeforeStartScriptAsync(string installDirectory, Guid gameId, Guid redistributableId, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -313,11 +306,11 @@ namespace LANCommander.SDK.Services { if (File.Exists(path)) { - var script = new PowerShellScript(Enums.ScriptType.BeforeStart); + var script = new PowerShellScript(Enums.ScriptType.BeforeStart, _client.Scripts); var playerAlias = await GameService.GetPlayerAliasAsync(installDirectory, gameId); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); @@ -357,20 +350,6 @@ namespace LANCommander.SDK.Services script.UseWorkingDirectory(extractionPath); script.UseFile(path); - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -395,7 +374,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunAfterStopScriptAsync(string installDirectory, Guid gameId, Guid redistributableId) + public async Task RunAfterStopScriptAsync(string installDirectory, Guid gameId, Guid redistributableId, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -410,10 +389,10 @@ namespace LANCommander.SDK.Services { if (File.Exists(path)) { - var script = new PowerShellScript(Enums.ScriptType.AfterStop); + var script = new PowerShellScript(Enums.ScriptType.AfterStop, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); @@ -453,20 +432,6 @@ namespace LANCommander.SDK.Services script.UseWorkingDirectory(extractionPath); script.UseFile(path); - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -492,7 +457,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunNameChangeScriptAsync(string installDirectory, Guid gameId, Guid redistributableId, string newName) + public async Task RunNameChangeScriptAsync(string installDirectory, Guid gameId, Guid redistributableId, string newName, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -517,10 +482,10 @@ namespace LANCommander.SDK.Services _logger?.LogTrace("New Name: {NewName}", newName); - var script = new PowerShellScript(Enums.ScriptType.NameChange); + var script = new PowerShellScript(Enums.ScriptType.NameChange, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", gameManifest); @@ -561,20 +526,6 @@ namespace LANCommander.SDK.Services script.UseWorkingDirectory(extractionPath); script.UseFile(path); - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -602,7 +553,7 @@ namespace LANCommander.SDK.Services #endregion #region Games - public async Task RunInstallScriptAsync(string installDirectory, Guid gameId) + public async Task RunInstallScriptAsync(string installDirectory, Guid gameId, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -615,10 +566,10 @@ namespace LANCommander.SDK.Services { if (File.Exists(path)) { - var script = new PowerShellScript(Enums.ScriptType.Install); + var script = new PowerShellScript(Enums.ScriptType.Install, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); @@ -648,20 +599,6 @@ namespace LANCommander.SDK.Services _logger?.LogError(ex, "Could not enrich logs"); } - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -686,7 +623,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunUninstallScriptAsync(string installDirectory, Guid gameId) + public async Task RunUninstallScriptAsync(string installDirectory, Guid gameId, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -699,10 +636,10 @@ namespace LANCommander.SDK.Services { if (File.Exists(path)) { - var script = new PowerShellScript(Enums.ScriptType.Uninstall); + var script = new PowerShellScript(Enums.ScriptType.Uninstall, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); @@ -732,20 +669,6 @@ namespace LANCommander.SDK.Services _logger?.LogError(ex, "Could not enrich logs"); } - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -770,7 +693,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunBeforeStartScriptAsync(string installDirectory, Guid gameId) + public async Task RunBeforeStartScriptAsync(string installDirectory, Guid gameId, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -783,11 +706,11 @@ namespace LANCommander.SDK.Services { if (File.Exists(path)) { - var script = new PowerShellScript(Enums.ScriptType.BeforeStart); + var script = new PowerShellScript(Enums.ScriptType.BeforeStart, _client.Scripts); var playerAlias = GameService.GetPlayerAlias(installDirectory, gameId); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); @@ -819,20 +742,6 @@ namespace LANCommander.SDK.Services _logger?.LogError(ex, "Could not enrich logs"); } - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -857,7 +766,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunAfterStopScriptAsync(string installDirectory, Guid gameId) + public async Task RunAfterStopScriptAsync(string installDirectory, Guid gameId, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -870,10 +779,10 @@ namespace LANCommander.SDK.Services { if (File.Exists(path)) { - var script = new PowerShellScript(Enums.ScriptType.AfterStop); + var script = new PowerShellScript(Enums.ScriptType.AfterStop, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); @@ -904,20 +813,6 @@ namespace LANCommander.SDK.Services _logger?.LogError(ex, "Could not enrich logs"); } - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -943,7 +838,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunNameChangeScriptAsync(string installDirectory, Guid gameId, string newName) + public async Task RunNameChangeScriptAsync(string installDirectory, Guid gameId, string newName, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -966,10 +861,10 @@ namespace LANCommander.SDK.Services _logger?.LogTrace("New Name: {NewName}", newName); - var script = new PowerShellScript(Enums.ScriptType.NameChange); + var script = new PowerShellScript(Enums.ScriptType.NameChange, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); script.AddVariable("InstallDirectory", installDirectory); script.AddVariable("GameManifest", manifest); @@ -1005,20 +900,6 @@ namespace LANCommander.SDK.Services GameService.UpdatePlayerAlias(installDirectory, gameId, newName); - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -1043,7 +924,7 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunKeyChangeScriptAsync(string installDirectory, Guid gameId, string key) + public async Task RunKeyChangeScriptAsync(string installDirectory, Guid gameId, string key, PowerShellDebugHandler debugHandler = null) { int result = default; @@ -1056,10 +937,10 @@ namespace LANCommander.SDK.Services { if (File.Exists(path)) { - var script = new PowerShellScript(Enums.ScriptType.KeyChange); + var script = new PowerShellScript(Enums.ScriptType.KeyChange, _client.Scripts); if (Debug) - script.DebugHandler.OnDebugStart = OnDebugStart; + script.EnableDebug(debugHandler); _logger?.LogTrace("New key is {Key}", key); @@ -1095,20 +976,6 @@ namespace LANCommander.SDK.Services GameService.UpdateCurrentKey(installDirectory, gameId, key); - try - { - if (Debug) - { - script.EnableDebug(); - script.DebugHandler.OnDebugBreak = OnDebugBreak; - script.DebugHandler.OnOutput = OnOutput; - } - } - catch (Exception ex) - { - _logger?.LogError(ex, "Could not debug script"); - } - bool handled = false; if (ExternalScriptRunner != null) @@ -1133,17 +1000,20 @@ namespace LANCommander.SDK.Services return result; } - public async Task RunPackageScriptAsync(Script packageScript, Game game) + public async Task RunPackageScriptAsync(Script packageScript, Game game, PowerShellDebugHandler debugHandler = null) { try { using (var op = _logger.BeginOperation("Executing game package script")) { - var script = new PowerShellScript(Enums.ScriptType.Package); + var script = new PowerShellScript(Enums.ScriptType.Package, _client.Scripts); script.AddVariable("Game", game); - + script.UseInline(packageScript.Contents); + + if (Debug) + script.EnableDebug(debugHandler); try { diff --git a/LANCommander.Server.Services/ServerEngines/DockerServerEngine.cs b/LANCommander.Server.Services/ServerEngines/DockerServerEngine.cs index 3426edbe..f3d7b64c 100644 --- a/LANCommander.Server.Services/ServerEngines/DockerServerEngine.cs +++ b/LANCommander.Server.Services/ServerEngines/DockerServerEngine.cs @@ -106,7 +106,7 @@ public class DockerServerEngine( { try { - var script = new PowerShellScript(SDK.Enums.ScriptType.BeforeStart); + var script = new PowerShellScript(SDK.Enums.ScriptType.BeforeStart, client.Scripts); script.AddVariable("Server", mapper.Map(server)); @@ -159,7 +159,7 @@ public class DockerServerEngine( { try { - var script = new PowerShellScript(SDK.Enums.ScriptType.AfterStop); + var script = new PowerShellScript(SDK.Enums.ScriptType.AfterStop, client.Scripts); script.AddVariable("Server", mapper.Map(server)); diff --git a/LANCommander.Server.Services/ServerEngines/LocalServerEngine.cs b/LANCommander.Server.Services/ServerEngines/LocalServerEngine.cs index 4a83671c..c4e3fa22 100644 --- a/LANCommander.Server.Services/ServerEngines/LocalServerEngine.cs +++ b/LANCommander.Server.Services/ServerEngines/LocalServerEngine.cs @@ -81,7 +81,7 @@ public class LocalServerEngine( { try { - var script = new PowerShellScript(SDK.Enums.ScriptType.BeforeStart); + var script = new PowerShellScript(SDK.Enums.ScriptType.BeforeStart, client.Scripts); script.AddVariable("Server", mapper.Map(server)); @@ -179,7 +179,7 @@ public class LocalServerEngine( { try { - var script = new PowerShellScript(SDK.Enums.ScriptType.AfterStop); + var script = new PowerShellScript(SDK.Enums.ScriptType.AfterStop, client.Scripts); script.AddVariable("Server", mapper.Map(server)); diff --git a/LANCommander.Server.Services/ServerService.cs b/LANCommander.Server.Services/ServerService.cs index 97c43321..2a8d9910 100644 --- a/LANCommander.Server.Services/ServerService.cs +++ b/LANCommander.Server.Services/ServerService.cs @@ -21,7 +21,8 @@ namespace LANCommander.Server.Services IHttpContextAccessor httpContextAccessor, IDbContextFactory contextFactory, IServiceProvider serviceProvider, - UserService userService) : BaseDatabaseService(logger, cache, mapper, httpContextAccessor, contextFactory) + UserService userService, + SDK.Client client) : BaseDatabaseService(logger, cache, mapper, httpContextAccessor, contextFactory) { public override async Task AddAsync(Data.Models.Server entity) { @@ -118,7 +119,7 @@ namespace LANCommander.Server.Services { try { - var scriptContext = new PowerShellScript(ScriptType.GameStarted); + var scriptContext = new PowerShellScript(ScriptType.GameStarted, client.Scripts); scriptContext.AddVariable("Server", mapper.Map(server)); scriptContext.AddVariable("Game", mapper.Map(server.Game)); @@ -151,7 +152,7 @@ namespace LANCommander.Server.Services { try { - var scriptContext = new PowerShellScript(ScriptType.GameStopped); + var scriptContext = new PowerShellScript(ScriptType.GameStopped, client.Scripts); scriptContext.AddVariable("Server", mapper.Map(server)); scriptContext.AddVariable("Game", mapper.Map(server.Game)); diff --git a/LANCommander.Server/Hubs/Log.cs b/LANCommander.Server/Hubs/Log.cs new file mode 100644 index 00000000..51212fe3 --- /dev/null +++ b/LANCommander.Server/Hubs/Log.cs @@ -0,0 +1,6 @@ +namespace LANCommander.Server.Hubs; + +public partial class RpcHub +{ + +} \ No newline at end of file diff --git a/LANCommander.Server/Hubs/Script.cs b/LANCommander.Server/Hubs/Script.cs new file mode 100644 index 00000000..ec718bc8 --- /dev/null +++ b/LANCommander.Server/Hubs/Script.cs @@ -0,0 +1,59 @@ +using System.Management.Automation; +using LANCommander.SDK.Enums; +using LANCommander.SDK.Models; +using LANCommander.SDK.PowerShell; + +namespace LANCommander.Server.Hubs; + +public partial class RpcHub +{ + private readonly Dictionary _scripts = new Dictionary(); + private string GetScriptSessionGroupName(Guid sessionId) => $"Script/Sessions/{sessionId}"; + + public async Task Script_ExecuteAsync(Guid scriptId) + { + var script = await scriptService.GetAsync(scriptId); + var game = await gameService.GetAsync(script.GameId ?? Guid.Empty); + + if (script.Type == ScriptType.Package) + { + var debugHandler = new PowerShellDebugHandler(); + + debugHandler.OnDebugOutput += DebugOutput; + debugHandler.OnDebugBreak += DebugBreak; + debugHandler.OnDebugStart += DebugStart; + + await Groups.AddToGroupAsync(Context.ConnectionId, GetScriptSessionGroupName(debugHandler.SessionId)); + await Clients.Group(GetScriptSessionGroupName(debugHandler.SessionId)) + .Script_DebugStartAsync(debugHandler.SessionId); + + await client.Scripts.RunPackageScriptAsync(mapper.Map(script), mapper.Map(game), debugHandler); + } + } + + public async Task Script_ConsoleInputAsync(Guid sessionId, string input) + { + var debugHandler = client.Scripts.GetDebugHandler(sessionId); + + if (debugHandler != null) + await debugHandler.ExecuteAsync(input); + } + + private void DebugStart(object? sender, OnDebugStartEventArgs args) + { + if (sender is PowerShellDebugHandler debugHandler) + Clients.Caller.Script_DebugStartAsync(debugHandler.SessionId); + } + + private void DebugBreak(object? sender, OnDebugBreakEventArgs args) + { + if (sender is PowerShellDebugHandler debugHandler) + Clients.Caller.Script_DebugBreakAsync(debugHandler.SessionId); + } + + private void DebugOutput(object? sender, OnDebugOutputEventArgs args) + { + if (sender is PowerShellDebugHandler debugHandler) + Clients.Caller.Script_DebugOutputAsync(debugHandler.SessionId, args.LogLevel, args.Message); + } +} \ No newline at end of file diff --git a/LANCommander.Server/Hubs/_RpcHub.cs b/LANCommander.Server/Hubs/_RpcHub.cs index a0473d3e..4568b299 100644 --- a/LANCommander.Server/Hubs/_RpcHub.cs +++ b/LANCommander.Server/Hubs/_RpcHub.cs @@ -10,7 +10,10 @@ namespace LANCommander.Server.Hubs; public partial class RpcHub( IFusionCache cache, IMapper mapper, + SDK.Client client, ChatService chatService, + GameService gameService, + ScriptService scriptService, ServerService serverService) : Hub, IRpcHub { diff --git a/LANCommander.UI/Components/DebugConsole/DebugConsole.razor b/LANCommander.UI/Components/DebugConsole/DebugConsole.razor new file mode 100644 index 00000000..1fc4c3cc --- /dev/null +++ b/LANCommander.UI/Components/DebugConsole/DebugConsole.razor @@ -0,0 +1,111 @@ +@using LANCommander.SDK +@using LANCommander.SDK.PowerShell +@using XtermBlazor +@using LogLevel = Microsoft.Extensions.Logging.LogLevel +@inject Client Client +@namespace LANCommander.UI.Components + +
+ +
+ +@code { + [Parameter] public PowerShellDebugHandler DebugHandler { get; set; } + [Parameter] public Guid? SessionId { get; set; } + + private bool Visible { get; set; } = false; + private Xterm Terminal; + private TaskCompletionSource InputTaskCompletionSource; + + private TerminalOptions _options = new() + { + CursorBlink = true, + CursorStyle = CursorStyle.Bar, + }; + + private HashSet Addons = new() + { + "readline", + "addon-fit" + }; + + protected override void OnInitialized() + { + if (SessionId != null) + DebugHandler = Client.Scripts.GetDebugHandler(SessionId.Value); + + DebugHandler.OnDebugStart += OnDebugStart; + DebugHandler.OnDebugBreak += OnDebugBreak; + DebugHandler.OnDebugOutput += OnDebugOutput; + DebugHandler.OnDebugStop += OnDebugStop; + } + + private async void OnDebugStart(object? sender, OnDebugStartEventArgs e) + { + Visible = true; + await InvokeAsync(StateHasChanged); + + await Terminal.Addon("addon-fit").InvokeVoidAsync("fit"); + } + + private async void OnDebugBreak(object? sender, OnDebugBreakEventArgs e) + { + while (true) + { + var input = await ReadLine(); + + if (input.Trim().Equals("exit", StringComparison.OrdinalIgnoreCase)) + { + await Terminal.Clear(); + break; + } + + await DebugHandler.ExecuteAsync(input); + } + + Visible = false; + await InvokeAsync(StateHasChanged); + } + + private async void OnDebugOutput(object? sender, OnDebugOutputEventArgs e) + { + switch (e.LogLevel) + { + case LogLevel.Error: + await Terminal.WriteLine($"\x1b[0;31m{e.Message}"); + break; + + case LogLevel.Warning: + await Terminal.WriteLine($"\x1b[0;33m{e.Message}"); + break; + + case LogLevel.Debug: + await Terminal.WriteLine($"\x1b[0;37m{e.Message}"); + break; + + case LogLevel.Trace: + await Terminal.WriteLine($"\x1b[0;36m{e.Message}"); + break; + + case LogLevel.Information: + await Terminal.WriteLine($"\x1b[0;37m{e.Message}"); + break; + } + } + + private async void OnDebugStop(object? sender, OnDebugStopEventArgs e) + { + await Terminal.Clear(); + Visible = false; + } + + private async Task OnFirstRender() + { + await Terminal.Addon("addon-fit").InvokeVoidAsync("fit"); + } + + private async Task ReadLine() + { + return await Terminal.Addon("readline").InvokeAsync("read", "> "); + } +} \ No newline at end of file