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