Skip to main content

isb_core/
rpc.rs

1//! `isb rpc`: the protocol the language SDKs speak.
2//!
3//! Line-delimited JSON over stdin/stdout, one object per line, documented in
4//! docs/reference/rpc.md. The server announces itself first, then answers requests:
5//!
6//! ```text
7//! <- {"isb":"0.1.1","protocol":1}
8//! -> {"id":1,"method":"sandbox.get","params":{"name":"web"}}
9//! <- {"id":1,"result":{...}}
10//! ```
11//!
12//! Requests run concurrently, each on its own thread, so a long `exec` or `up`
13//! never blocks another request. A streaming exec sends `event` lines (output,
14//! base64) before its result, and accepts `exec.write`/`exec.signal`/... calls
15//! naming its request id while it runs. When stdin closes, the server exits;
16//! commands still running then lose their control connection and incus kills
17//! them.
18
19use std::collections::{BTreeMap, HashMap};
20use std::io::{BufRead, Write};
21use std::path::{Path, PathBuf};
22use std::sync::{Arc, Mutex};
23use std::time::Duration;
24
25use serde::Deserialize;
26use serde::de::DeserializeOwned;
27use serde_json::{Value, json};
28
29use crate::compose::{self, LoadOptions};
30use crate::error::{Error, Result};
31use crate::exec::{self, ExecController, ExecEvent, ExecOptions, Stdin};
32use crate::plan::{DiffOptions, VolumeDefs};
33use crate::sandbox::{self, EnsureOptions, LabelFilter, Sandbox};
34use crate::spec::{ExecDefaults, PortSpec, ReadyCheck, SandboxSpec};
35use crate::{Client, flex};
36
37/// Protocol version. Bumped on incompatible changes.
38pub const PROTOCOL: u32 = 1;
39
40type Out = Arc<Mutex<Box<dyn Write + Send>>>;
41
42/// A control call received for an exec that has not started yet.
43enum Queued {
44    Write(Vec<u8>),
45    CloseStdin,
46    Signal(i32),
47    Resize(u16, u16),
48}
49
50/// An exec is registered as `Pending` the moment its request is read, before
51/// it reaches incusd, so control calls sent right behind it are queued rather
52/// than lost; they are replayed in order once it runs.
53enum ExecSlot {
54    Pending(Vec<Queued>),
55    Running(ExecController),
56}
57
58struct Server {
59    out: Out,
60    client: Client,
61    execs: Mutex<HashMap<String, ExecSlot>>,
62}
63
64#[derive(Deserialize)]
65struct Request {
66    #[serde(default)]
67    id: Value,
68    method: String,
69    #[serde(default)]
70    params: Value,
71}
72
73/// Serve requests from `input` until it closes, writing to `output`.
74pub fn serve<R: BufRead, W: Write + Send + 'static>(
75    input: R,
76    output: W,
77    client: Client,
78) -> Result<()> {
79    let server = Arc::new(Server {
80        out: Arc::new(Mutex::new(Box::new(output))),
81        client,
82        execs: Mutex::new(HashMap::new()),
83    });
84    server.send(&json!({"isb": env!("CARGO_PKG_VERSION"), "protocol": PROTOCOL}));
85    let mut workers = Vec::new();
86    for line in input.lines() {
87        let line = line?;
88        if line.trim().is_empty() {
89            continue;
90        }
91        let req: Request = match serde_json::from_str(&line) {
92            Ok(r) => r,
93            Err(e) => {
94                server.send(&json!({"id": null, "error": {"code": "bad_request", "message": format!("not a request: {e}")}}));
95                continue;
96            }
97        };
98        if req.method == "sandbox.exec" {
99            server
100                .execs
101                .lock()
102                .unwrap_or_else(|p| p.into_inner())
103                .insert(exec_key(&req.id), ExecSlot::Pending(Vec::new()));
104        }
105        // Stream control must never queue behind the stream it controls.
106        if req.method.starts_with("exec.") {
107            server.clone().handle(req);
108            continue;
109        }
110        let s = server.clone();
111        workers.push(std::thread::spawn(move || s.handle(req)));
112        workers.retain(|w| !w.is_finished());
113    }
114    // Input closed: let in-flight requests that do not depend on the client
115    // finish writing, briefly, then exit.
116    for w in workers {
117        let _ = w.join();
118    }
119    Ok(())
120}
121
122impl Server {
123    fn send(&self, v: &Value) {
124        let mut o = self.out.lock().unwrap_or_else(|p| p.into_inner());
125        let _ = writeln!(o, "{v}");
126        let _ = o.flush();
127    }
128
129    fn event(&self, id: &Value, event: &str, data: Value) {
130        self.send(&json!({"id": id, "event": event, "data": data}));
131    }
132
133    fn handle(self: Arc<Self>, req: Request) {
134        let id = req.id.clone();
135        let r = self.dispatch(&req);
136        match r {
137            Ok(v) => self.send(&json!({"id": id, "result": v})),
138            Err(e) => self.send(&json!({"id": id, "error": error_json(&e)})),
139        }
140    }
141
142    fn client(&self, project: &Option<String>) -> Client {
143        match project {
144            Some(p) => self.client.clone().project(p),
145            None => self.client.clone(),
146        }
147    }
148
149    #[expect(
150        clippy::too_many_lines,
151        reason = "predates the lint ratchet; split it when next changed"
152    )]
153    fn dispatch(&self, req: &Request) -> Result<Value> {
154        let id = &req.id;
155        let p = &req.params;
156        let mut progress = |line: &str| self.event(id, "progress", json!(line));
157        match req.method.as_str() {
158            "version" => Ok(json!({"isb": env!("CARGO_PKG_VERSION"), "protocol": PROTOCOL})),
159            "schema" => Ok(crate::spec::compose_schema()),
160
161            "sandbox.create" => {
162                let a: SpecParams = params(p)?;
163                let c = self.client(&a.project);
164                let opts = EnsureOptions {
165                    wait_ready: a.wait_ready,
166                    ..Default::default()
167                };
168                let d = resolve_spec(&c, &a)?;
169                let sb = create_desired(&c, &d, opts, &mut progress)?;
170                Ok(json!(sb.info()?))
171            }
172            "sandbox.ensure" => {
173                let a: SpecParams = params(p)?;
174                let c = self.client(&a.project);
175                let opts = EnsureOptions {
176                    wait_ready: a.wait_ready,
177                    diff: DiffOptions {
178                        prune_devices: a.prune_devices,
179                    },
180                    ..Default::default()
181                };
182                let d = resolve_spec(&c, &a)?;
183                let report = sandbox::ensure(&c, &d, opts, &mut progress)?;
184                let info = Sandbox::get(&c, &d.name)?.info()?;
185                Ok(json!({"info": info, "report": report}))
186            }
187            "sandbox.plan" => {
188                let a: SpecParams = params(p)?;
189                let c = self.client(&a.project);
190                let d = resolve_spec(&c, &a)?;
191                Ok(json!(sandbox::plan_desired(
192                    &c,
193                    &d,
194                    DiffOptions {
195                        prune_devices: a.prune_devices
196                    }
197                )?))
198            }
199            "sandbox.resolve" => {
200                let a: SpecParams = params(p)?;
201                let c = self.client(&a.project);
202                Ok(json!(resolve_spec(&c, &a)?))
203            }
204            "sandbox.get" => {
205                let a: NameParams = params(p)?;
206                Ok(json!(
207                    Sandbox::get(&self.client(&a.project), &a.name)?.info()?
208                ))
209            }
210            "sandbox.list" => {
211                let a: ListParams = params(p)?;
212                let filters: Vec<LabelFilter> =
213                    a.labels.iter().map(|l| LabelFilter::parse(l)).collect();
214                Ok(json!(Sandbox::list_with(
215                    &self.client(&a.project),
216                    &filters
217                )?))
218            }
219            "sandbox.remove" => {
220                let a: RemoveParams = params(p)?;
221                Sandbox::remove(&self.client(&a.project), &a.name, a.force)?;
222                Ok(Value::Null)
223            }
224            "sandbox.start" => {
225                let a: NameParams = params(p)?;
226                Sandbox::get(&self.client(&a.project), &a.name)?.start()?;
227                Ok(Value::Null)
228            }
229            "sandbox.stop" => {
230                let a: StopParams = params(p)?;
231                let t = match &a.timeout {
232                    Some(s) => flex::parse_duration(s).map_err(Error::Invalid)?,
233                    None => Duration::from_secs(30),
234                };
235                Sandbox::get(&self.client(&a.project), &a.name)?.stop(a.force, t)?;
236                Ok(Value::Null)
237            }
238            "sandbox.restart" => {
239                let a: NameParams = params(p)?;
240                Sandbox::get(&self.client(&a.project), &a.name)?.restart()?;
241                Ok(Value::Null)
242            }
243            "sandbox.wait_ready" => {
244                let a: ReadyParams = params(p)?;
245                let t = match &a.ready_timeout {
246                    Some(s) => flex::parse_duration(s).map_err(Error::Invalid)?,
247                    None => Duration::from_secs(60),
248                };
249                let checks = a.ready.unwrap_or_else(|| vec![ReadyCheck::Running]);
250                sandbox::wait_ready(&self.client(&a.project), &a.name, &checks, t, &a.exec)?;
251                Ok(Value::Null)
252            }
253            "sandbox.exec" => {
254                let r = self.exec(id, p);
255                if r.is_err() {
256                    self.execs
257                        .lock()
258                        .unwrap_or_else(|p| p.into_inner())
259                        .remove(&exec_key(id));
260                }
261                r
262            }
263            "exec.write" => {
264                let a: ExecWriteParams = params(p)?;
265                let data = b64_decode(&a.data)?;
266                self.control(&a.exec, Queued::Write(data))
267            }
268            "exec.close_stdin" => {
269                let a: ExecRef = params(p)?;
270                self.control(&a.exec, Queued::CloseStdin)
271            }
272            "exec.signal" => {
273                let a: ExecSignalParams = params(p)?;
274                self.control(&a.exec, Queued::Signal(a.signal))
275            }
276            "exec.resize" => {
277                let a: ExecResizeParams = params(p)?;
278                self.control(&a.exec, Queued::Resize(a.width, a.height))
279            }
280            "sandbox.add_port" => {
281                let a: AddPortParams = params(p)?;
282                let listen = Sandbox::get(&self.client(&a.project), &a.name)?.add_port(&a.port)?;
283                Ok(json!({"listen": listen}))
284            }
285            "sandbox.remove_device" => {
286                let a: DeviceParams = params(p)?;
287                let removed =
288                    Sandbox::get(&self.client(&a.project), &a.name)?.remove_device(&a.device)?;
289                Ok(json!({"removed": removed}))
290            }
291
292            "volume.list" => {
293                let a: PoolParams = params(p)?;
294                let c = self.client(&a.project);
295                let pools = match a.pool {
296                    Some(p) => vec![p],
297                    None => sandbox::host_facts(&c)?.pools,
298                };
299                let mut all = Vec::new();
300                for pool in pools {
301                    all.extend(crate::volume::list(&c, &pool)?);
302                }
303                Ok(json!(all))
304            }
305            "volume.get" => {
306                let a: VolumeParams = params(p)?;
307                let c = self.client(&a.project);
308                let pool = sandbox::host_facts(&c)?.pick_pool(a.pool.as_deref())?;
309                crate::volume::get(&c, &pool, &a.name)?
310                    .map(|v| json!(v))
311                    .ok_or_else(|| Error::NotFound(format!("volume {} in pool {pool}", a.name)))
312            }
313            "volume.create" => {
314                let a: VolumeParams = params(p)?;
315                let c = self.client(&a.project);
316                let pool = sandbox::host_facts(&c)?.pick_pool(a.pool.as_deref())?;
317                let created = crate::volume::ensure(&c, &pool, &a.name, &a.config)?;
318                Ok(json!({"created": created, "pool": pool}))
319            }
320            "volume.remove" => {
321                let a: VolumeParams = params(p)?;
322                let c = self.client(&a.project);
323                let pool = sandbox::host_facts(&c)?.pick_pool(a.pool.as_deref())?;
324                crate::volume::remove(&c, &pool, &a.name)?;
325                Ok(Value::Null)
326            }
327            "prune" => {
328                let a: PruneParams = params(p)?;
329                Ok(json!(sandbox::prune_missing_path(
330                    &self.client(&a.project),
331                    &a.label,
332                    a.dry_run,
333                    &mut progress
334                )?))
335            }
336
337            "compose.load" => {
338                let a: ComposeParams = params(p)?;
339                let proj = compose::load(&a.load())?;
340                Ok(
341                    json!({"name": proj.name, "base_dir": proj.base_dir, "files": proj.files, "file": proj.file}),
342                )
343            }
344            "compose.up" => {
345                let a: ComposeParams = params(p)?;
346                let proj = compose::load(&a.load())?;
347                let opts = EnsureOptions {
348                    wait_ready: a.wait_ready,
349                    diff: DiffOptions {
350                        prune_devices: a.prune_devices,
351                    },
352                    ..Default::default()
353                };
354                let out = compose::up(
355                    &self.client(&a.project),
356                    &proj,
357                    &a.services,
358                    opts,
359                    &mut progress,
360                )?;
361                Ok(json!(
362                    out.into_iter()
363                        .map(|(s, r)| json!({"service": s, "report": r}))
364                        .collect::<Vec<_>>()
365                ))
366            }
367            "compose.plan" => {
368                let a: ComposeParams = params(p)?;
369                let proj = compose::load(&a.load())?;
370                Ok(json!(compose::plan(
371                    &self.client(&a.project),
372                    &proj,
373                    &a.services,
374                    DiffOptions {
375                        prune_devices: a.prune_devices
376                    }
377                )?))
378            }
379            "compose.down" => {
380                let a: ComposeParams = params(p)?;
381                let proj = compose::load(&a.load())?;
382                compose::down(
383                    &self.client(&a.project),
384                    &proj,
385                    &a.services,
386                    a.volumes,
387                    &mut progress,
388                )?;
389                Ok(Value::Null)
390            }
391            other => Err(Error::Protocol(format!("unknown method {other:?}"))),
392        }
393    }
394
395    /// Apply a control call to a running exec, or queue it for a pending one.
396    fn control(&self, exec: &Value, op: Queued) -> Result<Value> {
397        let key = exec_key(exec);
398        let mut execs = self.execs.lock().unwrap_or_else(|p| p.into_inner());
399        match execs.get_mut(&key) {
400            Some(ExecSlot::Pending(q)) => {
401                q.push(op);
402                Ok(Value::Null)
403            }
404            Some(ExecSlot::Running(c)) => {
405                let c = c.clone();
406                drop(execs);
407                apply_op(&c, op)?;
408                Ok(Value::Null)
409            }
410            None => Err(Error::NotFound(format!("running exec {key}"))),
411        }
412    }
413
414    fn exec(&self, id: &Value, p: &Value) -> Result<Value> {
415        let a: ExecParams = params(p)?;
416        let c = self.client(&a.project);
417        let stdin = match &a.stdin {
418            None => Stdin::Null,
419            Some(StdinParam::Mode(m)) if m == "null" => Stdin::Null,
420            Some(StdinParam::Mode(m)) if m == "piped" => Stdin::Piped,
421            Some(StdinParam::Mode(m)) => {
422                return Err(Error::invalid(format!(
423                    "stdin must be \"null\", \"piped\" or {{\"data\": base64}}, got {m:?}"
424                )));
425            }
426            Some(StdinParam::Data { data }) => Stdin::Bytes(b64_decode(data)?),
427        };
428        let timeout = match &a.timeout {
429            Some(t) => Some(flex::parse_duration(t).map_err(Error::Invalid)?),
430            None => None,
431        };
432        let opts = ExecOptions {
433            cwd: a.cwd.clone(),
434            user: a.user.clone(),
435            env: a.env.clone(),
436            login: a.login,
437            tty: a.tty,
438            width: a.width,
439            height: a.height,
440            timeout,
441            stdin: stdin.clone(),
442        };
443        let key = exec_key(id);
444        let started = exec::build_request(&c, &a.name, &a.argv, &a.defaults, &opts)
445            .and_then(|req| exec::start_with_timeout(&c, &a.name, req, stdin, timeout));
446        let mut stream = match started {
447            Ok(s) => s,
448            Err(e) => {
449                self.execs
450                    .lock()
451                    .unwrap_or_else(|p| p.into_inner())
452                    .remove(&key);
453                return Err(e);
454            }
455        };
456        // Replay what arrived while it was starting, then go live. Holding the
457        // lock across both keeps later calls behind the replayed ones.
458        {
459            let ctl = stream.controller();
460            let mut execs = self.execs.lock().unwrap_or_else(|p| p.into_inner());
461            if let Some(ExecSlot::Pending(q)) = execs.remove(&key) {
462                for op in q {
463                    let _ = apply_op(&ctl, op);
464                }
465            }
466            execs.insert(key.clone(), ExecSlot::Running(ctl));
467        }
468        let mut stdout = Vec::new();
469        let mut stderr = Vec::new();
470        while let Some(ev) = stream.next_event() {
471            match (ev, a.stream) {
472                (ExecEvent::Stdout(b), true) => self.event(id, "stdout", json!(b64_encode(&b))),
473                (ExecEvent::Stderr(b), true) => self.event(id, "stderr", json!(b64_encode(&b))),
474                (ExecEvent::Stdout(b), false) => stdout.extend(b),
475                (ExecEvent::Stderr(b), false) => stderr.extend(b),
476            }
477        }
478        let code = stream.wait();
479        self.execs
480            .lock()
481            .unwrap_or_else(|p| p.into_inner())
482            .remove(&key);
483        let code = code?;
484        if a.stream {
485            Ok(json!({"exit_code": code}))
486        } else {
487            Ok(
488                json!({"exit_code": code, "stdout": b64_encode(&stdout), "stderr": b64_encode(&stderr)}),
489            )
490        }
491    }
492}
493
494fn apply_op(c: &ExecController, op: Queued) -> Result<()> {
495    match op {
496        Queued::Write(d) => c.write_stdin(&d),
497        Queued::CloseStdin => c.close_stdin(),
498        Queued::Signal(s) => c.signal(s),
499        Queued::Resize(w, h) => c.resize(w, h),
500    }
501}
502
503fn exec_key(v: &Value) -> String {
504    match v {
505        Value::String(s) => s.clone(),
506        other => other.to_string(),
507    }
508}
509
510fn params<T: DeserializeOwned>(p: &Value) -> Result<T> {
511    let v = if p.is_null() { json!({}) } else { p.clone() };
512    serde_json::from_value(v).map_err(|e| Error::Invalid(format!("params: {e}")))
513}
514
515fn resolve_spec(c: &Client, a: &SpecParams) -> Result<crate::plan::Desired> {
516    let base = match &a.base_dir {
517        Some(b) => PathBuf::from(b),
518        None => std::env::current_dir()?,
519    };
520    sandbox::resolve(c, &a.spec, &a.volumes, Path::new(&base))
521}
522
523fn create_desired(
524    c: &Client,
525    d: &crate::plan::Desired,
526    opts: EnsureOptions,
527    report: &mut dyn FnMut(&str),
528) -> Result<Sandbox> {
529    let _lock =
530        crate::lock::NameLock::acquire(c.project_name(), &d.name, opts.lock_wait, &mut |_| {})?;
531    if Sandbox::get(c, &d.name).is_ok() {
532        return Err(Error::AlreadyExists(d.name.clone()));
533    }
534    let plan = sandbox::plan_desired(c, d, opts.diff)?;
535    sandbox::apply(c, d, &plan, report)?;
536    if opts.wait_ready {
537        sandbox::wait_ready(c, &d.name, &d.ready, d.ready_timeout, &d.exec)?;
538    }
539    Sandbox::get(c, &d.name)
540}
541
542/// The error object: a stable `code`, a human `message`, and extra `data`.
543pub fn error_json(e: &Error) -> Value {
544    let (code, data) = match e {
545        Error::Connect { socket, .. } => ("connect", json!({"socket": socket})),
546        Error::RequestTimeout {
547            method,
548            path,
549            timeout,
550        } => (
551            "request_timeout",
552            json!({"method": method, "path": path, "timeout_secs": timeout.as_secs_f64()}),
553        ),
554        Error::Api {
555            status,
556            method,
557            path,
558            ..
559        } => (
560            if *status == 404 {
561                "not_found"
562            } else if e.is_conflict() {
563                "already_exists"
564            } else {
565                "api"
566            },
567            json!({"status": status, "method": method, "path": path}),
568        ),
569        Error::OperationTimeout {
570            step,
571            operation,
572            cancelled,
573            waited,
574            ..
575        } => (
576            "operation_timeout",
577            json!({"step": step, "operation": operation, "cancelled": cancelled, "waited_secs": waited.as_secs_f64()}),
578        ),
579        Error::OperationFailed { step, .. } => ("operation_failed", json!({"step": step})),
580        Error::NotReady {
581            sandbox,
582            check,
583            detail,
584            waited,
585        } => (
586            "not_ready",
587            json!({"sandbox": sandbox, "check": check, "detail": detail, "waited_secs": waited.as_secs_f64()}),
588        ),
589        Error::ExecTimeout { timeout, .. } => (
590            "exec_timeout",
591            json!({"timeout_secs": timeout.as_secs_f64()}),
592        ),
593        Error::NotFound(_) => ("not_found", Value::Null),
594        Error::AlreadyExists(_) => ("already_exists", Value::Null),
595        Error::Invalid(_) => ("invalid", Value::Null),
596        Error::Forbidden(_) => ("forbidden", Value::Null),
597        Error::Interpolation(_) => ("interpolation", Value::Null),
598        Error::Parse { path, .. } => ("parse", json!({"path": path})),
599        Error::WebSocket(_) => ("websocket", Value::Null),
600        Error::Protocol(_) => ("protocol", Value::Null),
601        Error::Io(_) => ("io", Value::Null),
602        Error::Json(_) => ("json", Value::Null),
603        Error::Remote { code, data, .. } => (code.as_str(), data.clone()),
604    };
605    let mut o = json!({"code": code, "message": e.to_string()});
606    if !data.is_null() {
607        o["data"] = data;
608    }
609    o
610}
611
612fn yes() -> bool {
613    true
614}
615
616#[derive(Deserialize)]
617#[serde(deny_unknown_fields)]
618struct SpecParams {
619    spec: SandboxSpec,
620    #[serde(default)]
621    base_dir: Option<String>,
622    #[serde(default)]
623    volumes: VolumeDefs,
624    #[serde(default = "yes")]
625    wait_ready: bool,
626    #[serde(default)]
627    prune_devices: bool,
628    #[serde(default)]
629    project: Option<String>,
630}
631
632#[derive(Deserialize)]
633#[serde(deny_unknown_fields)]
634struct NameParams {
635    name: String,
636    #[serde(default)]
637    project: Option<String>,
638}
639
640#[derive(Deserialize)]
641#[serde(deny_unknown_fields)]
642struct ListParams {
643    #[serde(default)]
644    labels: Vec<String>,
645    #[serde(default)]
646    project: Option<String>,
647}
648
649#[derive(Deserialize)]
650#[serde(deny_unknown_fields)]
651struct RemoveParams {
652    name: String,
653    #[serde(default)]
654    force: bool,
655    #[serde(default)]
656    project: Option<String>,
657}
658
659#[derive(Deserialize)]
660#[serde(deny_unknown_fields)]
661struct StopParams {
662    name: String,
663    #[serde(default)]
664    force: bool,
665    #[serde(default)]
666    timeout: Option<String>,
667    #[serde(default)]
668    project: Option<String>,
669}
670
671#[derive(Deserialize)]
672#[serde(deny_unknown_fields)]
673struct ReadyParams {
674    name: String,
675    #[serde(default)]
676    ready: Option<Vec<ReadyCheck>>,
677    #[serde(default)]
678    ready_timeout: Option<String>,
679    #[serde(default)]
680    exec: ExecDefaults,
681    #[serde(default)]
682    project: Option<String>,
683}
684
685#[derive(Deserialize)]
686#[serde(untagged)]
687enum StdinParam {
688    Mode(String),
689    Data { data: String },
690}
691
692#[derive(Deserialize)]
693#[serde(deny_unknown_fields)]
694struct ExecParams {
695    name: String,
696    argv: Vec<String>,
697    #[serde(default)]
698    defaults: ExecDefaults,
699    #[serde(default)]
700    cwd: Option<String>,
701    #[serde(default)]
702    user: Option<String>,
703    #[serde(default)]
704    env: BTreeMap<String, String>,
705    #[serde(default)]
706    login: Option<bool>,
707    #[serde(default)]
708    tty: bool,
709    #[serde(default)]
710    width: Option<u16>,
711    #[serde(default)]
712    height: Option<u16>,
713    /// `"90s"`, `"5m"` or seconds as a string.
714    #[serde(default)]
715    timeout: Option<String>,
716    #[serde(default)]
717    stdin: Option<StdinParam>,
718    /// Send output as events instead of collecting it into the result.
719    #[serde(default)]
720    stream: bool,
721    #[serde(default)]
722    project: Option<String>,
723}
724
725#[derive(Deserialize)]
726#[serde(deny_unknown_fields)]
727struct ExecRef {
728    exec: Value,
729}
730
731#[derive(Deserialize)]
732#[serde(deny_unknown_fields)]
733struct ExecWriteParams {
734    exec: Value,
735    data: String,
736}
737
738#[derive(Deserialize)]
739#[serde(deny_unknown_fields)]
740struct ExecSignalParams {
741    exec: Value,
742    signal: i32,
743}
744
745#[derive(Deserialize)]
746#[serde(deny_unknown_fields)]
747struct ExecResizeParams {
748    exec: Value,
749    width: u16,
750    height: u16,
751}
752
753#[derive(Deserialize)]
754#[serde(deny_unknown_fields)]
755struct AddPortParams {
756    name: String,
757    port: PortSpec,
758    #[serde(default)]
759    project: Option<String>,
760}
761
762#[derive(Deserialize)]
763#[serde(deny_unknown_fields)]
764struct DeviceParams {
765    name: String,
766    device: String,
767    #[serde(default)]
768    project: Option<String>,
769}
770
771#[derive(Deserialize)]
772#[serde(deny_unknown_fields)]
773struct PoolParams {
774    #[serde(default)]
775    pool: Option<String>,
776    #[serde(default)]
777    project: Option<String>,
778}
779
780#[derive(Deserialize)]
781#[serde(deny_unknown_fields)]
782struct VolumeParams {
783    name: String,
784    #[serde(default)]
785    pool: Option<String>,
786    #[serde(default)]
787    config: BTreeMap<String, String>,
788    #[serde(default)]
789    project: Option<String>,
790}
791
792#[derive(Deserialize)]
793#[serde(deny_unknown_fields)]
794struct PruneParams {
795    label: String,
796    #[serde(default = "yes")]
797    dry_run: bool,
798    #[serde(default)]
799    project: Option<String>,
800}
801
802#[derive(Deserialize)]
803#[serde(deny_unknown_fields)]
804struct ComposeParams {
805    #[serde(default)]
806    files: Vec<PathBuf>,
807    #[serde(default)]
808    env_files: Vec<PathBuf>,
809    #[serde(default)]
810    project_name: Option<String>,
811    #[serde(default)]
812    vars: BTreeMap<String, String>,
813    #[serde(default)]
814    services: Vec<String>,
815    #[serde(default = "yes")]
816    wait_ready: bool,
817    #[serde(default)]
818    prune_devices: bool,
819    #[serde(default)]
820    volumes: bool,
821    #[serde(default)]
822    project: Option<String>,
823}
824
825impl ComposeParams {
826    fn load(&self) -> LoadOptions {
827        LoadOptions {
828            files: self.files.clone(),
829            env_files: self.env_files.clone(),
830            project_name: self.project_name.clone(),
831            vars: self.vars.clone(),
832        }
833    }
834}
835
836const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
837
838/// Standard base64 with padding.
839pub fn b64_encode(data: &[u8]) -> String {
840    let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
841    for chunk in data.chunks(3) {
842        let b = [
843            chunk[0],
844            *chunk.get(1).unwrap_or(&0),
845            *chunk.get(2).unwrap_or(&0),
846        ];
847        let n = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32;
848        out.push(B64[(n >> 18) as usize & 63] as char);
849        out.push(B64[(n >> 12) as usize & 63] as char);
850        out.push(if chunk.len() > 1 {
851            B64[(n >> 6) as usize & 63] as char
852        } else {
853            '='
854        });
855        out.push(if chunk.len() > 2 {
856            B64[n as usize & 63] as char
857        } else {
858            '='
859        });
860    }
861    out
862}
863
864/// Decode standard base64 (padding optional).
865pub fn b64_decode(s: &str) -> Result<Vec<u8>> {
866    let mut out = Vec::with_capacity(s.len() / 4 * 3);
867    let (mut acc, mut bits) = (0u32, 0u32);
868    for c in s.bytes() {
869        let v = match c {
870            b'A'..=b'Z' => c - b'A',
871            b'a'..=b'z' => c - b'a' + 26,
872            b'0'..=b'9' => c - b'0' + 52,
873            b'+' => 62,
874            b'/' => 63,
875            b'=' => break,
876            b'\n' | b'\r' | b' ' => continue,
877            _ => return Err(Error::invalid("invalid base64")),
878        };
879        acc = acc << 6 | v as u32;
880        bits += 6;
881        if bits >= 8 {
882            bits -= 8;
883            out.push((acc >> bits) as u8);
884        }
885    }
886    Ok(out)
887}
888
889#[cfg(test)]
890mod tests {
891    use super::*;
892    use std::io::Cursor;
893
894    #[derive(Clone)]
895    struct Buf(Arc<Mutex<Vec<u8>>>);
896    impl Write for Buf {
897        fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
898            self.0.lock().unwrap().write(b)
899        }
900        fn flush(&mut self) -> std::io::Result<()> {
901            Ok(())
902        }
903    }
904
905    fn run(input: &str) -> Vec<Value> {
906        let buf = Buf(Arc::new(Mutex::new(Vec::new())));
907        let client = Client::with_socket("/nonexistent/isb-test.socket");
908        serve(Cursor::new(input.to_string()), buf.clone(), client).unwrap();
909        let out = String::from_utf8(buf.0.lock().unwrap().clone()).unwrap();
910        out.lines()
911            .map(|l| serde_json::from_str(l).unwrap())
912            .collect()
913    }
914
915    fn by_id(msgs: &[Value], id: i64) -> Value {
916        msgs.iter()
917            .find(|m| {
918                m["id"] == json!(id) && (m.get("result").is_some() || m.get("error").is_some())
919            })
920            .cloned()
921            .unwrap_or_else(|| panic!("no reply for {id}: {msgs:?}"))
922    }
923
924    #[test]
925    fn hello_version_schema_and_errors() {
926        let msgs = run(concat!(
927            "{\"id\":1,\"method\":\"version\"}\n",
928            "{\"id\":2,\"method\":\"schema\"}\n",
929            "not json\n",
930            "{\"id\":3,\"method\":\"nope\"}\n",
931            "{\"id\":4,\"method\":\"sandbox.get\",\"params\":{\"nam\":\"x\"}}\n",
932            "{\"id\":5,\"method\":\"sandbox.get\",\"params\":{\"name\":\"x\"}}\n",
933            "{\"id\":6,\"method\":\"exec.signal\",\"params\":{\"exec\":99,\"signal\":2}}\n",
934        ));
935        assert_eq!(msgs[0]["protocol"], json!(PROTOCOL));
936        assert_eq!(by_id(&msgs, 1)["result"]["protocol"], json!(PROTOCOL));
937        assert!(by_id(&msgs, 2)["result"].to_string().contains("services"));
938        assert!(msgs.iter().any(|m| m["error"]["code"] == "bad_request"));
939        assert_eq!(by_id(&msgs, 3)["error"]["code"], "protocol");
940        assert_eq!(by_id(&msgs, 4)["error"]["code"], "invalid");
941        let e = by_id(&msgs, 5);
942        assert_eq!(e["error"]["code"], "connect");
943        assert_eq!(e["error"]["data"]["socket"], "/nonexistent/isb-test.socket");
944        assert_eq!(by_id(&msgs, 6)["error"]["code"], "not_found");
945    }
946
947    #[test]
948    fn compose_load_over_rpc() {
949        let dir = tempfile::tempdir().unwrap();
950        let f = dir.path().join("isb.yaml");
951        std::fs::write(&f, "services:\n  web: {image: \"${IMG}\", cpus: 2}\n").unwrap();
952        let req = json!({"id": 1, "method": "compose.load", "params": {"files": [f], "vars": {"IMG": "dev-base"}, "project_name": "demo"}});
953        let msgs = run(&format!("{req}\n"));
954        let r = &by_id(&msgs, 1)["result"];
955        assert_eq!(r["name"], "demo");
956        assert_eq!(r["file"]["services"]["web"]["image"], "dev-base");
957        assert_eq!(r["file"]["services"]["web"]["container_name"], "demo-web");
958    }
959
960    #[test]
961    fn base64_roundtrip() {
962        for s in [
963            &b""[..],
964            b"f",
965            b"fo",
966            b"foo",
967            b"foob",
968            b"fooba",
969            b"foobar",
970            &[0, 255, 128, 7],
971        ] {
972            assert_eq!(b64_decode(&b64_encode(s)).unwrap(), s);
973        }
974        assert_eq!(b64_encode(b"foobar"), "Zm9vYmFy");
975        assert_eq!(b64_encode(b"fo"), "Zm8=");
976        assert!(b64_decode("@@").is_err());
977    }
978}