shushrun
Run a command quietly unless it fails — then surface its full output, with stdout and stderr put back on their own file descriptors and in (best-effort) original order.
Why?
No single tool does both halves of this: log every run of a command (to journald or a file) and stay silent unless it fails, replaying the captured output only on a non-zero exit. chronic gives the silence but keeps no log; systemd-cat and tee log but always print. shushrun is the combination.
The problem
cron mails a job's output whenever it writes to stdout/stderr or exits non-zero. That's useful, but a job that prints harmless warnings to stderr on every run produces mail on every run. Capturing the warnings to silence them means that on a failure cron reports only that the job failed — the output then needed has already been discarded.
The naive fix — output="$(cmd 2>&1)"; [ $? -ne 0 ] && echo "$output" — loses the distinction between stdout and stderr, because merging the two streams is the one thing that destroys it.
shushrun captures both streams, always logs them, and on a non-zero exit replays stdout to stdout and stderr to stderr (in capture order) before exiting with the command's own code — so cron stays silent on success and mails the full, faithful picture on failure.
Behaviour
- Always: on a systemd host, re-run itself inside a transient unit (
systemd-run) so that shushrun and the command share one cgroup, and report that cgroup's CPU time and peak memory alongside the exit status. Nothing to turn on, nothing to configure; where systemd-run cannot be used the command simply runs as before. - Always: record the command's output — to journald by default (tagged with the command's basename; stdout at info, stderr at warning, plus an exit-status entry at err on failure / info on success), or to a file with
--log FILE(plain lines;--log-emojiprefixes🗒stdout /⚠stderr, or set custom prefixes with--log-stdout-prefix/--log-stderr-prefix). - Success (exit 0): emit nothing — cron stays quiet even if the command printed warnings.
- Failure (exit ≠ 0): replay stdout on stdout and stderr on stderr, in order, then exit with the command's exit code. The replay is capped at 256 KiB (
--replay-max-bytes) — past that it keeps the first and last half and elides the middle, marking what it dropped and where the whole run is, because mail too big to deliver can be discarded silently.
Installation
shushrun is a single-file, dependency-free tool on PyPI. Run it without installing via uv:
uvx shushrun -- mycmd --flags
or install the shushrun command with pipx install shushrun, uv tool install shushrun, or pip install shushrun. It needs only Python ≥ 3.9 and, for the default journald sink, a systemd host — no compiler, no C-extension wheel, nothing to build.
Compatibility
The 1.x series is not stable. Any release may change options, defaults, output, or the environment a command runs in, and nothing is kept working for backwards compatibility's sake.
Usage
shushrun [--reader poll|threads] [--log FILE] -- mycmd --flags
journald is the default sink, so dropping it in front of an existing cronjob needs no flags:
MAILTO=admin@example.com
17 3 * * * shushrun -- /usr/local/bin/myjob.sh
Every run is logged to journald (journalctl --identifier myjob.sh); cron mails only when myjob.sh exits non-zero. Pass --log FILE (or set SHUSHRUN_LOG) to write a file instead — there is no default file. The default journald sink needs no dependency (shushrun writes journald's native socket directly); it prefers the python3-systemd or cysystemd binding when one is installed, and shushrun --help shows which sink is in use. See Design decisions.
Environment variables
Every option has an environment-variable equivalent, named SHUSHRUN_<OPTION> — set a default once (in a cron file, a systemd unit's Environment=, or a shell profile) instead of repeating flags on every invocation. An explicit command-line flag takes precedence over its variable.
| Variable | Option | Example value |
|---|---|---|
SHUSHRUN_READER |
--reader |
poll or threads |
SHUSHRUN_LOG |
--log |
/var/log/myjob.log |
SHUSHRUN_LOG_EMOJI |
--log-emoji |
1 (also true / yes / on) |
SHUSHRUN_LOG_STDOUT_PREFIX |
--log-stdout-prefix |
O> |
SHUSHRUN_LOG_STDERR_PREFIX |
--log-stderr-prefix |
E> |
SHUSHRUN_REPLAY_MAX_BYTES |
--replay-max-bytes |
262144 (0 = exit status only, -1 = no cap) |
SHUSHRUN_STATS |
--stats |
1 |
Example
demo — a job that prints progress to stdout, a warning to stderr, and exits with the code it is given:
#!/bin/sh
echo "syncing 42 records"
echo "warning: connection retried once" >&2
echo "sync complete"
exit "${1:-0}"
Success is silent — a stderr warning alone produces no output, so cron stays quiet:
$ shushrun --log demo.log --log-emoji -- ./demo 0
$ echo $?
0
Failure replays both streams and re-exits with the command's code:
$ shushrun --log demo.log --log-emoji -- ./demo 1
syncing 42 records
sync complete
warning: connection retried once
./demo 1: exited with status 1
$ echo $?
1
(The streams come out grouped rather than interleaved: this three-line job finishes faster than the reader can interleave them — the best-effort ordering discussed below. Within each stream the order is always exact.)
Both runs are recorded — to journald by default, or to a file with --log. With --log-emoji (opt-in, used here) stdout lines are marked 🗒 and stderr ⚠:
$ cat demo.log
# 2026-06-16T10:27:15+07:00 exit 0 cmd: ./demo 0
🗒 syncing 42 records
🗒 sync complete
⚠ warning: connection retried once
# 2026-06-16T10:27:15+07:00 exit 1 cmd: ./demo 1
🗒 syncing 42 records
🗒 sync complete
⚠ warning: connection retried once
stdbuf matters
A C/libc program block-buffers stdout (4 KB) when it's a pipe but leaves stderr unbuffered, so stderr races ahead and the captured order is wrong before shushrun ever sees it. Prepend stdbuf --output=L --error=L to line-buffer the source. It's best-effort: no effect on Go, static binaries, or programs that set their own buffering.
The readers, and a concurrency note
--reader poll (default) is a single thread running select.poll() over both pipes. --reader threads runs one thread per stream. They produce equivalent results for any normal (non-CPU-bound) producer.
There is no meaningful GIL vs free-threaded difference here. A blocking read releases the GIL in CPython, so two reader threads already run their reads concurrently on a stock build; free-threading only parallelizes the nanosecond-scale Python work (append under a lock), which is never the bottleneck. This is an I/O-bound problem, and the GIL already steps out of the way for I/O. The single poll loop is simpler, has no lock, and is the one to use.
--reader threads --stats makes this observable: it prints the live gil state and the captured line count; between a stock python3 and a free-threaded python3.14t the gil flag flips True/False while the captured order barely moves.
The fundamental limit: order vs distinction
Exact cross-stream order and the stdout/stderr distinction cannot coexist. Once the command's two write() calls land in two separate kernel pipe buffers, nothing records which happened first — a reader cannot recover it, no matter how many CPUs it has. The only way to preserve exact order is to merge into one stream (2>&1), which is exactly what discards the distinction.
So shushrun keeps the distinction and accepts best-effort cross-stream order (exact within each stream, always). For a normal logger that's effectively perfect; it only clumps under sustained, simultaneous high-rate output on both streams.
Similar tools
shushrun sits in a well-trodden space; if it isn't the right fit, one of these likely is. Install lines are Debian/Ubuntu.
chronic — apt install moreutils
The closest match: run a command, show its output only if it fails, keeping stdout/stderr on their own fds. It does not interleave (it buffers each stream and prints stdout then stderr) and keeps no log. For quiet-unless-failed alone, with no log needed, this is the first choice — it's tiny and packaged everywhere.
$ chronic ./demo 0 # exit 0 (even with a stderr warning) -> silent
$ chronic ./demo 1 # failure -> dump, exit 1
syncing 42 records
sync complete
warning: connection retried once
cronic — apt install cronic
cronic — a classic shell script for taming cron spam. It treats a non-zero exit or any stderr as failure and prints a sectioned report. Policy difference worth noting: a stderr warning on an otherwise-successful run does trigger output, where shushrun and chronic stay silent.
$ cronic ./demo 0 # exit 0 but stderr present -> reported anyway
Cronic detected failure or error output for the command:
./demo 0
RESULT CODE: 0
ERROR OUTPUT:
warning: connection retried once
STANDARD OUTPUT:
syncing 42 records
sync complete
annotate-output — apt install devscripts
annotate-output(1) — not quiet-unless-failed; it always prints, prefixing every line with a timestamp and O:/E: so the streams stay distinguishable in one merged stream, in arrival order. Complementary to shushrun, and subject to the same best-effort ordering (here it happened to read stderr first):
$ annotate-output ./demo 0
10:27:15 I: Started ./demo 0
10:27:15 E: warning: connection retried once
10:27:15 O: syncing 42 records
10:27:15 O: sync complete
10:27:15 I: Finished with exitcode 0
ts (also apt install moreutils) timestamps arbitrary lines and pairs well with these. In the systemd world, a timer unit with OnFailure= plus journalctl is the non-wrapper alternative: journald records each line's stream and order, with alerting driven from the OnFailure handler.
Tests
tests/test_shushrun.py (pytest) is two tiers:
- Hard invariants (gate the build): silence on success, split replay on failure, the replay cap eliding only the middle, exit-code propagation, missing-command → 127, and the deterministic ordering guarantee — within-stream order is exact and no line is lost — each run against both readers.
- Measurements (do not gate): cross-stream
max_run/ mean displacement. These are scheduling-dependent with no fixed correct value, so they arexfail(strict=False)and recorded viarecord_property— they run and report their real numbers, but neither outcome fails the suite. (xfailis isolated to the flaky metric so it can't mask a real regression in the hard tier.)
pytest # green; flaky measurements never break it
pytest -rxX -s -k interleave # see the recorded numbers and XFAIL/XPASS
uv run --with pytest --python 3.13 pytest # GIL build
uv run --with pytest --python 3.14t pytest # free-threaded; gil flips, suite stays green
SHUSHRUN_BIN=/usr/local/bin/shushrun pytest # point at an installed binary
Visualizing the reordering
tests/visualize_ordering.py runs the producer (whose sent order is known to be strict o,e,o,e,…) through shushrun and prints the sent (TX) and received (RX) sequences as aligned, terminal-width-wrapped line pairs. Out-of-place characters are shown in UPPERCASE (lowercase = correct place), so the marking survives copy-paste; with colour on they also get a red background.
python3 tests/visualize_ordering.py --reader threads --delay-us 1000 # best-effort: scattered swaps
python3 tests/visualize_ordering.py --reader threads --delay-us 0 # bursty: order collapses
python3 tests/visualize_ordering.py --reader poll --delay-us 1000 # single reader: exact here
Design decisions
Why things are built this way — so a future reader can tell a deliberate choice from an accident, and knows what would justify changing it. Each entry has a Revisit if: when that becomes true, change the decision rather than carrying it forward unexamined.
journald sinks: native socket by default, bindings optional
record_to_journald logs to journald through the best of three sinks, tried in order, using the first available. shushrun --help prints the list, marks the one chosen, and gives the reason any are not available:
systemd.journal— the canonical binding: thepython3-systemddistro package (apt install python3-systemd) or thesystemd-pythonPyPI sdist (source-only — needslibsystemd-dev+ a C compiler). Theshushrun[systemd]extra pulls the latter.cysystemd— a separate PyPI implementation with the samejournal.send(MESSAGE=…, PRIORITY=…, SYSLOG_IDENTIFIER=…)API that ships prebuilt wheels, sopip/uvinstall it with no toolchain. Theshushrun[cysystemd]extra pulls it.- The native socket (stdlib) — with no binding installed, shushrun writes journald's submission protocol directly to
/run/systemd/journal/socket: framedKEY=VALUEdatagrams, length-prefixed for any value containing a newline. This is the default and needs nothing — no dependency, no wheel, no compiler — souvx shushrun/pip install shushrunworks on any interpreter. That is why the bindings are optional extras, not hard requirements.AF_UNIXdatagram sends are reliable (they block rather than drop when journald is busy).
Bindings rank first because they are maintained and handle one case the socket writer does not — a single message larger than the socket buffer, which a binding sends via a sealed memfd. The socket is the zero-dependency floor that makes the tool installable anywhere journald runs.
Revisit if: a single log line can exceed the datagram limit in practice — add the memfd path to the socket writer, or document installing a binding. If systemd-python starts shipping wheels, fold it and cysystemd into one recommended extra.
journald is the default destination (file is opt-in)
With no flags, shushrun -- cmd logs to journald; --log FILE (or SHUSHRUN_LOG) writes a file instead. There is intentionally no "log nowhere" default. The point is to drop in front of an existing command with zero ceremony, and journald is the right default on a systemd host: structured, queryable (journalctl --identifier <cmd>), and it carries the stream in the entry priority for free.
Revisit if: it's often run on non-systemd hosts and the missing-binding warning becomes noise — then auto-detect journald and fall back to silence, or make the default configurable.
No default log file path
--log has no built-in default; only an explicit --log or SHUSHRUN_LOG produces a file. An earlier version defaulted to ~/shushrun.log, which scatters stray, never-rotated files wherever the tool runs, under whatever user invoked it. Opt-in only.
Revisit if: never, probably — a surprise file per host has no upside.
File prefixes: plain by default, emoji via --log-emoji, or custom
--log files are plain by default; with --log-emoji, stdout lines are prefixed 🗒 and stderr ⚠ (journald distinguishes streams by PRIORITY regardless). Off by default because per-line emoji are a matter of taste — opinionated output shouldn't be forced on everyone. The emoji are only defaults — --log-stdout-prefix STR / --log-stderr-prefix STR override either prefix (used verbatim; pass '' to suppress one), so anyone who prefers ASCII (O: / E: ) or other glyphs sets them without touching the code. As for the prefix glyph itself: a [stderr] text prefix is verbose and marks only one stream. A hidden / zero-width per-line prefix was considered and rejected: invisible marking defeats the prefix's only purpose (a human can't see the stream), and zero-width bytes silently break grep, wc, copy-paste, and editors that strip "weird" characters. The prefixes are bare codepoints (U+1F5D2, U+26A0); append U+FE0F to force ⚠ to emoji width so the two columns align.
Revisit if: machine-parseable file logs are needed (use journald, or emit JSON lines). Emoji rendering badly is now a runtime fix, not a code change — pass ASCII via --log-stdout-prefix / --log-stderr-prefix.
Keep the stdout/stderr distinction; accept best-effort cross-stream order
shushrun preserves within-stream order exactly and cross-stream order best-effort. Exact order and the distinction can't coexist: once a process's two write()s land in separate kernel pipe buffers, nothing records which came first, so no reader can recover it at any CPU count; merging (2>&1) restores exact order but destroys the distinction.
Revisit if: a consumer needs exact order more than the split — merge the streams with 2>&1 (losing the distinction) rather than changing this.
Default reader is the single poll loop; threads is a teaching aid
--reader poll (one thread, select.poll) is the default; --reader threads exists but is documented as a lesson, not a speedup. This is I/O-bound, and in CPython a blocking read releases the GIL, so two reader threads already read concurrently on a stock build — and a free-threaded build only parallelizes the nanosecond-scale append. Measured: GIL vs free-threaded changes nothing here. poll is also simpler (no lock, no shared state) and deterministic.
Revisit if: the reader ever becomes CPU-bound (it won't for line-tagging).
stdbuf is the caller's job, not baked in
The tool doesn't auto-prepend stdbuf --output=L --error=L; it's documented for the caller to add when cross-stream order matters. stdbuf only affects libc stdio buffering — no effect on Go, static binaries, or programs that set their own — and it silently changes the child's behavior, so baking in a best-effort, sometimes-no-op change would be surprising.
Revisit if: only libc programs are ever wrapped and automatic line-buffering would be preferred.
The tool runs the command; it is not a pipe filter
Usage is shushrun -- cmd, not cmd | shushrun. A pipe gives the downstream tool only stdout (stderr bypasses it), and a shell pipeline's exit status is the last command's — so stderr would never reach journald or the mail, and "mail only on non-zero exit" could never fire. Running the command is the only way to capture both streams and propagate the real exit code.
Revisit if: never — this is structural, not a preference.
Capture fully, then record/replay after the command exits
Output is buffered per line in memory; journald/file writes and the failure replay happen after the command exits. This decouples logging from the job — a journald restart, a full disk, or a slow sink can't break, block, or SIGPIPE the wrapped command (a real risk with cmd | tee | systemd-cat). The cost: a run's journald entries appear at completion, not live.
Revisit if: a long job needs live progress — then stream through systemd-cat, accepting the coupling and SIGPIPE caveat.
shushrun puts itself inside the transient unit, and there is no option for it
On a systemd host shushrun re-executes itself under systemd-run and runs the command from in there, so both processes land in one cgroup. Wrapping only the command would have been simpler, and wrong: shushrun holds the entire captured run in memory, so it is the half most worth accounting for and limiting, and leaving it outside would measure the wrong process. That is also why no library is used — pystemd can create a transient unit, but nothing can move an already-running process into one, so re-executing is the mechanism either way and systemd-run needs no dependency, no wheel and no compiler.
The figures then come from the cgroup itself — cpu.stat and memory.peak under /sys/fs/cgroup, read at the end of the run — not from systemctl show. systemd turns accounting on by default, so nothing has to be requested, nothing has to be kept alive with RemainAfterExit to be read afterwards, and there is nothing to clean up.
Whether shushrun is already in a unit is decided by INVOCATION_ID, which systemd sets in the environment of every unit it starts. One check answers both questions: do not wrap what is already wrapped, and only report cgroup figures when they describe this run alone. It costs no invented variable, and it means a shushrun started from a timer or a service is left alone and still gets its figures.
Every option on the systemd-run command line is load-bearing: --pipe keeps stdin, stdout and stderr on their own file descriptors (--pty would merge the two output streams, the one thing this tool exists to avoid) and propagates the exit status; --quiet keeps systemd-run's own Running as unit … line out of the captured stderr; --collect removes the unit even when the run fails, which it otherwise leaves in systemctl --failed for good; and --working-directory plus one --setenv per variable are there because systemd-run otherwise hands the command the service manager's environment and working directory, silently changing what a job sees.
It is skipped, silently, when systemd is not running, systemd-run is not installed, the interpreter cannot say where it lives, or there is no manager to take the request — root talks to the system manager, everyone else to their own, because system scope wants interactive polkit authentication from a non-root caller and a cron job has nobody to answer it. --user needs the user bus, which exists only while that user has a session or loginctl enable-linger is set for them. None of this is worth a line of output on a host that can never satisfy it; --stats reports systemd-run=yes|no for anyone who wants to know.
Revisit if: a job needs resource limits rather than accounting — the properties (MemoryMax=, CPUQuota=) would need a way through, which is the first real argument for an option here.
The failure replay is capped by default, at one merged budget
replay_on_failure writes at most --replay-max-bytes (default 262144): the first and last half of the output, with the middle elided and replaced by a line naming how many lines and bytes were dropped and where the whole run is — the --log file, or the journalctl --identifier <basename> command for the journald default. 0 is a real budget of zero, mailing nothing but that marker and the exit status; -1 (any negative) removes the cap.
The cap exists because the replay is what cron mails, and mail that is too big can be silently discarded — no bounce, nothing in the sender's logs, so a failure alert becomes no alert at all. Measured against Slack's email-to-channel integration on 2026-08-01: its inbound endpoint answers 250 OK and then drops the message when the HTML file Slack renders from it exceeds 1 MiB (1048576 bytes). Plain text expands ≈2.05× when rendered (measured at both 80-byte and 990-byte line lengths), and the boundary is exact and reproducible — 6312 lines of 80 bytes post, 6313 do not. 256 KiB leaves the rendered mail at about half that ceiling.
On by default, because silent loss is the failure it prevents and an opt-in cap only protects those who already knew to ask for one. One merged budget over both streams rather than one each, because what it has to bound is the size of a single mail and a mail carries both. Nothing is lost: only the replay is capped, and the full run is always recorded to journald or --log.
Two consequences worth knowing:
- Lines are kept whole. The budget never falls in the middle of a line — a line that does not fit ends that side of the excerpt instead of being cut, so a single line longer than the whole budget is elided entirely and its side can come out empty. It is still counted in the marker, so an output that is one enormous line mails the marker alone rather than a fragment. The head is offered half the budget and the tail gets what the head left, then the head gets back what the tail could not spend either, so the excerpt is as long as the line lengths allow wherever the long lines happen to sit.
- The budget counts the replayed output only. The elision marker and the
exited with status Nline are written on top of it, so the mail is up to about 200 bytes larger than--replay-max-bytes.
Revisit if: the receiver's ceiling changes, or a consumer needs the whole replay in the mail — raise it or pass --replay-max-bytes -1.
Tests: invariants gate the build; ordering measurements don't
The pytest suite hard-asserts the deterministic guarantees (silence on success, split replay, exit-code propagation, within-stream order, no lines lost). Cross-stream ordering numbers are xfail(strict=False) and recorded, never gating — they're scheduling-dependent with no fixed correct value, so asserting a specific number would be flaky and would dress a best-effort property up as a guarantee. The xfail is isolated to the measurement so it can't mask a regression in the invariants.
Revisit if: an ordering property becomes deterministic (it won't, short of merging the streams).
TODO
- Don't break silence-on-success when journald is unreachable. With no binding installed and no journald socket (a non-systemd host), the native-socket write fails and shushrun warns to stderr on every run — which a successful cron job would then mail. Detect an absent
/run/systemd/journal/socketand fall back to a file, or to silence, instead of warning. - Bound memory on huge output. The whole run is buffered in memory before it is recorded and replayed — fine for normal logs, but a job that emits gigabytes uses that much RAM. Optionally spill to a temp file past a size threshold. Related: a single buffered line larger than the journald socket's datagram limit is dropped by the socket sink — the sealed-memfd path a binding provides would carry it.
Releasing
shushrun publishes to PyPI from GitLab CI via Trusted Publishing (OIDC): no API token is stored anywhere — PyPI mints a one-shot upload token from a short-lived GitLab id_token. To cut a release, bump version in pyproject.toml, commit, and push a matching tag:
git tag v1.0.1 && git push origin v1.0.1
The tag pipeline (.gitlab-ci.yml) builds the sdist + wheel and uploads them. The trusted publisher is registered on PyPI for namespace paulto, project shushrun, pipeline .gitlab-ci.yml, environment pypi.
License
GNU Affero General Public License v3.0 or later. Copyright (C) 2026 Paul Tobias. The Affero clause is the point: anyone who runs a modified shushrun as a network service must offer its source to those users — copyleft the SaaS loophole can't route around.
Release files for shushrun 1.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| shushrun-1.1.1.tar.gz | 38.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| shushrun-1.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 71.3 kB
Release files / shushrun-1.1.1.tar.gz
| Download URL | shushrun-1.1.1.tar.gz |
|---|---|
| Size | 38.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
03053c1d9949fb1f7feb8faaca03ddc645b074d5987ed2b361c3fcbe685364ab
|
|
BLAKE2b-256 checksum How to use checksums |
cfbb6ee1aec78aed8754c6d98d7438f6a65e06142e6e289ce68db2eb5ecc2102
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / shushrun-1.1.1-py3-none-any.whl
| Download URL | shushrun-1.1.1-py3-none-any.whl |
|---|---|
| Size | 32.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5a80dfe0361c91a356b5f98d25d0b13aac12fc068a3031a0e863b790a8966ae0
|
|
BLAKE2b-256 checksum How to use checksums |
91ec798cba0b4a23b8d42e08d8650c14cdfdcc5b2a97429d9f5d62cfeb5e4f06
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|