1use super::service_journal::landed_commits;
16use super::service_land::{LandContext, Outcome, land_batch, recover};
17use super::service_work::{GitHandlers, drain};
18use super::service_protocol::{
19 ClientMessage, CommitRequest, FailureCode, MAX_LINE_BYTES, PROTOCOL_VERSION, ServiceMessage, decode_client, encode_service,
20};
21use std::collections::HashMap;
22use std::io::{BufRead, BufReader, Read, Write};
23use std::os::unix::net::{UnixListener, UnixStream};
24use std::sync::atomic::{AtomicUsize, Ordering};
25use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
26use std::sync::Arc;
27use std::time::Duration;
28
29pub const MAX_BATCH: usize = 64;
31
32pub const REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(10);
34
35pub const IDLE_EXIT: Duration = Duration::from_secs(3 * 60 * 60);
38
39pub const INHERITED_DESCRIPTOR: i32 = 3;
41
42#[derive(Clone, Debug, Eq, PartialEq)]
44pub enum Bind {
45 Abstract(String),
47 Path(std::path::PathBuf),
49 Inherited,
51}
52
53pub fn bind(address: &Bind) -> std::io::Result<UnixListener> {
63 return match address {
64 #[cfg(target_os = "linux")]
65 Bind::Abstract(name) => {
66 use std::os::linux::net::SocketAddrExt;
67 let socket = std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())?;
68 UnixListener::bind_addr(&socket)
69 }
70 #[cfg(not(target_os = "linux"))]
71 Bind::Abstract(_) => Err(std::io::Error::other("abstract sockets need Linux")),
72 Bind::Path(path) => UnixListener::bind(path),
73 Bind::Inherited => {
74 use std::os::fd::FromRawFd;
75 let announced_for: bool = std::env::var("LISTEN_PID").is_ok_and(|pid| { return pid == std::process::id().to_string(); });
76 let count: i32 = std::env::var("LISTEN_FDS").ok().and_then(|text| { return text.parse().ok(); }).unwrap_or(0);
77 if !announced_for || count < 1 {
78 return Err(std::io::Error::other("no socket was passed in"));
79 }
80 Ok(unsafe { UnixListener::from_raw_fd(INHERITED_DESCRIPTOR) })
82 }
83 };
84}
85
86fn peer_user(stream: &UnixStream) -> Option<u32> {
88 use std::os::fd::AsRawFd;
89 let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
90 let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
91 let status = unsafe {
93 libc::getsockopt(stream.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PEERCRED, (&raw mut credentials).cast(), &raw mut length)
94 };
95 return if status == 0 { Some(credentials.uid) } else { None };
96}
97
98enum Job {
99 Commit(Box<CommitRequest>, UnixStream),
100 Retire,
101 Activity,
103}
104
105fn send(stream: &UnixStream, message: &ServiceMessage) {
106 let mut line: String = encode_service(message);
107 line.push('\n');
108 let _written = (&*stream).write_all(line.as_bytes());
110}
111
112fn handle_connection(stream: UnixStream, queue: &Sender<Job>, work: &Sender<std::path::PathBuf>) {
114 send(&stream, &ServiceMessage::Welcome { version: PROTOCOL_VERSION });
115 if peer_user(&stream) != Some(unsafe { libc::geteuid() }) {
117 return;
118 }
119 let _timeout = stream.set_read_timeout(Some(REQUEST_READ_TIMEOUT));
120 let Ok(reader_stream) = stream.try_clone() else { return };
121 let mut line: String = String::new();
122 let limit: u64 = u64::try_from(MAX_LINE_BYTES).unwrap_or(u64::MAX) + 1;
123 if BufReader::new(reader_stream.take(limit)).read_line(&mut line).is_err() {
124 return;
125 }
126 let _cleared = stream.set_read_timeout(None);
127 if line.is_empty() {
128 return;
130 }
131 match decode_client(line.trim_end_matches('\n')) {
132 Ok(ClientMessage::Commit(request)) => {
133 let _queued = queue.send(Job::Commit(request, stream));
134 }
135 Ok(ClientMessage::Retire) => {
136 let _queued = queue.send(Job::Retire);
137 }
138 Ok(ClientMessage::Work { common_dir }) => {
139 let _queued = work.send(common_dir);
140 let _touched = queue.send(Job::Activity);
141 }
142 Err(reason) => {
143 eprintln!("cli-git-service: refused a request line: {reason}");
144 send(&stream, &ServiceMessage::Failed { id: String::new(), code: FailureCode::Rejected });
145 }
146 }
147}
148
149fn land_group(context: &LandContext, seq: &mut u64, group: Vec<(Box<CommitRequest>, UnixStream)>, landed: &mut HashMap<String, String>) {
151 let (again, pending): (Vec<_>, Vec<_>) = group.into_iter().partition(|(request, _)| { return landed.contains_key(&request.id); });
152 for (request, stream) in again {
153 send(&stream, &ServiceMessage::Landed { id: request.id.clone(), commit: landed[&request.id].clone(), already: true });
154 }
155 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
157 let (fresh, repeats): (Vec<_>, Vec<_>) = pending.into_iter().partition(|(request, _)| { return seen.insert(request.id.clone()); });
158 if fresh.is_empty() {
159 return;
160 }
161 *seq += 1;
162 for (index, (_, stream)) in fresh.iter().enumerate().skip(1) {
163 send(stream, &ServiceMessage::Position { number: index + 1, of: fresh.len() });
164 }
165 let requests: Vec<CommitRequest> = fresh.iter().map(|(request, _)| { return (**request).clone(); }).collect();
166 match land_batch(context, *seq, &requests) {
167 Ok(outcomes) => {
168 super::service_fault::reach("before-ack");
169 for ((request, _), outcome) in fresh.iter().zip(&outcomes) {
170 if let Outcome::Landed { commit } = outcome {
171 for (repeat, stream) in repeats.iter().filter(|(repeat, _)| { return repeat.id == request.id; }) {
172 send(stream, &ServiceMessage::Landed { id: repeat.id.clone(), commit: commit.clone(), already: true });
173 }
174 }
175 }
176 for ((request, stream), outcome) in fresh.iter().zip(outcomes) {
177 match outcome {
178 Outcome::Landed { commit } => {
179 landed.insert(request.id.clone(), commit.clone());
180 send(stream, &ServiceMessage::Landed { id: request.id.clone(), commit, already: false });
181 }
182 Outcome::RetryPrepare { base } => send(stream, &ServiceMessage::RetryPrepare { id: request.id.clone(), base }),
183 }
184 }
185 }
186 Err(code) => {
187 for (request, stream) in &fresh {
188 send(stream, &ServiceMessage::Failed { id: request.id.clone(), code });
189 }
190 }
191 }
192}
193
194fn work_loop(receiver: &Receiver<std::path::PathBuf>, handlers: &GitHandlers, busy: &AtomicUsize) {
205 while let Ok(first) = receiver.recv() {
206 busy.fetch_add(1, Ordering::SeqCst);
207 let mut directories: Vec<std::path::PathBuf> = vec![first];
208 while let Ok(more) = receiver.try_recv() {
209 if !directories.contains(&more) {
210 directories.push(more);
211 }
212 }
213 for directory in directories {
214 drain(directory.as_path(), handlers, std::time::SystemTime::now());
215 }
216 busy.fetch_sub(1, Ordering::SeqCst);
217 }
218}
219
220pub fn serve(listener: UnixListener, context: &LandContext, idle: Duration) {
231 let _reports = recover(context);
232 let mut landed: HashMap<String, String> = landed_commits(context.journal.as_path());
233 let (sender, receiver): (Sender<Job>, Receiver<Job>) = channel();
234 let acceptor_queue: Sender<Job> = sender.clone();
235 let (work_sender, work_receiver): (Sender<std::path::PathBuf>, Receiver<std::path::PathBuf>) = channel();
237 let handlers: GitHandlers = GitHandlers { context: context.git.clone() };
238 let busy: Arc<AtomicUsize> = Arc::new(AtomicUsize::new(0));
239 let worker_busy: Arc<AtomicUsize> = Arc::clone(&busy);
240 std::thread::spawn(move || { work_loop(&work_receiver, &handlers, &worker_busy); });
241 std::thread::spawn(move || {
242 for connection in listener.incoming().flatten() {
243 let queue: Sender<Job> = acceptor_queue.clone();
244 let work: Sender<std::path::PathBuf> = work_sender.clone();
245 std::thread::spawn(move || { handle_connection(connection, &queue, &work); });
246 }
247 });
248 let mut seq: u64 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(1, |elapsed| { return u64::try_from(elapsed.as_millis()).unwrap_or(1); });
249 let mut retiring: bool = false;
250 while !retiring {
251 let first: Job = match receiver.recv_timeout(idle) {
252 Ok(job) => job,
253 Err(RecvTimeoutError::Timeout) if busy.load(Ordering::SeqCst) > 0 => continue,
255 Err(_) => return,
256 };
257 let mut jobs: Vec<Job> = vec![first];
258 while jobs.len() < MAX_BATCH {
259 match receiver.try_recv() {
260 Ok(job) => jobs.push(job),
261 Err(_) => break,
262 }
263 }
264 let mut groups: Vec<((std::path::PathBuf, String), Vec<(Box<CommitRequest>, UnixStream)>)> = Vec::new();
266 for job in jobs {
267 match job {
268 Job::Retire => retiring = true,
269 Job::Activity => {}
270 Job::Commit(request, stream) => {
271 let key = (request.common_dir.clone(), request.reference.clone());
272 match groups.iter_mut().find(|(existing, _)| { return *existing == key; }) {
273 Some((_, members)) => members.push((request, stream)),
274 None => groups.push((key, vec![(request, stream)])),
275 }
276 }
277 }
278 }
279 for (_, group) in groups {
280 land_group(context, &mut seq, group, &mut landed);
281 }
282 }
283}