porch(1), part one

5 minute read Published: 2024-10-10

Today, we'll be looking at some software that I've been working on since January 2024, porch(1).

porch(1) is a lua-based (p)rogram (orch)estration tool and library. You can think of it like expect(1), but without as many features and primarily intended as a testing tool. We won't get too in-depth with this post to encourage me to try and write more consistently, but today we'll at least talk about motivation and high level design goals. In a future post, we'll dive deeper into how it's designed and included functionality.

Motivation

I was looking at a tty bug that came in to bugzilla. The short version is that we didn't handle VEOF termination of a line correctly if the application requested one-byte short of the VEOF. Instead of terminating the line and dropping the VEOF token, we would chew through the line and the next read(2) would interpret VEOF alone as an empty line and signal EOF instead of blocking for more input.

I fixed the bug as well as some other bugs in the area, particularly in recanonicalizing on tty configuration changes. I broke things in the process because we didn't really have a good way to cover any of this in testing.

Naturally, something like expect(1) would make sense for testing with a pty. I've used expect(1) quite a bit in the past, but I'm not a fan of it. My biggest annoyance with it is that it's a lot to grok. For instance, take a look at the manpage: it's very well documented, but there are a lot of commands to sort through to try and understand what you want. A close second is that it uses TCL, which requires significant effort for me to page back in when I need expect(1) and want to do anything even remotely complicated since my uses of it are so far and few between. Finally, I'm not a fan of the spawn_id design, but that's a minor nit.

Thus, porch(1) is born. I wanted something relatively simple, and I wanted it in a language that I use at least semi-frequently. Bonus points if it's a language that is available in FreeBSD base, as I tend to think that something we can test interactive programs with would be an excellent addition. We can bring in ports for testing, but it's nice to avoid having to skip tests in runs by developers/users/!ci.freebsd.org if we can help it and documentation of all of the ports one needs for a full test run is historically lacking (and hard to place). Lua is thus the natural choice, fitting all of the bullet points and being reasonably elegant for the limited set that we need.

High-Level Design Overview

porch(1) is designed to execute exactly one program at a time. This limitation is lifted if you instead opt to use the library in your own lua script instead, but we'll talk about that in more detail in a later post. Before we go further, please refer to this execution diagram:

As depicted in the diagram, an .orch script spawns a program, but the program does not begin execution immediately. Instead, the forked child moves into a process hold until it's released by the .orch script, either implicitly or explicitly. The process hold exists so that we can queue up any input or tty changes before the program has a chance to observe stdin- this might be necessary to avoid some forms of races if we, e.g., disable canonicalization. The child is implicitly released by calling release(), or it's explicitly released by attempting to match some program output. The script may then either wait until eof or simply spawn something else and start all over again.

Now let's slide over and see what happens from the child process perspective:

Many of these steps will look familiar. Notice that following the child release, the child moves into the run state. porch(1) won't attempt to do anything hinky (e.g., ptrace) to try and manipulate the process flow- once it's released, it runs freely until one of these events:

To "close" the process may require a few steps. If it hasn't exited naturally by the time we want to close it, we will send it a SIGINT and wait for some time for it to exit somewhat gracefully. If it still hasn't exited after we hit our timeout, then we'll issue a SIGKILL and reap it. This isn't the most graceful exit regimen, but for most of what I'm wanting to test it works well enough.

Conclusion

This concludes our first post on porch(1). Next time, we'll dive in further and discuss how porch(1) processes .orch scripts, the features that are made available for scripts, and some of the niceties we get from using Lua.