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.
- package/CHANGELOG.md +202 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +435 -0
- package/README.en.md +126 -0
- package/README.md +115 -2
- package/README.zh.md +116 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/release-0.2.1.md +72 -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 +998 -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,106 @@
|
|
|
1
|
+
// Modified by dsh-ops (https://github.com/T-Auto/dsh-ops): removed the TUI-only editable-input parser and its error type, which the deleted src/tui/ editors were the only callers of.
|
|
2
|
+
//! Shared detection and validation for grep/glob CPU parallelism.
|
|
3
|
+
|
|
4
|
+
use std::fmt;
|
|
5
|
+
|
|
6
|
+
/// Hard ceiling for effective `P`: one request-local base lane plus shared extra lanes.
|
|
7
|
+
pub(crate) const MAX_SEARCH_PARALLELISM: usize = 16;
|
|
8
|
+
|
|
9
|
+
/// Resolved search parallelism using the same ceiling as the executor.
|
|
10
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
11
|
+
pub(crate) struct SearchParallelism {
|
|
12
|
+
/// Engine-visible CPU ceiling derived from `available_parallelism`.
|
|
13
|
+
pub(crate) available: usize,
|
|
14
|
+
/// Explicit user limit, or `None` for automatic parallelism.
|
|
15
|
+
pub(crate) configured: Option<usize>,
|
|
16
|
+
/// Effective `P`, including the request-local base lane.
|
|
17
|
+
pub(crate) effective: usize,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Range failure for a typed persisted CPU limit.
|
|
21
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
22
|
+
pub(crate) struct SearchParallelismRangeError {
|
|
23
|
+
pub(crate) maximum: usize,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl fmt::Display for SearchParallelismRangeError {
|
|
27
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
28
|
+
write!(
|
|
29
|
+
formatter,
|
|
30
|
+
"must be an integer in 1..={} or omitted for automatic parallelism",
|
|
31
|
+
self.maximum
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Detects the exact upper bound used by the search executor.
|
|
37
|
+
pub(crate) fn detected_available() -> usize {
|
|
38
|
+
std::thread::available_parallelism()
|
|
39
|
+
.map(usize::from)
|
|
40
|
+
.unwrap_or(1)
|
|
41
|
+
.clamp(1, MAX_SEARCH_PARALLELISM)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Resolves an optional persisted limit against the current engine-visible ceiling.
|
|
45
|
+
pub(crate) fn resolve(
|
|
46
|
+
configured: Option<i64>,
|
|
47
|
+
) -> Result<SearchParallelism, SearchParallelismRangeError> {
|
|
48
|
+
resolve_with_available(configured, detected_available())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn resolve_with_available(
|
|
52
|
+
configured: Option<i64>,
|
|
53
|
+
available: usize,
|
|
54
|
+
) -> Result<SearchParallelism, SearchParallelismRangeError> {
|
|
55
|
+
let available = available.clamp(1, MAX_SEARCH_PARALLELISM);
|
|
56
|
+
let configured = configured
|
|
57
|
+
.map(|value| validate(value, available))
|
|
58
|
+
.transpose()?;
|
|
59
|
+
Ok(SearchParallelism {
|
|
60
|
+
available,
|
|
61
|
+
configured,
|
|
62
|
+
effective: configured.unwrap_or(available),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn validate(value: i64, maximum: usize) -> Result<usize, SearchParallelismRangeError> {
|
|
67
|
+
usize::try_from(value)
|
|
68
|
+
.ok()
|
|
69
|
+
.filter(|value| (1..=maximum).contains(value))
|
|
70
|
+
.ok_or(SearchParallelismRangeError { maximum })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[cfg(test)]
|
|
74
|
+
mod tests {
|
|
75
|
+
use super::{MAX_SEARCH_PARALLELISM, resolve_with_available};
|
|
76
|
+
|
|
77
|
+
#[test]
|
|
78
|
+
fn engine_ceiling_and_configured_limit_share_one_resolution_path() {
|
|
79
|
+
let automatic = resolve_with_available(None, 64).unwrap();
|
|
80
|
+
assert_eq!(automatic.available, MAX_SEARCH_PARALLELISM);
|
|
81
|
+
assert_eq!(automatic.configured, None);
|
|
82
|
+
assert_eq!(automatic.effective, MAX_SEARCH_PARALLELISM);
|
|
83
|
+
|
|
84
|
+
let configured = resolve_with_available(Some(4), 64).unwrap();
|
|
85
|
+
assert_eq!(configured.available, MAX_SEARCH_PARALLELISM);
|
|
86
|
+
assert_eq!(configured.configured, Some(4));
|
|
87
|
+
assert_eq!(configured.effective, 4);
|
|
88
|
+
|
|
89
|
+
let fallback_machine = resolve_with_available(None, 0).unwrap();
|
|
90
|
+
assert_eq!(fallback_machine.available, 1);
|
|
91
|
+
assert_eq!(fallback_machine.effective, 1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[test]
|
|
95
|
+
fn persisted_limits_accept_both_boundaries_and_reject_every_out_of_range_shape() {
|
|
96
|
+
for value in [1, 4, 8] {
|
|
97
|
+
let resolved = resolve_with_available(Some(value), 8).unwrap();
|
|
98
|
+
assert_eq!(resolved.configured, Some(value as usize));
|
|
99
|
+
assert_eq!(resolved.effective, value as usize);
|
|
100
|
+
}
|
|
101
|
+
for value in [i64::MIN, -1, 0, 9, i64::MAX] {
|
|
102
|
+
let error = resolve_with_available(Some(value), 8).unwrap_err();
|
|
103
|
+
assert_eq!(error.maximum, 8);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
//! Immutable UTF-8 search input shared by ripgrep callbacks and captured ranges.
|
|
2
|
+
|
|
3
|
+
use crate::file_snapshot::{SnapshotByteRange, SnapshotReader};
|
|
4
|
+
use crate::operation::{WorkCheckpoint, WorkStop};
|
|
5
|
+
use std::fs::File;
|
|
6
|
+
use std::io::{self, BufReader, Cursor, Read, Seek, SeekFrom, Write};
|
|
7
|
+
use std::ops::Range;
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
|
|
10
|
+
const MEMORY_TEXT_LIMIT: usize = 8 * 1024 * 1024;
|
|
11
|
+
const TEXT_CHUNK_BYTES: usize = 64 * 1024;
|
|
12
|
+
|
|
13
|
+
#[derive(Debug)]
|
|
14
|
+
struct TempText {
|
|
15
|
+
file: tempfile::NamedTempFile,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl TempText {
|
|
19
|
+
fn open_reader(&self) -> io::Result<TextReader<'static>> {
|
|
20
|
+
Ok(TextReader::Temp(BufReader::new(self.file.reopen()?)))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fn read_range(&self, range: Range<u64>) -> io::Result<Vec<u8>> {
|
|
24
|
+
let range_len = range.end.checked_sub(range.start).ok_or_else(|| {
|
|
25
|
+
io::Error::new(
|
|
26
|
+
io::ErrorKind::InvalidData,
|
|
27
|
+
"captured text range is reversed",
|
|
28
|
+
)
|
|
29
|
+
})?;
|
|
30
|
+
let length = usize::try_from(range_len).map_err(|_| {
|
|
31
|
+
io::Error::new(
|
|
32
|
+
io::ErrorKind::InvalidData,
|
|
33
|
+
"captured text range does not fit in memory",
|
|
34
|
+
)
|
|
35
|
+
})?;
|
|
36
|
+
let mut file = self.file.reopen()?;
|
|
37
|
+
file.seek(SeekFrom::Start(range.start))?;
|
|
38
|
+
let mut bytes = vec![0_u8; length];
|
|
39
|
+
file.read_exact(&mut bytes)?;
|
|
40
|
+
Ok(bytes)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[derive(Debug)]
|
|
45
|
+
enum TextStorage {
|
|
46
|
+
Snapshot(SnapshotByteRange),
|
|
47
|
+
Memory(Arc<[u8]>),
|
|
48
|
+
Temp(Arc<TempText>),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// One immutable decoded UTF-8 source whose offsets are ripgrep's absolute offsets.
|
|
52
|
+
#[derive(Debug)]
|
|
53
|
+
pub(crate) struct SearchText {
|
|
54
|
+
len: u64,
|
|
55
|
+
storage: TextStorage,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl SearchText {
|
|
59
|
+
/// Reuses a validated UTF-8 snapshot suffix without copying its memory or temp backing.
|
|
60
|
+
pub(crate) fn from_snapshot(range: SnapshotByteRange) -> Arc<Self> {
|
|
61
|
+
Arc::new(Self {
|
|
62
|
+
len: range.len(),
|
|
63
|
+
storage: TextStorage::Snapshot(range),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Captures a validated UTF-8 reader once, spilling above 8 MiB without an unbounded heap copy.
|
|
68
|
+
pub(crate) fn capture(
|
|
69
|
+
mut reader: impl Read,
|
|
70
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
71
|
+
) -> Result<Arc<Self>, SearchTextFailure> {
|
|
72
|
+
let mut memory = Vec::with_capacity(MEMORY_TEXT_LIMIT.min(TEXT_CHUNK_BYTES));
|
|
73
|
+
let mut temp: Option<tempfile::NamedTempFile> = None;
|
|
74
|
+
let mut total = 0_u64;
|
|
75
|
+
let mut chunk = [0_u8; TEXT_CHUNK_BYTES];
|
|
76
|
+
|
|
77
|
+
loop {
|
|
78
|
+
check_operation(operation)?;
|
|
79
|
+
let read = reader.read(&mut chunk).map_err(SearchTextFailure::Io)?;
|
|
80
|
+
check_operation(operation)?;
|
|
81
|
+
if read == 0 {
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
total = total.checked_add(read as u64).ok_or_else(|| {
|
|
85
|
+
SearchTextFailure::Io(io::Error::other("search text is too large"))
|
|
86
|
+
})?;
|
|
87
|
+
|
|
88
|
+
if temp.is_none() && memory.len().saturating_add(read) > MEMORY_TEXT_LIMIT {
|
|
89
|
+
let mut promoted = tempfile::NamedTempFile::new().map_err(SearchTextFailure::Io)?;
|
|
90
|
+
promoted.write_all(&memory).map_err(SearchTextFailure::Io)?;
|
|
91
|
+
memory.clear();
|
|
92
|
+
temp = Some(promoted);
|
|
93
|
+
}
|
|
94
|
+
if let Some(file) = &mut temp {
|
|
95
|
+
file.write_all(&chunk[..read])
|
|
96
|
+
.map_err(SearchTextFailure::Io)?;
|
|
97
|
+
} else {
|
|
98
|
+
memory.extend_from_slice(&chunk[..read]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
check_operation(operation)?;
|
|
103
|
+
let storage = if let Some(mut file) = temp {
|
|
104
|
+
file.flush().map_err(SearchTextFailure::Io)?;
|
|
105
|
+
check_operation(operation)?;
|
|
106
|
+
TextStorage::Temp(Arc::new(TempText { file }))
|
|
107
|
+
} else {
|
|
108
|
+
TextStorage::Memory(Arc::from(memory))
|
|
109
|
+
};
|
|
110
|
+
Ok(Arc::new(Self {
|
|
111
|
+
len: total,
|
|
112
|
+
storage,
|
|
113
|
+
}))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub(crate) fn memory_bytes(&self) -> Option<&[u8]> {
|
|
117
|
+
match &self.storage {
|
|
118
|
+
TextStorage::Snapshot(range) => range.memory_bytes(),
|
|
119
|
+
TextStorage::Memory(bytes) => Some(bytes),
|
|
120
|
+
TextStorage::Temp(_) => None,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub(crate) fn open_reader(&self) -> io::Result<TextReader<'_>> {
|
|
125
|
+
match &self.storage {
|
|
126
|
+
TextStorage::Snapshot(range) => Ok(TextReader::Snapshot(range.open_reader()?)),
|
|
127
|
+
TextStorage::Memory(bytes) => Ok(TextReader::Memory(Cursor::new(bytes))),
|
|
128
|
+
TextStorage::Temp(temp) => temp.open_reader(),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
pub(crate) fn range_str(&self, range: Range<u64>) -> io::Result<RangeText<'_>> {
|
|
133
|
+
self.validate_range(&range)?;
|
|
134
|
+
match &self.storage {
|
|
135
|
+
TextStorage::Snapshot(backing) => {
|
|
136
|
+
if let Some(bytes) = backing.memory_bytes() {
|
|
137
|
+
let text = borrowed_utf8_range(bytes, range)?;
|
|
138
|
+
Ok(RangeText::Borrowed(text))
|
|
139
|
+
} else {
|
|
140
|
+
let bytes = backing.read_range(range)?;
|
|
141
|
+
let text = String::from_utf8(bytes)
|
|
142
|
+
.map_err(|error| invalid_utf8_range(error.utf8_error()))?;
|
|
143
|
+
Ok(RangeText::Owned(Arc::from(text)))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
TextStorage::Memory(bytes) => {
|
|
147
|
+
let text = borrowed_utf8_range(bytes, range)?;
|
|
148
|
+
Ok(RangeText::Borrowed(text))
|
|
149
|
+
}
|
|
150
|
+
TextStorage::Temp(temp) => {
|
|
151
|
+
let bytes = temp.read_range(range)?;
|
|
152
|
+
let text = String::from_utf8(bytes)
|
|
153
|
+
.map_err(|error| invalid_utf8_range(error.utf8_error()))?;
|
|
154
|
+
Ok(RangeText::Owned(Arc::from(text)))
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn validate_range(&self, range: &Range<u64>) -> io::Result<()> {
|
|
160
|
+
if range.start <= range.end && range.end <= self.len {
|
|
161
|
+
Ok(())
|
|
162
|
+
} else {
|
|
163
|
+
Err(io::Error::new(
|
|
164
|
+
io::ErrorKind::InvalidData,
|
|
165
|
+
"ripgrep reported a byte range outside its immutable search input",
|
|
166
|
+
))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn borrowed_utf8_range(bytes: &[u8], range: Range<u64>) -> io::Result<&str> {
|
|
172
|
+
let start = usize::try_from(range.start).map_err(|_| text_range_error())?;
|
|
173
|
+
let end = usize::try_from(range.end).map_err(|_| text_range_error())?;
|
|
174
|
+
let bytes = bytes.get(start..end).ok_or_else(text_range_error)?;
|
|
175
|
+
std::str::from_utf8(bytes).map_err(invalid_utf8_range)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn text_range_error() -> io::Error {
|
|
179
|
+
io::Error::new(
|
|
180
|
+
io::ErrorKind::InvalidData,
|
|
181
|
+
"captured text range lies outside its immutable search input",
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn invalid_utf8_range(error: std::str::Utf8Error) -> io::Error {
|
|
186
|
+
io::Error::new(
|
|
187
|
+
io::ErrorKind::InvalidData,
|
|
188
|
+
format!("ripgrep reported a range that is not valid UTF-8: {error}"),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn check_operation(operation: Option<&dyn WorkCheckpoint>) -> Result<(), SearchTextFailure> {
|
|
193
|
+
match operation.map(WorkCheckpoint::check_work) {
|
|
194
|
+
Some(Err(stop)) => Err(SearchTextFailure::Stopped(stop)),
|
|
195
|
+
Some(Ok(())) | None => Ok(()),
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// A decoded text range, borrowed in memory and owned only when read from temp storage.
|
|
200
|
+
#[derive(Debug)]
|
|
201
|
+
pub(crate) enum RangeText<'a> {
|
|
202
|
+
Borrowed(&'a str),
|
|
203
|
+
Owned(Arc<str>),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// A reader over the same immutable decoded UTF-8 backing.
|
|
207
|
+
pub(crate) enum TextReader<'a> {
|
|
208
|
+
Snapshot(SnapshotReader<'a>),
|
|
209
|
+
Memory(Cursor<&'a [u8]>),
|
|
210
|
+
Temp(BufReader<File>),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
impl Read for TextReader<'_> {
|
|
214
|
+
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
|
|
215
|
+
match self {
|
|
216
|
+
Self::Snapshot(reader) => reader.read(output),
|
|
217
|
+
Self::Memory(reader) => reader.read(output),
|
|
218
|
+
Self::Temp(reader) => reader.read(output),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[derive(Debug)]
|
|
224
|
+
pub(crate) enum SearchTextFailure {
|
|
225
|
+
Io(io::Error),
|
|
226
|
+
Stopped(WorkStop),
|
|
227
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
//! Unified rmcp registration, feature gating, and shared tool state.
|
|
2
|
+
|
|
3
|
+
use crate::budget::{GLOB_TOKEN_BUDGET_ENV, GREP_TOKEN_BUDGET_ENV, READ_TOKEN_BUDGET_ENV};
|
|
4
|
+
use crate::edit::ReplaceService;
|
|
5
|
+
use crate::file_executor::GrepGlobExecutor;
|
|
6
|
+
use crate::glob_tool::{GlobRequest, glob_files_cancellable};
|
|
7
|
+
use crate::grep_tool::{GrepRequest, grep_files_cancellable};
|
|
8
|
+
use crate::read_tool::{ReadRequest, read_file};
|
|
9
|
+
use crate::server_manifest::{ToolContract, ToolManifest};
|
|
10
|
+
use crate::server_support::{
|
|
11
|
+
BudgetRetry, CancellableBlockingRequest, run_blocking, run_blocking_cancellable,
|
|
12
|
+
};
|
|
13
|
+
use crate::session::SessionContext;
|
|
14
|
+
use crate::shell::FastShell;
|
|
15
|
+
use rmcp::handler::server::router::tool::ToolRouter;
|
|
16
|
+
use rmcp::handler::server::wrapper::Parameters;
|
|
17
|
+
use rmcp::model::{CallToolResult, Implementation, ServerCapabilities, ServerInfo};
|
|
18
|
+
use rmcp::service::RequestContext;
|
|
19
|
+
use rmcp::{RoleServer, ServerHandler, tool, tool_handler, tool_router};
|
|
20
|
+
use std::sync::Arc;
|
|
21
|
+
use tokio::sync::Semaphore;
|
|
22
|
+
|
|
23
|
+
const MAX_FILE_OPERATIONS: usize = 8;
|
|
24
|
+
const MAX_SHELL_OPERATIONS: usize = 16;
|
|
25
|
+
const MAX_REPLACE_OPERATIONS: usize = 8;
|
|
26
|
+
|
|
27
|
+
/// Optional tool groups published by the single `fastctx` server.
|
|
28
|
+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
29
|
+
pub struct ServerOptions {
|
|
30
|
+
/// Publish the five shell tools.
|
|
31
|
+
pub enable_shell: bool,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
impl ServerOptions {
|
|
35
|
+
/// Enables all nine tools; intended for contract tests and doctor probes.
|
|
36
|
+
pub const fn all() -> Self {
|
|
37
|
+
Self { enable_shell: true }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The single stateful MCP server for default file tools and the optional shell group.
|
|
42
|
+
#[derive(Clone, Debug)]
|
|
43
|
+
pub struct FastCtxServer {
|
|
44
|
+
tool_router: ToolRouter<Self>,
|
|
45
|
+
options: ServerOptions,
|
|
46
|
+
pub(crate) shell: FastShell,
|
|
47
|
+
pub(crate) replace: ReplaceService,
|
|
48
|
+
pub(crate) file_permits: Arc<Semaphore>,
|
|
49
|
+
pub(crate) grep_glob_executor: Arc<GrepGlobExecutor>,
|
|
50
|
+
pub(crate) shell_permits: Arc<Semaphore>,
|
|
51
|
+
pub(crate) replace_permits: Arc<Semaphore>,
|
|
52
|
+
pub(crate) session: Arc<SessionContext>,
|
|
53
|
+
pub(crate) activity: Arc<crate::runtime::activity::RuntimeActivity>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Expensive executors and process-wide admission gates shared by every control-center session.
|
|
57
|
+
#[derive(Clone, Debug)]
|
|
58
|
+
pub struct SharedRuntime {
|
|
59
|
+
file_permits: Arc<Semaphore>,
|
|
60
|
+
grep_glob_executor: Arc<GrepGlobExecutor>,
|
|
61
|
+
shell_permits: Arc<Semaphore>,
|
|
62
|
+
replace: ReplaceService,
|
|
63
|
+
replace_permits: Arc<Semaphore>,
|
|
64
|
+
activity: Arc<crate::runtime::activity::RuntimeActivity>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
impl SharedRuntime {
|
|
68
|
+
/// Creates one per-user runtime around the configured search executor.
|
|
69
|
+
pub(crate) fn new(grep_glob_executor: Arc<GrepGlobExecutor>) -> Arc<Self> {
|
|
70
|
+
Self::with_activity(
|
|
71
|
+
grep_glob_executor,
|
|
72
|
+
crate::runtime::activity::RuntimeActivity::new(),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub(crate) fn with_activity(
|
|
77
|
+
grep_glob_executor: Arc<GrepGlobExecutor>,
|
|
78
|
+
activity: Arc<crate::runtime::activity::RuntimeActivity>,
|
|
79
|
+
) -> Arc<Self> {
|
|
80
|
+
Arc::new(Self {
|
|
81
|
+
file_permits: Arc::new(Semaphore::new(MAX_FILE_OPERATIONS)),
|
|
82
|
+
grep_glob_executor,
|
|
83
|
+
shell_permits: Arc::new(Semaphore::new(MAX_SHELL_OPERATIONS)),
|
|
84
|
+
replace: ReplaceService::new(),
|
|
85
|
+
replace_permits: Arc::new(Semaphore::new(MAX_REPLACE_OPERATIONS)),
|
|
86
|
+
activity,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl FastCtxServer {
|
|
92
|
+
/// Creates the default four-tool server, including byte-preserving replacement.
|
|
93
|
+
pub fn new() -> Self {
|
|
94
|
+
Self::with_options(ServerOptions::default())
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Creates one server whose visible tools are selected by startup flags.
|
|
98
|
+
pub fn with_options(options: ServerOptions) -> Self {
|
|
99
|
+
Self::with_options_and_executor(options, GrepGlobExecutor::shared())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Creates a server with the process-startup search executor selected by current-user config.
|
|
103
|
+
pub(crate) fn with_options_and_executor(
|
|
104
|
+
options: ServerOptions,
|
|
105
|
+
grep_glob_executor: Arc<GrepGlobExecutor>,
|
|
106
|
+
) -> Self {
|
|
107
|
+
Self::with_session_and_runtime(
|
|
108
|
+
options,
|
|
109
|
+
SessionContext::library_default(),
|
|
110
|
+
SharedRuntime::new(grep_glob_executor),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Creates one isolated MCP connection backed by a shared per-user runtime.
|
|
115
|
+
pub(crate) fn with_session_and_runtime(
|
|
116
|
+
options: ServerOptions,
|
|
117
|
+
session: Arc<SessionContext>,
|
|
118
|
+
runtime: Arc<SharedRuntime>,
|
|
119
|
+
) -> Self {
|
|
120
|
+
let mut tool_router = Self::file_tool_router();
|
|
121
|
+
tool_router.merge(Self::shell_tool_router());
|
|
122
|
+
tool_router.merge(Self::edit_tool_router());
|
|
123
|
+
// rmcp's attribute accepts only a literal. Replace its inert placeholder before the router
|
|
124
|
+
// is observable so the positive route still has one production source.
|
|
125
|
+
tool_router
|
|
126
|
+
.map
|
|
127
|
+
.get_mut("inspect_local_file")
|
|
128
|
+
.expect("the compiled file router must contain inspect_local_file")
|
|
129
|
+
.attr
|
|
130
|
+
.description = Some(crate::model_guidance::inspect_tool_description().into());
|
|
131
|
+
for entry in ToolManifest::entries() {
|
|
132
|
+
if !entry.group.enabled(options.enable_shell) {
|
|
133
|
+
tool_router.remove_route(entry.name);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Every consumer of a tool definition reads it back out of this router — the
|
|
137
|
+
// stdio `tools/list` answer, `tool_definitions`, and the contract hashes doctor
|
|
138
|
+
// compares across processes. Normalizing here keeps all of them on one shape;
|
|
139
|
+
// doing it in `tool_definitions` instead would make doctor compare a normalized
|
|
140
|
+
// expectation against an underived wire answer.
|
|
141
|
+
for route in tool_router.map.values_mut() {
|
|
142
|
+
route.attr.input_schema = Arc::new(crate::tool_schema::normalize_published_schema(
|
|
143
|
+
&route.attr.input_schema,
|
|
144
|
+
));
|
|
145
|
+
}
|
|
146
|
+
let definitions = tool_router.list_all();
|
|
147
|
+
ToolManifest::validate(&definitions, options.enable_shell)
|
|
148
|
+
.expect("the compiled tool router must match ToolManifest");
|
|
149
|
+
Self {
|
|
150
|
+
tool_router,
|
|
151
|
+
options,
|
|
152
|
+
shell: FastShell::with_session(Arc::clone(&session)),
|
|
153
|
+
replace: runtime.replace.clone(),
|
|
154
|
+
file_permits: Arc::clone(&runtime.file_permits),
|
|
155
|
+
grep_glob_executor: Arc::clone(&runtime.grep_glob_executor),
|
|
156
|
+
shell_permits: Arc::clone(&runtime.shell_permits),
|
|
157
|
+
replace_permits: Arc::clone(&runtime.replace_permits),
|
|
158
|
+
activity: Arc::clone(&runtime.activity),
|
|
159
|
+
session,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Returns every definition exposed by MCP `tools/list` for tests and diagnostics.
|
|
164
|
+
pub fn tool_definitions(&self) -> Vec<rmcp::model::Tool> {
|
|
165
|
+
self.tool_router.list_all()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Returns stable contract hashes for every currently published tool.
|
|
169
|
+
pub fn tool_contracts(&self) -> Vec<ToolContract> {
|
|
170
|
+
ToolManifest::contracts(&self.tool_definitions())
|
|
171
|
+
.expect("validated server tools must have manifest entries")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Returns the startup feature selection used by this server.
|
|
175
|
+
pub const fn options(&self) -> ServerOptions {
|
|
176
|
+
self.options
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
impl Default for FastCtxServer {
|
|
181
|
+
fn default() -> Self {
|
|
182
|
+
Self::new()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#[tool_router(router = file_tool_router, vis = "pub(crate)")]
|
|
187
|
+
impl FastCtxServer {
|
|
188
|
+
#[tool(
|
|
189
|
+
name = "inspect_local_file",
|
|
190
|
+
description = "Inspect local files.",
|
|
191
|
+
annotations(
|
|
192
|
+
title = "Inspect local file",
|
|
193
|
+
read_only_hint = true,
|
|
194
|
+
destructive_hint = false,
|
|
195
|
+
open_world_hint = false
|
|
196
|
+
)
|
|
197
|
+
)]
|
|
198
|
+
async fn inspect_local_file(
|
|
199
|
+
&self,
|
|
200
|
+
Parameters(request): Parameters<ReadRequest>,
|
|
201
|
+
) -> CallToolResult {
|
|
202
|
+
let _activity = self.activity.request();
|
|
203
|
+
let status_shell = self.shell.clone();
|
|
204
|
+
run_blocking(
|
|
205
|
+
Arc::clone(&self.session),
|
|
206
|
+
Arc::clone(&self.file_permits),
|
|
207
|
+
READ_TOKEN_BUDGET_ENV,
|
|
208
|
+
move || status_shell.background_status(None),
|
|
209
|
+
BudgetRetry::Safe,
|
|
210
|
+
move || read_file(request.clone()),
|
|
211
|
+
)
|
|
212
|
+
.await
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[tool(
|
|
216
|
+
name = "grep",
|
|
217
|
+
description = "Fast regex content search (ripgrep engine; Rust regex, no lookaround). Output\nmodes: \"files_with_matches\" (default, paths only), \"content\", \"count\" (total\nmatches, not matching lines), \"summary\" (global totals). Respects .gitignore;\nsearches hidden files; skips .git and binaries. Files are decoded to UTF-8\nbefore searching; files whose encoding can't be determined, that change, or\nthat cannot be searched are skipped and listed for directory targets; the\nequivalent single-file failure returns an error. Matching is line-by-line:\n`^` and `$` anchor line boundaries and are CRLF-aware. A path component of the\nform ~fastctx~b...~ (reversible bytes/UTF-8) or ~fastctx~w...~ (Windows UTF-16)\nis a filename escape; copy that whole component verbatim in later calls and\ndo not decode or rewrite it. The last line of every successful result states\nComplete or Partial — continue only with the exact offset a Partial note\nprovides; errors are self-contained.",
|
|
218
|
+
annotations(
|
|
219
|
+
title = "Search file contents",
|
|
220
|
+
read_only_hint = true,
|
|
221
|
+
destructive_hint = false,
|
|
222
|
+
open_world_hint = false
|
|
223
|
+
)
|
|
224
|
+
)]
|
|
225
|
+
async fn grep(
|
|
226
|
+
&self,
|
|
227
|
+
Parameters(request): Parameters<GrepRequest>,
|
|
228
|
+
context: RequestContext<RoleServer>,
|
|
229
|
+
) -> CallToolResult {
|
|
230
|
+
let _activity = self.activity.request();
|
|
231
|
+
run_blocking_cancellable(
|
|
232
|
+
CancellableBlockingRequest::new(
|
|
233
|
+
Arc::clone(&self.session),
|
|
234
|
+
context.id,
|
|
235
|
+
context.ct,
|
|
236
|
+
Arc::clone(&self.file_permits),
|
|
237
|
+
Arc::clone(&self.grep_glob_executor),
|
|
238
|
+
GREP_TOKEN_BUDGET_ENV,
|
|
239
|
+
),
|
|
240
|
+
{
|
|
241
|
+
let shell = self.shell.clone();
|
|
242
|
+
move || shell.background_status(None)
|
|
243
|
+
},
|
|
244
|
+
move |operation, executor| grep_files_cancellable(operation, executor, request.clone()),
|
|
245
|
+
)
|
|
246
|
+
.await
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#[tool(
|
|
250
|
+
name = "glob",
|
|
251
|
+
description = "Find files by glob pattern, e.g. \"**/*.rs\" or \"src/**/*.ts\". Matches files\nonly, never directories. Returns absolute paths sorted by path (or newest first\nwith sort=\"modified\"), 100 per page by default. filter_mode defaults to\n\"ignore\" (plain .ignore files only); \"all\" disables that filtering. Hidden\nfiles and .git are always visible unless a negative pattern excludes them.\noutput_mode defaults to \"paths\"; \"details\" returns compact JSON lines with\npath, byte size, and UTC modification time. Omit `path` entirely for the session\nworking directory — never pass \"null\" or \"undefined\". A path component of the\nform ~fastctx~b...~ (reversible bytes/UTF-8) or ~fastctx~w...~ (Windows UTF-16)\nis a filename escape; copy that whole component verbatim in later calls and do\nnot decode or rewrite it. The last line of every successful result states\nComplete or Partial — continue only with the exact offset a Partial note\nprovides; errors are self-contained.",
|
|
252
|
+
annotations(
|
|
253
|
+
title = "Match file paths",
|
|
254
|
+
read_only_hint = true,
|
|
255
|
+
destructive_hint = false,
|
|
256
|
+
open_world_hint = false
|
|
257
|
+
)
|
|
258
|
+
)]
|
|
259
|
+
async fn glob(
|
|
260
|
+
&self,
|
|
261
|
+
Parameters(request): Parameters<GlobRequest>,
|
|
262
|
+
context: RequestContext<RoleServer>,
|
|
263
|
+
) -> CallToolResult {
|
|
264
|
+
let _activity = self.activity.request();
|
|
265
|
+
run_blocking_cancellable(
|
|
266
|
+
CancellableBlockingRequest::new(
|
|
267
|
+
Arc::clone(&self.session),
|
|
268
|
+
context.id,
|
|
269
|
+
context.ct,
|
|
270
|
+
Arc::clone(&self.file_permits),
|
|
271
|
+
Arc::clone(&self.grep_glob_executor),
|
|
272
|
+
GLOB_TOKEN_BUDGET_ENV,
|
|
273
|
+
),
|
|
274
|
+
{
|
|
275
|
+
let shell = self.shell.clone();
|
|
276
|
+
move || shell.background_status(None)
|
|
277
|
+
},
|
|
278
|
+
move |operation, executor| glob_files_cancellable(operation, executor, request.clone()),
|
|
279
|
+
)
|
|
280
|
+
.await
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[tool_handler(router = self.tool_router)]
|
|
285
|
+
impl ServerHandler for FastCtxServer {
|
|
286
|
+
fn get_info(&self) -> ServerInfo {
|
|
287
|
+
self.activity.touch();
|
|
288
|
+
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
|
289
|
+
.with_server_info(Implementation::new(
|
|
290
|
+
env!("CARGO_PKG_NAME"),
|
|
291
|
+
env!("CARGO_PKG_VERSION"),
|
|
292
|
+
))
|
|
293
|
+
// 2026-07-24: hosts render these instructions as the tool namespace's one-line
|
|
294
|
+
// blurb and may keep only its first line and first 250 characters, so this text
|
|
295
|
+
// has to introduce the toolset within that budget. Behavioural rules belong in
|
|
296
|
+
// the host guidance file, which has no such limit.
|
|
297
|
+
.with_instructions(crate::model_guidance::server_instructions(
|
|
298
|
+
self.options.enable_shell,
|
|
299
|
+
))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// The three `resources/*` methods stay on the rmcp defaults on purpose: both list methods
|
|
303
|
+
// answer with an empty list, and `resources/read` answers method-not-found. Overriding them
|
|
304
|
+
// to reject uniformly (added 0.2.2, reverted 2026-08-01) turned "this server has none" into a
|
|
305
|
+
// failure, and a failed call makes a model retry with a different `server` argument rather
|
|
306
|
+
// than switch tools — users reported chains of invented server names that the empty list
|
|
307
|
+
// never produced. Do not reintroduce an override without evidence from a released build.
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[cfg(test)]
|
|
311
|
+
mod tests {
|
|
312
|
+
use super::{FastCtxServer, ServerOptions, SharedRuntime};
|
|
313
|
+
use crate::file_executor::GrepGlobExecutor;
|
|
314
|
+
use crate::search_parallelism::MAX_SEARCH_PARALLELISM;
|
|
315
|
+
use std::sync::Arc;
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn configured_executor_is_the_server_search_source_for_serial_mid_and_maximum_p() {
|
|
319
|
+
let middle = (MAX_SEARCH_PARALLELISM / 2).max(1);
|
|
320
|
+
for parallelism in [1, middle, MAX_SEARCH_PARALLELISM] {
|
|
321
|
+
let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(parallelism));
|
|
322
|
+
let server = FastCtxServer::with_options_and_executor(
|
|
323
|
+
ServerOptions::default(),
|
|
324
|
+
Arc::clone(&executor),
|
|
325
|
+
);
|
|
326
|
+
assert!(Arc::ptr_eq(&server.grep_glob_executor, &executor));
|
|
327
|
+
assert_eq!(server.grep_glob_executor.parallelism(), parallelism);
|
|
328
|
+
assert_eq!(server.grep_glob_executor.extra_capacity(), parallelism - 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
#[test]
|
|
333
|
+
fn connections_share_runtime_resources_but_keep_distinct_session_contexts() {
|
|
334
|
+
let executor = Arc::new(GrepGlobExecutor::with_test_parallelism(1));
|
|
335
|
+
let runtime = SharedRuntime::new(Arc::clone(&executor));
|
|
336
|
+
let first = FastCtxServer::with_session_and_runtime(
|
|
337
|
+
ServerOptions::all(),
|
|
338
|
+
crate::session::SessionContext::library_default(),
|
|
339
|
+
Arc::clone(&runtime),
|
|
340
|
+
);
|
|
341
|
+
let second = FastCtxServer::with_session_and_runtime(
|
|
342
|
+
ServerOptions::all(),
|
|
343
|
+
crate::session::SessionContext::library_default(),
|
|
344
|
+
runtime,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
assert!(Arc::ptr_eq(&first.grep_glob_executor, &executor));
|
|
348
|
+
assert!(Arc::ptr_eq(
|
|
349
|
+
&first.grep_glob_executor,
|
|
350
|
+
&second.grep_glob_executor
|
|
351
|
+
));
|
|
352
|
+
assert!(Arc::ptr_eq(&first.file_permits, &second.file_permits));
|
|
353
|
+
assert!(Arc::ptr_eq(&first.shell_permits, &second.shell_permits));
|
|
354
|
+
assert!(Arc::ptr_eq(&first.replace_permits, &second.replace_permits));
|
|
355
|
+
assert!(first.replace.shares_locks_with(&second.replace));
|
|
356
|
+
assert!(Arc::ptr_eq(&first.activity, &second.activity));
|
|
357
|
+
assert!(!Arc::ptr_eq(&first.session, &second.session));
|
|
358
|
+
}
|
|
359
|
+
}
|