1use std::{path::Path, sync::Arc, time::Duration};
6
7use anyhow::{Context, Result, anyhow};
8use scv_client::Layout;
9use scv_tools::delegation::{self as delegations, DelegationRegistry};
10use tokio::{
11 net::{UnixListener, UnixStream},
12 sync::Mutex,
13};
14use tokio_util::{sync::CancellationToken, task::TaskTracker};
15
16use crate::{
17 components,
18 config::{ConfigOverrides, Instance},
19 confirm,
20 connection::{Shared, run_managed},
21 disk, restart,
22};
23
24pub async fn run_stdio(layout: &Layout, overrides: ConfigOverrides) -> Result<()> {
26 let stdin = tokio::io::stdin();
27 let stdout = tokio::io::stdout();
28 let tasks = TaskTracker::new();
29 let registry = instance_delegations(layout);
30 tokio::spawn(reconcile_delegations(Arc::clone(®istry)));
33 let result = run_managed(
34 stdin,
35 stdout,
36 Instance {
37 layout: layout.clone(),
38 overrides,
39 },
40 Shared {
41 components: None,
42 registry,
43 hosts: None,
44 },
45 CancellationToken::new(),
46 tasks.clone(),
47 )
48 .await;
49 tasks.close();
50 tasks.wait().await;
51 result
52}
53
54pub async fn run_socket(layout: &Layout, overrides: ConfigOverrides) -> Result<()> {
56 let socket = layout.socket();
57 let path = socket.as_path();
58 let instance = Instance {
59 layout: layout.clone(),
60 overrides,
61 };
62 if let Some(parent) = path.parent() {
63 tokio::fs::create_dir_all(parent)
64 .await
65 .context("create SCV socket directory")?;
66 #[cfg(unix)]
67 {
68 use std::os::unix::fs::PermissionsExt;
69 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
70 .context("secure SCV socket directory")?;
71 }
72 }
73 let _lock = SocketLock::acquire(path)?;
74 for warning in instance.claude_warnings() {
75 tracing::warn!("{warning}");
76 }
77 instance.log_policy();
78 if let Ok(strays) = layout.strays() {
81 for stray in strays.into_iter().filter(|stray| stray.legacy) {
82 tracing::warn!(
83 "{} is from an older SCV layout and is not used; see `scv config show`",
84 stray.path.display()
85 );
86 }
87 }
88 if path.exists() {
89 if UnixStream::connect(path).await.is_ok() {
90 return Err(anyhow!(
91 "SCV server is already running at {}",
92 path.display()
93 ));
94 }
95 use std::os::unix::fs::FileTypeExt;
96 if !std::fs::symlink_metadata(path)?.file_type().is_socket() {
97 return Err(anyhow!(
98 "refusing to remove a non-socket at SCV socket path"
99 ));
100 }
101 tokio::fs::remove_file(path)
102 .await
103 .with_context(|| format!("remove stale SCV socket {}", path.display()))?;
104 }
105 let listener = UnixListener::bind(path)
106 .with_context(|| format!("bind SCV server socket {}", path.display()))?;
107 #[cfg(unix)]
108 {
109 use std::os::unix::fs::PermissionsExt;
110 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
111 .context("secure SCV socket")?;
112 }
113 let hub = scv_channels::hub::Hub::new(Some(layout.last_owner()));
114 let startup = restart::startup(layout, &hub);
117 let components = Arc::new(Mutex::new(components::Components::with_hub(
118 instance.clone(),
119 std::env::current_dir()?,
120 Arc::clone(&hub),
121 )?));
122 let registry = instance_delegations(layout);
123 if !delegations::become_child_subreaper() {
125 tracing::debug!("SCV daemon is not a child subreaper on this platform");
126 }
127 let cancellation = CancellationToken::new();
128 let restarter = restart::Restarter::new(
129 instance.clone(),
130 Arc::clone(&hub),
131 Arc::clone(®istry),
132 &components,
133 cancellation.clone(),
134 );
135 let disk = tokio::spawn(disk::monitor(
136 instance.clone(),
137 Arc::clone(&hub),
138 restarter.notifier().clone(),
139 cancellation.clone(),
140 ));
141 let _disk_abort = AbortGuard(disk.abort_handle());
142 let confirmer = confirm::Confirmer::new(
143 hub,
144 Arc::clone(®istry),
145 restarter.notifier().clone(),
146 cancellation.clone(),
147 );
148 let hosts = crate::workbench::Hosts::new(
150 instance.clone(),
151 Arc::clone(®istry),
152 Some(Arc::clone(&confirmer)),
153 );
154 {
155 let mut components = components.lock().await;
156 components.set_restarter(Arc::clone(&restarter));
157 components.set_confirmer(confirmer);
158 components.follow_ups().set_hosts(Arc::clone(&hosts));
159 }
160 let notices = tokio::spawn(restart::announce(
161 layout.clone(),
162 startup,
163 restarter.notifier().clone(),
164 cancellation.clone(),
165 ));
166 let _notices_abort = AbortGuard(notices.abort_handle());
167 let monitor = tokio::spawn(restart::monitor(
168 restarter.notifier().clone(),
169 cancellation.clone(),
170 ));
171 let _monitor_abort = AbortGuard(monitor.abort_handle());
172 let delegation_registry = Arc::clone(®istry);
173 let delegation_cancel = cancellation.clone();
174 let maintained = Arc::clone(&hosts);
175 let subscriptions = components.lock().await.follow_ups();
176 let mut delegation_task = tokio::spawn(async move {
177 let mut interval = tokio::time::interval(DELEGATION_RECONCILE_INTERVAL);
181 loop {
182 tokio::select! {
183 biased;
184 () = delegation_cancel.cancelled() => break,
185 _ = interval.tick() => {
186 subscriptions.reload();
192 reconcile_delegations(Arc::clone(&delegation_registry)).await;
193 subscriptions.retry_pending().await;
194 let zombies = delegations::reap_orphaned_zombies();
195 if zombies > 0 {
196 tracing::debug!("Reaped {zombies} exited orphan processes");
197 }
198 crate::workbench::maintain(&maintained).await;
199 }
200 }
201 }
202 });
203 let _delegation_abort = AbortGuard(delegation_task.abort_handle());
204 let tasks = TaskTracker::new();
205 let mut clients = tokio::task::JoinSet::new();
206 let refresh_components = components.clone();
207 let refresh_cancel = cancellation.clone();
208 let mut refresh_task = tokio::spawn(async move {
209 let mut refresh = tokio::time::interval(Duration::from_secs(2));
210 loop {
211 tokio::select! {
212 biased;
213 () = refresh_cancel.cancelled() => break,
214 _ = refresh.tick() => {
215 tokio::select! {
216 biased;
217 () = refresh_cancel.cancelled() => break,
218 result = async { refresh_components.lock().await.reconcile().await } => {
219 if result.is_err() { tracing::warn!("Component account discovery failed"); }
220 }
221 }
222 }
223 }
224 }
225 });
226 let _refresh_abort = AbortGuard(refresh_task.abort_handle());
227 let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
228 let result = loop {
229 tokio::select! {
230 accepted = listener.accept() => {
231 let (stream, _) = match accepted { Ok(value) => value, Err(error) => break Err(error.into()) };
232 let instance = instance.clone();
233 let components = components.clone();
234 let registry = Arc::clone(®istry);
235 let hosts = Arc::clone(&hosts);
236 let cancellation = cancellation.clone();
237 let tasks = tasks.clone();
238 clients.spawn(async move {
239 let (reader, writer) = stream.into_split();
240 if let Err(error) = run_managed(reader, writer, instance, Shared { components: Some(components), registry, hosts: Some(hosts) }, cancellation, tasks).await {
241 tracing::warn!(error = format!("{error:#}"), "SCV socket client stopped");
242 }
243 });
244 }
245 _ = clients.join_next(), if !clients.is_empty() => {},
246 _ = tokio::signal::ctrl_c() => break Ok(()),
247 _ = terminate.recv() => break Ok(()),
248 }
249 };
250 drop(listener);
251 cancellation.cancel();
252 let _ = (&mut refresh_task).await;
253 let _ = (&mut delegation_task).await;
254 components.lock().await.shutdown().await;
255 hosts.shutdown().await;
257 if tokio::time::timeout(Duration::from_secs(8), async {
258 while clients.join_next().await.is_some() {}
259 })
260 .await
261 .is_err()
262 {
263 clients.abort_all();
264 while clients.join_next().await.is_some() {}
265 }
266 tasks.close();
267 tasks.wait().await;
268 let _ = tokio::fs::remove_file(path).await;
269 restart::clean_shutdown(layout);
270 result
271}
272
273pub(crate) const DELEGATION_RECONCILE_INTERVAL: Duration = Duration::from_secs(60);
274
275pub(crate) fn instance_delegations(layout: &Layout) -> Arc<DelegationRegistry> {
277 Arc::new(DelegationRegistry::new(layout))
278}
279
280pub(crate) async fn reconcile_delegations(registry: Arc<DelegationRegistry>) {
282 let report = registry.reconcile().await;
283 if !report.reaped.is_empty() {
284 tracing::info!(
285 "Reaped {} orphaned delegations: {}",
286 report.reaped.len(),
287 report.reaped.join(", ")
288 );
289 }
290 if report.removed > 0 {
291 tracing::debug!(
292 "Removed {} delegation records whose processes had exited",
293 report.removed
294 );
295 }
296 if report.stale_markers > 0 {
297 tracing::debug!(
298 "Removed {} conversation markers whose SCV process had exited",
299 report.stale_markers
300 );
301 }
302}
303
304pub(crate) struct SocketLock(std::fs::File);
306impl SocketLock {
307 pub(crate) fn acquire(socket: &Path) -> Result<Self> {
308 use std::os::unix::{fs::OpenOptionsExt, io::AsRawFd};
309 let file = std::fs::OpenOptions::new()
310 .read(true)
311 .write(true)
312 .create(true)
313 .truncate(false)
314 .mode(0o600)
315 .custom_flags(libc::O_NOFOLLOW)
316 .open(socket.with_extension("lock"))?;
317 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
319 return Err(anyhow!("SCV daemon already owns this socket"));
320 }
321 Ok(Self(file))
322 }
323}
324impl Drop for SocketLock {
325 fn drop(&mut self) {
326 use std::os::unix::io::AsRawFd;
327 unsafe {
329 libc::flock(self.0.as_raw_fd(), libc::LOCK_UN);
330 }
331 }
332}
333
334pub(crate) struct AbortGuard(pub(crate) tokio::task::AbortHandle);
336impl Drop for AbortGuard {
337 fn drop(&mut self) {
338 self.0.abort();
339 }
340}
341
342#[cfg(test)]
343mod tests;