Skip to main content

mj_controller/
hel_git_proxy.rs

1//! Authenticated, path-confined Git smart-protocol bridge for local bundles.
2
3use std::collections::BTreeMap;
4use std::fs::{File, OpenOptions, TryLockError};
5use std::future::Future;
6use std::io::Write as _;
7use std::path::{Path, PathBuf};
8use std::process::Stdio;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{Duration, Instant};
11
12use anyhow::{Context, Result, anyhow, bail, ensure};
13use serde::{Deserialize, Serialize};
14use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
15use tokio::process::Command;
16use tokio_util::sync::CancellationToken;
17
18use hel::hel_local_git::canonical_repository;
19use hel::hel_targets::CommandSpec;
20
21const BRIDGE_MAGIC: &[u8] = b"HEL-GIT-BRIDGE-2\n";
22const MAX_FRAME: usize = 1024 * 1024;
23const MAX_OPEN: usize = 16 * 1024;
24/// How long a broker waits for its target bridge to exit once the frame
25/// stream between them has closed.
26const BRIDGE_EXIT_GRACE: Duration = Duration::from_secs(5);
27/// How long a client may take to name its repository and service.
28///
29/// The proxy writes its open line as soon as it connects, so a connection that
30/// is still silent this much later is one that will never speak. Exchanges are
31/// served one at a time, so without this deadline such a client holds the
32/// session's whole bridge.
33#[cfg(unix)]
34const HANDSHAKE_DEADLINE: Duration = Duration::from_secs(30);
35/// How long one exchange may move nothing in either direction before the
36/// bridge gives up on it.
37///
38/// The deadline is idle rather than total because a legitimate transfer can
39/// run for a very long time while still making progress — a huge clone is slow
40/// but never silent. Only the longest legitimately quiet phase of a Git
41/// service has to fit inside the window: counting and compressing objects
42/// before the first pack byte, or indexing a pushed pack before the report.
43/// Five minutes covers that for very large repositories while still turning a
44/// wedged client into a reported failure in minutes instead of never.
45const EXCHANGE_IDLE_DEADLINE: Duration = Duration::from_secs(300);
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct GitBrokerSpec {
50    pub session_id: String,
51    pub bridge: CommandSpec,
52    pub repositories: BTreeMap<String, PathBuf>,
53    pub ready_path: PathBuf,
54    pub pid_path: PathBuf,
55}
56
57#[derive(Debug, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59struct GitOpen {
60    repository: String,
61    service: String,
62}
63
64impl GitBrokerSpec {
65    pub fn write(&self, path: &Path) -> Result<()> {
66        let parent = path.parent().unwrap_or_else(|| Path::new("."));
67        std::fs::create_dir_all(parent)?;
68        hel::hel_config::atomic_write(path, &serde_json::to_vec_pretty(self)?)
69    }
70
71    pub fn read(path: &Path) -> Result<Self> {
72        let bytes = std::fs::read(path)
73            .with_context(|| format!("read Git broker spec {}", path.display()))?;
74        serde_json::from_slice(&bytes)
75            .with_context(|| format!("parse Git broker spec {}", path.display()))
76    }
77}
78
79/// What a PID file says about the broker that wrote it.
80enum BrokerLock {
81    /// A broker holds the lock; the file names its process when it is
82    /// readable, which it is for every broker past its own startup.
83    Held(Option<i32>),
84    /// Nobody holds the lock, so no broker is serving this session.
85    Free,
86}
87
88/// Read a broker's PID file through the lock its owner holds.
89///
90/// A live broker holds an exclusive advisory lock on its PID file for as long
91/// as it runs, so liveness is the lock rather than the number written in the
92/// file: a PID file left behind by a killed broker, or one whose PID the
93/// system has since handed to an unrelated process, reads as dead and is
94/// restarted instead of being trusted, blocking the session forever, or —
95/// worse — being signalled.
96fn broker_lock(pid_path: &Path) -> BrokerLock {
97    let Ok(file) = OpenOptions::new().read(true).write(true).open(pid_path) else {
98        return BrokerLock::Free;
99    };
100    match file.try_lock() {
101        // Nobody holds the lock, so the broker that wrote this file is gone.
102        Ok(()) => {
103            let _ = file.unlock();
104            BrokerLock::Free
105        }
106        Err(TryLockError::WouldBlock) => BrokerLock::Held(
107            std::fs::read_to_string(pid_path)
108                .ok()
109                .and_then(|pid| pid.trim().parse().ok()),
110        ),
111        Err(TryLockError::Error(error)) => {
112            tracing::warn!(
113                path = %pid_path.display(),
114                error = %error,
115                "could not test the Git broker lock; treating the broker as gone"
116            );
117            BrokerLock::Free
118        }
119    }
120}
121
122/// Whether a broker process still owns this session's bridge.
123pub fn broker_is_alive(pid_path: &Path) -> bool {
124    matches!(broker_lock(pid_path), BrokerLock::Held(_))
125}
126
127/// The process ID of the broker serving this session, when one is running.
128///
129/// Only a broker that still holds its lock is named, so a caller that stops a
130/// broker can never signal a PID the system has reassigned.
131pub fn running_broker_pid(pid_path: &Path) -> Option<i32> {
132    match broker_lock(pid_path) {
133        BrokerLock::Held(pid) => pid,
134        BrokerLock::Free => None,
135    }
136}
137
138/// Take ownership of this session's broker slot for the life of the process.
139///
140/// Visible to the crate so tests can stand in for a broker exactly as one
141/// behaves: holding this lock is what makes a process the session's broker.
142pub(crate) fn claim_broker_pid_file(pid_path: &Path) -> Result<File> {
143    let mut options = OpenOptions::new();
144    options.create(true).read(true).write(true);
145    #[cfg(unix)]
146    {
147        use std::os::unix::fs::OpenOptionsExt;
148        options.mode(0o600);
149    }
150    let mut file = options
151        .open(pid_path)
152        .with_context(|| format!("open Git broker lock {}", pid_path.display()))?;
153    match file.try_lock() {
154        Ok(()) => {}
155        Err(TryLockError::WouldBlock) => bail!(
156            "another local Git broker already owns {}",
157            pid_path.display()
158        ),
159        Err(TryLockError::Error(error)) => {
160            return Err(error)
161                .with_context(|| format!("lock Git broker file {}", pid_path.display()));
162        }
163    }
164    file.set_len(0)?;
165    file.write_all(std::process::id().to_string().as_bytes())?;
166    file.flush()?;
167    Ok(file)
168}
169
170pub async fn run_broker(spec_path: &Path) -> Result<()> {
171    let spec = GitBrokerSpec::read(spec_path)?;
172    let repositories = spec
173        .repositories
174        .iter()
175        .map(|(id, path)| Ok((id.clone(), canonical_repository(path)?)))
176        .collect::<Result<BTreeMap<_, _>>>()?;
177    if let Some(parent) = spec.ready_path.parent() {
178        std::fs::create_dir_all(parent)?;
179    }
180    // Held until this process exits: it is what `broker_is_alive` observes.
181    let pid_file = claim_broker_pid_file(&spec.pid_path)?;
182    let result = run_bridge_process(&spec, repositories).await;
183    for path in [&spec.ready_path, &spec.pid_path] {
184        if let Err(error) = std::fs::remove_file(path)
185            && error.kind() != std::io::ErrorKind::NotFound
186        {
187            tracing::warn!(
188                path = %path.display(),
189                %error,
190                "could not remove Git broker lifecycle marker"
191            );
192        }
193    }
194    drop(pid_file);
195    result
196}
197
198async fn run_bridge_process(
199    spec: &GitBrokerSpec,
200    repositories: BTreeMap<String, PathBuf>,
201) -> Result<()> {
202    let mut child = Command::new(&spec.bridge.program)
203        .args(&spec.bridge.args)
204        .envs(&spec.bridge.env)
205        .stdin(Stdio::piped())
206        .stdout(Stdio::piped())
207        .stderr(Stdio::inherit())
208        .spawn()
209        .with_context(|| format!("start {}", spec.bridge.purpose))?;
210    let mut input = child.stdin.take().context("Git bridge stdin is missing")?;
211    let mut output = child
212        .stdout
213        .take()
214        .context("Git bridge stdout is missing")?;
215    let mut magic = vec![0; BRIDGE_MAGIC.len()];
216    output
217        .read_exact(&mut magic)
218        .await
219        .context("read Git bridge greeting")?;
220    ensure!(magic == BRIDGE_MAGIC, "target Git bridge version mismatch");
221    hel::hel_config::atomic_write(&spec.ready_path, b"ready\n")?;
222
223    let outcome = serve_bridge(
224        &mut input,
225        &mut output,
226        &repositories,
227        &spec.session_id,
228        EXCHANGE_IDLE_DEADLINE,
229    )
230    .await;
231    // Closing the frame stream is how an idle target bridge learns that its
232    // broker is finished with it.
233    drop(input);
234    drop(output);
235    let status = match tokio::time::timeout(BRIDGE_EXIT_GRACE, child.wait()).await {
236        Ok(status) => Some(status.context("wait for target Git bridge")?),
237        Err(_) => {
238            if let Err(error) = child.kill().await
239                && error.kind() != std::io::ErrorKind::NotFound
240            {
241                tracing::warn!(
242                    session_id = %spec.session_id,
243                    %error,
244                    "could not terminate a timed-out target Git bridge"
245                );
246            }
247            None
248        }
249    };
250    outcome?;
251    let status = status.context("target Git bridge did not exit after its stream closed")?;
252    ensure!(status.success(), "target Git bridge exited with {status}");
253    Ok(())
254}
255
256/// Outcome of one bridged Git exchange whose frame stream is still in sync.
257///
258/// A failed exchange costs its own connection and nothing else, so both loops
259/// report it and keep serving. A frame stream that can no longer be
260/// interpreted is returned as `Err` instead, because no later exchange could
261/// be framed correctly after it.
262#[must_use]
263enum Exchange {
264    Completed,
265    Failed(anyhow::Error),
266}
267
268/// Idle watchdog for one exchange, shared by both halves of its transfer.
269///
270/// Every frame and every byte moved in either direction marks progress. When
271/// nothing moves for a whole idle window the exchange is aborted, which is how
272/// a client that wedges mid-transfer becomes its own connection's failure
273/// instead of a bridge that serves nobody again.
274struct ExchangeWatch {
275    idle: Duration,
276    start: Instant,
277    /// Milliseconds after `start` at which progress was last marked.
278    progress: AtomicU64,
279    abort: CancellationToken,
280}
281
282impl ExchangeWatch {
283    fn new(idle: Duration) -> Self {
284        Self {
285            idle,
286            start: Instant::now(),
287            progress: AtomicU64::new(0),
288            abort: CancellationToken::new(),
289        }
290    }
291
292    /// Record that this exchange moved something, in either direction.
293    fn mark(&self) {
294        let elapsed = self.start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
295        self.progress.store(elapsed, Ordering::Relaxed);
296    }
297
298    /// Resolve once no progress has been marked for a whole idle window.
299    async fn stalled(&self) {
300        loop {
301            let progress = self.progress.load(Ordering::Relaxed);
302            let deadline = self.start + Duration::from_millis(progress) + self.idle;
303            tokio::time::sleep_until(deadline.into()).await;
304            if self.progress.load(Ordering::Relaxed) == progress {
305                return;
306            }
307        }
308    }
309
310    /// Give up on this exchange: both halves stop touching their own endpoint
311    /// and finish their framing. The idle window restarts, so the peer still
312    /// gets a full one to end its half of the exchange.
313    fn abort(&self) {
314        self.mark();
315        self.abort.cancel();
316    }
317
318    fn is_aborted(&self) -> bool {
319        self.abort.is_cancelled()
320    }
321
322    /// Resolve once this exchange has been given up on.
323    async fn aborted(&self) {
324        self.abort.cancelled().await;
325    }
326
327    fn stall(&self) -> anyhow::Error {
328        anyhow!(
329            "a bridged Git exchange moved nothing for {} seconds",
330            self.idle.as_secs_f64()
331        )
332    }
333}
334
335/// Run one exchange's transfer under its idle watchdog.
336///
337/// The watchdog never drops the transfer: a half dropped mid-frame would leave
338/// a partial frame in the stream and desynchronise every exchange after it.
339/// The first idle window aborts the transfer instead, which makes both halves
340/// let go of their own endpoint and still write the framing the peer is
341/// reading for. Only a second idle window is fatal, because a peer that never
342/// ends its half of an aborted exchange leaves a frame stream that can no
343/// longer be interpreted.
344///
345/// Returns the transfer's own result together with the stall that aborted it,
346/// if one did.
347async fn watch_exchange<T>(
348    watch: &ExchangeWatch,
349    transfer: impl Future<Output = Result<T>>,
350) -> Result<(T, Option<anyhow::Error>)> {
351    tokio::pin!(transfer);
352    tokio::select! {
353        result = &mut transfer => Ok((result?, None)),
354        () = watch.stalled() => {
355            watch.abort();
356            tokio::select! {
357                result = &mut transfer => Ok((result?, Some(watch.stall()))),
358                () = watch.stalled() => Err(watch
359                    .stall()
360                    .context("a stalled Git bridge exchange was never ended by its peer")),
361            }
362        }
363    }
364}
365
366/// Serve bridged Git exchanges until the target bridge closes its stream.
367async fn serve_bridge(
368    input: &mut (impl AsyncWrite + Unpin),
369    output: &mut (impl AsyncRead + Unpin),
370    repositories: &BTreeMap<String, PathBuf>,
371    session_id: &str,
372    idle: Duration,
373) -> Result<()> {
374    loop {
375        let open = match read_frame(output, MAX_OPEN).await? {
376            Frame::Data(open) => open,
377            // The target bridge is gone: no further exchange is possible.
378            Frame::Closed => return Ok(()),
379            Frame::End => {
380                tracing::warn!(
381                    session_id,
382                    "target Git bridge ended an exchange that was not open"
383                );
384                continue;
385            }
386        };
387        match serve_exchange(input, output, repositories, &open, idle).await? {
388            Exchange::Completed => {}
389            Exchange::Failed(error) => tracing::warn!(
390                session_id,
391                error = format!("{error:#}"),
392                "bridged Git exchange failed"
393            ),
394        }
395    }
396}
397
398async fn serve_exchange(
399    input: &mut (impl AsyncWrite + Unpin),
400    output: &mut (impl AsyncRead + Unpin),
401    repositories: &BTreeMap<String, PathBuf>,
402    open: &[u8],
403    idle: Duration,
404) -> Result<Exchange> {
405    let request: GitOpen = match serde_json::from_slice(open) {
406        Ok(request) => request,
407        Err(error) => {
408            let error = anyhow!(error).context("decode Git bridge request");
409            return refuse_exchange(input, output, error).await;
410        }
411    };
412    let Some(repository) = repositories.get(&request.repository) else {
413        let error = anyhow!(
414            "Git bridge requested unknown repository {:?}",
415            request.repository
416        );
417        return refuse_exchange(input, output, error).await;
418    };
419    let command = match git_service(&request.service) {
420        Ok(command) => command,
421        Err(error) => return refuse_exchange(input, output, error).await,
422    };
423    serve_git(input, output, repository, command, idle).await
424}
425
426fn git_service(service: &str) -> Result<&'static str> {
427    match service {
428        "git-upload-pack" => Ok("upload-pack"),
429        "git-receive-pack" => Ok("receive-pack"),
430        _ => bail!("unsupported Git service {service:?}"),
431    }
432}
433
434/// Refuse one exchange without losing the frame stream: end this side of it,
435/// then read the target's side through to its end frame.
436async fn refuse_exchange(
437    input: &mut (impl AsyncWrite + Unpin),
438    output: &mut (impl AsyncRead + Unpin),
439    error: anyhow::Error,
440) -> Result<Exchange> {
441    write_frame(input, &[]).await?;
442    drain_exchange(output).await?;
443    Ok(Exchange::Failed(error))
444}
445
446/// Read the peer's remaining frames for the exchange in progress.
447async fn drain_exchange(output: &mut (impl AsyncRead + Unpin)) -> Result<()> {
448    loop {
449        match read_frame(output, MAX_FRAME).await? {
450            Frame::Data(_) => {}
451            Frame::End => return Ok(()),
452            Frame::Closed => bail!("the target Git bridge closed during an exchange"),
453        }
454    }
455}
456
457async fn serve_git<W, R>(
458    bridge_input: &mut W,
459    bridge_output: &mut R,
460    repository: &Path,
461    command: &str,
462    idle: Duration,
463) -> Result<Exchange>
464where
465    W: AsyncWrite + Unpin,
466    R: AsyncRead + Unpin,
467{
468    let mut git = Command::new("git");
469    git.args([
470        "-c",
471        "core.hooksPath=/dev/null",
472        "-c",
473        "receive.denyCurrentBranch=updateInstead",
474        "-c",
475        "receive.denyNonFastForwards=true",
476        "-c",
477        "receive.denyDeletes=true",
478        command,
479    ]);
480    git.arg(repository)
481        .stdin(Stdio::piped())
482        .stdout(Stdio::piped())
483        .stderr(Stdio::inherit());
484    let mut git = match git.spawn().with_context(|| format!("start git {command}")) {
485        Ok(git) => git,
486        Err(error) => return refuse_exchange(bridge_input, bridge_output, error).await,
487    };
488    let (Some(mut git_input), Some(mut git_output)) = (git.stdin.take(), git.stdout.take()) else {
489        if let Err(kill_error) = git.kill().await {
490            tracing::warn!(%kill_error, "could not stop Git service with missing pipes");
491        }
492        let error = anyhow!("git {command} was started without both pipes");
493        return refuse_exchange(bridge_input, bridge_output, error).await;
494    };
495
496    let watch = ExchangeWatch::new(idle);
497    let watch = &watch;
498    let to_target = async {
499        let mut buffer = vec![0; 64 * 1024];
500        let mut failure = None;
501        loop {
502            let count = tokio::select! {
503                biased;
504                // The watchdog gave up on this exchange: let go of a service
505                // that has stopped moving and end our half of the stream.
506                () = watch.aborted() => break,
507                // `read` is cancel-safe, and every frame is written outside
508                // the select, so no partial frame can reach the target.
509                result = git_output.read(&mut buffer) => match result {
510                    Ok(0) => break,
511                    Ok(count) => count,
512                    Err(error) => {
513                        failure = Some(anyhow!(error).context(format!("read git {command} output")));
514                        break;
515                    }
516                },
517            };
518            watch.mark();
519            write_frame(bridge_input, &buffer[..count]).await?;
520        }
521        // Exactly one end frame per exchange, on every path: the target reads
522        // until it arrives.
523        write_frame(bridge_input, &[]).await?;
524        Ok::<_, anyhow::Error>(failure)
525    };
526    let from_target = async move {
527        loop {
528            match read_frame(bridge_output, MAX_FRAME).await? {
529                // A service that has already exited must not stop the drain:
530                // the frame stream stays in sync only if every frame of this
531                // exchange is read. Writing to the service is local to this
532                // exchange, so the watchdog may abandon a write; reading the
533                // shared frame stream never stops short of the end frame.
534                Frame::Data(frame) => {
535                    watch.mark();
536                    if !watch.is_aborted() {
537                        tokio::select! {
538                            biased;
539                            () = watch.aborted() => {}
540                            result = git_input.write_all(&frame) => {
541                                let _ = result;
542                            }
543                        }
544                    }
545                }
546                Frame::End => break,
547                Frame::Closed => bail!("the target Git bridge closed mid-exchange"),
548            }
549        }
550        // Closing the handle is what ends the service's input: shutting a
551        // child's stdin down leaves the pipe open, and Git would wait on it
552        // forever.
553        drop(git_input);
554        Ok::<_, anyhow::Error>(())
555    };
556    // Both halves always run to completion, so one interrupted transfer can
557    // never leave unread frames in front of the next exchange.
558    let ((service_failure, ()), stalled) =
559        watch_exchange(watch, async { tokio::try_join!(to_target, from_target) }).await?;
560    if let Some(stalled) = stalled {
561        // A service that outlived its exchange would hold the repository and
562        // its pipes for as long as it liked.
563        if let Err(kill_error) = git.kill().await {
564            tracing::warn!(
565                service = command,
566                %kill_error,
567                "could not stop stalled Git service"
568            );
569        }
570        return Ok(Exchange::Failed(
571            stalled.context(format!("git {command} stalled")),
572        ));
573    }
574    if let Some(failure) = service_failure {
575        if let Err(kill_error) = git.kill().await {
576            tracing::warn!(
577                service = command,
578                %kill_error,
579                "could not stop failed Git service"
580            );
581        }
582        return Ok(Exchange::Failed(failure));
583    }
584    match git.wait().await {
585        Ok(status) if status.success() => Ok(Exchange::Completed),
586        Ok(status) => Ok(Exchange::Failed(anyhow!(
587            "git {command} exited with {status}"
588        ))),
589        Err(error) => Ok(Exchange::Failed(
590            anyhow!(error).context("wait for Git service"),
591        )),
592    }
593}
594
595async fn write_frame(writer: &mut (impl AsyncWrite + Unpin), data: &[u8]) -> Result<()> {
596    ensure!(data.len() <= MAX_FRAME, "Git bridge frame is too large");
597    writer.write_u32(data.len() as u32).await?;
598    writer.write_all(data).await?;
599    writer.flush().await?;
600    Ok(())
601}
602
603/// One read from a bridge frame stream.
604enum Frame {
605    /// Payload bytes belonging to the exchange in progress.
606    Data(Vec<u8>),
607    /// The peer finished its half of the exchange in progress.
608    End,
609    /// The peer closed the stream: no further exchange is possible.
610    Closed,
611}
612
613async fn read_frame(reader: &mut (impl AsyncRead + Unpin), maximum: usize) -> Result<Frame> {
614    let length = match reader.read_u32().await {
615        Ok(length) => length as usize,
616        Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
617            return Ok(Frame::Closed);
618        }
619        Err(error) => return Err(error.into()),
620    };
621    if length == 0 {
622        return Ok(Frame::End);
623    }
624    ensure!(length <= maximum, "Git bridge frame is too large");
625    let mut data = vec![0; length];
626    reader.read_exact(&mut data).await?;
627    Ok(Frame::Data(data))
628}
629
630#[cfg(unix)]
631pub async fn run_worker_bridge(root: &Path) -> Result<()> {
632    run_worker_bridge_over(
633        root,
634        tokio::io::stdin(),
635        tokio::io::stdout(),
636        HANDSHAKE_DEADLINE,
637        EXCHANGE_IDLE_DEADLINE,
638    )
639    .await
640}
641
642#[cfg(unix)]
643async fn run_worker_bridge_over(
644    root: &Path,
645    mut broker_input: impl AsyncRead + Unpin,
646    mut broker_output: impl AsyncWrite + Unpin,
647    handshake: Duration,
648    idle: Duration,
649) -> Result<()> {
650    use tokio::net::UnixListener;
651
652    std::fs::create_dir_all(root)?;
653    let socket = root.join("git.sock");
654    if socket.exists() {
655        std::fs::remove_file(&socket)
656            .with_context(|| format!("remove stale Git proxy socket {}", socket.display()))?;
657    }
658    let listener = UnixListener::bind(&socket)
659        .with_context(|| format!("bind Git proxy socket {}", socket.display()))?;
660    use std::os::unix::fs::PermissionsExt;
661    std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o600))?;
662    broker_output.write_all(BRIDGE_MAGIC).await?;
663    broker_output.flush().await?;
664    loop {
665        let stream = tokio::select! {
666            accepted = listener.accept() => accepted.context("accept Git proxy client")?.0,
667            // Between exchanges the broker sends nothing, so the only thing
668            // this read can report is the broker going away. Nothing is in
669            // flight to lose when the branch that is not taken is dropped.
670            frame = read_frame(&mut broker_input, MAX_FRAME) => match frame? {
671                Frame::Closed => return Ok(()),
672                _ => bail!("Git broker sent a frame between exchanges"),
673            },
674        };
675        match serve_client(
676            stream,
677            &mut broker_input,
678            &mut broker_output,
679            handshake,
680            idle,
681        )
682        .await?
683        {
684            Exchange::Completed => {}
685            Exchange::Failed(error) => {
686                tracing::warn!(error = format!("{error:#}"), "bridged Git client failed")
687            }
688        }
689    }
690}
691
692/// Bridge one accepted client through the broker's frame stream.
693#[cfg(unix)]
694async fn serve_client(
695    stream: tokio::net::UnixStream,
696    broker_input: &mut (impl AsyncRead + Unpin),
697    broker_output: &mut (impl AsyncWrite + Unpin),
698    handshake: Duration,
699    idle: Duration,
700) -> Result<Exchange> {
701    let (mut read, mut write) = stream.into_split();
702    // Nothing has been framed upstream yet, so a client that dies — or never
703    // speaks — during its handshake costs the bridge nothing but this wait.
704    let open = match tokio::time::timeout(handshake, read_handshake(&mut read)).await {
705        Ok(Ok(open)) => open,
706        Ok(Err(error)) => return Ok(Exchange::Failed(error)),
707        Err(_) => {
708            return Ok(Exchange::Failed(anyhow!(
709                "a Git proxy client sent no handshake within {} seconds",
710                handshake.as_secs_f64()
711            )));
712        }
713    };
714    write_frame(broker_output, &open).await?;
715
716    let watch = ExchangeWatch::new(idle);
717    let watch = &watch;
718    let served = tokio::sync::Notify::new();
719    let to_broker = async {
720        let mut buffer = vec![0; 64 * 1024];
721        let mut failure = None;
722        loop {
723            let count = tokio::select! {
724                biased;
725                // The service finished, so stop reading a client that may
726                // never hang up instead of holding the exchange open.
727                () = served.notified() => break,
728                // The watchdog gave up on this exchange: let go of a client
729                // that has stopped moving and end our half of the stream.
730                () = watch.aborted() => break,
731                // `read` is cancel-safe, and every frame is written outside
732                // the select, so no partial frame can reach the broker.
733                result = read.read(&mut buffer) => match result {
734                    Ok(0) => break,
735                    Ok(count) => count,
736                    Err(error) => {
737                        failure = Some(anyhow!(error).context("read Git proxy client"));
738                        break;
739                    }
740                },
741            };
742            watch.mark();
743            write_frame(broker_output, &buffer[..count]).await?;
744        }
745        // Exactly one end frame per exchange, on every path.
746        write_frame(broker_output, &[]).await?;
747        Ok::<_, anyhow::Error>(failure)
748    };
749    let from_broker = async {
750        loop {
751            match read_frame(broker_input, MAX_FRAME).await? {
752                // A client that has gone away must not stop the drain: the
753                // frame stream stays in sync only if every frame of this
754                // exchange is read. Writing to the client is local to this
755                // exchange, so the watchdog may abandon a write; reading the
756                // shared frame stream never stops short of the end frame.
757                Frame::Data(frame) => {
758                    watch.mark();
759                    if !watch.is_aborted() {
760                        tokio::select! {
761                            biased;
762                            () = watch.aborted() => {}
763                            result = write.write_all(&frame) => {
764                                let _ = result;
765                            }
766                        }
767                    }
768                }
769                Frame::End => break,
770                Frame::Closed => bail!("the Git broker closed the bridge"),
771            }
772        }
773        if let Err(error) = write.shutdown().await {
774            tracing::debug!(%error, "Git proxy client closed before its response was flushed");
775        }
776        served.notify_one();
777        Ok::<_, anyhow::Error>(())
778    };
779    let (client_failure, stalled) = watch_exchange(watch, async {
780        let (failure, ()) = tokio::try_join!(to_broker, from_broker)?;
781        Ok(failure)
782    })
783    .await?;
784    Ok(match stalled.or(client_failure) {
785        Some(error) => Exchange::Failed(error),
786        None => Exchange::Completed,
787    })
788}
789
790#[cfg(unix)]
791pub async fn run_worker_proxy(root: &Path, repository: &str, service: &str) -> Result<()> {
792    use tokio::net::UnixStream;
793
794    hel::hel_config::validate_id("repository", repository)?;
795    ensure!(
796        matches!(service, "git-upload-pack" | "git-receive-pack"),
797        "unsupported Git service"
798    );
799    let mut socket = UnixStream::connect(root.join("git.sock"))
800        .await
801        .with_context(|| format!("connect Git bridge at {}", root.display()))?;
802    let open = serde_json::to_vec(&GitOpen {
803        repository: repository.into(),
804        service: service.into(),
805    })?;
806    socket.write_all(&open).await?;
807    socket.write_all(b"\n").await?;
808    let (mut socket_read, mut socket_write) = socket.into_split();
809    let mut stdin = tokio::io::stdin();
810    let mut stdout = tokio::io::stdout();
811    let from_git = async {
812        tokio::io::copy(&mut stdin, &mut socket_write).await?;
813        socket_write.shutdown().await
814    };
815    let to_git = async {
816        tokio::io::copy(&mut socket_read, &mut stdout).await?;
817        stdout.shutdown().await
818    };
819    tokio::pin!(from_git);
820    tokio::pin!(to_git);
821    tokio::select! {
822        result = &mut to_git => {
823            result?;
824        }
825        result = &mut from_git => {
826            result?;
827            to_git.await?;
828        }
829    }
830    Ok(())
831}
832
833#[cfg(unix)]
834async fn read_handshake(reader: &mut (impl AsyncRead + Unpin)) -> Result<Vec<u8>> {
835    let mut data = Vec::new();
836    loop {
837        ensure!(data.len() < MAX_OPEN, "Git proxy handshake is too large");
838        let byte = reader.read_u8().await?;
839        if byte == b'\n' {
840            break;
841        }
842        data.push(byte);
843    }
844    Ok(data)
845}
846
847#[cfg(not(unix))]
848pub async fn run_worker_bridge(_root: &Path) -> Result<()> {
849    bail!("Git proxy workers require Unix")
850}
851
852#[cfg(not(unix))]
853pub async fn run_worker_proxy(_root: &Path, _repository: &str, _service: &str) -> Result<()> {
854    bail!("Git proxy workers require Unix")
855}
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860
861    /// Read one exchange's frames through the peer's end frame.
862    async fn read_exchange(reader: &mut (impl AsyncRead + Unpin)) -> Vec<u8> {
863        let mut received = Vec::new();
864        loop {
865            match read_frame(reader, MAX_FRAME).await.unwrap() {
866                Frame::Data(frame) => received.extend_from_slice(&frame),
867                Frame::End => return received,
868                Frame::Closed => panic!("the bridge stream closed mid-exchange"),
869            }
870        }
871    }
872
873    async fn read_data_frame(reader: &mut (impl AsyncRead + Unpin)) -> Vec<u8> {
874        match read_frame(reader, MAX_FRAME).await.unwrap() {
875            Frame::Data(frame) => frame,
876            Frame::End => panic!("expected a data frame, not the end of an exchange"),
877            Frame::Closed => panic!("expected a data frame, not a closed stream"),
878        }
879    }
880
881    #[cfg(unix)]
882    async fn write_all_framed(writer: &mut (impl AsyncWrite + Unpin), data: &[u8]) {
883        for chunk in data.chunks(64 * 1024) {
884            write_frame(writer, chunk).await.unwrap();
885        }
886        write_frame(writer, &[]).await.unwrap();
887    }
888
889    /// What a client writes to the proxy socket to open its exchange.
890    #[cfg(unix)]
891    fn handshake_line(repository: &str) -> Vec<u8> {
892        let mut line = open_frame(repository);
893        line.push(b'\n');
894        line
895    }
896
897    fn open_frame(repository: &str) -> Vec<u8> {
898        serde_json::to_vec(&GitOpen {
899            repository: repository.into(),
900            service: "git-upload-pack".into(),
901        })
902        .unwrap()
903    }
904
905    fn git(directory: &Path, args: &[&str]) {
906        let status = std::process::Command::new("git")
907            .args(args)
908            .current_dir(directory)
909            .status()
910            .unwrap();
911        assert!(status.success(), "git {args:?} failed");
912    }
913
914    /// A repository whose ref advertisement is far larger than one pipe
915    /// buffer, so a bridged transfer really has to stream.
916    fn repository_with_large_advertisement(root: &Path) -> PathBuf {
917        let repository = root.join("main");
918        std::fs::create_dir_all(&repository).unwrap();
919        git(&repository, &["init", "-q", "-b", "main"]);
920        git(&repository, &["config", "user.name", "Hel Test"]);
921        git(&repository, &["config", "user.email", "hel@example.test"]);
922        std::fs::write(repository.join("tracked"), "content").unwrap();
923        git(&repository, &["add", "."]);
924        git(&repository, &["commit", "-qm", "base"]);
925        let head = std::process::Command::new("git")
926            .args(["rev-parse", "HEAD"])
927            .current_dir(&repository)
928            .output()
929            .unwrap();
930        let head = String::from_utf8(head.stdout).unwrap().trim().to_owned();
931        let mut packed = String::from("# pack-refs with: peeled fully-peeled sorted \n");
932        packed.push_str(&format!("{head} refs/heads/main\n"));
933        for index in 0..3000 {
934            packed.push_str(&format!("{head} refs/tags/advertised-{index:04}\n"));
935        }
936        std::fs::write(repository.join(".git/packed-refs"), packed).unwrap();
937        repository
938    }
939
940    /// A refused request, a failing Git service, and a transfer the target
941    /// abandons midway are all one exchange's failure: the broker keeps
942    /// serving the exchanges that follow them.
943    #[tokio::test]
944    async fn the_broker_serves_later_exchanges_after_one_fails_mid_transfer() {
945        let directory = tempfile::tempdir().unwrap();
946        let repository = repository_with_large_advertisement(directory.path());
947        let not_a_repository = directory.path().join("not-a-repository");
948        std::fs::create_dir_all(&not_a_repository).unwrap();
949        let repositories = BTreeMap::from([
950            ("main".to_owned(), repository),
951            ("broken".to_owned(), not_a_repository),
952        ]);
953
954        let (broker_end, target_end) = tokio::io::duplex(16 * 1024);
955        let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
956        let (mut target_read, mut target_write) = tokio::io::split(target_end);
957        let serving = tokio::spawn(async move {
958            serve_bridge(
959                &mut broker_write,
960                &mut broker_read,
961                &repositories,
962                "test",
963                EXCHANGE_IDLE_DEADLINE,
964            )
965            .await
966        });
967
968        // An unknown repository is refused, not fatal.
969        write_frame(&mut target_write, &open_frame("absent"))
970            .await
971            .unwrap();
972        write_frame(&mut target_write, &[]).await.unwrap();
973        assert!(read_exchange(&mut target_read).await.is_empty());
974
975        // A Git service that exits non-zero is refused the same way.
976        write_frame(&mut target_write, &open_frame("broken"))
977            .await
978            .unwrap();
979        write_frame(&mut target_write, &[]).await.unwrap();
980        assert!(read_exchange(&mut target_read).await.is_empty());
981
982        // A client that hangs up midway through a large transfer leaves the
983        // frame stream in sync for the next exchange.
984        write_frame(&mut target_write, &open_frame("main"))
985            .await
986            .unwrap();
987        let mut abandoned = read_data_frame(&mut target_read).await.len();
988        while abandoned <= 64 * 1024 {
989            abandoned += read_data_frame(&mut target_read).await.len();
990        }
991        write_frame(&mut target_write, &[]).await.unwrap();
992        let remainder = read_exchange(&mut target_read).await;
993        assert!(abandoned + remainder.len() > 64 * 1024);
994
995        // The bridge still serves a complete exchange afterwards.
996        write_frame(&mut target_write, &open_frame("main"))
997            .await
998            .unwrap();
999        // A client that wants nothing sends a flush packet and hangs up.
1000        write_frame(&mut target_write, b"0000").await.unwrap();
1001        write_frame(&mut target_write, &[]).await.unwrap();
1002        let advertisement = read_exchange(&mut target_read).await;
1003        assert!(
1004            advertisement.len() > 64 * 1024,
1005            "advertisement was {} bytes",
1006            advertisement.len()
1007        );
1008        assert!(String::from_utf8_lossy(&advertisement).contains("refs/heads/main"));
1009
1010        // Both halves have to go for the stream itself to close.
1011        drop((target_read, target_write));
1012        serving.await.unwrap().unwrap();
1013    }
1014
1015    /// A client that dies mid-transfer must cost its own connection only.
1016    #[cfg(unix)]
1017    #[tokio::test]
1018    async fn the_worker_bridge_serves_the_next_client_after_one_dies_mid_transfer() {
1019        let directory = tempfile::tempdir().unwrap();
1020        let root = directory.path().to_path_buf();
1021        let (worker_end, broker_end) = tokio::io::duplex(16 * 1024);
1022        let (worker_read, worker_write) = tokio::io::split(worker_end);
1023        let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
1024        let serving = tokio::spawn(async move {
1025            run_worker_bridge_over(
1026                &root,
1027                worker_read,
1028                worker_write,
1029                HANDSHAKE_DEADLINE,
1030                EXCHANGE_IDLE_DEADLINE,
1031            )
1032            .await
1033        });
1034
1035        let mut magic = vec![0; BRIDGE_MAGIC.len()];
1036        broker_read.read_exact(&mut magic).await.unwrap();
1037        assert_eq!(magic, BRIDGE_MAGIC);
1038        let socket = directory.path().join("git.sock");
1039        let request = vec![b'q'; 256 * 1024];
1040        let reply = vec![b'r'; 256 * 1024];
1041
1042        // The first client streams a large request and vanishes without ever
1043        // reading its reply.
1044        let mut client = tokio::net::UnixStream::connect(&socket).await.unwrap();
1045        client
1046            .write_all(b"{\"repository\":\"main\",\"service\":\"git-upload-pack\"}\n")
1047            .await
1048            .unwrap();
1049        let open = read_data_frame(&mut broker_read).await;
1050        assert!(String::from_utf8_lossy(&open).contains("git-upload-pack"));
1051        let abandoning = tokio::spawn({
1052            let request = request.clone();
1053            async move {
1054                let _ = client.write_all(&request).await;
1055                drop(client);
1056            }
1057        });
1058        let abandoned = read_exchange(&mut broker_read).await;
1059        abandoning.await.unwrap();
1060        assert!(!abandoned.is_empty());
1061        write_all_framed(&mut broker_write, &reply).await;
1062
1063        // The next client is served in full, both ways.
1064        let (mut client_read, mut client_write) = tokio::net::UnixStream::connect(&socket)
1065            .await
1066            .unwrap()
1067            .into_split();
1068        client_write
1069            .write_all(b"{\"repository\":\"main\",\"service\":\"git-upload-pack\"}\n")
1070            .await
1071            .unwrap();
1072        let open = read_data_frame(&mut broker_read).await;
1073        assert!(String::from_utf8_lossy(&open).contains("main"));
1074        let sending = tokio::spawn(async move {
1075            client_write.write_all(&request).await.unwrap();
1076            client_write.shutdown().await.unwrap();
1077        });
1078        let receiving = tokio::spawn(async move {
1079            let mut received = Vec::new();
1080            client_read.read_to_end(&mut received).await.unwrap();
1081            received
1082        });
1083        let received_request = read_exchange(&mut broker_read).await;
1084        sending.await.unwrap();
1085        write_all_framed(&mut broker_write, &reply).await;
1086        let received_reply = receiving.await.unwrap();
1087
1088        assert_eq!(received_request.len(), 256 * 1024);
1089        assert_eq!(received_reply.len(), 256 * 1024);
1090
1091        // Closing the frame stream stops an idle bridge; both halves have to
1092        // go for the stream itself to close.
1093        drop((broker_read, broker_write));
1094        serving.await.unwrap().unwrap();
1095    }
1096
1097    /// A client that connects and then says nothing must not hold the socket
1098    /// the whole session shares.
1099    #[cfg(unix)]
1100    #[tokio::test]
1101    async fn a_client_that_never_finishes_its_handshake_is_timed_out() {
1102        let directory = tempfile::tempdir().unwrap();
1103        let root = directory.path().to_path_buf();
1104        let handshake = Duration::from_millis(500);
1105        let (worker_end, broker_end) = tokio::io::duplex(16 * 1024);
1106        let (worker_read, worker_write) = tokio::io::split(worker_end);
1107        let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
1108        let serving = tokio::spawn(async move {
1109            run_worker_bridge_over(
1110                &root,
1111                worker_read,
1112                worker_write,
1113                handshake,
1114                EXCHANGE_IDLE_DEADLINE,
1115            )
1116            .await
1117        });
1118        let mut magic = vec![0; BRIDGE_MAGIC.len()];
1119        broker_read.read_exact(&mut magic).await.unwrap();
1120        let socket = directory.path().join("git.sock");
1121
1122        // A client that connects and stalls before its newline.
1123        let mut silent = tokio::net::UnixStream::connect(&socket).await.unwrap();
1124        let line = handshake_line("silent");
1125        silent.write_all(&line[..line.len() - 1]).await.unwrap();
1126
1127        // The next client is served once the silent one runs out of time, and
1128        // the silent one never reaches the broker at all.
1129        let (mut client_read, mut client_write) = tokio::net::UnixStream::connect(&socket)
1130            .await
1131            .unwrap()
1132            .into_split();
1133        client_write
1134            .write_all(&handshake_line("served"))
1135            .await
1136            .unwrap();
1137        let open = tokio::time::timeout(Duration::from_secs(30), read_data_frame(&mut broker_read))
1138            .await
1139            .expect("a silent client held the bridge");
1140        assert!(
1141            String::from_utf8_lossy(&open).contains("served"),
1142            "first framed request was {}",
1143            String::from_utf8_lossy(&open)
1144        );
1145
1146        // A timed-out client is disconnected rather than left hanging.
1147        let mut byte = [0u8; 1];
1148        assert_eq!(
1149            tokio::time::timeout(Duration::from_secs(30), silent.read(&mut byte))
1150                .await
1151                .expect("a timed-out client was left connected")
1152                .unwrap(),
1153            0
1154        );
1155
1156        // The served exchange still completes normally. Its reply is far
1157        // larger than one pipe buffer, so the client has to be read while the
1158        // broker writes.
1159        let reply = vec![b'r'; 256 * 1024];
1160        let receiving = tokio::spawn(async move {
1161            let mut received = Vec::new();
1162            client_read.read_to_end(&mut received).await.unwrap();
1163            received
1164        });
1165        write_all_framed(&mut broker_write, &reply).await;
1166        assert!(read_exchange(&mut broker_read).await.is_empty());
1167        assert_eq!(receiving.await.unwrap().len(), 256 * 1024);
1168        drop(client_write);
1169
1170        drop((broker_read, broker_write));
1171        serving.await.unwrap().unwrap();
1172    }
1173
1174    /// A client that wedges mid-transfer must lose its own exchange and
1175    /// nothing else: the frame stream stays in sync for the next one.
1176    #[cfg(unix)]
1177    #[tokio::test]
1178    async fn a_client_that_stalls_mid_transfer_loses_only_its_own_exchange() {
1179        let directory = tempfile::tempdir().unwrap();
1180        let root = directory.path().to_path_buf();
1181        let idle = Duration::from_millis(500);
1182        let (worker_end, broker_end) = tokio::io::duplex(16 * 1024);
1183        let (worker_read, worker_write) = tokio::io::split(worker_end);
1184        let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
1185        let serving = tokio::spawn(async move {
1186            run_worker_bridge_over(&root, worker_read, worker_write, HANDSHAKE_DEADLINE, idle).await
1187        });
1188        let mut magic = vec![0; BRIDGE_MAGIC.len()];
1189        broker_read.read_exact(&mut magic).await.unwrap();
1190        let socket = directory.path().join("git.sock");
1191        let request = vec![b'q'; 256 * 1024];
1192        let reply = vec![b'r'; 256 * 1024];
1193
1194        // This client streams a large request and then wedges: it sends no
1195        // more, reads nothing, and never hangs up.
1196        let mut wedged = tokio::net::UnixStream::connect(&socket).await.unwrap();
1197        wedged.write_all(&handshake_line("wedged")).await.unwrap();
1198        let open = read_data_frame(&mut broker_read).await;
1199        assert!(String::from_utf8_lossy(&open).contains("wedged"));
1200        let started = Instant::now();
1201        let draining = tokio::spawn(async move {
1202            let received =
1203                tokio::time::timeout(Duration::from_secs(30), read_exchange(&mut broker_read))
1204                    .await
1205                    .expect("a wedged client held the bridge");
1206            (broker_read, received)
1207        });
1208        wedged.write_all(&request).await.unwrap();
1209        let (mut broker_read, received) = draining.await.unwrap();
1210        assert_eq!(received.len(), 256 * 1024);
1211        assert!(
1212            started.elapsed() >= idle,
1213            "the exchange ended after {:?}, before its idle window",
1214            started.elapsed()
1215        );
1216
1217        // The broker still owes its half of the aborted exchange, which the
1218        // bridge reads through without touching the client it gave up on.
1219        write_all_framed(&mut broker_write, &reply).await;
1220        let mut byte = [0u8; 1];
1221        assert_eq!(
1222            tokio::time::timeout(Duration::from_secs(30), wedged.read(&mut byte))
1223                .await
1224                .expect("an abandoned client was left connected")
1225                .unwrap(),
1226            0
1227        );
1228
1229        // The next client is served in full, both ways.
1230        let (mut client_read, mut client_write) = tokio::net::UnixStream::connect(&socket)
1231            .await
1232            .unwrap()
1233            .into_split();
1234        client_write
1235            .write_all(&handshake_line("main"))
1236            .await
1237            .unwrap();
1238        let open = read_data_frame(&mut broker_read).await;
1239        assert!(String::from_utf8_lossy(&open).contains("main"));
1240        let sending = tokio::spawn(async move {
1241            client_write.write_all(&request).await.unwrap();
1242            client_write.shutdown().await.unwrap();
1243        });
1244        let receiving = tokio::spawn(async move {
1245            let mut received = Vec::new();
1246            client_read.read_to_end(&mut received).await.unwrap();
1247            received
1248        });
1249        let received_request = read_exchange(&mut broker_read).await;
1250        sending.await.unwrap();
1251        write_all_framed(&mut broker_write, &reply).await;
1252        assert_eq!(received_request.len(), 256 * 1024);
1253        assert_eq!(receiving.await.unwrap().len(), 256 * 1024);
1254
1255        drop((broker_read, broker_write));
1256        serving.await.unwrap().unwrap();
1257    }
1258
1259    /// A Git service that stops moving must not hold the broker either.
1260    #[tokio::test]
1261    async fn the_broker_gives_up_on_a_service_that_stops_moving() {
1262        let directory = tempfile::tempdir().unwrap();
1263        let repository = repository_with_large_advertisement(directory.path());
1264        let repositories = BTreeMap::from([("main".to_owned(), repository)]);
1265        let idle = Duration::from_secs(1);
1266        let (broker_end, target_end) = tokio::io::duplex(16 * 1024);
1267        let (mut broker_read, mut broker_write) = tokio::io::split(broker_end);
1268        let (mut target_read, mut target_write) = tokio::io::split(target_end);
1269        let serving = tokio::spawn(async move {
1270            serve_bridge(
1271                &mut broker_write,
1272                &mut broker_read,
1273                &repositories,
1274                "test",
1275                idle,
1276            )
1277            .await
1278        });
1279
1280        // `upload-pack` advertises its refs and then waits for a client that
1281        // never answers and never hangs up.
1282        write_frame(&mut target_write, &open_frame("main"))
1283            .await
1284            .unwrap();
1285        let started = Instant::now();
1286        let advertisement =
1287            tokio::time::timeout(Duration::from_secs(60), read_exchange(&mut target_read))
1288                .await
1289                .expect("a stalled Git service held the broker");
1290        assert!(
1291            advertisement.len() > 64 * 1024,
1292            "advertisement was {} bytes",
1293            advertisement.len()
1294        );
1295        assert!(
1296            started.elapsed() >= idle,
1297            "the exchange ended after {:?}, before its idle window",
1298            started.elapsed()
1299        );
1300        // The target still owes its half of the aborted exchange.
1301        write_frame(&mut target_write, &[]).await.unwrap();
1302
1303        // The broker serves the next exchange over the same stream.
1304        write_frame(&mut target_write, &open_frame("main"))
1305            .await
1306            .unwrap();
1307        write_frame(&mut target_write, b"0000").await.unwrap();
1308        write_frame(&mut target_write, &[]).await.unwrap();
1309        let advertisement = read_exchange(&mut target_read).await;
1310        assert!(advertisement.len() > 64 * 1024);
1311        assert!(String::from_utf8_lossy(&advertisement).contains("refs/heads/main"));
1312
1313        drop((target_read, target_write));
1314        serving.await.unwrap().unwrap();
1315    }
1316
1317    /// A PID file whose broker is gone must read as dead, however it was
1318    /// left behind.
1319    #[test]
1320    fn broker_liveness_follows_the_lock_and_not_the_written_pid() {
1321        let directory = tempfile::tempdir().unwrap();
1322        let pid_path = directory.path().join("session.pid");
1323
1324        assert!(!broker_is_alive(&pid_path));
1325        assert_eq!(running_broker_pid(&pid_path), None);
1326
1327        // A PID file naming this very much alive process still reads as dead
1328        // while no broker holds its lock, and never names a process to signal.
1329        std::fs::write(&pid_path, std::process::id().to_string()).unwrap();
1330        assert!(!broker_is_alive(&pid_path));
1331        assert_eq!(running_broker_pid(&pid_path), None);
1332
1333        let claimed = claim_broker_pid_file(&pid_path).unwrap();
1334        assert!(broker_is_alive(&pid_path));
1335        assert_eq!(
1336            running_broker_pid(&pid_path),
1337            Some(std::process::id() as i32)
1338        );
1339        assert!(claim_broker_pid_file(&pid_path).is_err());
1340        let written = std::fs::read_to_string(&pid_path).unwrap();
1341        assert_eq!(written.trim(), std::process::id().to_string());
1342
1343        drop(claimed);
1344        // The lock lives on the open file description, so it survives until
1345        // every copy of that description is closed. A sibling thread that
1346        // forks while this one is open hands a copy to its child until the
1347        // child execs, which is why an owner that let go is observed free
1348        // shortly rather than instantly. Measured here: a few hundred
1349        // microseconds at worst.
1350        let released = std::time::Instant::now();
1351        while broker_is_alive(&pid_path) {
1352            assert!(
1353                released.elapsed() < Duration::from_secs(5),
1354                "a released broker lock was never observed free"
1355            );
1356            std::thread::sleep(Duration::from_millis(1));
1357        }
1358        assert_eq!(running_broker_pid(&pid_path), None);
1359    }
1360}