mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
* Move to Central Package Management. Allows us to store NuGet package versions all in one place. Yay! * Update NuGet packages and fix code for changes. Notable: Changes to ILVerify. Npgsql doesn't need hacks for inet anymore, now we need hacks to make the old code work with this new reality. NUnit's analyzers are already complaining and I didn't even update it to 4.x yet. TerraFX changed to GetLastSystemError so error handling had to be changed. Buncha APIs have more NRT annotations. * Remove dotnet-eng NuGet package source. I genuinely don't know what this was for, and Central Package Management starts throwing warnings about it, so YEET. * Fix double loading of assemblies due to ALC shenanigans. Due to how the "sideloading" code for the ModLoader was set up, it would first try to load Microsoft.Extensions.Primitives from next to the content dll. But we already have that library in Robust! Chaos ensues. We now try to forcibly prioritize loading from the default ALC first to avoid this. * Remove Robust.Physics project. Never used. * Remove erroneous NVorbis reference. Should be VorbisPizza and otherwise wasn't used. * Sandbox fixes * Remove unused unit test package references. Castle.Core and NUnit.ConsoleRunner. * Update NUnit to 4.0.1 This requires replacing all the old assertion methods because they removed them 🥲 * Mute CA1416 (platform check) errors TerraFX started annotating APIs with this and I can't be arsed to entertain this analyzer so out it goes. * Fine ya cranky, no more CPM for Robust.Client.Injectors * Changelog * Oh so that's what dotnet-eng was used for. Yeah ok that makes sense. * Central package management for remaining 2 robust projects * Ok that was a bad idea let's just use NUnit 3 on the analyzer test project * Oh right forgot to remove this one * Update to a newer version of RemoteExecutor * Disable RemoteExecutor test https://github.com/dotnet/arcade/issues/8483 Yeah this package is not well maintained and clearly we can't rely on it. * Fix immutable list serialization
406 lines
15 KiB
C#
406 lines
15 KiB
C#
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<ICommonSession, Dictionary<int, ScriptInstance>> _instances =
|
|
new();
|
|
|
|
private ISawmill _sawmill = default!;
|
|
|
|
public void Initialize()
|
|
{
|
|
_sawmill = _logManager.GetSawmill("script");
|
|
|
|
_netManager.RegisterNetMessage<MsgScriptStop>(ReceiveScriptEnd);
|
|
_netManager.RegisterNetMessage<MsgScriptEval>(ReceiveScriptEval);
|
|
_netManager.RegisterNetMessage<MsgScriptStart>(ReceiveScriptStart);
|
|
_netManager.RegisterNetMessage<MsgScriptCompletion>(ReceiveScriptCompletion);
|
|
_netManager.RegisterNetMessage<MsgScriptCompletionResponse>();
|
|
_netManager.RegisterNetMessage<MsgScriptResponse>();
|
|
_netManager.RegisterNetMessage<MsgScriptStartAck>();
|
|
|
|
_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<LiteResult>();
|
|
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<Diagnostic> 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)
|
|
{
|
|
}
|
|
}
|
|
}
|