#lang scribble/manual @(require scribble/manual (for-label racket/base racket/system racket/string "../private/webui-wire-download.rkt" "../private/webui-wire-ipc.rkt") ) @title{WebUI Wire IPC Bridge} @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @defmodule[webui-wire-ipc] This module provides a small IPC abstraction around the external @tt{webui-wire} executable. It is responsible for: @itemlist[ @item{starting the @tt{webui-wire} process (via @racket[ww-get-webui-wire-command]);} @item{reading and decoding its stdout and stderr streams;} @item{dispatching incoming events to a callback;} @item{serialising access to the process so it can be shared by multiple threads.} ] The public API consists of a single function, @racket[webui-ipc], which starts the helper process and returns a function for sending commands and receiving replies. The helper process itself implements a simple text-based protocol; this module hides those details and presents a synchronous “send a string, get a string back” interface. @section{IPC entry point} @defproc[ (webui-ipc [event-queuer (-> string? any)] [log-processor (-> symbol? string? any)]) (-> string? string?) ]{ Starts the @tt{webui-wire} executable and returns a function that can be used to send commands to it. The arguments: @itemlist[ @item[@racket[event-queuer]]{ A callback that receives event payloads produced by the helper process. Whenever the process emits an @tt{EVENT} line on stderr, the textual payload after the @tt{"EVENT:"} tag is passed to this callback as: @racketblock[ (event-queuer line) ] The callback is executed in the context of a dedicated reader thread and should return quickly (for example by placing the event on a queue to be handled elsewhere). } @item[@racket[log-processor]]{ A callback used for all non-event messages and error conditions. It is called as: @racketblock[ (log-processor kind msg) ] where @racket[kind] is a symbol describing the source or category of the message (for example @racket['stderr-reader] or a tag from the incoming line) and @racket[msg] is a human-readable string. } ] The return value is a @emph{command function} with the contract @racket[(-> string? string?)]: @itemlist[ @item{When you call it with a command string, it sends that command to the @tt{webui-wire} process and blocks until one reply is received.} @item{The reply payload (decoded as UTF-8) is returned as a plain string.} @item{If anything goes wrong with the IPC protocol (invalid prefix, unexpected EOF, etc.), an exception is raised.} ] A typical usage pattern: @racketblock[ (define (enqueue-event line) ;; Handle incoming EVENT messages from webui-wire (printf "EVENT: ~a\n" line)) (define (log-message kind msg) ;; Central logging hook (printf "[~a] ~a\n" kind msg)) ;; Start the IPC bridge and obtain the command function: (define send-cmd (webui-ipc enqueue-event log-message)) ;; Send a command (no newline required) and wait for the reply: (define reply (send-cmd "HELLO 42")) (printf "Reply was: ~a\n" reply) ] Notes: @itemlist[ @item{The command function writes a line to the process stdin using @racket[displayln]; callers do @emph{not} need to append a newline themselves.} @item{Calls to the command function are executed one at a time, even when invoked from multiple threads (see @secref["ipc-internal-concurrency"]).} ] } @section[#:tag "ipc-protocol"]{Protocol overview (informative)} The @tt{webui-wire} process uses a simple length-prefixed text protocol on both stdout and stderr. This section describes the format for reference; the details are handled internally by this module. @subsection{Message framing} Each message from the helper has the following structure: @itemlist[ @item{An 8-character decimal length prefix (ASCII digits),} @item{followed by a single colon character @tt{":"},} @item{followed by @emph{exactly} that many bytes of payload,} @item{followed by a single newline character.} ] The payload is treated as UTF-8 text by this module. @subsection{Stdout replies} On @bold{stdout}: @itemlist[ @item{Each command sent via the returned command function results in exactly one reply on stdout with the framing described above.} @item{The payload is decoded as UTF-8 and returned to the caller as a plain string.} ] If the helper sends malformed output (for example, a non-numeric prefix, incorrect length, or missing colon), the IPC code raises an error indicating “unexpected input from webui-wire executable”. @subsection{Stderr events and logs} On @bold{stderr}: @itemlist[ @item{Each payload is interpreted as a single logical line;} @item{The line is expected to start with a symbolic tag followed by a colon, e.g. @tt{"EVENT:..."} or @tt{"INFO:..."}.} ] The module distinguishes: @itemlist[ @item{@tt{EVENT} lines: the text after the @tt{"EVENT:"} prefix is delivered to the @racket[event-queuer] callback.} @item{All other tags (and untagged lines): turned into @racket[(kind msg)] calls to @racket[log-processor], where @racket[kind] is a symbol derived from the tag or from the stderr reader, and @racket[msg] is the remaining text.} ] If the stderr reader encounters EOF, it reports this via @racket[log-processor] (using an appropriate @racket[kind] symbol) and then terminates its reader thread. @section[#:tag "ipc-internal"]{Internal behaviour (informative)} This section summarises how @racket[webui-ipc] is implemented. The details are not part of the public API, but they can help to interpret log messages and errors. @subsection{Process startup} When you call @racket[webui-ipc], the following steps are performed: @itemlist[ @item{Obtain the command string for @tt{webui-wire} by calling @racket[ww-get-webui-wire-command] from @racketmodname[webui-wire-download].} @item{Launch the helper process using @racket[process] from @racketmodname[racket/system].} @item{Keep handles to the process stdin, stdout, and stderr ports.} @item{Spawn a background thread dedicated to reading and decoding stderr events and log lines.} ] If the process cannot be started, an exception is raised immediately from @racket[webui-ipc]. @subsection[#:tag "ipc-internal-concurrency"]{Concurrency and serialisation} The command function returned by @racket[webui-ipc] may be called from multiple threads. Internally, a semaphore is used to ensure that only one caller at a time: @itemlist[ @item{writes a command line to the process stdin,} @item{waits for a single, complete reply on stdout,} @item{returns that reply to the caller.} ] This guarantees that requests and replies do not interleave: the reply string you receive always corresponds to the command you sent in that call. The stderr reader runs independently in its own thread and does not block command calls. @subsection{Error handling and shutdown} Several error conditions can occur: @itemlist[ @item{EOF or broken pipe on stdin/stdout/stderr;} @item{malformed length prefix or framing errors;} @item{helper process exiting unexpectedly.} ] In these cases, the module: @itemlist[ @item{raises an exception from the command function on the next call that attempts to send a command or read a reply;} @item{reports context via @racket[log-processor] (for example, that the stderr reader hit EOF or that the executable exited).} ] It is the responsibility of the caller to decide how to react: typically by catching such exceptions at a higher level, informing the user, and possibly restarting the application or IPC bridge. Once the @tt{webui-wire} process has exited, the previously returned command function is no longer usable; further calls are expected to fail with an error.