mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* Add ProfManager.Value guard, write first command argument as a ProfManager value in ExecuteInShell * Make EntitySystemManager use the new Value method
357 lines
12 KiB
C#
357 lines
12 KiB
C#
using System;
|
|
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.Profiling;
|
|
using Robust.Shared.Toolshed;
|
|
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!;
|
|
[Dependency] private readonly ProfManager _prof = 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;
|
|
|
|
var cmdName = args[0];
|
|
using var _ = _prof.Group(cmdName);
|
|
|
|
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 toolshedCommands = _toolshed.DefaultEnvironment.AllCommands();
|
|
message.Commands = new List<MsgConCmdReg.Command>(AvailableCommands.Count + toolshedCommands.Count);
|
|
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 shedRes = _toolshed.GetCompletions(shell, message.ArgString);
|
|
if (shedRes == null)
|
|
goto done;
|
|
|
|
IEnumerable<CompletionOption> options = result?.Options ?? Array.Empty<CompletionOption>();
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|