Adventures with stdout

Recently, I started working on a cat(1)-like tool that uses splice(2) and sendfile(2). This tool has been published in a GitHub repo. Throughout the process I learned plenty more than I expected to about make(1) and stdout.

Motivation

After seeing fcat on GitHub, I became more interested. In the splice and sendfile syscalls. These syscalls are able to copy data between two file descriptors without the need to copy the data into userspace (as would usually be done by using read(2) and write(2)). Interestingly, splice(2) only works when one of the file descriptors is a pipe and sendfile(2) only works when neither file descriptor is a pipe. Based on fcat, it seems like some magic can be done to make splice(2) work for copying to stdout in all cases, but I wondered if it could be done more cleanly by using both splice(2) and sendfile(2).

Neither of these syscalls supports writing to a file descriptor that was opened with O_APPEND set.

Starting the project

Initially when testing the project, I was simply compiling by calling gcc directly. I didn’t actually intend to share the code, so setting up a whole project with GNU Make didn’t really seem worthwhile. After playing with it for a bit and getting it to work as I wanted it to, I realized that it’s not really much faster than cat(1) in many scenarios, but deciding it was a fun project, I decided to throw it on GitHub and create a Makefile for it.

The Makefile is rather boring. It sets my favorite CFLAGS and compiles and links the single .c file for the project.

It seemed good enough and I figured I was done with this adventure.

Oops

After finishing the Makefile and being content with the fact it succeeded in compiling the project, I decided to play around with it and try to actually use it. I realized that every once in awhile, it would hit the following if- statement.

if (stdout_append()) {
    fprintf(stderr, "Unable to append to files.\n");
    return EXIT_FAILURE;
}

stdout_append() here does about what you’d expect: it uses fcntl(2) to check the flags on STDOUT_FILENO and performs a bitwise and operation to see if O_APPEND is set. If it is, we print an error and exit with a failure.

This would last until I would close my current terminal (I use XTerm). When I opened a new terminal, it would happily work for awhile and then eventually suddenly stop working.

Around the time I nearly gave up, I realized that this would only happen after I rebuilt the project. I’d build the project, it wouldn’t work, I’d open a fresh terminal, it’d work, I’d rebuild, and it’d stop. After reading my code a million times, I started to wonder if there was something about make(1) that was messing with all this. It couldn’t be, right? Surely a program can’t change the mode of stdout, and if it can, there’s absolutely no way it’d ever be allowed for that change to remain in effect after the program terminates, right?

Wrong

Apparently, make(1) does modify the mode of stdout. It performs this operation in order to ensure that when several jobs are all writing to stdout, none of them cause issues if the writes overlap; however, make(1) is never kind enough to ensure that append mode is ever unset.

Setting it back

Clearly the solution would be unset O_APPEND for stdout; however, in some scenarios, the user may have (albeit, invalidly), asked to append to a file rather than overwrite it. One simple ./altcat in.txt >> out.txt and a user could have their file overwritten if O_APPEND is removed in all scenarios.

Luckily, unistd.h includes isatty(3) which can test whether or not a particular file descriptor refers to a terminal. In these cases, it is safe to remove the O_APPEND flag. Doing this is just as easy as setting it:

void remove_append(int fd) {
    int flags = fcntl(fd, F_GETFL);
    if (isatty(fd) && flags >= 0) {
        fcntl(fd, F_SETFL, flags ^ O_APPEND);
    }
}

Rather than using |, we just use ^.

Now altcat can print to stdout even after make(1) has gone ahead and made a mess of things. I have no idea whether XTerm or ZSH should do a better job of cleaning up stdout’s mode, but apparently it’s something applications might have to deal with if they can’t tolerate appending to file descriptors.