Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion rust/contracts/rust-core-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2570,7 +2570,12 @@ its optional `hostOptions` (plugin-owned sessions only; `editor:{actionIcons,
actionSourceBytes, providerTypes}` declares the icons, collector source bound and
provider types the host implements, reported through `lithe/host/getCapabilities`
to sessions granted the matching editor methods), plus
initialize, request, and shutdown deadlines.
initialize, request, and shutdown deadlines. Omitted
`initializeTimeoutMilliseconds` allows 60000 milliseconds for one startup
handshake (previously 10000); cold runtime loading can exceed ten seconds.
An explicit deadline still takes precedence. Cancellation and timeout still
terminate and dispose of the owned process tree; this does not add retries or
change feature-request deadlines.
Rust owns the returned
session's child process, stdin/stdout/stderr, framing buffer, JSON-RPC request
IDs, document versions, pending deadlines, capabilities, diagnostics, and
Expand Down
27 changes: 26 additions & 1 deletion rust/crates/core/src/lsp/interface/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};
use std::thread;
use std::time::{Duration, Instant};

const DEFAULT_INITIALIZE_TIMEOUT_MS: u64 = 10_000;
// Cold language-server runtimes can spend more than ten seconds loading their
// bundles before answering initialize. This remains one bounded handshake,
// independent of request deadlines, with normal cancellation and disposal.
const DEFAULT_INITIALIZE_TIMEOUT_MS: u64 = 60_000;
const DEFAULT_REQUEST_TIMEOUT_MS: u64 = 30_000;
const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 2_000;
/// Upper bound of one build-provider request. A model request can load a
Expand Down Expand Up @@ -6233,6 +6236,28 @@ mod tests {
}
}

/// A real cold Windows language server can take longer than ten seconds.
/// Exercise the omitted-field default with an injected instant: it must
/// allow that handshake while still disposing of a server that never replies.
#[test]
fn default_initialize_budget_survives_cold_start_and_still_expires() {
let parsed: StartServerRequest = serde_json::from_value(json!({
"providerId": "cold-fixture", "executablePath": "/scripted-server",
"rootUri": "file:///workspace", "workingDirectory": "/workspace"
}))
.unwrap();
let harness = Harness::start(|request| {
request.initialize_timeout_milliseconds = parsed.initialize_timeout_milliseconds;
});
let session = harness.session();
let now = Instant::now();
session.expire_deadlines_at(now + Duration::from_secs(15));
assert_eq!(harness.snapshot().state, LspLifecycleState::Initializing);
session.expire_deadlines_at(now + Duration::from_secs(61));
assert_eq!(harness.snapshot().state, LspLifecycleState::Failed);
assert!(session.process.exit_status().is_some());
}

/// Criterion 1: a spawned process that never initializes cannot become ready.
#[test]
fn a_server_that_never_answers_initialize_fails_instead_of_becoming_ready() {
Expand Down
Loading