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