Дизайн горячей перезагрузки среды выполнения LSP
Контекст
Этот дизайн следует той же архитектуре слоев, что и
mcp-runtime-reinitialization.md: CLI решает, когда запускать перезагрузку, а
Core определяет, как обновлять состояние среды выполнения. Он также переиспользует
принципы работы наблюдателя (watcher) из settings-change-detection.md:
отсутствие побочных эффектов файловой системы при запуске, изменения с debounce,
семантические диффы, сериализованные слушатели и сбои слушателей, которые не
влияют на основную сессию.
Ключевое отличие LSP от MCP заключается в том, что конфигурация серверов LSP не
хранится в settings.json. Сегодня нативный LSP-сервис использует
LspConfigLoader для чтения .lsp.json рабочего пространства и объявлений
lspServers включенных расширений, записывает результат в LspServerManager
для одной сессии через NativeLspService.discoverAndPrepare() и, наконец,
запускает все настроенные серверы с помощью start(). Поэтому одного
SettingsWatcher недостаточно для обнаружения изменений в .lsp.json рабочего
пространства.
Анализ текущего кода
- Запуск LSP управляется только флагом
--experimental-lspвpackages/cli/src/config/config.ts. В настоящее время нет флага--allowed-lsp-server-namesили эквивалентного параметра allow-list для LSP CLI; существующий флаг--allowed-mcp-server-namesработает только для MCP. NativeLspServiceсоздается один раз при загрузке конфигурации CLI. Путь запуска вызываетdiscoverAndPrepare(), затемstart(), затем оборачивает сервис вNativeLspClientи прикрепляет его кConfig.Config.setLspClient()иConfig.setLspInitializationError()в настоящее время вызывают исключения после инициализации, поэтому горячая перезагрузка среды выполнения не должна заменять объект клиента. Она должна сохранять существующийNativeLspClientи только инкрементально согласовывать сервис за ним.LspConfigLoaderчитает только.lsp.jsonрабочего пространства иlspServersактивных расширений..lsp.jsonрабочего пространства переопределяет конфигурацию расширений по имени сервера.LspServerManager.setServerConfigs()в настоящее время очищает все хэндлы; он пока не поддерживает инкрементальное согласование.- В текущем репозитории нет общего пула для LSP. Каждая сессия владеет своим
собственным
NativeLspServiceи подключениями к подпроцессам/сокетам. Дизайн должен оставить границу для будущего общего пула, но v1 реализует только режим одной сессии.
Цели
Сделать так, чтобы изменения конфигурации серверов LSP вступали в силу без перезапуска текущей сессии Qwen Code:
- запускать сервер при его добавлении;
- останавливать сервер при его удалении и удалять его из статуса и маршрутизации инструментов;
- перезапускать только измененный сервер при изменении его конфигурации;
- сохранять подключение неизменных серверов и сохранять их состояние прогрева;
- никогда не запускать ненадежные или неразрешенные серверы;
- позволить инструментам LSP и статусу
/lspнаблюдать новое состояние среды выполнения через существующий объект клиента.
Не входит в задачи
- Не добавлять общий пул процессов LSP в этом изменении.
- Не поддерживать переключение
--experimental-lspво время выполнения. Если LSP не был включен при запуске, нет сервиса для перезагрузки. - Не отслеживать полностью изменения установки/удаления расширений, влияющие на
lspServers; ручная команда/reloadпокроет изменения конфигурации расширений.
Дизайн
1. Идентификация каждого LSP-сервера с помощью стабильного хэша
Добавьте небольшой хелпер рядом с кодом конфигурации LSP:
export function lspServerConfigHash(config: LspServerConfig): string;Хэш должен быть стабильным и основываться на нормализованной конфигурации среды
выполнения, созданной LspConfigLoader:
namelanguagestransportcommandargsenvinitializationOptionssettingsextensionToLanguageworkspaceFolderrootUristartupTimeoutshutdownTimeoutrestartOnCrashmaxRestartstrustRequiredsocket
Ключи объектов должны быть отсортированы, чтобы порядок свойств в JSON не вызывал ненужных перезапусков. Порядок массивов остается значимым, так как порядок аргументов команды и приоритет языков могут иметь значение. Не включайте поля среды выполнения, такие как id процесса, статус, количество перезапусков, диагностика или состояние прогрева.
Для совместимости с будущим общим пулом определите идентификатор пула как:
lsp:<workspaceRoot>:<serverName>:<configHash>Менеджеру одной сессии в v1 нужно только поддерживать serverName -> configHash,
но тот же хэш позже может быть напрямую переиспользован в ключе пула.
2. Добавление инкрементального согласования в LspServerManager
Горячая перезагрузка не должна переиспользовать setServerConfigs(), который
очищает каждый хэндл. Добавьте:
async reconcileServerConfigs(
configs: LspServerConfig[],
): Promise<LspReconcileResult>Алгоритм:
- Постройте желаемые мапы:
name -> configиname -> hash. - Для существующих хэндлов, чей сервер больше не существует, вызовите
существующий
stopServer(), затем удалите хэндл. - Для существующих хэндлов, чей хэш изменился, вызовите
stopServer(), замените хэндл на{ config, status: 'NOT_STARTED' }, затем запустите его. - Для новых серверов создайте
{ config, status: 'NOT_STARTED' }и запустите их. - Для серверов, чей хэш не изменился, ничего не делайте и сохраните существующий хэндл.
Добавьте приватное поле:
private serverConfigHashes = new Map<string, string>();Очищайте его в stopAll() и clearServerHandles().
Возвращайте:
interface LspReconcileResult {
added: string[];
removed: string[];
restarted: string[];
unchanged: string[];
failed: string[];
}skipped не является частью результата LspServerManager. Менеджер обрабатывает
только конфигурации, прошедшие проверку допуска; серверы, отклоненные проверкой
допуска, агрегируются в результат на уровне сервиса с помощью
NativeLspService.reinitialize().
Конкурентность:
- Добавьте очередь согласования в
LspServerManagerилиNativeLspService, чтобы согласования выполнялись последовательно. Остановка и запуск одного и того же процесса не должны приводить к состоянию гонки. - Если новая конфигурация поступает, пока сервер все еще запускается, дождитесь
handle.startingPromiseперед его остановкой. Переиспользуйте существующую блокировку запуска вместо добавления дополнительной блокировки для каждого сервера. - Сам
stopServer()должен ожидатьhandle.startingPromiseпосле установкиstopRequested, чтобы путиstopAll(), удаления и перезапуска охватывали перезапуски после сбоев, которые все еще назначают свое подключение/процесс.
Поведение при сбоях:
- Если только что добавленный или измененный сервер не может запуститься,
сохраните хэндл и пометьте его как
FAILED, чтобы/lspмог объяснить причину сбоя. - Не считайте неудачный запуск как
addedилиrestarted; сообщите о нем вfailed. - Не кэшируйте хэш конфигурации для неудачного запуска. Последующее сохранение с
той же конфигурацией должно повторить попытку, а не быть классифицировано как
unchanged. - Если запуск не удался после создания подключения или процесса, освободите это
подключение/процесс перед возвратом. Неудачная инициализация не должна оставлять
процесс языкового сервера или сокет-подключение активными за хэндлом
FAILED. - Если запуск не удался до создания подключения, включая отклонение из-за недоверия, небезопасный путь команды или отсутствующую команду, очистите кэшированный хэш конфигурации. Последующее согласование с той же конфигурацией должно повторить попытку, а не считать неудачный хэндл неизменным.
- Если удаленный сервер логирует ошибку во время завершения работы, все равно удалите его из мапы хэндлов.
- Сбой запуска одного сервера не должен блокировать согласование для других серверов.
Очистка ресурсов:
stopServer()должен освобождать обе стороны принадлежащего сервера: корректно завершать работу и закрывать LSP-подключение, затем убивать созданный процесс, если он все еще жив. Это важно для транспортовtcp/socket, которые были запущены с помощьюcommand; одного закрытия сокета недостаточно.process.kill()должен быть изолирован с собственной обработкой ошибок. Процесс, который завершается во время очистки, не должен прерывать остальную часть согласования.- Корректное завершение работы всегда должно иметь ограниченное время ожидания.
Если конфигурация сервера не указывает
shutdownTimeout, используйте таймаут завершения работы по умолчанию вместо бесконечного ожиданияconnection.shutdown(). - Таймеры таймаута завершения работы должны очищаться при успешном завершении или сбое, чтобы большой таймаут не удерживал хэндл дольше необходимого.
- За базовым промисом
shutdown()нужно следить, даже если таймаут выигрывает гонку, чтобы позднее отклонение на стороне сервера не проявилось как необработанное отклонение. stopAll()должен участвовать в той же очереди согласования, что и горячая перезагрузка. Недостаточно просто дождаться текущей очереди, а затем перебрать хэндлы, потому что в противном случае новое согласование может попасть в очередь между ожиданием и очисткой хэндлов.NativeLspService.stop()также должен отменять любую выполняющуюся или поставленную в очередь операциюreinitialize()перед остановкой серверов. Реализация использует кооперативную отмену с помощьюAbortController: stop помечает сервис как останавливающийся, прерывает активную перезагрузку, и каждая перезагрузка в очереди проверяет сигнал перед загрузкой конфигурации, согласованием, очисткой отслеживания документов, повторным открытием документов или ожиданием задержек повторного открытия. Это предотвращает бесконечную блокировку завершения работы из-за медленной перезагрузки, а также предотвращает запуск отмененной перезагрузкой новых процессов LSP послеstopAll().- Перезапуски после сбоев также должны сериализоваться через очередь согласования или очищать хэш при окончательном сбое. Они не должны запускать процесс замены параллельно с согласованием изменения конфигурации.
- Сброс перезапуска после сбоя должен изолировать ошибки
connection.end()иprocess.kill(). Сброс выполняется, когда старое подключение/процесс уже может быть сломано, и сбои очистки не должны препятствовать продолжению перезапуска в очереди. NativeLspService.stop()должен очищатьopenedDocumentsиlastConnectionsпослеserverManager.stopAll(), чтобы остановленный сервис не сохранял старые наборы документов или объекты подключений.
3. Добавление NativeLspService.reinitialize()
Добавьте:
async reinitialize(): Promise<LspServiceReinitializeResult>Алгоритм:
- Если
requireTrustedWorkspaceравно true и!config.isTrustedFolder(), вызовитеserverManager.stopAll()и вернитесь. Это предотвратит продолжение работы старых процессов LSP после того, как рабочее пространство станет ненадежным. - Используйте существующий
LspConfigLoaderдля загрузки.lsp.jsonрабочего пространства и конфигураций расширений. - Объедините конфигурации, используя текущий приоритет.
- Примените фильтр допуска LSP перед согласованием.
- Вызовите
serverManager.reconcileServerConfigs(serverConfigs). - Очищайте
openedDocumentsиlastConnectionsтолько для удаленных и успешно перезапущенных серверов; сохраняйте состояние документов для неизменных и упавших серверов. Упавшие серверы сохраняют свое отслеживание документов, чтобы последующий успешный перезапуск мог повторно открыть те же документы. - Для успешно перезапущенных серверов повторно отправьте
textDocument/didOpenдля документов, которые были открыты до перезапуска. Это даст серверу-заменителю тот же контекст документов без ожидания следующего запроса наведения, автодополнения или диагностики для ленивого повторного открытия каждого файла. После повторной отправки одного или нескольких документов для сервера, дождитесь той же задержки открытия документа, которая используется ленивымensureDocumentOpen(), прежде чем сообщать о завершении перезагрузки.
Снимок открытых документов должен быть сделан после возврата из
reconcileServerConfigs() и должен быть ограничен reconcile.restarted.
Документы, открытые во время ожидания согласования, затем включаются в снимок для
повторной отправки до того, как отслеживание будет очищено для перезапущенных
серверов.
Начальное обнаружение должно использовать тот же фильтр допуска перед вызовом
setServerConfigs(). Это обеспечивает согласованность статуса запуска и горячей
перезагрузки для фильтрации trustRequired для каждого сервера в ненадежных
рабочих пространствах.
Ошибки парсинга .lsp.json требуют специальной обработки: не считайте ошибку
парсинга пустой конфигурацией. Наблюдатель должен сообщить о событии невалидной
конфигурации, чтобы CLI мог показать видимую пользователю ошибку, но он не должен
вызывать reinitialize() для этого события. reinitialize() должен сохранить
старое состояние среды выполнения, пропустить согласование и записать ошибку в
статус/логи. Только удаление файла или парсинг валидной пустой JSON-конфигурации
означает, что желаемая конфигурация пуста.
Холодный запуск и горячая перезагрузка намеренно используют разную строгость парсинга пользовательской конфигурации:
loadUserConfigs()остается мягким для совместимости при запуске. Он пропускает невалидные записи серверов и возвращает валидные записи, которые могут быть построены.loadUserConfigsStrict()используется горячей перезагрузкой. Если существующий.lsp.jsonсинтаксически валиден, но содержит невалидную форму верхнего уровня или запись сервера, которая не может быть построена, он возвращает ошибку, иreinitialize()не выполняет согласование. Это сохраняет текущее работающее состояние LSP при невалидных правках. Строгий путь не должен вводить валидацию на уровне полей, которую холодный запуск также не обеспечивает, потому что это сделает конфигурацию валидной при запуске, но невалидной при следующем сохранении. Ужесточение валидации известных полей должно обрабатываться как отдельное решение о совместимости как для запуска, так и для горячей перезагрузки. Если файл отсутствует или удаляется во время строгой загрузки, считайте этотENOENTвалидной пустой пользовательской конфигурацией, потому что удаление.lsp.json— это явный способ удалить все пользовательские LSP-серверы рабочего пространства.NativeLspService.reinitialize()возвращает результат на уровне сервиса:
interface LspServiceReinitializeResult {
reconcile: LspReconcileResult;
skipped: Array<{
name: string;
reason: 'server_trust_required';
}>;
}Добавьте опциональный метод reinitialize() в NativeLspClient и делегируйте его сервису. Чтобы избежать непрозрачных приведений типов в Config.reinitializeLsp(), расширьте интерфейс LspClient напрямую:
reinitialize?: () => Promise<LspServiceReinitializeResult>;Добавьте в Config:
async reinitializeLsp(): Promise<LspServiceReinitializeResult | undefined>Если LSP отключен или клиент отсутствует, этот метод ничего не делает (no-op). Этот метод не должен заменять клиент после Config.initialize().
Поскольку setLspInitializationError() в настоящее время отклоняет вызовы после инициализации, добавьте безопасный для runtime приватный сеттер состояния:
private setRuntimeLspInitializationError(error: Error | string | undefined): voidreinitializeLsp() использует его для отображения сбоев перезагрузки через getLspStatusSnapshot(), не ослабляя публичный API мутации клиента после инициализации. Возвращенный результат reconcile с серверами в статусе failed является частичным сбоем, а не полным успехом. reinitializeLsp() должен устанавливать initializationError в этом случае и очищать ошибку только если при перезагрузке нет серверов со статусом failed.
4. Границы допуска и разрешений
Текущие проверки безопасности LSP включают:
--experimental-lsp— единственный переключатель для включения;- доверие к рабочей области (workspace trust) проверяется перед обнаружением/запуском;
- для каждого сервера
trustRequiredпо умолчанию равно true; - существование команды и безопасность пути к команде проверяются перед spawn;
workspaceFolderограничен корнем рабочей области.
Hot reload должен сохранять эти проверки и выполнять их до запуска нового сервера или перезапуска измененного сервера. Главное правило: не запускать (spawn) сервер до того, как будет принято решение о его допустимости.
Файл .lsp.json в рабочей области — это входные данные, контролируемые рабочей областью. Поэтому пользовательские конфигурации всегда должны рассматриваться как trustRequired: true, даже если в файле явно указано "trustRequired": false. Конфигурации LSP, предоставляемые расширениями, по-прежнему могут использовать свое объявленное значение trustRequired. Это предотвращает снижение границы доверия ненадежной рабочей областью.
Переменные окружения из .lsp.json также контролируются рабочей областью. Runtime spawn может объединять разрешенные переопределения переменных окружения, но переменные для внедрения кода, такие как NODE_OPTIONS, LD_PRELOAD, LD_LIBRARY_PATH, DYLD_INSERT_LIBRARIES и DYLD_LIBRARY_PATH, не должны переопределяться конфигурацией LSP. PATH разрешен для фактического процесса сервера, чтобы сохранить стандартные настройки тулчейна. Проверка существования команды может сохранять обычные значения переменных окружения из конфигурации, которые могут понадобиться для проверки, но она не должна использовать PATH из конфигурации при разрешении простых имен команд. Это предотвращает перенаправление вредоносным PATH рабочей области проверки, такой как clangd --version, на непреднамеренный исполняемый файл до фактического пути запуска. Фильтрация чувствительных ключей переменных окружения, включая фильтр PATH только для проверок, должна быть нечувствительна к регистру, чтобы имена переменных окружения в стиле Windows, нечувствительные к регистру, такие как Path, node_options или Ld_PreLoad, не могли обойти denylist.
Границы allow-list:
- Текущий репозиторий не поддерживает CLI allow-list для имен LSP-серверов. Я подтвердил, что LSP имеет только
--experimental-lsp; параметры allow-list существуют только для MCP. - Если эта функция добавит
--allowed-lsp-server-names, он должен вести себя как allow-list запуска MCP и действовать как верхняя граница на протяжении всего времени жизни сессии. Runtime конфигурация может сужать этот набор, но не должна расширять его сверх верхней границы, заданной при запуске через CLI. - Сохраните верхнюю границу запуска в
ConfigParameters.lsp:
cliAllowedLspServerNames?: string[];Предоставьте геттер для него. Не читайте верхнюю границу из изменяемых настроек.
Проверку допуска следует вынести в чистую функцию:
filterLspServerConfigs(configs, {
workspaceTrusted,
requireTrustedWorkspace,
cliAllowedServerNames,
}): {
admitted: LspServerConfig[];
skipped: Array<{
name: string;
reason: 'server_trust_required';
}>;
}Хотя сегодня в LSP нет хранилища одобрений или CLI allow-list, этот хелпер делает границу безопасности явной и оставляет место для будущих проверок одобрения на основе хешей. Если в будущем будет добавлен флаг --allowed-lsp-server-names, он должен будет добавлять причину пропуска not_allowed в то время, вместо того чтобы нести неподключенный путь allow-list в v1.
Семантика доверия должна соответствовать текущему пути запуска:
- Если
requireTrustedWorkspaceравно true и рабочая область не является доверенной,NativeLspService.reinitialize()останавливает все серверы на уровне сервиса и возвращает управление. Он не входит в фильтр допуска и не сохраняет старые серверы. - Если
requireTrustedWorkspaceравно false, сервис не прерывает выполнение глобально, но фильтр допуска все равно пропускает отдельные серверы сtrustRequired: true. - Если рабочая область доверенная,
trustRequiredне блокирует сервер.
5. Триггеры
Необходимы два пути срабатывания.
Автоматический триггер для .lsp.json рабочей области
Добавьте узконаправленный LspConfigWatcher в CLI, по образу SettingsWatcher, но с меньшей зоной ответственности:
- отслеживать только корень рабочей области и строго сопоставлять базовое имя
.lsp.json; - не создавать никаких каталогов или файлов;
- использовать debounce 300 мс;
- сравнивать
.lsp.jsonдо и после с помощью парсинга и каноникализации, чтобы изменения только форматирования не вызывали перезагрузку; - считать
ENOENTудалением; - различать ошибки парсинга JSON и другие ошибки чтения. Оба случая должны уведомлять слушателя о видимом пользователю событии невалидной конфигурации и сохранять старое состояние runtime, но сообщение об ошибке должно отражать, был ли файл невалидным JSON или нечитаемым;
- удаление файла — это отдельное событие, и оно должно уведомлять слушателя перезагрузки, создавая пустую конфигурацию рабочей области;
- выполнять коллбэки последовательно;
- использовать таймаут слушателя и изоляцию сбоев, аналогичные
SettingsWatcher; - обновлять сохраненный семантический снимок только после успешного уведомления слушателя. Если слушатель выдает ошибку или срабатывает таймаут, сохраняйте предыдущий снимок, чтобы повторное сохранение того же содержимого повторило попытку перезагрузки.
Регистрируйте watcher только если config.isLspEnabled() и клиент поддерживает reinitialize(). При изменении вызывайте:
await config.reinitializeLsp();Затем отправьте явное runtime-событие, такое как AppEvent.LspStatusChanged. UI-поверхности, такие как /lsp, /about или /status, могут подписаться на это событие для обновления. Если reconcile возвращает частичные сбои, отправьте событие изменения статуса перед тем, как пробросить ошибку обратно в watcher; это позволит UI наблюдать за успешно перезапущенными серверами, пока watcher все еще сохраняет старый семантический снимок для повторной попытки. При сбое также покажите видимую пользователю ошибку через AppEvent.LogError; включите базовое сообщение об ошибке парсера/запуска, если оно доступно, и не ограничивайтесь записью в debug-лог.
Ручной триггер /reload
Когда появится будущая команда /reload, она должна вызывать оба метода:
await config.reinitializeMcpServers(...);
await config.reinitializeLsp();Ручная перезагрузка также предоставляет путь отката для изменений lspServers в расширениях, поскольку эти изменения могут не соответствовать событию изменения файла .lsp.json в рабочей области.
Одиночная сессия и общий пул
Текущее состояние: существует только режим одиночной сессии. В репозитории нет аналога транспортного пула MCP для LSP.
v1: реализуйте инкрементальный reconcile внутри LspServerManager. Каждая сессия владеет своим собственным процессом и сокетом.
Будущий общий пул: оставьте NativeLspService в качестве потребителя и замените внутреннюю логику LspServerManager на acquire/release для:
lsp:<workspaceRoot>:<name>:<hash>записей пула. Фильтрация допуска по-прежнему должна происходить до acquire, соответствуя исправлению общего пула MCP, чтобы запрещенные или ненадежные серверы не могли быть запущены через путь пула.
План модульных тестов
Приоритет — модульные тесты. Интеграционные тесты с реальными LSP-серверами работают медленно и зависят от окружения, поэтому они не требуются.
Основные тесты
packages/core/src/lsp/configHash.test.ts
- хеш игнорирует порядок ключей объекта;
- изменения команды, порядка аргументов, переменных окружения, настроек, папки рабочей области, сокета и требований доверия изменяют хеш;
- хеш исключает поля status/process/runtime.
packages/core/src/lsp/LspServerManager.test.ts
- добавление сервера запускает его ровно один раз;
- удаление сервера останавливает его и удаляет из handles;
- изменения хеша останавливают старый handle и запускают новый;
- неизменный хеш не вызывает остановку/запуск и сохраняет идентичность handle;
- сбой запуска после создания соединения освобождает соединение и принадлежащий процесс;
- остановка
tcp/socketсервера, запущенного черезcommand, закрывает соединение и убивает принадлежащий процесс; - таймеры таймаута shutdown очищаются, если shutdown завершается первым;
- отсутствие
shutdownTimeoutвсе равно использует таймаут shutdown по умолчанию и не может блокировать reconcile бесконечно; stopAll()ожидает незавершенные запуски перед освобождением ресурсов;stopAll()сериализуется через очередь reconcile и не может выполняться параллельно с последующим reconcile;- ошибки
process.kill()логируются и не прерывают очистку; - сбой запуска одного сервера не влияет на reconcile другого сервера;
- параллельные reconcile выполняются последовательно;
stopAll()иclearServerHandles()очищают хеш-карту;- неудачные запуски сообщаются в
failed, не сообщаются как добавленные/перезапущенные и не кэшируют свой хеш конфигурации; - начальные сбои запуска очищают кэшированный хеш, чтобы последующий reconcile с той же конфигурацией повторил попытку;
- перезапуски после краха сериализуются вместе с reconcile и очищают кэшированные хеши при постоянном сбое;
- сброс перезапуска после краха игнорирует ошибки очистки соединения/процесса и продолжает перезапуск из очереди;
- проверка существования команды сохраняет обычные значения переменных окружения из конфигурации, но не использует
PATHиз конфигурации, а переопределения переменных окружения для внедрения кода фильтруются перед spawn; - возвращаемое значение reconcile содержит added/removed/restarted/unchanged/failed, но не admission skipped.
Мокайте createLspConnection, инициализацию и shutdown в тестах. Не запускайте реальные языковые серверы.
packages/core/src/lsp/NativeLspService.test.ts
reinitialize()загружает конфигурацию рабочей области и расширений и передает объединенную конфигурацию в reconcile менеджера;- сбой парсинга
.lsp.jsonсохраняет старое состояние runtime и не вызывает reconcile менеджера; - строгий hot reload отклоняет невалидные структуры верхнего уровня и записи серверов, которые не могут быть построены без reconcile, в то время как холодный запуск продолжает загружать валидные записи из того же файла;
- удаление
.lsp.jsonрассматривает конфигурацию рабочей области как пустую и запускает reconcile; - строгая загрузка рассматривает
ENOENTкак пустую пользовательскую конфигурацию, включая гонку при удалении, когда файл исчезает между уведомлением watcher и перезагрузкой; - ненадежная рабочая область останавливает все серверы и не выполняет reconcile/запуск;
- начальное обнаружение применяет тот же фильтр допуска
trustRequiredдля каждого сервера, что и hot reload; .lsp.jsonрабочей области не может отказаться отtrustRequired;- если реализован CLI allow-list, верхняя граница фильтрует допущенные конфигурации;
- возвращаемое значение на уровне сервиса агрегирует причины пропуска при допуске;
- перезапущенные/удаленные серверы очищают только собственное отслеживание документов.
- упавшие серверы не очищают отслеживание документов и могут воспроизвести эти документы после последующего успешного перезапуска.
- перезапущенные серверы воспроизводят
textDocument/didOpenдля ранее открытых документов после того, как сервер на замену готов, затем ожидают задержки обработки открытия документа. - документы, открытые во время ожидания reconcile, включаются в снимок воспроизведения для перезапущенных серверов.
stop()отменяет незавершенные задержки воспроизведения и поставленные в очередь вызовыreinitialize()до того, как они смогут запустить новые серверы.stop()очищает кэши отслеживания документов после остановки всех серверов.
packages/core/src/config/config.test.ts
reinitializeLsp()является no-op, когда отключен или не существует клиент;- когда включен и клиент поддерживает
reinitialize, он делегирует вызов; - когда reinitialize выдает ошибку, снимок статуса отображает ошибку инициализации/перезагрузки.
- когда reinitialize возвращает частичные сбои, снимок статуса отображает ошибку инициализации/перезагрузки до тех пор, пока последующая полностью успешная перезагрузка не очистит ее.
Тесты CLI
packages/cli/src/config/lspConfigWatcher.test.ts
- не создает
.lsp.json; - отслеживает создание/изменение/удаление;
- игнорирует нерелевантные файлы;
- игнорирует изменения только форматирования после канонического парсинга;
- при ошибке парсинга отправляет уведомление о невалидной конфигурации для отображения пользователю и не запускает переинициализацию LSP;
- при ошибке чтения (не ENOENT) отправляет отображаемое пользователю сообщение об ошибке чтения и не запускает переинициализацию LSP;
- удаление
.lsp.jsonзапускает слушатель перезагрузки; - для дублирующихся событий файла применяется debounce;
- медленные слушатели выполняются последовательно;
- сбой слушателя не обновляет сохраненный снимок, и тот же контент может быть повторно обработан при последующем уведомлении.
packages/cli/src/ui/AppContainer.test.tsx или соответствующий тест событий
AppEvent.LspStatusChangedзапускает обновление UI;- при сбое перезагрузки через
AppEvent.LogErrorотправляется отображаемая пользователю ошибка. - при частичном сбое согласования
AppEvent.LspStatusChangedвсё равно отправляется до того, как слушатель отклонит выполнение, чтобы состояние UI могло отразить успешные части перезагрузки.
packages/cli/src/config/config.test.ts
- сохранить существующую проверку того, что
--experimental-lspсоздает и запускает нативный LSP; - если добавлен
--allowed-lsp-server-names, парсер поддерживает значения, разделенные запятыми, и повторяющиеся флаги, и сохраняет их как верхнюю границу при запуске.
packages/cli/src/ui/commands/lspCommand.test.ts
- если
LspStatusSnapshotпредоставляет причины пропуска, вывод статуса может показывать пропущенные/запрещенные серверы.
Цели по покрытию: новые чистые функции должны быть покрыты почти на 100%; покрытие веток watcher
должно быть сопоставимо с SettingsWatcher; согласование менеджера должно покрывать
добавление/удаление/изменение/отсутствие изменений/сбой/параллелизм.
Строгое ревью
Заключение
-
В v1 не следует использовать stop-all/start-all. Эта реализация самая простая, но каждое сохранение приводило бы к перезапуску неизменных языковых серверов и потере прогретого состояния. Текущий менеджер уже имеет методы жизненного цикла для каждого сервера, а инкрементальное согласование — это посильный объем дополнительного кода.
-
Не следует отслеживать изменения
.lsp.jsonвSettingsWatcher.SettingsWatcherотвечает за перезагрузку в области настроек. Если заставить его отслеживать произвольные файлы рабочего пространства, это размоет контракт и усложнит анализ поведения MCP/настроек. Отдельный, узконаправленный watcher для.lsp.jsonбудет понятнее. -
Не заменять
NativeLspClientпосле инициализации.Config.setLspClient()явно запрещает мутацию после инициализации. Обновление сервиса за адаптером позволяет избежать расширения API жизненного цикла. -
Проверка допуска должна выполняться до запуска процесса или захвата пула. Это тот же риск, на который указывалось в дизайне общего пула MCP. Хотя у LSP сегодня нет пула, результаты перезагрузки на уровне сервиса должны возвращать причины пропуска при предстартовой фильтрации, чтобы будущий путь с пулом случайно не запустил отклоненный сервер.
-
Новый allow-list LSP в CLI необязателен, но если он добавлен, то должен быть верхней границей. В текущем коде нет allow-list для LSP. Дизайн не должен позволять настройкам расширять ограничения командной строки во время выполнения, иначе это будет слабее, чем семантика безопасности hot-reload в MCP.
Оставшиеся риски
lspServersрасширения могут измениться без изменения.lsp.json. Автоматический watcher не покрывает все изменения файловой системы расширений; ручная команда/reloadпокрывает этот путь.- Некоторые языковые серверы плохо переносят быстрые перезапуски. Последовательное согласование и debounce снижают риск, но тесты должны покрывать быстрые последовательные изменения.
- TCP/сокет-серверы могут быть внешними управляемыми демонами. Согласование должно закрывать
соединение, но оно должно брать на себя ответственность за процесс только в том случае, если этот
процесс запустил сервер через
command.