1use super::service_journal::{DONE_RETAINED, landed_commits, prune_done};
16use super::service_index_lock::Announce;
17use super::service_land::{LandContext, Outcome, land_batch_announcing, recover, recover_instance};
18use super::private_storage::remove_tree;
19use super::service_instance::instance_of;
20use super::claim_time::ClaimTime;
21use super::service_retry::RetrySchedule;
22use super::service_work::{GitHandlers, Wake, run_round};
23use super::service_protocol::{
24 ClientMessage, CommitRequest, FailureCode, MAX_LINE_BYTES, PROTOCOL_VERSION, RELEASE, ServiceMessage, decode_client, encode_service,
25};
26use std::collections::HashMap;
27use std::io::{BufRead, BufReader, Read, Write};
28use std::os::unix::net::{UnixListener, UnixStream};
29use std::sync::atomic::{AtomicUsize, Ordering};
30use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
31use std::sync::Arc;
32use std::path::PathBuf;
33use std::time::{Duration, Instant};
34
35pub const MAX_BATCH: usize = 64;
37
38pub const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(10);
40
41pub const IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
44
45pub const INHERITED_DESCRIPTOR: i32 = 3;
47
48#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct ServeSettings {
51 pub idle: Duration,
53 pub release: String,
56 pub instance: Option<String>,
59}
60
61impl ServeSettings {
62 pub fn new(idle: Duration) -> Self {
64 return Self { idle, release: String::from(RELEASE), instance: None };
65 }
66}
67
68#[derive(Clone, Debug, Eq, PartialEq)]
70pub enum Bind {
71 Abstract(String),
73 Path(std::path::PathBuf),
75 Inherited,
77}
78
79pub fn bind(address: &Bind) -> std::io::Result<UnixListener> {
89 return match address {
90 #[cfg(target_os = "linux")]
91 Bind::Abstract(name) => {
92 use std::os::linux::net::SocketAddrExt;
93 let socket = std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?;
94 UnixListener::bind_addr(&socket)
95 }
96 #[cfg(not(target_os = "linux"))]
97 Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
98 Bind::Path(path) => bind_path(path),
99 Bind::Inherited => {
100 use std::os::fd::FromRawFd;
101 let announced_for: bool = std::env::var("LISTEN_PID").is_ok_and(|pid| { return pid == std::process::id().to_string(); });
102 let count: i32 = std::env::var("LISTEN_FDS").ok().and_then(|text| { return text.parse().ok(); }).unwrap_or(0);
103 if !announced_for || count < 1 {
104 return Err(std::io::Error::other("no socket was passed in"));
105 }
106 Ok(unsafe { UnixListener::from_raw_fd(INHERITED_DESCRIPTOR) })
108 }
109 };
110}
111
112fn bind_path(path: &std::path::Path) -> std::io::Result<UnixListener> {
125 use std::os::unix::fs::FileTypeExt;
126 let first: std::io::Error = match UnixListener::bind(path) {
127 Ok(listener) => return Ok(listener),
128 Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => error,
129 Err(error) => return Err(error),
130 };
131 let is_socket: bool = std::fs::symlink_metadata(path).is_ok_and(|meta| { return meta.file_type().is_socket(); });
132 let refused: bool = matches!(UnixStream::connect(path), Err(error) if error.kind() == std::io::ErrorKind::ConnectionRefused);
133 if !is_socket || !refused {
134 return Err(first);
135 }
136 std::fs::remove_file(path)?;
137 return UnixListener::bind(path);
138}
139
140fn peer_user(stream: &UnixStream) -> Option<u32> {
142 use std::os::fd::AsRawFd;
143 let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
144 let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
145 let status = unsafe {
147 libc::getsockopt(stream.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PEERCRED, (&raw mut credentials).cast(), &raw mut length)
148 };
149 return if status == 0 { Some(credentials.uid) } else { None };
150}
151
152enum Job {
153 Commit(Box<CommitRequest>, UnixStream),
154 Retire,
155 Activity,
157 Release(std::path::PathBuf),
160}
161
162fn wait_for_close(stream: &UnixStream) {
164 let mut sink: [u8; 64] = [0; 64];
165 let mut reader = stream;
166 loop {
167 match reader.read(&mut sink) {
168 Ok(0) | Err(_) => return,
169 Ok(_) => {}
170 }
171 }
172}
173
174fn send(stream: &UnixStream, message: &ServiceMessage) {
175 let mut line: String = encode_service(message);
176 line.push('\n');
177 let _written = (&*stream).write_all(line.as_bytes());
179}
180
181fn handle_connection(stream: UnixStream, queue: &Sender<Job>, work: &Sender<Wake>, busy: &AtomicUsize, release: &str, instance: Option<&str>) {
183 send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(release.to_string()) });
184 if peer_user(&stream) != Some(unsafe { libc::geteuid() }) {
186 return;
187 }
188 let _timeout = stream.set_read_timeout(Some(REQUEST_READ_TIMEOUT));
189 let Ok(reader_stream) = stream.try_clone() else { return };
190 let mut line: String = String::new();
191 let limit: u64 = u64::try_from(MAX_LINE_BYTES).unwrap_or(u64::MAX) + 1;
192 if BufReader::new(reader_stream.take(limit)).read_line(&mut line).is_err() {
193 return;
194 }
195 let _cleared = stream.set_read_timeout(None);
196 if line.is_empty() {
197 return;
199 }
200 let foreign = |git_dir: &std::path::Path| -> bool { return instance.is_some_and(|mine| { return instance_of(git_dir) != mine; }); };
203 match decode_client(line.trim_end_matches('\n')) {
204 Ok(ClientMessage::Commit(request)) if foreign(request.git_dir.as_path()) => {
205 eprintln!("cli-git-service: refused a commit for {}, which another instance serves", request.git_dir.display());
206 send(&stream, &ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::Rejected });
207 }
208 Ok(ClientMessage::Scratch { git_dir, .. } | ClientMessage::Work { git_dir, .. }) if foreign(git_dir.as_path()) => {
209 eprintln!("cli-git-service: refused a message for {}, which another instance serves", git_dir.display());
210 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
211 }
212 Ok(ClientMessage::Commit(request)) => {
213 let _queued = queue.send(Job::Commit(request, stream));
214 }
215 Ok(ClientMessage::Retire) => {
216 let _queued = queue.send(Job::Retire);
217 }
218 Ok(ClientMessage::Scratch { git_dir, path }) => {
219 busy.fetch_add(1, Ordering::SeqCst);
222 wait_for_close(&stream);
223 busy.fetch_sub(1, Ordering::SeqCst);
224 let _unused = git_dir;
225 let _queued = queue.send(Job::Release(path));
226 }
227 Ok(ClientMessage::Work { git_dir, review_key }) => {
228 let _queued = work.send((git_dir, review_key));
229 let _touched = queue.send(Job::Activity);
230 }
231 Err(reason) => {
232 eprintln!("cli-git-service: refused a request line: {reason}");
233 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
234 }
235 }
236}
237
238fn settle_open_batches(context: &LandContext, instance: Option<&str>) -> Vec<super::service_land::RecoveryReport> {
240 return match instance {
241 Some(mine) => recover_instance(context, mine),
242 None => recover(context),
243 };
244}
245
246fn land_group(context: &LandContext, instance: Option<&str>, seq: &mut u64, group: Vec<(Box<CommitRequest>, UnixStream)>, landed: &mut HashMap<String, String>) {
248 let (again, pending): (Vec<_>, Vec<_>) = group.into_iter().partition(|(request, _)| { return landed.contains_key(&request.id); });
249 for (request, stream) in again {
250 send(&stream, &ServiceMessage::Landed { id: request.id.clone(), commit: landed[&request.id].clone(), already: true });
251 }
252 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
254 let (fresh, repeats): (Vec<_>, Vec<_>) = pending.into_iter().partition(|(request, _)| { return seen.insert(request.id.clone()); });
255 if fresh.is_empty() {
256 return;
257 }
258 let _settled = settle_open_batches(context, instance);
261 *seq += 1;
262 for (index, (_, stream)) in fresh.iter().enumerate().skip(1) {
263 send(stream, &ServiceMessage::Position { number: index + 1, of: fresh.len() });
264 }
265 let requests: Vec<CommitRequest> = fresh.iter().map(|(request, _)| { return (**request).clone(); }).collect();
266 let announce = |note: Announce| {
267 for (_, stream) in &fresh {
268 match ¬e {
269 Announce::Waiting(text) => send(stream, &ServiceMessage::Waiting { text: text.clone() }),
270 Announce::Holder(who) => send(stream, &ServiceMessage::Holder { who: who.clone() }),
271 }
272 }
273 };
274 match land_batch_announcing(context, *seq, &requests, &announce) {
275 Ok(outcomes) => {
276 super::service_fault::reach("before-ack");
277 for ((request, _), outcome) in fresh.iter().zip(&outcomes) {
278 if let Outcome::Landed { commit } = outcome {
279 for (repeat, stream) in repeats.iter().filter(|(repeat, _)| { return repeat.id == request.id; }) {
280 send(stream, &ServiceMessage::Landed { id: repeat.id.clone(), commit: commit.clone(), already: true });
281 }
282 }
283 }
284 for ((request, stream), outcome) in fresh.iter().zip(outcomes) {
285 match outcome {
286 Outcome::Landed { commit } => {
287 landed.insert(request.id.clone(), commit.clone());
288 send(stream, &ServiceMessage::Landed { id: request.id.clone(), commit, already: false });
289 }
290 Outcome::RetryPrepare { base } => {
291 let captures = super::service_captures::remembered(request.common_dir.as_path(), request.repository.as_path());
292 send(stream, &ServiceMessage::RetryPrepare { id: request.id.clone(), base, landed: captures });
293 }
294 Outcome::BranchMoved => send(stream, &ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::BranchMoved }),
295 }
296 }
297 }
298 Err(code) => {
299 for (request, stream) in &fresh {
300 send(stream, &ServiceMessage::Failed { id: request.id.clone(), code });
301 }
302 }
303 }
304}
305
306fn work_loop(receiver: &Receiver<Wake>, handlers: &GitHandlers, busy: &AtomicUsize) {
319 let mut retries: RetrySchedule = RetrySchedule::default();
320 let mut counted_waiting: bool = false;
321 loop {
322 let first: Option<Wake> = match retries.next_wait(Instant::now()) {
323 None => match receiver.recv() {
324 Ok(wake) => Some(wake),
325 Err(_) => return,
326 },
327 Some(wait) => match receiver.recv_timeout(wait) {
328 Ok(wake) => Some(wake),
329 Err(RecvTimeoutError::Timeout) => None,
330 Err(RecvTimeoutError::Disconnected) => return,
331 },
332 };
333 busy.fetch_add(1, Ordering::SeqCst);
334 let mut wakes: Vec<Wake> = first.into_iter().chain(std::iter::from_fn(|| { return receiver.try_recv().ok(); })).collect();
335 wakes.extend(retries.take_due(Instant::now()).into_iter().map(|directory| { return (directory, None); }));
336 let ran: Vec<PathBuf> = wakes.iter().map(|(directory, _)| { return directory.clone(); }).collect();
337 let asked: Vec<(PathBuf, Duration)> = run_round(wakes, handlers, &ClaimTime::read());
338 retries.replace(ran.as_slice(), asked, Instant::now());
339 if !retries.is_empty() && !counted_waiting {
341 busy.fetch_add(1, Ordering::SeqCst);
342 counted_waiting = true;
343 } else if retries.is_empty() && counted_waiting {
344 busy.fetch_sub(1, Ordering::SeqCst);
345 counted_waiting = false;
346 }
347 busy.fetch_sub(1, Ordering::SeqCst);
348 }
349}
350
351pub fn serve(listener: UnixListener, context: &LandContext, settings: &ServeSettings) {
362 let idle: Duration = settings.idle;
363 let _reports = settle_open_batches(context, settings.instance.as_deref());
364 let pruned: usize = prune_done(context.journal.as_path(), DONE_RETAINED);
366 if pruned > 0 {
367 eprintln!("cli-git-service: removed the records of {pruned} old finished batches from {}", context.journal.display());
368 }
369 let mut landed: HashMap<String, String> = landed_commits(context.journal.as_path());
370 let (sender, receiver): (Sender<Job>, Receiver<Job>) = channel();
371 let acceptor_queue: Sender<Job> = sender.clone();
372 let (work_sender, work_receiver): (Sender<Wake>, Receiver<Wake>) = channel();
374 let handlers: GitHandlers = GitHandlers::new(context.git.clone());
375 let busy: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
376 let worker_busy: Arc<AtomicUsize> = Arc::clone(&busy);
377 std::thread::spawn(move || { work_loop(&work_receiver, &handlers, &worker_busy); });
378 let announced_release: String = settings.release.clone();
379 let connection_busy: Arc<AtomicUsize> = Arc::clone(&busy);
380 let instance: Option<String> = settings.instance.clone();
381 std::thread::spawn(move || {
382 for connection in listener.incoming().flatten() {
383 let queue: Sender<Job> = acceptor_queue.clone();
384 let work: Sender<Wake> = work_sender.clone();
385 let release: String = announced_release.clone();
386 let counted: Arc<AtomicUsize> = Arc::clone(&connection_busy);
387 let mine: Option<String> = instance.clone();
388 std::thread::spawn(move || { handle_connection(connection, &queue, &work, &counted, release.as_str(), mine.as_deref()); });
389 }
390 });
391 let mut seq: u64 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(1, |elapsed| { return u64::try_from(elapsed.as_micros()).unwrap_or(1); });
393 let mut retiring: bool = false;
394 while !retiring {
395 let first: Job = match receiver.recv_timeout(idle) {
396 Ok(job) => job,
397 Err(RecvTimeoutError::Timeout) if busy.load(Ordering::SeqCst) > 0 => continue,
399 Err(_) => return,
400 };
401 let mut jobs: Vec<Job> = vec![first];
402 while jobs.len() < MAX_BATCH {
403 match receiver.try_recv() {
404 Ok(job) => jobs.push(job),
405 Err(_) => break,
406 }
407 }
408 let mut groups: Vec<((std::path::PathBuf, String, bool), Vec<(Box<CommitRequest>, UnixStream)>)> = Vec::new();
410 let mut releases: Vec<std::path::PathBuf> = Vec::new();
411 for job in jobs {
412 match job {
413 Job::Retire => retiring = true,
414 Job::Activity => {}
415 Job::Release(path) => releases.push(path),
416 Job::Commit(request, stream) => {
417 let queue_reference: String = if request.reference == "HEAD" {
421 format!("HEAD@{}", request.repository.display())
422 } else {
423 request.reference.clone()
424 };
425 let key = (request.common_dir.clone(), queue_reference, request.lands_alone());
426 let joins = groups.iter_mut().rev().find(|((common, reference, _), _)| { return *common == key.0 && *reference == key.1; });
427 match joins {
428 Some((existing, members)) if !key.2 && !existing.2 => members.push((request, stream)),
429 _ => groups.push((key, vec![(request, stream)])),
430 }
431 }
432 }
433 }
434 for (_, group) in groups {
435 land_group(context, settings.instance.as_deref(), &mut seq, group, &mut landed);
436 }
437 for path in releases {
438 if let Err(error) = remove_tree(path.as_path()) {
439 eprintln!("cli-git-service: the scratch directory {} could not be removed: {error}", path.display());
440 }
441 }
442 }
443}
444
445#[cfg(test)]
447#[path = "service_server_tests.rs"]
448mod tests;