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.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Players; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Utility; namespace Robust.Server.Console { /// 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!; [Dependency] private readonly ToolshedManager _toolshed = default!; public ServerConsoleHost() : base(isServer: true) {} public override event ConAnyCommandCallback? AnyCommandExecuted; /// public override void ExecuteCommand(ICommonSession? session, string command) { var shell = new ConsoleShell(this, session, session == null); ExecuteInShell(shell, command); } /// 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); } /// public override void WriteLine(ICommonSession? session, string text) { var msg = new FormattedMessage(); msg.AddText(text); if (session is IPlayerSession playerSession) OutputText(playerSession, msg, false); else OutputText(null, msg, false); } public override void WriteLine(ICommonSession? session, FormattedMessage msg) { if (session is IPlayerSession playerSession) OutputText(playerSession, msg, false); else OutputText(null, msg, false); } /// public override void WriteError(ICommonSession? session, string text) { var msg = new FormattedMessage(); msg.AddText(text); if (session is IPlayerSession playerSession) OutputText(playerSession, msg, true); else OutputText(null, msg, true); } public bool IsCmdServer(IConsoleCommand cmd) => true; /// public void Initialize() { RegisterCommand("sudo", "sudo make me a sandwich", "sudo ", (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 CalcCompletions(sudoShell, args, argStr); #pragma warning restore CA2012 }); LoadConsoleCommands(); // setup networking with clients NetManager.RegisterNetMessage(ProcessCommand); NetManager.RegisterNetMessage(); NetManager.RegisterNetMessage(message => HandleRegistrationRequest(message.MsgChannel)); NetManager.RegisterNetMessage(HandleConCompletions); NetManager.RegisterNetMessage(); } private void ExecuteInShell(IConsoleShell shell, string command) { try { var args = new List(); 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); foreach (var err in ctx.GetErrors()) { ctx.WriteLine(err.Describe()); } 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((IPlayerSession)shell.Player, cmdName); } private void HandleRegistrationRequest(INetChannel senderConnection) { var message = new MsgConCmdReg(); var counter = 0; var toolshedCommands = _toolshed.DefaultEnvironment.AllCommands().ToArray(); message.Commands = new MsgConCmdReg.Command[AvailableCommands.Count + toolshedCommands.Length]; foreach (var command in AvailableCommands.Values) { message.Commands[counter++] = new MsgConCmdReg.Command { Name = command.Command, Description = command.Description, Help = command.Help }; } foreach (var spec in toolshedCommands) { message.Commands[counter++] = new MsgConCmdReg.Command { Name = spec.FullName(), 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(IPlayerSession? session, FormattedMessage 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, false); var result = await CalcCompletions(shell, message.Args, message.ArgString); if ((result.Options.Length == 0 && result.Hint is 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; shedRes ??= CompletionResult.Empty; result = new CompletionResult(shedRes.Options.Concat(result.Options).ToArray(), shedRes.Hint ?? result.Hint); } done: var msg = new MsgConCompletionResp { Result = result, Seq = message.Seq }; if (!message.MsgChannel.IsConnected) return; NetManager.ServerSendMessage(msg, message.MsgChannel); } private ValueTask CalcCompletions(IConsoleShell shell, string[] args, string argStr) { // Logger.Debug(string.Join(", ", args)); if (args.Length <= 1) { // Typing out command name, handle this ourselves. return ValueTask.FromResult(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 ValueTask.FromResult(CompletionResult.Empty); if (!ShellCanExecute(shell, cmdName)) return ValueTask.FromResult(CompletionResult.Empty); return cmd.GetCompletionAsync(shell, args[1..], argStr, 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 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(); } } } }