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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
//! What:
//! The linter the executable gives `markdown/autofix`:
//! `monochromatic-lint`,
//! linked into the executable and run in-process once per candidate,
//! on a guarded thread with a deadline.
//! Why:
//! The owner's rule is that the wrapper embeds the repository's own tools
//! and starts other programs as rarely as possible.
//! The linter's library entry `monochromatic_lint::run_embedded::fix_source` is the executable's
//! `--config --stdin --stdin-filename --fix` run without the program start
//! (`package/linter/monochromatic-lint/src/run_embedded.rs`),
//! so there is no program to find on a search path,
//! no configuration file to write and no stream contract to parse.
//! The child process used to be the wrapper's kill boundary;
//! `markdown_linter_guard.rs` replaces it with a thread, a deadline and panic containment,
//! which is a hang and panic boundary and not a crash boundary:
//! a stack overflow or an abort in the linter ends the wrapper.
//! The size of a finished result is checked against the former child's output bounds
//! (`markdown_linter_output.rs`), after the linter has produced it.
//! The configuration is rebuilt from the policy's options for each candidate,
//! which costs a small JSONC document,
//! so no state outlives the options it was built from.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const run = embeddedLinter().lint({ topLevel, options, path, source });
//! ```
/// Import the guarded thread runner.
use super::markdown_linter_guard::{GuardFailure, run_guarded};
/// Import the one-rule configuration.
use super::markdown_linter_config::one_rule_configuration;
/// Import the reading of one run.
use super::markdown_linter_output::{LintRun, explanation, interpret_embedded_run};
/// Import the conversion of Git's pathname bytes to a path.
use super::git_metadata::path_from_git_bytes;
/// Import the interface this linter provides.
use super::policy_markdown::{LintRequest, MarkdownLinter};
/// When a long run prints its notices.
use super::wait_progress::Cadence;
/// The linter library's in-process entry and its types.
use monochromatic_lint::run_embedded::{EmbeddedFailure, EmbeddedRequest, EmbeddedRun, fix_source};
/// The sink of those notices.
use std::io::Write;
/// `PathBuf` is an owned filesystem path.
use std::path::{Path, PathBuf};
/// `Duration` is a span of time.
use std::time::Duration;
/// What:
/// How long one candidate may take before the wrapper stops waiting for it.
/// Why:
/// Measured on the host with the former child process,
/// one run takes about 11 milliseconds warm and 227 cold;
/// the LFS rule also hashes every image a file links,
/// which for a large image takes seconds,
/// and the host has run at load averages of 40 to 90.
/// A minute leaves room for all of that and still ends a hung linter.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const LINT_DEADLINE_MS = 60_000;
/// ```
pub const LINT_DEADLINE: Duration = Duration::from_secs(60);
/// What:
/// The signature of what fixes one source:
/// the linter library's entry in production,
/// a planted function in the controls of the guard.
/// Why:
/// A plain function pointer is `Send`, so it moves onto the guarded thread without capturing anything.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type FixFunction = (request: EmbeddedRequest) => EmbeddedRun; // throws EmbeddedFailure
/// ```
pub type FixFunction = fn(&EmbeddedRequest<'_>) -> Result<EmbeddedRun, EmbeddedFailure>;
/// What:
/// The linter of one invocation:
/// what fixes a source,
/// its deadline,
/// and where long-wait notices go.
/// Why:
/// The deadline and the function are fields so the controls can plant a stalled or panicking fix.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class EmbeddedLinter implements MarkdownLinter { constructor(fix, deadline, cadence, notices) {} }
/// ```
pub struct EmbeddedLinter {
/// What fixes one source.
fix: FixFunction,
/// How long one candidate may take.
deadline: Duration,
/// When a run that takes long prints its notices.
cadence: Cadence,
/// Where those notices go.
notices: Box<dyn Write>,
/// Whether an earlier candidate of this command ran past its deadline and left its thread running.
abandoned: bool,
}
/// What:
/// The linter of the running executable:
/// the linked linter, `LINT_DEADLINE`, and notices on standard error.
/// Why:
/// Checks built before an environment is known get one too;
/// making it touches nothing,
/// and a command with no Markdown candidate never runs it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const embeddedLinter = () => new EmbeddedLinter(fixSource, LINT_DEADLINE, cadenceFromEnvironment(), stderr);
/// ```
pub fn embedded_linter() -> EmbeddedLinter {
return EmbeddedLinter {
fix: fix_source,
deadline: LINT_DEADLINE,
cadence: Cadence::from_environment(),
notices: Box::new(std::io::stderr()),
abandoned: false,
};
}
/// What:
/// A linter over `fix` with this deadline.
/// Why:
/// Controls plant a fix that panics or never returns, with a short deadline.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const embeddedLinterWith = (fix, deadline) => new EmbeddedLinter(fix, deadline, cadenceFromEnvironment(), stderr);
/// ```
pub fn embedded_linter_with(fix: FixFunction, deadline: Duration) -> EmbeddedLinter {
return EmbeddedLinter {
fix,
deadline,
cadence: Cadence::from_environment(),
notices: Box::new(std::io::stderr()),
abandoned: false,
};
}
/// What:
/// The reason every candidate after an abandoned one gets.
/// Why:
/// An abandoned thread cannot be stopped and may still be working or holding memory;
/// starting another beside it for each later candidate could pile them up, so the rest of the command
/// leaves its Markdown candidates unchecked and says why.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const ABANDONED_REASON = 'an earlier Markdown check did not finish ...';
/// ```
const ABANDONED_REASON: &str = "an earlier Markdown check in this command did not finish and cli-git stopped waiting \
for it, so monochromatic-lint is not run again in this command; run the command again, or set the policy \
markdown/autofix to off to skip the check";
/// What:
/// The sentence for a guarded run that produced no result.
/// Why:
/// Each cause tells the person what happened and the way forward in the linter's terms.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function guardFailureReason(failure: GuardFailure): string;
/// ```
fn guard_failure_reason(failure: &GuardFailure) -> String {
match failure {
GuardFailure::Start(reason) => {
return format!("cli-git could not start the thread that runs monochromatic-lint: {reason}");
}
GuardFailure::Panicked(reason) => {
return format!(
"monochromatic-lint stopped with an internal error ({}); the file was not checked, run the \
command again, run monochromatic-lint by hand on the file to see the error, or set the policy \
markdown/autofix to off to skip the check",
explanation(reason.as_str())
);
}
GuardFailure::TimedOut(deadline) => {
return format!(
"monochromatic-lint did not finish within {} milliseconds and cli-git stopped waiting for it \
(its thread stays in this process until the command ends); run the command again, run \
monochromatic-lint by hand on the file to see why it stalls, or set the policy markdown/autofix \
to off to skip the check",
deadline.as_millis()
);
}
GuardFailure::Vanished => {
return String::from(
"the thread that runs monochromatic-lint ended without a result; this is a defect in cli-git.",
);
}
}
}
/// What:
/// `impl MarkdownLinter for EmbeddedLinter` runs the linked linter for each request.
/// Why:
/// See the module comment.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class EmbeddedLinter implements MarkdownLinter {}
/// ```
impl MarkdownLinter for EmbeddedLinter {
/// What:
/// Lint one candidate in-process.
/// Why:
/// The linter reads `.lfsconfig` and linked images from the worktree,
/// so it is given the top level as its working directory
/// and the candidate's name relative to it;
/// the thread owns copies of everything it uses,
/// so abandoning it after the deadline leaves nothing the caller needs locked.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// lint(request: LintRequest): LintRun; // throws string
/// ```
fn lint(&mut self, request: &LintRequest<'_>) -> Result<LintRun, String> {
if self.abandoned {
return Err(String::from(ABANDONED_REASON));
}
let Some(name) = path_from_git_bytes(request.path) else {
return Err(String::from(
"cli-git cannot spell this candidate's pathname for monochromatic-lint on this platform",
));
};
let waiting_for: String = format!("monochromatic-lint to check {}", name.display());
let top_level: PathBuf = request.top_level.to_path_buf();
let configuration: String = one_rule_configuration(
request.options.rules.as_slice(),
request.options.exclude.as_slice(),
);
let source: Vec<u8> = request.source.to_vec();
let fix: FixFunction = self.fix;
let outcome: Result<Result<EmbeddedRun, EmbeddedFailure>, GuardFailure> = run_guarded(
move || {
return fix(&EmbeddedRequest {
top_level: top_level.as_path(),
configuration: configuration.as_str(),
name: Path::new(name.as_os_str()),
source: source.as_slice(),
});
},
self.deadline,
self.cadence,
waiting_for.as_str(),
&mut self.notices,
);
match outcome {
Ok(finished) => {
return interpret_embedded_run(request.source, request.options.rules.as_slice(), finished);
}
Err(failure) => {
// A thread that outlived its deadline may still be running, so no second one starts beside it.
self.abandoned = matches!(failure, GuardFailure::TimedOut(_));
return Err(guard_failure_reason(&failure));
}
}
}
}
impl EmbeddedLinter {
/// The same linter with another notice cadence and sink;
/// tests choose a short cadence and read the notices from a buffer.
pub fn with_notices(mut self, cadence: Cadence, notices: Box<dyn Write>) -> Self {
self.cadence = cadence;
self.notices = notices;
return self;
}
}
/// Guard, reason and contract controls stay out of the release executable.
#[cfg(test)]
#[path = "markdown_linter_tests.rs"]
mod tests;