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 if let Ok(strays) = layout.strays() {
80 for stray in strays.into_iter().filter(|stray| stray.legacy) {
81 tracing::warn!(
82 "{} is from an older SCV layout and is not used; see `scv config show`",
83 stray.path.display()
84 );
85 }
86 }
87 if path.exists() {
88 if UnixStream::connect(path).await.is_ok() {
89 return Err(anyhow!(
90 "SCV server is already running at {}",
91 path.display()
92 ));
93 }
94 use std::os::unix::fs::FileTypeExt;
95 if !std::fs::symlink_metadata(path)?.file_type().is_socket() {
96 return Err(anyhow!(
97 "refusing to remove a non-socket at SCV socket path"
98 ));
99 }
100 tokio::fs::remove_file(path)
101 .await
102 .with_context(|| format!("remove stale SCV socket {}", path.display()))?;
103 }
104 let listener = UnixListener::bind(path)
105 .with_context(|| format!("bind SCV server socket {}", path.display()))?;
106 #[cfg(unix)]
107 {
108 use std::os::unix::fs::PermissionsExt;
109 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
110 .context("secure SCV socket")?;
111 }
112 let hub = scv_channels::hub::Hub::new(Some(layout.last_owner()));
113 let startup = restart::startup(layout, &hub);
116 let components = Arc::new(Mutex::new(components::Components::with_hub(
117 instance.clone(),
118 std::env::current_dir()?,
119 Arc::clone(&hub),
120 )?));
121 let registry = instance_delegations(layout);
122 let hosts = crate::workbench::Hosts::new(instance.clone(), Arc::clone(®istry));
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 {
149 let mut components = components.lock().await;
150 components.set_restarter(Arc::clone(&restarter));
151 components.set_confirmer(confirmer);
152 }
153 let notices = tokio::spawn(restart::announce(
154 layout.clone(),
155 startup,
156 restarter.notifier().clone(),
157 cancellation.clone(),
158 ));
159 let _notices_abort = AbortGuard(notices.abort_handle());
160 let monitor = tokio::spawn(restart::monitor(
161 restarter.notifier().clone(),
162 cancellation.clone(),
163 ));
164 let _monitor_abort = AbortGuard(monitor.abort_handle());
165 let delegation_registry = Arc::clone(®istry);
166 let delegation_cancel = cancellation.clone();
167 let maintained = Arc::clone(&hosts);
168 let mut delegation_task = tokio::spawn(async move {
169 let mut interval = tokio::time::interval(DELEGATION_RECONCILE_INTERVAL);
172 loop {
173 tokio::select! {
174 biased;
175 () = delegation_cancel.cancelled() => break,
176 _ = interval.tick() => {
177 reconcile_delegations(Arc::clone(&delegation_registry)).await;
178 let zombies = delegations::reap_orphaned_zombies();
179 if zombies > 0 {
180 tracing::debug!("Reaped {zombies} exited orphan processes");
181 }
182 crate::workbench::maintain(&maintained).await;
183 }
184 }
185 }
186 });
187 let _delegation_abort = AbortGuard(delegation_task.abort_handle());
188 let tasks = TaskTracker::new();
189 let mut clients = tokio::task::JoinSet::new();
190 let refresh_components = components.clone();
191 let refresh_cancel = cancellation.clone();
192 let mut refresh_task = tokio::spawn(async move {
193 let mut refresh = tokio::time::interval(Duration::from_secs(2));
194 loop {
195 tokio::select! {
196 biased;
197 () = refresh_cancel.cancelled() => break,
198 _ = refresh.tick() => {
199 tokio::select! {
200 biased;
201 () = refresh_cancel.cancelled() => break,
202 result = async { refresh_components.lock().await.reconcile().await } => {
203 if result.is_err() { tracing::warn!("Component account discovery failed"); }
204 }
205 }
206 }
207 }
208 }
209 });
210 let _refresh_abort = AbortGuard(refresh_task.abort_handle());
211 let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
212 let result = loop {
213 tokio::select! {
214 accepted = listener.accept() => {
215 let (stream, _) = match accepted { Ok(value) => value, Err(error) => break Err(error.into()) };
216 let instance = instance.clone();
217 let components = components.clone();
218 let registry = Arc::clone(®istry);
219 let hosts = Arc::clone(&hosts);
220 let cancellation = cancellation.clone();
221 let tasks = tasks.clone();
222 clients.spawn(async move {
223 let (reader, writer) = stream.into_split();
224 if let Err(error) = run_managed(reader, writer, instance, Shared { components: Some(components), registry, hosts: Some(hosts) }, cancellation, tasks).await {
225 tracing::warn!(error = format!("{error:#}"), "SCV socket client stopped");
226 }
227 });
228 }
229 _ = clients.join_next(), if !clients.is_empty() => {},
230 _ = tokio::signal::ctrl_c() => break Ok(()),
231 _ = terminate.recv() => break Ok(()),
232 }
233 };
234 drop(listener);
235 cancellation.cancel();
236 let _ = (&mut refresh_task).await;
237 let _ = (&mut delegation_task).await;
238 components.lock().await.shutdown().await;
239 hosts.shutdown().await;
241 if tokio::time::timeout(Duration::from_secs(8), async {
242 while clients.join_next().await.is_some() {}
243 })
244 .await
245 .is_err()
246 {
247 clients.abort_all();
248 while clients.join_next().await.is_some() {}
249 }
250 tasks.close();
251 tasks.wait().await;
252 let _ = tokio::fs::remove_file(path).await;
253 restart::clean_shutdown(layout);
254 result
255}
256
257pub(crate) const DELEGATION_RECONCILE_INTERVAL: Duration = Duration::from_secs(60);
258
259pub(crate) fn instance_delegations(layout: &Layout) -> Arc<DelegationRegistry> {
261 Arc::new(DelegationRegistry::new(layout))
262}
263
264pub(crate) async fn reconcile_delegations(registry: Arc<DelegationRegistry>) {
266 let report = registry.reconcile().await;
267 if !report.reaped.is_empty() {
268 tracing::info!(
269 "Reaped {} orphaned delegations: {}",
270 report.reaped.len(),
271 report.reaped.join(", ")
272 );
273 }
274 if report.removed > 0 {
275 tracing::debug!(
276 "Removed {} delegation records whose processes had exited",
277 report.removed
278 );
279 }
280 if report.stale_markers > 0 {
281 tracing::debug!(
282 "Removed {} conversation markers whose SCV process had exited",
283 report.stale_markers
284 );
285 }
286}
287
288pub(crate) struct SocketLock(std::fs::File);
290impl SocketLock {
291 pub(crate) fn acquire(socket: &Path) -> Result<Self> {
292 use std::os::unix::{fs::OpenOptionsExt, io::AsRawFd};
293 let file = std::fs::OpenOptions::new()
294 .read(true)
295 .write(true)
296 .create(true)
297 .truncate(false)
298 .mode(0o600)
299 .custom_flags(libc::O_NOFOLLOW)
300 .open(socket.with_extension("lock"))?;
301 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } != 0 {
303 return Err(anyhow!("SCV daemon already owns this socket"));
304 }
305 Ok(Self(file))
306 }
307}
308impl Drop for SocketLock {
309 fn drop(&mut self) {
310 use std::os::unix::io::AsRawFd;
311 unsafe {
313 libc::flock(self.0.as_raw_fd(), libc::LOCK_UN);
314 }
315 }
316}
317
318pub(crate) struct AbortGuard(pub(crate) tokio::task::AbortHandle);
320impl Drop for AbortGuard {
321 fn drop(&mut self) {
322 self.0.abort();
323 }
324}
325
326#[cfg(test)]
327mod tests;