Skip to main content

git_policy_cli/
bounded_query.rs

1//! What:
2//!  Run one read-only Git query with a deadline and separate output streams.
3//! Why:
4//!  The auto-push asks Git a handful of questions before it pushes,
5//!  and a question that never returns would hold the commit and the branch's push lock for good.
6//!  A query that outlives its deadline is stopped and reported, so the commit stays saved.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! const { exitCode, stdout, stderr } = await nanoSpawn(gitPath, args, { cwd, signal: AbortSignal.timeout(deadlineMs) });
11//! ```
12
13/// The failure type of the auto-push coordination.
14use super::auto_push_single_flight::SingleFlightError;
15/// The deadline runner shared with the push, and its stop causes.
16use super::bounded_push::{MAX_PUSH_OUTPUT, StopCause, push_deadline, stop_push, wait_until_done};
17/// Owned OS strings of the child environment.
18use std::ffi::OsString;
19/// The anonymous output files and their reading back.
20use std::fs::File;
21use std::io::{Read, Seek, SeekFrom};
22/// `Command` describes the query; `Stdio` connects its streams.
23use std::process::{Command, ExitStatus, Stdio};
24/// Time spans.
25use std::time::Duration;
26
27/// The longest a query may run whatever the push deadline is:
28///  queries read local repository state and finish in milliseconds.
29pub const QUERY_CEILING: Duration = Duration::from_secs(120);
30
31/// What:
32///  What a finished query printed and how it ended.
33/// Why:
34///   Callers read an exit code as an answer ("not an ancestor") or a failure, never both.
35///
36/// In TS you'd write (pseudocode):
37/// ```ts
38/// type QueryOutput = { code: number | undefined; stdout: Uint8Array; stderr: Uint8Array };
39/// ```
40#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct QueryOutput {
42    /// Exit code, or nothing when a signal ended the process.
43    pub code: Option<i32>,
44    /// Complete standard output, up to the output cap.
45    pub stdout: Vec<u8>,
46    /// Complete standard error, up to the output cap.
47    pub stderr: Vec<u8>,
48}
49
50/// What:
51///  The deadline of a query: the push deadline, but never more than the ceiling.
52/// Why:
53///   One setting bounds every Git child of the auto-push, and a long push deadline must not stretch queries.
54///
55/// In TS you'd write (pseudocode):
56/// ```ts
57/// Math.min(pushDeadlineMs(env), QUERY_CEILING_MS)
58/// ```
59pub fn query_deadline(environment: &[(OsString, OsString)]) -> Duration {
60    return push_deadline(environment).min(QUERY_CEILING);
61}
62
63/// Read one output file back from its start, up to the output cap.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// await readFile(path)
68/// ```
69fn read_back(file: &mut File) -> Result<Vec<u8>, SingleFlightError> {
70    file.seek(SeekFrom::Start(0))
71        .map_err(SingleFlightError::Start)?;
72    let mut bytes: Vec<u8> = Vec::new();
73    file.take(MAX_PUSH_OUTPUT)
74        .read_to_end(&mut bytes)
75        .map_err(SingleFlightError::Start)?;
76    return Ok(bytes);
77}
78
79/// What:
80///  Run a query command to its end or its deadline.
81/// Why:
82///   Both streams go to anonymous files, so no reader thread can outlive a stopped query.
83///  A query past its deadline is a coordination failure, not an answer.
84///
85/// In TS you'd write (pseudocode):
86/// ```ts
87/// async function runBoundedQuery({ command, arguments, deadlineMs, graceMs }): Promise<QueryOutput>;
88/// ```
89pub fn run_bounded_query(
90    mut command: Command,
91    arguments: &str,
92    deadline: Duration,
93    grace: Duration,
94) -> Result<QueryOutput, SingleFlightError> {
95    let mut stdout: File = tempfile::tempfile().map_err(SingleFlightError::Start)?;
96    let mut stderr: File = tempfile::tempfile().map_err(SingleFlightError::Start)?;
97    let for_stdout: File = stdout.try_clone().map_err(SingleFlightError::Start)?;
98    let for_stderr: File = stderr.try_clone().map_err(SingleFlightError::Start)?;
99    command
100        .stdin(Stdio::null())
101        .stdout(Stdio::from(for_stdout))
102        .stderr(Stdio::from(for_stderr));
103    let mut child = command.spawn().map_err(SingleFlightError::Start)?;
104    drop(command);
105    let ended: Result<ExitStatus, StopCause> =
106        wait_until_done(&mut child, &[&stdout, &stderr], deadline)?;
107    return match ended {
108        Ok(status) => Ok(QueryOutput {
109            code: status.code(),
110            stdout: read_back(&mut stdout)?,
111            stderr: read_back(&mut stderr)?,
112        }),
113        Err(cause) => {
114            stop_push(&mut child, grace);
115            Err(match cause {
116                StopCause::Deadline => SingleFlightError::TimedOut {
117                    arguments: arguments.to_owned(),
118                    seconds: deadline.as_secs(),
119                },
120                StopCause::Flood => SingleFlightError::Git {
121                    arguments: arguments.to_owned(),
122                    code: None,
123                    stderr: format!(
124                        "the query wrote more than {MAX_PUSH_OUTPUT} bytes and was stopped"
125                    ),
126                },
127            })
128        }
129    };
130}
131
132/// Query deadlines are controlled in the test module.
133#[cfg(test)]
134#[path = "bounded_query_tests.rs"]
135mod tests;