Skip to main content

opencode_ctl/tools/
service.rs

1//! `service_*` tools.
2
3use 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
14/// How long a start or restart that succeeded may still read as stopped, and how often to read again.
15const 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        // Managed mode: ask the CLI first; a server.info call on a stopped service would start it.
51        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    /// The status after a start or restart that succeeded. The read right after it can still say stopped
100    /// (MEASURED on Windows, Task 25: `service_restart` answered `running: false` and a read 4 s later showed
101    /// the new pid), so read again every `SETTLE_POLL` for up to `SETTLE_MS` and answer what the last read
102    /// saw. No `server.info` runs while it reads stopped, so a dead service is never started to answer.
103    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}