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 }
159 let notices = tokio::spawn(restart::announce(
160 layout.clone(),
161 startup,
162 restarter.notifier().clone(),
163 cancellation.clone(),
164 ));
165 let _notices_abort = AbortGuard(notices.abort_handle());
166 let monitor = tokio::spawn(restart::monitor(
167 restarter.notifier().clone(),
168 cancellation.clone(),
169 ));
170 let _monitor_abort = AbortGuard(monitor.abort_handle());
171 let delegation_registry = Arc::clone(®istry);
172 let delegation_cancel = cancellation.clone();
173 let maintained = Arc::clone(&hosts);
174 let mut delegation_task = tokio::spawn(async move {
175 let mut interval = tokio::time::interval(DELEGATION_RECONCILE_INTERVAL);
178 loop {
179 tokio::select! {
180 biased;
181 () = delegation_cancel.cancelled() => break,
182 _ = interval.tick() => {
183 reconcile_delegations(Arc::clone(&delegation_registry)).await;
184 let zombies = delegations::reap_orphaned_zombies();
185 if zombies > 0 {
186 tracing::debug!("Reaped {zombies} exited orphan processes");
187 }
188 crate::workbench::maintain(&maintained).await;
189 }
190 }
191 }
192 });
193 let _delegation_abort = AbortGuard(delegation_task.abort_handle());
194 let tasks = TaskTracker::new();
195 let mut clients = tokio::task::JoinSet::new();
196 let refresh_components = components.clone();
197 let refresh_cancel = cancellation.clone();
198 let mut refresh_task = tokio::spawn(async move {
199 let mut refresh = tokio::time::interval(Duration::from_secs(2));
200 loop {
201 tokio::select! {
202 biased;
203 () = refresh_cancel.cancelled() => break,
204 _ = refresh.tick() => {
205 tokio::select! {
206 biased;
207 () = refresh_cancel.cancelled() => break,
208 result = async { refresh_components.lock().await.reconcile().await } => {
209 if result.is_err() { tracing::warn!("Component account discovery failed"); }
210 }
211 }
212 }
213 }
214 }
215 });
216 let _refresh_abort = AbortGuard(refresh_task.abort_handle());
217 let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
218 let result = loop {
219 tokio::select! {
220 accepted = listener.accept() => {
221 let (stream, _) = match accepted { Ok(value) => value, Err(error) => break Err(error.into()) };
222 let instance = instance.clone();
223 let components = components.clone();
224 let registry = Arc::clone(®istry);
225 let hosts = Arc::clone(&hosts);
226 let cancellation = cancellation.clone();
227 let tasks = tasks.clone();
228 clients.spawn(async move {
229 let (reader, writer) = stream.into_split();
230 if let Err(error) = run_managed(reader, writer, instance, Shared { components: Some(components), registry, hosts: Some(hosts) }, cancellation, tasks).await {
231 tracing::warn!(error = format!("{error:#}"), "SCV socket client stopped");
232 }
233 });
234 }
235 _ = clients.join_next(), if !clients.is_empty() => {},
236 _ = tokio::signal::ctrl_c() => break Ok(()),
237 _ = terminate.recv() => break Ok(()),
238 }
239 };
240 drop(listener);
241 cancellation.cancel();
242 let _ = (&mut refresh_task).await;
243 let _ = (&mut delegation_task).await;
244 components.lock().await.shutdown().await;
245 hosts.shutdown().await;
247 if tokio::time::timeout(Duration::from_secs(8), async {
248 while clients.join_next().await.is_some() {}
249 })
250 .await
251 .is_err()
252 {
253 clients.abort_all();
254 while clients.join_next().await.is_some() {}
255 }
256 tasks.close();
257 tasks.wait().await;
258 let _ = tokio::fs::remove_file(path).await;
259 restart::clean_shutdown(layout);
260 result
261}
262
263pub(crate) const DELEGATION_RECONCILE_INTERVAL: Duration = Duration::from_secs(60);
264
265pub(crate) fn instance_delegations(layout: &Layout) -> Arc<DelegationRegistry> {
267 Arc::new(DelegationRegistry::new(layout))
268}
269
270pub(crate) async fn reconcile_delegations(registry: Arc<DelegationRegistry>) {
272 let report = registry.reconcile().await;
273 if !report.reaped.is_empty() {
274 tracing::info!(
275 "Reaped {} orphaned delegations: {}",
276 report.reaped.len(),
277 report.reaped.join(", ")
278 );
279 }
280 if report.removed > 0 {
281 tracing::debug!(
282 "Removed {} delegation records whose processes had exited",
283 report.removed
284 );
285 }
286 if report.stale_markers > 0 {
287 tracing::debug!(
288 "Removed {} conversation markers whose SCV process had exited",
289 report.stale_markers
290 );
291 }
292}
293
294pub(crate) struct SocketLock(std::fs::File);
296impl SocketLock {
297 pub(crate) fn acquire(socket: &Path) -> Result<Self> {
298 use std::os::unix::{fs::OpenOptionsExt, io::AsRawFd};
299 let file = std::fs::OpenOptions::new()
300 .read(true)
301 .write(true)
302 .create(true)
303 .truncate(false)
304 .mode(0o600)
305 .custom_flags(libc::O_NOFOLLOW)
306 .open(socket.with_extension("lock"))?;
307 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
309 return Err(anyhow!("SCV daemon already owns this socket"));
310 }
311 Ok(Self(file))
312 }
313}
314impl Drop for SocketLock {
315 fn drop(&mut self) {
316 use std::os::unix::io::AsRawFd;
317 unsafe {
319 libc::flock(self.0.as_raw_fd(), libc::LOCK_UN);
320 }
321 }
322}
323
324pub(crate) struct AbortGuard(pub(crate) tokio::task::AbortHandle);
326impl Drop for AbortGuard {
327 fn drop(&mut self) {
328 self.0.abort();
329 }
330}
331
332#[cfg(test)]
333mod tests;