Based on recent bad experience trying to resurrect and understand ptyplumber.c, I thought I would write myself some notes about this program! The core of this program is the pthreadwrapper_data array of the following structure: typedef struct { bool is_in_use; bool is_thread; pid_t pid; pthread_t tid; int read_fd; int write_fd; bool has_pty; int master_fd; int slave_fd; pthread_mutex_t is_initialising; char *alias; char *partner_alias; } pthreadwrapper_rec; Tasks, which may be threads or "one-shot" functions (only the rather oddly named "human" is not a thread), write (not directly) into this structure to record if: the slot in the array is in use whether the task is a thread or not (only "human" is not) thread ID (only if is a thread, else zeroed memory) the fds that the "iorelay" task reads and writes to communicate with the task whether the task has a pty associated with it (a pty is a *bidirectional* pipe that has terminal settings like a terminal; see openpty(3)) master and slave fds (only if the task has a pty) a mutex which allows the task to signal to the task instantiator that the task has completed its initialisation the task's alias, which is used by many functions to read/write other attributes the task's "partner", which is the task that it has been plumbed in to. This program is arranged as follows: plb.c entry point; main function is mode_normal(), which calls pthreadwrapper_init to zero the above-described array, launches the "human" task (see description below) and then calls on of the models. Models are tiny functions that launch some tasks and plumb them together. thd.c library of pthread wrapper functions, which do pthread-ish things (e.g. create, join) and some file descriptor-ish things (e.g. close, select). tsk_iorelay.c All the other tasks communicate bidirectionally via their slave fd. This makes them pretty normal (with the exception that they don't have distinct input and output descriptors). They read the slave fd from the table and, just before they exit, the close that slave fd. However, te "iorelay" task is special: it makes heavy use of the above-described array. Firstly it looks what tasks are say they have open read descriptors *and* partners and then it does a select() on those until it can read something (possibly an EOF) from one of them and it relays that. The code for relaying an EOF is little more complicated because it's a moment to clear up some other fields. tsk_human.c This task is not a thread, which means it is just a normal function. As such we don't need a mutex to allow it to tell us when it has initialised. In fact, initialisation is the only thing it does: it writes 0 and 1 into the the read and write columns of the array. That's it! As an aside, consider the command: cat cat's stdin is explicitly closed by the terminal when you press CTRL-D, leading cat to stop reading and exit. However, cat does not explicitly close stdout: it simply exits. The OS then reclaims and closes all remaining open file descriptors, including stdout. Now consider: cat | wc -l In a pipeline, cat *must* close its stdout (the pipe's input) explicitly when it reads EOF from its stdin as it must signal EOF to the next process (wc). I mention this because the entering and removal of human's file descriptors 0 and 1 in the table is possibly not very symmetrical. But the above (which was from a discussion with ChatGPT) shows that it *shouldn't* be symmetrical. In the human case, which file descriptor is for what is a bit confusing. We need to think about the human as a task *on the other side of the screen from where plb runs*. For plb to send a message to the human, it writes to stdout, which, from the human's perspective, the human *reads*. Similarly, for plb to read a message from the human, it reads from stdin, which from the human's perspective, the human *write* to. The READ_FD and WRITE_FD attributes in the table describe how the *relay* task is to communicate with the specified task: it reads from 0 and writes to 1). The human person does the opposite: the human person reads what went to 1 and writes onto 0. Getting to the point now, consider: pulse <--> human Pulse eventually closes its stdout, the relay task, which relays data from pulse to human, reads EOF on human's output descriptor and relays EOF by closing human's input descriptor. Human does *not* close its own output handle (nor should it and nor should anybody else). So 0 got closed by relay and 1 stayed open. Now consider: human <--> sink The human person eventually pressed CTRL-D thereby closing stdin, the relay task, which relays data from human to sink, reads EOF on human's output descriptor and relays EOF by closing sink's input descriptor. So human *did* close its output desriptor. So 0 got closed *and* 1 got closed. So it's correct that there is not total symmetry here. tsk_cli.c tsk_clock.c tsk_dot.c tsk_quittee.c tsk_sink.c These are the other tasks. They all do something incredibly simple but this allows me to test the functionality of thd.c and iorelay.c. model_clock2human.c model_clock2sink.c model_dot2human.c model_human2quittee.c model_human2sink.c These are the tiny programs: they launch a couple of tasks, plumb the tasks together (currently it is not possible to plumb three or more tasks together in a ring, or feed output of one task to the input of *multiple* other tasks) and then wait for the tasks to exit. utils.c This contains functions, which could be moved to miniade, were it not for the fact that I wouldn't implement them in Perl/Python/bash.