Module nh_io_capture

Send what an application prints to NervesHub.

Description

Send what an application prints to NervesHub.

nh_logger catches what goes through logger. This catches io:format/2, io:put_chars/1, and Elixir's IO.puts/1 and IO.inspect/1 -- which is how a great deal of AtomVM code, including most examples, actually reports what it is doing.

  {ok, Capture} = nh_io_capture:start(#{agent => nerves_hub_link}),
  ok = nh_io_capture:attach(Capture)

attach/1 makes the capture process the calling process's group leader. io:format/2 resolves standard_io to the group leader and sends it an io_request, so everything that process prints arrives here.

Call it from the application's own process, early. spawn copies the parent's group leader, so every process started after that call is captured and every one started before it is not.

Sharp edges

**A missing reply hangs the printer.** io:execute_request/2 waits for {io_reply, Ref, _} in a receive with no timeout, so a group leader that does not answer a request blocks the process that printed, permanently. Every request is answered here, including shapes this does not understand, and the handling runs inside a try so that a crash still produces a reply. That is why this module answers first and asks questions later.

**It must never print through io.** Echoing with io:format/2 would send the capture process a request it is already handling. It echoes with console:print/1, and makes itself its own group leader so that anything inside it that does reach io goes straight to the console instead of back here.

**The text is unstructured.** io:format/2 carries no level, no module and no timestamp, so every line arrives at one level with the time it was received. Where structure matters, use logger and nh_logger; this is the net for code already written.

**Reading is answered with eof.** io:get_line/1 and friends would otherwise wait on a console this does not have.

**Attaching another process takes effect later.** erlang:group_leader/2 is synchronous only for the calling process; for any other it sends a signal and returns, so a process that prints immediately can print somewhere else first. Prefer attach/1 and inheritance to attach/2 on a process already running.

**logger_std_h output is captured too, and duplicates.** logger runs its handlers in the process that logged, and logger_std_h reports by printing, so with a capture attached every line logged reaches NervesHub twice: once structured from nh_logger, and once as the text logger_std_h printed. nh_console_h is logger_std_h printing straight to the console instead, and swapping it in resolves this.

What it cannot catch

ESP-IDF's own logging -- the I (1234) wifi: ... lines -- is written to the UART from C and never passes through Erlang.

Function Index

attach/1Equivalent to attach(Capture, self()).
attach/2Make Capture the group leader of Pid.
detach/1Hand a process back to the group leader it had before capture.
start/0Equivalent to start(#{}).
start/1Start a capture process.
stop/1

Function Details

attach/1

attach(Capture::pid()) -> ok

Equivalent to attach(Capture, self()).

attach/2

attach(Capture::pid(), Pid::pid()) -> ok

Make Capture the group leader of Pid.

Asynchronous for any process other than the caller, so anything Pid prints in the meantime goes to its old leader. attach/1 is synchronous.

detach/1

detach(Pid::pid()) -> ok

Hand a process back to the group leader it had before capture.

A process that is its own group leader is how AtomVM spells "no group leader" -- io:put_chars/1 checks for exactly that and calls console:print/1 instead of sending a request -- so this puts printing back on the console.

start/0

start() -> {ok, pid()}

Equivalent to start(#{}).

start/1

start(Opts::map()) -> {ok, pid()}

Start a capture process.

agent is the registered name or pid to send to, level the level every line is reported at, and echo whether to keep printing to the console -- on by default, because a device whose console goes quiet the moment you turn this on is a device nobody can debug.

stop/1

stop(Capture::pid()) -> ok


Generated by EDoc