1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
//! The two ends of a console, and the wire between them.
//!
//! A *console server* speaks JSON-RPC over stdio and runs commands somewhere: this
//! host, a micro-VM, wherever. A *console client* drives one. Servers differ only in
//! that "somewhere"; the methods, which end may ask what, and request/response pairing
//! are defined once here so servers cannot drift apart on them.
//!
//! - [`Client`] and [`Server`] — what each end does on a channel, independent of
//! transport: a client only asks, a server only answers, and neither reads meaning into
//! a message (where a command runs, what a session allows, when something has booted).
//! - [`Message`] and the methods, errors and payloads both ends agree on.
//! - [`stdio`] — the current transport; another (e.g. a micro-VM's virtio port) would
//! change nothing above it.
//! - [`ConsoleClient`](crate::console::ConsoleClient) — the public end: the channel driving a
//! server, plus the session it was opened with.
//!
//! A server never issues a request, so neither end needs a pending table, a listener, or
//! a reader that must not block on work only it can unblock.
//!
//! # The wire
//!
//! Every [`Message`] is a JSON-RPC 2.0 object, shown here as BSON Extended JSON:
//!
//! ```text
//! {"jsonrpc":"2.0","id":2,"method":"exec","params":{"cmd":["sh","-c","ls"]}}
//! {"jsonrpc":"2.0","id":2,"method":"exec","result":{"code":0,"stdout":<Binary>,"stderr":<Binary>,"truncated":false}}
//! {"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"timed out after 1000ms"}}
//! {"jsonrpc":"2.0","method":"quit"}
//! ```
//!
//! **The object model is the spec's; the encoding is not.** Member presence decides a
//! message's shape, `id` pairs a response with its request, and the error codes are
//! JSON-RPC 2.0's. The bytes are BSON documents; framing is [`stdio`]'s.
//!
//! # Methods
//!
//! | Method | `params` | `result` | Errors |
//! |---|---|---|---|
//! | `version` | [`VersionCall`] | [`VersionResp`] | — |
//! | `build_image` | [`BuildImageCall`] | [`BuildImageResp`] | [`INVALID_PARAMS`](Error::INVALID_PARAMS) |
//! | `remove_image` | [`RemoveImageCall`] | [`RemoveImageResp`] | [`INVALID_PARAMS`](Error::INVALID_PARAMS) |
//! | `list_images` | [`ListImagesCall`] | [`ListImagesResp`] | — |
//! | `init` | [`InitCall`] | [`InitResp`] | [`INVALID_PARAMS`](Error::INVALID_PARAMS), [`UNSUPPORTED_MOUNT`](Error::UNSUPPORTED_MOUNT), [`UNSUPPORTED_NETWORK`](Error::UNSUPPORTED_NETWORK), [`UNSUPPORTED_IMAGE`](Error::UNSUPPORTED_IMAGE), [`UNKNOWN_IMAGE`](Error::UNKNOWN_IMAGE), [`UNSUPPORTED_MACHINE`](Error::UNSUPPORTED_MACHINE) |
//! | `exec` | [`ExecCall`] | [`ExecResp`] | [`INVALID_PARAMS`](Error::INVALID_PARAMS), [`TIMED_OUT`](Error::TIMED_OUT), [`NOT_EXECUTABLE`](Error::NOT_EXECUTABLE), [`BOOT_FAILED`](Error::BOOT_FAILED), [`MOUNT_FAILED`](Error::MOUNT_FAILED) |
//! | `read` | [`ReadCall`] | [`ReadResp`] | [`NOT_FOUND`](Error::NOT_FOUND), [`IS_A_DIRECTORY`](Error::IS_A_DIRECTORY), [`IO_FAILED`](Error::IO_FAILED), [`BOOT_FAILED`](Error::BOOT_FAILED), [`MOUNT_FAILED`](Error::MOUNT_FAILED) |
//! | `write` | [`WriteCall`] | [`WriteResp`] | [`NOT_FOUND`](Error::NOT_FOUND), [`IS_A_DIRECTORY`](Error::IS_A_DIRECTORY), [`IO_FAILED`](Error::IO_FAILED), [`BOOT_FAILED`](Error::BOOT_FAILED), [`MOUNT_FAILED`](Error::MOUNT_FAILED) |
//! | `snapshot` | [`SnapshotCall`] | [`SnapshotResp`] | [`BOOT_FAILED`](Error::BOOT_FAILED), [`MOUNT_FAILED`](Error::MOUNT_FAILED) |
//! | `start` | — | *(notification — no response)* | — |
//! | `stop` | — | *(notification — no response)* | — |
//! | `quit` | — | *(notification — no response)* | — |
//!
//! # Design
//!
//! - **Booting is not a method.** Anything that needs a booted session boots one, so
//! [`Start`](Notification::Start) and [`Stop`](Notification::Stop) are resource
//! management only and go unanswered. `init` is a call because it says what the session
//! *is*, and its answer is the first thing a client can act on.
//! - **Failure is an `error` with a code**, the only failure channel: a requester branches
//! on [`Error::code`], and an `error` cannot be mistaken for a command's exit status.
//! - **An execution is one request and one response, never a stream.** The caller is an
//! agent that cannot act on partial output, so [`ExecResp`] arrives whole, bounded by
//! [`MAX_PAYLOAD`]. It takes no stdin (stage files with `write`, collect them with
//! `read`), and a command that never ends needs [`timeout_ms`](ExecCall::timeout_ms).
//! - **Program bytes are bytes; names are text.** Output and file contents are raw bytes;
//! an argv and a path are UTF-8, since both ends must interpret them.
//!
//! ## Why the codec is BSON
//!
//! **Self-describing.** `{"method":..,"params":..}` is serde adjacent tagging, and
//! `result` xor `error` is decided by which member is *present*; both need a deserializer
//! that can look ahead, which postcard and bincode cannot.
//!
//! **A byte type.** Exec output and file contents are most of the traffic and are program
//! bytes, not text. JSON would need base64 (1.33×, plus decoding); BSON's `Binary`
//! carries them as themselves.
//!
//! ### What that costs
//!
//! An off-the-shelf JSON-RPC library: a peer needs a BSON codec. That matters little,
//! since such a peer already needs bespoke framing and both ends live in this workspace.
//!
//! Not frame size where it matters. BSON writes array indices as keys (`cmd` becomes
//! `{"0":"ls"}`) and names as C strings, so a control frame like `stop` is slightly
//! *larger* than JSON; the large, frequent output frames shrink. MessagePack and CBOR
//! beat BSON on both; BSON wins on being self-delimiting (which can retire the framing
//! layer) and on `doc!`/Extended JSON keeping the wire readable to people and tests.
//!
//! # Every trait method hands back a boxed future
//!
//! Everything that waits is a future, so one runtime can drive many consoles at a task
//! each rather than a thread parked on a pipe each. An `async fn` in a trait returns a
//! type a `dyn` cannot name, and a [`ConsoleClient`](crate::console::ConsoleClient) holds
//! a `dyn Client` so the transport stays out of its type. A [`BoxFuture`] costs one
//! allocation per call, negligible against a pipe round trip; derived methods use the
//! same shape.
//!
//! Futures and both ends are [`Send`] so a session can be handed to a task.
use io;
pub use *;
use BoxFuture;
pub use *;
/// The asking end of a channel: issue a call, get its answer back.
///
/// A transport need implement only [`call`](Self::call) and [`notify`](Self::notify); the
/// per-method wrappers are derived here, which is also the one place a
/// [`Response`] becomes what its method returns.
///
/// `&mut self` throughout, with no `&self`-plus-lock: one call is outstanding at a time,
/// and a `read` interleaved with a running command is a question the server cannot answer
/// yet. The exclusive borrow checks this; for concurrency, open a second console.
/// A peer that answered one method with another's result.
///
/// Unreachable against a sane peer (one call is outstanding and the transport already
/// dropped frames with other ids), so it is reported as
/// [`INTERNAL_ERROR`](Error::INTERNAL_ERROR).
/// The answering end of a channel: take what arrived, put an answer out.
///
/// Moves frames and holds no state; what a message means is the backend's.