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::{
25 AiAgent, AiSession, CommandResult, DockerContainer, DockerStatus, RemoteExecResult,
26 SandboxInfo, ShellSessionInfo, SnapshotInfo,
27};
28
29const MACHINE_FIELDS: &str = "id name image kind command status running exitCode pid detached \
34 cpus mem volume stateDir createdAt finishedAt network netIp origin \
35 ports { bind host guest }";
36const SNAPSHOT_FIELDS: &str = "id name machineId machineName kind image path parent description \
37 cpus mem size createdAt ports { bind host guest }";
38const CMD_RESULT_FIELDS: &str = "exitCode stdout stderr";
39const SESSION_FIELDS: &str = "id machineId finished truncated";
40
41fn list_query() -> String {
42 format!("query($all: Boolean!) {{ machines(all: $all) {{ {MACHINE_FIELDS} }} }}")
43}
44fn get_query() -> String {
45 format!("query($id: String!) {{ machine(id: $id) {{ {MACHINE_FIELDS} }} }}")
46}
47const LOGS_QUERY: &str =
48 "query($id: String!, $boot: Boolean!) { machineLogs(id: $id, boot: $boot) }";
49
50fn stop_mutation() -> String {
51 format!("mutation($id: String!) {{ stopMachine(id: $id) {{ {CMD_RESULT_FIELDS} }} }}")
52}
53fn start_mutation() -> String {
54 format!("mutation($id: String!) {{ startMachine(id: $id) {{ {CMD_RESULT_FIELDS} }} }}")
55}
56fn remove_mutation() -> String {
57 format!(
58 "mutation($ids: [String!]!, $force: Boolean!) {{ \
59 removeMachines(ids: $ids, force: $force) {{ {CMD_RESULT_FIELDS} }} }}"
60 )
61}
62fn update_mutation() -> String {
63 format!(
64 "mutation($id: String!, $cpus: Int, $mem: Int) {{ \
65 updateMachine(id: $id, cpus: $cpus, mem: $mem) {{ {CMD_RESULT_FIELDS} }} }}"
66 )
67}
68fn commit_mutation() -> String {
69 format!(
70 "mutation($id: String!, $name: String!, $description: String!) {{ \
71 commitMachine(id: $id, name: $name, description: $description) {{ {CMD_RESULT_FIELDS} }} }}"
72 )
73}
74
75const AI_AGENT_FIELDS: &str = "id label flavor description installed running";
76const AI_SESSION_FIELDS: &str = "id name agent running workspace createdAt";
77
78fn ai_agents_query() -> String {
79 format!("{{ aiAgents {{ {AI_AGENT_FIELDS} }} }}")
80}
81fn ai_sessions_query() -> String {
82 format!("{{ aiSessions {{ {AI_SESSION_FIELDS} }} }}")
83}
84const AI_SHELL_COMMAND_QUERY: &str = "query($agent: String!, $machineId: String!) \
85 { aiShellCommand(agent: $agent, machineId: $machineId) }";
86const AI_START_MUTATION: &str = "mutation($input: AiStartInput!) { aiStart(input: $input) }";
87fn ai_stop_mutation() -> String {
88 format!("mutation($agent: String!) {{ aiStop(agent: $agent) {{ {CMD_RESULT_FIELDS} }} }}")
89}
90fn ai_remove_mutation() -> String {
91 format!(
92 "mutation($agent: String!, $keepHome: Boolean!) {{ \
93 aiRemove(agent: $agent, keepHome: $keepHome) {{ {CMD_RESULT_FIELDS} }} }}"
94 )
95}
96
97const DOCKER_STATUS_FIELDS: &str =
98 "running machineId machineRunning socket socketReady apiPort version \
99 containers images mounts disk diskSize";
100const DOCKER_CONTAINER_FIELDS: &str = "id name image command state status ports created";
101
102fn docker_status_query() -> String {
103 format!("{{ dockerStatus {{ {DOCKER_STATUS_FIELDS} }} }}")
104}
105fn docker_containers_query() -> String {
106 format!(
107 "query($all: Boolean!) {{ dockerContainers(all: $all) \
108 {{ {DOCKER_CONTAINER_FIELDS} }} }}"
109 )
110}
111const DOCKER_LOGS_QUERY: &str =
112 "query($id: String!, $tail: Int!) { dockerContainerLogs(id: $id, tail: $tail) }";
113fn docker_start_mutation() -> String {
114 format!(
115 "mutation($input: DockerStartInput!) {{ dockerStart(input: $input) \
116 {{ {DOCKER_STATUS_FIELDS} }} }}"
117 )
118}
119fn docker_stop_mutation() -> String {
120 format!("mutation {{ dockerStop {{ {CMD_RESULT_FIELDS} }} }}")
121}
122fn docker_container_mutation() -> String {
123 format!(
124 "mutation($action: String!, $ids: [String!]!) {{ \
125 dockerContainer(action: $action, ids: $ids) {{ {CMD_RESULT_FIELDS} }} }}"
126 )
127}
128
129fn snapshots_query() -> String {
130 format!("query($machine: String) {{ snapshots(machine: $machine) {{ {SNAPSHOT_FIELDS} }} }}")
131}
132fn snapshot_mutation() -> String {
133 format!(
134 "mutation($id: String!, $name: String, $description: String!) {{ \
135 snapshotMachine(id: $id, name: $name, description: $description) \
136 {{ {SNAPSHOT_FIELDS} }} }}"
137 )
138}
139fn remove_snapshots_mutation() -> String {
140 format!(
141 "mutation($names: [String!]!) {{ \
142 removeSnapshots(names: $names) {{ {CMD_RESULT_FIELDS} }} }}"
143 )
144}
145fn restore_mutation() -> String {
146 format!(
147 "mutation($id: String!, $snapshot: String!, $force: Boolean!, $backup: Boolean!) {{ \
148 restoreMachine(id: $id, snapshot: $snapshot, force: $force, backup: $backup) \
149 {{ {CMD_RESULT_FIELDS} }} }}"
150 )
151}
152fn rollback_mutation() -> String {
153 format!(
154 "mutation($id: String!, $force: Boolean!, $backup: Boolean!) {{ \
155 rollbackMachine(id: $id, force: $force, backup: $backup) {{ {CMD_RESULT_FIELDS} }} }}"
156 )
157}
158const BRANCH_MUTATION: &str = "mutation($input: BranchInput!) { branchSnapshot(input: $input) }";
159
160const RUN_LINUX_MUTATION: &str = "mutation($input: RunLinuxInput!) { runLinux(input: $input) }";
161const RUN_BSD_MUTATION: &str = "mutation($input: RunBsdInput!) { runBsd(input: $input) }";
162const RUN_NANOS_MUTATION: &str = "mutation($input: RunNanosInput!) { runNanos(input: $input) }";
163const RUN_UNIKRAFT_MUTATION: &str =
164 "mutation($input: RunUnikraftInput!) { runUnikraft(input: $input) }";
165const RUN_SOLO5_MUTATION: &str = "mutation($input: RunSolo5Input!) { runSolo5(input: $input) }";
166const RUN_OSV_MUTATION: &str = "mutation($input: RunOsvInput!) { runOsv(input: $input) }";
167const RUN_FLAVOR_MUTATION: &str = "mutation($input: RunFlavorInput!) { runFlavor(input: $input) }";
168
169const MACHINE_LOGS_SUBSCRIPTION: &str =
170 "subscription($id: String!, $follow: Boolean!, $boot: Boolean!) { \
171 machineLogs(id: $id, follow: $follow, boot: $boot) { dataBase64 exitCode } }";
172
173fn open_shell_mutation() -> String {
174 format!(
175 "mutation($machineId: String!, $command: [String!]!, $env: [String!]!, \
176 $rows: Int!, $cols: Int!) {{ \
177 openShell(machineId: $machineId, command: $command, env: $env, \
178 rows: $rows, cols: $cols) {{ {SESSION_FIELDS} }} }}"
179 )
180}
181const SHELL_OUTPUT_SUBSCRIPTION: &str = "subscription($sessionId: String!) { \
182 shellOutput(sessionId: $sessionId) { dataBase64 exitCode } }";
183const SEND_INPUT_MUTATION: &str = "mutation($sessionId: String!, $dataBase64: String!) { \
184 sendShellInput(sessionId: $sessionId, dataBase64: $dataBase64) }";
185const RESIZE_MUTATION: &str = "mutation($sessionId: String!, $rows: Int!, $cols: Int!) { \
186 resizeShell(sessionId: $sessionId, rows: $rows, cols: $cols) }";
187const CLOSE_MUTATION: &str = "mutation($sessionId: String!) { closeShell(sessionId: $sessionId) }";
188
189struct ClientInner {
194 url: String,
195 token: String,
196 ws: Mutex<Option<Arc<WsTransport>>>,
200}
201
202#[derive(Clone)]
209pub struct Client {
210 inner: Arc<ClientInner>,
211}
212
213impl std::fmt::Debug for Client {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 f.debug_struct("Client")
216 .field("url", &self.inner.url)
217 .finish()
218 }
219}
220
221impl Client {
222 pub fn new(url: impl Into<String>, token: impl Into<String>) -> Result<Client> {
227 let url = normalize_url(&url.into());
228 if url.is_empty() {
229 return Err(Error::InvalidInput("the daemon URL is empty".into()));
230 }
231 let token = token.into().trim().to_string();
232 if token.is_empty() {
233 return Err(Error::InvalidInput(
234 "a daemon URL without a token is a configuration error; pass the bearer token"
235 .into(),
236 ));
237 }
238 Ok(Client {
239 inner: Arc::new(ClientInner {
240 url,
241 token,
242 ws: Mutex::new(None),
243 }),
244 })
245 }
246
247 pub fn from_env() -> Result<Client> {
254 let url = std::env::var(URL_ENV)
255 .unwrap_or_default()
256 .trim()
257 .to_string();
258 if url.is_empty() {
259 return Err(Error::InvalidInput(format!(
260 "{URL_ENV} is not set; nothing to connect to"
261 )));
262 }
263 let token = std::env::var(TOKEN_ENV)
264 .unwrap_or_default()
265 .trim()
266 .to_string();
267 if token.is_empty() {
268 return Err(Error::InvalidInput(format!(
269 "{URL_ENV} is set but {TOKEN_ENV} is not"
270 )));
271 }
272 Client::new(url, token)
273 }
274
275 pub fn url(&self) -> &str {
277 &self.inner.url
278 }
279
280 pub fn request(&self, query: &str, variables: Value) -> Result<Value> {
284 http_request(&self.inner.url, &self.inner.token, query, &variables)
285 }
286
287 pub fn subscribe(
290 &self,
291 query: &str,
292 variables: Value,
293 on_next: impl FnMut(Value) + Send + 'static,
294 ) -> Result<Subscription> {
295 self.subscribe_with(query, variables, on_next, |_| {}, || {})
296 }
297
298 pub fn subscribe_with(
300 &self,
301 query: &str,
302 variables: Value,
303 on_next: impl FnMut(Value) + Send + 'static,
304 on_error: impl FnMut(Error) + Send + 'static,
305 on_complete: impl FnMut() + Send + 'static,
306 ) -> Result<Subscription> {
307 let transport = self.ws();
308 let id = transport.subscribe(
309 query,
310 variables,
311 Box::new(on_next),
312 Box::new(on_error),
313 Box::new(on_complete),
314 )?;
315 Ok(Subscription { transport, id })
316 }
317
318 fn ws(&self) -> Arc<WsTransport> {
319 let mut guard = self.inner.ws.lock().unwrap();
320 guard
321 .get_or_insert_with(|| {
322 Arc::new(WsTransport::new(
323 ws_url(&self.inner.url),
324 self.inner.token.clone(),
325 ))
326 })
327 .clone()
328 }
329
330 pub fn list(&self, all: bool) -> Result<Vec<SandboxInfo>> {
334 let data = self.request(&list_query(), json!({"all": all}))?;
335 Ok(data
336 .get("machines")
337 .and_then(Value::as_array)
338 .map(|machines| machines.iter().map(SandboxInfo::from_graphql).collect())
339 .unwrap_or_default())
340 }
341
342 pub fn get(&self, id: &str) -> Result<Option<SandboxInfo>> {
344 let data = self.request(&get_query(), json!({"id": id}))?;
345 Ok(data
346 .get("machine")
347 .filter(|m| !m.is_null())
348 .map(SandboxInfo::from_graphql))
349 }
350
351 pub fn stop(&self, id: &str) -> Result<CommandResult> {
352 let data = self.request(&stop_mutation(), json!({"id": id}))?;
353 Ok(CommandResult::from_graphql(&data["stopMachine"]))
354 }
355
356 pub fn start(&self, id: &str) -> Result<CommandResult> {
357 let data = self.request(&start_mutation(), json!({"id": id}))?;
358 Ok(CommandResult::from_graphql(&data["startMachine"]))
359 }
360
361 pub fn remove<S: AsRef<str>>(&self, ids: &[S], force: bool) -> Result<CommandResult> {
362 let ids: Vec<&str> = ids.iter().map(AsRef::as_ref).collect();
363 let data = self.request(&remove_mutation(), json!({"ids": ids, "force": force}))?;
364 Ok(CommandResult::from_graphql(&data["removeMachines"]))
365 }
366
367 pub fn update(&self, id: &str, cpus: Option<u32>, mem: Option<u32>) -> Result<CommandResult> {
369 let data = self.request(
370 &update_mutation(),
371 json!({"id": id, "cpus": cpus, "mem": mem}),
372 )?;
373 Ok(CommandResult::from_graphql(&data["updateMachine"]))
374 }
375
376 pub fn commit(&self, id: &str, name: &str, description: &str) -> Result<CommandResult> {
378 let data = self.request(
379 &commit_mutation(),
380 json!({"id": id, "name": name, "description": description}),
381 )?;
382 Ok(CommandResult::from_graphql(&data["commitMachine"]))
383 }
384
385 pub fn ai_agents(&self) -> Result<Vec<AiAgent>> {
392 let data = self.request(&ai_agents_query(), json!({}))?;
393 Ok(data
394 .get("aiAgents")
395 .and_then(Value::as_array)
396 .map(|rows| rows.iter().map(AiAgent::from_graphql).collect())
397 .unwrap_or_default())
398 }
399
400 pub fn ai_sessions(&self) -> Result<Vec<AiSession>> {
402 let data = self.request(&ai_sessions_query(), json!({}))?;
403 Ok(data
404 .get("aiSessions")
405 .and_then(Value::as_array)
406 .map(|rows| rows.iter().map(AiSession::from_graphql).collect())
407 .unwrap_or_default())
408 }
409
410 pub fn ai_start(&self, agent: impl Into<String>) -> AiStartBuilder {
412 AiStartBuilder {
413 client: self.clone(),
414 agent: agent.into(),
415 cpus: None,
416 mem: None,
417 workspace: None,
418 new: false,
419 }
420 }
421
422 pub fn ai_shell_command(&self, agent: &str, machine_id: &str) -> Result<Vec<String>> {
424 let data = self.request(
425 AI_SHELL_COMMAND_QUERY,
426 json!({"agent": agent, "machineId": machine_id}),
427 )?;
428 Ok(data
429 .get("aiShellCommand")
430 .and_then(Value::as_array)
431 .map(|xs| {
432 xs.iter()
433 .filter_map(Value::as_str)
434 .map(str::to_string)
435 .collect()
436 })
437 .unwrap_or_default())
438 }
439
440 pub fn ai_stop(&self, agent: &str) -> Result<CommandResult> {
442 let data = self.request(&ai_stop_mutation(), json!({ "agent": agent }))?;
443 Ok(CommandResult::from_graphql(&data["aiStop"]))
444 }
445
446 pub fn ai_remove(&self, agent: &str, keep_home: bool) -> Result<CommandResult> {
448 let data = self.request(
449 &ai_remove_mutation(),
450 json!({"agent": agent, "keepHome": keep_home}),
451 )?;
452 Ok(CommandResult::from_graphql(&data["aiRemove"]))
453 }
454
455 pub fn docker_status(&self) -> Result<DockerStatus> {
462 let data = self.request(&docker_status_query(), json!({}))?;
463 Ok(DockerStatus::from_graphql(&data["dockerStatus"]))
464 }
465
466 pub fn docker_containers(&self, all: bool) -> Result<Vec<DockerContainer>> {
468 let data = self.request(&docker_containers_query(), json!({ "all": all }))?;
469 Ok(data
470 .get("dockerContainers")
471 .and_then(Value::as_array)
472 .map(|rows| rows.iter().map(DockerContainer::from_graphql).collect())
473 .unwrap_or_default())
474 }
475
476 pub fn docker_start(&self) -> DockerStartBuilder {
481 DockerStartBuilder {
482 client: self.clone(),
483 cpus: None,
484 mem: None,
485 mounts: Vec::new(),
486 no_home: false,
487 publish_bind: None,
488 disk_size: None,
489 }
490 }
491
492 pub fn docker_stop(&self) -> Result<CommandResult> {
494 let data = self.request(&docker_stop_mutation(), json!({}))?;
495 Ok(CommandResult::from_graphql(&data["dockerStop"]))
496 }
497
498 pub fn docker_container<S: AsRef<str>>(
500 &self,
501 action: &str,
502 ids: &[S],
503 ) -> Result<CommandResult> {
504 let ids: Vec<&str> = ids.iter().map(AsRef::as_ref).collect();
505 let data = self.request(
506 &docker_container_mutation(),
507 json!({"action": action, "ids": ids}),
508 )?;
509 Ok(CommandResult::from_graphql(&data["dockerContainer"]))
510 }
511
512 pub fn docker_logs(&self, id: &str, tail: u32) -> Result<String> {
514 let data = self.request(DOCKER_LOGS_QUERY, json!({"id": id, "tail": tail}))?;
515 Ok(data
516 .get("dockerContainerLogs")
517 .and_then(Value::as_str)
518 .unwrap_or_default()
519 .to_string())
520 }
521
522 pub fn snapshots(&self, machine: Option<&str>) -> Result<Vec<SnapshotInfo>> {
530 let data = self.request(&snapshots_query(), json!({ "machine": machine }))?;
531 Ok(data
532 .get("snapshots")
533 .and_then(Value::as_array)
534 .map(|rows| rows.iter().map(SnapshotInfo::from_graphql).collect())
535 .unwrap_or_default())
536 }
537
538 pub fn snapshot(
544 &self,
545 id: &str,
546 name: Option<&str>,
547 description: &str,
548 ) -> Result<SnapshotInfo> {
549 let data = self.request(
550 &snapshot_mutation(),
551 json!({"id": id, "name": name, "description": description}),
552 )?;
553 Ok(SnapshotInfo::from_graphql(&data["snapshotMachine"]))
554 }
555
556 pub fn remove_snapshots<S: AsRef<str>>(&self, names: &[S]) -> Result<CommandResult> {
558 let names: Vec<&str> = names.iter().map(AsRef::as_ref).collect();
559 let data = self.request(&remove_snapshots_mutation(), json!({ "names": names }))?;
560 Ok(CommandResult::from_graphql(&data["removeSnapshots"]))
561 }
562
563 pub fn restore(
569 &self,
570 id: &str,
571 snapshot: &str,
572 force: bool,
573 backup: bool,
574 ) -> Result<CommandResult> {
575 let data = self.request(
576 &restore_mutation(),
577 json!({"id": id, "snapshot": snapshot, "force": force, "backup": backup}),
578 )?;
579 Ok(CommandResult::from_graphql(&data["restoreMachine"]))
580 }
581
582 pub fn rollback(&self, id: &str, force: bool, backup: bool) -> Result<CommandResult> {
584 let data = self.request(
585 &rollback_mutation(),
586 json!({"id": id, "force": force, "backup": backup}),
587 )?;
588 Ok(CommandResult::from_graphql(&data["rollbackMachine"]))
589 }
590
591 pub fn branch(&self, snapshot: impl Into<String>) -> BranchBuilder {
596 BranchBuilder {
597 client: self.clone(),
598 snapshot: snapshot.into(),
599 name: None,
600 cpus: None,
601 mem: None,
602 ports: Vec::new(),
603 no_ports: false,
604 }
605 }
606
607 pub fn logs(&self, id: &str, boot: bool) -> Result<String> {
610 let data = self.request(LOGS_QUERY, json!({"id": id, "boot": boot}))?;
611 Ok(data
612 .get("machineLogs")
613 .and_then(Value::as_str)
614 .unwrap_or_default()
615 .to_string())
616 }
617
618 pub fn follow_logs(&self, id: &str) -> FollowLogsBuilder {
629 FollowLogsBuilder {
630 client: self.clone(),
631 id: id.to_string(),
632 follow: true,
633 boot: false,
634 on_data: None,
635 on_error: None,
636 on_complete: None,
637 }
638 }
639
640 pub fn run_linux(&self) -> RunLinuxBuilder {
644 RunLinuxBuilder {
645 client: self.clone(),
646 image: None,
647 cpus: None,
648 mem: None,
649 net: NetOpts::default(),
650 volume: None,
651 mounts: Vec::new(),
652 attach_disk: Vec::new(),
653 env: Vec::new(),
654 entrypoint: None,
655 initramfs: false,
656 kernel: None,
657 kernel_version: None,
658 console: None,
659 repo: None,
660 command: Vec::new(),
661 }
662 }
663
664 pub fn run_bsd(&self, os: BsdOs) -> RunBsdBuilder {
666 RunBsdBuilder {
667 client: self.clone(),
668 os,
669 version: None,
670 cpus: None,
671 mem: None,
672 net: NetOpts::default(),
673 volume: None,
674 persist: false,
675 force: false,
676 firmware: None,
677 attach_disk: Vec::new(),
678 disk_size: None,
679 repo: None,
680 command: Vec::new(),
681 }
682 }
683
684 pub fn run_nanos(&self) -> RunNanosBuilder {
686 RunNanosBuilder {
687 client: self.clone(),
688 image: None,
689 cpus: None,
690 mem: None,
691 net: NetOpts::default(),
692 kernel: None,
693 cmdline: None,
694 persist: false,
695 }
696 }
697
698 pub fn run_unikraft(&self) -> RunUnikraftBuilder {
700 RunUnikraftBuilder {
701 client: self.clone(),
702 path: None,
703 cpus: None,
704 mem: None,
705 net: NetOpts::default(),
706 cmdline: None,
707 initramfs: None,
708 mounts: Vec::new(),
709 }
710 }
711
712 pub fn run_solo5(&self) -> RunSolo5Builder {
717 RunSolo5Builder {
718 client: self.clone(),
719 path: None,
720 cpus: None,
721 mem: None,
722 net: NetOpts::default(),
723 block: Vec::new(),
724 args: Vec::new(),
725 }
726 }
727
728 pub fn run_osv(&self) -> RunOsvBuilder {
730 RunOsvBuilder {
731 client: self.clone(),
732 image: None,
733 cpus: None,
734 mem: None,
735 net: NetOpts::default(),
736 cmdline: None,
737 disk: None,
738 no_disk: false,
739 attach_disk: Vec::new(),
740 gic: None,
741 persist: false,
742 volume: None,
743 }
744 }
745
746 pub fn run_flavor(&self, name: impl Into<String>) -> RunFlavorBuilder {
748 RunFlavorBuilder {
749 client: self.clone(),
750 name: name.into(),
751 cpus: None,
752 mem: None,
753 ports: Vec::new(),
754 volume: None,
755 repo: None,
756 }
757 }
758
759 pub fn exec<I, S>(&self, id: &str, command: I) -> Result<RemoteExecResult>
763 where
764 I: IntoIterator<Item = S>,
765 S: Into<String>,
766 {
767 self.exec_with_env(id, command, Vec::<String>::new())
768 }
769
770 pub fn exec_with_env<I, S, E, T>(
781 &self,
782 id: &str,
783 command: I,
784 env: E,
785 ) -> Result<RemoteExecResult>
786 where
787 I: IntoIterator<Item = S>,
788 S: Into<String>,
789 E: IntoIterator<Item = T>,
790 T: Into<String>,
791 {
792 let transport = self.ws();
793 let data = self.request(
794 &open_shell_mutation(),
795 json!({
796 "machineId": id,
797 "command": strvec(command),
798 "env": strvec(env),
799 "rows": 24,
800 "cols": 80,
801 }),
802 )?;
803 let session = ShellSessionInfo::from_graphql(&data["openShell"]);
804
805 let chunks = Arc::new(Mutex::new(Vec::<u8>::new()));
806 let (done_tx, done_rx) = mpsc::channel::<Result<i32>>();
811
812 let chunk_sink = Arc::clone(&chunks);
813 let exit_tx = done_tx.clone();
814 let error_tx = done_tx.clone();
815 let complete_tx = done_tx;
816
817 let outcome: Result<i32> = (|| {
818 let sub_id = transport.subscribe(
819 SHELL_OUTPUT_SUBSCRIPTION,
820 json!({"sessionId": session.id}),
821 Box::new(move |data: Value| {
822 let payload = &data["shellOutput"];
823 if let Some(b64) = payload["dataBase64"].as_str() {
824 if let Ok(bytes) = B64.decode(b64) {
825 chunk_sink.lock().unwrap().extend_from_slice(&bytes);
826 }
827 }
828 if let Some(code) = payload["exitCode"].as_i64() {
829 let _ = exit_tx.send(Ok(code as i32));
830 }
831 }),
832 Box::new(move |err: Error| {
833 let _ = error_tx.send(Err(err));
834 }),
835 Box::new(move || {
836 let _ = complete_tx.send(Err(Error::GraphQL {
840 message: "shell session ended before an exit code arrived".to_string(),
841 code: None,
842 }));
843 }),
844 )?;
845 let outcome = done_rx.recv().unwrap_or_else(|_| {
846 Err(Error::GraphQL {
847 message: "the shell output subscription was dropped".to_string(),
848 code: None,
849 })
850 });
851 transport.unsubscribe(&sub_id);
852 outcome
853 })();
854
855 let _ = self.request(CLOSE_MUTATION, json!({"sessionId": session.id}));
858
859 let exit_code = outcome?;
860 let output = chunks.lock().unwrap().clone();
861 Ok(RemoteExecResult { exit_code, output })
862 }
863
864 pub fn shell(&self, id: &str) -> ShellBuilder {
875 ShellBuilder {
876 client: self.clone(),
877 machine_id: id.to_string(),
878 command: Vec::new(),
879 env: Vec::new(),
880 rows: 24,
881 cols: 80,
882 }
883 }
884}
885
886pub struct Subscription {
890 transport: Arc<WsTransport>,
891 id: String,
892}
893
894impl Subscription {
895 pub fn id(&self) -> &str {
897 &self.id
898 }
899
900 pub fn unsubscribe(self) {
902 self.transport.unsubscribe(&self.id);
903 }
904}
905
906type DataFn = Box<dyn FnMut(Vec<u8>) + Send>;
911type ErrFn = Box<dyn FnMut(Error) + Send>;
912type DoneFn = Box<dyn FnMut() + Send>;
913
914pub struct FollowLogsBuilder {
916 client: Client,
917 id: String,
918 follow: bool,
919 boot: bool,
920 on_data: Option<DataFn>,
921 on_error: Option<ErrFn>,
922 on_complete: Option<DoneFn>,
923}
924
925impl FollowLogsBuilder {
926 pub fn follow(mut self, follow: bool) -> Self {
928 self.follow = follow;
929 self
930 }
931
932 pub fn boot(mut self, boot: bool) -> Self {
934 self.boot = boot;
935 self
936 }
937
938 pub fn on_data(mut self, cb: impl FnMut(Vec<u8>) + Send + 'static) -> Self {
940 self.on_data = Some(Box::new(cb));
941 self
942 }
943
944 pub fn on_error(mut self, cb: impl FnMut(Error) + Send + 'static) -> Self {
946 self.on_error = Some(Box::new(cb));
947 self
948 }
949
950 pub fn on_complete(mut self, cb: impl FnMut() + Send + 'static) -> Self {
952 self.on_complete = Some(Box::new(cb));
953 self
954 }
955
956 pub fn start(self) -> Result<Subscription> {
958 let mut on_data = self.on_data.unwrap_or_else(|| Box::new(|_| {}));
959 let on_error = self.on_error.unwrap_or_else(|| Box::new(|_| {}));
960 let on_complete = self.on_complete.unwrap_or_else(|| Box::new(|| {}));
961 let transport = self.client.ws();
962 let id = transport.subscribe(
963 MACHINE_LOGS_SUBSCRIPTION,
964 json!({"id": self.id, "follow": self.follow, "boot": self.boot}),
965 Box::new(move |data: Value| {
966 if let Some(b64) = data
967 .pointer("/machineLogs/dataBase64")
968 .and_then(Value::as_str)
969 {
970 if let Ok(bytes) = B64.decode(b64) {
971 on_data(bytes);
972 }
973 }
974 }),
978 on_error,
979 on_complete,
980 )?;
981 Ok(Subscription { transport, id })
982 }
983}
984
985#[derive(Debug, Clone, Copy, PartialEq, Eq)]
991pub enum BsdOs {
992 Freebsd,
993 Netbsd,
994}
995
996impl BsdOs {
997 fn graphql(self) -> &'static str {
998 match self {
999 BsdOs::Freebsd => "FREEBSD",
1000 BsdOs::Netbsd => "NETBSD",
1001 }
1002 }
1003}
1004
1005fn net_input(net: &NetOpts) -> Value {
1006 if !net.touched {
1007 return Value::Null;
1008 }
1009 json!({
1010 "noNet": net.no_net,
1011 "ports": net.ports,
1012 "mac": net.mac,
1013 "network": net.network,
1014 "name": net.name,
1015 })
1016}
1017
1018fn launch_mutation(client: &Client, mutation: &str, key: &str, input: Value) -> Result<String> {
1019 let data = client.request(mutation, json!({"input": input}))?;
1020 data.get(key)
1021 .and_then(Value::as_str)
1022 .map(str::to_string)
1023 .ok_or_else(|| Error::GraphQL {
1024 message: format!("the daemon's {key} response carried no machine id"),
1025 code: None,
1026 })
1027}
1028
1029macro_rules! remote_net_vm_setters {
1033 () => {
1034 pub fn cpus(mut self, cpus: u32) -> Self {
1036 self.cpus = Some(cpus);
1037 self
1038 }
1039
1040 pub fn mem(mut self, mib: u32) -> Self {
1042 self.mem = Some(mib);
1043 self
1044 }
1045
1046 pub fn port(mut self, forward: impl Into<String>) -> Self {
1048 self.net.touched = true;
1049 self.net.ports.push(forward.into());
1050 self
1051 }
1052
1053 pub fn forward(self, host: u16, guest: u16) -> Self {
1055 self.port(format!("{host}:{guest}"))
1056 }
1057
1058 pub fn mac(mut self, mac: impl Into<String>) -> Self {
1060 self.net.touched = true;
1061 self.net.mac = Some(mac.into());
1062 self
1063 }
1064
1065 pub fn network(mut self, network: impl Into<String>) -> Self {
1067 self.net.touched = true;
1068 self.net.network = Some(network.into());
1069 self
1070 }
1071
1072 pub fn name(mut self, name: impl Into<String>) -> Self {
1074 self.net.touched = true;
1075 self.net.name = Some(name.into());
1076 self
1077 }
1078
1079 pub fn no_net(mut self) -> Self {
1081 self.net.touched = true;
1082 self.net.no_net = true;
1083 self
1084 }
1085 };
1086}
1087
1088pub struct AiStartBuilder {
1090 client: Client,
1091 agent: String,
1092 cpus: Option<u32>,
1093 mem: Option<u32>,
1094 workspace: Option<String>,
1095 new: bool,
1096}
1097
1098impl AiStartBuilder {
1099 pub fn cpus(mut self, cpus: u32) -> Self {
1101 self.cpus = Some(cpus);
1102 self
1103 }
1104
1105 pub fn mem(mut self, mib: u32) -> Self {
1107 self.mem = Some(mib);
1108 self
1109 }
1110
1111 pub fn workspace(mut self, path: impl Into<String>) -> Self {
1116 self.workspace = Some(path.into());
1117 self
1118 }
1119
1120 pub fn new_session(mut self) -> Self {
1123 self.new = true;
1124 self
1125 }
1126
1127 pub fn launch(self) -> Result<String> {
1129 let data = self.client.request(
1130 AI_START_MUTATION,
1131 json!({"input": {
1132 "agent": self.agent,
1133 "cpus": self.cpus,
1134 "mem": self.mem,
1135 "workspace": self.workspace,
1136 "new": self.new,
1137 }}),
1138 )?;
1139 Ok(data
1140 .get("aiStart")
1141 .and_then(Value::as_str)
1142 .unwrap_or_default()
1143 .to_string())
1144 }
1145}
1146
1147pub struct DockerStartBuilder {
1149 client: Client,
1150 cpus: Option<u32>,
1151 mem: Option<u32>,
1152 mounts: Vec<String>,
1153 no_home: bool,
1154 publish_bind: Option<String>,
1155 disk_size: Option<String>,
1156}
1157
1158impl DockerStartBuilder {
1159 pub fn cpus(mut self, cpus: u32) -> Self {
1161 self.cpus = Some(cpus);
1162 self
1163 }
1164
1165 pub fn mem(mut self, mib: u32) -> Self {
1167 self.mem = Some(mib);
1168 self
1169 }
1170
1171 pub fn mount(mut self, spec: impl Into<String>) -> Self {
1174 self.mounts.push(spec.into());
1175 self
1176 }
1177
1178 pub fn no_home(mut self) -> Self {
1180 self.no_home = true;
1181 self
1182 }
1183
1184 pub fn publish_bind(mut self, bind: impl Into<String>) -> Self {
1187 self.publish_bind = Some(bind.into());
1188 self
1189 }
1190
1191 pub fn disk_size(mut self, size: impl Into<String>) -> Self {
1194 self.disk_size = Some(size.into());
1195 self
1196 }
1197
1198 pub fn launch(self) -> Result<DockerStatus> {
1200 let data = self.client.request(
1201 &docker_start_mutation(),
1202 json!({"input": {
1203 "cpus": self.cpus,
1204 "mem": self.mem,
1205 "mounts": self.mounts,
1206 "noHome": self.no_home,
1207 "publishBind": self.publish_bind,
1208 "diskSize": self.disk_size,
1209 }}),
1210 )?;
1211 Ok(DockerStatus::from_graphql(&data["dockerStart"]))
1212 }
1213}
1214
1215pub struct BranchBuilder {
1221 client: Client,
1222 snapshot: String,
1223 name: Option<String>,
1224 cpus: Option<u32>,
1225 mem: Option<u32>,
1226 ports: Vec<String>,
1227 no_ports: bool,
1228}
1229
1230impl BranchBuilder {
1231 pub fn name(mut self, name: impl Into<String>) -> Self {
1233 self.name = Some(name.into());
1234 self
1235 }
1236
1237 pub fn cpus(mut self, cpus: u32) -> Self {
1239 self.cpus = Some(cpus);
1240 self
1241 }
1242
1243 pub fn mem(mut self, mib: u32) -> Self {
1245 self.mem = Some(mib);
1246 self
1247 }
1248
1249 pub fn port(mut self, forward: impl Into<String>) -> Self {
1252 self.ports.push(forward.into());
1253 self
1254 }
1255
1256 pub fn forward(self, host: u16, guest: u16) -> Self {
1258 self.port(format!("{host}:{guest}"))
1259 }
1260
1261 pub fn no_ports(mut self) -> Self {
1263 self.no_ports = true;
1264 self
1265 }
1266
1267 pub fn launch(self) -> Result<String> {
1273 let input = json!({
1274 "snapshot": self.snapshot,
1275 "name": self.name,
1276 "cpus": self.cpus,
1277 "mem": self.mem,
1278 "ports": self.ports,
1279 "noPorts": self.no_ports,
1280 });
1281 launch_mutation(&self.client, BRANCH_MUTATION, "branchSnapshot", input)
1282 }
1283}
1284
1285pub struct RunLinuxBuilder {
1287 client: Client,
1288 image: Option<String>,
1289 cpus: Option<u32>,
1290 mem: Option<u32>,
1291 net: NetOpts,
1292 volume: Option<String>,
1293 mounts: Vec<String>,
1294 attach_disk: Vec<String>,
1295 env: Vec<String>,
1296 entrypoint: Option<String>,
1297 initramfs: bool,
1298 kernel: Option<String>,
1299 kernel_version: Option<String>,
1300 console: Option<String>,
1301 repo: Option<String>,
1302 command: Vec<String>,
1303}
1304
1305impl RunLinuxBuilder {
1306 remote_net_vm_setters!();
1307
1308 pub fn image(mut self, image: impl Into<String>) -> Self {
1310 self.image = Some(image.into());
1311 self
1312 }
1313
1314 pub fn volume(mut self, name: impl Into<String>) -> Self {
1316 self.volume = Some(name.into());
1317 self
1318 }
1319
1320 pub fn mount(mut self, mount: impl Into<String>) -> Self {
1322 self.mounts.push(mount.into());
1323 self
1324 }
1325
1326 pub fn attach_disk(mut self, disk: impl Into<String>) -> Self {
1329 self.attach_disk.push(disk.into());
1330 self
1331 }
1332
1333 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1335 self.env.push(format!("{}={}", key.into(), value.into()));
1336 self
1337 }
1338
1339 pub fn entrypoint(mut self, entrypoint: impl Into<String>) -> Self {
1341 self.entrypoint = Some(entrypoint.into());
1342 self
1343 }
1344
1345 pub fn initramfs(mut self) -> Self {
1347 self.initramfs = true;
1348 self
1349 }
1350
1351 pub fn kernel(mut self, kernel: impl Into<String>) -> Self {
1353 self.kernel = Some(kernel.into());
1354 self
1355 }
1356
1357 pub fn kernel_version(mut self, version: impl Into<String>) -> Self {
1359 self.kernel_version = Some(version.into());
1360 self
1361 }
1362
1363 pub fn console(mut self, console: impl Into<String>) -> Self {
1365 self.console = Some(console.into());
1366 self
1367 }
1368
1369 pub fn repo(mut self, repo: impl Into<String>) -> Self {
1371 self.repo = Some(repo.into());
1372 self
1373 }
1374
1375 pub fn command<I, S>(mut self, command: I) -> Self
1377 where
1378 I: IntoIterator<Item = S>,
1379 S: Into<String>,
1380 {
1381 self.command = strvec(command);
1382 self
1383 }
1384
1385 pub fn launch(self) -> Result<String> {
1387 let Some(image) = self.image else {
1388 return Err(Error::InvalidInput("run_linux requires an image".into()));
1389 };
1390 let input = json!({
1391 "image": image,
1392 "cpus": self.cpus,
1393 "mem": self.mem,
1394 "net": net_input(&self.net),
1395 "volume": self.volume,
1396 "mounts": self.mounts,
1397 "attachDisk": self.attach_disk,
1398 "env": self.env,
1399 "entrypoint": self.entrypoint,
1400 "initramfs": self.initramfs,
1401 "kernel": self.kernel,
1402 "kernelVersion": self.kernel_version,
1403 "console": self.console,
1404 "repo": self.repo,
1405 "command": self.command,
1406 });
1407 launch_mutation(&self.client, RUN_LINUX_MUTATION, "runLinux", input)
1408 }
1409}
1410
1411pub struct RunBsdBuilder {
1413 client: Client,
1414 os: BsdOs,
1415 version: Option<String>,
1416 cpus: Option<u32>,
1417 mem: Option<u32>,
1418 net: NetOpts,
1419 volume: Option<String>,
1420 persist: bool,
1421 force: bool,
1422 firmware: Option<String>,
1423 attach_disk: Vec<String>,
1424 disk_size: Option<String>,
1425 repo: Option<String>,
1426 command: Vec<String>,
1427}
1428
1429impl RunBsdBuilder {
1430 remote_net_vm_setters!();
1431
1432 pub fn version(mut self, version: impl Into<String>) -> Self {
1434 self.version = Some(version.into());
1435 self
1436 }
1437
1438 pub fn volume(mut self, name: impl Into<String>) -> Self {
1440 self.volume = Some(name.into());
1441 self
1442 }
1443
1444 pub fn persist(mut self) -> Self {
1446 self.persist = true;
1447 self
1448 }
1449
1450 pub fn force(mut self) -> Self {
1452 self.force = true;
1453 self
1454 }
1455
1456 pub fn firmware(mut self, firmware: impl Into<String>) -> Self {
1458 self.firmware = Some(firmware.into());
1459 self
1460 }
1461
1462 pub fn attach_disk(mut self, disk: impl Into<String>) -> Self {
1464 self.attach_disk.push(disk.into());
1465 self
1466 }
1467
1468 pub fn disk_size(mut self, size: impl Into<String>) -> Self {
1470 self.disk_size = Some(size.into());
1471 self
1472 }
1473
1474 pub fn repo(mut self, repo: impl Into<String>) -> Self {
1476 self.repo = Some(repo.into());
1477 self
1478 }
1479
1480 pub fn command<I, S>(mut self, command: I) -> Self
1482 where
1483 I: IntoIterator<Item = S>,
1484 S: Into<String>,
1485 {
1486 self.command = strvec(command);
1487 self
1488 }
1489
1490 pub fn launch(self) -> Result<String> {
1492 let input = json!({
1493 "os": self.os.graphql(),
1494 "version": self.version,
1495 "cpus": self.cpus,
1496 "mem": self.mem,
1497 "net": net_input(&self.net),
1498 "volume": self.volume,
1499 "persist": self.persist,
1500 "force": self.force,
1501 "firmware": self.firmware,
1502 "attachDisk": self.attach_disk,
1503 "diskSize": self.disk_size,
1504 "repo": self.repo,
1505 "command": self.command,
1506 });
1507 launch_mutation(&self.client, RUN_BSD_MUTATION, "runBsd", input)
1508 }
1509}
1510
1511pub struct RunNanosBuilder {
1513 client: Client,
1514 image: Option<String>,
1515 cpus: Option<u32>,
1516 mem: Option<u32>,
1517 net: NetOpts,
1518 kernel: Option<String>,
1519 cmdline: Option<String>,
1520 persist: bool,
1521}
1522
1523impl RunNanosBuilder {
1524 remote_net_vm_setters!();
1525
1526 pub fn image(mut self, image: impl Into<String>) -> Self {
1528 self.image = Some(image.into());
1529 self
1530 }
1531
1532 pub fn kernel(mut self, kernel: impl Into<String>) -> Self {
1534 self.kernel = Some(kernel.into());
1535 self
1536 }
1537
1538 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
1540 self.cmdline = Some(cmdline.into());
1541 self
1542 }
1543
1544 pub fn persist(mut self) -> Self {
1546 self.persist = true;
1547 self
1548 }
1549
1550 pub fn launch(self) -> Result<String> {
1552 let Some(image) = self.image else {
1553 return Err(Error::InvalidInput("run_nanos requires an image".into()));
1554 };
1555 let input = json!({
1556 "image": image,
1557 "cpus": self.cpus,
1558 "mem": self.mem,
1559 "net": net_input(&self.net),
1560 "kernel": self.kernel,
1561 "cmdline": self.cmdline,
1562 "persist": self.persist,
1563 });
1564 launch_mutation(&self.client, RUN_NANOS_MUTATION, "runNanos", input)
1565 }
1566}
1567
1568pub struct RunUnikraftBuilder {
1570 client: Client,
1571 path: Option<String>,
1572 cpus: Option<u32>,
1573 mem: Option<u32>,
1574 net: NetOpts,
1575 cmdline: Option<String>,
1576 initramfs: Option<String>,
1577 mounts: Vec<String>,
1578}
1579
1580impl RunUnikraftBuilder {
1581 remote_net_vm_setters!();
1582
1583 pub fn path(mut self, path: impl Into<String>) -> Self {
1585 self.path = Some(path.into());
1586 self
1587 }
1588
1589 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
1591 self.cmdline = Some(cmdline.into());
1592 self
1593 }
1594
1595 pub fn initramfs(mut self, path: impl Into<String>) -> Self {
1597 self.initramfs = Some(path.into());
1598 self
1599 }
1600
1601 pub fn mount(mut self, mount: impl Into<String>) -> Self {
1604 self.mounts.push(mount.into());
1605 self
1606 }
1607
1608 pub fn launch(self) -> Result<String> {
1610 let input = json!({
1611 "path": self.path,
1612 "cpus": self.cpus,
1613 "mem": self.mem,
1614 "net": net_input(&self.net),
1615 "cmdline": self.cmdline,
1616 "initramfs": self.initramfs,
1617 "mounts": self.mounts,
1618 });
1619 launch_mutation(&self.client, RUN_UNIKRAFT_MUTATION, "runUnikraft", input)
1620 }
1621}
1622
1623pub struct RunSolo5Builder {
1625 client: Client,
1626 path: Option<String>,
1627 cpus: Option<u32>,
1628 mem: Option<u32>,
1629 net: NetOpts,
1630 block: Vec<String>,
1631 args: Vec<String>,
1632}
1633
1634impl RunSolo5Builder {
1635 remote_net_vm_setters!();
1636
1637 pub fn path(mut self, path: impl Into<String>) -> Self {
1640 self.path = Some(path.into());
1641 self
1642 }
1643
1644 pub fn block(mut self, block: impl Into<String>) -> Self {
1646 self.block.push(block.into());
1647 self
1648 }
1649
1650 pub fn args<I, S>(mut self, args: I) -> Self
1652 where
1653 I: IntoIterator<Item = S>,
1654 S: Into<String>,
1655 {
1656 self.args = strvec(args);
1657 self
1658 }
1659
1660 pub fn launch(self) -> Result<String> {
1662 let input = json!({
1663 "path": self.path,
1664 "cpus": self.cpus,
1665 "mem": self.mem,
1666 "net": net_input(&self.net),
1667 "block": self.block,
1668 "args": self.args,
1669 });
1670 launch_mutation(&self.client, RUN_SOLO5_MUTATION, "runSolo5", input)
1671 }
1672}
1673
1674pub struct RunOsvBuilder {
1676 client: Client,
1677 image: Option<String>,
1678 cpus: Option<u32>,
1679 mem: Option<u32>,
1680 net: NetOpts,
1681 cmdline: Option<String>,
1682 disk: Option<String>,
1683 no_disk: bool,
1684 attach_disk: Vec<String>,
1685 gic: Option<String>,
1686 persist: bool,
1687 volume: Option<String>,
1688}
1689
1690impl RunOsvBuilder {
1691 remote_net_vm_setters!();
1692
1693 pub fn image(mut self, image: impl Into<String>) -> Self {
1695 self.image = Some(image.into());
1696 self
1697 }
1698
1699 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
1701 self.cmdline = Some(cmdline.into());
1702 self
1703 }
1704
1705 pub fn disk(mut self, disk: impl Into<String>) -> Self {
1707 self.disk = Some(disk.into());
1708 self
1709 }
1710
1711 pub fn no_disk(mut self) -> Self {
1713 self.no_disk = true;
1714 self
1715 }
1716
1717 pub fn attach_disk(mut self, disk: impl Into<String>) -> Self {
1719 self.attach_disk.push(disk.into());
1720 self
1721 }
1722
1723 pub fn gic(mut self, gic: impl Into<String>) -> Self {
1725 self.gic = Some(gic.into());
1726 self
1727 }
1728
1729 pub fn persist(mut self) -> Self {
1731 self.persist = true;
1732 self
1733 }
1734
1735 pub fn volume(mut self, name: impl Into<String>) -> Self {
1737 self.volume = Some(name.into());
1738 self
1739 }
1740
1741 pub fn launch(self) -> Result<String> {
1743 let Some(image) = self.image else {
1744 return Err(Error::InvalidInput("run_osv requires an image".into()));
1745 };
1746 let input = json!({
1747 "image": image,
1748 "cpus": self.cpus,
1749 "mem": self.mem,
1750 "net": net_input(&self.net),
1751 "cmdline": self.cmdline,
1752 "disk": self.disk,
1753 "noDisk": self.no_disk,
1754 "attachDisk": self.attach_disk,
1755 "gic": self.gic,
1756 "persist": self.persist,
1757 "volume": self.volume,
1758 });
1759 launch_mutation(&self.client, RUN_OSV_MUTATION, "runOsv", input)
1760 }
1761}
1762
1763pub struct RunFlavorBuilder {
1765 client: Client,
1766 name: String,
1767 cpus: Option<u32>,
1768 mem: Option<u32>,
1769 ports: Vec<String>,
1770 volume: Option<String>,
1771 repo: Option<String>,
1772}
1773
1774impl RunFlavorBuilder {
1775 pub fn cpus(mut self, cpus: u32) -> Self {
1777 self.cpus = Some(cpus);
1778 self
1779 }
1780
1781 pub fn mem(mut self, mib: u32) -> Self {
1783 self.mem = Some(mib);
1784 self
1785 }
1786
1787 pub fn port(mut self, forward: impl Into<String>) -> Self {
1789 self.ports.push(forward.into());
1790 self
1791 }
1792
1793 pub fn volume(mut self, name: impl Into<String>) -> Self {
1795 self.volume = Some(name.into());
1796 self
1797 }
1798
1799 pub fn repo(mut self, repo: impl Into<String>) -> Self {
1801 self.repo = Some(repo.into());
1802 self
1803 }
1804
1805 pub fn launch(self) -> Result<String> {
1807 let input = json!({
1808 "name": self.name,
1809 "cpus": self.cpus,
1810 "mem": self.mem,
1811 "ports": self.ports,
1812 "volume": self.volume,
1813 "repo": self.repo,
1814 });
1815 launch_mutation(&self.client, RUN_FLAVOR_MUTATION, "runFlavor", input)
1816 }
1817}
1818
1819pub struct ShellBuilder {
1825 client: Client,
1826 machine_id: String,
1827 command: Vec<String>,
1828 env: Vec<String>,
1829 rows: u32,
1830 cols: u32,
1831}
1832
1833impl ShellBuilder {
1834 pub fn command<I, S>(mut self, command: I) -> Self
1836 where
1837 I: IntoIterator<Item = S>,
1838 S: Into<String>,
1839 {
1840 self.command = strvec(command);
1841 self
1842 }
1843
1844 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1846 self.env.push(format!("{}={}", key.into(), value.into()));
1847 self
1848 }
1849
1850 pub fn rows(mut self, rows: u32) -> Self {
1852 self.rows = rows;
1853 self
1854 }
1855
1856 pub fn cols(mut self, cols: u32) -> Self {
1858 self.cols = cols;
1859 self
1860 }
1861
1862 pub fn open(self) -> Result<ShellSession> {
1864 let data = self.client.request(
1865 &open_shell_mutation(),
1866 json!({
1867 "machineId": self.machine_id,
1868 "command": self.command,
1869 "env": self.env,
1870 "rows": self.rows,
1871 "cols": self.cols,
1872 }),
1873 )?;
1874 let info = ShellSessionInfo::from_graphql(&data["openShell"]);
1875 ShellSession::start(self.client, info.id)
1876 }
1877}
1878
1879type OutputFn = Box<dyn FnMut(&[u8]) + Send>;
1880type ExitFn = Box<dyn FnMut(i32) + Send>;
1881
1882struct ShellShared {
1883 output_cb: Option<OutputFn>,
1884 exit_cb: Option<ExitFn>,
1885 buffered_output: Vec<Vec<u8>>,
1891 buffered_exit: Option<i32>,
1892 exit_fired: bool,
1893}
1894
1895pub struct ShellSession {
1903 id: String,
1904 client: Client,
1905 transport: Arc<WsTransport>,
1906 sub_id: String,
1907 shared: Arc<Mutex<ShellShared>>,
1908 closed: bool,
1909}
1910
1911impl std::fmt::Debug for ShellSession {
1912 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1913 f.debug_struct("ShellSession")
1914 .field("id", &self.id)
1915 .finish()
1916 }
1917}
1918
1919impl ShellSession {
1920 fn start(client: Client, session_id: String) -> Result<ShellSession> {
1921 let shared = Arc::new(Mutex::new(ShellShared {
1922 output_cb: None,
1923 exit_cb: None,
1924 buffered_output: Vec::new(),
1925 buffered_exit: None,
1926 exit_fired: false,
1927 }));
1928 let transport = client.ws();
1929
1930 let on_next_shared = Arc::clone(&shared);
1931 let on_error_shared = Arc::clone(&shared);
1932 let sub_id = transport.subscribe(
1933 SHELL_OUTPUT_SUBSCRIPTION,
1934 json!({"sessionId": session_id}),
1935 Box::new(move |data: Value| {
1936 let payload = &data["shellOutput"];
1937 if let Some(b64) = payload["dataBase64"].as_str() {
1938 if let Ok(bytes) = B64.decode(b64) {
1939 emit_output(&on_next_shared, bytes);
1940 }
1941 }
1942 if let Some(code) = payload["exitCode"].as_i64() {
1943 emit_exit(&on_next_shared, code as i32);
1944 }
1945 }),
1946 Box::new(move |_err: Error| {
1947 emit_exit(&on_error_shared, -1);
1952 }),
1953 Box::new(|| {}),
1954 )?;
1955
1956 Ok(ShellSession {
1957 id: session_id,
1958 client,
1959 transport,
1960 sub_id,
1961 shared,
1962 closed: false,
1963 })
1964 }
1965
1966 pub fn id(&self) -> &str {
1968 &self.id
1969 }
1970
1971 pub fn on_output(&self, mut cb: impl FnMut(&[u8]) + Send + 'static) {
1974 let mut shared = self.shared.lock().unwrap();
1975 for chunk in std::mem::take(&mut shared.buffered_output) {
1976 cb(&chunk);
1977 }
1978 shared.output_cb = Some(Box::new(cb));
1979 }
1980
1981 pub fn on_exit(&self, mut cb: impl FnMut(i32) + Send + 'static) {
1983 let mut shared = self.shared.lock().unwrap();
1984 if let Some(code) = shared.buffered_exit.take() {
1985 cb(code);
1986 }
1987 shared.exit_cb = Some(Box::new(cb));
1988 }
1989
1990 pub fn write(&self, data: impl AsRef<[u8]>) -> Result<()> {
1992 self.client.request(
1993 SEND_INPUT_MUTATION,
1994 json!({"sessionId": self.id, "dataBase64": B64.encode(data.as_ref())}),
1995 )?;
1996 Ok(())
1997 }
1998
1999 pub fn resize(&self, rows: u32, cols: u32) -> Result<()> {
2001 self.client.request(
2002 RESIZE_MUTATION,
2003 json!({"sessionId": self.id, "rows": rows, "cols": cols}),
2004 )?;
2005 Ok(())
2006 }
2007
2008 pub fn close(&mut self) {
2010 if self.closed {
2011 return;
2012 }
2013 self.closed = true;
2014 self.transport.unsubscribe(&self.sub_id);
2015 let _ = self
2017 .client
2018 .request(CLOSE_MUTATION, json!({"sessionId": self.id}));
2019 }
2020}
2021
2022fn emit_output(shared: &Arc<Mutex<ShellShared>>, data: Vec<u8>) {
2023 let mut guard = shared.lock().unwrap();
2024 match &mut guard.output_cb {
2025 Some(cb) => cb(&data),
2026 None => guard.buffered_output.push(data),
2027 }
2028}
2029
2030fn emit_exit(shared: &Arc<Mutex<ShellShared>>, code: i32) {
2031 let mut guard = shared.lock().unwrap();
2032 if guard.exit_fired {
2033 return;
2034 }
2035 guard.exit_fired = true;
2036 match &mut guard.exit_cb {
2037 Some(cb) => cb(code),
2038 None => guard.buffered_exit = Some(code),
2039 }
2040}