1use super::service_journal::{DONE_RETAINED, landed_commits, prune_done};
16use super::service_index_lock::{Announce, unsettled_locks};
17use super::service_land::{LandContext, Outcome, land_batch_timed, recover, recover_instance};
18use super::service_timing::{Answered, Arrival, ExitReason, LandTiming, answered_line, exit_line, log, retire_line};
19use super::private_storage::remove_tree;
20use super::service_instance::instance_of;
21use super::claim_time::ClaimTime;
22use super::service_retry::RetrySchedule;
23use super::service_work::{GitHandlers, Wake, run_round};
24use super::service_protocol::{
25 ClientMessage, CommitRequest, FailureCode, MAX_LINE_BYTES, PROTOCOL_VERSION, RELEASE, ServiceMessage, decode_client, encode_service,
26};
27use std::collections::HashMap;
28use std::io::{BufRead, BufReader, Read, Write};
29use std::os::unix::net::{UnixListener, UnixStream};
30use std::sync::atomic::{AtomicUsize, Ordering};
31use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
32use std::sync::Arc;
33use std::path::PathBuf;
34use std::time::{Duration, Instant};
35
36pub const MAX_BATCH: usize = 64;
38
39pub const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(10);
41
42pub const IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
45
46pub const INHERITED_DESCRIPTOR: i32 = 3;
48
49#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct ServeSettings {
52 pub idle: Duration,
54 pub release: String,
57 pub instance: Option<String>,
60}
61
62impl ServeSettings {
63 pub fn new(idle: Duration) -> Self {
65 return Self { idle, release: String::from(RELEASE), instance: None };
66 }
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
71pub enum Bind {
72 Abstract(String),
74 Path(std::path::PathBuf),
76 Inherited,
78}
79
80pub fn bind(address: &Bind) -> std::io::Result<UnixListener> {
90 return match address {
91 #[cfg(target_os = "linux")]
92 Bind::Abstract(name) => {
93 use std::os::linux::net::SocketAddrExt;
94 let socket = std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?;
95 UnixListener::bind_addr(&socket)
96 }
97 #[cfg(not(target_os = "linux"))]
98 Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
99 Bind::Path(path) => bind_path(path),
100 Bind::Inherited => {
101 use std::os::fd::FromRawFd;
102 ensure_socket_passed_in(std::env::var("LISTEN_PID").ok().as_deref(), std::env::var("LISTEN_FDS").ok().as_deref(), std::process::id())?;
103 Ok(unsafe { UnixListener::from_raw_fd(INHERITED_DESCRIPTOR) })
105 }
106 };
107}
108
109fn ensure_socket_passed_in(listen_pid: Option<&str>, listen_fds: Option<&str>, own_pid: u32) -> std::io::Result<()> {
125 let announced_for: bool = listen_pid.is_some_and(|pid| { return pid == own_pid.to_string(); });
126 let count: i32 = listen_fds.and_then(|text| { return text.parse().ok(); }).unwrap_or(0);
127 if !announced_for || count < 1 {
128 return Err(std::io::Error::other("no socket was passed in"));
129 }
130 return Ok(());
131}
132
133fn bind_path(path: &std::path::Path) -> std::io::Result<UnixListener> {
146 use std::os::unix::fs::FileTypeExt;
147 let first: std::io::Error = match UnixListener::bind(path) {
148 Ok(listener) => return Ok(listener),
149 Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => error,
150 Err(error) => return Err(error),
151 };
152 let is_socket: bool = std::fs::symlink_metadata(path).is_ok_and(|meta| { return meta.file_type().is_socket(); });
153 let refused: bool = matches!(UnixStream::connect(path), Err(error) if error.kind() == std::io::ErrorKind::ConnectionRefused);
154 if !is_socket || !refused {
155 return Err(first);
156 }
157 std::fs::remove_file(path)?;
158 return UnixListener::bind(path);
159}
160
161fn peer_user(stream: &UnixStream) -> Option<u32> {
163 use std::os::fd::AsRawFd;
164 let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
165 let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
166 let status = unsafe {
168 libc::getsockopt(stream.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PEERCRED, (&raw mut credentials).cast(), &raw mut length)
169 };
170 return if status == 0 { Some(credentials.uid) } else { None };
171}
172
173type Queued = (Box<CommitRequest>, UnixStream, Arrival);
178
179type GroupKey = (std::path::PathBuf, String, bool);
184
185fn place_in_groups<T>(groups: &mut Vec<(GroupKey, Vec<T>)>, key: GroupKey, member: T) {
205 let joins = groups.iter_mut().rev().find(|((common, reference, _), _)| { return *common == key.0 && *reference == key.1; });
206 match joins {
207 Some((existing, members)) if !key.2 && !existing.2 => members.push(member),
208 _ => groups.push((key, vec![member])),
209 }
210}
211
212enum Job {
213 Commit(Box<CommitRequest>, UnixStream, Arrival),
214 Retire,
215 Activity,
217 Release(std::path::PathBuf),
220}
221
222fn wait_for_close(stream: &UnixStream) {
224 let mut sink: [u8; 64] = [0; 64];
225 let mut reader = stream;
226 loop {
227 match reader.read(&mut sink) {
228 Ok(0) | Err(_) => return,
229 Ok(_) => {}
230 }
231 }
232}
233
234fn send(stream: &UnixStream, message: &ServiceMessage) {
235 let mut line: String = encode_service(message);
236 line.push('\n');
237 let _written = (&*stream).write_all(line.as_bytes());
239}
240
241fn start_thread_or_exit(what: &str, body: impl FnOnce() + Send + 'static) {
244 if let Err(error) = std::thread::Builder::new().spawn(body) {
245 eprintln!("cli-git-service: cannot start the thread for {what}: {error}");
246 std::process::exit(4);
247 }
248}
249
250const ACCEPT_BACKOFF: Duration = Duration::from_millis(100);
252
253fn handle_connection(stream: UnixStream, queue: &Sender<Job>, work: &Sender<Wake>, busy: &AtomicUsize, release: &str, instance: Option<&str>) {
255 let accepted: Instant = Instant::now();
256 send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(release.to_string()) });
257 if peer_user(&stream) != Some(unsafe { libc::geteuid() }) {
259 return;
260 }
261 let _timeout = stream.set_read_timeout(Some(REQUEST_READ_TIMEOUT));
262 let Ok(reader_stream) = stream.try_clone() else { return };
263 let mut line: String = String::new();
264 let limit: u64 = u64::try_from(MAX_LINE_BYTES).unwrap_or(u64::MAX) + 1;
265 if BufReader::new(reader_stream.take(limit)).read_line(&mut line).is_err() {
266 return;
267 }
268 let _cleared = stream.set_read_timeout(None);
269 if line.is_empty() {
270 return;
272 }
273 let foreign = |git_dir: &std::path::Path| -> bool { return instance.is_some_and(|mine| { return instance_of(git_dir) != mine; }); };
276 match decode_client(line.trim_end_matches('\n')) {
277 Ok(ClientMessage::Commit(request)) if foreign(request.git_dir.as_path()) => {
278 eprintln!("cli-git-service: refused a commit for {}, which another instance serves", request.git_dir.display());
279 send(&stream, &ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::Rejected });
280 }
281 Ok(ClientMessage::Scratch { git_dir, .. } | ClientMessage::Work { git_dir, .. }) if foreign(git_dir.as_path()) => {
282 eprintln!("cli-git-service: refused a message for {}, which another instance serves", git_dir.display());
283 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
284 }
285 Ok(ClientMessage::Commit(request)) => {
286 let _queued = queue.send(Job::Commit(request, stream, Arrival { accepted, read: accepted.elapsed() }));
287 }
288 Ok(ClientMessage::Retire) => {
289 log(retire_line(release).as_str());
290 let _queued = queue.send(Job::Retire);
291 }
292 Ok(ClientMessage::Scratch { git_dir, path }) => {
293 busy.fetch_add(1, Ordering::SeqCst);
296 wait_for_close(&stream);
297 busy.fetch_sub(1, Ordering::SeqCst);
298 let _unused = git_dir;
299 let _queued = queue.send(Job::Release(path));
300 }
301 Ok(ClientMessage::Work { git_dir, review_key }) => {
302 let _queued = work.send((git_dir, review_key));
303 let _touched = queue.send(Job::Activity);
304 }
305 Err(reason) => {
306 eprintln!("cli-git-service: refused a request line: {reason}");
307 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
308 }
309 }
310}
311
312fn settle_open_batches(context: &LandContext, instance: Option<&str>) -> Vec<super::service_land::RecoveryReport> {
314 return match instance {
315 Some(mine) => recover_instance(context, mine),
316 None => recover(context),
317 };
318}
319
320struct BatchTimes {
323 batch: u64,
326 size: usize,
328 group_began: Instant,
331 settle: Duration,
333 landing: LandTiming,
335 land_total: Duration,
338 landed_at: Instant,
341}
342
343impl BatchTimes {
344 fn not_landed(group_began: Instant) -> Self {
346 return Self { batch: 0, size: 0, group_began, settle: Duration::ZERO, landing: LandTiming::default(), land_total: Duration::ZERO, landed_at: Instant::now() };
347 }
348}
349
350fn log_answer(times: &BatchTimes, request: &CommitRequest, arrival: &Arrival, position: usize, outcome: &str) {
354 let queued: Duration = times.group_began.saturating_duration_since(arrival.accepted + arrival.read);
355 log(answered_line(&Answered {
356 request: request.id.as_str(),
357 reference: request.reference.as_str(),
358 outcome,
359 batch: times.batch,
360 size: times.size,
361 position,
362 arrival: *arrival,
363 queued,
364 settle: times.settle,
365 landing: ×.landing,
366 land_total: times.land_total,
367 answer: times.landed_at.elapsed(),
368 total: arrival.accepted.elapsed(),
369 })
370 .as_str());
371}
372
373fn land_group(context: &LandContext, instance: Option<&str>, seq: &mut u64, group: Vec<Queued>, landed: &mut HashMap<String, String>) {
375 let group_began: Instant = Instant::now();
376 let (again, pending): (Vec<_>, Vec<_>) = group.into_iter().partition(|(request, _, _)| { return landed.contains_key(&request.id); });
377 let unlanded: BatchTimes = BatchTimes::not_landed(group_began);
378 for (request, stream, arrival) in again {
379 send(&stream, &ServiceMessage::Landed { id: request.id.clone(), commit: landed[&request.id].clone(), already: true });
380 log_answer(&unlanded, &request, &arrival, 1, "already");
381 }
382 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
384 let (fresh, repeats): (Vec<_>, Vec<_>) = pending.into_iter().partition(|(request, _, _)| { return seen.insert(request.id.clone()); });
385 if fresh.is_empty() {
386 return;
387 }
388 let settling: Instant = Instant::now();
391 let _settled = settle_open_batches(context, instance);
392 let settle: Duration = settling.elapsed();
393 *seq += 1;
394 for (index, (_, stream, _)) in fresh.iter().enumerate().skip(1) {
395 send(stream, &ServiceMessage::Position { number: index + 1, of: fresh.len() });
396 }
397 let requests: Vec<CommitRequest> = fresh.iter().map(|(request, _, _)| { return (**request).clone(); }).collect();
398 let announce = |note: Announce| {
399 for (_, stream, _) in &fresh {
400 match ¬e {
401 Announce::Waiting(text) => send(stream, &ServiceMessage::Waiting { text: text.clone() }),
402 Announce::Holder(who) => send(stream, &ServiceMessage::Holder { who: who.clone() }),
403 }
404 }
405 };
406 let mut landing: LandTiming = LandTiming::default();
407 let landing_began: Instant = Instant::now();
408 let result = land_batch_timed(context, *seq, &requests, &announce, &mut landing);
409 let times: BatchTimes = BatchTimes { batch: *seq, size: fresh.len(), group_began, settle, landing, land_total: landing_began.elapsed(), landed_at: Instant::now() };
410 match result {
411 Ok(outcomes) => {
412 super::service_fault::reach("before-ack");
413 for ((request, _, _), outcome) in fresh.iter().zip(&outcomes) {
414 if let Outcome::Landed { commit } = outcome {
415 for (repeat, stream, arrival) in repeats.iter().filter(|(repeat, _, _)| { return repeat.id == request.id; }) {
416 send(stream, &ServiceMessage::Landed { id: repeat.id.clone(), commit: commit.clone(), already: true });
417 log_answer(×, repeat, arrival, 1, "already");
418 }
419 }
420 }
421 for (position, ((request, stream, arrival), outcome)) in fresh.iter().zip(outcomes).enumerate() {
422 let word: &str = match outcome {
423 Outcome::Landed { commit } => {
424 landed.insert(request.id.clone(), commit.clone());
425 send(stream, &ServiceMessage::Landed { id: request.id.clone(), commit, already: false });
426 "landed"
427 }
428 Outcome::RetryPrepare { base } => {
429 let captures = super::service_captures::remembered(request.common_dir.as_path(), request.repository.as_path());
430 send(stream, &ServiceMessage::RetryPrepare { id: request.id.clone(), base, landed: captures });
431 "retry-prepare"
432 }
433 Outcome::BranchMoved => {
434 send(stream, &ServiceMessage::Failed { id: request.id.clone(), code: FailureCode::BranchMoved });
435 "branch-moved"
436 }
437 };
438 log_answer(×, request, arrival, position + 1, word);
439 }
440 }
441 Err(code) => {
442 for (position, (request, stream, arrival)) in fresh.iter().enumerate() {
443 send(stream, &ServiceMessage::Failed { id: request.id.clone(), code });
444 log_answer(×, request, arrival, position + 1, format!("failed:{}", code.word()).as_str());
445 }
446 }
447 }
448}
449
450fn work_loop(receiver: &Receiver<Wake>, handlers: &GitHandlers, busy: &AtomicUsize) {
463 let mut retries: RetrySchedule = RetrySchedule::default();
464 let mut counted_waiting: bool = false;
465 loop {
466 let first: Option<Wake> = match retries.next_wait(Instant::now()) {
467 None => match receiver.recv() {
468 Ok(wake) => Some(wake),
469 Err(_) => return,
470 },
471 Some(wait) => match receiver.recv_timeout(wait) {
472 Ok(wake) => Some(wake),
473 Err(RecvTimeoutError::Timeout) => None,
474 Err(RecvTimeoutError::Disconnected) => return,
475 },
476 };
477 busy.fetch_add(1, Ordering::SeqCst);
478 let mut wakes: Vec<Wake> = first.into_iter().chain(std::iter::from_fn(|| { return receiver.try_recv().ok(); })).collect();
479 wakes.extend(retries.take_due(Instant::now()).into_iter().map(|directory| { return (directory, None); }));
480 let ran: Vec<PathBuf> = wakes.iter().map(|(directory, _)| { return directory.clone(); }).collect();
481 let asked: Vec<(PathBuf, Duration)> = run_round(wakes, handlers, &ClaimTime::read());
482 retries.replace(ran.as_slice(), asked, Instant::now());
483 if !retries.is_empty() && !counted_waiting {
485 busy.fetch_add(1, Ordering::SeqCst);
486 counted_waiting = true;
487 } else if retries.is_empty() && counted_waiting {
488 busy.fetch_sub(1, Ordering::SeqCst);
489 counted_waiting = false;
490 }
491 busy.fetch_sub(1, Ordering::SeqCst);
492 }
493}
494
495pub fn serve(listener: UnixListener, context: &LandContext, settings: &ServeSettings) {
506 let serving_since: Instant = Instant::now();
507 let idle: Duration = settings.idle;
508 let _reports = settle_open_batches(context, settings.instance.as_deref());
509 let pruned: usize = prune_done(context.journal.as_path(), DONE_RETAINED);
511 if pruned > 0 {
512 eprintln!("cli-git-service: removed the records of {pruned} old finished batches from {}", context.journal.display());
513 }
514 let mut landed: HashMap<String, String> = landed_commits(context.journal.as_path());
515 let (sender, receiver): (Sender<Job>, Receiver<Job>) = channel();
516 let acceptor_queue: Sender<Job> = sender.clone();
517 let (work_sender, work_receiver): (Sender<Wake>, Receiver<Wake>) = channel();
519 let handlers: GitHandlers = GitHandlers::new(context.git.clone());
520 let busy: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
521 let worker_busy: Arc<AtomicUsize> = Arc::clone(&busy);
522 start_thread_or_exit("the after-commit work", move || { work_loop(&work_receiver, &handlers, &worker_busy); });
523 let announced_release: String = settings.release.clone();
524 let connection_busy: Arc<AtomicUsize> = Arc::clone(&busy);
525 let instance: Option<String> = settings.instance.clone();
526 start_thread_or_exit("the acceptor of connections", move || {
527 for connection in listener.incoming().flatten() {
528 let queue: Sender<Job> = acceptor_queue.clone();
529 let work: Sender<Wake> = work_sender.clone();
530 let release: String = announced_release.clone();
531 let counted: Arc<AtomicUsize> = Arc::clone(&connection_busy);
532 let mine: Option<String> = instance.clone();
533 let notice: Option<UnixStream> = connection.try_clone().ok();
536 let spawned = std::thread::Builder::new().spawn(move || { handle_connection(connection, &queue, &work, &counted, release.as_str(), mine.as_deref()); });
537 if let Err(error) = spawned {
538 eprintln!("cli-git-service: no thread for a connection, which is closed after telling the client: {error}");
539 if let Some(stream) = notice {
540 send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION, release: Some(announced_release.clone()) });
541 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Unavailable });
542 }
543 std::thread::sleep(ACCEPT_BACKOFF);
545 }
546 }
547 });
548 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); });
550 let mut retiring: bool = false;
551 while !retiring {
552 let first: Job = match receiver.recv_timeout(idle) {
553 Ok(job) => job,
554 Err(RecvTimeoutError::Timeout) if busy.load(Ordering::SeqCst) > 0 => continue,
556 Err(RecvTimeoutError::Timeout) => {
557 log(exit_line(ExitReason::Idle, serving_since.elapsed()).as_str());
558 return;
559 }
560 Err(RecvTimeoutError::Disconnected) => {
561 log(exit_line(ExitReason::Error, serving_since.elapsed()).as_str());
562 return;
563 }
564 };
565 let mut jobs: Vec<Job> = vec![first];
566 while jobs.len() < MAX_BATCH {
567 match receiver.try_recv() {
568 Ok(job) => jobs.push(job),
569 Err(_) => break,
570 }
571 }
572 let mut groups: Vec<(GroupKey, Vec<Queued>)> = Vec::new();
574 let mut releases: Vec<std::path::PathBuf> = Vec::new();
575 for job in jobs {
576 match job {
577 Job::Retire => retiring = true,
578 Job::Activity => {}
579 Job::Release(path) => releases.push(path),
580 Job::Commit(request, stream, arrival) => {
581 let queue_reference: String = if request.reference == "HEAD" {
585 format!("HEAD@{}", request.repository.display())
586 } else {
587 request.reference.clone()
588 };
589 let key: GroupKey = (request.common_dir.clone(), queue_reference, request.lands_alone());
590 place_in_groups(&mut groups, key, (request, stream, arrival));
591 }
592 }
593 }
594 for (_, group) in groups {
595 land_group(context, settings.instance.as_deref(), &mut seq, group, &mut landed);
596 }
597 if unsettled_locks() > 0 {
599 eprintln!("cli-git-service: ending, because an index lock could not be settled; the next connection starts the service again and its recovery reclaims the lock");
600 return;
601 }
602 for path in releases {
603 if let Err(error) = remove_tree(path.as_path()) {
604 eprintln!("cli-git-service: the scratch directory {} could not be removed: {error}", path.display());
605 }
606 }
607 }
608 log(exit_line(ExitReason::Retire, serving_since.elapsed()).as_str());
609}
610
611#[cfg(test)]
613#[path = "service_server_tests.rs"]
614mod tests;