Skip to main content

code_moniker_daemon_client/
lib.rs

1#![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
218// After asking an outdated daemon to shut down, give it a moment to leave
219// the registry so the fresh start does not race its guarded removal.
220fn 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}