Killing custom processes in Unity-based OS(-ish)

I’m working on a custom shell and a sort of pseudo-OS (process management etc., not virtualizing resources) in Unity. At the moment, it’s set up so the different processes inherit from an abstract Process class, and override Start, Update, and Exit, which are called right after creation, once per frame, and right before the process ends respectively. This is of course similar to how Unity script/components work (tho Unity’s OnDestroy is probably most similar to my process Exit). Now, everything is working fine at the moment (woo!), but there’s an edge case I’d like to be able to handle.

I have system in place to stop/cancel processes (with ctrl+C as expected). However, if someone were to create and then run a process like this:

[Process("exampleprocess", IsCommand = true, IsCritical = false, IsRoot = false)]
class ExampleProcess : Process
{
    public override void Update()
    {
        while (true)
        {
            //infinite loop BABYYYYYY!!!!
        }
    }
   
    public override void Exit() { }
}

//elsewhere in the code
ProcessManager.Run<ExampleProcess>();

Then the entire Unity application would crash, as it’s all in 1 thread. An OS(-ish) that entirely fails when one process encounters an error is kind of a shitty OS. In this sort of system, you should be able to ctrl+C the process and force quit it, even if the process is as poorly designed as the example above.

I currently have 2 thoughts on how to fix this issue. The first is running each process in its own thread, and then somehow cancelling/aborting the thread on ctrl+C as a last resort (and then still calling Exit so it can handle open resources). This feels like it would get cumbersome as more and more threads start running simultaneously and accessing shared resources. The second approach is leaving almost everything single-threaded, and just running the input manager in a separate thread, so it can continuously monitor for ctrl+C. Of course, then there’s the issue of how do you kill the infinite loop with this approach. Probably some reflection and potentially IL-related stuff. I think of the two, I prefer the 2nd if I can get it working.

If you have an idea for another approach, please feel free to share and I’ll happily consider it!

It’s worth noting that this was built with Unity 2017.4.13 (.NET Framework 4.7.2). Yes, I have a reason for using a 7 year old version. No, it’s not really relevant to the problem I’m proposing, though it may change what tools & features I have available at my disposal.

You absolutely should run these things in separate threads. But I think that’s kind of dodging the main issue here.

There’s are really good reasons that most “scripting frameworks” that games use are not something like “running C# in the same CLR that the game itself is running in”. What you have run into here is one such reason. I highly recommend running some kind of script framework on top of the C# runtime such as https://www.moonsharp.org/

The benefits with such a setup are:

  • You can tightly control which data the scripting engine has access to, which means the user can’t run wild and break things.
  • You can do things like enforce a time limit on the user’s running script and terminate when they run too long. (which would solve your infinite loop problem)
  • You can break up the user’s script execution to continue to render your graphics loop and maintain application responsiveness
3 Likes

Aside from the process manager itself, everything’s a process! That approach makes sense from more of an OS point of view, just look at Linux, but definitely isn’t the right approach if I’m just say, wanting to add a scripting console to a game. The entire project is the OS.

As of right now, there’s the Init process that’s the parent of all the rest (again, based on Linux), and then some other critical processes that run the whole time such as the Shell process, TextTerminal process (output/UI), InputManager, etc. Most “commands” and other user-run processes (which is primarily what this post is about) will be children of the Shell process, or children of other processes that themselves belong to the Shell. As basically everything is a process, rewriting it so that all processes are in Lua doesn’t make sense.

Users aren’t really going to be creating their own processes in this way, though I do have a LoadDll command which allows users to essentially mod in their own process definitions made with full access to everything. If someone breaks the game with a mod, that’s kinda on them lol, though assuming they aren’t trying to break things I don’t think a failed process should shut down the whole OS.

That said, for any processes made by the user within the OS I do like the idea having them interact more with that sort of limited scripting, so I do think I’ll implement Moonsharp or some equivalent as more user-facing. Of course, there’ll also be shell scripting available, so we’ll see how necessary the this all is. And ofc this project is bound to change over time.

For now, I’ll go ahead and set things up to use threads.
Thanks!

Who is that someone?

When you think of modding, go with the scripting solution.

If you intend to make a framework for other developers then ignore it. You cannot stop stupid developers from writing infinite loops. Moreover you cannot even prevent them from writing highly inefficient code that works fine but takes ten seconds to complete when it could complete in a tenth of a second.

If you want to write actual processes that deserve the name, you ought to have a ProcessManager running on the main thread that spawns processes on background threads with their Update methods returning an IEnumerator so they work like coroutines and can yield return some conditional. That way a Process developer can schedule his own process at his own leisure while ProcessManager is at liberty to kill any process that does not return within x milliseconds - much like Watchdog on iOS.

These can’t be actual processes because even though you write this system you cannot take control over the processes. Meaning you can’t do what the OS does with processes: time-slice them. You can’t at any point of your choosing merely suspend one of your processes and resume them at a later time without using actual System.Threading.Thread instances - and even those require cooperation of the developer because they can control whether their thread can be paused/aborted at this stage or not.

Anyway, this sort of project smells like overengineering UNLESS your goal is to provide a programmable, Unity runtime based operating system (although the purpose of its existence could still be questioned).
For that use case you may want to provide both options to make the project more compelling, a C# interface for the engaged and a Lua script interface for the uninitiated or quick prototyping from within the OS itself.

1 Like