1use std::sync::{mpsc, Arc, Mutex};
16
17use base64::engine::general_purpose::STANDARD as B64;
18use base64::Engine as _;
19use serde_json::{json, Value};
20
21use crate::args::{strvec, NetOpts};
22use crate::error::{Error, Result};
23use crate::transport::{http_request, normalize_url, ws_url, WsTransport, TOKEN_ENV, URL_ENV};
24use crate::types::{CommandResult, RemoteExecResult, SandboxInfo, ShellSessionInfo};
25
26const MACHINE_FIELDS: &str = "id name image kind command status running exitCode pid detached \
31 cpus mem volume stateDir createdAt finishedAt network netIp \
32 ports { bind host guest }";
33const CMD_RESULT_FIELDS: &str = "exitCode stdout stderr";
34const SESSION_FIELDS: &str = "id machineId finished truncated";
35
36fn list_query() -> String {
37 format!("query($all: Boolean!) {{ machines(all: $all) {{ {MACHINE_FIELDS} }} }}")
38}
39fn get_query() -> String {
40 format!("query($id: String!) {{ machine(id: $id) {{ {MACHINE_FIELDS} }} }}")
41}
42const LOGS_QUERY: &str =
43 "query($id: String!, $boot: Boolean!) { machineLogs(id: $id, boot: $boot) }";
44
45fn stop_mutation() -> String {
46 format!("mutation($id: String!) {{ stopMachine(id: $id) {{ {CMD_RESULT_FIELDS} }} }}")
47}
48fn start_mutation() -> String {
49 format!("mutation($id: String!) {{ startMachine(id: $id) {{ {CMD_RESULT_FIELDS} }} }}")
50}
51fn remove_mutation() -> String {
52 format!(
53 "mutation($ids: [String!]!, $force: Boolean!) {{ \
54 removeMachines(ids: $ids, force: $force) {{ {CMD_RESULT_FIELDS} }} }}"
55 )
56}
57fn update_mutation() -> String {
58 format!(
59 "mutation($id: String!, $cpus: Int, $mem: Int) {{ \
60 updateMachine(id: $id, cpus: $cpus, mem: $mem) {{ {CMD_RESULT_FIELDS} }} }}"
61 )
62}
63fn commit_mutation() -> String {
64 format!(
65 "mutation($id: String!, $name: String!, $description: String!) {{ \
66 commitMachine(id: $id, name: $name, description: $description) {{ {CMD_RESULT_FIELDS} }} }}"
67 )
68}
69
70const RUN_LINUX_MUTATION: &str = "mutation($input: RunLinuxInput!) { runLinux(input: $input) }";
71const RUN_BSD_MUTATION: &str = "mutation($input: RunBsdInput!) { runBsd(input: $input) }";
72const RUN_NANOS_MUTATION: &str = "mutation($input: RunNanosInput!) { runNanos(input: $input) }";
73const RUN_UNIKRAFT_MUTATION: &str =
74 "mutation($input: RunUnikraftInput!) { runUnikraft(input: $input) }";
75const RUN_SOLO5_MUTATION: &str = "mutation($input: RunSolo5Input!) { runSolo5(input: $input) }";
76const RUN_OSV_MUTATION: &str = "mutation($input: RunOsvInput!) { runOsv(input: $input) }";
77const RUN_FLAVOR_MUTATION: &str = "mutation($input: RunFlavorInput!) { runFlavor(input: $input) }";
78
79const MACHINE_LOGS_SUBSCRIPTION: &str =
80 "subscription($id: String!, $follow: Boolean!, $boot: Boolean!) { \
81 machineLogs(id: $id, follow: $follow, boot: $boot) { dataBase64 exitCode } }";
82
83fn open_shell_mutation() -> String {
84 format!(
85 "mutation($machineId: String!, $command: [String!]!, $env: [String!]!, \
86 $rows: Int!, $cols: Int!) {{ \
87 openShell(machineId: $machineId, command: $command, env: $env, \
88 rows: $rows, cols: $cols) {{ {SESSION_FIELDS} }} }}"
89 )
90}
91const SHELL_OUTPUT_SUBSCRIPTION: &str = "subscription($sessionId: String!) { \
92 shellOutput(sessionId: $sessionId) { dataBase64 exitCode } }";
93const SEND_INPUT_MUTATION: &str = "mutation($sessionId: String!, $dataBase64: String!) { \
94 sendShellInput(sessionId: $sessionId, dataBase64: $dataBase64) }";
95const RESIZE_MUTATION: &str = "mutation($sessionId: String!, $rows: Int!, $cols: Int!) { \
96 resizeShell(sessionId: $sessionId, rows: $rows, cols: $cols) }";
97const CLOSE_MUTATION: &str = "mutation($sessionId: String!) { closeShell(sessionId: $sessionId) }";
98
99struct ClientInner {
104 url: String,
105 token: String,
106 ws: Mutex<Option<Arc<WsTransport>>>,
110}
111
112#[derive(Clone)]
119pub struct Client {
120 inner: Arc<ClientInner>,
121}
122
123impl std::fmt::Debug for Client {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 f.debug_struct("Client")
126 .field("url", &self.inner.url)
127 .finish()
128 }
129}
130
131impl Client {
132 pub fn new(url: impl Into<String>, token: impl Into<String>) -> Result<Client> {
137 let url = normalize_url(&url.into());
138 if url.is_empty() {
139 return Err(Error::InvalidInput("the daemon URL is empty".into()));
140 }
141 let token = token.into().trim().to_string();
142 if token.is_empty() {
143 return Err(Error::InvalidInput(
144 "a daemon URL without a token is a configuration error; pass the bearer token"
145 .into(),
146 ));
147 }
148 Ok(Client {
149 inner: Arc::new(ClientInner {
150 url,
151 token,
152 ws: Mutex::new(None),
153 }),
154 })
155 }
156
157 pub fn from_env() -> Result<Client> {
164 let url = std::env::var(URL_ENV)
165 .unwrap_or_default()
166 .trim()
167 .to_string();
168 if url.is_empty() {
169 return Err(Error::InvalidInput(format!(
170 "{URL_ENV} is not set; nothing to connect to"
171 )));
172 }
173 let token = std::env::var(TOKEN_ENV)
174 .unwrap_or_default()
175 .trim()
176 .to_string();
177 if token.is_empty() {
178 return Err(Error::InvalidInput(format!(
179 "{URL_ENV} is set but {TOKEN_ENV} is not"
180 )));
181 }
182 Client::new(url, token)
183 }
184
185 pub fn url(&self) -> &str {
187 &self.inner.url
188 }
189
190 pub fn request(&self, query: &str, variables: Value) -> Result<Value> {
194 http_request(&self.inner.url, &self.inner.token, query, &variables)
195 }
196
197 pub fn subscribe(
200 &self,
201 query: &str,
202 variables: Value,
203 on_next: impl FnMut(Value) + Send + 'static,
204 ) -> Result<Subscription> {
205 self.subscribe_with(query, variables, on_next, |_| {}, || {})
206 }
207
208 pub fn subscribe_with(
210 &self,
211 query: &str,
212 variables: Value,
213 on_next: impl FnMut(Value) + Send + 'static,
214 on_error: impl FnMut(Error) + Send + 'static,
215 on_complete: impl FnMut() + Send + 'static,
216 ) -> Result<Subscription> {
217 let transport = self.ws();
218 let id = transport.subscribe(
219 query,
220 variables,
221 Box::new(on_next),
222 Box::new(on_error),
223 Box::new(on_complete),
224 )?;
225 Ok(Subscription { transport, id })
226 }
227
228 fn ws(&self) -> Arc<WsTransport> {
229 let mut guard = self.inner.ws.lock().unwrap();
230 guard
231 .get_or_insert_with(|| {
232 Arc::new(WsTransport::new(
233 ws_url(&self.inner.url),
234 self.inner.token.clone(),
235 ))
236 })
237 .clone()
238 }
239
240 pub fn list(&self, all: bool) -> Result<Vec<SandboxInfo>> {
244 let data = self.request(&list_query(), json!({"all": all}))?;
245 Ok(data
246 .get("machines")
247 .and_then(Value::as_array)
248 .map(|machines| machines.iter().map(SandboxInfo::from_graphql).collect())
249 .unwrap_or_default())
250 }
251
252 pub fn get(&self, id: &str) -> Result<Option<SandboxInfo>> {
254 let data = self.request(&get_query(), json!({"id": id}))?;
255 Ok(data
256 .get("machine")
257 .filter(|m| !m.is_null())
258 .map(SandboxInfo::from_graphql))
259 }
260
261 pub fn stop(&self, id: &str) -> Result<CommandResult> {
262 let data = self.request(&stop_mutation(), json!({"id": id}))?;
263 Ok(CommandResult::from_graphql(&data["stopMachine"]))
264 }
265
266 pub fn start(&self, id: &str) -> Result<CommandResult> {
267 let data = self.request(&start_mutation(), json!({"id": id}))?;
268 Ok(CommandResult::from_graphql(&data["startMachine"]))
269 }
270
271 pub fn remove<S: AsRef<str>>(&self, ids: &[S], force: bool) -> Result<CommandResult> {
272 let ids: Vec<&str> = ids.iter().map(AsRef::as_ref).collect();
273 let data = self.request(&remove_mutation(), json!({"ids": ids, "force": force}))?;
274 Ok(CommandResult::from_graphql(&data["removeMachines"]))
275 }
276
277 pub fn update(&self, id: &str, cpus: Option<u32>, mem: Option<u32>) -> Result<CommandResult> {
279 let data = self.request(
280 &update_mutation(),
281 json!({"id": id, "cpus": cpus, "mem": mem}),
282 )?;
283 Ok(CommandResult::from_graphql(&data["updateMachine"]))
284 }
285
286 pub fn commit(&self, id: &str, name: &str, description: &str) -> Result<CommandResult> {
288 let data = self.request(
289 &commit_mutation(),
290 json!({"id": id, "name": name, "description": description}),
291 )?;
292 Ok(CommandResult::from_graphql(&data["commitMachine"]))
293 }
294
295 pub fn logs(&self, id: &str, boot: bool) -> Result<String> {
298 let data = self.request(LOGS_QUERY, json!({"id": id, "boot": boot}))?;
299 Ok(data
300 .get("machineLogs")
301 .and_then(Value::as_str)
302 .unwrap_or_default()
303 .to_string())
304 }
305
306 pub fn follow_logs(&self, id: &str) -> FollowLogsBuilder {
317 FollowLogsBuilder {
318 client: self.clone(),
319 id: id.to_string(),
320 follow: true,
321 boot: false,
322 on_data: None,
323 on_error: None,
324 on_complete: None,
325 }
326 }
327
328 pub fn run_linux(&self) -> RunLinuxBuilder {
332 RunLinuxBuilder {
333 client: self.clone(),
334 image: None,
335 cpus: None,
336 mem: None,
337 net: NetOpts::default(),
338 volume: None,
339 mounts: Vec::new(),
340 attach_disk: Vec::new(),
341 env: Vec::new(),
342 entrypoint: None,
343 initramfs: false,
344 kernel: None,
345 kernel_version: None,
346 console: None,
347 repo: None,
348 command: Vec::new(),
349 }
350 }
351
352 pub fn run_bsd(&self, os: BsdOs) -> RunBsdBuilder {
354 RunBsdBuilder {
355 client: self.clone(),
356 os,
357 version: None,
358 cpus: None,
359 mem: None,
360 net: NetOpts::default(),
361 volume: None,
362 persist: false,
363 force: false,
364 firmware: None,
365 attach_disk: Vec::new(),
366 disk_size: None,
367 repo: None,
368 command: Vec::new(),
369 }
370 }
371
372 pub fn run_nanos(&self) -> RunNanosBuilder {
374 RunNanosBuilder {
375 client: self.clone(),
376 image: None,
377 cpus: None,
378 mem: None,
379 net: NetOpts::default(),
380 kernel: None,
381 cmdline: None,
382 persist: false,
383 }
384 }
385
386 pub fn run_unikraft(&self) -> RunUnikraftBuilder {
388 RunUnikraftBuilder {
389 client: self.clone(),
390 path: None,
391 cpus: None,
392 mem: None,
393 net: NetOpts::default(),
394 cmdline: None,
395 initramfs: None,
396 mounts: Vec::new(),
397 }
398 }
399
400 pub fn run_solo5(&self) -> RunSolo5Builder {
405 RunSolo5Builder {
406 client: self.clone(),
407 path: None,
408 cpus: None,
409 mem: None,
410 net: NetOpts::default(),
411 block: Vec::new(),
412 args: Vec::new(),
413 }
414 }
415
416 pub fn run_osv(&self) -> RunOsvBuilder {
418 RunOsvBuilder {
419 client: self.clone(),
420 image: None,
421 cpus: None,
422 mem: None,
423 net: NetOpts::default(),
424 cmdline: None,
425 disk: None,
426 no_disk: false,
427 attach_disk: Vec::new(),
428 gic: None,
429 persist: false,
430 volume: None,
431 }
432 }
433
434 pub fn run_flavor(&self, name: impl Into<String>) -> RunFlavorBuilder {
436 RunFlavorBuilder {
437 client: self.clone(),
438 name: name.into(),
439 cpus: None,
440 mem: None,
441 ports: Vec::new(),
442 volume: None,
443 repo: None,
444 }
445 }
446
447 pub fn exec<I, S>(&self, id: &str, command: I) -> Result<RemoteExecResult>
451 where
452 I: IntoIterator<Item = S>,
453 S: Into<String>,
454 {
455 self.exec_with_env(id, command, Vec::<String>::new())
456 }
457
458 pub fn exec_with_env<I, S, E, T>(
469 &self,
470 id: &str,
471 command: I,
472 env: E,
473 ) -> Result<RemoteExecResult>
474 where
475 I: IntoIterator<Item = S>,
476 S: Into<String>,
477 E: IntoIterator<Item = T>,
478 T: Into<String>,
479 {
480 let transport = self.ws();
481 let data = self.request(
482 &open_shell_mutation(),
483 json!({
484 "machineId": id,
485 "command": strvec(command),
486 "env": strvec(env),
487 "rows": 24,
488 "cols": 80,
489 }),
490 )?;
491 let session = ShellSessionInfo::from_graphql(&data["openShell"]);
492
493 let chunks = Arc::new(Mutex::new(Vec::<u8>::new()));
494 let (done_tx, done_rx) = mpsc::channel::<Result<i32>>();
499
500 let chunk_sink = Arc::clone(&chunks);
501 let exit_tx = done_tx.clone();
502 let error_tx = done_tx.clone();
503 let complete_tx = done_tx;
504
505 let outcome: Result<i32> = (|| {
506 let sub_id = transport.subscribe(
507 SHELL_OUTPUT_SUBSCRIPTION,
508 json!({"sessionId": session.id}),
509 Box::new(move |data: Value| {
510 let payload = &data["shellOutput"];
511 if let Some(b64) = payload["dataBase64"].as_str() {
512 if let Ok(bytes) = B64.decode(b64) {
513 chunk_sink.lock().unwrap().extend_from_slice(&bytes);
514 }
515 }
516 if let Some(code) = payload["exitCode"].as_i64() {
517 let _ = exit_tx.send(Ok(code as i32));
518 }
519 }),
520 Box::new(move |err: Error| {
521 let _ = error_tx.send(Err(err));
522 }),
523 Box::new(move || {
524 let _ = complete_tx.send(Err(Error::GraphQL {
528 message: "shell session ended before an exit code arrived".to_string(),
529 code: None,
530 }));
531 }),
532 )?;
533 let outcome = done_rx.recv().unwrap_or_else(|_| {
534 Err(Error::GraphQL {
535 message: "the shell output subscription was dropped".to_string(),
536 code: None,
537 })
538 });
539 transport.unsubscribe(&sub_id);
540 outcome
541 })();
542
543 let _ = self.request(CLOSE_MUTATION, json!({"sessionId": session.id}));
546
547 let exit_code = outcome?;
548 let output = chunks.lock().unwrap().clone();
549 Ok(RemoteExecResult { exit_code, output })
550 }
551
552 pub fn shell(&self, id: &str) -> ShellBuilder {
563 ShellBuilder {
564 client: self.clone(),
565 machine_id: id.to_string(),
566 command: Vec::new(),
567 env: Vec::new(),
568 rows: 24,
569 cols: 80,
570 }
571 }
572}
573
574pub struct Subscription {
578 transport: Arc<WsTransport>,
579 id: String,
580}
581
582impl Subscription {
583 pub fn id(&self) -> &str {
585 &self.id
586 }
587
588 pub fn unsubscribe(self) {
590 self.transport.unsubscribe(&self.id);
591 }
592}
593
594type DataFn = Box<dyn FnMut(Vec<u8>) + Send>;
599type ErrFn = Box<dyn FnMut(Error) + Send>;
600type DoneFn = Box<dyn FnMut() + Send>;
601
602pub struct FollowLogsBuilder {
604 client: Client,
605 id: String,
606 follow: bool,
607 boot: bool,
608 on_data: Option<DataFn>,
609 on_error: Option<ErrFn>,
610 on_complete: Option<DoneFn>,
611}
612
613impl FollowLogsBuilder {
614 pub fn follow(mut self, follow: bool) -> Self {
616 self.follow = follow;
617 self
618 }
619
620 pub fn boot(mut self, boot: bool) -> Self {
622 self.boot = boot;
623 self
624 }
625
626 pub fn on_data(mut self, cb: impl FnMut(Vec<u8>) + Send + 'static) -> Self {
628 self.on_data = Some(Box::new(cb));
629 self
630 }
631
632 pub fn on_error(mut self, cb: impl FnMut(Error) + Send + 'static) -> Self {
634 self.on_error = Some(Box::new(cb));
635 self
636 }
637
638 pub fn on_complete(mut self, cb: impl FnMut() + Send + 'static) -> Self {
640 self.on_complete = Some(Box::new(cb));
641 self
642 }
643
644 pub fn start(self) -> Result<Subscription> {
646 let mut on_data = self.on_data.unwrap_or_else(|| Box::new(|_| {}));
647 let on_error = self.on_error.unwrap_or_else(|| Box::new(|_| {}));
648 let on_complete = self.on_complete.unwrap_or_else(|| Box::new(|| {}));
649 let transport = self.client.ws();
650 let id = transport.subscribe(
651 MACHINE_LOGS_SUBSCRIPTION,
652 json!({"id": self.id, "follow": self.follow, "boot": self.boot}),
653 Box::new(move |data: Value| {
654 if let Some(b64) = data
655 .pointer("/machineLogs/dataBase64")
656 .and_then(Value::as_str)
657 {
658 if let Ok(bytes) = B64.decode(b64) {
659 on_data(bytes);
660 }
661 }
662 }),
666 on_error,
667 on_complete,
668 )?;
669 Ok(Subscription { transport, id })
670 }
671}
672
673#[derive(Debug, Clone, Copy, PartialEq, Eq)]
679pub enum BsdOs {
680 Freebsd,
681 Netbsd,
682}
683
684impl BsdOs {
685 fn graphql(self) -> &'static str {
686 match self {
687 BsdOs::Freebsd => "FREEBSD",
688 BsdOs::Netbsd => "NETBSD",
689 }
690 }
691}
692
693fn net_input(net: &NetOpts) -> Value {
694 if !net.touched {
695 return Value::Null;
696 }
697 json!({
698 "noNet": net.no_net,
699 "ports": net.ports,
700 "mac": net.mac,
701 "network": net.network,
702 "name": net.name,
703 })
704}
705
706fn launch_mutation(client: &Client, mutation: &str, key: &str, input: Value) -> Result<String> {
707 let data = client.request(mutation, json!({"input": input}))?;
708 data.get(key)
709 .and_then(Value::as_str)
710 .map(str::to_string)
711 .ok_or_else(|| Error::GraphQL {
712 message: format!("the daemon's {key} response carried no machine id"),
713 code: None,
714 })
715}
716
717macro_rules! remote_net_vm_setters {
721 () => {
722 pub fn cpus(mut self, cpus: u32) -> Self {
724 self.cpus = Some(cpus);
725 self
726 }
727
728 pub fn mem(mut self, mib: u32) -> Self {
730 self.mem = Some(mib);
731 self
732 }
733
734 pub fn port(mut self, forward: impl Into<String>) -> Self {
736 self.net.touched = true;
737 self.net.ports.push(forward.into());
738 self
739 }
740
741 pub fn forward(self, host: u16, guest: u16) -> Self {
743 self.port(format!("{host}:{guest}"))
744 }
745
746 pub fn mac(mut self, mac: impl Into<String>) -> Self {
748 self.net.touched = true;
749 self.net.mac = Some(mac.into());
750 self
751 }
752
753 pub fn network(mut self, network: impl Into<String>) -> Self {
755 self.net.touched = true;
756 self.net.network = Some(network.into());
757 self
758 }
759
760 pub fn name(mut self, name: impl Into<String>) -> Self {
762 self.net.touched = true;
763 self.net.name = Some(name.into());
764 self
765 }
766
767 pub fn no_net(mut self) -> Self {
769 self.net.touched = true;
770 self.net.no_net = true;
771 self
772 }
773 };
774}
775
776pub struct RunLinuxBuilder {
778 client: Client,
779 image: Option<String>,
780 cpus: Option<u32>,
781 mem: Option<u32>,
782 net: NetOpts,
783 volume: Option<String>,
784 mounts: Vec<String>,
785 attach_disk: Vec<String>,
786 env: Vec<String>,
787 entrypoint: Option<String>,
788 initramfs: bool,
789 kernel: Option<String>,
790 kernel_version: Option<String>,
791 console: Option<String>,
792 repo: Option<String>,
793 command: Vec<String>,
794}
795
796impl RunLinuxBuilder {
797 remote_net_vm_setters!();
798
799 pub fn image(mut self, image: impl Into<String>) -> Self {
801 self.image = Some(image.into());
802 self
803 }
804
805 pub fn volume(mut self, name: impl Into<String>) -> Self {
807 self.volume = Some(name.into());
808 self
809 }
810
811 pub fn mount(mut self, mount: impl Into<String>) -> Self {
813 self.mounts.push(mount.into());
814 self
815 }
816
817 pub fn attach_disk(mut self, disk: impl Into<String>) -> Self {
820 self.attach_disk.push(disk.into());
821 self
822 }
823
824 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
826 self.env.push(format!("{}={}", key.into(), value.into()));
827 self
828 }
829
830 pub fn entrypoint(mut self, entrypoint: impl Into<String>) -> Self {
832 self.entrypoint = Some(entrypoint.into());
833 self
834 }
835
836 pub fn initramfs(mut self) -> Self {
838 self.initramfs = true;
839 self
840 }
841
842 pub fn kernel(mut self, kernel: impl Into<String>) -> Self {
844 self.kernel = Some(kernel.into());
845 self
846 }
847
848 pub fn kernel_version(mut self, version: impl Into<String>) -> Self {
850 self.kernel_version = Some(version.into());
851 self
852 }
853
854 pub fn console(mut self, console: impl Into<String>) -> Self {
856 self.console = Some(console.into());
857 self
858 }
859
860 pub fn repo(mut self, repo: impl Into<String>) -> Self {
862 self.repo = Some(repo.into());
863 self
864 }
865
866 pub fn command<I, S>(mut self, command: I) -> Self
868 where
869 I: IntoIterator<Item = S>,
870 S: Into<String>,
871 {
872 self.command = strvec(command);
873 self
874 }
875
876 pub fn launch(self) -> Result<String> {
878 let Some(image) = self.image else {
879 return Err(Error::InvalidInput("run_linux requires an image".into()));
880 };
881 let input = json!({
882 "image": image,
883 "cpus": self.cpus,
884 "mem": self.mem,
885 "net": net_input(&self.net),
886 "volume": self.volume,
887 "mounts": self.mounts,
888 "attachDisk": self.attach_disk,
889 "env": self.env,
890 "entrypoint": self.entrypoint,
891 "initramfs": self.initramfs,
892 "kernel": self.kernel,
893 "kernelVersion": self.kernel_version,
894 "console": self.console,
895 "repo": self.repo,
896 "command": self.command,
897 });
898 launch_mutation(&self.client, RUN_LINUX_MUTATION, "runLinux", input)
899 }
900}
901
902pub struct RunBsdBuilder {
904 client: Client,
905 os: BsdOs,
906 version: Option<String>,
907 cpus: Option<u32>,
908 mem: Option<u32>,
909 net: NetOpts,
910 volume: Option<String>,
911 persist: bool,
912 force: bool,
913 firmware: Option<String>,
914 attach_disk: Vec<String>,
915 disk_size: Option<String>,
916 repo: Option<String>,
917 command: Vec<String>,
918}
919
920impl RunBsdBuilder {
921 remote_net_vm_setters!();
922
923 pub fn version(mut self, version: impl Into<String>) -> Self {
925 self.version = Some(version.into());
926 self
927 }
928
929 pub fn volume(mut self, name: impl Into<String>) -> Self {
931 self.volume = Some(name.into());
932 self
933 }
934
935 pub fn persist(mut self) -> Self {
937 self.persist = true;
938 self
939 }
940
941 pub fn force(mut self) -> Self {
943 self.force = true;
944 self
945 }
946
947 pub fn firmware(mut self, firmware: impl Into<String>) -> Self {
949 self.firmware = Some(firmware.into());
950 self
951 }
952
953 pub fn attach_disk(mut self, disk: impl Into<String>) -> Self {
955 self.attach_disk.push(disk.into());
956 self
957 }
958
959 pub fn disk_size(mut self, size: impl Into<String>) -> Self {
961 self.disk_size = Some(size.into());
962 self
963 }
964
965 pub fn repo(mut self, repo: impl Into<String>) -> Self {
967 self.repo = Some(repo.into());
968 self
969 }
970
971 pub fn command<I, S>(mut self, command: I) -> Self
973 where
974 I: IntoIterator<Item = S>,
975 S: Into<String>,
976 {
977 self.command = strvec(command);
978 self
979 }
980
981 pub fn launch(self) -> Result<String> {
983 let input = json!({
984 "os": self.os.graphql(),
985 "version": self.version,
986 "cpus": self.cpus,
987 "mem": self.mem,
988 "net": net_input(&self.net),
989 "volume": self.volume,
990 "persist": self.persist,
991 "force": self.force,
992 "firmware": self.firmware,
993 "attachDisk": self.attach_disk,
994 "diskSize": self.disk_size,
995 "repo": self.repo,
996 "command": self.command,
997 });
998 launch_mutation(&self.client, RUN_BSD_MUTATION, "runBsd", input)
999 }
1000}
1001
1002pub struct RunNanosBuilder {
1004 client: Client,
1005 image: Option<String>,
1006 cpus: Option<u32>,
1007 mem: Option<u32>,
1008 net: NetOpts,
1009 kernel: Option<String>,
1010 cmdline: Option<String>,
1011 persist: bool,
1012}
1013
1014impl RunNanosBuilder {
1015 remote_net_vm_setters!();
1016
1017 pub fn image(mut self, image: impl Into<String>) -> Self {
1019 self.image = Some(image.into());
1020 self
1021 }
1022
1023 pub fn kernel(mut self, kernel: impl Into<String>) -> Self {
1025 self.kernel = Some(kernel.into());
1026 self
1027 }
1028
1029 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
1031 self.cmdline = Some(cmdline.into());
1032 self
1033 }
1034
1035 pub fn persist(mut self) -> Self {
1037 self.persist = true;
1038 self
1039 }
1040
1041 pub fn launch(self) -> Result<String> {
1043 let Some(image) = self.image else {
1044 return Err(Error::InvalidInput("run_nanos requires an image".into()));
1045 };
1046 let input = json!({
1047 "image": image,
1048 "cpus": self.cpus,
1049 "mem": self.mem,
1050 "net": net_input(&self.net),
1051 "kernel": self.kernel,
1052 "cmdline": self.cmdline,
1053 "persist": self.persist,
1054 });
1055 launch_mutation(&self.client, RUN_NANOS_MUTATION, "runNanos", input)
1056 }
1057}
1058
1059pub struct RunUnikraftBuilder {
1061 client: Client,
1062 path: Option<String>,
1063 cpus: Option<u32>,
1064 mem: Option<u32>,
1065 net: NetOpts,
1066 cmdline: Option<String>,
1067 initramfs: Option<String>,
1068 mounts: Vec<String>,
1069}
1070
1071impl RunUnikraftBuilder {
1072 remote_net_vm_setters!();
1073
1074 pub fn path(mut self, path: impl Into<String>) -> Self {
1076 self.path = Some(path.into());
1077 self
1078 }
1079
1080 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
1082 self.cmdline = Some(cmdline.into());
1083 self
1084 }
1085
1086 pub fn initramfs(mut self, path: impl Into<String>) -> Self {
1088 self.initramfs = Some(path.into());
1089 self
1090 }
1091
1092 pub fn mount(mut self, mount: impl Into<String>) -> Self {
1095 self.mounts.push(mount.into());
1096 self
1097 }
1098
1099 pub fn launch(self) -> Result<String> {
1101 let input = json!({
1102 "path": self.path,
1103 "cpus": self.cpus,
1104 "mem": self.mem,
1105 "net": net_input(&self.net),
1106 "cmdline": self.cmdline,
1107 "initramfs": self.initramfs,
1108 "mounts": self.mounts,
1109 });
1110 launch_mutation(&self.client, RUN_UNIKRAFT_MUTATION, "runUnikraft", input)
1111 }
1112}
1113
1114pub struct RunSolo5Builder {
1116 client: Client,
1117 path: Option<String>,
1118 cpus: Option<u32>,
1119 mem: Option<u32>,
1120 net: NetOpts,
1121 block: Vec<String>,
1122 args: Vec<String>,
1123}
1124
1125impl RunSolo5Builder {
1126 remote_net_vm_setters!();
1127
1128 pub fn path(mut self, path: impl Into<String>) -> Self {
1131 self.path = Some(path.into());
1132 self
1133 }
1134
1135 pub fn block(mut self, block: impl Into<String>) -> Self {
1137 self.block.push(block.into());
1138 self
1139 }
1140
1141 pub fn args<I, S>(mut self, args: I) -> Self
1143 where
1144 I: IntoIterator<Item = S>,
1145 S: Into<String>,
1146 {
1147 self.args = strvec(args);
1148 self
1149 }
1150
1151 pub fn launch(self) -> Result<String> {
1153 let input = json!({
1154 "path": self.path,
1155 "cpus": self.cpus,
1156 "mem": self.mem,
1157 "net": net_input(&self.net),
1158 "block": self.block,
1159 "args": self.args,
1160 });
1161 launch_mutation(&self.client, RUN_SOLO5_MUTATION, "runSolo5", input)
1162 }
1163}
1164
1165pub struct RunOsvBuilder {
1167 client: Client,
1168 image: Option<String>,
1169 cpus: Option<u32>,
1170 mem: Option<u32>,
1171 net: NetOpts,
1172 cmdline: Option<String>,
1173 disk: Option<String>,
1174 no_disk: bool,
1175 attach_disk: Vec<String>,
1176 gic: Option<String>,
1177 persist: bool,
1178 volume: Option<String>,
1179}
1180
1181impl RunOsvBuilder {
1182 remote_net_vm_setters!();
1183
1184 pub fn image(mut self, image: impl Into<String>) -> Self {
1186 self.image = Some(image.into());
1187 self
1188 }
1189
1190 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
1192 self.cmdline = Some(cmdline.into());
1193 self
1194 }
1195
1196 pub fn disk(mut self, disk: impl Into<String>) -> Self {
1198 self.disk = Some(disk.into());
1199 self
1200 }
1201
1202 pub fn no_disk(mut self) -> Self {
1204 self.no_disk = true;
1205 self
1206 }
1207
1208 pub fn attach_disk(mut self, disk: impl Into<String>) -> Self {
1210 self.attach_disk.push(disk.into());
1211 self
1212 }
1213
1214 pub fn gic(mut self, gic: impl Into<String>) -> Self {
1216 self.gic = Some(gic.into());
1217 self
1218 }
1219
1220 pub fn persist(mut self) -> Self {
1222 self.persist = true;
1223 self
1224 }
1225
1226 pub fn volume(mut self, name: impl Into<String>) -> Self {
1228 self.volume = Some(name.into());
1229 self
1230 }
1231
1232 pub fn launch(self) -> Result<String> {
1234 let Some(image) = self.image else {
1235 return Err(Error::InvalidInput("run_osv requires an image".into()));
1236 };
1237 let input = json!({
1238 "image": image,
1239 "cpus": self.cpus,
1240 "mem": self.mem,
1241 "net": net_input(&self.net),
1242 "cmdline": self.cmdline,
1243 "disk": self.disk,
1244 "noDisk": self.no_disk,
1245 "attachDisk": self.attach_disk,
1246 "gic": self.gic,
1247 "persist": self.persist,
1248 "volume": self.volume,
1249 });
1250 launch_mutation(&self.client, RUN_OSV_MUTATION, "runOsv", input)
1251 }
1252}
1253
1254pub struct RunFlavorBuilder {
1256 client: Client,
1257 name: String,
1258 cpus: Option<u32>,
1259 mem: Option<u32>,
1260 ports: Vec<String>,
1261 volume: Option<String>,
1262 repo: Option<String>,
1263}
1264
1265impl RunFlavorBuilder {
1266 pub fn cpus(mut self, cpus: u32) -> Self {
1268 self.cpus = Some(cpus);
1269 self
1270 }
1271
1272 pub fn mem(mut self, mib: u32) -> Self {
1274 self.mem = Some(mib);
1275 self
1276 }
1277
1278 pub fn port(mut self, forward: impl Into<String>) -> Self {
1280 self.ports.push(forward.into());
1281 self
1282 }
1283
1284 pub fn volume(mut self, name: impl Into<String>) -> Self {
1286 self.volume = Some(name.into());
1287 self
1288 }
1289
1290 pub fn repo(mut self, repo: impl Into<String>) -> Self {
1292 self.repo = Some(repo.into());
1293 self
1294 }
1295
1296 pub fn launch(self) -> Result<String> {
1298 let input = json!({
1299 "name": self.name,
1300 "cpus": self.cpus,
1301 "mem": self.mem,
1302 "ports": self.ports,
1303 "volume": self.volume,
1304 "repo": self.repo,
1305 });
1306 launch_mutation(&self.client, RUN_FLAVOR_MUTATION, "runFlavor", input)
1307 }
1308}
1309
1310pub struct ShellBuilder {
1316 client: Client,
1317 machine_id: String,
1318 command: Vec<String>,
1319 env: Vec<String>,
1320 rows: u32,
1321 cols: u32,
1322}
1323
1324impl ShellBuilder {
1325 pub fn command<I, S>(mut self, command: I) -> Self
1327 where
1328 I: IntoIterator<Item = S>,
1329 S: Into<String>,
1330 {
1331 self.command = strvec(command);
1332 self
1333 }
1334
1335 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1337 self.env.push(format!("{}={}", key.into(), value.into()));
1338 self
1339 }
1340
1341 pub fn rows(mut self, rows: u32) -> Self {
1343 self.rows = rows;
1344 self
1345 }
1346
1347 pub fn cols(mut self, cols: u32) -> Self {
1349 self.cols = cols;
1350 self
1351 }
1352
1353 pub fn open(self) -> Result<ShellSession> {
1355 let data = self.client.request(
1356 &open_shell_mutation(),
1357 json!({
1358 "machineId": self.machine_id,
1359 "command": self.command,
1360 "env": self.env,
1361 "rows": self.rows,
1362 "cols": self.cols,
1363 }),
1364 )?;
1365 let info = ShellSessionInfo::from_graphql(&data["openShell"]);
1366 ShellSession::start(self.client, info.id)
1367 }
1368}
1369
1370type OutputFn = Box<dyn FnMut(&[u8]) + Send>;
1371type ExitFn = Box<dyn FnMut(i32) + Send>;
1372
1373struct ShellShared {
1374 output_cb: Option<OutputFn>,
1375 exit_cb: Option<ExitFn>,
1376 buffered_output: Vec<Vec<u8>>,
1382 buffered_exit: Option<i32>,
1383 exit_fired: bool,
1384}
1385
1386pub struct ShellSession {
1394 id: String,
1395 client: Client,
1396 transport: Arc<WsTransport>,
1397 sub_id: String,
1398 shared: Arc<Mutex<ShellShared>>,
1399 closed: bool,
1400}
1401
1402impl std::fmt::Debug for ShellSession {
1403 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1404 f.debug_struct("ShellSession")
1405 .field("id", &self.id)
1406 .finish()
1407 }
1408}
1409
1410impl ShellSession {
1411 fn start(client: Client, session_id: String) -> Result<ShellSession> {
1412 let shared = Arc::new(Mutex::new(ShellShared {
1413 output_cb: None,
1414 exit_cb: None,
1415 buffered_output: Vec::new(),
1416 buffered_exit: None,
1417 exit_fired: false,
1418 }));
1419 let transport = client.ws();
1420
1421 let on_next_shared = Arc::clone(&shared);
1422 let on_error_shared = Arc::clone(&shared);
1423 let sub_id = transport.subscribe(
1424 SHELL_OUTPUT_SUBSCRIPTION,
1425 json!({"sessionId": session_id}),
1426 Box::new(move |data: Value| {
1427 let payload = &data["shellOutput"];
1428 if let Some(b64) = payload["dataBase64"].as_str() {
1429 if let Ok(bytes) = B64.decode(b64) {
1430 emit_output(&on_next_shared, bytes);
1431 }
1432 }
1433 if let Some(code) = payload["exitCode"].as_i64() {
1434 emit_exit(&on_next_shared, code as i32);
1435 }
1436 }),
1437 Box::new(move |_err: Error| {
1438 emit_exit(&on_error_shared, -1);
1443 }),
1444 Box::new(|| {}),
1445 )?;
1446
1447 Ok(ShellSession {
1448 id: session_id,
1449 client,
1450 transport,
1451 sub_id,
1452 shared,
1453 closed: false,
1454 })
1455 }
1456
1457 pub fn id(&self) -> &str {
1459 &self.id
1460 }
1461
1462 pub fn on_output(&self, mut cb: impl FnMut(&[u8]) + Send + 'static) {
1465 let mut shared = self.shared.lock().unwrap();
1466 for chunk in std::mem::take(&mut shared.buffered_output) {
1467 cb(&chunk);
1468 }
1469 shared.output_cb = Some(Box::new(cb));
1470 }
1471
1472 pub fn on_exit(&self, mut cb: impl FnMut(i32) + Send + 'static) {
1474 let mut shared = self.shared.lock().unwrap();
1475 if let Some(code) = shared.buffered_exit.take() {
1476 cb(code);
1477 }
1478 shared.exit_cb = Some(Box::new(cb));
1479 }
1480
1481 pub fn write(&self, data: impl AsRef<[u8]>) -> Result<()> {
1483 self.client.request(
1484 SEND_INPUT_MUTATION,
1485 json!({"sessionId": self.id, "dataBase64": B64.encode(data.as_ref())}),
1486 )?;
1487 Ok(())
1488 }
1489
1490 pub fn resize(&self, rows: u32, cols: u32) -> Result<()> {
1492 self.client.request(
1493 RESIZE_MUTATION,
1494 json!({"sessionId": self.id, "rows": rows, "cols": cols}),
1495 )?;
1496 Ok(())
1497 }
1498
1499 pub fn close(&mut self) {
1501 if self.closed {
1502 return;
1503 }
1504 self.closed = true;
1505 self.transport.unsubscribe(&self.sub_id);
1506 let _ = self
1508 .client
1509 .request(CLOSE_MUTATION, json!({"sessionId": self.id}));
1510 }
1511}
1512
1513fn emit_output(shared: &Arc<Mutex<ShellShared>>, data: Vec<u8>) {
1514 let mut guard = shared.lock().unwrap();
1515 match &mut guard.output_cb {
1516 Some(cb) => cb(&data),
1517 None => guard.buffered_output.push(data),
1518 }
1519}
1520
1521fn emit_exit(shared: &Arc<Mutex<ShellShared>>, code: i32) {
1522 let mut guard = shared.lock().unwrap();
1523 if guard.exit_fired {
1524 return;
1525 }
1526 guard.exit_fired = true;
1527 match &mut guard.exit_cb {
1528 Some(cb) => cb(code),
1529 None => guard.buffered_exit = Some(code),
1530 }
1531}