Implementation of script debugger component with RPC via SignalR
This commit is contained in:
parent
f242f66813
commit
17fa802c19
19 changed files with 516 additions and 62 deletions
|
|
@ -6,31 +6,40 @@ namespace LANCommander.Launcher.Services.PowerShell;
|
|||
|
||||
public class ScriptDebugger : IScriptDebugger
|
||||
{
|
||||
public Func<System.Management.Automation.PowerShell, Task> OnDebugStart;
|
||||
public Func<System.Management.Automation.PowerShell, Task> OnDebugBreak;
|
||||
public Func<System.Management.Automation.PowerShell, Task> OnDebugEnd;
|
||||
public Func<LogLevel, string, Task> OnOutput;
|
||||
public Func<IScriptDebugContext, Task>? OnDebugStart;
|
||||
public Func<IScriptDebugContext, Task>? OnDebugBreak;
|
||||
public Func<IScriptDebugContext, Task>? OnDebugEnd;
|
||||
public Func<LogLevel, string, Task>? OnOutput;
|
||||
|
||||
private static readonly Regex TokenRegex = new(@"\{[^}]+\}", RegexOptions.Compiled);
|
||||
|
||||
public async Task StartAsync(System.Management.Automation.PowerShell ps)
|
||||
public Task StartAsync(IScriptDebugContext context)
|
||||
{
|
||||
await OnDebugStart?.Invoke(ps)!;
|
||||
return OnDebugStart is null
|
||||
? Task.CompletedTask
|
||||
: OnDebugStart(context);
|
||||
}
|
||||
|
||||
public async Task EndAsync(System.Management.Automation.PowerShell ps)
|
||||
public Task EndAsync(IScriptDebugContext context)
|
||||
{
|
||||
await OnDebugEnd?.Invoke(ps)!;
|
||||
return OnDebugEnd is null
|
||||
? Task.CompletedTask
|
||||
: OnDebugEnd(context);
|
||||
}
|
||||
|
||||
public async Task BreakAsync(System.Management.Automation.PowerShell ps)
|
||||
public Task BreakAsync(IScriptDebugContext context)
|
||||
{
|
||||
await OnDebugBreak?.Invoke(ps)!;
|
||||
return OnDebugBreak is null
|
||||
? Task.CompletedTask
|
||||
: OnDebugBreak(context);
|
||||
}
|
||||
|
||||
public async Task OutputAsync(LogLevel level, string message, params object[] args)
|
||||
public Task OutputAsync(IScriptDebugContext context, LogLevel level, string message, params object[] args)
|
||||
{
|
||||
await OnOutput?.Invoke(level, Format(message, args))!;
|
||||
if (OnOutput is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return OnOutput(level, Format(message, args));
|
||||
}
|
||||
|
||||
private string Format(string template, object?[] args)
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
Debugger.OnDebugBreak = async (ps) =>
|
||||
Debugger.OnDebugBreak = async (context) =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
|
|
@ -73,10 +73,7 @@
|
|||
if (input.StartsWith('$'))
|
||||
input = "Write-Host " + input;
|
||||
|
||||
ps.Commands.Clear();
|
||||
ps.AddScript(input);
|
||||
|
||||
await ps.InvokeAsync();
|
||||
await context.ExecuteAsync(input);
|
||||
}
|
||||
|
||||
Visible = false;
|
||||
|
|
|
|||
10
LANCommander.SDK/PowerShell/IScriptDebugContext.cs
Normal file
10
LANCommander.SDK/PowerShell/IScriptDebugContext.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LANCommander.SDK.PowerShell;
|
||||
|
||||
public interface IScriptDebugContext
|
||||
{
|
||||
Guid SessionId { get; set; }
|
||||
Task ExecuteAsync(string script);
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ namespace LANCommander.SDK.PowerShell;
|
|||
|
||||
public interface IScriptDebugger
|
||||
{
|
||||
Task StartAsync(System.Management.Automation.PowerShell ps);
|
||||
Task EndAsync(System.Management.Automation.PowerShell ps);
|
||||
Task BreakAsync(System.Management.Automation.PowerShell ps);
|
||||
Task OutputAsync(LogLevel level, string message, params object[] args);
|
||||
Task StartAsync(IScriptDebugContext context);
|
||||
Task EndAsync(IScriptDebugContext context);
|
||||
Task BreakAsync(IScriptDebugContext context);
|
||||
Task OutputAsync(IScriptDebugContext context, LogLevel level, string message, params object[] args);
|
||||
}
|
||||
17
LANCommander.SDK/PowerShell/PowerShellDebugContext.cs
Normal file
17
LANCommander.SDK/PowerShell/PowerShellDebugContext.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LANCommander.SDK.PowerShell;
|
||||
|
||||
public class PowerShellDebugContext(System.Management.Automation.PowerShell ps) : IScriptDebugContext
|
||||
{
|
||||
public Guid SessionId { get; set; } = Guid.NewGuid();
|
||||
|
||||
public async Task ExecuteAsync(string script)
|
||||
{
|
||||
ps.Commands.Clear();
|
||||
ps.AddScript(script);
|
||||
|
||||
await ps.InvokeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ namespace LANCommander.SDK.PowerShell
|
|||
private ILogger<PowerShellScript> Logger { get; set; }
|
||||
private IEnumerable<IScriptDebugger> Debuggers { get; set; }
|
||||
private System.Management.Automation.PowerShell Context { get; set; }
|
||||
private IScriptDebugContext DebugContext { get; set; }
|
||||
|
||||
private const string Logo = @"
|
||||
__ ___ _ _______ __
|
||||
|
|
@ -187,9 +188,11 @@ namespace LANCommander.SDK.PowerShell
|
|||
|
||||
Context.Runspace = runspace;
|
||||
|
||||
DebugContext = new PowerShellDebugContext(Context);
|
||||
|
||||
await DebugAsync(async dbg =>
|
||||
{
|
||||
await dbg.StartAsync(Context);
|
||||
await dbg.StartAsync(DebugContext);
|
||||
});
|
||||
|
||||
Context.AddScript("Write-Host $Logo");
|
||||
|
|
@ -222,7 +225,7 @@ namespace LANCommander.SDK.PowerShell
|
|||
|
||||
await DebugAsync(async dbg =>
|
||||
{
|
||||
await dbg.BreakAsync(Context);
|
||||
await dbg.BreakAsync(DebugContext);
|
||||
});
|
||||
|
||||
var returnValue = Context.Runspace.SessionStateProxy.PSVariable.GetValue("Return");
|
||||
|
|
@ -265,8 +268,8 @@ namespace LANCommander.SDK.PowerShell
|
|||
|
||||
await DebugAsync(async dbg =>
|
||||
{
|
||||
await dbg.OutputAsync(LogLevel.Error, "{InvocationName} : {ExceptionMessage}", record.InvocationInfo.InvocationName, record.Exception.Message);
|
||||
await dbg.OutputAsync(LogLevel.Error, record.InvocationInfo.PositionMessage);
|
||||
await dbg.OutputAsync(DebugContext, LogLevel.Error, "{InvocationName} : {ExceptionMessage}", record.InvocationInfo.InvocationName, record.Exception.Message);
|
||||
await dbg.OutputAsync(DebugContext, LogLevel.Error, record.InvocationInfo.PositionMessage);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +279,7 @@ namespace LANCommander.SDK.PowerShell
|
|||
|
||||
await DebugAsync(async dbg =>
|
||||
{
|
||||
await dbg.OutputAsync(LogLevel.Warning, record.Message);
|
||||
await dbg.OutputAsync(DebugContext, LogLevel.Warning, record.Message);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +289,7 @@ namespace LANCommander.SDK.PowerShell
|
|||
|
||||
await DebugAsync(async dbg =>
|
||||
{
|
||||
await dbg.OutputAsync(LogLevel.Debug, record.Message);
|
||||
await dbg.OutputAsync(DebugContext, LogLevel.Debug, record.Message);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +299,7 @@ namespace LANCommander.SDK.PowerShell
|
|||
|
||||
await DebugAsync(async dbg =>
|
||||
{
|
||||
await dbg.OutputAsync(LogLevel.Trace, record.Message);
|
||||
await dbg.OutputAsync(DebugContext, LogLevel.Trace, record.Message);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +309,7 @@ namespace LANCommander.SDK.PowerShell
|
|||
|
||||
await DebugAsync(async dbg =>
|
||||
{
|
||||
await dbg.OutputAsync(LogLevel.Information, (record.MessageData as HostInformationMessage).Message);
|
||||
await dbg.OutputAsync(DebugContext, LogLevel.Information, (record.MessageData as HostInformationMessage).Message);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
12
LANCommander.SDK/PowerShell/Rpc/IScriptDebuggerClient.cs
Normal file
12
LANCommander.SDK/PowerShell/Rpc/IScriptDebuggerClient.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LANCommander.SDK.PowerShell.Rpc;
|
||||
|
||||
public interface IScriptDebuggerClient
|
||||
{
|
||||
Task Start(IScriptDebugContext context);
|
||||
Task End(IScriptDebugContext context);
|
||||
Task Break(IScriptDebugContext context);
|
||||
Task Output(IScriptDebugContext context, LogLevel level, string message);
|
||||
}
|
||||
10
LANCommander.SDK/PowerShell/Rpc/IScriptDebuggerHub.cs
Normal file
10
LANCommander.SDK/PowerShell/Rpc/IScriptDebuggerHub.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LANCommander.SDK.PowerShell.Rpc;
|
||||
|
||||
public interface IScriptDebuggerHub
|
||||
{
|
||||
Task DebugPackageScript(Guid gameId);
|
||||
Task SendInput(Guid sessionId, string input);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LANCommander.SDK.Rpc.Server;
|
||||
|
||||
public partial interface IRpcHub
|
||||
{
|
||||
Task Server_GetStatusAsync(Guid serverId);
|
||||
Task Server_UpdateStatusAsync(Guid serverId);
|
||||
Task Server_StartAsync(Guid serverId);
|
||||
Task Server_StopAsync(Guid serverId);
|
||||
Task Server_LogAsync(Guid serverId, string message);
|
||||
}
|
||||
|
|
@ -15,7 +15,6 @@ public class RpcClient(IRpcSubscriber subscriber)
|
|||
IRpcSubscriber _subscriber = subscriber;
|
||||
|
||||
public RpcChatClient Chat => new(_subscriber);
|
||||
public RpcServerClient Servers => new(_subscriber);
|
||||
|
||||
public bool IsConnected => _subscriber.IsConnectedAsync().Result;
|
||||
|
||||
|
|
@ -60,22 +59,4 @@ public class RpcChatClient(IRpcSubscriber subscriber)
|
|||
|
||||
public async Task<IEnumerable<User>> GetUsersAsync()
|
||||
=> await RpcClient.Hub.Chat_GetUsersAsync();
|
||||
}
|
||||
|
||||
public class RpcServerClient(IRpcSubscriber subscriber)
|
||||
{
|
||||
public async Task GetStatusAsync(Guid serverId)
|
||||
=> await RpcClient.Hub.Server_GetStatusAsync(serverId);
|
||||
|
||||
public async Task UpdateStatusAsync(Guid serverId)
|
||||
=> await RpcClient.Hub.Server_UpdateStatusAsync(serverId);
|
||||
|
||||
public async Task StartAsync(Guid serverId)
|
||||
=> await RpcClient.Hub.Server_StartAsync(serverId);
|
||||
|
||||
public async Task StopAsync(Guid serverId)
|
||||
=> await RpcClient.Hub.Server_StopAsync(serverId);
|
||||
|
||||
public async Task LogAsync(Guid serverId, string message)
|
||||
=> await RpcClient.Hub.Server_LogAsync(serverId, message);
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
using LANCommander.SDK;
|
||||
using LANCommander.SDK.Interceptors;
|
||||
using LANCommander.SDK.Models;
|
||||
using LANCommander.SDK.PowerShell;
|
||||
using LANCommander.SDK.Services;
|
||||
using LANCommander.Server.Services.Abstractions;
|
||||
using LANCommander.Server.Services.Factories;
|
||||
using LANCommander.Server.Services.Interceptors;
|
||||
using LANCommander.Server.Services.MediaGrabbers;
|
||||
using LANCommander.Server.Services.PowerShell;
|
||||
using LANCommander.Server.Services.ServerEngines;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
|
|
@ -64,6 +66,10 @@ public static class IServiceCollectionExtensions
|
|||
services.AddSingleton<DockerServerEngine>();
|
||||
services.AddSingleton<IServerEngine>(provider => provider.GetService<DockerServerEngine>());
|
||||
|
||||
services.AddSingleton<ScriptDebugger>();
|
||||
services.AddSingleton<IScriptDebugger>(sp =>
|
||||
sp.GetRequiredService<ScriptDebugger>());
|
||||
|
||||
services.AddSingleton<IPXRelayService>();
|
||||
services.AddSingleton<IBeaconMessageInterceptor, BeaconMessageInterceptor>();
|
||||
|
||||
|
|
|
|||
79
LANCommander.Server.Services/PowerShell/ScriptDebugger.cs
Normal file
79
LANCommander.Server.Services/PowerShell/ScriptDebugger.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using LANCommander.SDK.PowerShell;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LANCommander.Server.Services.PowerShell;
|
||||
|
||||
public class ScriptDebugger : IScriptDebugger
|
||||
{
|
||||
public Func<IScriptDebugContext, Task>? OnStart;
|
||||
public Func<IScriptDebugContext, Task>? OnBreak;
|
||||
public Func<IScriptDebugContext, Task>? OnEnd;
|
||||
public Func<IScriptDebugContext, LogLevel, string, Task>? OnOutput;
|
||||
|
||||
private IDictionary<Guid, IScriptDebugContext?> _contexts = new Dictionary<Guid, IScriptDebugContext?>();
|
||||
|
||||
private static readonly Regex TokenRegex = new(@"\{[^}]+\}", RegexOptions.Compiled);
|
||||
|
||||
public Guid CreateSession()
|
||||
{
|
||||
var sessionId = Guid.NewGuid();
|
||||
|
||||
_contexts.Add(sessionId, null);
|
||||
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public Task StartAsync(IScriptDebugContext context)
|
||||
{
|
||||
var latestSession = _contexts.FirstOrDefault(kvp => kvp.Value == null);
|
||||
|
||||
context.SessionId = latestSession.Key;
|
||||
_contexts[latestSession.Key] = context;
|
||||
|
||||
return OnStart is null
|
||||
? Task.CompletedTask
|
||||
: OnStart(context);
|
||||
}
|
||||
|
||||
public Task EndAsync(IScriptDebugContext context)
|
||||
{
|
||||
_contexts.Remove(context.SessionId);
|
||||
|
||||
return OnEnd is null
|
||||
? Task.CompletedTask
|
||||
: OnEnd(context);
|
||||
}
|
||||
|
||||
public Task BreakAsync(IScriptDebugContext context)
|
||||
{
|
||||
return OnBreak is null
|
||||
? Task.CompletedTask
|
||||
: OnBreak(context);
|
||||
}
|
||||
|
||||
public Task OutputAsync(IScriptDebugContext context, LogLevel level, string message, params object[] args)
|
||||
{
|
||||
if (OnOutput is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return OnOutput(context, level, Format(message, args));
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(Guid sessionId, string input)
|
||||
{
|
||||
if (_contexts.ContainsKey(sessionId))
|
||||
await _contexts[sessionId]!.ExecuteAsync(input);
|
||||
}
|
||||
|
||||
private string Format(string template, object?[] args)
|
||||
{
|
||||
if (args == null || args.Length == 0)
|
||||
return template;
|
||||
|
||||
int i = 0;
|
||||
|
||||
return TokenRegex.Replace(template, _ =>
|
||||
i < args.Length ? args[i++]?.ToString() ?? string.Empty : string.Empty);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
using LANCommander.SDK.PowerShell.Rpc;
|
||||
using LANCommander.SDK.Rpc.Client;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace LANCommander.Server.Extensions;
|
||||
|
||||
public static class SignalRExtensions
|
||||
{
|
||||
public static T DebugSession<T>(this IHubCallerClients<T> clients, Guid sessionId) where T : IScriptDebuggerClient
|
||||
=> clients.Group($"DebugSession/{sessionId}");
|
||||
|
||||
public static async Task AddDebugSessionAsync(this IGroupManager manager, string connectionId, Guid sessionId)
|
||||
=> await manager.AddToGroupAsync(connectionId, $"DebugSession/{sessionId}");
|
||||
}
|
||||
62
LANCommander.Server/Hubs/Script.cs
Normal file
62
LANCommander.Server/Hubs/Script.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using LANCommander.SDK.PowerShell;
|
||||
using LANCommander.SDK.PowerShell.Rpc;
|
||||
using LANCommander.SDK.Services;
|
||||
using LANCommander.Server.Extensions;
|
||||
using LANCommander.Server.Services;
|
||||
using LANCommander.Server.Services.PowerShell;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace LANCommander.Server.Hubs;
|
||||
|
||||
public class ScriptDebuggerHub(
|
||||
GameService gameService,
|
||||
ScriptClient scriptClient,
|
||||
ScriptDebugger scriptDebugger) : Hub<IScriptDebuggerClient>, IScriptDebuggerHub
|
||||
{
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
var sessionId = scriptDebugger.CreateSession();
|
||||
|
||||
await Groups.AddDebugSessionAsync(Context.ConnectionId, sessionId);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
private void Script_Initialize()
|
||||
{
|
||||
scriptClient.Debug = true;
|
||||
scriptDebugger.OnBreak = Script_DebugBreak;
|
||||
scriptDebugger.OnStart = Script_DebugStart;
|
||||
scriptDebugger.OnEnd = Script_DebugEnd;
|
||||
scriptDebugger.OnOutput = Script_DebugOutput;
|
||||
}
|
||||
|
||||
private async Task Script_DebugStart(IScriptDebugContext context)
|
||||
=> await Clients.DebugSession(context.SessionId).Start(context);
|
||||
|
||||
private async Task Script_DebugEnd(IScriptDebugContext context)
|
||||
=> await Clients.DebugSession(context.SessionId).End(context);
|
||||
|
||||
private async Task Script_DebugBreak(IScriptDebugContext context)
|
||||
{
|
||||
// Need to wait for user input?
|
||||
// This could get tricky. How do we separate debug sessions per-user?
|
||||
// The ScriptDebugger is a singleton, so you wouldn't be able to debug multiple scripts at once
|
||||
// Maybe the script debugger has to be scoped only for the current user session?
|
||||
await Clients.DebugSession(context.SessionId).Break(context);
|
||||
}
|
||||
|
||||
private async Task Script_DebugOutput(IScriptDebugContext context, LogLevel level, string mesasge)
|
||||
=> await Clients.DebugSession(context.SessionId).Output(context, level, mesasge);
|
||||
|
||||
public async Task DebugPackageScript(Guid gameId)
|
||||
{
|
||||
Script_Initialize();
|
||||
|
||||
await gameService.PackageAsync(gameId);
|
||||
}
|
||||
|
||||
public async Task SendInput(Guid sessionId, string input)
|
||||
{
|
||||
await scriptDebugger.ExecuteAsync(sessionId, input);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
using AutoMapper;
|
||||
using LANCommander.SDK.Rpc.Client;
|
||||
using LANCommander.SDK.Rpc.Server;
|
||||
using LANCommander.SDK.Services;
|
||||
using LANCommander.Server.Services;
|
||||
using LANCommander.Server.Services.PowerShell;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
|
|
@ -12,7 +14,10 @@ public partial class RpcHub(
|
|||
IMapper mapper,
|
||||
ILogger<RpcHub> logger,
|
||||
ChatService chatService,
|
||||
ServerService serverService) : Hub<IRpcSubscriber>, IRpcHub
|
||||
ServerService serverService,
|
||||
GameService gameService,
|
||||
ScriptDebugger scriptDebugger,
|
||||
ScriptClient scriptClient) : Hub<IRpcSubscriber>, IRpcHub
|
||||
{
|
||||
private string GetConnectionsCacheKey(string userIdentifier) => $"RPC/Connections/{userIdentifier}";
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public static class SignalR
|
|||
app.MapHub<RpcHub>("/rpc");
|
||||
app.MapHub<GameServerHub>("/hubs/gameserver");
|
||||
app.MapHub<LoggingHub>("/logging");
|
||||
app.MapHub<ScriptDebuggerHub>("/RPC/ScriptDebugger");
|
||||
|
||||
return app;
|
||||
}
|
||||
|
|
|
|||
104
LANCommander.Server/UI/Components/PowerShellConsole.razor
Normal file
104
LANCommander.Server/UI/Components/PowerShellConsole.razor
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
@using LANCommander.SDK.PowerShell
|
||||
@using LANCommander.SDK.PowerShell.Rpc
|
||||
@using LANCommander.Server.Services.PowerShell
|
||||
@using XtermBlazor
|
||||
@using LogLevel = Microsoft.Extensions.Logging.LogLevel
|
||||
@inherits RpcComponentBase<IScriptDebuggerClient, IScriptDebuggerHub>
|
||||
@inject ScriptDebugger
|
||||
|
||||
<div class="terminal @(Visible ? "" : "hidden")">
|
||||
<Xterm @ref="Terminal" Options="_options" OnFirstRender="@OnFirstRender" Addons="@Addons" />
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool Visible { get; set; } = false;
|
||||
private Xterm Terminal;
|
||||
private TaskCompletionSource<string> InputTaskCompletionSource;
|
||||
|
||||
private TerminalOptions _options = new()
|
||||
{
|
||||
CursorBlink = true,
|
||||
CursorStyle = CursorStyle.Bar,
|
||||
};
|
||||
|
||||
private HashSet<string> Addons = new()
|
||||
{
|
||||
"readline",
|
||||
"addon-fit"
|
||||
};
|
||||
|
||||
protected override string HubUrl => "/RPC/ScriptDebugger";
|
||||
|
||||
private async Task OnFirstRender()
|
||||
{
|
||||
await Terminal.Addon("addon-fit").InvokeVoidAsync("fit");
|
||||
}
|
||||
|
||||
private async Task<string> ReadLine()
|
||||
{
|
||||
return await Terminal.Addon("readline").InvokeAsync<string>("read", "> ");
|
||||
}
|
||||
|
||||
public async Task DebugPackagingScript(Guid gameId)
|
||||
{
|
||||
await Hub.DebugPackageScript(gameId);
|
||||
}
|
||||
|
||||
public async Task Start(IScriptDebugContext context)
|
||||
{
|
||||
Terminal?.Clear();
|
||||
Visible = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await Terminal.Addon("addon-fit").InvokeVoidAsync("fit");
|
||||
}
|
||||
|
||||
public async Task End(IScriptDebugContext context)
|
||||
{
|
||||
Visible = false;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async Task Break(IScriptDebugContext context)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var input = await ReadLine();
|
||||
|
||||
if (input.Trim().Equals("exit", StringComparison.OrdinalIgnoreCase))
|
||||
break;
|
||||
|
||||
if (input.StartsWith('$'))
|
||||
input = "Write-Host " + input;
|
||||
|
||||
await context.ExecuteAsync(input);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Output(IScriptDebugContext context, LogLevel level, string 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,13 @@
|
|||
}
|
||||
|
||||
<Button Icon="@IconType.Outline.Build" OnClick="() => RegToPowerShell.Open()" Type="@ButtonType.Text">Import .reg</Button>
|
||||
|
||||
@if (Options.GameId.HasValue && Options.GameId != Guid.Empty && IsDebuggable)
|
||||
{
|
||||
<Tooltip Title="Debug">
|
||||
<Button Icon="@IconType.Outline.CaretRight" Type="@ButtonType.Text" OnClick="() => _console.DebugPackagingScript(Options.GameId.Value)" />
|
||||
</Tooltip>
|
||||
}
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
|
|
@ -59,6 +66,8 @@
|
|||
<FormItem Label="Description">
|
||||
<TextArea @bind-Value="context.Description" MaxLength=500 ShowCount />
|
||||
</FormItem>
|
||||
|
||||
<PowerShellConsole @ref="_console" />
|
||||
</Form>
|
||||
|
||||
<RegToPowerShell @ref="RegToPowerShell" OnParsed="(text) => InsertText(text)" />
|
||||
|
|
@ -69,6 +78,7 @@
|
|||
Form<Script> Form;
|
||||
MonacoCodeEditor? Editor;
|
||||
RegToPowerShell RegToPowerShell;
|
||||
PowerShellConsole _console;
|
||||
IEnumerable<Snippet> Snippets { get; set; }
|
||||
|
||||
Archive? _archive;
|
||||
|
|
@ -86,6 +96,9 @@
|
|||
|
||||
Script Script = new();
|
||||
|
||||
bool IsDebuggable =>
|
||||
Script.Type == ScriptType.Package;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (Options.ScriptId != Guid.Empty)
|
||||
|
|
|
|||
144
LANCommander.UI/Components/RpcComponent/RpcComponent.cs
Normal file
144
LANCommander.UI/Components/RpcComponent/RpcComponent.cs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
using System.Reflection;
|
||||
using LANCommander.SDK.Extensions;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace LANCommander.UI.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Base component that connects to a SignalR hub and exposes the component
|
||||
/// as a strongly-typed client (THubClient).
|
||||
///
|
||||
/// Derived components must:
|
||||
/// - Implement THubClient
|
||||
/// - Provide the HubUrl
|
||||
/// </summary>
|
||||
public abstract class RpcComponentBase<THubClient, THub> : ComponentBase, IAsyncDisposable
|
||||
where THubClient : class where THub : class
|
||||
{
|
||||
private readonly List<IDisposable> _clientHandlerSubscriptions = new();
|
||||
|
||||
protected HubConnection? HubConnection { get; private set; }
|
||||
|
||||
protected THub? Hub { get; private set; }
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Relative hub URL, e.g. "/hubs/notifications".
|
||||
/// </summary>
|
||||
protected abstract string HubUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The component cast as the hub client interface.
|
||||
/// </summary>
|
||||
protected THubClient Client => (THubClient)(object)this;
|
||||
|
||||
protected bool IsConnected =>
|
||||
HubConnection?.State == HubConnectionState.Connected;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
var absoluteHubUrl = NavigationManager.ToAbsoluteUri(HubUrl);
|
||||
|
||||
var builder = new HubConnectionBuilder()
|
||||
.WithUrl(absoluteHubUrl)
|
||||
.WithAutomaticReconnect();
|
||||
|
||||
HubConnection = builder.Build();
|
||||
|
||||
Hub = HubConnection.ServerProxy<THub>();
|
||||
|
||||
WireClientInterfaceHandlers(HubConnection);
|
||||
|
||||
HubConnection.Reconnected += async _ =>
|
||||
{
|
||||
await OnReconnectedAsync();
|
||||
};
|
||||
|
||||
HubConnection.Reconnecting += async ex =>
|
||||
{
|
||||
await OnReconnectingAsync(ex);
|
||||
};
|
||||
|
||||
HubConnection.Closed += async ex =>
|
||||
{
|
||||
await OnClosedAsync(ex);
|
||||
};
|
||||
|
||||
await HubConnection.StartAsync();
|
||||
await OnConnectedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the connection is successfully started.
|
||||
/// </summary>
|
||||
protected virtual Task OnConnectedAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the connection is in the process of reconnecting.
|
||||
/// </summary>
|
||||
protected virtual Task OnReconnectingAsync(Exception? exception) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the connection has reconnected.
|
||||
/// </summary>
|
||||
protected virtual Task OnReconnectedAsync() => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the connection is closed and will not reconnect automatically.
|
||||
/// </summary>
|
||||
protected virtual Task OnClosedAsync(Exception? exception) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Registers handlers for every method on THubClient so that when the hub
|
||||
/// invokes these methods on the client, they are forwarded to this component.
|
||||
/// </summary>
|
||||
private void WireClientInterfaceHandlers(HubConnection hubConnection)
|
||||
{
|
||||
var clientType = typeof(THubClient);
|
||||
var methods = clientType
|
||||
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m => !m.IsSpecialName); // ignore property accessors, etc.
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var parameters = method.GetParameters();
|
||||
var parameterTypes = parameters.Select(p => p.ParameterType).ToArray();
|
||||
|
||||
// Use the generic On(string, Type[], Func<object[], Task>) overload
|
||||
var subscription = hubConnection.On(
|
||||
methodName: method.Name,
|
||||
parameterTypes: parameterTypes,
|
||||
handler: async args =>
|
||||
{
|
||||
var result = method.Invoke(Client, args);
|
||||
|
||||
if (result is Task task)
|
||||
await task;
|
||||
|
||||
// Ensure UI updates are marshaled back onto the Blazor renderer
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
_clientHandlerSubscriptions.Add(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (var subscription in _clientHandlerSubscriptions)
|
||||
subscription.Dispose();
|
||||
|
||||
_clientHandlerSubscriptions.Clear();
|
||||
|
||||
if (HubConnection is not null)
|
||||
{
|
||||
await HubConnection.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue