Home > Enterprise >  Cancel a task running blocking methods
Cancel a task running blocking methods

Time:08-17

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.");
  • Related