dsh-ops 0.0.0-stage → 0.2.2

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.
Files changed (140) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LICENSE +30 -0
  3. package/NOTICE +106 -0
  4. package/PROVENANCE.md +435 -0
  5. package/README.en.md +126 -0
  6. package/README.md +115 -2
  7. package/README.zh.md +116 -0
  8. package/bin/dsh-ops.mjs +1216 -0
  9. package/cordis.patch.yml +160 -0
  10. package/docs/manual-validation.md +53 -0
  11. package/docs/release-0.2.1.md +72 -0
  12. package/docs/schema-baseline.json +64 -0
  13. package/docs/schema-current.json +84 -0
  14. package/docs/schema-measurement.md +17 -0
  15. package/dsh-plugin.json +88 -0
  16. package/icon.svg +12 -0
  17. package/lib/binary.js +409 -0
  18. package/lib/config.js +198 -0
  19. package/lib/handshake.js +252 -0
  20. package/lib/index.js +108 -0
  21. package/lib/jobs.js +42 -0
  22. package/lib/policy.js +64 -0
  23. package/lib/presentation.js +63 -0
  24. package/lib/profile-install.js +61 -0
  25. package/lib/rust.js +194 -0
  26. package/lib/session-shells.js +78 -0
  27. package/lib/shells.js +998 -0
  28. package/lib/tools.js +657 -0
  29. package/locale/en.json +6 -0
  30. package/locale/zh.json +6 -0
  31. package/package.json +114 -4
  32. package/vendor/fastctx/Cargo.lock +3210 -0
  33. package/vendor/fastctx/Cargo.toml +94 -0
  34. package/vendor/fastctx/FORK.md +119 -0
  35. package/vendor/fastctx/LICENSE-APACHE +201 -0
  36. package/vendor/fastctx/NOTICE +40 -0
  37. package/vendor/fastctx/README.md +439 -0
  38. package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
  39. package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
  40. package/vendor/fastctx/UPSTREAM.md +49 -0
  41. package/vendor/fastctx/build.rs +413 -0
  42. package/vendor/fastctx/src/background_status.rs +403 -0
  43. package/vendor/fastctx/src/binary.rs +75 -0
  44. package/vendor/fastctx/src/bounded_sort.rs +500 -0
  45. package/vendor/fastctx/src/budget.rs +781 -0
  46. package/vendor/fastctx/src/cli/mod.rs +110 -0
  47. package/vendor/fastctx/src/context_guard.rs +289 -0
  48. package/vendor/fastctx/src/control/mod.rs +6 -0
  49. package/vendor/fastctx/src/control/paths.rs +49 -0
  50. package/vendor/fastctx/src/control/settings.rs +753 -0
  51. package/vendor/fastctx/src/control/transaction.rs +531 -0
  52. package/vendor/fastctx/src/edit/document.rs +535 -0
  53. package/vendor/fastctx/src/edit/locks.rs +371 -0
  54. package/vendor/fastctx/src/edit/mod.rs +213 -0
  55. package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
  56. package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
  57. package/vendor/fastctx/src/edit/private_storage.rs +234 -0
  58. package/vendor/fastctx/src/edit/replace.rs +1030 -0
  59. package/vendor/fastctx/src/edit_server.rs +53 -0
  60. package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
  61. package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
  62. package/vendor/fastctx/src/encoding.rs +1118 -0
  63. package/vendor/fastctx/src/file_executor.rs +1151 -0
  64. package/vendor/fastctx/src/file_snapshot.rs +1491 -0
  65. package/vendor/fastctx/src/glob_filter.rs +98 -0
  66. package/vendor/fastctx/src/glob_tool.rs +653 -0
  67. package/vendor/fastctx/src/grep_sink.rs +1162 -0
  68. package/vendor/fastctx/src/grep_tool.rs +2449 -0
  69. package/vendor/fastctx/src/lib.rs +45 -0
  70. package/vendor/fastctx/src/main.rs +15 -0
  71. package/vendor/fastctx/src/model.rs +51 -0
  72. package/vendor/fastctx/src/model_guidance.rs +62 -0
  73. package/vendor/fastctx/src/operation.rs +356 -0
  74. package/vendor/fastctx/src/ordered_window.rs +1235 -0
  75. package/vendor/fastctx/src/os_environment.rs +414 -0
  76. package/vendor/fastctx/src/path_codec.rs +850 -0
  77. package/vendor/fastctx/src/paths.rs +244 -0
  78. package/vendor/fastctx/src/process_identity.rs +763 -0
  79. package/vendor/fastctx/src/process_policy.rs +74 -0
  80. package/vendor/fastctx/src/read_tool/batch.rs +496 -0
  81. package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
  82. package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
  83. package/vendor/fastctx/src/read_tool/mod.rs +245 -0
  84. package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
  85. package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
  86. package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
  87. package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
  88. package/vendor/fastctx/src/render_plan.rs +468 -0
  89. package/vendor/fastctx/src/runtime/activity.rs +159 -0
  90. package/vendor/fastctx/src/runtime/hosts.rs +99 -0
  91. package/vendor/fastctx/src/runtime/journal.rs +556 -0
  92. package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
  93. package/vendor/fastctx/src/runtime/mod.rs +746 -0
  94. package/vendor/fastctx/src/runtime/protocol.rs +296 -0
  95. package/vendor/fastctx/src/runtime/session.rs +536 -0
  96. package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
  97. package/vendor/fastctx/src/search_parallelism.rs +106 -0
  98. package/vendor/fastctx/src/search_text.rs +227 -0
  99. package/vendor/fastctx/src/server.rs +359 -0
  100. package/vendor/fastctx/src/server_manifest.rs +468 -0
  101. package/vendor/fastctx/src/server_support.rs +826 -0
  102. package/vendor/fastctx/src/session.rs +629 -0
  103. package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
  104. package/vendor/fastctx/src/shell/bash.rs +263 -0
  105. package/vendor/fastctx/src/shell/buffer.rs +108 -0
  106. package/vendor/fastctx/src/shell/encoding.rs +403 -0
  107. package/vendor/fastctx/src/shell/foreground.rs +115 -0
  108. package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
  109. package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
  110. package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
  111. package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
  112. package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
  113. package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
  114. package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
  115. package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
  116. package/vendor/fastctx/src/shell/mod.rs +345 -0
  117. package/vendor/fastctx/src/shell/normalize.rs +389 -0
  118. package/vendor/fastctx/src/shell/output.rs +406 -0
  119. package/vendor/fastctx/src/shell/process.rs +493 -0
  120. package/vendor/fastctx/src/shell_server.rs +156 -0
  121. package/vendor/fastctx/src/skip_report.rs +83 -0
  122. package/vendor/fastctx/src/stdio_transport.rs +177 -0
  123. package/vendor/fastctx/src/tool_schema.rs +204 -0
  124. package/vendor/fastctx/src/traversal.rs +846 -0
  125. package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
  126. package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
  127. package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
  128. package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
  129. package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
  130. package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
  131. package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
  132. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
  133. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
  134. package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
  135. package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
  136. package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
  137. package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
  138. package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
  139. package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
  140. package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
@@ -0,0 +1,403 @@
1
+ //! Delivery-time decoding for raw shell output.
2
+
3
+ use crate::encoding::decode_text;
4
+ use crate::shell::normalize::StreamEncoding;
5
+ use encoding_rs::{DecoderResult, Encoding, UTF_8, UTF_16BE, UTF_16LE};
6
+
7
+ const MAX_PRESENTED_LINE_CHARS: usize = 2_000;
8
+
9
+ /// One stored line borrowed from the foreground ring or a direct/legacy job record.
10
+ #[derive(Clone, Copy, Debug)]
11
+ pub(crate) struct EncodedLine<'a> {
12
+ pub(crate) bytes: &'a [u8],
13
+ pub(crate) total_bytes: u64,
14
+ pub(crate) stream_encoding: Option<StreamEncoding>,
15
+ pub(crate) legacy_text: Option<&'a str>,
16
+ pub(crate) known_truncated: bool,
17
+ }
18
+
19
+ /// A validated ASCII-compatible WHATWG label.
20
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
21
+ pub(crate) struct OutputEncoding {
22
+ encoding: &'static Encoding,
23
+ }
24
+
25
+ impl OutputEncoding {
26
+ pub(crate) fn label(self) -> &'static str {
27
+ self.encoding.name()
28
+ }
29
+ }
30
+
31
+ /// UTF-8 presentation plus notes and loss facts derived independently from the raw bytes.
32
+ #[derive(Clone, Debug, Default, Eq, PartialEq)]
33
+ pub(crate) struct DecodedLines {
34
+ pub(crate) lines: Vec<String>,
35
+ pub(crate) invalid_sequences: u64,
36
+ pub(crate) invalid_sequences_per_line: Vec<u64>,
37
+ pub(crate) truncated_per_line: Vec<bool>,
38
+ pub(crate) transcoding_note: Option<String>,
39
+ pub(crate) had_truncation: bool,
40
+ }
41
+
42
+ #[derive(Clone, Copy, Debug)]
43
+ enum DecodeKind {
44
+ Whatwg(&'static Encoding),
45
+ Wide(StreamEncoding),
46
+ }
47
+
48
+ #[derive(Clone, Copy, Debug)]
49
+ struct DecodePlan {
50
+ kind: DecodeKind,
51
+ requested: bool,
52
+ }
53
+
54
+ impl DecodePlan {
55
+ fn label(self) -> &'static str {
56
+ match self.kind {
57
+ DecodeKind::Whatwg(encoding) => encoding.name(),
58
+ DecodeKind::Wide(encoding) => encoding.label(),
59
+ }
60
+ }
61
+
62
+ fn is_utf8(self) -> bool {
63
+ matches!(self.kind, DecodeKind::Whatwg(encoding) if encoding == UTF_8)
64
+ }
65
+ }
66
+
67
+ #[derive(Debug)]
68
+ struct DecodedBytes {
69
+ text: String,
70
+ invalid_offsets: Vec<usize>,
71
+ }
72
+
73
+ /// Validates an output encoding before a command can cause side effects.
74
+ pub(crate) fn validate_output_encoding(value: &str) -> Result<OutputEncoding, String> {
75
+ let label = value.trim_matches(|character: char| character.is_ascii_whitespace());
76
+ if label.eq_ignore_ascii_case("utf-32")
77
+ || label.eq_ignore_ascii_case("utf-32le")
78
+ || label.eq_ignore_ascii_case("utf-32be")
79
+ {
80
+ return Err(wide_label_error(value));
81
+ }
82
+ let Some(encoding) = Encoding::for_label_no_replacement(label.as_bytes()) else {
83
+ return Err(format!(
84
+ // The examples must stay inside what this function accepts: UTF-16/UTF-32 labels are
85
+ // rejected below, so offering them here would send the caller straight back into a
86
+ // second rejection (2026-07-24).
87
+ "Invalid encoding value \"{value}\". Use a WHATWG encoding label such as \"gbk\", \"shift_jis\", \"big5\", \"euc-kr\", or \"windows-1252\"."
88
+ ));
89
+ };
90
+ if encoding == UTF_16LE || encoding == UTF_16BE {
91
+ return Err(wide_label_error(value));
92
+ }
93
+ Ok(OutputEncoding { encoding })
94
+ }
95
+
96
+ fn wide_label_error(value: &str) -> String {
97
+ format!(
98
+ "Encoding \"{value}\" is not supported for command output. UTF-16/UTF-32 output is decoded automatically when the stream starts with a BOM; otherwise redirect the command to a file (command > file 2>&1) and read it with the inspect_local_file tool."
99
+ )
100
+ }
101
+
102
+ /// Decodes a complete foreground capture, using trusted automatic detection when no label is given.
103
+ pub(crate) fn decode_run(
104
+ lines: &[EncodedLine<'_>],
105
+ explicit: Option<OutputEncoding>,
106
+ ) -> DecodedLines {
107
+ let plan = explicit.map_or_else(
108
+ || automatic_run_plan(lines),
109
+ |encoding| DecodePlan {
110
+ kind: DecodeKind::Whatwg(encoding.encoding),
111
+ requested: true,
112
+ },
113
+ );
114
+ decode_lines(lines, plan)
115
+ }
116
+
117
+ /// Decodes a job page without automatic detection.
118
+ pub(crate) fn decode_job(
119
+ lines: &[EncodedLine<'_>],
120
+ call_encoding: Option<OutputEncoding>,
121
+ job_encoding: Option<OutputEncoding>,
122
+ ) -> DecodedLines {
123
+ let plan = call_encoding
124
+ .or(job_encoding)
125
+ .map(|encoding| DecodePlan {
126
+ kind: DecodeKind::Whatwg(encoding.encoding),
127
+ requested: true,
128
+ })
129
+ .or_else(|| bom_plan(lines))
130
+ .unwrap_or(DecodePlan {
131
+ kind: DecodeKind::Whatwg(UTF_8),
132
+ requested: false,
133
+ });
134
+ decode_lines(lines, plan)
135
+ }
136
+
137
+ fn automatic_run_plan(lines: &[EncodedLine<'_>]) -> DecodePlan {
138
+ if let Some(plan) = bom_plan(lines) {
139
+ return plan;
140
+ }
141
+ let mut evidence = Vec::new();
142
+ for (index, line) in lines.iter().enumerate() {
143
+ if line.legacy_text.is_some() {
144
+ continue;
145
+ }
146
+ if index > 0 {
147
+ evidence.push(b'\n');
148
+ }
149
+ evidence.extend_from_slice(line.bytes);
150
+ }
151
+ match decode_text(&evidence).and_then(|decoded| decoded.source_encoding) {
152
+ Some(label) => {
153
+ let encoding = Encoding::for_label_no_replacement(label.as_bytes())
154
+ .expect("trusted detector returned a WHATWG encoding");
155
+ DecodePlan {
156
+ kind: DecodeKind::Whatwg(encoding),
157
+ requested: false,
158
+ }
159
+ }
160
+ None => DecodePlan {
161
+ kind: DecodeKind::Whatwg(UTF_8),
162
+ requested: false,
163
+ },
164
+ }
165
+ }
166
+
167
+ fn bom_plan(lines: &[EncodedLine<'_>]) -> Option<DecodePlan> {
168
+ lines
169
+ .iter()
170
+ .find_map(|line| line.stream_encoding)
171
+ .map(|encoding| DecodePlan {
172
+ kind: DecodeKind::Wide(encoding),
173
+ requested: false,
174
+ })
175
+ }
176
+
177
+ fn decode_lines(lines: &[EncodedLine<'_>], plan: DecodePlan) -> DecodedLines {
178
+ let mut decoded = DecodedLines {
179
+ transcoding_note: (!plan.is_utf8()).then(|| {
180
+ if plan.requested {
181
+ format!(
182
+ "(Note: decoded from {} as requested; output is UTF-8.)",
183
+ plan.label()
184
+ )
185
+ } else {
186
+ format!("(Note: decoded from {}; output is UTF-8.)", plan.label())
187
+ }
188
+ }),
189
+ ..DecodedLines::default()
190
+ };
191
+
192
+ for line in lines {
193
+ if let Some(text) = line.legacy_text {
194
+ decoded.lines.push(text.to_string());
195
+ decoded.invalid_sequences_per_line.push(0);
196
+ decoded.truncated_per_line.push(line.known_truncated);
197
+ decoded.had_truncation |= line.known_truncated;
198
+ continue;
199
+ }
200
+ let source = decode_bytes(line.bytes, plan.kind);
201
+ let (text, shown_end, truncated) =
202
+ present_line(&source.text, line.total_bytes, line.known_truncated);
203
+ let invalid_sequences = source
204
+ .invalid_offsets
205
+ .iter()
206
+ .filter(|offset| **offset < shown_end)
207
+ .count() as u64;
208
+ decoded.invalid_sequences = decoded.invalid_sequences.saturating_add(invalid_sequences);
209
+ decoded.invalid_sequences_per_line.push(invalid_sequences);
210
+ decoded.truncated_per_line.push(truncated);
211
+ decoded.had_truncation |= truncated;
212
+ decoded.lines.push(text);
213
+ }
214
+ decoded
215
+ }
216
+
217
+ fn present_line(text: &str, total_bytes: u64, known_truncated: bool) -> (String, usize, bool) {
218
+ let mut characters = text.char_indices();
219
+ let mut shown_end = text.len();
220
+ for _ in 0..MAX_PRESENTED_LINE_CHARS {
221
+ if characters.next().is_none() {
222
+ break;
223
+ }
224
+ }
225
+ let has_more_characters = if let Some((offset, _)) = characters.next() {
226
+ shown_end = offset;
227
+ true
228
+ } else {
229
+ false
230
+ };
231
+ let truncated = known_truncated || has_more_characters;
232
+ let mut shown = text[..shown_end].to_string();
233
+ if truncated {
234
+ shown.push_str(&format!("... [line truncated: {total_bytes} bytes total]"));
235
+ }
236
+ (shown, shown_end, truncated)
237
+ }
238
+
239
+ fn decode_bytes(bytes: &[u8], kind: DecodeKind) -> DecodedBytes {
240
+ match kind {
241
+ DecodeKind::Whatwg(encoding) => decode_whatwg(bytes, encoding),
242
+ DecodeKind::Wide(StreamEncoding::Utf16Le) => decode_whatwg(bytes, UTF_16LE),
243
+ DecodeKind::Wide(StreamEncoding::Utf16Be) => decode_whatwg(bytes, UTF_16BE),
244
+ DecodeKind::Wide(StreamEncoding::Utf32Le) => decode_utf32(bytes, true),
245
+ DecodeKind::Wide(StreamEncoding::Utf32Be) => decode_utf32(bytes, false),
246
+ }
247
+ }
248
+
249
+ fn decode_whatwg(bytes: &[u8], encoding: &'static Encoding) -> DecodedBytes {
250
+ let mut decoder = encoding.new_decoder_without_bom_handling();
251
+ let capacity = bytes.len().saturating_mul(4).saturating_add(16);
252
+ let mut text = String::with_capacity(capacity);
253
+ let mut invalid_offsets = Vec::new();
254
+ let mut offset = 0;
255
+ loop {
256
+ if text.capacity().saturating_sub(text.len()) < 16 {
257
+ text.reserve(bytes.len().saturating_sub(offset).saturating_mul(4).max(64));
258
+ }
259
+ let (result, read) =
260
+ decoder.decode_to_string_without_replacement(&bytes[offset..], &mut text, true);
261
+ offset = offset.saturating_add(read);
262
+ match result {
263
+ DecoderResult::InputEmpty => break,
264
+ DecoderResult::OutputFull => {
265
+ text.reserve(bytes.len().saturating_sub(offset).saturating_mul(4).max(64));
266
+ }
267
+ DecoderResult::Malformed(_, _) => {
268
+ invalid_offsets.push(text.len());
269
+ text.push(char::REPLACEMENT_CHARACTER);
270
+ if read == 0 && offset < bytes.len() {
271
+ offset += 1;
272
+ decoder = encoding.new_decoder_without_bom_handling();
273
+ }
274
+ }
275
+ }
276
+ }
277
+ DecodedBytes {
278
+ text,
279
+ invalid_offsets,
280
+ }
281
+ }
282
+
283
+ fn decode_utf32(bytes: &[u8], little_endian: bool) -> DecodedBytes {
284
+ let mut text = String::with_capacity(bytes.len());
285
+ let mut invalid_offsets = Vec::new();
286
+ let (chunks, remainder) = bytes.as_chunks::<4>();
287
+ for chunk in chunks {
288
+ let value = if little_endian {
289
+ u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])
290
+ } else {
291
+ u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])
292
+ };
293
+ if let Some(character) = char::from_u32(value) {
294
+ text.push(character);
295
+ } else {
296
+ invalid_offsets.push(text.len());
297
+ text.push(char::REPLACEMENT_CHARACTER);
298
+ }
299
+ }
300
+ if !remainder.is_empty() {
301
+ invalid_offsets.push(text.len());
302
+ text.push(char::REPLACEMENT_CHARACTER);
303
+ }
304
+ DecodedBytes {
305
+ text,
306
+ invalid_offsets,
307
+ }
308
+ }
309
+
310
+ pub(crate) fn run_garble_note(invalid_sequences: u64) -> Option<String> {
311
+ if invalid_sequences == 0 {
312
+ return None;
313
+ }
314
+ let noun = if invalid_sequences == 1 {
315
+ "sequence"
316
+ } else {
317
+ "sequences"
318
+ };
319
+ Some(match legacy_code_page_label() {
320
+ Some(label) => format!(
321
+ "(Note: {invalid_sequences} invalid byte {noun} shown as U+FFFD — the command likely wrote {label}, this system's legacy code page. Re-run with encoding=\"{label}\", or redirect to a file and use the inspect_local_file tool.)"
322
+ ),
323
+ None => format!(
324
+ "(Note: {invalid_sequences} invalid byte {noun} shown as U+FFFD. If the text looks garbled, pass the source encoding via the encoding parameter.)"
325
+ ),
326
+ })
327
+ }
328
+
329
+ pub(crate) fn job_garble_note(invalid_sequences: u64, anchor: u64) -> Option<String> {
330
+ if invalid_sequences == 0 {
331
+ return None;
332
+ }
333
+ let noun = if invalid_sequences == 1 {
334
+ "sequence"
335
+ } else {
336
+ "sequences"
337
+ };
338
+ Some(match legacy_code_page_label() {
339
+ Some(label) => format!(
340
+ "(Note: {invalid_sequences} invalid byte {noun} shown as U+FFFD — the job likely wrote {label}, this system's legacy code page. Call job_output again with after_seq={anchor} and encoding=\"{label}\" to re-read this page.)"
341
+ ),
342
+ None => format!(
343
+ "(Note: {invalid_sequences} invalid byte {noun} shown as U+FFFD. If the text looks garbled, call job_output again with after_seq={anchor} and the source encoding via encoding.)"
344
+ ),
345
+ })
346
+ }
347
+
348
+ #[cfg(windows)]
349
+ fn legacy_code_page_label() -> Option<&'static str> {
350
+ use windows_sys::Win32::Globalization::GetACP;
351
+
352
+ // SAFETY: GetACP has no preconditions and returns process-global system state.
353
+ match unsafe { GetACP() } {
354
+ 874 => Some("windows-874"),
355
+ 932 => Some("shift_jis"),
356
+ 936 => Some("gbk"),
357
+ 949 => Some("euc-kr"),
358
+ 950 => Some("big5"),
359
+ 1_250 => Some("windows-1250"),
360
+ 1_251 => Some("windows-1251"),
361
+ 1_252 => Some("windows-1252"),
362
+ 1_253 => Some("windows-1253"),
363
+ 1_254 => Some("windows-1254"),
364
+ 1_255 => Some("windows-1255"),
365
+ 1_256 => Some("windows-1256"),
366
+ 1_257 => Some("windows-1257"),
367
+ 1_258 => Some("windows-1258"),
368
+ 54_936 => Some("gb18030"),
369
+ _ => None,
370
+ }
371
+ }
372
+
373
+ #[cfg(not(windows))]
374
+ fn legacy_code_page_label() -> Option<&'static str> {
375
+ None
376
+ }
377
+
378
+ #[cfg(test)]
379
+ mod tests {
380
+ use super::{EncodedLine, decode_run, validate_output_encoding};
381
+ use crate::shell::normalize::StreamEncoding;
382
+
383
+ #[test]
384
+ fn utf16_bom_locked_lines_decode_without_accepting_wide_parameters() {
385
+ let wide = EncodedLine {
386
+ bytes: &[b'a', 0, b'b', 0],
387
+ total_bytes: 4,
388
+ stream_encoding: Some(StreamEncoding::Utf16Le),
389
+ legacy_text: None,
390
+ known_truncated: false,
391
+ };
392
+ let decoded = decode_run(&[wide], None);
393
+ assert_eq!(decoded.lines, ["ab"]);
394
+ assert_eq!(
395
+ decoded.transcoding_note.as_deref(),
396
+ Some("(Note: decoded from UTF-16LE; output is UTF-8.)")
397
+ );
398
+ assert_eq!(
399
+ validate_output_encoding("utf-16le").unwrap_err(),
400
+ "Encoding \"utf-16le\" is not supported for command output. UTF-16/UTF-32 output is decoded automatically when the stream starts with a BOM; otherwise redirect the command to a file (command > file 2>&1) and read it with the inspect_local_file tool."
401
+ );
402
+ }
403
+ }
@@ -0,0 +1,115 @@
1
+ //! Foreground command supervision with timeout and output-independent process life.
2
+
3
+ use crate::model::ToolResponse;
4
+ use crate::shell::encoding::OutputEncoding;
5
+ use crate::shell::output::{capture_foreground, format_foreground};
6
+ use crate::shell::process::{exit_code, spawn_bash};
7
+ use std::path::Path;
8
+ use std::time::{Duration, Instant};
9
+
10
+ pub(crate) struct ForegroundCommand<'a> {
11
+ pub(crate) bash: &'a Path,
12
+ pub(crate) command: &'a str,
13
+ pub(crate) cwd: &'a Path,
14
+ pub(crate) timeout_ms: u64,
15
+ pub(crate) login_shell: bool,
16
+ pub(crate) encoding: Option<OutputEncoding>,
17
+ pub(crate) environment: &'a crate::session::SessionEnvironment,
18
+ pub(crate) utf8_locale: &'a str,
19
+ }
20
+
21
+ /// Runs one command while reserving enough time for MCP response serialization.
22
+ pub(crate) fn run(spec: ForegroundCommand<'_>, cancelled: impl Fn() -> bool) -> ToolResponse {
23
+ let ForegroundCommand {
24
+ bash,
25
+ command,
26
+ cwd,
27
+ timeout_ms,
28
+ login_shell,
29
+ encoding,
30
+ environment,
31
+ utf8_locale,
32
+ } = spec;
33
+ let mut process = match spawn_bash(bash, command, cwd, login_shell, environment, utf8_locale) {
34
+ Ok(process) => process,
35
+ Err(error) => return ToolResponse::error(format!("Cannot start the command: {error}.")),
36
+ };
37
+ let reader = process.take_output();
38
+ let reader_thread = std::thread::spawn(move || capture_foreground(reader));
39
+
40
+ let started = Instant::now();
41
+ let mut timed_out = false;
42
+ let status = loop {
43
+ match process.try_wait() {
44
+ Ok(Some(status)) => {
45
+ // A shell that detached descendants must not leave them outside this tool's lifetime.
46
+ if let Err(error) = process.terminate_tree() {
47
+ drop(reader_thread);
48
+ return ToolResponse::error(format!(
49
+ "The command exited, but its remaining process tree could not be terminated: {error}. Stop the descendants manually before retrying."
50
+ ));
51
+ }
52
+ break status;
53
+ }
54
+ Ok(None) => {}
55
+ Err(error) => {
56
+ if let Err(kill_error) = process.terminate_tree() {
57
+ drop(reader_thread);
58
+ return ToolResponse::error(format!(
59
+ "Cannot monitor the command process ({error}), and its process tree could not be terminated ({kill_error}). Stop it manually and retry."
60
+ ));
61
+ }
62
+ let _ = reader_thread.join();
63
+ return ToolResponse::error(format!(
64
+ "Cannot monitor the command process: {error}. Retry the command."
65
+ ));
66
+ }
67
+ }
68
+ if cancelled() {
69
+ if let Err(error) = process.terminate_tree() {
70
+ drop(reader_thread);
71
+ return ToolResponse::error(format!(
72
+ "The command was cancelled, but its process tree could not be terminated: {error}. Stop it manually before retrying."
73
+ ));
74
+ }
75
+ let _ = reader_thread.join();
76
+ return ToolResponse::error(
77
+ "The command was cancelled and its process tree was terminated.".to_string(),
78
+ );
79
+ }
80
+ if started.elapsed() >= Duration::from_millis(timeout_ms) {
81
+ timed_out = true;
82
+ match process.terminate_tree() {
83
+ Ok(status) => break status,
84
+ Err(error) => {
85
+ drop(reader_thread);
86
+ return ToolResponse::error(format!(
87
+ "The command timed out after {timeout_ms} ms, but its process tree could not be terminated: {error}. Stop it manually and retry."
88
+ ));
89
+ }
90
+ }
91
+ }
92
+ std::thread::sleep(Duration::from_millis(10));
93
+ };
94
+
95
+ let captured = match reader_thread.join() {
96
+ Ok(Ok(output)) => output,
97
+ Ok(Err(error)) => {
98
+ return ToolResponse::error(format!(
99
+ "Cannot read command output: {error}. Retry the command or redirect output to a file."
100
+ ));
101
+ }
102
+ Err(_) => {
103
+ return ToolResponse::error(
104
+ "Internal tool failure: the command-output reader panicked.".to_string(),
105
+ );
106
+ }
107
+ };
108
+ format_foreground(
109
+ &captured,
110
+ command,
111
+ exit_code(status),
112
+ timed_out.then_some(timeout_ms),
113
+ encoding,
114
+ )
115
+ }
@@ -0,0 +1,91 @@
1
+ // Modified by dsh-ops (https://github.com/T-Auto/dsh-ops): removed advance_generation and its generation-path field, which only the deleted Apply/Unapply path used to publish.
2
+ //! Cross-process admission barrier for job starts and running limits.
3
+
4
+ use crate::control::paths::ControlPaths;
5
+ use sha2::{Digest, Sha256};
6
+ use std::fs::File;
7
+ use std::path::Path;
8
+
9
+ /// Exclusive admission guard. It does not serialize job-record readers or log writers.
10
+ pub(crate) struct AdmissionGuard {
11
+ file: File,
12
+ generation: u64,
13
+ }
14
+
15
+ impl AdmissionGuard {
16
+ pub(crate) fn acquire(paths: &ControlPaths) -> Result<Self, String> {
17
+ let directory = crate::edit::private_storage::job_control_directory();
18
+ crate::edit::private_storage::ensure_private_directory(
19
+ &directory,
20
+ "background job control",
21
+ )?;
22
+ let home_key = hex::encode(Sha256::digest(home_identity(&paths.home)));
23
+ let lock_path = directory.join(format!("{home_key}.lock"));
24
+ let generation_path = directory.join(format!("{home_key}.generation"));
25
+ let file = crate::edit::private_storage::open_lock_file(
26
+ &lock_path,
27
+ "background job admission lock",
28
+ )?;
29
+ fs2::FileExt::lock_exclusive(&file).map_err(|error| {
30
+ format!(
31
+ "Cannot lock background job admission at {}: {error}. Retry after the other FastCtx process finishes.",
32
+ crate::paths::display_path(&lock_path)
33
+ )
34
+ })?;
35
+ let generation = read_generation(&generation_path)?;
36
+ Ok(Self { file, generation })
37
+ }
38
+
39
+ pub(crate) fn generation(&self) -> u64 {
40
+ self.generation
41
+ }
42
+ }
43
+
44
+ impl Drop for AdmissionGuard {
45
+ fn drop(&mut self) {
46
+ let _ = fs2::FileExt::unlock(&self.file);
47
+ }
48
+ }
49
+
50
+ pub(crate) fn observe_generation(paths: &ControlPaths) -> Result<u64, String> {
51
+ Ok(AdmissionGuard::acquire(paths)?.generation())
52
+ }
53
+
54
+ fn read_generation(path: &Path) -> Result<u64, String> {
55
+ let source = match std::fs::read_to_string(path) {
56
+ Ok(source) => source,
57
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(0),
58
+ Err(error) => {
59
+ return Err(format!(
60
+ "Cannot read the background job admission generation {}: {error}",
61
+ crate::paths::display_path(path)
62
+ ));
63
+ }
64
+ };
65
+ source.trim().parse::<u64>().map_err(|error| {
66
+ format!(
67
+ "Cannot parse the background job admission generation {}: {error}. Remove this damaged runtime file and restart FastCtx.",
68
+ crate::paths::display_path(path)
69
+ )
70
+ })
71
+ }
72
+
73
+ #[cfg(unix)]
74
+ fn home_identity(home: &Path) -> Vec<u8> {
75
+ use std::os::unix::ffi::OsStrExt;
76
+ home.as_os_str().as_bytes().to_vec()
77
+ }
78
+
79
+ #[cfg(windows)]
80
+ fn home_identity(home: &Path) -> Vec<u8> {
81
+ use std::os::windows::ffi::OsStrExt;
82
+ home.as_os_str()
83
+ .encode_wide()
84
+ .flat_map(u16::to_le_bytes)
85
+ .collect()
86
+ }
87
+
88
+ #[cfg(not(any(unix, windows)))]
89
+ fn home_identity(home: &Path) -> Vec<u8> {
90
+ home.to_string_lossy().as_bytes().to_vec()
91
+ }