11.5 Console Output

peinit writes its own operational messages to /dev/console :

  • Phase 1 progress — mount results, registryd starting.
  • Phase 2 progress — services starting and failing, dependency errors.
  • Shutdown progress.
  • Recovery mode entry.
  • Critical service failures.

Service output is never echoed to the console. The console is for peinit's own messages; a service that wants a terminal asks for one with TTYPath.

11.5.1 The line format #

Every line carries a fixed-width outcome tag, then the component name, then the message:

[      ] peinit: phase1 starting
[  OK  ] peinit: service authd started
[ SKIP ] peinit: service resolvd skipped: DependencyFailure
[ WARN ] peinit warning: calendar timer not armed: Malformed
[FAILED] peinit: service netd failed to launch: token materialisation failed
[ CRIT ] peinit: entering recovery: BootAttempts(3)

The tag column is the point of the format. An operator scanning a boot reads down one column rather than reading sentences, and a failure stops looking exactly like the twenty lines around it.

TagMeans
[ ]Progress with no outcome yet. Holds the column while receding.
[ OK ]It worked.
[ SKIP ]Deliberately not done.
[ WARN ]Wrong, but the boot continues.
[FAILED]It did not work.
[ CRIT ]The machine is about to be lost.

An [ OK ] line reads "mounted", never "mounting": the tag already claims the outcome, so the verb has to agree with it.

11.5.1.1 Tag and severity are different axes #

The tag is presentation. Severity decides who is allowed to print. They correlate but neither determines the other — a Normal service failing and a Critical service failing both render [FAILED] while carrying different severities — and collapsing them would quietly change what peios.quiet=2 suppresses.

11.5.1.2 Colour #

Tags are coloured with ANSI SGR escapes: green for OK, yellow for SKIP and WARN, red for FAILED, and white-on-red for CRIT. Untagged progress is never coloured.

Colour is decided once, from the kernel command line, following the same rule systemd uses: on unless the command line says TERM=dumb. peinit is PID 1 and has no inherited TERM to consult, so the command line is the only place an operator can say a console cannot render escapes. Deliberately not a peios.* token — that set is kept small on purpose, and TERM=dumb is a spelling people already know.

Colour changes the bytes and never the layout, so a serial log and a virtual terminal agree about where the message starts.

11.5.1.3 The stage banner #

Before Phase 1 does any work, peinit prints a banner naming the stage and the boot mode:

  ══════════════════════════════════════════════════════════════
   peinit · real root · PID 1 · Full boot                 v0.0.1
  ══════════════════════════════════════════════════════════════

It is punctuation rather than decoration. prelude prints the matching one when the initramfs takes over, so the two mark the handover from kernel to initramfs to real root — the one thing a boot log otherwise never says out loud.

The mode is the one this boot starts in: Full boot, Safe mode, or RECOVERY MODE. A later downgrade to Safe is announced by its own message rather than by reprinting a banner, because a second banner would read as a second stage.

Naming the mode is why the kernel command line is read at the very top of run_init, before any Phase 1 work. That reverses an earlier ordering which assumed mounting the virtual filesystems is what makes /proc/cmdline readable. It is not: the mount step reads /proc/self/mountinfo first, and /proc is provided by the initramfs. One consequence — a machine with both an unreadable command line and another Phase 1 fault now reports the command line as its recovery reason.

The banner carries ordinary status severity, so peios.quiet=2 drops it along with every other kind of progress.

11.5.1.4 Who else writes to this console #

The format is shared by specification, not by shared code. peinit, prelude and the hook scripts each implement it separately, because prelude is a size-critical initramfs PID 1 with no dependencies and the hooks are shell. Changing a tag word or a width means changing it in all three.

Everything peinit runs that reaches the console is in this format, including output peinit did not write itself. Autorun scripts are captured, not left to inherit peinit's streams, and their lines are relayed under the script's own file name :

[      ] 10-apply-seeds.sh: applied 20 file(s), 92 key(s); 0 failed
[  OK  ] peinit: ran 1 autorun script(s)

Relayed lines always carry the blank tag. peinit does not know whether a given line of somebody else's output is good news — the producer knows and peinit does not, and guessing from prose is how a rename silently turns an error green. The producer's outcome is reported separately, from its exit code, which peinit does know.

Relayed lines are sanitised first. A script sharing the console could otherwise steer it with an escape sequence, and peinit's own output shares the device. Three rules apply:

  • Control characters become ?.
  • A line is cut at 512 characters.
  • A producer is capped at 200 lines, with the remainder counted.

The only lines on a normal boot that are not in this format come from outside peinit's reach entirely: the firmware's own messages before any kernel runs, and a service that owns the terminal through TTYPath writing to it directly.

11.5.2 Severity and quiet #

Each message carries a severity, and peios.quiet (§2.6) decides what that means:

  • At 0, everything is written.
  • At 1, the default, peinit stays out of a terminal held as the controlling terminal of a running service, except to announce loss of the system. Terminals are matched by device rather than by path, since /dev/console and /dev/ttyS<n> can be the same device; where the device cannot be determined peinit assumes the terminal is held.
  • At 2, ordinary progress is dropped everywhere while errors still get through.

Suppressed messages are discarded rather than buffered for later.

Shutdown progress carries ordinary status severity, so peios.quiet=2 suppresses it along with every other kind of progress.

The autorun step in Phase 1 (§2.3) bypasses the policy entirely, on the grounds that a script running that early and going wrong is worth interrupting anything for. Its lines are still tagged.

11.5.3 Phase 1 registryd failures #

One deliberate exception to "service output is never echoed to the console". peinit gives every service capture pipes and drains them in the runtime loop — but Phase 1 has no loop, so a registryd that printed exactly why it could not serve was reported to the operator as nothing but registryd readiness timeout expired before READY=1, with the reason sitting unread in a pipe.

When Phase 1 registryd fails to become ready, or says READY=1 and then cannot serve, peinit now drains those pipes and relays what it found :

[FAILED] peinit: registryd failed; what it said follows
[      ] registryd(stderr): open hive Machine: permission denied

Failure path only. On a normal boot the descriptors are retained for the runtime, which drains them into the pre-eventd buffer and on to eventd like any other service's output — so registryd's startup lines are queryable with evctl and are not on the console.

The drain is safe to run from PID 1 because the read ends are non-blocking: a registryd that is alive and silent yields EAGAIN rather than hanging the boot.

Edit this page