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