Skip to content

fix copy_buf_to_buf: flush partial lines when source pipe closes#1

Merged
HoratiuVultur merged 1 commit intomasterfrom
master.fix-partial-line-drain-on-child-exit
Apr 9, 2026
Merged

fix copy_buf_to_buf: flush partial lines when source pipe closes#1
HoratiuVultur merged 1 commit intomasterfrom
master.fix-partial-line-drain-on-child-exit

Conversation

@jeso-mchp
Copy link
Copy Markdown
Contributor

@jeso-mchp jeso-mchp commented Mar 3, 2026

Flush remaining output data when a child process's pipe is closed, even if the data doesn't end with a newline.

copy_buf_to_buf() only forwards data to the output buffer when one of three conditions is met:

  1. A complete line is found (terminated by \n)
  2. The line exceeds LINE_SIZE_MAX
  3. A 1-second line_timeout() expires

When a child process writes output without a trailing newline and exits immediately, the main loop terminates before any of these conditions are met:

  1. Child writes partial line to stdout and exits
  2. SIGCHLD sets child_running = 0
  3. copy_to_buffer() reads remaining pipe data and gets EOF, closing the fd
  4. copy_buf_to_buf() finds no \n, line isn't too long, and line_timeout() returns false (< 1s elapsed)
  5. Data stays in the read buffer, unformatted
  6. Next iteration: all fds are closed, cnt == 0, loop breaks
  7. EXIT message is emitted and STDOUT data silently lost

This was discovered via easyframes (ef), which prints an error without a trailing newline:

po("ERROR: Frame stack size is too big");

When ef was invoked with 29+ tx clauses through er, the error was lost and the test framework only saw "Exit 255" with no explanation, which made debugging harder.

Fix

Check whether the source pipe has been closed (from->fd == -1) before falling through to line_timeout(). When the pipe is closed, any remaining data is the final output from the child and should be flushed immediately as a normal ER-L line:

} else if (from->fd == -1) {
    /* Source pipe closed, make sure we clean the pipe even if there is
     * no newline.
     */
    prefix_size = snprintf(prefix_buf, PREFIX_SIZE, "ER-L-%05d-%s-%s ",
                           pid, ts(ts_buf, 32), prefix_msg_buf);
    copy_to_tx = 1;

This is placed before the line_timeout check since it is a more specific condition. There is no point waiting for a timeout on a pipe that will never produce more data.

copy_buf_to_buf() only forwards data to the output buffer when it
finds a complete line (terminated by '\n'), the line exceeds
LINE_SIZE_MAX, or a 1-second timeout expires.  When a child process
writes output without a trailing newline and exits immediately, the
main loop can terminate before any of these conditions are met:

  1. Child writes partial line to stdout and exits.
  2. SIGCHLD sets child_running = 0.
  3. copy_to_buffer() reads pipe data and gets EOF, closing the fd.
  4. copy_buf_to_buf() finds no '\n', line isn't too long, and
     line_timeout() returns false (< 1s elapsed).
  5. Data stays in the read buffer, unformatted.
  6. Next iteration: all fds closed, cnt == 0, loop breaks.
  7. EXIT message emitted STDOUT data silently lost.

This was discovered via easyframes (ef), which prints an error
without a trailing newline:

  po("ERROR: Frame stack size is too big");

When ef was invoked with 29+ tx clauses through er, the error was
lost and the test framework only saw "Exit 255" with no explanation.

Fix this by checking whether the source pipe has been closed
(from->fd == -1) before falling through to line_timeout().  When
the pipe is closed, any remaining data is the final output from
the child and should be flushed immediately as a normal ER-L line.
@jeso-mchp jeso-mchp requested a review from HoratiuVultur March 3, 2026 13:41
@HoratiuVultur HoratiuVultur merged commit 91df50d into master Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants