I'm using a Lua interpreter library ('NeoLua') to allow the user to write code in my app. I'm running the interpreter in a task. I would like to be able to start and stop the program using two buttons, but I can't find a way to end the task reliably.
public partial class Form1 : Form {
Lua lua = new();
LuaGlobal g;
Task interpreterTask;
private void StartProgram() {
g = lua.CreateEnvironment();
dynamic dg = g;
dg.custom_function = new Action<int>(CustomFunction); // add my custom functions to the lua environment
interpreterTask = Task.Factory.StartNew(() => {
string pgrm = System.IO.File.ReadAllText("program.lua");
g.DoChunk(pgrm, "program.lua");
}
);
}
private void StopProgram() {
// ?
}
public void CustomFunction(int x) {
// do stuff
}
}
I could easily use the CancellationTokenSource
class to cancel the task inside the custom functions that I implement in C#, since they should regularily be called in the Lua program. But what if the user writes an infinite loop ? The DoChunk
method (that comes from an external library which does not provide any way of properly stopping the interpreter) would never return and I would never be able to check for cancellation.
I am looking for a way to completely 'kill' the task, similarily to how I would kill a process or abort a thread (which I can't do because I'm using .NET 6.0). Whould I have better luck by running the interpreter in a separate thread ? Should I just stick with checking for a cancellation tocken in each function (and force the user to close the whole app if they unintentionally create an infinite loop) ?
CodePudding user response:
There is no way to kill a task (or abort a thread) in .NET 6.
In .NET Framework, ApplicationDomain
was available to isolate this sort of plug-in/untrusted code from the rest of your application. Due to the complexity of supporting and maintaining that in a cross-platform manner, it was not carried forward to .NET Core.
An option still available to run untrusted code in a robust manner is to create a child process to run the code, and have the parent (your code) and child (plug-in code) communicate via interprocess communication.
Here's a simple implementation using pipes for communication:
Program.cs (main process)
using System.Diagnostics;
using System.IO.Pipes;
var childPath = @"..\..\..\..\Child\bin\Debug\net6.0\Child.exe";
Console.WriteLine($"Starting child process from {childPath}");
using var pipeServer =
new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);
var child = new Process();
child.StartInfo.FileName = childPath;
child.StartInfo.UseShellExecute = false;
child.StartInfo.Arguments = pipeServer.GetClientHandleAsString();
child.Start();
await Task.Delay(1000);
try
{
using var sr = new StreamReader(pipeServer);
while (!child.HasExited)
{
var line = sr.ReadLine();
if (line == "exit")
break;
if (int.TryParse(line, out var result))
CustomFunction(result);
}
}
catch (IOException e)
{
Console.WriteLine("[SERVER] Error: {0}", e.Message);
}
// If you want to kill the process before it exits:
//child.Kill();
await child.WaitForExitAsync();
Console.WriteLine("Parent says bye.");
void CustomFunction(int x)
{
Console.WriteLine($"Received {x} from child process");
}
Program.cs (child process)
using System.IO.Pipes;
Console.WriteLine("Child running.");
if (args.Length > 0)
{
using var pipeClient =
new AnonymousPipeClientStream(PipeDirection.Out, args[0]);
using var sw = new StreamWriter(pipeClient);
var dataForParent = new List<string>() { "1", "1", "2", "3", "5", "8", "13", "exit" };
foreach (var item in dataForParent)
sw.WriteLine(item);
}
else
Console.WriteLine("No pipe passed in.");
Console.WriteLine("All data written.");
// Give the parent time to finish processing.
// In production code, you probably want to signal the child to self-terminate
await Task.Delay(1000);
Console.WriteLine("Child says bye.");