onlyne_client/backend/
command.rs1use super::*;
2
3pub(super) fn command_error(program: &str, output: &CommandOutput) -> anyhow::Error {
4 anyhow::anyhow!(
5 "runtime command failed: {program} (status {}): {}",
6 output.status,
7 String::from_utf8_lossy(&output.stderr).trim()
8 )
9}
10
11pub(super) fn run_checked(
12 runner: &dyn Runner,
13 program: &str,
14 args: &[String],
15 cwd: Option<&Path>,
16 env: &BTreeMap<String, String>,
17) -> Result<CommandOutput> {
18 let output = runner.run(program, args, cwd, env)?;
19 if output.status != 0 {
20 return Err(command_error(program, &output));
21 }
22 Ok(output)
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct CommandFailure {
33 command: String,
34 status: i32,
35 code: Option<String>,
36 message: Option<String>,
37}
38
39impl CommandFailure {
40 pub(crate) fn new(
42 command: impl Into<String>,
43 status: i32,
44 code: Option<String>,
45 message: Option<String>,
46 ) -> Self {
47 Self {
48 command: command.into(),
49 status,
50 code,
51 message,
52 }
53 }
54
55 pub fn code(&self) -> Option<&str> {
57 self.code.as_deref()
58 }
59}
60
61impl std::fmt::Display for CommandFailure {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(
64 f,
65 "runtime command failed: {} (status {}",
66 self.command, self.status
67 )?;
68 if let Some(code) = &self.code {
69 write!(f, ", {code}")?;
70 }
71 write!(f, ")")?;
72 if let Some(message) = &self.message {
73 write!(f, ": {message}")?;
74 }
75 Ok(())
76 }
77}
78
79impl std::error::Error for CommandFailure {}
80
81pub(super) fn run_json(
89 runner: &dyn Runner,
90 program: &str,
91 args: &[String],
92 cwd: Option<&Path>,
93 env: &BTreeMap<String, String>,
94) -> Result<Value> {
95 let output = runner.run(program, args, cwd, env)?;
96 let body = serde_json::from_slice::<Value>(&output.stdout).ok();
97 let command = format!("{program} {}", args.join(" "));
98 if output.status == 0 {
99 let Some(value) = body else {
100 return Err(command_failure(&command, &output, None).into());
101 };
102 if value.get("ok").and_then(Value::as_bool) == Some(false) {
103 return Err(command_failure(&command, &output, Some(&value)).into());
104 }
105 return Ok(match value.get("result") {
106 Some(result) => result.clone(),
107 None => value,
108 });
109 }
110 Err(command_failure(&command, &output, body.as_ref()).into())
111}
112
113pub(super) fn command_failure(
119 command: &str,
120 output: &CommandOutput,
121 body: Option<&Value>,
122) -> CommandFailure {
123 let stderr_body = || -> Option<Value> {
124 let text = String::from_utf8_lossy(&output.stderr);
125 let text = text.trim();
126 if text.is_empty() {
127 return None;
128 }
129 serde_json::from_str::<Value>(text).ok()
130 };
131 let fallback = stderr_body();
132 let error = body
133 .and_then(|body| body.get("error"))
134 .or_else(|| fallback.as_ref().and_then(|value| value.get("error")));
135 let text = |key: &str| {
136 error
137 .and_then(|error| error.get(key))
138 .and_then(Value::as_str)
139 .filter(|value| !value.is_empty())
140 .map(str::to_owned)
141 };
142 let message = text("message").or_else(|| {
143 let stderr = String::from_utf8_lossy(&output.stderr);
144 let stderr = stderr.trim();
145 if stderr.is_empty() {
146 body.map(Value::to_string)
147 } else {
148 Some(stderr.to_string())
149 }
150 });
151 CommandFailure::new(command, output.status, text("code"), message)
152}
153
154pub(super) fn failure_code(error: &anyhow::Error) -> Option<&str> {
156 error
157 .downcast_ref::<CommandFailure>()
158 .and_then(CommandFailure::code)
159}
160
161pub(super) fn unsupported(backend: &str, operation: &str, detail: &str) -> anyhow::Error {
162 anyhow::anyhow!("runtime backend {backend} does not support {operation}: {detail}")
163}