Buffer mode change

I am using ppscheck to monitor with PPS status recently, and found a problem for the tool.

The task is quite simple. It checks the PPS status, with ppscheck, queries the output periodically, and it is done with Python subprocess.

Sample code is like this:

cmd = "sudo ppscheck /dev/ttyS0"
args = shlex.split(cmd)
proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
fd = proc.stdout.fileno()

poller = select.epoll()
poller.register(fd, select.EPOLLIN)

I create a process and register the stdout fd to epoll, but after the running the fd is not readable.

I try to execute the command by shell, the output is normal. And then I redirect all the outputs to one file, but find the file is empty!

# running OK
$ sudo ppscheck /dev/ttyS0

# redirect all output to file, file is empty
$ sudo ppscheck /dev/ttyS0 > pps.log 2>&1
$ cat pps.log
$

The first thing came to me is that the ppscheck tool buffers the output, and furthermore the mode is not line buffered.

And after some search I find stdbuf can be use to change the buffer mode of a program.

After add “stdbuf -oL” to the command, and the problem is solved.

cmd = "sudo stdbuf -oL ppscheck /dev/ttyS0"

And now I am wondering how stdbuf is implemented to achieve this goal.

It may read the output of the program, and when receive newline and then make a flush() call? Then I realize it can't be done in this way, since the original program is buffered, you will wait until the buffer is full and produce the outputs.

So I check the source code of stdbuf, the main code is as following:

/* main function */
if (! set_libstdbuf_options ())
  {
    error (0, 0, _("you must specify a buffering mode option"));
    usage (EXIT_CANCELED);
  }

/* Try to preload libstdbuf first from the same path as
   stdbuf is running from.  */
set_program_path (program_name);
if (!program_path)
  program_path = xstrdup (PKGLIBDIR);  /* Need to init to non-NULL.  */
set_LD_PRELOAD ();
free (program_path);

execvp (*argv, argv);

int exit_status = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
error (0, errno, _("failed to run command %s"), quote (argv[0]));
return exit_status;

/* set_libstdbuf_options */
if (*stdbuf[i].optarg == 'L')
  ret = asprintf (&var, "%s%c=L", "_STDBUF_",
                  toupper (stdbuf[i].optc));
else
  ret = asprintf (&var, "%s%c=%" PRIuMAX, "_STDBUF_",
                  toupper (stdbuf[i].optc),
                  (uintmax_t) stdbuf[i].size);
if (ret < 0)
  xalloc_die ();

if (putenv (var) != 0)
{
  die (EXIT_CANCELED, errno,
       _("failed to update the environment with %s"),
       quote (var));
}

The above logic is very simple, basically it only set buffer options, and then executed the program with execvp.

The key part is how the buffer mode is set, the program use a nice way to archive this, via LD_PRELOAD trick.

The stdbuf has a nother part called libstd.

Stdbuf set two kinds of environment variables before run the program. The first one is the buffer mode, variables are _STDBUF_I, _STDBUF_O and _STDBUF_E for stdin, stdout and stderr. The second one is the LD_PRELOAD environment variable, adding libstdbuf. And after libstdbuf is loaded, the buffer mode is set, the code is as following:

/* Use __attribute to avoid elision of __attribute__ on SUNPRO_C etc.  */
static void __attribute ((constructor))
stdbuf (void)
{
  char *e_mode = getenv ("_STDBUF_E");
  char *i_mode = getenv ("_STDBUF_I");
  char *o_mode = getenv ("_STDBUF_O");
  if (e_mode) /* Do first so can write errors to stderr  */
    apply_mode (stderr, e_mode);
  if (i_mode)
    apply_mode (stdin, i_mode);
  if (o_mode)
    apply_mode (stdout, o_mode);
}

/* core part of apply_mode */
if (setvbuf (stream, buf, setvbuf_mode, size) != 0)
  {
    fprintf (stderr, _("could not set buffering of %s to mode %s\n"),
             fileno_to_name (fileno (stream)), mode);
    free (buf);
  }

The __attribute ((constructor)) is the entry point of shared library in GCC.

So when libstdbuf.so is loaded, stdbuf function is called and buffer mode is set.

And after that, the original program is executed normally.

#stdbuf #libstdbuf #ld #LD\_PRELOAD #ppscheck #coreutil