Skip to main content

mcpls_core/bridge/translator/
restart.rs

1//! Manual restart of LSP servers on request (`restart_server`).
2//!
3//! A restart is kill-then-respawn: the old process tree is terminated first,
4//! then a replacement is spawned through the same body automatic respawn uses
5//! ([`Translator::respawn_locked`]), with the diagnostics pump re-wired so the
6//! restarted server returns to live push diagnostics. While the old process is
7//! being stopped the server is deregistered and reported as initializing, so
8//! concurrent tool calls get a retryable error instead of a dead client.
9
10use std::collections::HashSet;
11use std::sync::Arc;
12use std::time::Duration;
13
14use futures::StreamExt;
15use futures::future::BoxFuture;
16use schemars::JsonSchema;
17use serde::Serialize;
18use tokio::sync::mpsc;
19use tokio::task::AbortHandle;
20
21use super::Translator;
22use super::respawn::{BackoffPolicy, RespawnConfig};
23use super::servers::{Backend, Phase};
24use crate::bridge::indexing::IndexingReset;
25use crate::bridge::{DiagnosticsKey, IndexingState, NotificationCache};
26use crate::config::{LanguageId, ServerId};
27use crate::error::{Error, ErrorChain, Result};
28use crate::lsp::{ExitGrace, LspNotification};
29use crate::redaction::{Redactions, ServerText};
30use crate::util::lock_std;
31
32/// Minimum interval between two manual restart attempts of the same server.
33const RESTART_COOLDOWN: Duration = Duration::from_secs(5);
34
35/// How long the old server gets to answer `shutdown` on a restart. Short
36/// because a server that does not answer is killed anyway and the caller is
37/// waiting.
38const RESTART_HANDSHAKE_BUDGET: Duration = Duration::from_secs(3);
39
40/// How long a server that did answer `shutdown` may take to exit on its own
41/// on a restart (rust-analyzer and jdtls flush caches), before its process
42/// tree is killed.
43const RESTART_EXIT_GRACE: Duration = crate::lsp::SHUTDOWN_TIMEOUT;
44
45/// Most server ids one request may name.
46pub const MAX_RESTART_SERVER_IDS: usize = 64;
47
48/// How many servers one `restart_server` call restarts concurrently.
49const RESTART_CONCURRENCY: usize = 4;
50
51/// Longest accepted server id, in bytes.
52pub const MAX_SERVER_ID_BYTES: usize = 256;
53
54/// Why a list of server ids was rejected by [`ServerIds::try_new`] or [`ServerIds::parse`].
55#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
56pub enum ServerIdsError {
57    /// No ids were given.
58    #[error("`servers` must not be empty")]
59    Empty,
60    /// More than [`MAX_RESTART_SERVER_IDS`] ids were given.
61    #[error("`servers` may name at most {MAX_RESTART_SERVER_IDS} servers")]
62    TooMany,
63    /// An id was empty or whitespace; only [`ServerIds::parse`] returns it.
64    #[error("server ids must not be blank")]
65    Blank,
66    /// An id was longer than [`MAX_SERVER_ID_BYTES`].
67    #[error("server ids must be at most {MAX_SERVER_ID_BYTES} bytes")]
68    TooLong,
69}
70
71/// A non-empty, duplicate-free, bounded list of server ids: at most
72/// [`MAX_RESTART_SERVER_IDS`] ids of [`MAX_SERVER_ID_BYTES`] bytes each.
73///
74/// # Examples
75///
76/// ```
77/// use mcpls_core::bridge::{ServerIds, ServerIdsError};
78/// use mcpls_core::config::ServerId;
79///
80/// let ids = ServerIds::try_new(vec![ServerId::from_static("rust"), ServerId::from_static("rust")]).unwrap();
81/// assert_eq!(ids.as_slice().len(), 1);
82/// assert_eq!(ServerIds::try_new(Vec::new()), Err(ServerIdsError::Empty));
83/// ```
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct ServerIds(Vec<ServerId>);
86
87impl ServerIds {
88    /// Build the list, dropping repeated ids.
89    ///
90    /// # Errors
91    ///
92    /// [`ServerIdsError`] when `ids` is empty, has more than
93    /// [`MAX_RESTART_SERVER_IDS`] entries, or holds an oversized id.
94    pub fn try_new(ids: Vec<ServerId>) -> std::result::Result<Self, ServerIdsError> {
95        if ids.len() > MAX_RESTART_SERVER_IDS {
96            return Err(ServerIdsError::TooMany);
97        }
98        if ids.iter().any(|id| id.as_str().len() > MAX_SERVER_ID_BYTES) {
99            return Err(ServerIdsError::TooLong);
100        }
101        let mut seen = HashSet::new();
102        let unique: Vec<ServerId> = ids
103            .into_iter()
104            .filter(|id| seen.insert(id.clone()))
105            .collect();
106        (!unique.is_empty())
107            .ok_or(ServerIdsError::Empty)
108            .map(|()| Self(unique))
109    }
110
111    /// Parse the ids of a request, checking the count, blankness and length
112    /// in that order, then drop repeats like [`Self::try_new`].
113    ///
114    /// # Errors
115    ///
116    /// [`ServerIdsError`] when `ids` is empty, has more than
117    /// [`MAX_RESTART_SERVER_IDS`] entries, or holds a blank or oversized id.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use mcpls_core::bridge::{ServerIds, ServerIdsError};
123    ///
124    /// assert_eq!(ServerIds::parse(vec![" ".to_owned()]), Err(ServerIdsError::Blank));
125    /// assert_eq!(ServerIds::parse(vec!["rust".to_owned()]).unwrap().as_slice().len(), 1);
126    /// ```
127    pub fn parse(ids: Vec<String>) -> std::result::Result<Self, ServerIdsError> {
128        if ids.len() > MAX_RESTART_SERVER_IDS {
129            return Err(ServerIdsError::TooMany);
130        }
131        let ids = ids
132            .into_iter()
133            .map(|id| ServerId::new(id).map_err(|_| ServerIdsError::Blank))
134            .collect::<std::result::Result<Vec<_>, _>>()?;
135        Self::try_new(ids)
136    }
137
138    /// The ids, in request order.
139    #[must_use]
140    pub fn as_slice(&self) -> &[ServerId] {
141        &self.0
142    }
143}
144
145/// Which servers a restart applies to.
146///
147/// # Examples
148///
149/// ```
150/// use mcpls_core::bridge::{RestartTarget, ServerIds};
151/// use mcpls_core::config::ServerId;
152///
153/// let one = RestartTarget::Servers(ServerIds::try_new(vec![ServerId::from_static("rust")]).unwrap());
154/// assert_ne!(one, RestartTarget::All);
155/// ```
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum RestartTarget {
158    /// Every configured server.
159    All,
160    /// Exactly these servers.
161    Servers(ServerIds),
162}
163
164/// Why a restart failed.
165///
166/// [`Self::Refused`] leaves the old process running. Every other reason means
167/// the old process is already stopped: the server stays registered as dead,
168/// and the next tool call retries the spawn under the normal crash-loop
169/// backoff.
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
171#[serde(tag = "kind", rename_all = "snake_case")]
172#[non_exhaustive]
173pub enum RestartFailure {
174    /// Refused before the old process was stopped, which keeps serving.
175    ///
176    /// Covers every failure of the pre-stop check of the config the
177    /// replacement would start from: an untrusted-workspace refusal, and the
178    /// planning work itself failing.
179    Refused {
180        /// Why the restart was refused.
181        message: String,
182    },
183    /// The replacement process could not be started.
184    SpawnFailed {
185        /// Why the spawn failed.
186        message: String,
187    },
188    /// The replacement started but did not complete the `initialize` handshake.
189    InitializeFailed {
190        /// Why initialization failed.
191        message: String,
192    },
193    /// mcpls is shutting down, so no replacement was started.
194    ShuttingDown,
195}
196
197impl RestartFailure {
198    fn from_error(error: &Error) -> Self {
199        let message = ErrorChain(error).to_string();
200        match error {
201            Error::ServerNotFound { .. }
202            | Error::ServerSpawnFailed { .. }
203            | Error::ServerFailedToStart(_) => Self::SpawnFailed { message },
204            Error::LspInitFailed { .. }
205            | Error::ServerExitedDuringInit { .. }
206            | Error::ServerTerminated
207            | Error::TaskFailed { .. }
208            | Error::StdioCapture(_)
209            | Error::Timeout(_)
210            | Error::LspServerError(..)
211            | Error::LspProtocolError(_)
212            | Error::ShutdownTimeout
213            | Error::Io(_)
214            | Error::Json(_)
215            // Spawning a replacement never yields the variants below; they are
216            // reported as an initialization failure rather than a new reason.
217            | Error::McpServerStart(_)
218            | Error::PathToUri(_)
219            | Error::HttpBind { .. }
220            | Error::InvalidClientPath(_)
221            | Error::MalformedPath { .. }
222            | Error::DocumentNotFound(_)
223            | Error::NoServerForLanguage { .. }
224            | Error::NoServerForTool { .. }
225            | Error::ServerInitializing { .. }
226            | Error::ServerRestarted { .. }
227            | Error::SymbolResolution(_)
228            | Error::UnknownServers { .. }
229            | Error::WorkspaceServersInitializing
230            | Error::NoServerConfigured
231            | Error::NoServerForWorkspaceTool { .. }
232            | Error::ConfigNotFound(_)
233            | Error::ConfigInsideWorkspace { .. }
234            | Error::Config(_)
235            | Error::TomlDe(_)
236            | Error::TomlSer(_)
237            | Error::NoResolvableListenUris
238            | Error::ResourceUri(_)
239            | Error::InvalidPositionInput(_)
240            | Error::InvalidRangeInput(_)
241            | Error::InvalidHierarchyItemInput(_)
242            | Error::PositionBeyondDocument { .. }
243            | Error::ServerUnavailable { .. }
244            | Error::InvalidToolParams(_)
245            | Error::FileIo { .. }
246            | Error::PathOutsideWorkspace(_)
247            | Error::NoWorkspaceRoots(_)
248            | Error::DocumentLimitExceeded { .. }
249            | Error::SubscriptionLimitReached { .. }
250            | Error::ListenStreamsExhausted { .. }
251            | Error::ListenFilterTooLarge { .. }
252            | Error::FileSizeLimitExceeded { .. }
253            | Error::NotARegularFile(_)
254            | Error::AllServersFailedToInit { .. }
255            | Error::CapabilityNotSupported { .. }
256            | Error::WorkspaceIndexing { .. } => Self::InitializeFailed { message },
257        }
258    }
259}
260
261/// What happened to one server in a restart request.
262///
263/// # Examples
264///
265/// ```
266/// use mcpls_core::bridge::RestartOutcome;
267///
268/// let value = serde_json::to_value(RestartOutcome::Throttled { retry_in_ms: 1500 }).unwrap();
269/// assert_eq!(value["status"], "throttled");
270/// ```
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
272#[serde(tag = "status", rename_all = "snake_case")]
273#[non_exhaustive]
274pub enum RestartOutcome {
275    /// The server runs a freshly initialized process.
276    Restarted {
277        /// Another restart of this server finished while the request waited, so none was repeated.
278        coalesced: bool,
279        /// Indexing readiness of the new process.
280        indexing_state: IndexingState,
281        /// Push diagnostics for this server are still not live. A restart
282        /// clears this; it stays set if a crash respawn raced in afterwards
283        /// and restarting again is needed.
284        push_notifications_degraded: bool,
285    },
286    /// The restart failed; see [`RestartFailure`].
287    Failed {
288        /// Why it failed.
289        reason: RestartFailure,
290    },
291    /// The server was restarted too recently; retry after `retry_in_ms`.
292    Throttled {
293        /// Milliseconds until a restart is allowed again.
294        retry_in_ms: u64,
295    },
296    /// The server has not finished its initial startup; retry shortly.
297    Initializing,
298    /// The server never started (or failed at startup), so there is no
299    /// process to restart. Fix the cause and restart mcpls.
300    NotRunning {
301        /// Why the server is not running.
302        message: String,
303    },
304}
305
306impl RestartOutcome {
307    /// The outcome for a server that is not registered, or whose process is gone.
308    pub(crate) fn not_running(id: &ServerId) -> Self {
309        Self::NotRunning {
310            message: format!("LSP server '{id}' is not running"),
311        }
312    }
313}
314
315/// Whether a restart request ran the restart itself or found it done already.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317enum RestartRun {
318    /// This request stopped the old process and started the replacement.
319    Performed,
320    /// Another restart of the same server finished while this request waited.
321    Coalesced,
322}
323
324/// One server's entry in a [`RestartServerResult`].
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
326pub struct ServerRestartEntry {
327    /// The server this entry reports on.
328    pub server_id: ServerId,
329    /// What happened to it.
330    #[serde(flatten)]
331    pub outcome: RestartOutcome,
332}
333
334/// Result of a restart request: one entry per targeted server, sorted by id.
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
336pub struct RestartServerResult {
337    /// Per-server outcomes.
338    pub servers: Vec<ServerRestartEntry>,
339}
340
341/// Count of manual restarts completed for one server.
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
343pub(super) struct RestartGeneration(u64);
344
345impl RestartGeneration {
346    const fn bump(&mut self) {
347        self.0 = self.0.saturating_add(1);
348    }
349}
350
351/// The two notification lanes of a freshly spawned server.
352#[derive(Debug)]
353pub struct NotificationReceivers {
354    /// Diagnostics, log and `showMessage` lane.
355    pub(crate) notifications: crate::lsp::NotificationInbox,
356    /// `$/progress` and unrecognized notifications lane.
357    pub(crate) lifecycle: mpsc::Receiver<LspNotification>,
358    /// The `tsserver` path mcpls pinned for this server, so the lifecycle lane
359    /// can warn when the server reports it did not take effect.
360    pub(crate) pinned_tsserver: Option<std::path::PathBuf>,
361}
362
363impl NotificationReceivers {
364    /// The receivers a pump drains for a spawned server's `lanes`.
365    pub(crate) fn new(
366        lanes: crate::lsp::ServerLanes,
367        pinned_tsserver: Option<std::path::PathBuf>,
368    ) -> Self {
369        let crate::lsp::ServerLanes {
370            notifications,
371            lifecycle,
372        } = lanes;
373        Self {
374            notifications,
375            lifecycle,
376            pinned_tsserver,
377        }
378    }
379}
380
381/// How a respawned server's notifications are consumed.
382#[derive(Clone, Copy)]
383pub(super) enum NotificationRouting<'a> {
384    /// Drain and discard the notification lane; the diagnostics-route server
385    /// is flagged push-degraded (automatic crash respawn).
386    Discard,
387    /// Re-wire the diagnostics pump (manual restart).
388    Pump(&'a dyn NotificationWiring),
389}
390
391/// Whether a server's `publishDiagnostics` pushes are the ones the cache
392/// keeps for its language -- see #174 section 8.
393///
394/// The role is re-evaluated whenever a server settles: a catch-all that
395/// registered as `Secondary` becomes `Authoritative` when the explicit
396/// diagnostics server for its language fails afterwards. Pushes the catch-all
397/// made while it was `Secondary` are not replayed; the cache fills with its
398/// next publish for each file (the next `didOpen`/`didChange`). A file nobody
399/// opens may never get one, since only the disk sweep sends `didSave`, and
400/// only for changed files of a server with a subscribed file.
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402pub enum DiagnosticsRole {
403    /// The server is the language's diagnostics route: its pushes are cached
404    /// and subscribers are notified.
405    Authoritative,
406    /// Another server owns the language's diagnostics (or none does): pushes
407    /// are skipped so they cannot overwrite or spuriously notify about the
408    /// owner's entries.
409    Secondary,
410}
411
412/// Whether a server's push diagnostics are live or discarded after a cache reset.
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414pub(super) enum PushDelivery {
415    /// The replacement's pushes are cached again (manual restart re-wired the pump).
416    Live,
417    /// Pushes are not cached; the server is flagged push-degraded.
418    Degraded,
419}
420
421/// Drops what a stopped or replaced server left in `cache`: its indexing
422/// state per `indexing`, and, for the diagnostics-route server only, its
423/// cached diagnostics, then updates its push-degraded flag per `push`.
424/// Returns the cleared keys so subscribers can be told to re-read them.
425pub(super) fn reset_server_cache(
426    cache: &mut NotificationCache,
427    id: &ServerId,
428    indexing: IndexingReset,
429    role: DiagnosticsRole,
430    push: PushDelivery,
431) -> Vec<DiagnosticsKey> {
432    cache.reset_indexing_state(id, indexing);
433    if role != DiagnosticsRole::Authoritative {
434        return Vec::new();
435    }
436    let cleared = cache.clear_server_diagnostics(id);
437    match push {
438        PushDelivery::Live => cache.clear_push_degraded(id),
439        PushDelivery::Degraded => cache.mark_push_degraded(id),
440    }
441    cleared
442}
443
444impl DiagnosticsRole {
445    /// The role of a server that is, or is not, the language's diagnostics route.
446    pub(crate) const fn from_route(is_diagnostics_route: bool) -> Self {
447        if is_diagnostics_route {
448            Self::Authoritative
449        } else {
450            Self::Secondary
451        }
452    }
453}
454
455/// The part of notification handling that lives in `serve_with`'s scope
456/// (shutdown token, subscription registry) and so cannot be built by the
457/// translator itself.
458pub trait NotificationWiring: std::fmt::Debug + Send + Sync {
459    /// Start a diagnostics pump over `receivers` for `id`, returning a handle
460    /// that stops it. A panicking pump must mark `id` push-degraded.
461    fn spawn_pump(
462        &self,
463        id: ServerId,
464        receivers: NotificationReceivers,
465        role: DiagnosticsRole,
466    ) -> AbortHandle;
467
468    /// Tell subscribers of the resources behind the `cleared` cache keys to
469    /// re-read them: after a server's entries were cleared, and after a pull
470    /// write evicted other files' entries.
471    fn publish_invalidated<'a>(&'a self, cleared: &'a [DiagnosticsKey]) -> BoxFuture<'a, ()>;
472
473    /// Whether any session holds a subscription at all, so a caller can skip
474    /// work that only matters to subscribers. Defaults to `true`, which only
475    /// forgoes the saving.
476    fn has_subscriptions(&self) -> BoxFuture<'_, bool> {
477        Box::pin(async { true })
478    }
479
480    /// Tell subscribers of the resource behind `file`, the canonical URI of a
481    /// tracked file, that a `textDocument/diagnostic` report changed what a
482    /// read of it returns.
483    fn publish_changed<'a>(&'a self, file: &'a lsp_types::Uri) -> BoxFuture<'a, ()>;
484}
485
486/// Keeps restarts reporting `initializing` until dropped, so an init panic,
487/// task abort or early return can never leave the flag set.
488#[derive(Debug)]
489pub struct StartupGuard<'a>(&'a Translator);
490
491impl Drop for StartupGuard<'_> {
492    fn drop(&mut self) {
493        lock_std(&self.0.phase).finish_startup();
494    }
495}
496
497/// A server taken out of its slot for termination, restored on every exit
498/// path (error, early return, panic, cancelled future).
499struct Deregistered<'a> {
500    translator: &'a Translator,
501    id: ServerId,
502    backend: Option<Backend>,
503}
504
505impl<'a> Deregistered<'a> {
506    /// Marks `id` restarting while taking its backend, in one step, so no
507    /// caller ever finds it in neither state (which would read as a missing
508    /// server).
509    fn take(translator: &'a Translator, id: &ServerId) -> Option<Self> {
510        let backend = lock_std(&translator.servers).take_for_restart(id)?;
511        let held = Self {
512            translator,
513            id: id.clone(),
514            backend: Some(backend),
515        };
516        held.backend
517            .as_ref()
518            .is_some_and(|backend| backend.server().is_some())
519            .then_some(held)
520    }
521
522    /// Stop the held server. Termination errors (a wedged server times out)
523    /// are not fatal.
524    async fn terminate(&mut self) {
525        let Some(backend) = self.backend.as_mut() else {
526            return;
527        };
528        backend.client().mark_restarted(self.id.clone());
529        let Some(server) = backend.server_mut() else {
530            return;
531        };
532        let stopped = server
533            .terminate(
534                RESTART_HANDSHAKE_BUDGET,
535                ExitGrace::IfAnswered(RESTART_EXIT_GRACE),
536            )
537            .await;
538        if let Err(error) = stopped {
539            tracing::warn!(id = %self.id, error = %ErrorChain(&error), "old LSP server did not stop cleanly; continuing with restart");
540        }
541    }
542
543    /// Put the held backend back; idempotent.
544    fn restore(&mut self) {
545        if let Some(backend) = self.backend.take() {
546            let refused = lock_std(&self.translator.servers).restore(&self.id, backend);
547            if refused.is_some() {
548                tracing::warn!(id = %self.id, "restored server dropped: its slot was settled meanwhile");
549            }
550            drop(refused);
551        }
552    }
553}
554
555impl Drop for Deregistered<'_> {
556    fn drop(&mut self) {
557        self.restore();
558    }
559}
560
561impl Translator {
562    /// Restart the servers named by `target`, each independently.
563    ///
564    /// The old process tree is stopped (graceful `shutdown`/`exit` bounded by
565    /// the LSP shutdown deadline, then the whole process group is killed) and a
566    /// replacement is spawned and initialized from the server's existing
567    /// config. Requests in flight on the old process fail with a retryable
568    /// [`Error::ServerRestarted`]; documents are re-opened on the new process
569    /// on next access. Servers not named are untouched. Restarts of the same
570    /// server are serialized, and a request that queued behind another restart
571    /// of the same server is reported as coalesced.
572    ///
573    /// # Errors
574    ///
575    /// [`Error::UnknownServers`] when `target` names a server that is not
576    /// configured; nothing is restarted then. Per-server failures are reported
577    /// in the result, not as errors.
578    ///
579    /// # Examples
580    ///
581    /// ```no_run
582    /// use mcpls_core::bridge::{RestartTarget, Translator};
583    ///
584    /// # async fn run(translator: &Translator) -> mcpls_core::error::Result<()> {
585    /// let result = translator.restart_servers(RestartTarget::All).await?;
586    /// for entry in &result.servers {
587    ///     println!("{}: {:?}", entry.server_id, entry.outcome);
588    /// }
589    /// # Ok(())
590    /// # }
591    /// ```
592    pub async fn restart_servers(&self, target: RestartTarget) -> Result<RestartServerResult> {
593        let known = self.known_server_ids();
594        let ids = match target {
595            RestartTarget::All => known,
596            RestartTarget::Servers(requested) => {
597                let unknown: Vec<ServerId> = requested
598                    .as_slice()
599                    .iter()
600                    .filter(|id| !known.contains(id))
601                    .cloned()
602                    .collect();
603                if !unknown.is_empty() {
604                    return Err(Error::UnknownServers {
605                        unknown,
606                        configured: known,
607                    });
608                }
609                requested.0
610            }
611        };
612
613        // Snapshotted for every id up front: ids queued behind the
614        // concurrency limit must not observe a restart of their own request.
615        let requests: Vec<(ServerId, RestartGeneration)> = ids
616            .into_iter()
617            .map(|id| {
618                let generation = self.restart_generation(&id);
619                (id, generation)
620            })
621            .collect();
622        let mut servers: Vec<ServerRestartEntry> = futures::stream::iter(requests)
623            .map(|(id, generation)| async move {
624                let outcome = Box::pin(self.restart_one(&id, generation)).await;
625                ServerRestartEntry {
626                    server_id: id,
627                    outcome,
628                }
629            })
630            .buffer_unordered(RESTART_CONCURRENCY)
631            .collect()
632            .await;
633        servers.sort_by(|a, b| a.server_id.as_str().cmp(b.server_id.as_str()));
634        Ok(RestartServerResult { servers })
635    }
636
637    /// Every server a restart may address: registered, still expected, or
638    /// failed at startup. Sorted by id.
639    fn known_server_ids(&self) -> Vec<ServerId> {
640        let mut ids: Vec<ServerId> = lock_std(&self.servers).ids().cloned().collect();
641        ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
642        ids
643    }
644
645    fn restart_generation(&self, id: &ServerId) -> RestartGeneration {
646        lock_std(&self.servers)
647            .get(id)
648            .map_or_default(|slot| slot.restart.generation)
649    }
650
651    /// Install how restarted servers get their diagnostics pump back.
652    ///
653    /// Called once, before the first server settles, so each initial pump is
654    /// registered with [`Self::set_notification_task`] as its server
655    /// registers; a repeat call keeps the first wiring. Restarts stay blocked
656    /// until startup has settled (see [`Self::begin_startup`]).
657    pub(crate) fn install_wiring(&self, wiring: Arc<dyn NotificationWiring>) {
658        if self.wiring.set(wiring).is_err() {
659            tracing::debug!("notification wiring already installed");
660        }
661    }
662
663    /// Record the task consuming `id`'s notification lanes, so a restart can
664    /// stop it before starting its replacement.
665    pub(crate) fn set_notification_task(&self, id: &ServerId, handle: AbortHandle) {
666        if let Some(slot) = lock_std(&self.servers).get_mut(id) {
667            slot.notification_task = Some(handle);
668        }
669    }
670
671    /// Take the task consuming `id`'s notification lanes out of its slot.
672    pub(super) fn take_notification_task(&self, id: &ServerId) -> Option<AbortHandle> {
673        lock_std(&self.servers)
674            .get_mut(id)
675            .and_then(|slot| slot.notification_task.take())
676    }
677
678    /// Declare that the initial server startup is still settling; restarts
679    /// report `initializing` until the returned guard is dropped, because routes
680    /// (and so diagnostics roles) still change while servers settle; a restarted
681    /// pump follows those changes through the shared role registry.
682    pub(crate) fn begin_startup(&self) -> StartupGuard<'_> {
683        lock_std(&self.phase).begin_startup();
684        StartupGuard(self)
685    }
686
687    /// Declare that shutdown has begun; no restart starts a server after this.
688    pub(crate) fn begin_shutdown(&self) {
689        lock_std(&self.phase).begin_shutdown();
690    }
691
692    fn is_shutting_down(&self) -> bool {
693        *lock_std(&self.phase) == Phase::ShuttingDown
694    }
695
696    /// The cooldown still to wait before `id` may be restarted again.
697    fn restart_cooldown_remaining(&self, id: &ServerId) -> Option<Duration> {
698        let last = lock_std(&self.servers).get(id)?.restart.last_attempt?;
699        let elapsed = self.clock.now().saturating_duration_since(last);
700        RESTART_COOLDOWN.checked_sub(elapsed)
701    }
702
703    async fn restarted_outcome(&self, id: &ServerId, run: RestartRun) -> RestartOutcome {
704        let (indexing_state, push_notifications_degraded) = match &self.notification_cache {
705            Some(cache) => {
706                let cache = cache.lock().await;
707                (cache.indexing_state(id), cache.is_push_degraded(id))
708            }
709            None => (IndexingState::Unknown, false),
710        };
711        RestartOutcome::Restarted {
712            coalesced: run == RestartRun::Coalesced,
713            indexing_state,
714            push_notifications_degraded,
715        }
716    }
717
718    /// The notification wiring a restart of `id` needs, or why it cannot be
719    /// restarted right now, in classification order: shutting down, never
720    /// started or failed to start, init panicked, startup still settling or not
721    /// yet wired, then the cooldown.
722    fn restart_permit(
723        &self,
724        id: &ServerId,
725    ) -> std::result::Result<&Arc<dyn NotificationWiring>, RestartOutcome> {
726        let phase = *lock_std(&self.phase);
727        if phase == Phase::ShuttingDown {
728            return Err(RestartOutcome::Failed {
729                reason: RestartFailure::ShuttingDown,
730            });
731        }
732        let not_running = {
733            let servers = lock_std(&self.servers);
734            let outcome = servers
735                .server(id)
736                .is_none()
737                .then(|| match servers.failure(id) {
738                    Some(failure) => RestartOutcome::NotRunning {
739                        message: Error::ServerFailedToStart(Box::new(failure.clone())).to_string(),
740                    },
741                    None if servers.is_expected(id) => RestartOutcome::Initializing,
742                    None => RestartOutcome::not_running(id),
743                });
744            drop(servers);
745            outcome
746        };
747        if let Some(outcome) = not_running {
748            return Err(outcome);
749        }
750        if phase == Phase::InitPanicked {
751            return Err(RestartOutcome::NotRunning {
752                message: "startup was interrupted by a panic, so servers cannot be restarted; restart mcpls".to_string(),
753            });
754        }
755        let wiring = match (phase, self.wiring.get()) {
756            (Phase::Settling, _) | (_, None) => return Err(RestartOutcome::Initializing),
757            (_, Some(wiring)) => wiring,
758        };
759        self.restart_cooldown_remaining(id)
760            .map_or(Ok(wiring), |remaining| {
761                Err(RestartOutcome::Throttled {
762                    retry_in_ms: u64::try_from(remaining.as_millis()).unwrap_or(u64::MAX),
763                })
764            })
765    }
766
767    async fn restart_one(&self, id: &ServerId, seen: RestartGeneration) -> RestartOutcome {
768        let lock = self.respawn_lock(id);
769        let _serialized = lock.lock().await;
770
771        if self.restart_generation(id) != seen {
772            return self.restarted_outcome(id, RestartRun::Coalesced).await;
773        }
774        let wiring = match self.restart_permit(id) {
775            Ok(wiring) => wiring,
776            Err(outcome) => return outcome,
777        };
778
779        let running = lock_std(&self.servers)
780            .server(id)
781            .map(|server| server.init_config().clone());
782        let Some(running) = running else {
783            return RestartOutcome::not_running(id);
784        };
785        let vetted = match running.for_respawn().await {
786            Ok(vetted) => vetted,
787            Err(error) => {
788                tracing::warn!(%id, error = %ErrorChain(&error), "LSP server restart refused");
789                return RestartOutcome::Failed {
790                    reason: RestartFailure::Refused {
791                        message: ErrorChain(&error).to_string(),
792                    },
793                };
794            }
795        };
796
797        // A refusal stopped nothing, so it must not start the restart cooldown.
798        self.note_restart_attempt(id);
799        tracing::info!(%id, "restarting LSP server on request");
800
801        let Some(mut old) = Deregistered::take(self, id) else {
802            return RestartOutcome::not_running(id);
803        };
804        let language_id = vetted.server_config().language_id.clone();
805        old.terminate().await;
806        drop(old);
807
808        if self.is_shutting_down() {
809            self.invalidate_stopped_server(id, &language_id).await;
810            return RestartOutcome::Failed {
811                reason: RestartFailure::ShuttingDown,
812            };
813        }
814        let respawned = self
815            .respawn_locked(
816                id,
817                RespawnConfig::Vetted(vetted),
818                BackoffPolicy::Bypass,
819                NotificationRouting::Pump(wiring.as_ref()),
820            )
821            .await;
822        // Counted from completion too, so a slow restart cannot be chained at once.
823        self.note_restart_attempt(id);
824        if let Err(error) = respawned {
825            tracing::warn!(%id, error = %ErrorChain(&error), "LSP server restart failed");
826            self.invalidate_stopped_server(id, &language_id).await;
827            return RestartOutcome::Failed {
828                reason: RestartFailure::from_error(&error),
829            };
830        }
831
832        if self.is_shutting_down() {
833            self.discard_registered(id).await;
834            return RestartOutcome::Failed {
835                reason: RestartFailure::ShuttingDown,
836            };
837        }
838        if let Some(slot) = lock_std(&self.servers).get_mut(id) {
839            slot.restart.generation.bump();
840        }
841        tracing::info!(%id, "LSP server restarted");
842        self.restarted_outcome(id, RestartRun::Performed).await
843    }
844
845    fn note_restart_attempt(&self, id: &ServerId) {
846        if let Some(slot) = lock_std(&self.servers).get_mut(id) {
847            slot.restart.last_attempt = Some(self.clock.now());
848        }
849    }
850
851    /// The old process is gone and no replacement runs: drop what it cached
852    /// and say so, so its diagnostics are never served as live.
853    async fn invalidate_stopped_server(&self, id: &ServerId, language_id: &LanguageId) {
854        // Best effort: the old pump goes first, and an aborted task stops at its
855        // next await, so it cannot re-cache what the dead server buffered
856        // once the cache below is cleared.
857        if let Some(pump) = self.take_notification_task(id) {
858            pump.abort();
859            tokio::task::yield_now().await;
860        }
861        let cleared = match &self.notification_cache {
862            Some(cache) => {
863                let mut cache = cache.lock().await;
864                reset_server_cache(
865                    &mut cache,
866                    id,
867                    IndexingReset::Forget,
868                    DiagnosticsRole::from_route(self.is_diagnostics_route(language_id, id)),
869                    PushDelivery::Degraded,
870                )
871            }
872            None => Vec::new(),
873        };
874        if let Some(wiring) = self.wiring.get() {
875            wiring.publish_invalidated(&cleared).await;
876        }
877    }
878
879    /// Shut down and deregister the server just registered under `id`, after
880    /// shutdown began while its replacement was starting.
881    async fn discard_registered(&self, id: &ServerId) {
882        let server = lock_std(&self.servers).remove_server(id);
883        if let Some(server) = server
884            && let Err(error) = server.shutdown().await
885        {
886            tracing::warn!(%id, error = %ErrorChain(&error), "replacement LSP server shutdown failed");
887        }
888    }
889}
890
891impl ServerText for RestartFailure {
892    fn redact_server_text(&mut self, redactions: &Redactions) {
893        match self {
894            Self::Refused { message }
895            | Self::SpawnFailed { message }
896            | Self::InitializeFailed { message } => {
897                redactions.redact_in_place(message);
898            }
899            Self::ShuttingDown => {}
900        }
901    }
902}
903
904impl ServerText for RestartOutcome {
905    fn redact_server_text(&mut self, redactions: &Redactions) {
906        match self {
907            Self::Restarted {
908                coalesced: _,
909                indexing_state: _,
910                push_notifications_degraded: _,
911            }
912            | Self::Throttled { retry_in_ms: _ }
913            | Self::Initializing => {}
914            Self::Failed { reason } => reason.redact_server_text(redactions),
915            Self::NotRunning { message } => redactions.redact_in_place(message),
916        }
917    }
918}
919
920impl ServerText for ServerRestartEntry {
921    fn redact_server_text(&mut self, redactions: &Redactions) {
922        let Self {
923            server_id: _,
924            outcome,
925        } = self;
926        outcome.redact_server_text(redactions);
927    }
928}
929
930impl ServerText for RestartServerResult {
931    fn redact_server_text(&mut self, redactions: &Redactions) {
932        let Self { servers } = self;
933        servers.redact_server_text(redactions);
934    }
935}
936
937#[cfg(test)]
938mod tests {
939    use std::assert_matches;
940
941    use super::*;
942
943    #[test]
944    fn server_ids_drop_repeats_keep_order_and_reject_empty() {
945        let ids = ServerIds::try_new(vec![
946            ServerId::from_static("b"),
947            ServerId::from_static("a"),
948            ServerId::from_static("b"),
949        ])
950        .unwrap();
951        assert_eq!(
952            ids.as_slice(),
953            [ServerId::from_static("b"), ServerId::from_static("a")]
954        );
955        assert_eq!(ServerIds::try_new(Vec::new()), Err(ServerIdsError::Empty));
956        let ids = |count: usize| {
957            (0..count)
958                .map(|i| ServerId::new(format!("s{i}")).unwrap())
959                .collect()
960        };
961        assert!(ServerIds::try_new(ids(MAX_RESTART_SERVER_IDS)).is_ok());
962        assert_eq!(
963            ServerIds::try_new(ids(MAX_RESTART_SERVER_IDS + 1)),
964            Err(ServerIdsError::TooMany)
965        );
966        assert_eq!(
967            ServerIds::parse(vec![" ".to_owned()]),
968            Err(ServerIdsError::Blank)
969        );
970        assert_eq!(
971            ServerIds::try_new(vec![
972                ServerId::new("x".repeat(MAX_SERVER_ID_BYTES + 1)).unwrap()
973            ]),
974            Err(ServerIdsError::TooLong)
975        );
976        assert_eq!(
977            ServerIds::parse(vec![" ".to_owned(); MAX_RESTART_SERVER_IDS + 1]),
978            Err(ServerIdsError::TooMany)
979        );
980        assert_eq!(
981            ServerIds::parse(vec!["x".repeat(MAX_SERVER_ID_BYTES + 1), " ".to_owned()]),
982            Err(ServerIdsError::Blank)
983        );
984    }
985
986    #[test]
987    fn a_refusal_serializes_with_its_own_kind() {
988        let refused = RestartFailure::Refused {
989            message: "no".to_string(),
990        };
991        assert_eq!(
992            serde_json::to_value(refused).unwrap(),
993            serde_json::json!({"kind": "refused", "message": "no"})
994        );
995    }
996
997    #[test]
998    fn restart_entry_flattens_the_outcome_next_to_the_server_id() {
999        let entry = ServerRestartEntry {
1000            server_id: ServerId::from_static("rust"),
1001            outcome: RestartOutcome::Failed {
1002                reason: RestartFailure::SpawnFailed {
1003                    message: "gone".to_string(),
1004                },
1005            },
1006        };
1007        assert_eq!(
1008            serde_json::to_value(entry).unwrap(),
1009            serde_json::json!({
1010                "server_id": "rust",
1011                "status": "failed",
1012                "reason": {"kind": "spawn_failed", "message": "gone"},
1013            })
1014        );
1015    }
1016
1017    #[test]
1018    fn spawn_errors_map_to_spawn_failed_and_the_rest_to_initialize_failed() {
1019        let not_found = Error::ServerNotFound {
1020            command: crate::config::ServerCommand::from_static("x"),
1021            io: std::io::Error::from(std::io::ErrorKind::NotFound),
1022        };
1023        assert_matches!(
1024            RestartFailure::from_error(&not_found),
1025            RestartFailure::SpawnFailed { .. }
1026        );
1027        assert_matches!(
1028            RestartFailure::from_error(&Error::Timeout(Duration::from_secs(5))),
1029            RestartFailure::InitializeFailed { .. }
1030        );
1031    }
1032
1033    #[test]
1034    fn restart_failure_keeps_the_os_cause() {
1035        let spawn = Error::ServerSpawnFailed {
1036            command: crate::config::ServerCommand::from_static("x"),
1037            source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied"),
1038        };
1039        let RestartFailure::SpawnFailed { message } = RestartFailure::from_error(&spawn) else {
1040            panic!("expected SpawnFailed");
1041        };
1042        assert_eq!(message, "failed to spawn LSP server 'x': permission denied");
1043    }
1044
1045    /// #815: a refusal stops nothing, so it must not start the restart
1046    /// cooldown. Needs no process: the refusal is the argument recheck, whose
1047    /// trigger (an executable file named `tool.exe` in the workspace) holds on
1048    /// every host.
1049    #[tokio::test]
1050    async fn test_refused_restart_leaves_the_cooldown_untouched() {
1051        use tokio::sync::Mutex;
1052        use tokio_util::sync::CancellationToken;
1053
1054        use crate::bridge::translator::clock::{Clock, FakeClock};
1055        use crate::bridge::{NotificationCache, WorkspaceRoots};
1056        use crate::config::{LanguageId, ToolRouter};
1057        use crate::mcp::SubscriptionRegistry;
1058        use crate::runtime::pump::{DiagnosticsRoles, PumpShared, PumpWiring};
1059
1060        let dir = tempfile::TempDir::new().unwrap();
1061        let tool = dir.path().join("tool.exe");
1062        std::fs::write(&tool, "").unwrap();
1063        #[cfg(unix)]
1064        {
1065            use std::os::unix::fs::PermissionsExt as _;
1066            std::fs::set_permissions(&tool, std::fs::Permissions::from_mode(0o755)).unwrap();
1067        }
1068        let roots = WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap();
1069
1070        let id = ServerId::from_static("rust");
1071        let cache = Arc::new(Mutex::new(NotificationCache::new()));
1072        let clock = Arc::new(FakeClock::new());
1073        let mut translator = Translator::new()
1074            .with_router(ToolRouter::catch_all([(
1075                id.clone(),
1076                LanguageId::from_static("rust"),
1077            )]))
1078            .with_notification_cache(Arc::clone(&cache))
1079            .with_clock(Arc::clone(&clock) as Arc<dyn Clock>);
1080        translator.set_workspace_roots(roots.clone());
1081        translator.register_server_complete(crate::lsp::fake_lsp_server_with_config(
1082            crate::config::LspServerConfig::rust_analyzer(),
1083        ));
1084        translator.install_wiring(Arc::new(PumpWiring::new(
1085            PumpShared {
1086                roles: DiagnosticsRoles::default(),
1087                notification_cache: cache,
1088                subs: SubscriptionRegistry::new(),
1089                workspace_roots: translator.workspace_roots.clone(),
1090            },
1091            CancellationToken::new(),
1092        )));
1093
1094        let init = lock_std(&translator.servers)
1095            .server_mut(&id)
1096            .unwrap()
1097            .init_config()
1098            .clone();
1099        let command = init.server_config().command.server_command().clone();
1100        let mut init = init.with_trust(crate::lsp::SpawnTrust::Untrusted(
1101            crate::lsp::vetting::UntrustedVetting::new(roots, command),
1102        ));
1103        init.server_config_mut().args =
1104            crate::config::ServerArgs::new([tool.to_string_lossy().into_owned()]).unwrap();
1105        lock_std(&translator.servers)
1106            .server_mut(&id)
1107            .unwrap()
1108            .set_init_config(init);
1109
1110        let result = translator
1111            .restart_servers(RestartTarget::All)
1112            .await
1113            .unwrap();
1114
1115        assert_matches!(
1116            &result.servers[0].outcome,
1117            RestartOutcome::Failed {
1118                reason: RestartFailure::Refused { .. }
1119            },
1120            "{result:?}"
1121        );
1122        assert_eq!(translator.restart_cooldown_remaining(&id), None);
1123    }
1124
1125    #[cfg(unix)]
1126    mod restart_tests {
1127        use std::fs;
1128        use std::path::{Path, PathBuf};
1129
1130        use tempfile::TempDir;
1131        use tokio::sync::Mutex;
1132        use tokio_util::sync::CancellationToken;
1133
1134        use super::*;
1135        use crate::bridge::translator::clock::{Clock, FakeClock};
1136        use crate::bridge::translator::testing::{
1137            stub_server_config, write_crash_after_init_script, write_protocol_server_script,
1138            write_responder_script, write_slow_exit_server_script,
1139        };
1140        use crate::bridge::{NotificationCache, WorkspaceRoots};
1141        use crate::config::{LanguageId, ServerCommand, ToolRouter};
1142        use crate::error::{ServerSpawnFailure, StartupFailure};
1143        use crate::mcp::SubscriptionRegistry;
1144        use crate::runtime::pump::{PumpShared, PumpWiring};
1145
1146        struct Fixture {
1147            translator: Arc<Translator>,
1148            cache: Arc<Mutex<NotificationCache>>,
1149            clock: Arc<FakeClock>,
1150            id: ServerId,
1151            dir: TempDir,
1152        }
1153
1154        fn server_ids(names: &[&str]) -> RestartTarget {
1155            RestartTarget::Servers(
1156                ServerIds::try_new(names.iter().map(|n| ServerId::new(*n).unwrap()).collect())
1157                    .unwrap(),
1158            )
1159        }
1160
1161        async fn fixture(dir: TempDir, script: &Path, wired: bool) -> Fixture {
1162            let id = ServerId::from_static("rust");
1163            let cache = Arc::new(Mutex::new(NotificationCache::new()));
1164            let clock = Arc::new(FakeClock::new());
1165            let mut translator = Translator::new()
1166                .with_router(ToolRouter::catch_all([(
1167                    id.clone(),
1168                    LanguageId::from_static("rust"),
1169                )]))
1170                .with_notification_cache(Arc::clone(&cache))
1171                .with_clock(clock.clone());
1172            translator.set_workspace_roots(
1173                WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap(),
1174            );
1175
1176            let crate::lsp::SpawnedServer { server, lanes } =
1177                crate::lsp::LspServer::spawn(stub_server_config("rust", script))
1178                    .await
1179                    .unwrap();
1180            let receivers = NotificationReceivers::new(lanes, None);
1181            translator.register_server_complete(server);
1182
1183            let wiring = PumpWiring::new(
1184                PumpShared {
1185                    roles: crate::runtime::pump::DiagnosticsRoles::default(),
1186                    notification_cache: Arc::clone(&cache),
1187                    subs: SubscriptionRegistry::new(),
1188                    workspace_roots: translator.workspace_roots.clone(),
1189                },
1190                CancellationToken::new(),
1191            );
1192            let pump = wiring.spawn_pump(id.clone(), receivers, DiagnosticsRole::Authoritative);
1193            translator.set_notification_task(&id, pump);
1194            if wired {
1195                translator.install_wiring(Arc::new(wiring));
1196            }
1197            Fixture {
1198                translator: Arc::new(translator),
1199                cache,
1200                clock,
1201                id,
1202                dir,
1203            }
1204        }
1205
1206        async fn protocol_fixture(publish: Option<&str>) -> (Fixture, PathBuf) {
1207            let dir = TempDir::new().unwrap();
1208            let log = dir.path().join("server.log");
1209            let script = write_protocol_server_script(dir.path(), &log, publish);
1210            (fixture(dir, &script, true).await, log)
1211        }
1212
1213        fn read_log(log: &Path) -> String {
1214            fs::read_to_string(log).unwrap_or_default()
1215        }
1216
1217        fn only_outcome(result: &RestartServerResult) -> &RestartOutcome {
1218            assert_eq!(result.servers.len(), 1, "{result:?}");
1219            &result.servers[0].outcome
1220        }
1221
1222        impl Fixture {
1223            fn root(&self) -> &Path {
1224                self.dir.path()
1225            }
1226
1227            #[cfg(unix)]
1228            fn vet_arguments_against(&self, boundary: WorkspaceRoots, tool: &Path) {
1229                let init = lock_std(&self.translator.servers)
1230                    .server_mut(&self.id)
1231                    .unwrap()
1232                    .init_config()
1233                    .clone();
1234                let command = init.server_config().command.server_command().clone();
1235                let mut init = init.with_trust(crate::lsp::SpawnTrust::Untrusted(
1236                    crate::lsp::vetting::UntrustedVetting::new(boundary, command),
1237                ));
1238                let args = init.server_config_mut();
1239                args.args = crate::config::ServerArgs::new(
1240                    args.args
1241                        .iter()
1242                        .cloned()
1243                        .chain([tool.to_string_lossy().into_owned()]),
1244                )
1245                .unwrap();
1246                lock_std(&self.translator.servers)
1247                    .server_mut(&self.id)
1248                    .unwrap()
1249                    .set_init_config(init);
1250            }
1251
1252            fn registered(&self) -> bool {
1253                let servers = lock_std(&self.translator.servers);
1254                servers.server(&self.id).is_some() && servers.client(&self.id).is_some()
1255            }
1256
1257            fn expected(&self) -> bool {
1258                lock_std(&self.translator.servers).is_expected(&self.id)
1259            }
1260
1261            fn is_dead(&self) -> bool {
1262                lock_std(&self.translator.servers)
1263                    .server_mut(&self.id)
1264                    .unwrap()
1265                    .is_dead()
1266                    .unwrap()
1267            }
1268
1269            async fn wait_until_deregistered(&self) {
1270                tokio::time::timeout(Duration::from_secs(5), async {
1271                    while !self.expected() {
1272                        tokio::time::sleep(Duration::from_millis(10)).await;
1273                    }
1274                })
1275                .await
1276                .expect("the restart must deregister the server");
1277            }
1278        }
1279
1280        #[tokio::test]
1281        async fn test_restart_replaces_the_process_after_a_graceful_stop() {
1282            let (fx, log) = protocol_fixture(None).await;
1283
1284            let result = fx
1285                .translator
1286                .restart_servers(RestartTarget::All)
1287                .await
1288                .unwrap();
1289
1290            assert_matches!(
1291                only_outcome(&result),
1292                RestartOutcome::Restarted {
1293                    coalesced: false,
1294                    ..
1295                },
1296                "{result:?}"
1297            );
1298            let log = read_log(&log);
1299            assert_eq!(log.matches("started").count(), 2, "{log}");
1300            assert!(log.contains(r#""method":"shutdown""#), "{log}");
1301            assert!(log.contains(r#""method":"exit""#), "{log}");
1302            assert!(fx.registered() && !fx.expected() && !fx.is_dead());
1303        }
1304
1305        /// Regression test for #736: a restart refused by the argument recheck
1306        /// is decided before the running server is stopped.
1307        #[cfg(unix)]
1308        #[tokio::test]
1309        async fn test_restart_refused_by_the_argument_recheck_keeps_the_old_server_running() {
1310            use std::os::unix::fs::PermissionsExt as _;
1311
1312            let (fx, log) = protocol_fixture(None).await;
1313            let workspace = fx.root().join("ws");
1314            fs::create_dir_all(&workspace).unwrap();
1315            let tool = workspace.join("tool");
1316            fs::write(&tool, "").unwrap();
1317            fx.vet_arguments_against(WorkspaceRoots::from_paths(&[workspace]).unwrap(), &tool);
1318            fx.translator
1319                .restart_servers(RestartTarget::All)
1320                .await
1321                .unwrap();
1322            fx.clock.advance(RESTART_COOLDOWN + Duration::from_secs(1));
1323            fs::set_permissions(&tool, fs::Permissions::from_mode(0o755)).unwrap();
1324
1325            let result = fx
1326                .translator
1327                .restart_servers(RestartTarget::All)
1328                .await
1329                .unwrap();
1330
1331            let RestartOutcome::Failed {
1332                reason: RestartFailure::Refused { message },
1333            } = only_outcome(&result)
1334            else {
1335                panic!("expected a refused restart, got {result:?}");
1336            };
1337            assert!(
1338                message.contains("executable inside the workspace"),
1339                "{message}"
1340            );
1341            let log = read_log(&log);
1342            assert_eq!(log.matches("started").count(), 2, "{log}");
1343            assert!(fx.registered() && !fx.expected() && !fx.is_dead());
1344        }
1345
1346        /// A refusal stops nothing, so it must not start the restart cooldown.
1347        #[cfg(unix)]
1348        #[tokio::test]
1349        async fn test_refused_restart_does_not_start_the_cooldown() {
1350            use std::os::unix::fs::PermissionsExt as _;
1351
1352            let (fx, _log) = protocol_fixture(None).await;
1353            let workspace = fx.root().join("ws");
1354            fs::create_dir_all(&workspace).unwrap();
1355            let tool = workspace.join("tool");
1356            fs::write(&tool, "").unwrap();
1357            fx.vet_arguments_against(WorkspaceRoots::from_paths(&[workspace]).unwrap(), &tool);
1358            fx.translator
1359                .restart_servers(RestartTarget::All)
1360                .await
1361                .unwrap();
1362            fx.clock.advance(RESTART_COOLDOWN + Duration::from_secs(1));
1363            fs::set_permissions(&tool, fs::Permissions::from_mode(0o755)).unwrap();
1364            let refused = fx
1365                .translator
1366                .restart_servers(RestartTarget::All)
1367                .await
1368                .unwrap();
1369            assert_matches!(
1370                only_outcome(&refused),
1371                RestartOutcome::Failed {
1372                    reason: RestartFailure::Refused { .. }
1373                },
1374                "{refused:?}"
1375            );
1376
1377            fs::set_permissions(&tool, fs::Permissions::from_mode(0o644)).unwrap();
1378            let retried = fx
1379                .translator
1380                .restart_servers(RestartTarget::All)
1381                .await
1382                .unwrap();
1383
1384            assert_matches!(
1385                only_outcome(&retried),
1386                RestartOutcome::Restarted { .. },
1387                "{retried:?}"
1388            );
1389        }
1390
1391        #[tokio::test]
1392        async fn test_restart_of_a_wedged_server_completes_despite_the_terminate_error() {
1393            let dir = TempDir::new().unwrap();
1394            let script = write_responder_script(dir.path(), 600);
1395            let fx = fixture(dir, &script, true).await;
1396
1397            let result = fx
1398                .translator
1399                .restart_servers(RestartTarget::All)
1400                .await
1401                .unwrap();
1402
1403            assert_matches!(
1404                only_outcome(&result),
1405                RestartOutcome::Restarted { .. },
1406                "{result:?}"
1407            );
1408            assert!(fx.registered() && !fx.expected() && !fx.is_dead());
1409        }
1410
1411        #[tokio::test]
1412        async fn test_cancelled_restart_restores_the_registry() {
1413            let dir = TempDir::new().unwrap();
1414            let script = write_responder_script(dir.path(), 600);
1415            let fx = fixture(dir, &script, true).await;
1416
1417            let translator = Arc::clone(&fx.translator);
1418            let task =
1419                tokio::spawn(async move { translator.restart_servers(RestartTarget::All).await });
1420            fx.wait_until_deregistered().await;
1421            assert!(!fx.registered());
1422
1423            task.abort();
1424            assert!(task.await.unwrap_err().is_cancelled());
1425
1426            assert!(fx.registered() && !fx.expected());
1427        }
1428
1429        #[tokio::test]
1430        async fn test_restart_of_a_startup_failed_server_reports_not_running() {
1431            let id = ServerId::from_static("rust");
1432            let translator = Translator::new().with_router(ToolRouter::catch_all([(
1433                id.clone(),
1434                LanguageId::from_static("rust"),
1435            )]));
1436            translator.record_startup_failures(&[ServerSpawnFailure {
1437                server_id: id.clone(),
1438                language_id: LanguageId::from_static("rust"),
1439                command: ServerCommand::from_static("missing"),
1440                reason: StartupFailure::InitTaskPanicked,
1441            }]);
1442            translator.rebind_router(&HashSet::new());
1443            translator.clear_expected_servers();
1444
1445            let result = translator
1446                .restart_servers(server_ids(&["rust"]))
1447                .await
1448                .unwrap();
1449
1450            assert_matches!(
1451                only_outcome(&result),
1452                RestartOutcome::NotRunning { .. },
1453                "{result:?}"
1454            );
1455        }
1456
1457        #[tokio::test]
1458        async fn test_restart_all_reports_a_refused_server_as_not_running() {
1459            let config = crate::config::LspServerConfig::rust_analyzer();
1460            let translator =
1461                Translator::new().with_router(ToolRouter::from_configs([&config]).unwrap());
1462            translator.record_refusals(&[ServerSpawnFailure {
1463                server_id: config.id(),
1464                language_id: config.language_id.clone(),
1465                command: config.command.server_command().clone(),
1466                reason: StartupFailure::RefusedUntrustedWorkspace(
1467                    crate::error::UntrustedRefusal::NotAllowed { builtin: None },
1468                ),
1469            }]);
1470
1471            let result = translator
1472                .restart_servers(RestartTarget::All)
1473                .await
1474                .unwrap();
1475
1476            let outcome = only_outcome(&result);
1477            assert_matches!(
1478                outcome,
1479                RestartOutcome::NotRunning { message } if message.contains("--allow-server rust"),
1480                "{result:?}"
1481            );
1482        }
1483
1484        #[tokio::test]
1485        async fn test_restart_of_a_settled_server_while_startup_settles_reports_initializing() {
1486            let dir = TempDir::new().unwrap();
1487            let log = dir.path().join("server.log");
1488            let script = write_protocol_server_script(dir.path(), &log, None);
1489            let fx = fixture(dir, &script, true).await;
1490            let startup = fx.translator.begin_startup();
1491
1492            let during = fx
1493                .translator
1494                .restart_servers(server_ids(&["rust"]))
1495                .await
1496                .unwrap();
1497            assert_eq!(*only_outcome(&during), RestartOutcome::Initializing);
1498
1499            drop(startup);
1500            let after = fx
1501                .translator
1502                .restart_servers(server_ids(&["rust"]))
1503                .await
1504                .unwrap();
1505            assert_matches!(
1506                only_outcome(&after),
1507                RestartOutcome::Restarted { .. },
1508                "{after:?}"
1509            );
1510        }
1511
1512        #[tokio::test]
1513        async fn test_restart_permit_classifies_in_order() {
1514            let id = ServerId::from_static("rust");
1515            let translator = Translator::new();
1516            assert_matches!(
1517                translator.restart_permit(&id),
1518                Err(RestartOutcome::NotRunning { .. })
1519            );
1520
1521            translator.set_expected_servers(HashSet::from([id.clone()]));
1522            assert_matches!(
1523                translator.restart_permit(&id),
1524                Err(RestartOutcome::Initializing)
1525            );
1526
1527            translator.clear_expected_servers();
1528            translator.record_startup_failures(&[ServerSpawnFailure {
1529                server_id: id.clone(),
1530                language_id: crate::config::LanguageId::from_static("rust"),
1531                command: ServerCommand::from_static("missing"),
1532                reason: StartupFailure::InitTaskPanicked,
1533            }]);
1534            assert_matches!(
1535                translator.restart_permit(&id),
1536                Err(RestartOutcome::NotRunning { .. })
1537            );
1538
1539            let config = crate::config::LspServerConfig::rust_analyzer();
1540            let running = Translator::new();
1541            running
1542                .register_server_complete(crate::lsp::fake_lsp_server_with_config(config.clone()));
1543            let rid = config.id();
1544            assert_matches!(
1545                running.restart_permit(&rid),
1546                Err(RestartOutcome::Initializing)
1547            );
1548
1549            lock_std(&running.phase).init_panicked();
1550            assert_matches!(
1551                running.restart_permit(&rid),
1552                Err(RestartOutcome::NotRunning { .. })
1553            );
1554
1555            running.begin_shutdown();
1556            assert_matches!(
1557                running.restart_permit(&rid),
1558                Err(RestartOutcome::Failed {
1559                    reason: RestartFailure::ShuttingDown
1560                })
1561            );
1562        }
1563
1564        #[test]
1565        fn test_dropping_the_startup_guard_clears_the_flag() {
1566            let translator = Translator::new();
1567            let startup = translator.begin_startup();
1568            assert_eq!(*lock_std(&translator.phase), Phase::Settling);
1569            drop(startup);
1570            assert_eq!(*lock_std(&translator.phase), Phase::Settled);
1571        }
1572
1573        #[tokio::test]
1574        async fn test_restart_of_a_startup_failed_server_while_startup_settles_reports_not_running()
1575        {
1576            let id = ServerId::from_static("rust");
1577            let translator = Translator::new().with_router(ToolRouter::catch_all([(
1578                id.clone(),
1579                LanguageId::from_static("rust"),
1580            )]));
1581            translator.record_startup_failures(&[ServerSpawnFailure {
1582                server_id: id,
1583                language_id: LanguageId::from_static("rust"),
1584                command: ServerCommand::from_static("missing"),
1585                reason: StartupFailure::InitTaskPanicked,
1586            }]);
1587            let _startup = translator.begin_startup();
1588
1589            let result = translator
1590                .restart_servers(server_ids(&["rust"]))
1591                .await
1592                .unwrap();
1593
1594            assert_matches!(
1595                only_outcome(&result),
1596                RestartOutcome::NotRunning { .. },
1597                "{result:?}"
1598            );
1599        }
1600
1601        #[tokio::test]
1602        async fn test_restart_of_a_still_expected_server_reports_initializing() {
1603            let id = ServerId::from_static("rust");
1604            let translator = Translator::new().with_router(ToolRouter::catch_all([(
1605                id.clone(),
1606                LanguageId::from_static("rust"),
1607            )]));
1608            translator.set_expected_servers(HashSet::from([id]));
1609
1610            let result = translator
1611                .restart_servers(server_ids(&["rust"]))
1612                .await
1613                .unwrap();
1614
1615            assert_eq!(*only_outcome(&result), RestartOutcome::Initializing);
1616        }
1617
1618        #[tokio::test]
1619        async fn test_restart_before_the_wiring_is_installed_reports_initializing() {
1620            let dir = TempDir::new().unwrap();
1621            let log = dir.path().join("server.log");
1622            let script = write_protocol_server_script(dir.path(), &log, None);
1623            let fx = fixture(dir, &script, false).await;
1624
1625            let result = fx
1626                .translator
1627                .restart_servers(RestartTarget::All)
1628                .await
1629                .unwrap();
1630
1631            assert_eq!(*only_outcome(&result), RestartOutcome::Initializing);
1632            assert_eq!(read_log(&log).matches("started").count(), 1);
1633            assert!(fx.registered());
1634        }
1635
1636        #[tokio::test]
1637        async fn test_restart_rejects_unknown_ids_and_restarts_nothing() {
1638            let (fx, log) = protocol_fixture(None).await;
1639
1640            let err = fx
1641                .translator
1642                .restart_servers(server_ids(&["rust", "nope"]))
1643                .await
1644                .unwrap_err();
1645
1646            let Error::UnknownServers {
1647                unknown,
1648                configured,
1649            } = err
1650            else {
1651                panic!("expected UnknownServers, got {err:?}");
1652            };
1653            assert_eq!(unknown, [ServerId::from_static("nope")]);
1654            assert_eq!(configured, [ServerId::from_static("rust")]);
1655            assert_eq!(read_log(&log).matches("started").count(), 1);
1656        }
1657
1658        #[tokio::test]
1659        async fn test_restart_is_throttled_until_the_cooldown_elapses() {
1660            let (fx, _log) = protocol_fixture(None).await;
1661            let first = fx
1662                .translator
1663                .restart_servers(RestartTarget::All)
1664                .await
1665                .unwrap();
1666            assert_matches!(only_outcome(&first), RestartOutcome::Restarted { .. });
1667
1668            let second = fx
1669                .translator
1670                .restart_servers(RestartTarget::All)
1671                .await
1672                .unwrap();
1673            let RestartOutcome::Throttled { retry_in_ms } = only_outcome(&second) else {
1674                panic!("expected Throttled, got {second:?}");
1675            };
1676            assert!((1..=5000).contains(retry_in_ms));
1677
1678            fx.clock.advance(RESTART_COOLDOWN + Duration::from_secs(1));
1679            let third = fx
1680                .translator
1681                .restart_servers(RestartTarget::All)
1682                .await
1683                .unwrap();
1684            assert_matches!(only_outcome(&third), RestartOutcome::Restarted { .. });
1685        }
1686
1687        #[tokio::test]
1688        async fn test_concurrent_restarts_of_one_server_coalesce() {
1689            let (fx, log) = protocol_fixture(None).await;
1690
1691            let (a, b) = tokio::join!(
1692                fx.translator.restart_servers(RestartTarget::All),
1693                fx.translator.restart_servers(RestartTarget::All),
1694            );
1695
1696            let outcomes = [a.unwrap(), b.unwrap()];
1697            let coalesced = outcomes
1698                .iter()
1699                .filter(|result| {
1700                    matches!(
1701                        only_outcome(result),
1702                        RestartOutcome::Restarted {
1703                            coalesced: true,
1704                            ..
1705                        }
1706                    )
1707                })
1708                .count();
1709            assert_eq!(coalesced, 1, "{outcomes:?}");
1710            assert_eq!(read_log(&log).matches("started").count(), 2);
1711        }
1712
1713        #[tokio::test]
1714        async fn test_restart_clears_degradation_and_keeps_the_early_diagnostics_push() {
1715            let dir = TempDir::new().unwrap();
1716            let file = dunce::canonicalize(dir.path()).unwrap().join("main.rs");
1717            fs::write(&file, "fn main() {}").unwrap();
1718            let uri = crate::bridge::try_path_to_uri(&file).unwrap();
1719            let log = dir.path().join("server.log");
1720            let script = write_protocol_server_script(dir.path(), &log, Some(uri.as_ref()));
1721            let fx = fixture(dir, &script, true).await;
1722            {
1723                let mut cache = fx.cache.lock().await;
1724                cache.mark_push_degraded(&fx.id);
1725                cache.clear_server_diagnostics(&fx.id);
1726            }
1727
1728            let result = fx
1729                .translator
1730                .restart_servers(RestartTarget::All)
1731                .await
1732                .unwrap();
1733
1734            assert_matches!(
1735                only_outcome(&result),
1736                RestartOutcome::Restarted {
1737                    push_notifications_degraded: false,
1738                    ..
1739                },
1740                "{result:?}"
1741            );
1742            tokio::time::timeout(Duration::from_secs(5), async {
1743                while !fx.cache.lock().await.has_diagnostics(&uri) {
1744                    tokio::time::sleep(Duration::from_millis(20)).await;
1745                }
1746            })
1747            .await
1748            .expect("the diagnostics pushed during the new handshake must reach the cache");
1749        }
1750
1751        #[tokio::test]
1752        async fn test_restart_fails_requests_in_flight_with_server_restarted() {
1753            let (fx, _log) = protocol_fixture(None).await;
1754            let client = lock_std(&fx.translator.servers).client(&fx.id).unwrap();
1755            let in_flight = tokio::spawn(async move {
1756                client
1757                    .request::<_, serde_json::Value>(
1758                        "textDocument/hover",
1759                        serde_json::json!({}),
1760                        Duration::from_secs(30),
1761                    )
1762                    .await
1763            });
1764            tokio::time::sleep(Duration::from_millis(300)).await;
1765
1766            fx.translator
1767                .restart_servers(RestartTarget::All)
1768                .await
1769                .unwrap();
1770
1771            let failed = tokio::time::timeout(Duration::from_secs(5), in_flight)
1772                .await
1773                .expect("the in-flight request must fail promptly")
1774                .unwrap();
1775            assert_matches!(&failed, Err(Error::ServerRestarted { server_id }) if *server_id == fx.id, "{failed:?}");
1776        }
1777
1778        #[tokio::test]
1779        async fn test_restart_during_shutdown_starts_nothing() {
1780            let (fx, log) = protocol_fixture(None).await;
1781            fx.translator.begin_shutdown();
1782
1783            let result = fx
1784                .translator
1785                .restart_servers(RestartTarget::All)
1786                .await
1787                .unwrap();
1788
1789            assert_eq!(
1790                *only_outcome(&result),
1791                RestartOutcome::Failed {
1792                    reason: RestartFailure::ShuttingDown
1793                }
1794            );
1795            assert_eq!(read_log(&log).matches("started").count(), 1);
1796            assert!(fx.registered());
1797        }
1798
1799        #[tokio::test]
1800        async fn test_shutdown_beginning_while_the_old_server_stops_prevents_the_respawn() {
1801            let dir = TempDir::new().unwrap();
1802            let script = write_responder_script(dir.path(), 600);
1803            let fx = fixture(dir, &script, true).await;
1804
1805            let translator = Arc::clone(&fx.translator);
1806            let task =
1807                tokio::spawn(async move { translator.restart_servers(RestartTarget::All).await });
1808            fx.wait_until_deregistered().await;
1809            fx.translator.begin_shutdown();
1810
1811            let result = task.await.unwrap().unwrap();
1812
1813            assert_eq!(
1814                *only_outcome(&result),
1815                RestartOutcome::Failed {
1816                    reason: RestartFailure::ShuttingDown
1817                }
1818            );
1819            assert!(fx.registered() && !fx.expected() && fx.is_dead());
1820        }
1821
1822        #[tokio::test]
1823        async fn test_failed_replacement_leaves_the_server_registered_and_reports_spawn_failed() {
1824            let dir = TempDir::new().unwrap();
1825            let log = dir.path().join("server.log");
1826            let script = write_protocol_server_script(dir.path(), &log, None);
1827            let fx = fixture(dir, &script, true).await;
1828            let mut broken = stub_server_config("rust", &script);
1829            broken.server_config_mut().command =
1830                ServerCommand::from_static("mcpls-test-missing-server").into();
1831            lock_std(&fx.translator.servers)
1832                .server_mut(&fx.id)
1833                .unwrap()
1834                .set_init_config(broken);
1835
1836            let result = fx
1837                .translator
1838                .restart_servers(RestartTarget::All)
1839                .await
1840                .unwrap();
1841
1842            assert_matches!(
1843                only_outcome(&result),
1844                RestartOutcome::Failed {
1845                    reason: RestartFailure::SpawnFailed { .. }
1846                },
1847                "{result:?}"
1848            );
1849            assert!(fx.registered() && !fx.expected() && fx.is_dead());
1850        }
1851
1852        #[tokio::test]
1853        async fn test_take_leaves_the_server_expected_in_the_same_step_and_restore_runs_it_again() {
1854            let config = crate::config::LspServerConfig::rust_analyzer();
1855            let id = config.id();
1856            let translator = Translator::new();
1857            translator.register_server_complete(crate::lsp::fake_lsp_server_with_config(config));
1858
1859            let held = Deregistered::take(&translator, &id).unwrap();
1860            {
1861                let servers = lock_std(&translator.servers);
1862                assert!(
1863                    servers.is_expected(&id),
1864                    "the id must never be in neither state"
1865                );
1866                assert!(servers.server(&id).is_none());
1867                drop(servers);
1868            }
1869            drop(held);
1870
1871            let servers = lock_std(&translator.servers);
1872            assert!(!servers.is_expected(&id));
1873            assert!(servers.server(&id).is_some());
1874            drop(servers);
1875        }
1876
1877        /// Six servers, two overlapping `All` requests: ids queued behind the
1878        /// concurrency limit must snapshot their generation up front, so each
1879        /// server is restarted exactly once and the other request coalesces.
1880        #[tokio::test]
1881        async fn test_overlapping_requests_over_more_than_four_servers_restart_each_once() {
1882            let dir = TempDir::new().unwrap();
1883            let cache = Arc::new(Mutex::new(NotificationCache::new()));
1884            let mut translator = Translator::new().with_notification_cache(Arc::clone(&cache));
1885            translator.set_workspace_roots(
1886                WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap(),
1887            );
1888            let wiring = PumpWiring::new(
1889                PumpShared {
1890                    roles: crate::runtime::pump::DiagnosticsRoles::default(),
1891                    notification_cache: Arc::clone(&cache),
1892                    subs: SubscriptionRegistry::new(),
1893                    workspace_roots: translator.workspace_roots.clone(),
1894                },
1895                CancellationToken::new(),
1896            );
1897            let mut logs = Vec::new();
1898            for index in 0..6 {
1899                let name = format!("s{index}");
1900                let log = dir.path().join(format!("{name}.log"));
1901                let script_dir = dir.path().join(&name);
1902                fs::create_dir(&script_dir).unwrap();
1903                let script = write_protocol_server_script(&script_dir, &log, None);
1904                let crate::lsp::SpawnedServer { server, lanes } =
1905                    crate::lsp::LspServer::spawn(stub_server_config(&name, &script))
1906                        .await
1907                        .unwrap();
1908                let receivers = NotificationReceivers::new(lanes, None);
1909                translator.register_server_complete(server);
1910                let id = ServerId::new(name.as_str()).unwrap();
1911                let pump = wiring.spawn_pump(id.clone(), receivers, DiagnosticsRole::Secondary);
1912                translator.set_notification_task(&id, pump);
1913                logs.push(log);
1914            }
1915            translator.install_wiring(Arc::new(wiring));
1916
1917            let (a, b) = tokio::join!(
1918                translator.restart_servers(RestartTarget::All),
1919                translator.restart_servers(RestartTarget::All),
1920            );
1921            let (a, b) = (a.unwrap(), b.unwrap());
1922
1923            assert_eq!((a.servers.len(), b.servers.len()), (6, 6));
1924            for (first, second) in a.servers.iter().zip(&b.servers) {
1925                let coalesced = |entry: &ServerRestartEntry| match entry.outcome {
1926                    RestartOutcome::Restarted { coalesced, .. } => coalesced,
1927                    ref other => panic!("{}: unexpected outcome {other:?}", entry.server_id),
1928                };
1929                assert_ne!(
1930                    coalesced(first),
1931                    coalesced(second),
1932                    "{}: exactly one request restarts it",
1933                    first.server_id
1934                );
1935            }
1936            for log in logs {
1937                assert_eq!(read_log(&log).matches("started").count(), 2, "{log:?}");
1938            }
1939        }
1940
1941        #[tokio::test]
1942        async fn test_an_automatic_respawn_does_not_count_towards_coalescing() {
1943            let dir = TempDir::new().unwrap();
1944            let script = write_crash_after_init_script(dir.path());
1945            let fx = fixture(dir, &script, true).await;
1946            tokio::time::timeout(Duration::from_secs(3), async {
1947                while !fx.is_dead() {
1948                    tokio::time::sleep(Duration::from_millis(20)).await;
1949                }
1950            })
1951            .await
1952            .expect("the seed server crashes after initialize");
1953            let before = fx.translator.restart_generation(&fx.id);
1954            fx.translator.respawn_if_dead(&fx.id).await.unwrap();
1955            assert_eq!(fx.translator.restart_generation(&fx.id), before);
1956
1957            let result = fx
1958                .translator
1959                .restart_servers(RestartTarget::All)
1960                .await
1961                .unwrap();
1962
1963            assert_matches!(
1964                only_outcome(&result),
1965                RestartOutcome::Restarted {
1966                    coalesced: false,
1967                    ..
1968                },
1969                "{result:?}"
1970            );
1971        }
1972
1973        #[tokio::test]
1974        async fn test_an_unregistered_server_is_never_throttled() {
1975            let id = ServerId::from_static("rust");
1976            let clock = Arc::new(FakeClock::new());
1977            let translator = Translator::new().with_clock(clock.clone());
1978            translator.set_expected_servers(HashSet::from([id.clone()]));
1979            lock_std(&translator.servers)
1980                .get_mut(&id)
1981                .unwrap()
1982                .restart
1983                .last_attempt = Some(clock.now());
1984            let starting = translator
1985                .restart_servers(server_ids(&["rust"]))
1986                .await
1987                .unwrap();
1988            assert_eq!(*only_outcome(&starting), RestartOutcome::Initializing);
1989
1990            translator.clear_expected_servers();
1991            translator.record_startup_failures(&[ServerSpawnFailure {
1992                server_id: id,
1993                language_id: LanguageId::from_static("rust"),
1994                command: ServerCommand::from_static("missing"),
1995                reason: StartupFailure::InitTaskPanicked,
1996            }]);
1997            let failed = translator
1998                .restart_servers(server_ids(&["rust"]))
1999                .await
2000                .unwrap();
2001            assert_matches!(
2002                only_outcome(&failed),
2003                RestartOutcome::NotRunning { .. },
2004                "{failed:?}"
2005            );
2006        }
2007
2008        #[tokio::test]
2009        async fn test_restart_after_an_init_panic_without_wiring_reports_not_running() {
2010            let dir = TempDir::new().unwrap();
2011            let log = dir.path().join("server.log");
2012            let script = write_protocol_server_script(dir.path(), &log, None);
2013            let fx = fixture(dir, &script, false).await;
2014            fx.translator.settle_after_init_panic(&[]).await;
2015
2016            let result = fx
2017                .translator
2018                .restart_servers(RestartTarget::All)
2019                .await
2020                .unwrap();
2021
2022            assert_matches!(
2023                only_outcome(&result),
2024                RestartOutcome::NotRunning { .. },
2025                "{result:?}"
2026            );
2027        }
2028
2029        #[tokio::test]
2030        async fn test_restart_after_an_init_panic_with_wiring_installed_reports_not_running() {
2031            let dir = TempDir::new().unwrap();
2032            let log = dir.path().join("server.log");
2033            let script = write_protocol_server_script(dir.path(), &log, None);
2034            let fx = fixture(dir, &script, true).await;
2035            fx.translator.settle_after_init_panic(&[]).await;
2036
2037            let result = fx
2038                .translator
2039                .restart_servers(RestartTarget::All)
2040                .await
2041                .unwrap();
2042
2043            assert_matches!(
2044                only_outcome(&result),
2045                RestartOutcome::NotRunning { .. },
2046                "{result:?}"
2047            );
2048        }
2049
2050        #[tokio::test]
2051        async fn test_a_failed_restart_invalidates_the_stopped_servers_cached_diagnostics() {
2052            let dir = TempDir::new().unwrap();
2053            let file = dunce::canonicalize(dir.path()).unwrap().join("main.rs");
2054            fs::write(&file, "fn main() {}").unwrap();
2055            let uri = crate::bridge::try_path_to_uri(&file).unwrap();
2056            let log = dir.path().join("server.log");
2057            let script = write_protocol_server_script(dir.path(), &log, Some(uri.as_ref()));
2058            let fx = fixture(dir, &script, true).await;
2059            tokio::time::timeout(Duration::from_secs(5), async {
2060                while !fx.cache.lock().await.has_diagnostics(&uri) {
2061                    tokio::time::sleep(Duration::from_millis(20)).await;
2062                }
2063            })
2064            .await
2065            .expect("the seed server's diagnostics reach the cache");
2066            let mut broken = stub_server_config("rust", &script);
2067            broken.server_config_mut().command =
2068                ServerCommand::from_static("mcpls-test-missing-server").into();
2069            lock_std(&fx.translator.servers)
2070                .server_mut(&fx.id)
2071                .unwrap()
2072                .set_init_config(broken);
2073
2074            let result = fx
2075                .translator
2076                .restart_servers(RestartTarget::All)
2077                .await
2078                .unwrap();
2079
2080            assert_matches!(only_outcome(&result), RestartOutcome::Failed { .. });
2081            let (stale, degraded) = {
2082                let cache = fx.cache.lock().await;
2083                (cache.has_diagnostics(&uri), cache.is_push_degraded(&fx.id))
2084            };
2085            assert!(!stale, "stale diagnostics must go");
2086            assert!(degraded);
2087            assert!(
2088                lock_std(&fx.translator.servers)
2089                    .get(&fx.id)
2090                    .is_none_or(|slot| slot.notification_task.is_none()),
2091                "the dead server's pump handle must be dropped"
2092            );
2093        }
2094
2095        #[tokio::test]
2096        async fn test_cooldown_counts_from_the_completion_of_a_slow_restart() {
2097            let dir = TempDir::new().unwrap();
2098            let script = write_responder_script(dir.path(), 600);
2099            let fx = fixture(dir, &script, true).await;
2100            let translator = Arc::clone(&fx.translator);
2101            let restart =
2102                tokio::spawn(async move { translator.restart_servers(RestartTarget::All).await });
2103            fx.wait_until_deregistered().await;
2104            fx.clock.advance(Duration::from_secs(30));
2105
2106            restart.await.unwrap().unwrap();
2107            let again = fx
2108                .translator
2109                .restart_servers(RestartTarget::All)
2110                .await
2111                .unwrap();
2112
2113            assert_matches!(
2114                only_outcome(&again),
2115                RestartOutcome::Throttled { .. },
2116                "{again:?}"
2117            );
2118        }
2119
2120        #[tokio::test]
2121        async fn test_a_request_queued_behind_a_discarded_restart_is_not_reported_coalesced() {
2122            let dir = TempDir::new().unwrap();
2123            let script = write_responder_script(dir.path(), 600);
2124            let fx = fixture(dir, &script, true).await;
2125            let (first_tx, second_tx) = (Arc::clone(&fx.translator), Arc::clone(&fx.translator));
2126            let first =
2127                tokio::spawn(async move { first_tx.restart_servers(RestartTarget::All).await });
2128            fx.wait_until_deregistered().await;
2129            let second =
2130                tokio::spawn(async move { second_tx.restart_servers(RestartTarget::All).await });
2131            tokio::time::sleep(Duration::from_millis(100)).await;
2132            fx.translator.begin_shutdown();
2133
2134            for result in [
2135                first.await.unwrap().unwrap(),
2136                second.await.unwrap().unwrap(),
2137            ] {
2138                assert_eq!(
2139                    *only_outcome(&result),
2140                    RestartOutcome::Failed {
2141                        reason: RestartFailure::ShuttingDown
2142                    }
2143                );
2144            }
2145        }
2146
2147        /// A server that answered `shutdown` but needs longer than the
2148        /// handshake budget to exit is not killed at the budget.
2149        #[tokio::test]
2150        async fn test_a_healthy_server_slow_to_exit_is_not_killed_at_the_handshake_budget() {
2151            let dir = TempDir::new().unwrap();
2152            let log = dir.path().join("server.log");
2153            let delay = u32::try_from(RESTART_HANDSHAKE_BUDGET.as_secs()).unwrap() + 1;
2154            let script = write_slow_exit_server_script(dir.path(), &log, delay);
2155            let fx = fixture(dir, &script, true).await;
2156
2157            let result = fx
2158                .translator
2159                .restart_servers(RestartTarget::All)
2160                .await
2161                .unwrap();
2162
2163            assert_matches!(
2164                only_outcome(&result),
2165                RestartOutcome::Restarted { .. },
2166                "{result:?}"
2167            );
2168            assert!(
2169                read_log(&log).contains("exiting"),
2170                "the old server must have exited by itself, not been killed"
2171            );
2172        }
2173
2174        #[tokio::test]
2175        async fn test_restart_recovers_a_crashed_server() {
2176            let dir = TempDir::new().unwrap();
2177            let script = write_crash_after_init_script(dir.path());
2178            let fx = fixture(dir, &script, true).await;
2179            tokio::time::timeout(Duration::from_secs(3), async {
2180                while !fx.is_dead() {
2181                    tokio::time::sleep(Duration::from_millis(20)).await;
2182                }
2183            })
2184            .await
2185            .expect("the seed server crashes after initialize");
2186
2187            let result = fx
2188                .translator
2189                .restart_servers(RestartTarget::All)
2190                .await
2191                .unwrap();
2192
2193            assert_matches!(
2194                only_outcome(&result),
2195                RestartOutcome::Restarted { .. },
2196                "{result:?}"
2197            );
2198        }
2199
2200        /// #667: right after a restart of a server that reported readiness
2201        /// before, the result says `loading` and a gated call holds until the
2202        /// replacement's own signal arrives instead of reading an empty index.
2203        #[tokio::test]
2204        async fn test_restart_of_a_signalling_server_gates_until_its_signal() {
2205            let (fx, _log) = protocol_fixture(None).await;
2206            fx.cache.lock().await.observe_indexing_signal(
2207                &fx.id,
2208                "experimental/serverStatus",
2209                Some(&serde_json::json!({"quiescent": true})),
2210            );
2211
2212            let result = fx
2213                .translator
2214                .restart_servers(RestartTarget::All)
2215                .await
2216                .unwrap();
2217            assert_matches!(
2218                only_outcome(&result),
2219                RestartOutcome::Restarted {
2220                    indexing_state: IndexingState::Loading,
2221                    ..
2222                },
2223                "{result:?}"
2224            );
2225
2226            let translator = Arc::clone(&fx.translator);
2227            let id = fx.id.clone();
2228            let gated = tokio::spawn(async move { translator.wait_for_indexing_ready(&id).await });
2229            tokio::time::sleep(Duration::from_millis(150)).await;
2230            assert!(!gated.is_finished(), "the gate must hold before the signal");
2231
2232            fx.cache.lock().await.observe_indexing_signal(
2233                &fx.id,
2234                "experimental/serverStatus",
2235                Some(&serde_json::json!({"quiescent": true})),
2236            );
2237            tokio::time::timeout(Duration::from_secs(5), gated)
2238                .await
2239                .expect("the gate releases once the replacement is ready")
2240                .unwrap()
2241                .unwrap();
2242        }
2243
2244        /// #667: a server that never reported a signal is restarted without a
2245        /// gate (no new fixed delay).
2246        #[tokio::test]
2247        async fn test_restart_of_a_silent_server_reports_unknown() {
2248            let (fx, _log) = protocol_fixture(None).await;
2249
2250            let result = fx
2251                .translator
2252                .restart_servers(RestartTarget::All)
2253                .await
2254                .unwrap();
2255
2256            assert_matches!(
2257                only_outcome(&result),
2258                RestartOutcome::Restarted {
2259                    indexing_state: IndexingState::Unknown,
2260                    ..
2261                },
2262                "{result:?}"
2263            );
2264            fx.translator.wait_for_indexing_ready(&fx.id).await.unwrap();
2265        }
2266    }
2267}
2268
2269#[cfg(test)]
2270mod server_text_tests {
2271    use super::*;
2272
2273    #[test]
2274    fn test_restart_messages_are_redacted() {
2275        let secret = "SuperSecretValue123";
2276        let set = Redactions::new([("API_TOKEN".to_owned(), secret.to_owned())]);
2277        let mut result = RestartServerResult {
2278            servers: vec![
2279                ServerRestartEntry {
2280                    server_id: ServerId::from_static("a"),
2281                    outcome: RestartOutcome::Failed {
2282                        reason: RestartFailure::SpawnFailed {
2283                            message: format!("spawn {secret}"),
2284                        },
2285                    },
2286                },
2287                ServerRestartEntry {
2288                    server_id: ServerId::from_static("b"),
2289                    outcome: RestartOutcome::Failed {
2290                        reason: RestartFailure::InitializeFailed {
2291                            message: format!("init {secret}"),
2292                        },
2293                    },
2294                },
2295                ServerRestartEntry {
2296                    server_id: ServerId::from_static("c"),
2297                    outcome: RestartOutcome::NotRunning {
2298                        message: format!("gone {secret}"),
2299                    },
2300                },
2301            ],
2302        };
2303
2304        result.redact_server_text(&set);
2305
2306        let json = serde_json::to_string(&result).unwrap();
2307        assert!(!json.contains(secret), "{json}");
2308        assert_eq!(json.matches("[redacted:API_TOKEN]").count(), 3, "{json}");
2309    }
2310}