dsh-ops 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +189 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +417 -0
- package/README.en.md +121 -0
- package/README.md +113 -2
- package/README.zh.md +114 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +993 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// Modified by dsh-ops (https://github.com/T-Auto/dsh-ops): removed probe_bash, whose only caller was the deleted control/apply.rs preflight.
|
|
2
|
+
//! Deterministic bash discovery with explicit-override semantics.
|
|
3
|
+
|
|
4
|
+
use std::collections::HashSet;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
use std::sync::OnceLock;
|
|
7
|
+
|
|
8
|
+
const WINDOWS_MISSING_BASH: &str = "Cannot find a usable bash. fastshell runs every command with bash for consistent cross-platform behavior. Install Git for Windows (https://git-scm.com/downloads) or set FASTCTX_BASH to the absolute path of bash.exe. Note: C:/Windows/System32/bash.exe is the WSL launcher, not a standalone bash.";
|
|
9
|
+
const UNIX_MISSING_BASH: &str =
|
|
10
|
+
"Cannot find a usable bash. Install bash or set FASTCTX_BASH to its absolute path.";
|
|
11
|
+
|
|
12
|
+
/// Caches the validated backend for one MCP connection.
|
|
13
|
+
#[derive(Debug, Default)]
|
|
14
|
+
pub(crate) struct BashLocator {
|
|
15
|
+
cached: OnceLock<Result<PathBuf, String>>,
|
|
16
|
+
locale: OnceLock<String>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl BashLocator {
|
|
20
|
+
/// Returns the validated bash path, probing at most once per connection.
|
|
21
|
+
pub(crate) fn resolve(
|
|
22
|
+
&self,
|
|
23
|
+
environment: &crate::session::SessionEnvironment,
|
|
24
|
+
) -> Result<PathBuf, String> {
|
|
25
|
+
self.cached
|
|
26
|
+
.get_or_init(|| probe_bash_with_environment(environment))
|
|
27
|
+
.clone()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Returns a locale probed once for this connection's bash and environment.
|
|
31
|
+
pub(crate) fn utf8_locale<'a>(
|
|
32
|
+
&'a self,
|
|
33
|
+
environment: &crate::session::SessionEnvironment,
|
|
34
|
+
bash: &Path,
|
|
35
|
+
) -> &'a str {
|
|
36
|
+
self.locale
|
|
37
|
+
.get_or_init(|| crate::shell::process::detect_utf8_locale(bash, environment))
|
|
38
|
+
.as_str()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn probe_bash_with_environment(
|
|
43
|
+
environment: &crate::session::SessionEnvironment,
|
|
44
|
+
) -> Result<PathBuf, String> {
|
|
45
|
+
match environment.var("FASTCTX_BASH") {
|
|
46
|
+
Ok(value) => return validate_override_with_environment(&value, environment),
|
|
47
|
+
Err(std::env::VarError::NotUnicode(_)) => {
|
|
48
|
+
return Err("Invalid FASTCTX_BASH value \"<non-UTF-8>\": not a working bash (the path is not valid UTF-8). Fix or unset it.".to_string());
|
|
49
|
+
}
|
|
50
|
+
Err(std::env::VarError::NotPresent) => {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let mut seen = HashSet::new();
|
|
54
|
+
for candidate in automatic_candidates(environment) {
|
|
55
|
+
if excluded_candidate(&candidate, environment) || !seen.insert(candidate_key(&candidate)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if validate_bash(&candidate, environment).is_ok() {
|
|
59
|
+
return Ok(candidate);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
Err(missing_bash_message().to_string())
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[cfg(test)]
|
|
66
|
+
fn validate_override(value: &str) -> Result<PathBuf, String> {
|
|
67
|
+
let environment = crate::session::SessionEnvironment::capture()?;
|
|
68
|
+
validate_override_with_environment(value, &environment)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn validate_override_with_environment(
|
|
72
|
+
value: &str,
|
|
73
|
+
environment: &crate::session::SessionEnvironment,
|
|
74
|
+
) -> Result<PathBuf, String> {
|
|
75
|
+
let path = crate::paths::parse_input_path(value);
|
|
76
|
+
let result = if !path.is_absolute() {
|
|
77
|
+
Err("the path is not absolute".to_string())
|
|
78
|
+
} else if excluded_candidate(&path, environment) {
|
|
79
|
+
Err("the path points to the Windows/WSL launcher or a WindowsApps shim".to_string())
|
|
80
|
+
} else {
|
|
81
|
+
validate_bash(&path, environment)
|
|
82
|
+
};
|
|
83
|
+
result.map(|_| path).map_err(|reason| {
|
|
84
|
+
format!(
|
|
85
|
+
"Invalid FASTCTX_BASH value \"{value}\": not a working bash ({reason}). Fix or unset it."
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn validate_bash(
|
|
91
|
+
path: &Path,
|
|
92
|
+
environment: &crate::session::SessionEnvironment,
|
|
93
|
+
) -> Result<(), String> {
|
|
94
|
+
if !path.is_absolute() {
|
|
95
|
+
return Err("the path is not absolute".to_string());
|
|
96
|
+
}
|
|
97
|
+
if !path.is_file() {
|
|
98
|
+
return Err("the file does not exist".to_string());
|
|
99
|
+
}
|
|
100
|
+
let mut command = crate::process_policy::noninteractive_command(path);
|
|
101
|
+
environment.configure_command(&mut command);
|
|
102
|
+
let output = command
|
|
103
|
+
.arg("--version")
|
|
104
|
+
.output()
|
|
105
|
+
.map_err(|error| error.to_string())?;
|
|
106
|
+
let mut text = String::from_utf8_lossy(&output.stdout).into_owned();
|
|
107
|
+
text.push_str(&String::from_utf8_lossy(&output.stderr));
|
|
108
|
+
if !output.status.success() {
|
|
109
|
+
return Err(format!(
|
|
110
|
+
"--version exited {}",
|
|
111
|
+
output.status.code().unwrap_or(1)
|
|
112
|
+
));
|
|
113
|
+
}
|
|
114
|
+
if !text.contains("GNU bash") {
|
|
115
|
+
return Err("--version output did not contain GNU bash".to_string());
|
|
116
|
+
}
|
|
117
|
+
Ok(())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[cfg(windows)]
|
|
121
|
+
fn automatic_candidates(environment: &crate::session::SessionEnvironment) -> Vec<PathBuf> {
|
|
122
|
+
let mut candidates = Vec::new();
|
|
123
|
+
for git in path_candidates(environment, "git.exe") {
|
|
124
|
+
let mut ancestor = git.parent();
|
|
125
|
+
for _ in 0..4 {
|
|
126
|
+
let Some(directory) = ancestor else { break };
|
|
127
|
+
candidates.push(directory.join("usr").join("bin").join("bash.exe"));
|
|
128
|
+
ancestor = directory.parent();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for variable in ["ProgramFiles", "ProgramFiles(x86)"] {
|
|
132
|
+
if let Some(root) = environment.var_os(variable) {
|
|
133
|
+
candidates.push(PathBuf::from(root).join("Git/usr/bin/bash.exe"));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if let Some(root) = environment.var_os("LocalAppData") {
|
|
137
|
+
candidates.push(PathBuf::from(root).join("Programs/Git/usr/bin/bash.exe"));
|
|
138
|
+
}
|
|
139
|
+
// Scoop exposes only a shim directory on PATH, so walking up from its
|
|
140
|
+
// git.exe never reaches the Git root that carries usr/bin.
|
|
141
|
+
if let Some(root) = environment.var_os("SCOOP") {
|
|
142
|
+
candidates.push(PathBuf::from(root).join("apps/git/current/usr/bin/bash.exe"));
|
|
143
|
+
}
|
|
144
|
+
if let Some(profile) = environment.var_os("USERPROFILE") {
|
|
145
|
+
candidates.push(PathBuf::from(profile).join("scoop/apps/git/current/usr/bin/bash.exe"));
|
|
146
|
+
}
|
|
147
|
+
candidates.extend(path_candidates(environment, "bash.exe"));
|
|
148
|
+
candidates
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[cfg(not(windows))]
|
|
152
|
+
fn automatic_candidates(environment: &crate::session::SessionEnvironment) -> Vec<PathBuf> {
|
|
153
|
+
let mut candidates = vec![PathBuf::from("/bin/bash"), PathBuf::from("/usr/bin/bash")];
|
|
154
|
+
candidates.extend(path_candidates(environment, "bash"));
|
|
155
|
+
candidates
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn path_candidates(environment: &crate::session::SessionEnvironment, name: &str) -> Vec<PathBuf> {
|
|
159
|
+
environment
|
|
160
|
+
.var_os("PATH")
|
|
161
|
+
.map(|path| {
|
|
162
|
+
std::env::split_paths(&path)
|
|
163
|
+
.map(|directory| directory.join(name))
|
|
164
|
+
.collect()
|
|
165
|
+
})
|
|
166
|
+
.unwrap_or_default()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#[cfg(windows)]
|
|
170
|
+
fn excluded_candidate(path: &Path, environment: &crate::session::SessionEnvironment) -> bool {
|
|
171
|
+
let canonical = crate::paths::canonical_existing(path).unwrap_or_else(|_| path.to_path_buf());
|
|
172
|
+
let root = environment
|
|
173
|
+
.var_os("SystemRoot")
|
|
174
|
+
.map(PathBuf::from)
|
|
175
|
+
.map(|root| crate::paths::canonical_existing(&root).unwrap_or(root));
|
|
176
|
+
excluded_windows_candidate(&canonical, root.as_deref())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[cfg(windows)]
|
|
180
|
+
fn excluded_windows_candidate(path: &Path, system_root: Option<&Path>) -> bool {
|
|
181
|
+
let candidate = path
|
|
182
|
+
.to_string_lossy()
|
|
183
|
+
.replace('/', "\\")
|
|
184
|
+
.to_ascii_lowercase();
|
|
185
|
+
if candidate.contains("\\windowsapps\\") {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
system_root
|
|
189
|
+
.map(|root| {
|
|
190
|
+
let root = root
|
|
191
|
+
.to_string_lossy()
|
|
192
|
+
.replace('/', "\\")
|
|
193
|
+
.trim_end_matches('\\')
|
|
194
|
+
.to_ascii_lowercase();
|
|
195
|
+
candidate == root || candidate.starts_with(&format!("{root}\\"))
|
|
196
|
+
})
|
|
197
|
+
.unwrap_or(false)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[cfg(not(windows))]
|
|
201
|
+
fn excluded_candidate(_path: &Path, _environment: &crate::session::SessionEnvironment) -> bool {
|
|
202
|
+
false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn candidate_key(path: &Path) -> String {
|
|
206
|
+
let key = path.to_string_lossy().replace('\\', "/");
|
|
207
|
+
if cfg!(windows) {
|
|
208
|
+
key.to_ascii_lowercase()
|
|
209
|
+
} else {
|
|
210
|
+
key
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn missing_bash_message() -> &'static str {
|
|
215
|
+
if cfg!(windows) {
|
|
216
|
+
WINDOWS_MISSING_BASH
|
|
217
|
+
} else {
|
|
218
|
+
UNIX_MISSING_BASH
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[cfg(test)]
|
|
223
|
+
mod tests {
|
|
224
|
+
use super::{candidate_key, validate_override};
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn explicit_override_rejects_relative_paths_without_fallback() {
|
|
228
|
+
let error = validate_override("relative/bash").unwrap_err();
|
|
229
|
+
assert_eq!(
|
|
230
|
+
error,
|
|
231
|
+
"Invalid FASTCTX_BASH value \"relative/bash\": not a working bash (the path is not absolute). Fix or unset it."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#[test]
|
|
236
|
+
fn candidate_keys_follow_platform_path_case_rules() {
|
|
237
|
+
let key = candidate_key(std::path::Path::new("C:\\Tools\\Bash.exe"));
|
|
238
|
+
if cfg!(windows) {
|
|
239
|
+
assert_eq!(key, "c:/tools/bash.exe");
|
|
240
|
+
} else {
|
|
241
|
+
assert_eq!(key, "C:/Tools/Bash.exe");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#[cfg(windows)]
|
|
246
|
+
#[test]
|
|
247
|
+
fn windows_exclusion_rejects_systemroot_and_windowsapps_without_path_probing() {
|
|
248
|
+
use super::excluded_windows_candidate;
|
|
249
|
+
let root = std::path::Path::new("C:/Windows");
|
|
250
|
+
assert!(excluded_windows_candidate(
|
|
251
|
+
std::path::Path::new("C:/Windows/System32/bash.exe"),
|
|
252
|
+
Some(root)
|
|
253
|
+
));
|
|
254
|
+
assert!(excluded_windows_candidate(
|
|
255
|
+
std::path::Path::new("C:/Users/test/AppData/Local/Microsoft/WindowsApps/bash.exe"),
|
|
256
|
+
Some(root)
|
|
257
|
+
));
|
|
258
|
+
assert!(!excluded_windows_candidate(
|
|
259
|
+
std::path::Path::new("C:/Program Files/Git/usr/bin/bash.exe"),
|
|
260
|
+
Some(root)
|
|
261
|
+
));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//! Bounded line-oriented storage for foreground command output.
|
|
2
|
+
|
|
3
|
+
use crate::shell::normalize::NormalizedLine;
|
|
4
|
+
use crate::shell::normalize::StreamEncoding;
|
|
5
|
+
use std::collections::VecDeque;
|
|
6
|
+
use std::mem::size_of;
|
|
7
|
+
|
|
8
|
+
/// Maximum in-memory payload retained for one foreground command.
|
|
9
|
+
pub(crate) const OUTPUT_BUFFER_BYTES: usize = 8 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
/// One normalized line stored with a stable, job-local sequence number.
|
|
12
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
13
|
+
pub(crate) struct BufferedLine {
|
|
14
|
+
pub(crate) seq: u64,
|
|
15
|
+
pub(crate) bytes: Vec<u8>,
|
|
16
|
+
pub(crate) total_bytes: u64,
|
|
17
|
+
pub(crate) stream_encoding: Option<StreamEncoding>,
|
|
18
|
+
pub(crate) raw_truncated: bool,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl BufferedLine {
|
|
22
|
+
fn storage_bytes(&self) -> usize {
|
|
23
|
+
size_of::<Self>()
|
|
24
|
+
.saturating_add(self.bytes.len())
|
|
25
|
+
.saturating_add(1)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// An eight-megabyte ring that evicts only complete lines and never controls process life.
|
|
30
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
31
|
+
pub(crate) struct LineRing {
|
|
32
|
+
lines: VecDeque<BufferedLine>,
|
|
33
|
+
storage_bytes: usize,
|
|
34
|
+
limit_bytes: usize,
|
|
35
|
+
next_seq: u64,
|
|
36
|
+
had_truncation: bool,
|
|
37
|
+
had_drop: bool,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl LineRing {
|
|
41
|
+
pub(crate) fn new() -> Self {
|
|
42
|
+
Self::with_limit(OUTPUT_BUFFER_BYTES)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub(crate) fn with_limit(limit_bytes: usize) -> Self {
|
|
46
|
+
Self {
|
|
47
|
+
lines: VecDeque::new(),
|
|
48
|
+
storage_bytes: 0,
|
|
49
|
+
limit_bytes,
|
|
50
|
+
next_seq: 1,
|
|
51
|
+
had_truncation: false,
|
|
52
|
+
had_drop: false,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Adds a line and returns its monotonically increasing sequence number.
|
|
57
|
+
pub(crate) fn push(&mut self, line: NormalizedLine) -> u64 {
|
|
58
|
+
let seq = self.next_seq;
|
|
59
|
+
self.next_seq = self.next_seq.saturating_add(1);
|
|
60
|
+
self.had_truncation |= line.raw_truncated;
|
|
61
|
+
|
|
62
|
+
let buffered = BufferedLine {
|
|
63
|
+
seq,
|
|
64
|
+
bytes: line.bytes,
|
|
65
|
+
total_bytes: line.total_bytes,
|
|
66
|
+
stream_encoding: line.stream_encoding,
|
|
67
|
+
raw_truncated: line.raw_truncated,
|
|
68
|
+
};
|
|
69
|
+
let required = buffered.storage_bytes();
|
|
70
|
+
|
|
71
|
+
while self.storage_bytes.saturating_add(required) > self.limit_bytes {
|
|
72
|
+
let Some(removed) = self.lines.pop_front() else {
|
|
73
|
+
break;
|
|
74
|
+
};
|
|
75
|
+
self.storage_bytes = self.storage_bytes.saturating_sub(removed.storage_bytes());
|
|
76
|
+
self.had_drop = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A raw line prefix is bounded, but a deliberately tiny test ring can still
|
|
80
|
+
// be smaller than one record. Treat it as lost rather than violating the limit.
|
|
81
|
+
if required > self.limit_bytes {
|
|
82
|
+
self.had_drop = true;
|
|
83
|
+
return seq;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
self.storage_bytes = self.storage_bytes.saturating_add(required);
|
|
87
|
+
self.lines.push_back(buffered);
|
|
88
|
+
seq
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub(crate) fn total_lines(&self) -> u64 {
|
|
92
|
+
self.next_seq.saturating_sub(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pub(crate) fn dropped_lines(&self) -> u64 {
|
|
96
|
+
self.total_lines().saturating_sub(self.lines.len() as u64)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub(crate) fn all(&self) -> Vec<BufferedLine> {
|
|
100
|
+
self.lines.iter().cloned().collect()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl Default for LineRing {
|
|
105
|
+
fn default() -> Self {
|
|
106
|
+
Self::new()
|
|
107
|
+
}
|
|
108
|
+
}
|