Files
RobustToolbox/Robust.Server/Console/ServerConsoleHost.cs
T
Pieter-Jan Briers a77eee5658 Fix async console command completions on ServerConsoleHost
Fixes #4828

Asynchronous console command completions were just not being run on the server, the wrong function was being called. Hooray.

This caused sudo to break because it actually uses an async command completion (as other command completions it invokes might in turn be async).
2024-02-23 00:22:58 +01:00

362 lines
12 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Utility;
namespace Robust.Server.Console
{
/// <inheritdoc cref="IServerConsoleHost" />
[Virtual]
internal class ServerConsoleHost : ConsoleHost, IServerConsoleHost, IConsoleHostInternal
{
[Dependency] private readonly IConGroupController _groupController = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly ISystemConsoleManager _systemConsole = default!;
[Dependency] private readonly ToolshedManager _toolshed = default!;
public ServerConsoleHost() : base(isServer: true) {}
public override event ConAnyCommandCallback? AnyCommandExecuted;
/// <inheritdoc />
public override void ExecuteCommand(ICommonSession? session, string command)
{
var shell = new ConsoleShell(this, session, session == null);
ExecuteInShell(shell, command);
}
/// <inheritdoc />
public override void RemoteExecuteCommand(ICommonSession? session, string command)
{
if (!NetManager.IsConnected || session is null)
return;
var msg = new MsgConCmd();
msg.Text = command;
NetManager.ServerSendMessage(msg, session.Channel);
}
/// <inheritdoc />
public override void WriteLine(ICommonSession? session, string text)
{
var msg = new FormattedMessage();
msg.AddText(text);
OutputText(session, msg, false);
}
public override void WriteLine(ICommonSession? session, FormattedMessage msg)
{
OutputText(session, msg, false);
}
/// <inheritdoc />
public override void WriteError(ICommonSession? session, string text)
{
var msg = new FormattedMessage();
msg.AddText(text);
OutputText(session, msg, true);
}
public bool IsCmdServer(IConsoleCommand cmd) => true;
/// <inheritdoc />
public void Initialize()
{
RegisterCommand("sudo", "sudo make me a sandwich", "sudo <command>", (shell, argStr, _) =>
{
var localShell = shell.ConsoleHost.LocalShell;
var sudoShell = new SudoShell(this, localShell, shell);
ExecuteInShell(sudoShell, argStr["sudo ".Length..]);
}, (shell, args, argStr) =>
{
var localShell = shell.ConsoleHost.LocalShell;
var sudoShell = new SudoShell(this, localShell, shell);
#pragma warning disable CA2012
return CalcCompletionsOrEmpty(sudoShell, args, argStr);
#pragma warning restore CA2012
});
LoadConsoleCommands();
// setup networking with clients
NetManager.RegisterNetMessage<MsgConCmd>(ProcessCommand);
NetManager.RegisterNetMessage<MsgConCmdAck>();
NetManager.RegisterNetMessage<MsgConCmdReg>(message => HandleRegistrationRequest(message.MsgChannel));
NetManager.RegisterNetMessage<MsgConCompletion>(HandleConCompletions);
NetManager.RegisterNetMessage<MsgConCompletionResp>();
}
private void ExecuteInShell(IConsoleShell shell, string command)
{
try
{
var args = new List<string>();
CommandParsing.ParseArguments(command, args);
// missing cmdName
if (args.Count == 0)
return;
string? cmdName = args[0];
if (RegisteredCommands.TryGetValue(cmdName, out var conCmd)) // command registered
{
args.RemoveAt(0);
var cmdArgs = args.ToArray();
if (!ShellCanExecute(shell, cmdName))
{
shell.WriteError($"Unknown command: '{cmdName}'");
return;
}
AnyCommandExecuted?.Invoke(shell, cmdName, command, cmdArgs);
conCmd.Execute(shell, command, cmdArgs);
}
else
{
// toolshed time
_toolshed.InvokeCommand(shell, command, null, out var res, out var ctx);
bool anyErrors = false;
foreach (var err in ctx.GetErrors())
{
anyErrors = true;
ctx.WriteLine(err.Describe());
}
// why does ctx not have any write-error support?
if (anyErrors)
shell.WriteError($"Failed to execute toolshed command");
shell.WriteLine(FormattedMessage.FromMarkupPermissive(_toolshed.PrettyPrintType(res, out var more, moreUsed: true)));
ctx.WriteVar("more", more);
}
}
catch (Exception e)
{
LogManager.GetSawmill(SawmillName)
.Error($"{FormatPlayerString(shell.Player)}: ExecuteError - {command}:\n{e}");
shell.WriteError($"There was an error while executing the command: {e}");
}
}
private bool ShellCanExecute(IConsoleShell shell, string cmdName)
{
return shell.Player == null || _groupController.CanCommand(shell.Player, cmdName);
}
private void HandleRegistrationRequest(INetChannel senderConnection)
{
var message = new MsgConCmdReg();
var counter = 0;
var toolshedCommands = _toolshed.DefaultEnvironment.AllCommands().ToArray();
message.Commands = new List<MsgConCmdReg.Command>(AvailableCommands.Count + toolshedCommands.Length);
var commands = new HashSet<string>();
foreach (var command in AvailableCommands.Values)
{
if (!commands.Add(command.Command))
{
Sawmill.Error($"Duplicate command: {command.Command}");
continue;
}
message.Commands.Add(new MsgConCmdReg.Command
{
Name = command.Command,
Description = command.Description,
Help = command.Help
});
}
foreach (var spec in toolshedCommands)
{
var name = spec.FullName();
if (!commands.Add(name))
{
Sawmill.Warning($"Duplicate toolshed command: {name}");
continue;
}
message.Commands.Add(new MsgConCmdReg.Command
{
Name = name,
Description = spec.Cmd.Description(spec.SubCommand),
Help = spec.Cmd.GetHelp(spec.SubCommand)
});
}
NetManager.ServerSendMessage(message, senderConnection);
}
private void ProcessCommand(MsgConCmd message)
{
string? text = message.Text;
var sender = message.MsgChannel;
var session = _players.GetSessionByChannel(sender);
LogManager.GetSawmill(SawmillName).Info($"{FormatPlayerString(session)}:{text}");
ExecuteCommand(session, text);
}
private void OutputText(ICommonSession? session, FormattedMessage text, bool error)
{
if (session != null)
{
var replyMsg = new MsgConCmdAck();
replyMsg.Error = error;
replyMsg.Text = text;
NetManager.ServerSendMessage(replyMsg, session.Channel);
}
else
_systemConsole.Print(text + "\n");
}
private static string FormatPlayerString(ICommonSession? session)
{
return session != null ? $"{session.Name}" : "[HOST]";
}
private async void HandleConCompletions(MsgConCompletion message)
{
var session = _players.GetSessionByChannel(message.MsgChannel);
var shell = new ConsoleShell(this, session, false);
var result = await CalcCompletions(shell, message.Args, message.ArgString);
if ((result == null) || message.Args.Length <= 1)
{
var parser = new ParserContext(message.ArgString, _toolshed);
CommandRun.TryParse(true, parser, null, null, false, out _, out var completions, out _);
if (completions == null)
{
goto done;
}
var (shedRes, _) = await completions.Value;
IEnumerable<CompletionOption> options = result?.Options ?? Array.Empty<CompletionOption>();
if (shedRes != null)
options = options.Concat(shedRes.Options);
var hints = result?.Hint ?? shedRes?.Hint;
result = new CompletionResult(options.ToArray(), hints);
}
done:
result ??= CompletionResult.Empty;
var msg = new MsgConCompletionResp
{
Result = result,
Seq = message.Seq
};
if (!message.MsgChannel.IsConnected)
return;
NetManager.ServerSendMessage(msg, message.MsgChannel);
}
private async ValueTask<CompletionResult> CalcCompletionsOrEmpty(IConsoleShell shell, string[] args, string argStr)
{
return await CalcCompletions(shell, args, argStr) ?? CompletionResult.Empty;
}
/// <summary>
/// Get completions. Non-null results imply that the command was handled. If it is empty, it implies that
/// there are no completions for this command.
/// </summary>
private async ValueTask<CompletionResult?> CalcCompletions(IConsoleShell shell, string[] args, string argStr)
{
// Logger.Debug(string.Join(", ", args));
if (args.Length <= 1)
{
// Typing out command name, handle this ourselves.
return CompletionResult.FromOptions(
AvailableCommands.Values.Where(c => ShellCanExecute(shell, c.Command)).Select(c => new CompletionOption(c.Command, c.Description)));
}
var cmdName = args[0];
if (!RegisteredCommands.TryGetValue(cmdName, out var cmd))
return null;
if (!ShellCanExecute(shell, cmdName))
return CompletionResult.Empty;
return await cmd.GetCompletionAsync(shell, args[1..], argStr, CancellationToken.None);
}
private sealed class SudoShell : IConsoleShell
{
private readonly ServerConsoleHost _host;
private readonly IConsoleShell _owner;
private readonly IConsoleShell _sudoer;
public SudoShell(ServerConsoleHost host, IConsoleShell owner, IConsoleShell sudoer)
{
_host = host;
_owner = owner;
_sudoer = sudoer;
}
public IConsoleHost ConsoleHost => _host;
public bool IsServer => _owner.IsServer;
public ICommonSession? Player => _owner.Player;
public bool IsLocal => _owner.IsLocal;
public void ExecuteCommand(string command)
{
_host.ExecuteInShell(this, command);
}
public void RemoteExecuteCommand(string command)
{
_owner.RemoteExecuteCommand(command);
}
public void WriteLine(string text)
{
_owner.WriteLine(text);
_sudoer.WriteLine(text);
}
public void WriteLine(FormattedMessage message)
{
_owner.WriteLine(message);
_sudoer.WriteLine(message);
}
public void WriteError(string text)
{
_owner.WriteError(text);
_sudoer.WriteError(text);
}
public void Clear()
{
_owner.Clear();
_sudoer.Clear();
}
}
}
}