code_moniker_daemon_client/
lib.rs1#![cfg(unix)]
2
3use std::future::Future;
4use std::ops::Deref;
5use std::path::{Path, PathBuf};
6use std::process::{Command as ProcessCommand, Stdio};
7use std::sync::Arc;
8use std::thread;
9use std::time::Duration;
10
11use code_moniker_query::{
12 CommandRequest, CommandResponse, DaemonRpcClient, DaemonWorkspaceConfig, HandshakeResponse,
13 QueryRequest, QueryResponse,
14};
15use jsonrpsee::ws_client::{WsClient, WsClientBuilder};
16use tokio::runtime::Runtime;
17
18use code_moniker_query::{DaemonRegistryState, list_registry_files, pid_is_alive};
19
20pub use code_moniker_query::{
21 DaemonRegistryEntry, canonical_workspace_config, canonical_workspace_root,
22 canonical_workspace_roots, config_from_roots, config_roots, daemon_workspace_config,
23 list_registry_entries, read_registry_entry, registry_dir, registry_path_for_config,
24 registry_path_for_root, registry_path_for_roots, remove_registry_entry_if_own, workspace_label,
25};
26
27#[derive(Clone)]
28pub struct DaemonClient {
29 connection: DaemonConnection,
30 endpoint: DaemonEndpoint,
31}
32
33#[derive(Clone)]
34struct DaemonEndpoint {
35 config: DaemonWorkspaceConfig,
36 roots: Vec<PathBuf>,
37 address: String,
38}
39
40#[derive(Clone)]
41pub struct DaemonConnection {
42 runtime: Arc<Runtime>,
43 ws: Arc<WsClient>,
44}
45
46impl DaemonClient {
47 pub fn connect(roots: Vec<PathBuf>) -> anyhow::Result<Self> {
48 Self::connect_config(config_from_roots(roots)?)
49 }
50
51 pub fn connect_config(config: DaemonWorkspaceConfig) -> anyhow::Result<Self> {
52 let config = canonical_workspace_config(config)?;
53 let Some(entry) = read_registry_entry(&config)? else {
54 anyhow::bail!(
55 "no daemon registered for {}",
56 workspace_label(&config_roots(&config))
57 );
58 };
59 connect_entry(config, entry)
60 }
61
62 pub fn connect_or_start(roots: Vec<PathBuf>) -> anyhow::Result<Self> {
63 Self::connect_or_start_config(config_from_roots(roots)?)
64 }
65
66 pub fn connect_or_start_config(config: DaemonWorkspaceConfig) -> anyhow::Result<Self> {
67 let config = canonical_workspace_config(config)?;
68 if registry_entry_for(&config)?.is_some() {
69 return wait_for_daemon(config);
70 }
71 start_daemon_process(&config)?;
72 wait_for_daemon(config)
73 }
74
75 pub fn connect_or_start_supporting(
76 config: DaemonWorkspaceConfig,
77 capability: &str,
78 ) -> anyhow::Result<Self> {
79 let client = Self::connect_or_start_config(config.clone())?;
80 if client.supports_query(capability)? {
81 return Ok(client);
82 }
83 let _ = client.shutdown();
84 drop(client);
85 let config = canonical_workspace_config(config)?;
86 wait_for_deregistration(&config);
87 let _ = cleanup_stale_config(&config);
88 start_daemon_process(&config)?;
89 let client = wait_for_daemon(config)?;
90 if !client.supports_query(capability)? {
91 anyhow::bail!(
92 "the code-moniker daemon binary predates `{capability}`; update code-moniker and retry"
93 );
94 }
95 Ok(client)
96 }
97
98 pub fn root(&self) -> &Path {
99 &self.endpoint.roots[0]
100 }
101
102 pub fn roots(&self) -> &[PathBuf] {
103 &self.endpoint.roots
104 }
105
106 pub fn config(&self) -> &DaemonWorkspaceConfig {
107 &self.endpoint.config
108 }
109
110 pub fn endpoint(&self) -> &str {
111 &self.endpoint.address
112 }
113}
114
115impl Deref for DaemonClient {
116 type Target = DaemonConnection;
117
118 fn deref(&self) -> &Self::Target {
119 &self.connection
120 }
121}
122
123impl DaemonConnection {
124 pub fn handshake(&self, client: &str) -> anyhow::Result<HandshakeResponse> {
125 self.block(self.ws.handshake(client.to_string()))
126 .map_err(|err| anyhow::anyhow!("{err}"))
127 }
128
129 pub fn supports_query(&self, capability: &str) -> anyhow::Result<bool> {
130 let handshake = self.handshake("daemon-client")?;
131 Ok(handshake
132 .capabilities
133 .queries
134 .iter()
135 .any(|verb| verb == capability))
136 }
137
138 pub fn query(&self, request: QueryRequest) -> anyhow::Result<QueryResponse> {
139 self.block(self.ws.query(request))
140 .map_err(|err| anyhow::anyhow!("{err}"))
141 }
142
143 pub fn command(&self, request: CommandRequest) -> anyhow::Result<String> {
144 Ok(self.command_response(request)?.message)
145 }
146
147 pub fn command_response(&self, request: CommandRequest) -> anyhow::Result<CommandResponse> {
148 self.block(self.ws.command(request))
149 .map_err(|err| anyhow::anyhow!("{err}"))
150 }
151
152 pub fn shutdown(&self) -> anyhow::Result<()> {
153 self.block(self.ws.shutdown())
154 .map_err(|err| anyhow::anyhow!("{err}"))
155 }
156
157 fn block<F: Future>(&self, fut: F) -> F::Output {
158 self.runtime.block_on(fut)
159 }
160}
161
162fn connect_entry(
163 config: DaemonWorkspaceConfig,
164 entry: DaemonRegistryEntry,
165) -> anyhow::Result<DaemonClient> {
166 let runtime = Arc::new(build_runtime()?);
167 let url = format!("ws://{}", entry.endpoint);
168 let ws = runtime.block_on(async { WsClientBuilder::default().build(&url).await })?;
169 let client = DaemonClient {
170 connection: DaemonConnection {
171 runtime,
172 ws: Arc::new(ws),
173 },
174 endpoint: DaemonEndpoint {
175 roots: config_roots(&config),
176 config,
177 address: entry.endpoint,
178 },
179 };
180 client.handshake("daemon-client")?;
181 Ok(client)
182}
183
184fn registry_entry_for(
185 config: &DaemonWorkspaceConfig,
186) -> anyhow::Result<Option<DaemonRegistryEntry>> {
187 if let Some(entry) = read_registry_entry(config)? {
188 if pid_is_alive(entry.pid) {
189 return Ok(Some(entry));
190 }
191 let path = registry_path_for_config(config)?;
192 remove_registry_entry_if_own(&path, &entry);
193 }
194 for (path, entry) in list_registry_files()? {
195 if !pid_is_alive(entry.pid) {
196 remove_registry_entry_if_own(&path, &entry);
197 continue;
198 }
199 let serves_all_roots = config
200 .roots
201 .iter()
202 .all(|root| entry.workspace_roots.contains(root));
203 if serves_all_roots && pid_is_alive(entry.pid) {
204 return Ok(Some(entry));
205 }
206 }
207 Ok(None)
208}
209
210fn build_runtime() -> anyhow::Result<Runtime> {
211 Ok(tokio::runtime::Builder::new_multi_thread()
212 .worker_threads(2)
213 .enable_all()
214 .thread_name("code-moniker-daemon-client")
215 .build()?)
216}
217
218fn wait_for_deregistration(config: &DaemonWorkspaceConfig) {
221 for _ in 0..30 {
222 match read_registry_entry(config) {
223 Ok(Some(entry)) if pid_is_alive(entry.pid) => {
224 thread::sleep(Duration::from_millis(100));
225 }
226 _ => return,
227 }
228 }
229}
230
231fn wait_for_daemon(config: DaemonWorkspaceConfig) -> anyhow::Result<DaemonClient> {
232 for _ in 0..50 {
233 if let Some(entry) = registry_entry_for(&config)?
234 && entry.state == DaemonRegistryState::Ready
235 && let Ok(client) = connect_entry(config.clone(), entry)
236 {
237 return Ok(client);
238 }
239 thread::sleep(Duration::from_millis(100));
240 }
241 anyhow::bail!(
242 "daemon did not become ready for {}",
243 workspace_label(&config_roots(&config))
244 )
245}
246
247pub fn cleanup_stale_entry(roots: Vec<PathBuf>) -> anyhow::Result<()> {
248 cleanup_stale_config(&config_from_roots(roots)?)
249}
250
251pub fn cleanup_stale_config(config: &DaemonWorkspaceConfig) -> anyhow::Result<()> {
252 let path = registry_path_for_config(config)?;
253 if let Some(entry) = read_registry_entry(config)?
254 && !pid_is_alive(entry.pid)
255 {
256 remove_registry_entry_if_own(&path, &entry);
257 }
258 Ok(())
259}
260
261fn start_daemon_process(config: &DaemonWorkspaceConfig) -> anyhow::Result<()> {
262 let exe = std::env::current_exe()?;
263 let mut command = ProcessCommand::new(exe);
264 command
265 .arg("daemon")
266 .arg("start")
267 .stdin(Stdio::null())
268 .stdout(Stdio::null())
269 .stderr(Stdio::null());
270 if let Some(project) = &config.project {
271 command.arg("--project").arg(project);
272 }
273 if let Some(cache_dir) = &config.cache_dir {
274 command.arg("--cache").arg(cache_dir);
275 }
276 if let Some(live_refresh) = &config.live_refresh {
277 command.arg("--live-refresh").arg(live_refresh);
278 }
279 for root in config_roots(config) {
280 command.arg(root);
281 }
282 command.spawn().map(|_| ()).map_err(|err| {
283 anyhow::anyhow!(
284 "cannot start daemon for {}: {err}",
285 workspace_label(&config_roots(config))
286 )
287 })
288}