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,746 @@
|
|
|
1
|
+
//! Thin stdio proxy and the per-user, per-build FastCtx control center.
|
|
2
|
+
|
|
3
|
+
pub(crate) mod activity;
|
|
4
|
+
mod hosts;
|
|
5
|
+
mod journal;
|
|
6
|
+
mod local_ipc;
|
|
7
|
+
mod protocol;
|
|
8
|
+
mod session;
|
|
9
|
+
#[cfg(windows)]
|
|
10
|
+
mod windows_process;
|
|
11
|
+
|
|
12
|
+
use crate::control::paths::ControlPaths;
|
|
13
|
+
use crate::file_executor::GrepGlobExecutor;
|
|
14
|
+
use crate::process_identity::ProcessIdentity;
|
|
15
|
+
use crate::server::{FastCtxServer, ServerOptions, SharedRuntime};
|
|
16
|
+
use crate::session::{SessionContext, SessionEnvironment};
|
|
17
|
+
use fs2::FileExt;
|
|
18
|
+
use hosts::HostRegistry;
|
|
19
|
+
use local_ipc::{BoxedStream, Listener, LocalEndpoint};
|
|
20
|
+
use rmcp::ServiceExt;
|
|
21
|
+
use sha2::{Digest, Sha256};
|
|
22
|
+
use std::fs::File;
|
|
23
|
+
use std::path::Path;
|
|
24
|
+
use std::process::{Command, Stdio};
|
|
25
|
+
use std::sync::Arc;
|
|
26
|
+
use std::time::{Duration, Instant};
|
|
27
|
+
use tokio::io::split;
|
|
28
|
+
use tokio::sync::OnceCell;
|
|
29
|
+
use tokio_util::sync::CancellationToken;
|
|
30
|
+
|
|
31
|
+
pub(crate) use session::run_proxy_session;
|
|
32
|
+
|
|
33
|
+
/// How long a proxy waits for the control center before falling back to an in-process engine.
|
|
34
|
+
pub(crate) const STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
|
|
35
|
+
const CONNECT_RETRY: Duration = Duration::from_millis(20);
|
|
36
|
+
const ACCEPT_RETRY: Duration = Duration::from_secs(1);
|
|
37
|
+
/// How long a control center with nothing left to serve waits before exiting.
|
|
38
|
+
///
|
|
39
|
+
/// This timer only starts once every host that used the control center has exited, so in normal
|
|
40
|
+
/// use it measures the gap between closing the last Codex window and reclaiming the runtime.
|
|
41
|
+
const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(10 * 60);
|
|
42
|
+
const DEFAULT_MAINTENANCE_INTERVAL: Duration = Duration::from_secs(60);
|
|
43
|
+
const SERVICE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(3);
|
|
44
|
+
/// Backstop for a closing proxy waiting on answers the control center already owes. A session that
|
|
45
|
+
/// ends normally never reaches it: the control center closes the connection once it has answered.
|
|
46
|
+
const RESPONSE_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
|
|
47
|
+
/// How long work in progress may keep running after its client stopped reading. Long enough for a
|
|
48
|
+
/// finished handler to write its answer, short enough that nothing outlives the session by much.
|
|
49
|
+
const INPUT_CLOSED_GRACE: Duration = Duration::from_millis(250);
|
|
50
|
+
/// Buffer between the proxy and an in-process engine. Both directions are pumped by independent
|
|
51
|
+
/// tasks on either side, so this only bounds how far ahead a writer may run.
|
|
52
|
+
const IN_PROCESS_BUFFER_BYTES: usize = 256 * 1024;
|
|
53
|
+
#[cfg(unix)]
|
|
54
|
+
const MAX_UNIX_SOCKET_PATH_BYTES: usize = 100;
|
|
55
|
+
|
|
56
|
+
/// Captures the thin proxy's native state without loading settings or any heavy executor.
|
|
57
|
+
pub(crate) fn capture_proxy_environment() -> Result<SessionEnvironment, String> {
|
|
58
|
+
SessionEnvironment::capture()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Connects to the matching control center or starts exactly one.
|
|
62
|
+
pub(crate) async fn connect_or_start(
|
|
63
|
+
options: ServerOptions,
|
|
64
|
+
environment: &SessionEnvironment,
|
|
65
|
+
host: Option<ProcessIdentity>,
|
|
66
|
+
) -> Result<BoxedStream, String> {
|
|
67
|
+
let executable = std::env::current_exe()
|
|
68
|
+
.map_err(|error| format!("Cannot locate the running fastctx binary: {error}"))?;
|
|
69
|
+
let endpoint = endpoint_for(environment)?;
|
|
70
|
+
crate::edit::private_storage::ensure_private_directory(
|
|
71
|
+
endpoint.runtime_directory(),
|
|
72
|
+
"control-center runtime",
|
|
73
|
+
)?;
|
|
74
|
+
|
|
75
|
+
if let Ok(mut stream) = local_ipc::connect(&endpoint).await {
|
|
76
|
+
establish(&mut stream, options, environment.clone(), host.clone()).await?;
|
|
77
|
+
return Ok(stream);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let startup_lock = crate::edit::private_storage::open_lock_file(
|
|
81
|
+
&endpoint.startup_lock_path(),
|
|
82
|
+
"control-center startup lock",
|
|
83
|
+
)?;
|
|
84
|
+
acquire_startup_lock(&startup_lock, &endpoint.startup_lock_path()).await?;
|
|
85
|
+
|
|
86
|
+
if let Ok(mut stream) = local_ipc::connect(&endpoint).await {
|
|
87
|
+
establish(&mut stream, options, environment.clone(), host.clone()).await?;
|
|
88
|
+
return Ok(stream);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
spawn_bootstrap(&executable, environment)?;
|
|
92
|
+
let deadline = Instant::now() + STARTUP_TIMEOUT;
|
|
93
|
+
loop {
|
|
94
|
+
match local_ipc::connect(&endpoint).await {
|
|
95
|
+
Ok(mut stream) => {
|
|
96
|
+
establish(&mut stream, options, environment.clone(), host).await?;
|
|
97
|
+
return Ok(stream);
|
|
98
|
+
}
|
|
99
|
+
Err(_) if Instant::now() < deadline => {
|
|
100
|
+
tokio::time::sleep(CONNECT_RETRY).await;
|
|
101
|
+
}
|
|
102
|
+
Err(error) => {
|
|
103
|
+
return Err(format!(
|
|
104
|
+
"The FastCtx control center did not become ready within 10 seconds: {error}"
|
|
105
|
+
));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Runs a control-center session inside the proxy itself.
|
|
112
|
+
///
|
|
113
|
+
/// This is the engine of last resort. It costs this session its own search executor instead of a
|
|
114
|
+
/// shared one, which is exactly the cost the shared control center exists to avoid — but a session
|
|
115
|
+
/// that cannot reach the shared runtime has only two remaining options, and losing the host's MCP
|
|
116
|
+
/// transport is the worse one.
|
|
117
|
+
async fn start_in_process(
|
|
118
|
+
options: ServerOptions,
|
|
119
|
+
environment: &SessionEnvironment,
|
|
120
|
+
host: Option<ProcessIdentity>,
|
|
121
|
+
) -> Result<BoxedStream, String> {
|
|
122
|
+
let (proxy_side, engine_side) = tokio::io::duplex(IN_PROCESS_BUFFER_BYTES);
|
|
123
|
+
let state = HostState::new();
|
|
124
|
+
let connection = state
|
|
125
|
+
.activity
|
|
126
|
+
.try_connection()
|
|
127
|
+
.ok_or_else(|| "The in-process FastCtx engine refused its own session.".to_string())?;
|
|
128
|
+
tokio::spawn(serve_connection(
|
|
129
|
+
Box::new(engine_side) as BoxedStream,
|
|
130
|
+
state,
|
|
131
|
+
CancellationToken::new(),
|
|
132
|
+
connection,
|
|
133
|
+
));
|
|
134
|
+
let mut stream = Box::new(proxy_side) as BoxedStream;
|
|
135
|
+
establish(&mut stream, options, environment.clone(), host).await?;
|
|
136
|
+
Ok(stream)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async fn establish(
|
|
140
|
+
stream: &mut BoxedStream,
|
|
141
|
+
options: ServerOptions,
|
|
142
|
+
environment: SessionEnvironment,
|
|
143
|
+
host: Option<ProcessIdentity>,
|
|
144
|
+
) -> Result<(), String> {
|
|
145
|
+
tokio::time::timeout(STARTUP_TIMEOUT, async {
|
|
146
|
+
protocol::write_handshake(
|
|
147
|
+
stream,
|
|
148
|
+
&protocol::Handshake::new(options, environment, host),
|
|
149
|
+
)
|
|
150
|
+
.await?;
|
|
151
|
+
protocol::read_handshake_response(stream).await
|
|
152
|
+
})
|
|
153
|
+
.await
|
|
154
|
+
.map_err(|_| {
|
|
155
|
+
"Timed out waiting for the FastCtx control center to accept the session handshake."
|
|
156
|
+
.to_string()
|
|
157
|
+
})?
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async fn acquire_startup_lock(file: &File, path: &Path) -> Result<(), String> {
|
|
161
|
+
let deadline = Instant::now() + STARTUP_TIMEOUT;
|
|
162
|
+
loop {
|
|
163
|
+
match file.try_lock_exclusive() {
|
|
164
|
+
Ok(()) => return Ok(()),
|
|
165
|
+
Err(error)
|
|
166
|
+
if error.kind() == std::io::ErrorKind::WouldBlock && Instant::now() < deadline =>
|
|
167
|
+
{
|
|
168
|
+
tokio::time::sleep(CONNECT_RETRY).await;
|
|
169
|
+
}
|
|
170
|
+
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
|
|
171
|
+
return Err(
|
|
172
|
+
"Timed out waiting for another FastCtx proxy to finish starting the control center."
|
|
173
|
+
.to_string(),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
Err(error) => {
|
|
177
|
+
return Err(format!(
|
|
178
|
+
"Cannot lock the control-center startup gate {}: {error}",
|
|
179
|
+
crate::paths::display_path(path)
|
|
180
|
+
));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn endpoint_for(environment: &SessionEnvironment) -> Result<LocalEndpoint, String> {
|
|
187
|
+
let home = environment
|
|
188
|
+
.var_os("HOME")
|
|
189
|
+
.filter(|value| !value.is_empty())
|
|
190
|
+
.or_else(|| {
|
|
191
|
+
environment
|
|
192
|
+
.var_os("USERPROFILE")
|
|
193
|
+
.filter(|value| !value.is_empty())
|
|
194
|
+
})
|
|
195
|
+
.ok_or_else(|| {
|
|
196
|
+
"Cannot determine the user home directory for the FastCtx control center. Set HOME or USERPROFILE and retry."
|
|
197
|
+
.to_string()
|
|
198
|
+
})?;
|
|
199
|
+
let home_hash = short_hash(&crate::session::native_bytes(&home), 12);
|
|
200
|
+
let build_id = effective_build_id(environment);
|
|
201
|
+
let id = format!("fastctx-engine-{home_hash}-{build_id}");
|
|
202
|
+
let preferred_runtime_directory = crate::edit::private_storage::control_center_directory();
|
|
203
|
+
#[cfg(unix)]
|
|
204
|
+
let runtime_directory = select_unix_runtime_directory(
|
|
205
|
+
preferred_runtime_directory,
|
|
206
|
+
crate::edit::private_storage::short_control_center_directory(),
|
|
207
|
+
&id,
|
|
208
|
+
)?;
|
|
209
|
+
#[cfg(not(unix))]
|
|
210
|
+
let runtime_directory = preferred_runtime_directory;
|
|
211
|
+
Ok(LocalEndpoint::new(runtime_directory, id))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#[cfg(unix)]
|
|
215
|
+
fn select_unix_runtime_directory(
|
|
216
|
+
preferred: std::path::PathBuf,
|
|
217
|
+
fallback: std::path::PathBuf,
|
|
218
|
+
id: &str,
|
|
219
|
+
) -> Result<std::path::PathBuf, String> {
|
|
220
|
+
use std::os::unix::ffi::OsStrExt;
|
|
221
|
+
|
|
222
|
+
let socket_length = |directory: &Path| {
|
|
223
|
+
directory
|
|
224
|
+
.join(format!("{id}.sock"))
|
|
225
|
+
.as_os_str()
|
|
226
|
+
.as_bytes()
|
|
227
|
+
.len()
|
|
228
|
+
.saturating_add(1)
|
|
229
|
+
};
|
|
230
|
+
if socket_length(&preferred) <= MAX_UNIX_SOCKET_PATH_BYTES {
|
|
231
|
+
return Ok(preferred);
|
|
232
|
+
}
|
|
233
|
+
// Darwin's per-user temporary directory can already consume most of sockaddr_un::sun_path.
|
|
234
|
+
// Keep the endpoint and both ownership locks together in an owner-only short directory.
|
|
235
|
+
let fallback_length = socket_length(&fallback);
|
|
236
|
+
if fallback_length <= MAX_UNIX_SOCKET_PATH_BYTES {
|
|
237
|
+
return Ok(fallback);
|
|
238
|
+
}
|
|
239
|
+
Err(format!(
|
|
240
|
+
"The private control-center socket path is too long ({fallback_length} bytes): {}",
|
|
241
|
+
crate::paths::display_path(&fallback.join(format!("{id}.sock")))
|
|
242
|
+
))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn short_hash(bytes: &[u8], characters: usize) -> String {
|
|
246
|
+
let digest = Sha256::digest(bytes);
|
|
247
|
+
hex::encode(digest)[..characters].to_string()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn effective_build_id(_environment: &SessionEnvironment) -> String {
|
|
251
|
+
#[cfg(debug_assertions)]
|
|
252
|
+
if let Ok(override_id) = _environment.var("FASTCTX_TEST_BUILD_ID")
|
|
253
|
+
&& !override_id.is_empty()
|
|
254
|
+
&& override_id.len() <= 32
|
|
255
|
+
&& override_id
|
|
256
|
+
.bytes()
|
|
257
|
+
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
|
258
|
+
{
|
|
259
|
+
return override_id;
|
|
260
|
+
}
|
|
261
|
+
env!("FASTCTX_BUILD_ID").to_string()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn spawn_bootstrap(executable: &Path, environment: &SessionEnvironment) -> Result<(), String> {
|
|
265
|
+
let mut command = Command::new(executable);
|
|
266
|
+
environment.configure_command(&mut command);
|
|
267
|
+
command
|
|
268
|
+
.arg("runtime-bootstrap")
|
|
269
|
+
.current_dir(environment.cwd())
|
|
270
|
+
.stdin(Stdio::null())
|
|
271
|
+
.stdout(Stdio::null())
|
|
272
|
+
.stderr(Stdio::null());
|
|
273
|
+
#[cfg(windows)]
|
|
274
|
+
{
|
|
275
|
+
use std::os::windows::process::CommandExt;
|
|
276
|
+
command.creation_flags(crate::process_policy::noninteractive_creation_flags(0));
|
|
277
|
+
}
|
|
278
|
+
command
|
|
279
|
+
.spawn()
|
|
280
|
+
.map(|_| ())
|
|
281
|
+
.map_err(|error| format!("Cannot start the FastCtx control-center bootstrap: {error}"))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Intermediate child that reparents the long-lived control center before the proxy returns.
|
|
285
|
+
pub(crate) fn run_bootstrap_entry() -> Result<(), String> {
|
|
286
|
+
let executable = std::env::current_exe()
|
|
287
|
+
.map_err(|error| format!("Cannot locate the control-center binary: {error}"))?;
|
|
288
|
+
let environment = SessionEnvironment::capture()?;
|
|
289
|
+
let mut command = Command::new(&executable);
|
|
290
|
+
environment.configure_command(&mut command);
|
|
291
|
+
command
|
|
292
|
+
.arg("runtime-host")
|
|
293
|
+
.current_dir(environment.cwd())
|
|
294
|
+
.stdin(Stdio::null())
|
|
295
|
+
.stdout(Stdio::null())
|
|
296
|
+
.stderr(Stdio::null());
|
|
297
|
+
#[cfg(unix)]
|
|
298
|
+
{
|
|
299
|
+
use std::os::unix::process::CommandExt;
|
|
300
|
+
// SAFETY: pre_exec performs only the async-signal-safe setsid syscall.
|
|
301
|
+
unsafe {
|
|
302
|
+
command.pre_exec(|| {
|
|
303
|
+
if libc::setsid() < 0 {
|
|
304
|
+
Err(std::io::Error::last_os_error())
|
|
305
|
+
} else {
|
|
306
|
+
Ok(())
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
#[cfg(windows)]
|
|
312
|
+
{
|
|
313
|
+
use windows_sys::Win32::System::Threading::{
|
|
314
|
+
CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, DETACHED_PROCESS,
|
|
315
|
+
};
|
|
316
|
+
let detached = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
|
|
317
|
+
match windows_process::spawn_without_inherited_handles(
|
|
318
|
+
&executable,
|
|
319
|
+
environment.cwd(),
|
|
320
|
+
detached | CREATE_BREAKAWAY_FROM_JOB,
|
|
321
|
+
) {
|
|
322
|
+
Ok(_) => Ok(()),
|
|
323
|
+
Err(error) if error.raw_os_error() == Some(5) => {
|
|
324
|
+
windows_process::spawn_without_inherited_handles(
|
|
325
|
+
&executable,
|
|
326
|
+
environment.cwd(),
|
|
327
|
+
detached,
|
|
328
|
+
)
|
|
329
|
+
.map_err(|error| format!("Cannot detach the FastCtx control center: {error}"))
|
|
330
|
+
}
|
|
331
|
+
Err(error) => Err(format!("Cannot detach the FastCtx control center: {error}")),
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
#[cfg(not(windows))]
|
|
335
|
+
command
|
|
336
|
+
.spawn()
|
|
337
|
+
.map(|_| ())
|
|
338
|
+
.map_err(|error| format!("Cannot detach the FastCtx control center: {error}"))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
struct HostState {
|
|
342
|
+
runtime: OnceCell<Arc<SharedRuntime>>,
|
|
343
|
+
control_paths: OnceCell<ControlPaths>,
|
|
344
|
+
activity: Arc<activity::RuntimeActivity>,
|
|
345
|
+
hosts: Arc<HostRegistry>,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
impl HostState {
|
|
349
|
+
fn new() -> Arc<Self> {
|
|
350
|
+
Arc::new(Self {
|
|
351
|
+
runtime: OnceCell::new(),
|
|
352
|
+
control_paths: OnceCell::new(),
|
|
353
|
+
activity: activity::RuntimeActivity::new(),
|
|
354
|
+
hosts: Arc::new(HostRegistry::new()),
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async fn runtime_for(
|
|
359
|
+
&self,
|
|
360
|
+
session: &Arc<SessionContext>,
|
|
361
|
+
) -> Result<Arc<SharedRuntime>, String> {
|
|
362
|
+
let runtime = self
|
|
363
|
+
.runtime
|
|
364
|
+
.get_or_try_init(|| async {
|
|
365
|
+
let parallelism = session.settings.search_parallelism().map_err(|error| {
|
|
366
|
+
format!(
|
|
367
|
+
"Cannot start the MCP session with settings from {}: {error}. Repair the value and retry.",
|
|
368
|
+
crate::paths::display_path(&session.control_paths.fastctx_config)
|
|
369
|
+
)
|
|
370
|
+
})?;
|
|
371
|
+
let executor = Arc::new(GrepGlobExecutor::with_parallelism(parallelism.effective));
|
|
372
|
+
Ok::<_, String>(SharedRuntime::with_activity(
|
|
373
|
+
executor,
|
|
374
|
+
Arc::clone(&self.activity),
|
|
375
|
+
))
|
|
376
|
+
})
|
|
377
|
+
.await?;
|
|
378
|
+
let _ = self.control_paths.set(session.control_paths.clone());
|
|
379
|
+
Ok(Arc::clone(runtime))
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/// Final detached control-center entry point.
|
|
384
|
+
pub(crate) async fn run_host_entry(
|
|
385
|
+
idle_timeout_ms: Option<u64>,
|
|
386
|
+
maintenance_interval_ms: Option<u64>,
|
|
387
|
+
) -> Result<(), String> {
|
|
388
|
+
let environment = SessionEnvironment::capture()?;
|
|
389
|
+
let endpoint = endpoint_for(&environment)?;
|
|
390
|
+
crate::edit::private_storage::ensure_private_directory(
|
|
391
|
+
endpoint.runtime_directory(),
|
|
392
|
+
"control-center runtime",
|
|
393
|
+
)?;
|
|
394
|
+
let instance_lock = crate::edit::private_storage::open_lock_file(
|
|
395
|
+
&endpoint.instance_lock_path(),
|
|
396
|
+
"control-center instance lock",
|
|
397
|
+
)?;
|
|
398
|
+
match instance_lock.try_lock_exclusive() {
|
|
399
|
+
Ok(()) => {}
|
|
400
|
+
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => return Ok(()),
|
|
401
|
+
Err(error) => {
|
|
402
|
+
return Err(format!(
|
|
403
|
+
"Cannot lock the control-center instance gate: {error}"
|
|
404
|
+
));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
let mut listener = Listener::bind(&endpoint)?;
|
|
408
|
+
#[cfg(debug_assertions)]
|
|
409
|
+
record_test_host_start(&environment);
|
|
410
|
+
let state = HostState::new();
|
|
411
|
+
let shutdown = CancellationToken::new();
|
|
412
|
+
let idle_timeout = idle_timeout_ms
|
|
413
|
+
.or_else(|| duration_override(&environment, "FASTCTX_TEST_RUNTIME_IDLE_MS"))
|
|
414
|
+
.map(Duration::from_millis)
|
|
415
|
+
.unwrap_or(DEFAULT_IDLE_TIMEOUT);
|
|
416
|
+
let (idle_candidate_tx, mut idle_candidate_rx) = tokio::sync::mpsc::channel(1);
|
|
417
|
+
let monitor = tokio::spawn(monitor_idle(
|
|
418
|
+
Arc::clone(&state),
|
|
419
|
+
shutdown.clone(),
|
|
420
|
+
idle_timeout,
|
|
421
|
+
idle_candidate_tx,
|
|
422
|
+
));
|
|
423
|
+
let maintenance = tokio::spawn(monitor_maintenance(
|
|
424
|
+
Arc::clone(&state),
|
|
425
|
+
shutdown.clone(),
|
|
426
|
+
maintenance_interval_ms
|
|
427
|
+
.or_else(|| duration_override(&environment, "FASTCTX_TEST_RUNTIME_MAINTENANCE_MS"))
|
|
428
|
+
.map(Duration::from_millis)
|
|
429
|
+
.unwrap_or(DEFAULT_MAINTENANCE_INTERVAL),
|
|
430
|
+
));
|
|
431
|
+
let mut connections = tokio::task::JoinSet::new();
|
|
432
|
+
|
|
433
|
+
loop {
|
|
434
|
+
tokio::select! {
|
|
435
|
+
accepted = listener.accept() => {
|
|
436
|
+
match accepted {
|
|
437
|
+
Ok(stream) => {
|
|
438
|
+
let Some(connection) = state.activity.try_connection() else {
|
|
439
|
+
drop(stream);
|
|
440
|
+
continue;
|
|
441
|
+
};
|
|
442
|
+
connections.spawn(serve_connection(
|
|
443
|
+
stream,
|
|
444
|
+
Arc::clone(&state),
|
|
445
|
+
shutdown.clone(),
|
|
446
|
+
connection,
|
|
447
|
+
));
|
|
448
|
+
}
|
|
449
|
+
Err(error) => {
|
|
450
|
+
eprintln!("fastctx control center: {error}; retrying.");
|
|
451
|
+
tokio::select! {
|
|
452
|
+
() = shutdown.cancelled() => break,
|
|
453
|
+
() = tokio::time::sleep(ACCEPT_RETRY) => {}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
Some(()) = idle_candidate_rx.recv() => {
|
|
459
|
+
if state.activity.try_begin_shutdown(idle_timeout) {
|
|
460
|
+
shutdown.cancel();
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
() = shutdown.cancelled() => break,
|
|
465
|
+
result = connections.join_next(), if !connections.is_empty() => {
|
|
466
|
+
if let Some(Err(error)) = result {
|
|
467
|
+
eprintln!("fastctx control center: connection task failed: {error}");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
shutdown.cancel();
|
|
474
|
+
let deadline = tokio::time::sleep(SERVICE_SHUTDOWN_TIMEOUT);
|
|
475
|
+
tokio::pin!(deadline);
|
|
476
|
+
// Drain only while connections remain. The endpoint stays bound until this function returns,
|
|
477
|
+
// and a proxy that connects during the wait is never accepted: it stalls, then degrades to a
|
|
478
|
+
// standalone server. An always-enabled deadline branch would make every exit wait in full.
|
|
479
|
+
while !connections.is_empty() {
|
|
480
|
+
tokio::select! {
|
|
481
|
+
_ = &mut deadline => {
|
|
482
|
+
connections.abort_all();
|
|
483
|
+
while connections.join_next().await.is_some() {}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
result = connections.join_next() => {
|
|
487
|
+
if result.is_none() {
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
monitor.abort();
|
|
494
|
+
let _ = monitor.await;
|
|
495
|
+
maintenance.abort();
|
|
496
|
+
let _ = maintenance.await;
|
|
497
|
+
drop(instance_lock);
|
|
498
|
+
Ok(())
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/// Reads a millisecond timer override for the control center's own loops.
|
|
502
|
+
///
|
|
503
|
+
/// Deliberately honoured in every profile. While this was `debug_assertions`-only, one release
|
|
504
|
+
/// test run left sixty-five control centers holding the production ten-minute timeout, which is
|
|
505
|
+
/// the very process pile-up this runtime exists to remove. Both timers only shorten the host's
|
|
506
|
+
/// own life, so a value from the environment cannot outlive or override a caller's session.
|
|
507
|
+
fn duration_override(environment: &SessionEnvironment, name: &str) -> Option<u64> {
|
|
508
|
+
environment.var(name).ok()?.parse::<u64>().ok()
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#[cfg(debug_assertions)]
|
|
512
|
+
fn record_test_host_start(environment: &SessionEnvironment) {
|
|
513
|
+
use std::io::Write as _;
|
|
514
|
+
use std::path::PathBuf;
|
|
515
|
+
let Some(path) = environment.var_os("FASTCTX_TEST_RUNTIME_EVENT_LOG") else {
|
|
516
|
+
return;
|
|
517
|
+
};
|
|
518
|
+
let mut options = std::fs::OpenOptions::new();
|
|
519
|
+
options.create(true).append(true);
|
|
520
|
+
#[cfg(unix)]
|
|
521
|
+
{
|
|
522
|
+
use std::os::unix::fs::OpenOptionsExt;
|
|
523
|
+
options.mode(0o600);
|
|
524
|
+
}
|
|
525
|
+
if let Ok(mut file) = options.open(PathBuf::from(path)) {
|
|
526
|
+
let _ = writeln!(file, "START {}", std::process::id());
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async fn monitor_maintenance(
|
|
531
|
+
state: Arc<HostState>,
|
|
532
|
+
shutdown: CancellationToken,
|
|
533
|
+
interval: Duration,
|
|
534
|
+
) {
|
|
535
|
+
loop {
|
|
536
|
+
tokio::select! {
|
|
537
|
+
() = shutdown.cancelled() => return,
|
|
538
|
+
() = tokio::time::sleep(interval) => {}
|
|
539
|
+
}
|
|
540
|
+
let Some(paths) = state.control_paths.get().cloned() else {
|
|
541
|
+
continue;
|
|
542
|
+
};
|
|
543
|
+
match tokio::task::spawn_blocking(move || crate::shell::jobs::reap_history(&paths)).await {
|
|
544
|
+
Ok(Ok(_)) => {}
|
|
545
|
+
Ok(Err(error)) => {
|
|
546
|
+
eprintln!("fastctx control center: periodic background-job cleanup failed: {error}")
|
|
547
|
+
}
|
|
548
|
+
Err(error) => eprintln!(
|
|
549
|
+
"fastctx control center: periodic background-job cleanup task failed: {error}"
|
|
550
|
+
),
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async fn serve_connection(
|
|
556
|
+
mut stream: BoxedStream,
|
|
557
|
+
state: Arc<HostState>,
|
|
558
|
+
shutdown: CancellationToken,
|
|
559
|
+
_connection: activity::ConnectionActivityGuard,
|
|
560
|
+
) {
|
|
561
|
+
let handshake = match protocol::read_handshake(&mut stream).await {
|
|
562
|
+
Ok(handshake) => handshake,
|
|
563
|
+
Err(error) => {
|
|
564
|
+
let _ = protocol::write_handshake_error(&mut stream, error).await;
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
let session = match SessionContext::from_environment(handshake.environment) {
|
|
569
|
+
Ok(session) => session,
|
|
570
|
+
Err(error) => {
|
|
571
|
+
let _ = protocol::write_handshake_error(&mut stream, error).await;
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
// Recorded before the session runs, so a host that connects once keeps the runtime warm for
|
|
576
|
+
// every later conversation it opens, not only while this connection lasts.
|
|
577
|
+
state.hosts.remember(handshake.host);
|
|
578
|
+
let runtime = match state.runtime_for(&session).await {
|
|
579
|
+
Ok(runtime) => runtime,
|
|
580
|
+
Err(error) => {
|
|
581
|
+
let _ = protocol::write_handshake_error(&mut stream, error).await;
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
if protocol::write_handshake_success(&mut stream)
|
|
586
|
+
.await
|
|
587
|
+
.is_err()
|
|
588
|
+
{
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
let (mut reader, writer) = split(stream);
|
|
592
|
+
// Requests arrive framed so the proxy can mark the end of input; the MCP server reads the
|
|
593
|
+
// plain stream that comes back out, and the sink closing is what it sees as EOF.
|
|
594
|
+
let (requests, mut request_sink) = tokio::io::simplex(protocol::MAX_REQUEST_FRAME_BYTES);
|
|
595
|
+
let mut unframe = tokio::spawn(async move {
|
|
596
|
+
if let Err(error) = protocol::receive_requests(&mut reader, &mut request_sink).await {
|
|
597
|
+
eprintln!("fastctx control center: {error}");
|
|
598
|
+
}
|
|
599
|
+
drop(request_sink);
|
|
600
|
+
});
|
|
601
|
+
let service = match FastCtxServer::with_session_and_runtime(handshake.options, session, runtime)
|
|
602
|
+
.serve((requests, writer))
|
|
603
|
+
.await
|
|
604
|
+
{
|
|
605
|
+
Ok(service) => service,
|
|
606
|
+
Err(error) => {
|
|
607
|
+
eprintln!("fastctx control center: cannot start MCP connection: {error}");
|
|
608
|
+
unframe.abort();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
let cancellation = service.cancellation_token();
|
|
613
|
+
let mut waiting = tokio::spawn(service.waiting());
|
|
614
|
+
tokio::select! {
|
|
615
|
+
_ = shutdown.cancelled() => {
|
|
616
|
+
cancellation.cancel();
|
|
617
|
+
end_service(&mut waiting).await;
|
|
618
|
+
}
|
|
619
|
+
_ = &mut unframe => {
|
|
620
|
+
// The client is gone. Work that already finished still gets its answer written, but
|
|
621
|
+
// an MCP server left holding the transport would keep running a request nobody can
|
|
622
|
+
// read — the reason a closed stdin has to end foreground work rather than outlive it.
|
|
623
|
+
if tokio::time::timeout(INPUT_CLOSED_GRACE, &mut waiting).await.is_err() {
|
|
624
|
+
cancellation.cancel();
|
|
625
|
+
end_service(&mut waiting).await;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
_ = &mut waiting => {}
|
|
629
|
+
}
|
|
630
|
+
unframe.abort();
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/// Waits out a cancelled MCP service, then stops waiting.
|
|
634
|
+
async fn end_service<T>(waiting: &mut tokio::task::JoinHandle<T>) {
|
|
635
|
+
if tokio::time::timeout(SERVICE_SHUTDOWN_TIMEOUT, &mut *waiting)
|
|
636
|
+
.await
|
|
637
|
+
.is_err()
|
|
638
|
+
{
|
|
639
|
+
waiting.abort();
|
|
640
|
+
let _ = waiting.await;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async fn monitor_idle(
|
|
645
|
+
state: Arc<HostState>,
|
|
646
|
+
shutdown: CancellationToken,
|
|
647
|
+
idle_timeout: Duration,
|
|
648
|
+
candidate: tokio::sync::mpsc::Sender<()>,
|
|
649
|
+
) {
|
|
650
|
+
let interval = idle_timeout
|
|
651
|
+
.div_f64(4.0)
|
|
652
|
+
.clamp(Duration::from_millis(50), Duration::from_secs(30));
|
|
653
|
+
// First instant of the current unbroken run of registry-scan failures, if any.
|
|
654
|
+
let mut scan_failing_since: Option<Instant> = None;
|
|
655
|
+
loop {
|
|
656
|
+
tokio::select! {
|
|
657
|
+
() = shutdown.cancelled() => return,
|
|
658
|
+
() = tokio::time::sleep(interval) => {}
|
|
659
|
+
}
|
|
660
|
+
if !state.activity.is_shutdown_eligible(idle_timeout) {
|
|
661
|
+
scan_failing_since = None;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
// A host that is still running can open another conversation at any moment, and a host
|
|
665
|
+
// whose stdio MCP server has died never gets it back. Staying resident is the cheap side
|
|
666
|
+
// of that trade, so the idle timer does not even start while one of them is alive.
|
|
667
|
+
let hosts = Arc::clone(&state.hosts);
|
|
668
|
+
let hosts_alive = tokio::task::spawn_blocking(move || hosts.prune_and_check())
|
|
669
|
+
.await
|
|
670
|
+
.unwrap_or(true);
|
|
671
|
+
if hosts_alive {
|
|
672
|
+
scan_failing_since = None;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
let Some(paths) = state.control_paths.get().cloned() else {
|
|
676
|
+
if candidate.send(()).await.is_err() {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
continue;
|
|
680
|
+
};
|
|
681
|
+
let running = tokio::task::spawn_blocking(move || {
|
|
682
|
+
crate::shell::jobs::running_summaries(&paths).map(|jobs| !jobs.is_empty())
|
|
683
|
+
})
|
|
684
|
+
.await
|
|
685
|
+
.unwrap_or_else(|error| Err(format!("the inspection task failed: {error}")));
|
|
686
|
+
match running {
|
|
687
|
+
Ok(false) => {
|
|
688
|
+
scan_failing_since = None;
|
|
689
|
+
// A request may have arrived while the registry scan ran off-thread.
|
|
690
|
+
if !state.activity.is_shutdown_eligible(idle_timeout) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if candidate.send(()).await.is_err() {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
Ok(true) => scan_failing_since = None,
|
|
698
|
+
Err(error) => {
|
|
699
|
+
eprintln!(
|
|
700
|
+
"fastctx control center: cannot inspect running jobs for idle shutdown: {error}"
|
|
701
|
+
);
|
|
702
|
+
// Fail open once scans have failed for a full idle window: one damaged registry
|
|
703
|
+
// record fails every scan, and a host that insists on a clean scan before exiting
|
|
704
|
+
// would never exit. Exiting is safe — job supervisors are detached processes and
|
|
705
|
+
// the next connection bootstraps a fresh host that reads the same on-disk registry.
|
|
706
|
+
let failing_since = *scan_failing_since.get_or_insert_with(Instant::now);
|
|
707
|
+
if failing_since.elapsed() >= idle_timeout {
|
|
708
|
+
eprintln!(
|
|
709
|
+
"fastctx control center: registry scans kept failing for a full idle window; shutting down anyway."
|
|
710
|
+
);
|
|
711
|
+
if candidate.send(()).await.is_err() {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#[cfg(test)]
|
|
721
|
+
mod tests {
|
|
722
|
+
use super::short_hash;
|
|
723
|
+
|
|
724
|
+
#[test]
|
|
725
|
+
fn endpoint_hashes_are_stable_and_separate_inputs() {
|
|
726
|
+
assert_eq!(short_hash(b"home-a", 12), short_hash(b"home-a", 12));
|
|
727
|
+
assert_ne!(short_hash(b"home-a", 12), short_hash(b"home-b", 12));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#[cfg(unix)]
|
|
731
|
+
#[test]
|
|
732
|
+
fn long_unix_runtime_paths_fall_back_to_the_short_private_directory() {
|
|
733
|
+
use super::select_unix_runtime_directory;
|
|
734
|
+
use std::path::PathBuf;
|
|
735
|
+
|
|
736
|
+
let preferred = PathBuf::from("/").join("long".repeat(30));
|
|
737
|
+
let fallback = PathBuf::from("/tmp/fastctx-engine-1000");
|
|
738
|
+
let selected = select_unix_runtime_directory(
|
|
739
|
+
preferred,
|
|
740
|
+
fallback.clone(),
|
|
741
|
+
"fastctx-engine-0123456789ab-0123456789abcdef",
|
|
742
|
+
)
|
|
743
|
+
.unwrap();
|
|
744
|
+
assert_eq!(selected, fallback);
|
|
745
|
+
}
|
|
746
|
+
}
|