node: Use `git-daemon` as backend

This commit is contained in:
Alexis Sellier 2023-01-29 14:05:58 +01:00
parent d9e1055067
commit 02be334144
No known key found for this signature in database
5 changed files with 172 additions and 71 deletions

View File

@ -1,3 +1,4 @@
use std::io::{BufRead, BufReader};
use std::os::unix::net::UnixListener; use std::os::unix::net::UnixListener;
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs, io, net, thread, time}; use std::{fs, io, net, thread, time};
@ -66,13 +67,15 @@ pub struct Runtime<G: Signer + EcSign> {
pub home: Home, pub home: Home,
pub handle: Handle<G>, pub handle: Handle<G>,
pub control: thread::JoinHandle<Result<(), control::Error>>, pub control: thread::JoinHandle<Result<(), control::Error>>,
pub storage: Storage,
pub reactor: Reactor<wire::Control<G>>, pub reactor: Reactor<wire::Control<G>>,
pub daemon: net::SocketAddr,
pub pool: WorkerPool, pub pool: WorkerPool,
pub local_addrs: Vec<net::SocketAddr>, pub local_addrs: Vec<net::SocketAddr>,
} }
impl<G: Signer + EcSign> Runtime<G> { impl<G: Signer + EcSign> Runtime<G> {
/// Run the client. /// Initialize the runtime.
/// ///
/// This function spawns threads. /// This function spawns threads.
pub fn with( pub fn with(
@ -80,6 +83,7 @@ impl<G: Signer + EcSign> Runtime<G> {
config: service::Config, config: service::Config,
listen: Vec<net::SocketAddr>, listen: Vec<net::SocketAddr>,
proxy: net::SocketAddr, proxy: net::SocketAddr,
daemon: net::SocketAddr,
signer: G, signer: G,
) -> Result<Runtime<G>, Error> ) -> Result<Runtime<G>, Error>
where where
@ -140,6 +144,8 @@ impl<G: Signer + EcSign> Runtime<G> {
log::info!(target: "node", "Binding control socket {}..", node_sock.display()); log::info!(target: "node", "Binding control socket {}..", node_sock.display());
// TODO: Move this stuff to `run` function.
let listener = match UnixListener::bind(&node_sock) { let listener = match UnixListener::bind(&node_sock) {
Ok(sock) => sock, Ok(sock) => sock,
Err(err) if err.kind() == io::ErrorKind::AddrInUse => { Err(err) if err.kind() == io::ErrorKind::AddrInUse => {
@ -157,7 +163,8 @@ impl<G: Signer + EcSign> Runtime<G> {
let pool = WorkerPool::with( let pool = WorkerPool::with(
8, 8,
time::Duration::from_secs(9), time::Duration::from_secs(9),
storage, storage.clone(),
daemon,
worker_recv, worker_recv,
handle.clone(), handle.clone(),
id.to_human(), id.to_human(),
@ -167,7 +174,9 @@ impl<G: Signer + EcSign> Runtime<G> {
id, id,
home, home,
control, control,
storage,
reactor, reactor,
daemon,
handle, handle,
pool, pool,
local_addrs, local_addrs,
@ -176,11 +185,25 @@ impl<G: Signer + EcSign> Runtime<G> {
pub fn run(self) -> Result<(), Error> { pub fn run(self) -> Result<(), Error> {
log::info!(target: "node", "Running node {}..", self.id); log::info!(target: "node", "Running node {}..", self.id);
log::info!(target: "node", "Spawning git daemon at {}..", self.storage.path().display());
let mut daemon = daemon::spawn(self.storage.path(), self.daemon)?;
thread::Builder::new().name(self.id.to_human()).spawn({
let stderr = daemon.stderr.take().unwrap();
|| {
for line in BufReader::new(stderr).lines().flatten() {
log::debug!(target: "daemon", "{line}");
}
}
})?;
self.pool.run().unwrap(); self.pool.run().unwrap();
self.reactor.join().unwrap(); self.reactor.join().unwrap();
self.control.join().unwrap()?; self.control.join().unwrap()?;
daemon.kill().ok(); // Ignore error if daemon has already exited, for whatever reason.
daemon.wait()?;
fs::remove_file(self.home.socket()).ok(); fs::remove_file(self.home.socket()).ok();
log::debug!(target: "node", "Node shutdown completed for {}", self.id); log::debug!(target: "node", "Node shutdown completed for {}", self.id);
@ -188,3 +211,41 @@ impl<G: Signer + EcSign> Runtime<G> {
Ok(()) Ok(())
} }
} }
pub mod daemon {
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::{env, io, net};
pub fn spawn(storage: &Path, addr: net::SocketAddr) -> io::Result<Child> {
let storage = storage.canonicalize()?;
let listen = format!("--listen={}", addr.ip());
let port = format!("--port={}", addr.port());
let child = Command::new("git")
.env_clear()
.envs(env::vars().filter(|(k, _)| k == "PATH" || k.starts_with("GIT")))
.env("GIT_PROTOCOL", "version=2")
.current_dir(storage)
.arg("daemon")
// Make all git directories available.
.arg("--export-all")
.arg("--verbose")
// The git "root". Should be our storage path.
.arg("--base-path=.")
// Timeout (in seconds) between the moment the connection is established
// and the client request is received (typically a rather low value,
// since that should be basically immediate).
.arg("--init-timeout=3")
// Timeout (in seconds) for specific client sub-requests.
// This includes the time it takes for the server to process the sub-request
// and the time spent waiting for the next clients request.
.arg("--timeout=9")
.arg("--log-destination=stderr")
.arg(listen)
.arg(port)
.stderr(Stdio::piped())
.spawn()?;
Ok(child)
}
}

View File

@ -35,8 +35,6 @@ pub fn listen<H: Handle<Error = client::handle::Error>>(
log::debug!(target: "control", "Accepted new client on control socket.."); log::debug!(target: "control", "Accepted new client on control socket..");
if let Err(e) = drain(&stream, &mut handle) { if let Err(e) = drain(&stream, &mut handle) {
log::debug!(target: "control", "Received {} on control socket", e);
if let DrainError::Shutdown = e { if let DrainError::Shutdown = e {
log::debug!(target: "control", "Shutdown requested.."); log::debug!(target: "control", "Shutdown requested..");
// Channel might already be disconnected if shutdown // Channel might already be disconnected if shutdown

View File

@ -90,7 +90,8 @@ fn execute() -> anyhow::Result<()> {
..service::Config::default() ..service::Config::default()
}; };
let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050); let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050);
let runtime = Runtime::with(home, config, options.listen, proxy, signer)?; let daemon = ([0, 0, 0, 0], 9418).into();
let runtime = Runtime::with(home, config, options.listen, proxy, daemon, signer)?;
runtime.run()?; runtime.run()?;

View File

@ -104,7 +104,16 @@ impl Node {
fn spawn(self, config: service::Config) -> NodeHandle { fn spawn(self, config: service::Config) -> NodeHandle {
let listen = vec![([0, 0, 0, 0], 0).into()]; let listen = vec![([0, 0, 0, 0], 0).into()];
let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050); let proxy = net::SocketAddr::new(net::Ipv4Addr::LOCALHOST.into(), 9050);
let rt = Runtime::with(self.home, config, listen, proxy, self.signer.clone()).unwrap(); let daemon = ([0, 0, 0, 0], fastrand::u16(1025..)).into();
let rt = Runtime::with(
self.home,
config,
listen,
proxy,
daemon,
self.signer.clone(),
)
.unwrap();
let addr = *rt.local_addrs.first().unwrap(); let addr = *rt.local_addrs.first().unwrap();
let id = *self.signer.public_key(); let id = *self.signer.public_key();
let handle = ManuallyDrop::new(rt.handle.clone()); let handle = ManuallyDrop::new(rt.handle.clone());
@ -400,9 +409,7 @@ fn test_clone() {
match lookup { match lookup {
// Drain the channel. // Drain the channel.
FetchLookup::Found { seeds, results } => for _ in results.iter().take(seeds.len()) {}, FetchLookup::Found { seeds, results } => for _ in results.iter().take(seeds.len()) {},
other => { other => panic!("Unexpected fetch lookup: {:?}", other),
panic!("Unexpected fetch lookup: {:?}", other);
}
} }
rad::fork(acme, &alice.signer, &alice.storage).unwrap(); rad::fork(acme, &alice.signer, &alice.storage).unwrap();
@ -429,3 +436,34 @@ fn test_clone() {
assert_eq!(oid, *canonical); assert_eq!(oid, *canonical);
} }
#[test]
fn test_fetch_up_to_date() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let alice = Node::new(tmp.path());
let mut bob = Node::new(tmp.path());
let acme = bob.project("acme");
let mut alice = alice.spawn(service::Config::default());
let bob = bob.spawn(service::Config::default());
alice.connect(&bob);
converge([&alice, &bob]);
transport::local::register(alice.storage.clone());
let _ = alice.handle.track_repo(acme).unwrap();
match alice.handle.fetch(acme).unwrap() {
FetchLookup::Found { seeds, results } => for _ in results.iter().take(seeds.len()) {},
other => panic!("Unexpected fetch lookup: {:?}", other),
}
// Fetch again! This time, everything's up to date.
match alice.handle.fetch(acme).unwrap() {
FetchLookup::Found { seeds, results } => for _ in results.iter().take(seeds.len()) {},
other => panic!("Unexpected fetch lookup: {:?}", other),
}
}

View File

@ -35,6 +35,7 @@ pub struct WorkerResp<G: Signer + EcSign> {
struct Worker<G: Signer + EcSign> { struct Worker<G: Signer + EcSign> {
storage: Storage, storage: Storage,
tasks: chan::Receiver<WorkerReq<G>>, tasks: chan::Receiver<WorkerReq<G>>,
daemon: net::SocketAddr,
timeout: time::Duration, timeout: time::Duration,
handle: Handle<G>, handle: Handle<G>,
name: String, name: String,
@ -89,8 +90,11 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
Err((session, err)) => return (session, Err(err.into())), Err((session, err)) => return (session, Err(err.into())),
}; };
let result = self.fetch(fetch, &mut tunnel); let result = self.fetch(fetch, &mut tunnel);
let session = tunnel.into_session(); let mut session = tunnel.into_session();
if let Err(err) = pktline::flush(&mut session) {
log::error!(target: "worker", "Fetch error: {err}");
}
if let Err(err) = &result { if let Err(err) = &result {
log::error!(target: "worker", "Fetch error: {err}"); log::error!(target: "worker", "Fetch error: {err}");
} }
@ -132,7 +136,7 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
.arg("fetch") .arg("fetch")
.arg("--atomic") // FIXME: Not available on 2.30 (debian standard) .arg("--atomic") // FIXME: Not available on 2.30 (debian standard)
.arg("--verbose") .arg("--verbose")
.arg(format!("git://{tunnel_addr}/{}", repo.id)) .arg(format!("git://{tunnel_addr}/{}", repo.id.canonical()))
// FIXME: We need to omit our own namespace from this refspec in case we're fetching '*'. // FIXME: We need to omit our own namespace from this refspec in case we're fetching '*'.
.arg(fetch.namespaces.as_fetchspec()) .arg(fetch.namespaces.as_fetchspec())
.stdout(process::Stdio::piped()) .stdout(process::Stdio::piped())
@ -172,77 +176,52 @@ impl<G: Signer + EcSign + 'static> Worker<G> {
stream_r: &mut WireReader, stream_r: &mut WireReader,
stream_w: &mut WireWriter<G>, stream_w: &mut WireWriter<G>,
) -> Result<Vec<RefUpdate>, FetchError> { ) -> Result<Vec<RefUpdate>, FetchError> {
let repo = self.storage.repository(fetch.repo)?; let daemon = net::TcpStream::connect_timeout(&self.daemon, self.timeout)?;
let mut child = process::Command::new("git") let (mut daemon_r, mut daemon_w) = (daemon.try_clone().unwrap(), daemon);
.current_dir(repo.path()) let mut stream_reader = pktline::GitReader::new(drain, stream_r);
.env_clear() let mut daemon_reader = pktline::GitReader::new(vec![], &mut daemon_r);
.envs(env::vars().filter(|(k, _)| k == "PATH" || k.starts_with("GIT_TRACE"))) let mut buffer = [0; u16::MAX as usize + 1];
.args(["-c", "protocol.version=2"])
.arg("upload-pack")
.arg("--strict") // The path to the git repo must be exact.
.arg(".")
.stdout(process::Stdio::piped())
.stderr(process::Stdio::piped())
.stdin(process::Stdio::piped())
.spawn()?;
let mut stdin = child.stdin.take().unwrap(); let request = match stream_reader.read_request_pkt_line() {
let mut stdout = child.stdout.take().unwrap(); Ok((req, pktline)) => {
let mut reader = pktline::GitReader::new(drain, stream_r);
let stderr = child.stderr.take().unwrap();
thread::Builder::new().name(self.name.clone()).spawn(|| {
for line in BufReader::new(stderr).lines().flatten() {
log::error!(target: "worker", "Git: {}", line);
}
})?;
match reader.read_command_pkt_line() {
Ok((cmd, _pktline)) => {
log::debug!( log::debug!(
target: "worker", target: "worker",
"Parsed git command packet-line for {}: {:?}", fetch.repo, cmd "Parsed git command packet-line for {}: {:?}", fetch.repo, req
); );
if cmd.repo != fetch.repo { if req.repo != fetch.repo {
return Err(FetchError::Git(git::raw::Error::from_str( return Err(FetchError::Git(git::raw::Error::from_str(
"git pkt-line command does not match fetch request", "git pkt-line command does not match fetch request",
))); )));
} }
pktline
} }
Err(err) => { Err(err) => {
return Err(FetchError::Git(git::raw::Error::from_str(&format!( return Err(FetchError::Git(git::raw::Error::from_str(&format!(
"error parsing git command packet-line: {err}" "error parsing git command packet-line: {err}"
)))); ))));
} }
} };
daemon_w.write_all(&request)?;
thread::scope::<_, Result<Vec<RefUpdate>, FetchError>>(|scope| { loop {
// Output of `upload-pack` is sent back to the remote peer. if let Err(e) = daemon_reader.read_pkt_lines(stream_w, &mut buffer) {
let outgoing = scope.spawn(move || io::copy(&mut stdout, stream_w)); // This is the expected error when the remote disconnects.
if e.kind() == io::ErrorKind::UnexpectedEof {
let mut buf = [0; 65536];
// Data coming from the remote peer is written to the standard input of the
// `upload-pack` process.
while !outgoing.is_finished() {
let n = reader.read_pkt_line(&mut buf)?;
stdin.write_all(&buf[..n]).unwrap();
log::trace!(target: "worker", "Received {:?}", String::from_utf8_lossy(&buf[..n]));
if &buf[..n] == pktline::DONE {
break; break;
} }
} log::debug!(target: "worker", "Upload of {} to {} returned error: {e}", fetch.repo, fetch.remote);
// SAFETY: The thread should not panic, but if it does, we bubble up the panic.
outgoing.join().unwrap()?;
if child.wait()?.success() { return Err(e.into());
log::debug!(target: "worker", "Upload-pack for {} exited successfully", fetch.repo);
} else {
log::error!(target: "worker", "Upload-pack for {} exited with error", fetch.repo);
} }
Ok(vec![]) if let Err(e) = stream_reader.read_pkt_lines(&mut daemon_w, &mut buffer) {
}) log::debug!(target: "worker", "Remote returned error: {e}");
break;
}
}
log::debug!(target: "worker", "Upload of {} to {} exited successfully", fetch.repo, fetch.remote);
// TODO: Don't return anything when uploading.
Ok(vec![])
} }
} }
@ -257,6 +236,7 @@ impl WorkerPool {
capacity: usize, capacity: usize,
timeout: time::Duration, timeout: time::Duration,
storage: Storage, storage: Storage,
daemon: net::SocketAddr,
tasks: chan::Receiver<WorkerReq<G>>, tasks: chan::Receiver<WorkerReq<G>>,
handle: Handle<G>, handle: Handle<G>,
name: String, name: String,
@ -266,6 +246,7 @@ impl WorkerPool {
let worker = Worker { let worker = Worker {
tasks: tasks.clone(), tasks: tasks.clone(),
storage: storage.clone(), storage: storage.clone(),
daemon,
handle: handle.clone(), handle: handle.clone(),
timeout, timeout,
name: name.clone(), name: name.clone(),
@ -306,7 +287,10 @@ mod pktline {
pub const FLUSH_PKT: &[u8; HEADER_LEN] = b"0000"; pub const FLUSH_PKT: &[u8; HEADER_LEN] = b"0000";
pub const DELIM_PKT: &[u8; HEADER_LEN] = b"0001"; pub const DELIM_PKT: &[u8; HEADER_LEN] = b"0001";
pub const RESPONSE_END_PKT: &[u8; HEADER_LEN] = b"0002"; pub const RESPONSE_END_PKT: &[u8; HEADER_LEN] = b"0002";
pub const DONE: &[u8] = b"0009done\n";
pub fn flush<W: io::Write>(w: &mut W) -> io::Result<()> {
write!(w, "0000")
}
pub struct GitReader<'a, R> { pub struct GitReader<'a, R> {
drain: Vec<u8>, drain: Vec<u8>,
@ -318,16 +302,16 @@ mod pktline {
Self { drain, stream } Self { drain, stream }
} }
/// Parse a Git command packet-line. /// Parse a Git request packet-line.
/// ///
/// Example: `0032git-upload-pack /project.git\0host=myserver.com\0` /// Example: `0032git-upload-pack /project.git\0host=myserver.com\0`
/// ///
pub fn read_command_pkt_line(&mut self) -> io::Result<(GitCommand, Vec<u8>)> { pub fn read_request_pkt_line(&mut self) -> io::Result<(GitRequest, Vec<u8>)> {
let mut pktline = [0u8; 1024]; let mut pktline = [0u8; 1024];
let length = self.read_pkt_line(&mut pktline)?; let length = self.read_pkt_line(&mut pktline)?;
let Some(cmd) = GitCommand::parse(&pktline[4..length]) else { let Some(cmd) = GitRequest::parse(&pktline[4..length]) else {
return Err(io::ErrorKind::InvalidInput.into()); return Err(io::ErrorKind::InvalidInput.into());
}; };
Ok((cmd, Vec::from(&pktline[..length]))) Ok((cmd, Vec::from(&pktline[..length])))
} }
@ -350,6 +334,25 @@ mod pktline {
Ok(length) Ok(length)
} }
pub fn read_pkt_lines<W: io::Write>(
&mut self,
w: &mut W,
buf: &mut [u8],
) -> io::Result<()> {
loop {
let n = self.read_pkt_line(buf)?;
if n == 0 {
break;
}
w.write_all(&buf[..n])?;
if &buf[..n] == FLUSH_PKT {
return Ok(());
}
}
Ok(())
}
} }
impl<'a, R: io::Read> io::Read for GitReader<'a, R> { impl<'a, R: io::Read> io::Read for GitReader<'a, R> {
@ -366,14 +369,14 @@ mod pktline {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct GitCommand { pub struct GitRequest {
pub repo: Id, pub repo: Id,
pub path: String, pub path: String,
pub host: Option<(String, Option<u16>)>, pub host: Option<(String, Option<u16>)>,
pub extra: Vec<(String, Option<String>)>, pub extra: Vec<(String, Option<String>)>,
} }
impl GitCommand { impl GitRequest {
/// Parse a Git command from a packet-line. /// Parse a Git command from a packet-line.
fn parse(input: &[u8]) -> Option<Self> { fn parse(input: &[u8]) -> Option<Self> {
let input = str::from_utf8(input).ok()?; let input = str::from_utf8(input).ok()?;