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.
**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.
I (1234) wifi: ... lines -- is written to the
UART from C and never passes through Erlang.
| attach/1 | Equivalent to attach(Capture, self()).
|
| attach/2 | Make Capture the group leader of Pid. |
| detach/1 | Hand a process back to the group leader it had before capture. |
| start/0 | Equivalent to start(#{}).
|
| start/1 | Start a capture process. |
| stop/1 |
attach(Capture::pid()) -> ok
Equivalent to attach(Capture, self()).
attach(Capture::pid(), Pid::pid()) -> ok
Make Capture the group leader of Pid.
Pid prints
in the meantime goes to its old leader. attach/1 is synchronous.
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() -> {ok, pid()}
Equivalent to start(#{}).
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(Capture::pid()) -> ok
Generated by EDoc