Search code examples
c#multithreadingtaskcancellation

Cancel a task running blocking methods


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) ?


Solution

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