20. Errors you can read
An app's error log is visible to its creator without a shell.
PHP warnings and job output land in RC_FACTS/log.txt within a minute, readable by the app and shown on the creator's dashboard.
This is the code serving the zoo right now: read from disk for this request, from commit
77ff74344d (staging). The zoo's own self-check fetches this
page and compares it byte for byte with the file it runs.
exhibits/20-errors-you-can-read.php sha256 2a9855e105d3 · raw
1<?php 2// Exhibit 20. An app's PHP errors and its jobs' output go to the journal; every minute root 3// copies the last 200 lines into RC_FACTS/log.txt, which the app (and its creator, on the 4// dashboard) can read without a shell. Here, only the zoo's deliberate errors are shown, since 5// the zoo is public. 6const ERROR_MARK = "zoo exhibit 20: a deliberate warning"; 7 8function error_lines(): array { 9 $lines = preg_grep("/" . preg_quote(ERROR_MARK, "/") . "/", explode("\n", (string)@file_get_contents(env("RC_FACTS") . "/log.txt"))); 10 return array_slice(array_values($lines), -6); 11} 12 13return [ 14 "n" => 20, "wing" => "Building", 15 "title" => "Errors you can read", 16 "promise" => "An app's error log is visible to its creator without a shell.", 17 "block" => "PHP warnings and job output land in RC_FACTS/log.txt within a minute, readable by the app and shown on the creator's dashboard.", 18 "show" => function (?array $me): string { 19 $l = error_lines(); 20 return '<p><button id="err">Make an error</button> <span class="out muted" id="err-out">It appears below within a minute.</span></p>' 21 . '<pre class="log">' . h($l ? implode("\n", $l) : "no deliberate errors in the log yet") . "</pre>"; 22 }, 23 "api" => function (string $do, ?array $me, array $in, bool $post): ?array { 24 if ($do !== "error" || !$post) return null; 25 $mark = bin2hex(random_bytes(4)); 26 trigger_error(ERROR_MARK . ", " . env("RC_ENV") . ", $mark", E_USER_WARNING); 27 return ["made" => $mark]; 28 }, 29 // Each run makes an error, and looks for the one the previous run made (root copies the log 30 // every minute, and the check runs every five). 31 "check" => function (): array { 32 $prev = meta("error_mark"); 33 [, , $j] = (new Browser())->post("https://" . env("RC_HOST") . "/api/20/error"); 34 if (empty($j["made"])) return [false, "the \"make an error\" button did not answer"]; 35 meta_set("error_mark", $j["made"] . "@" . time()); 36 if (!$prev) return [false, "made the first error; the next check looks for it in the log"]; 37 [$mark, $at] = explode("@", $prev); 38 $found = (bool)preg_grep("/" . preg_quote($mark, "/") . "/", error_lines()); 39 if (!$found && time() - (int)$at < 70) return [true, "made an error; the previous one is too recent to look for"]; 40 if (!$found) return [false, "the error made " . intdiv(time() - (int)$at, 60) . " minutes ago is not in RC_FACTS/log.txt"]; 41 return [true, "the error made " . intdiv(time() - (int)$at, 60) . " minutes ago is in RC_FACTS/log.txt"]; 42 }, 43 "script" => <<<'JS' 44document.getElementById("err")?.addEventListener("click", async () => { 45 const j = await zoo.call("/api/20/error", {}); 46 document.getElementById("err-out").textContent = j.error || ("Made error " + j.made + ". Reload in a minute to see it below."); 47}); 48 49JS, 50];