1 file changed
+55
-35
crates/xcrs/src/xcrs.rs
+55
-35
@@ -135,41 +135,40 @@ impl ControlKit {
135
let body = self.send(method, params).await?;
136
137
if let Some(error) = body.get("error") {
138
- let (runner_info, runner_info_error) =
139
- if error.get("code").and_then(serde_json::Value::as_i64)
140
- == Some(CONTROLKIT_METHOD_NOT_FOUND)
141
- && method != "device.info"
138
+ // `send` cannot recurse; this guard only avoids repeating an unsupported probe.
139
+ let should_probe_runner =
140
+ is_controlkit_method_not_found(error) && method != "device.info";
141
+ let (runner_info, runner_info_error) = if should_probe_runner {
142
+ match tokio::time::timeout(
143
+ CONTROLKIT_RUNNER_INFO_TIMEOUT,
144
+ self.send("device.info", serde_json::json!({})),
145
+ )
146
+ .await
147
{
143
- match tokio::time::timeout(
144
- CONTROLKIT_RUNNER_INFO_TIMEOUT,
145
- self.send("device.info", serde_json::json!({})),
146
- )
147
- .await
148
- {
149
- Ok(Ok(body)) => match body.get("result") {
150
- Some(result) => (Some(result.clone()), None),
151
- None => {
152
- let reason = body
153
- .get("error")
154
- .map(|error| format!("device.info returned {error}"))
155
- .unwrap_or_else(|| {
156
- "device.info response did not contain a result".to_string()
157
- });
158
- (None, Some(reason))
159
- }
160
- },
161
- Ok(Err(error)) => (None, Some(error.to_string())),
162
- Err(_) => (
163
- None,
164
- Some(format!(
165
- "device.info timed out after {} seconds",
166
- CONTROLKIT_RUNNER_INFO_TIMEOUT.as_secs()
167
- )),
168
- ),
169
- }
170
- } else {
171
- (None, None)
172
- };
148
+ Ok(Ok(body)) => match body.get("result") {
149
+ Some(result) => (Some(result.clone()), None),
150
+ None => {
151
+ let reason = body
152
+ .get("error")
153
+ .map(|error| format!("device.info returned {error}"))
154
+ .unwrap_or_else(|| {
155
+ "device.info response did not contain a result".to_string()
156
+ });
157
+ (None, Some(reason))
158
+ }
159
+ },
160
+ Ok(Err(error)) => (None, Some(error.to_string())),
161
+ Err(_) => (
162
+ None,
163
+ Some(format!(
164
+ "device.info timed out after {} seconds",
165
+ CONTROLKIT_RUNNER_INFO_TIMEOUT.as_secs()
166
+ )),
167
+ ),
168
+ }
169
+ } else {
170
+ (None, None)
171
+ };
172
return Err(anyhow!(controlkit_rpc_error(
173
method,
174
error,
@@ -212,6 +211,15 @@ impl ControlKit {
211
}
212
}
213
214
+fn is_controlkit_method_not_found(error: &serde_json::Value) -> bool {
215
+ let Some(code) = error.get("code") else {
216
+ return false;
217
+ };
218
+
219
+ code.as_i64() == Some(CONTROLKIT_METHOD_NOT_FOUND)
220
+ || code.as_f64() == Some(CONTROLKIT_METHOD_NOT_FOUND as f64)
221
+}
222
+
223
fn controlkit_rpc_error(
224
method: &str,
225
error: &serde_json::Value,
@@ -227,7 +235,7 @@ fn controlkit_rpc_error(
235
.and_then(serde_json::Value::as_str)
236
.unwrap_or("unknown JSON-RPC error");
237
230
- if code != CONTROLKIT_METHOD_NOT_FOUND {
238
+ if !is_controlkit_method_not_found(error) {
239
return format!("ControlKit method '{method}' failed ({code}): {message}");
240
}
241
@@ -1381,6 +1389,18 @@ mod tests {
1389
assert!(message.contains("Rebuild or upgrade"));
1390
}
1391
1392
+ #[test]
1393
+ fn recognizes_integral_float_controlkit_method_not_found_code() {
1394
+ let error = serde_json::json!({
1395
+ "code": -32601.0,
1396
+ "message": "Method not found"
1397
+ });
1398
+
1399
+ assert!(is_controlkit_method_not_found(&error));
1400
+ let message = controlkit_rpc_error("device.dump.ui", &error, None, None);
1401
+ assert!(message.contains("does not implement ControlKit method 'device.dump.ui'"));
1402
+ }
1403
+
1404
#[test]
1405
fn reports_when_controlkit_runner_metadata_is_unavailable() {
1406
let error = serde_json::json!({