Files
RobustToolbox/Robust.Server/Console/ServerConsoleHost.cs
Pieter-Jan Briers 5057c91dcd Console command completions v1. (#2817)
* Console command completions v1.

I think it works™️

* Unify cvar commands

* Handle no-completions-at-all better.

* Don't crash if you tab complete while no completions available.

* Always show hints if available

* Properly null completion hint over the wire

* Unify help command, localize it.

* Clean up + localize cvar command.

* Remove debug logging

* List command unified & localized.

* Remove server completions debug logging

* Remote execute command.

Had to make everything async for this.

* Don't lower case enums or bools
Why

* GC commands converted and localized.

* Fix remote command completions.

Whoops

* Kick command completions

* lsasm unified & localized.

* Revert "Don't lower case enums or bools"

This reverts commit 2f825347c3.

* ToString gc_mode command enums instead of trying to fix Fluent.

Ah well.

* Unify szr_stats

* Unify log commands, completions

* Fix compile

* Improve completion with complex cases (quotes, escapes)

* Code cleanup, comments.

* Fix tab completion with empty arg ruining everything.

* Fix RegisteredCommand completions

* Add more complex completion options system.

* Refactor content directory entries into a proper resource manager API.

* Implement GetEntries for DirLoader

* Make type hint darker.

* Exec command autocomplete, pulled play global sound code out to engine.
2022-05-17 13:07:25 +10:00

271 lines
9.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Utility;
namespace Robust.Server.Console
{
/// <inheritdoc cref="IServerConsoleHost" />
internal sealed class ServerConsoleHost : ConsoleHost, IServerConsoleHost, IConsoleHostInternal
{
[Dependency] private readonly IConGroupController _groupController = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly ISystemConsoleManager _systemConsole = default!;
public override event ConAnyCommandCallback? AnyCommandExecuted;
/// <inheritdoc />
public override void ExecuteCommand(ICommonSession? session, string command)
{
var shell = new ConsoleShell(this, session);
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, ((IPlayerSession)session).ConnectedClient);
}
/// <inheritdoc />
public override void WriteLine(ICommonSession? session, string text)
{
if (session is IPlayerSession playerSession)
OutputText(playerSession, text, false);
else
OutputText(null, text, false);
}
/// <inheritdoc />
public override void WriteError(ICommonSession? session, string text)
{
if (session is IPlayerSession playerSession)
OutputText(playerSession, text, true);
else
OutputText(null, text, 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) =>
{
var localShell = shell.ConsoleHost.LocalShell;
var sudoShell = new SudoShell(this, localShell, shell);
Logger.Debug($"A: {string.Join(", ", args)}");
#pragma warning disable CA2012
return CalcCompletions(sudoShell, args);
#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 (AvailableCommands.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);
}
}
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((IPlayerSession)shell.Player, cmdName);
}
private void HandleRegistrationRequest(INetChannel senderConnection)
{
var netMgr = IoCManager.Resolve<IServerNetManager>();
var message = new MsgConCmdReg();
var counter = 0;
message.Commands = new MsgConCmdReg.Command[RegisteredCommands.Count];
foreach (var command in RegisteredCommands.Values)
{
message.Commands[counter++] = new MsgConCmdReg.Command
{
Name = command.Command,
Description = command.Description,
Help = command.Help
};
}
netMgr.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(IPlayerSession? session, string text, bool error)
{
if (session != null)
{
var replyMsg = new MsgConCmdAck();
replyMsg.Error = error;
replyMsg.Text = text;
NetManager.ServerSendMessage(replyMsg, session.ConnectedClient);
}
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);
var result = await CalcCompletions(shell, message.Args);
var msg = new MsgConCompletionResp
{
Result = result,
Seq = message.Seq
};
if (!message.MsgChannel.IsConnected)
return;
NetManager.ServerSendMessage(msg, message.MsgChannel);
}
private ValueTask<CompletionResult> CalcCompletions(IConsoleShell shell, string[] args)
{
// Logger.Debug(string.Join(", ", args));
if (args.Length <= 1)
{
// Typing out command name, handle this ourselves.
return ValueTask.FromResult(CompletionResult.FromOptions(
RegisteredCommands.Values.Select(c => new CompletionOption(c.Command, c.Description))));
}
var cmdName = args[0];
if (!AvailableCommands.TryGetValue(cmdName, out var cmd))
return ValueTask.FromResult(CompletionResult.Empty);
if (!ShellCanExecute(shell, cmdName))
return ValueTask.FromResult(CompletionResult.Empty);
return cmd.GetCompletionAsync(shell, args[1..], default);
}
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 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 WriteError(string text)
{
_owner.WriteError(text);
_sudoer.WriteError(text);
}
public void Clear()
{
_owner.Clear();
_sudoer.Clear();
}
}
}
}