--- a/helperlink.c 2026-09-23 13:43:37 +++ b/helperlink.c 2026-09-23 13:43:37 @@ -1,21 +1,25 @@ -/* EXERCISE 08 — BROKEN ON PURPOSE. Do not copy this shape into real code. +/* EXERCISE 08 — REPAIRED. * * An export feature streams a document to a helper process over a pipe. The * helper does the conversion and is expected to outlive the transfer. * - * Symptom as reported by the field: "if the helper crashes mid-export, our app - * vanishes too. No alert, no crash report we can read, nothing in our logs — - * the process is simply gone. It only happens when the helper dies first." + * The repair: turn "your process is terminated" into "your write returns an + * error you can handle". F_SETNOSIGPIPE suppresses SIGPIPE for ONE descriptor, + * so write(2) fails with EPIPE instead. The alternative, signal(SIGPIPE, + * SIG_IGN), has the same effect but process-wide: a library must not make that + * choice on its host application's behalf, which is why the per-descriptor + * control exists. (Sockets have the equivalent SO_NOSIGPIPE socket option.) * + * The second half of the repair is not a flag: a peer that can die is part of + * the API. This version reports the loss, stops, and exits deliberately. + * * Build and run: - * clang -O2 -g -Wall -Wextra helperlink.c -o /tmp/helperlink_broken - * /tmp/helperlink_broken ; echo "exit status: $?" + * clang -O2 -g -Wall -Wextra helperlink.c -o /tmp/helperlink_fixed + * /tmp/helperlink_fixed ; echo "exit status: $?" * - * EXPECTED: this program is KILLED BY A SIGNAL. From a shell that is exit - * status 141, which is 128 + 13, and 13 is SIGPIPE. That is the whole bug: - * the default disposition of SIGPIPE is to terminate the process, so a write - * to a pipe with no reader is not an error your code gets to handle — it is - * the end of your process. + * EXPECTED: exit status 0, with helperLost=1 in the output. The transfer still + * fails — the helper really is gone — but it fails as a reportable outcome + * rather than as the sudden disappearance of the whole process. * * Every measurement is printed as key=value so a script can assert on it. */ @@ -26,6 +30,7 @@ #include #include #include +#include #include #define CHUNK 8192 @@ -64,24 +69,34 @@ } close(p[0]); + /* Scope the change to this descriptor. The host application's own SIGPIPE + * disposition is none of our business. */ + if (fcntl(p[1], F_SETNOSIGPIPE, 1) != 0) { perror("F_SETNOSIGPIPE"); return 1; } + char *doc = malloc(CHUNK); memset(doc, 'D', CHUNK); long written = 0; + int helper_lost = 0; printf("phase=streaming\n"); for (int i = 0; i < CHUNKS; i++) { - /* No error from this call is ever observed when the reader is gone, - * because the process does not survive long enough to read errno. */ ssize_t w = write(p[1], doc, CHUNK); if (w < 0) { - printf("phase=write-error errno=%d message=%s\n", errno, strerror(errno)); + if (errno == EPIPE) { + /* The peer is gone. This is an expected outcome of talking to + * another process, not an exceptional one. */ + helper_lost = 1; + printf("phase=peer-gone errno=%d message=%s\n", errno, strerror(errno)); + } else { + printf("phase=write-error errno=%d message=%s\n", errno, strerror(errno)); + } break; } written += w; } printf("phase=finished bytesWritten=%ld\n", written); - printf("helperLost=0\n"); + printf("helperLost=%d\n", helper_lost); printf("reportedCleanly=1\n"); close(p[1]); int st; waitpid(helper, &st, 0);