opencode_ctl/tools/
service.rs1use std::time::Duration;
4
5use serde_json::{json, Value};
6
7use crate::clock::Clock;
8use crate::engine::Engine;
9use crate::error::{ErrorCode, ToolError, ToolResult};
10use crate::transport::{service_running, Lifecycle, Transport};
11
12use super::{ConfirmArgs, ServiceStartArgs, ServiceStatusArgs};
13
14const SETTLE_MS: i64 = 5_000;
16const SETTLE_POLL: Duration = Duration::from_millis(500);
17
18impl<T: Transport, C: Clock> Engine<T, C> {
19 fn refuse_lifecycle(&self, tool: &str) -> Result<(), ToolError> {
20 if self.host.refuses_lifecycle() {
21 return Err(ToolError::new(
22 ErrorCode::RefusedOnHost,
23 format!("{tool} is refused on this host: it would stop the daemon the host itself depends on"),
24 ));
25 }
26 if self.settings.server_url.is_some() {
27 return Err(ToolError::new(
28 ErrorCode::RefusedOnHost,
29 format!(
30 "server_url is set: the service is remote and not managed by this plugin, so {tool} is refused; stop or restart it where it runs"
31 ),
32 ));
33 }
34 Ok(())
35 }
36
37 fn confirm_required(&self, tool: &str, confirm: bool) -> Result<(), ToolError> {
38 if confirm {
39 Ok(())
40 } else {
41 Err(ToolError::new(
42 ErrorCode::ConfirmRequired,
43 format!("{tool} needs confirm: true"),
44 ))
45 }
46 }
47
48 pub async fn service_status(&self, _a: ServiceStatusArgs) -> ToolResult {
49 let managed = self.settings.server_url.is_none();
50 let mut running = if managed {
52 service_running(&self.lifecycle(Lifecycle::Status).await?)
53 } else {
54 true
55 };
56 let mut reachable = false;
57 let (mut urls, mut pid, mut version) = (json!([]), Value::Null, Value::Null);
58 if running {
59 match self.call_json("server.info", Vec::new(), None).await {
60 Ok((info, _, _)) => {
61 reachable = true;
62 urls = info.get("urls").cloned().unwrap_or(urls);
63 pid = info.get("pid").cloned().unwrap_or(Value::Null);
64 version = info.get("version").cloned().unwrap_or(Value::Null);
65 self.invalidate_if_changed(&info).await;
66 }
67 Err(e) if e.code == ErrorCode::ServiceDown => {
68 if !managed {
69 running = false;
70 }
71 }
72 Err(e) => return Err(e),
73 }
74 }
75 let paired = match &self.env.session {
76 Some(session) => {
77 let (tui, claude) = self.pair_liveness();
78 json!({
79 "pair_id": self.env.pair,
80 "session_id": session,
81 "directory": self.env.directory.as_ref().map(|d| d.to_string_lossy().to_string()),
82 "server_url": self.env.server,
83 "tui": tui.as_str(),
84 "claude": claude.as_str(),
85 })
86 }
87 None => Value::Null,
88 };
89 Ok(json!({
90 "running": running,
91 "urls": urls,
92 "pid": pid,
93 "version": version,
94 "reachable": reachable,
95 "paired": paired,
96 }))
97 }
98
99 async fn status_after_lifecycle(&self) -> ToolResult {
104 let deadline = self.clock().now_ms() + SETTLE_MS;
105 loop {
106 let status = self.service_status(ServiceStatusArgs {}).await?;
107 if status["running"] == json!(true) || self.clock().now_ms() >= deadline {
108 return Ok(status);
109 }
110 self.clock().sleep(SETTLE_POLL).await;
111 }
112 }
113
114 pub async fn service_start(&self, a: ServiceStartArgs) -> ToolResult {
115 let _ = a;
116 if self.settings.server_url.is_none() {
117 self.lifecycle(Lifecycle::Start).await?;
118 }
119 self.status_after_lifecycle().await
120 }
121
122 pub async fn service_stop(&self, a: ConfirmArgs) -> ToolResult {
123 self.refuse_lifecycle("service_stop")?;
124 self.confirm_required("service_stop", a.confirm)?;
125 self.lifecycle(Lifecycle::Stop).await?;
126 Ok(json!({"stopped": true}))
127 }
128
129 pub async fn service_restart(&self, a: ConfirmArgs) -> ToolResult {
130 self.refuse_lifecycle("service_restart")?;
131 self.confirm_required("service_restart", a.confirm)?;
132 self.lifecycle(Lifecycle::Restart).await?;
133 self.status_after_lifecycle().await
134 }
135}