using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.CSharp.Scripting.Hosting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Scripting; using Microsoft.CodeAnalysis.Text; using Robust.Server.Console; using Robust.Server.Player; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Player; using Robust.Shared.Reflection; using Robust.Shared.Scripting; using Robust.Shared.Utility; using static Robust.Shared.Network.Messages.MsgScriptCompletionResponse; #nullable enable namespace Robust.Server.Scripting { internal sealed class ScriptHost : IScriptHost { [Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IConGroupController _conGroupController = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!; [Dependency] private readonly IDependencyCollection _dependencyCollection = default!; [Dependency] private readonly ILogManager _logManager = default!; readonly Dictionary> _instances = new(); private ISawmill _sawmill = default!; public void Initialize() { _sawmill = _logManager.GetSawmill("script"); _netManager.RegisterNetMessage(ReceiveScriptEnd); _netManager.RegisterNetMessage(ReceiveScriptEval); _netManager.RegisterNetMessage(ReceiveScriptStart); _netManager.RegisterNetMessage(ReceiveScriptCompletion); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged; } private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { // GC it up. _instances.Remove(e.Session); } private void ReceiveScriptEnd(MsgScriptStop message) { if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session)) { return; } if (!_instances.TryGetValue(session, out var instances)) { return; } instances.Remove(message.ScriptSession); } private void ReceiveScriptStart(MsgScriptStart message) { var reply = new MsgScriptStartAck(); reply.ScriptSession = message.ScriptSession; reply.WasAccepted = false; if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session)) { _netManager.ServerSendMessage(reply, message.MsgChannel); return; } if (!_conGroupController.CanScript(session)) { _sawmill.Warning("Client {0} tried to access Scripting without permissions.", session); _netManager.ServerSendMessage(reply, message.MsgChannel); return; } var instances = _instances.GetOrNew(session); if (instances.ContainsKey(message.ScriptSession)) { // Already got one with this ID, client's problem. _netManager.ServerSendMessage(reply, message.MsgChannel); return; } ScriptInstanceShared.InitDummy(); var instance = new ScriptInstance(_reflectionManager, _dependencyCollection); instances.Add(message.ScriptSession, instance); reply.WasAccepted = true; _netManager.ServerSendMessage(reply, message.MsgChannel); } private async void ReceiveScriptEval(MsgScriptEval message) { if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session)) { return; } if (!_conGroupController.CanScript(session)) { _sawmill.Warning("Client {0} tried to access Scripting without permissions.", session); return; } if (!_instances.TryGetValue(session, out var instances) || !instances.TryGetValue(message.ScriptSession, out var instance)) { return; } var replyMessage = new MsgScriptResponse(); replyMessage.ScriptSession = message.ScriptSession; var code = message.Code; if (code == "y" && instance.AutoImportRepeatBuffer.HasValue) { var (imports, repeatCode) = instance.AutoImportRepeatBuffer.Value; var sb = new StringBuilder(); foreach (var import in imports) { sb.AppendFormat("using {0};\n", import); } sb.Append(repeatCode); code = sb.ToString(); replyMessage.WasComplete = true; } else { instance.InputBuffer.AppendLine(code); var tree = SyntaxFactory.ParseSyntaxTree(SourceText.From(instance.InputBuffer.ToString()), ScriptInstanceShared.ParseOptions); if (!SyntaxFactory.IsCompleteSubmission(tree)) { replyMessage.WasComplete = false; _netManager.ServerSendMessage(replyMessage, message.MsgChannel); return; } replyMessage.WasComplete = true; code = instance.InputBuffer.ToString().Trim(); instance.InputBuffer.Clear(); } Script newScript; if (instance.State != null) { newScript = instance.State.Script.ContinueWith(code); } else { var options = ScriptInstanceShared.GetScriptOptions(_reflectionManager); newScript = CSharpScript.Create(code, options, typeof(ScriptGlobals)); } // Compile ahead of time so that we can do syntax highlighting correctly for the echo. newScript.Compile(); // Echo entered script. var echoMessage = new FormattedMessage(); ScriptInstanceShared.AddWithSyntaxHighlighting(newScript, echoMessage, code, instance.HighlightWorkspace); replyMessage.Echo = echoMessage; var msg = new FormattedMessage(); try { instance.RunningScript = true; if (instance.State != null) { instance.State = await newScript.RunFromAsync(instance.State, _ => true); } else { instance.State = await newScript.RunAsync(instance.Globals, _ => true); } } catch (CompilationErrorException e) { msg.PushColor(Color.Crimson); foreach (var diagnostic in e.Diagnostics) { msg.AddText(diagnostic.ToString()); msg.AddText("\n"); } PromptAutoImports(e.Diagnostics, code, msg, instance); replyMessage.Response = msg; _netManager.ServerSendMessage(replyMessage, message.MsgChannel); return; } finally { instance.RunningScript = false; } if (!instance.OutputBuffer.IsEmpty) { msg.AddMessage(instance.OutputBuffer); instance.OutputBuffer.Clear(); } if (instance.State.Exception != null) { msg.PushColor(Color.Crimson); msg.AddText(CSharpObjectFormatter.Instance.FormatException(instance.State.Exception)); } else if (ScriptInstanceShared.HasReturnValue(newScript)) { msg.AddText(ScriptInstanceShared.SafeFormat(instance.State.ReturnValue)); } replyMessage.Response = msg; _netManager.ServerSendMessage(replyMessage, message.MsgChannel); } private async void ReceiveScriptCompletion(MsgScriptCompletion message) { if (!_playerManager.TryGetSessionByChannel(message.MsgChannel, out var session)) return; if (!_conGroupController.CanScript(session)) { _sawmill.Warning("Client {0} tried to access Scripting without permissions.", session); return; } if (!_instances.TryGetValue(session, out var instances) || !instances.TryGetValue(message.ScriptSession, out var instance)) return; var replyMessage = new MsgScriptCompletionResponse(); replyMessage.ScriptSession = message.ScriptSession; // Everything below here cribbed from // https://www.strathweb.com/2018/12/using-roslyn-c-completion-service-programmatically/ var workspace = new AdhocWorkspace(MefHostServices.Create(MefHostServices.DefaultAssemblies)); var scriptProject = workspace.AddProject(ProjectInfo.Create( ProjectId.CreateNewId(), VersionStamp.Create(), "Script", "Script", LanguageNames.CSharp, isSubmission: true ) .WithMetadataReferences( _reflectionManager.Assemblies.Select(a => MetadataReference.CreateFromFile(a.Location)) ) .WithCompilationOptions(new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, usings: ScriptInstanceShared.DefaultImports ))); var document = workspace.AddDocument(DocumentInfo.Create( DocumentId.CreateNewId(scriptProject.Id), "Script", sourceCodeKind: SourceCodeKind.Script, loader: TextLoader.From(TextAndVersion.Create(SourceText.From(message.Code), VersionStamp.Create())) )); var results = await (CompletionService .GetService(document)? .GetCompletionsAsync(document, message.Cursor) ?? Task.FromResult(CompletionList.Empty)); var ires = ImmutableArray.CreateBuilder(); foreach (var r in results.ItemsList) ires.Add(new LiteResult( displayText: r.DisplayText, displayTextPrefix: r.DisplayTextPrefix, displayTextSuffix: r.DisplayTextSuffix, inlineDescription: r.InlineDescription, tags: r.Tags, properties: r.Properties )); replyMessage.Results = ires.ToImmutable(); _netManager.ServerSendMessage(replyMessage, message.MsgChannel); } private void PromptAutoImports( IEnumerable diags, string code, FormattedMessage output, ScriptInstance instance) { if (!ScriptInstanceShared.CalcAutoImports(_reflectionManager, diags, out var found)) return; output.AddText($"Auto-import {string.Join(", ", found)} (enter 'y')?"); instance.AutoImportRepeatBuffer = (found.ToArray(), code); } private sealed class ScriptInstance { public Workspace HighlightWorkspace { get; } = new AdhocWorkspace(); public StringBuilder InputBuffer { get; } = new(); public FormattedMessage OutputBuffer { get; } = new(); public bool RunningScript { get; set; } public ScriptGlobals Globals { get; } public ScriptState? State { get; set; } public (string[] imports, string code)? AutoImportRepeatBuffer; public ScriptInstance(IReflectionManager reflection, IDependencyCollection dependency) { Globals = new ScriptGlobalsImpl(this, reflection, dependency); } } private sealed class ScriptGlobalsImpl : ScriptGlobals { private readonly IReflectionManager _reflectionManager; private readonly ScriptInstance _scriptInstance; public ScriptGlobalsImpl( ScriptInstance scriptInstance, IReflectionManager refl, IDependencyCollection dependency) : base(dependency) { _reflectionManager = refl; _scriptInstance = scriptInstance; } protected override void WriteSyntax(object toString) { if (_scriptInstance.RunningScript && toString?.ToString() is {} code) { var options = ScriptInstanceShared.GetScriptOptions(_reflectionManager); var script = CSharpScript.Create(code, options, typeof(ScriptGlobals)); script.Compile(); var syntax = new FormattedMessage(); ScriptInstanceShared.AddWithSyntaxHighlighting(script, syntax, code, _scriptInstance.HighlightWorkspace); _scriptInstance.OutputBuffer.AddMessage(syntax); } } public override void write(object toString) { if (_scriptInstance.RunningScript && toString.ToString() is {} value) { _scriptInstance.OutputBuffer.AddText(value); } } public override void show(object obj) { write(ScriptInstanceShared.SafeFormat(obj)); } } } [SuppressMessage("ReSharper", "InconsistentNaming")] [PublicAPI] public abstract class ScriptGlobals : ScriptGlobalsShared { protected ScriptGlobals(IDependencyCollection dependencies) : base(dependencies) { } } }