Zoo

staging

← back to the zoo

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];