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