Console protocol v1

This is the wire contract between a Kanzi Monitor console server and its clients. It exists so that a client can be written, or checked, without reading the server’s source.

Important

This specification describes what the server does today, warts included. Where the behavior is unhelpful it is documented as a defect and given a number, not quietly corrected. A client written against this document works against shipping Kanzi Monitor; a client written against what the protocol ought to be does not.

Scope

Applies to the remote console (TCP) and the serial console (UART). It is one contract: the framing, the request grammar, the response envelope and the known defects are the same on both, and a client written to this document works over either.

Where the two transports behave differently, a Serial or TCP note under the section concerned says so. The differences, all of them, are:

Behavior

TCP

Serial

A CR before the LF

Stripped, so CRLF works, from 1.41.3 — W2 — CRLF line endings break command lookup

Stripped, so CRLF works

Clients

Several at once, up to RemoteConsoleMaxConnections

One point-to-point link

A client that stops reading

Closed after 10 seconds without progress — see Connection model

Waited out; the response is sent whole once the other end reads again

Address family

IPv4 only

Not applicable

A client that sends LF only and keeps reading its responses behaves correctly on both.

The local console is an interactive terminal on stdin/stdout and is not a machine interface.

Transport

Transport

Details

TCP

The server listens on RemoteConsolePort (default 56000) on all IPv4 interfaces. It does not accept IPv6: connect to 127.0.0.1, not ::1. Clients connect directly; there is no handshake, no banner and no greeting. The first thing the server expects is a command.

Serial

SerialConsolePort at SerialConsoleBaudRate. Otherwise identical.

There is no version negotiation, no authentication and no encryption. Anything that can reach the port can issue any command, including commands that change application state. Treat the port as a debug interface on a trusted network.

Framing

Request framing is one command per line, terminated by a single LF (0x0A).

Response framing is: an optional body, an optional status line, then a blank line — that is, the response ends with two consecutive LFs. The blank line is the only reliable end-of-response marker, and a client must read until it sees \n\n.

The terminator is sent even when a handler throws or returns early. There is exactly one case in which no response is produced at all, and therefore no terminator: see W1 — an empty command produces no response at all.

Request grammar

request  = command [ SP arguments ] LF
command  = one or more characters, no space
arguments = everything after the first space, verbatim to end of line

The line is split at the first space. Everything after it is passed to the handler as a single opaque string; the server does no tokenising, no quoting and no escaping at this level. How a command reads its argument string, quoting included, is part of that command’s contract and is documented with the command.

The commands share one argument reader. An argument is a word, a quoted string in " or ' with no escapes, or, from 1.41.3, a string literal: $ directly followed by a JSON string, which can hold a line break and any other character as an escape. See Setting any text: the string literal.

Command names are matched by exact string equality. There is no case folding, no whitespace trimming and no abbreviation.

Response envelope

A response is the handler’s output followed by at most one status line. There are four shapes, and a client has to handle all of them:

Outcome

What arrives

Success

The body, then Command: <name> executed successfully in: <n> ms.

Command not found

Command <name> not found. and no body

Handler threw

The partial body, then a JSON object: {"error":"Command handler threw an exception","command":"…","reason":"…"}

Handler answered for itself

The body, and no status line at all. On this release every handler that does this is reporting a failure — see the note below.

Example of the success shape:

{"backend":"OpenGL","apiVersion":"4.6", …}
Command: overwatch.graphics executed successfully in: 0 ms.
<blank line>

Note

An absent status line means the command failed, on this release — but do not write a client that relies on it. The shape exists so that a handler which has already written a precise message is not followed by a generic one, and every handler that uses it today is rejecting something. Not one success uses it.

That is a property of the current handlers, not a guarantee of the protocol. Nothing stops a future handler from answering for itself on success, and a client that treated “no trailer” as “failed” would then misreport it. It is documented because it explains what you will observe, and because it is the opposite of what the shape’s name suggests.

Status vocabulary

There is no status vocabulary in the sense a client would want. The four outcomes above are expressed in three different notations — two prose sentences, one JSON object, and silence — because each is emitted from a different place in the server rather than from one definition.

The consequence is worth stating plainly, because it is the single most important thing to know when writing a client:

Warning

A v1 response carries no reliable machine-readable success or failure signal.

The absence of an error line does not mean success: a handler that rejects its arguments writes its own message and suppresses the status line entirely, which is structurally identical to what a successful command answering for itself would send. No command does the latter on this release, but the protocol does not prevent one, so the structure cannot tell you which of the two you are holding.

A client that must know whether a command worked has to parse the body of that particular command. Per-command response formats are documented with the commands themselves, in Using the Overwatch service and Using the Command Processor service.

Connection model

The server accepts several clients at once, up to RemoteConsoleMaxConnections (default 4). Each connection is independent and receives only the responses to its own commands.

Note

Serial: the serial console is one point-to-point link, so it is a single connection. The connection limit and the refusal message below do not apply to it. Its commands run on the same main thread as the TCP clients’ commands, under the same ordering rules: in order within the serial link, and no defined order against a TCP client except by cause and effect.

Commands from all connections execute one at a time on the Kanzi main thread. Two clients therefore interleave rather than run in parallel. The order in which they execute is guaranteed in two cases and no others:

  • Within one connection, commands execute in the order that connection sent them. The server waits about 100 ms after reading a command before it reads the next one from the same connection, on TCP and on the serial console alike, so one connection runs at most about 10 commands per second. Sending several commands without waiting for their responses does not raise that rate.

  • Across connections, by cause and effect. A response’s terminator is sent only after its command’s handler has returned. So a command that a client sends after it has received another connection’s complete response executes after that command. The guarantee covers the handler itself, not work a handler defers to a later frame.

Commands sent on different connections with no such dependency have no defined order. It is not the order they were sent, and not the order their bytes reached the server: each connection has its own reader thread, and those threads hand commands to the main thread in whichever order they are scheduled. Two clients writing the same state overwrite each other in an order a client cannot predict.

A client that connects when every slot is taken receives an unsolicited message and is then disconnected:

ERROR: remote console is at its connection limit; try again when a client disconnects.
<blank line>

This is the only message the server sends without a command behind it. It is terminated like any other response, so a client reading to the terminator handles it without special-casing.

A client must keep reading its responses. Each connection’s responses are sent by a thread of its own, so a client that stops reading never holds up the application or another connection, but the server does limit what it will do for it:

  • While a connection has about 1 MB or more of output that its client has not read, the server runs no further command from it until the client catches up. A single response of any size is still sent whole.

  • If sending a response makes no progress for 10 seconds, the server closes the connection, possibly partway through that response. The client then reads EOF — never the rest of that response followed by another.

A client may close its sending side after its last command (a half-close, as echo cmd | nc host port does). The server still sends every response it owes, then closes the connection.

Note

Serial: responses are sent by a thread of their own here too, and the first rule applies: further commands are held while about 1 MB is unsent. The other two do not. A serial link has no connection to close, so a response that makes no progress is waited out and sent whole once the other end reads again, and a half-close has no meaning.

Note

Before Kanzi Monitor 1.41.3 the server served one client at a time, and a second client’s connect() succeeded but was never answered. A client that must work against older servers should apply a connect-and-first-response timeout.

How a conforming client reads a response

  1. Send command [SP args] followed by a single LF. Both consoles take CRLF from 1.41.3, but only LF works with every release — see W2 — CRLF line endings break command lookup.

  2. Never send an empty line, or a line beginning with a space — see W1 — an empty command produces no response at all.

  3. Read until \n\n appears in the accumulated bytes. Do not read line by line and stop at the first line: many responses are multi-line, and a JSON body can be tens of kilobytes.

  4. Decode the response as UTF-8. A JSON string holds non-ASCII text as UTF-8, as it is, and a stored string that is not valid UTF-8 comes back with U+FFFD in place of each ill-formed sequence, so a strict decoder never fails on a JSON body — see W6 — non-ASCII characters in JSON strings are garbled (fixed in 1.41.3) for servers before 1.41.3.

  5. Apply a timeout to the read. There are server states in which no response is coming.

  6. Keep reading as responses arrive. A client that lets output pile up has its further commands held, and is disconnected after 10 seconds without progress — see Connection model.

  7. Strip the trailing blank line. Then, if the last line matches Command: <name> executed successfully in: <n> ms. or Command <name> not found., remove it — it is envelope, not content.

  8. Do not assume the trailer is present. Its absence is a valid outcome.

Known defects

These are numbered so that tickets, tests and client code can refer to them. All are present in Kanzi Monitor 1.41.3 on both transports, with three exceptions, each noted in its section: W2, W5 and W6 are fixed in that release.

W1 — an empty command produces no response at all

A line that parses to an empty command name is silently discarded. Nothing is dispatched, so no body, no status line and no terminator are emitted, and a client that is waiting for the terminator waits until its own timeout.

This is broader than it sounds. The line is split at the first space, so any line beginning with a space yields an empty command name:

""       -> no response
"   "    -> no response
" kzinfo" -> no response

A client that indents or pretty-prints what it sends will hang. Send no leading whitespace, and never send a blank line as a keepalive.

W2 — CRLF line endings break command lookup

Fixed in 1.41.3: the remote console strips one trailing CR, as the serial console always has. Before that, the server split on LF and did not strip a preceding CR, and command names are matched by exact equality. A CRLF client therefore sent a command name with a trailing CR, which matched nothing, and a CR at the end of a command with arguments ended its last argument:

"kzinfo\n"   -> Kanzi version: …
"kzinfo\r\n" -> Command kzinfo\r not found.   (1.41.2 and earlier)

Send LF to work with every release; CRLF works from 1.41.3. A carriage return inside a value is written as \r in a string literal.

Note

Serial: never affected. The serial console strips a trailing CR before it looks the command up, because serial terminals commonly send CR or CRLF.

W3 — the status line is indistinguishable from body content

The trailer is a plain line of text in the same stream as the body. A body that happens to contain a line beginning with Command: and a space cannot be told apart from the envelope.

W4 — “not found” also means “the handler returned something unrecognised”

The server maps a handler return value of 0 to success and 1 to “answered for itself”. Every other value, not only the “no such command” code, produces Command <name> not found. A client cannot distinguish a misspelled command from a handler that failed in a way nobody enumerated.

W5 — a partially sent response is reported as complete (fixed in 1.41.3)

Before Kanzi Monitor 1.41.3, the server treated a short write as a full write, on both transports. Under a full send buffer — most likely with a large response or a slow client — the remainder was dropped and no error was reported. The symptom at the client was a truncated body, usually with no terminator, and therefore a read that timed out.

From 1.41.3 every byte of a response is sent. Over TCP a client sees either the whole response or EOF (KZMON-793); over serial it sees the whole response (KZMON-816). A client that must work against older servers should still treat a read timeout as a possibly truncated response.

W6 — non-ASCII characters in JSON strings are garbled (fixed in 1.41.3)

Before Kanzi Monitor 1.41.3, what the server wrote for a byte of 0x80 and above depended on whether char is signed for the compiler that built it:

  • Signed char — Windows, and x86 and x86-64 Linux and Android: every such byte in a JSON string was escaped as its own \u00XX. A JSON parser turns each of those into a separate Latin-1 character, so the two bytes of é came back as é.

  • Unsigned char — Arm: arm64 and armv7 Android, aarch64 Linux and QNX: the bytes were written as they are. Valid UTF-8 came back right, and a string that is not valid UTF-8 made the response invalid UTF-8 too.

stored:                      café
before 1.41.3, signed char:  "caf\u00c3\u00a9"   -> parses as "café"
before 1.41.3, Arm:          "café"              (the bytes 63 61 66 C3 A9)
from 1.41.3, every target:   "café"              (the bytes 63 61 66 C3 A9)

The text the application holds was never affected, only what the response says about it. But on the signed-char targets, a path or a name read from a response does not match the node it came from, so a client cannot send it back: a node listed with a non-ASCII name answers Node not found.

From 1.41.3 valid UTF-8 is written as it is on every target, and a stored string that is not valid UTF-8 is written with U+FFFD in place of each ill-formed sequence (KZMON-925): in a JSON string as the six-character escape \ufffd, and in the plain-text responses, such as a Command … not found. line or nodetreeinfo, as the three bytes of U+FFFD. Every response is valid UTF-8.

A client that must work against older servers can recover the text from a signed-char server: there, each byte from 0x80 up parses as one character in U+0080–U+00FF, so encoding the parsed string as Latin-1 and decoding the result as UTF-8 gives the original string. Against an Arm server that same step corrupts text that is already right, so apply it only when both steps succeed, and otherwise keep the string as parsed. A string from a signed-char server always passes both steps; one with a character above U+00FF, or whose Latin-1 bytes are not valid UTF-8, came from an Arm server and is already right.

What this document does not specify

  • Per-command arguments and responses. The envelope is one contract. How each command reads its arguments, quoting included, and what its body contains are documented with the commands.

  • The local console. It is an interactive terminal, not a machine interface.

  • Any other protocol version. This page describes protocol v1 only.

See also

Using the Command Processor service

Using the console clients