1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-08-10 13:40:40 +02:00

Add form for sending commands to instances

This commit is contained in:
chylex 2022-10-07 16:31:21 +02:00
parent 0695ee8405
commit 7f1e838331
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
14 changed files with 139 additions and 0 deletions

View File

@ -124,6 +124,10 @@ sealed class Instance : IDisposable {
}
}
public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
return await currentState.SendCommand(command, cancellationToken);
}
private sealed class InstanceContextImpl : InstanceContext {
private readonly Instance instance;
private int statusUpdateCounter;

View File

@ -131,6 +131,28 @@ sealed class InstanceSessionManager : IDisposable {
}
}
public async Task<SendCommandToInstanceResult> SendCommand(Guid instanceGuid, string command) {
try {
await semaphore.WaitAsync(shutdownCancellationToken);
} catch (OperationCanceledException) {
return SendCommandToInstanceResult.AgentShuttingDown;
}
try {
if (!instances.TryGetValue(instanceGuid, out var instance)) {
return SendCommandToInstanceResult.InstanceDoesNotExist;
}
if (!await instance.SendCommand(command, shutdownCancellationToken)) {
return SendCommandToInstanceResult.UnknownError;
}
return SendCommandToInstanceResult.Success;
} finally {
semaphore.Release();
}
}
public async Task StopAll() {
shutdownCancellationTokenSource.Cancel();

View File

@ -3,4 +3,5 @@
interface IInstanceState {
IInstanceState Launch(InstanceContext context);
IInstanceState Stop();
Task<bool> SendCommand(string command, CancellationToken cancellationToken);
}

View File

@ -94,6 +94,10 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
return this;
}
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
return Task.FromResult(false);
}
public void Dispose() {
cancellationTokenSource.Dispose();
}

View File

@ -23,4 +23,8 @@ sealed class InstanceNotRunningState : IInstanceState {
public IInstanceState Stop() {
return this;
}
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
return Task.FromResult(false);
}
}

View File

@ -53,6 +53,19 @@ sealed class InstanceRunningState : IInstanceState {
return new InstanceStoppingState(context, session, sessionObjects);
}
public async Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
try {
context.Logger.Information("Sending command: {Command}", command);
await session.SendCommand(command, cancellationToken);
return true;
} catch (OperationCanceledException) {
return false;
} catch (Exception e) {
context.Logger.Warning(e, "Caught exception while sending command.");
return false;
}
}
public sealed class SessionObjects {
private readonly InstanceContext context;
private readonly InstanceSession session;

View File

@ -65,6 +65,10 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
return this; // TODO maybe provide a way to kill?
}
public Task<bool> SendCommand(string command, CancellationToken cancellationToken) {
return Task.FromResult(false);
}
public void Dispose() {
sessionObjects.Dispose();
}

View File

@ -61,4 +61,8 @@ public sealed class MessageListener : IMessageToAgentListener {
public async Task HandleStopInstance(StopInstanceMessage message) {
await socket.SendSimpleReply(message, await agent.InstanceSessionManager.Stop(message.InstanceGuid));
}
public async Task HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
await socket.SendSimpleReply(message, await agent.InstanceSessionManager.SendCommand(message.InstanceGuid, message.Command));
}
}

View File

@ -0,0 +1,21 @@
namespace Phantom.Common.Data.Replies;
public enum SendCommandToInstanceResult {
Success,
InstanceDoesNotExist,
AgentShuttingDown,
AgentCommunicationError,
UnknownError
}
public static class SendCommandToInstanceResultExtensions {
public static string ToSentence(this SendCommandToInstanceResult reason) {
return reason switch {
SendCommandToInstanceResult.Success => "Command sent.",
SendCommandToInstanceResult.InstanceDoesNotExist => "Instance does not exist.",
SendCommandToInstanceResult.AgentShuttingDown => "Agent is shutting down.",
SendCommandToInstanceResult.AgentCommunicationError => "Agent did not reply in time.",
_ => "Unknown error."
};
}
}

View File

@ -8,4 +8,5 @@ public interface IMessageToAgentListener {
Task HandleConfigureInstance(ConfigureInstanceMessage message);
Task HandleLaunchInstance(LaunchInstanceMessage message);
Task HandleStopInstance(StopInstanceMessage message);
Task HandleSendCommandToInstance(SendCommandToInstanceMessage message);
}

View File

@ -15,6 +15,7 @@ public static class MessageRegistries {
ToAgent.Add<ConfigureInstanceMessage>(2);
ToAgent.Add<LaunchInstanceMessage>(3);
ToAgent.Add<StopInstanceMessage>(4);
ToAgent.Add<SendCommandToInstanceMessage>(5);
ToServer.Add<RegisterAgentMessage>(0);
ToServer.Add<UnregisterAgentMessage>(1);

View File

@ -0,0 +1,14 @@
using MessagePack;
namespace Phantom.Common.Messages.ToAgent;
[MessagePackObject]
public sealed record SendCommandToInstanceMessage(
[property: Key(0)] uint SequenceId,
[property: Key(1)] Guid InstanceGuid,
[property: Key(2)] string Command
) : IMessageToAgent, IMessageWithReply {
public Task Accept(IMessageToAgentListener listener) {
return listener.HandleSendCommandToInstance(this);
}
}

View File

@ -144,6 +144,16 @@ public sealed class InstanceManager {
}
}
public async Task<SendCommandToInstanceResult> SendCommand(Guid instanceGuid, string command) {
var instance = GetInstance(instanceGuid);
if (instance != null) {
var reply = (SendCommandToInstanceResult?) await agentManager.SendMessageWithReply(instance.Configuration.AgentGuid, sequenceId => new SendCommandToInstanceMessage(sequenceId, instanceGuid, command), TimeSpan.FromSeconds(10));
return reply ?? SendCommandToInstanceResult.AgentCommunicationError;
}
return SendCommandToInstanceResult.InstanceDoesNotExist;
}
internal ImmutableArray<InstanceConfiguration> GetInstanceConfigurationsForAgent(Guid agentGuid) {
return instances.GetInstances().Values.Select(static instance => instance.Configuration).Where(configuration => configuration.AgentGuid == agentGuid).ToImmutableArray();
}

View File

@ -23,6 +23,20 @@ else {
}
<InstanceLog InstanceGuid="InstanceGuid" />
<div class="mb-3">
<form @onsubmit="ExecuteCommand" class="@(commandError == null ? "" : "is-invalid")">
<label for="command-input" class="form-label">Instance Name</label>
<div class="input-group flex-nowrap">
<span class="input-group-text" style="padding-top: 0.3rem;">/</span>
<input id="command-input" class="form-control" type="text" placeholder="command" @bind="commandInput" @bind:event="oninput" disabled="@(isSendingCommand || !Instance.Status.CanSendCommand())" @ref="commandInputElement" />
<button type="submit" class="btn btn-primary" disabled="@(string.IsNullOrWhiteSpace(commandInput) || isSendingCommand)">Execute</button>
</div>
</form>
<div class="invalid-feedback mt-2">
@commandError
</div>
</div>
}
@code {
@ -34,6 +48,11 @@ else {
private bool isLaunchingInstance = false;
private bool isStoppingInstance = false;
private ElementReference commandInputElement;
private string commandInput = string.Empty;
private string? commandError = null;
private bool isSendingCommand = false;
private Instance? Instance { get; set; }
protected override void OnInitialized() {
@ -64,6 +83,23 @@ else {
isStoppingInstance = false;
}
private async Task ExecuteCommand() {
isSendingCommand = true;
commandError = null;
await Task.Yield();
var result = await InstanceManager.SendCommand(InstanceGuid, commandInput);
if (result == SendCommandToInstanceResult.Success) {
commandInput = string.Empty;
}
else {
commandError = result.ToSentence();
}
isSendingCommand = false;
await commandInputElement.FocusAsync(preventScroll: true);
}
public void Dispose() {
InstanceManager.InstancesChanged.Unsubscribe(this);
}