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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ MacServiceContainer
查询全部为空。不要用睡眠或反复重新注入能力掩盖这个顺序;搜索排除回归与插件页面
生命周期测试都使用同一个明确的完成边界。

### 语言服务恢复必须等待工作区清理

macOS 打开项目时会异步清理上一轮模块图;清理结束前,语言能力仍可能留在缓存中。
#1524 曾让 Java 预热拿到旧能力,接着旧清理才执行到 Java 插件,取消刚启动的会话。
`LanguageIntelligenceModuleCoordinator` 与执行、调试协调器共用
`awaitModuleRuntimeShutdown()`:先等待整轮清理,再检查调用方取消状态,最后读取能力缓存或激活模块。
后台维护入口也必须经过这个顺序,不能提前捕获 session 然后在等待后返回旧对象。
最终关闭项目会话时,`shutdownProjectSession()` 在首次挂起前标记会话已关闭,并使工作区身份失效;Settings 即使继续持有工作区 URL 和文档,延后启动也不得恢复。语言协调器等待清理后再次检查关闭状态并以取消结束,覆盖能力激活和测试扩展入口。不能用窗口是否前台作为关闭标记,因为后台项目仍可合法运行语言服务。
同步文档入口在清理期间延后启动,恢复时重新检查工作区身份和文档是否仍打开;等待不会阻塞界面线程。

Java 的准备状态是应用操作的投影。当前 operation ID 对应的 `.stopped` 到达后,
取消该准备 owner 并回到 idle;旧 operation 的迟到事件不得清除较新准备状态。
不能通过删除导航的就绪检查、增加固定延时或无限重试来掩盖清理顺序错误。
`LanguageIntelligenceModuleCoordinatorTests` 用有截止的异步闸门暂停清理,覆盖普通激活、
后台维护、缓存复用和等待期间取消;`JavaLanguageServerRuntimeTests` 覆盖停止后的状态收尾及迟到事件。

### 数据库视图在模块释放后的刷新

退出应用或休眠数据库模块时,SwiftUI(macOS 的声明式界面框架)可能先刷新
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ JDK 也一样:`ProjectRuntimeService` 不再问“app 内置的 JDK 在哪”

新增一个集成时,只需要新增一个实现协议的类型并在注册处加一行;Rust Core 的契约校验仍然决定哪些 ID 可以被声明。

插件身份与宿主集成必须同时传入 Core(#1523):`StdioLanguageServerSession` 的插件启动入口携带 `hostIntegrationLaunch`、平台缓存目录、工作区指纹、Maven/Gradle 上下文,以及插件配置和 host services。不能因为存在 `pluginID` 就退回只传可执行文件和静态参数的入口,否则 JDT 会把 Equinox jar 当包装脚本,缺少 `-configuration`,随后退出并在发行目录写日志。继续复用 Core 的 `prepare_jdt_configuration_area`,从包内只读配置生成宿主缓存中的可写副本;没有新增资源目录或缓存所有者,也不改变应用签名和增量更新的发行基线。

同一工作区的服务连续三次启动失败后,自动同步直接返回已有失败,直到显式停止/重启或切换工作区;进入 ready 后清零。这保留短暂故障后的重试,但阻止每次编辑器同步重新创建 JVM。这里选择有限尝试而非增加定时器和无限退避;代价是持续失败需要用户修复环境后重新启动服务。

### 4. 运行、测试、调试由插件声明后交还宿主

插件一旦声明某个语言,宿主就认为这个语言的进程类能力归插件所有:能力目录(`PluginLanguageProviderCatalogSource`)去掉运行、测试、调试标记,语言包注册表(`LanguagePackRegistry`)不再创建内置的测试提供方,运行和测试服务要求必须有插件的执行模块。这条规则是为 PHP、Go 这类整套由插件提供的语言定的,目的是插件缺失或停用时宿主不偷偷退回内置工具。
Expand Down Expand Up @@ -182,6 +186,8 @@ dev.lithe.plugin.java-support/
- `./scripts/verify-macos-package.sh`:打出的应用里 Java Support 的文件齐全,且不存在 `Contents/Resources/LanguageServers`。
- `./scripts/verify-runtime-bundle-immutability.sh`:启动与插件工作流不改写安装目录。

生产启动适配的回归使用 `RustLanguageServerLaunchTests`,直接从 `RustCoreBridge.startPluginLanguageServer` 经 C ABI 启动本地标准输入输出协议夹具;不经过协议默认回退。它用函数所属插件校验、credentials 请求与响应、通用插件配置查询、Java 启动参数、缓存配置副本和工作区指纹日志验证实际接线。夹具使用 macOS 开发工具已有的 Python,不依赖真实 JVM 或联网;Core 会话负责子进程,测试始终停止并销毁会话,再移除临时资源。运行命令:`./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter RustLanguageServerLaunchTests`。

## 适用范围

- `shared/contracts/plugins/v1/`
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,9 @@ final class JavaLanguageServerPreparationCoordinator {
? .timedOut(message: failure.message ?? "JDTLS failed to start.")
: .failed(message: failure.message ?? "JDTLS failed to start.")
)
case .startingProcess, .initializing, .stopping, .stopped:
case .stopped:
javaFeature.cancelLanguageServerPreparation()
case .startingProcess, .initializing, .stopping:
break
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,28 @@ import LitheModuleAPI
final class LanguageIntelligenceModuleCoordinator {
private let runtime: ModuleRuntime
private let store: ModuleCapabilityStore
private let awaitShutdown: () async throws -> Void
private let bind: (LanguageIntelligenceCapability) -> Void
private let notify: @MainActor (String) -> Void

init(
runtime: ModuleRuntime,
store: ModuleCapabilityStore,
awaitShutdown: @escaping () async throws -> Void,
bind: @escaping (LanguageIntelligenceCapability) -> Void,
notify: @escaping @MainActor (String) -> Void
) {
self.runtime = runtime
self.store = store
self.awaitShutdown = awaitShutdown
self.bind = bind
self.notify = notify
}

func activate() async throws -> LanguageIntelligenceCapability {
// A cached capability can still belong to the graph being torn down.
try await awaitShutdown()
try Task.checkCancellation()
if let cached: LanguageIntelligenceCapability = store.capability(.languageIntelligence) {
return cached
}
Expand Down Expand Up @@ -58,13 +64,8 @@ final class LanguageIntelligenceModuleCoordinator {
resetWorkspaceState(feature)
}

func sessionsForMaintenance(
current: LanguageToolingSessionManager?
) async throws -> LanguageToolingSessionManager {
if let current {
return current
}
return try await activate().sessions
func sessionsForMaintenance() async throws -> LanguageToolingSessionManager {
try await activate().sessions
}

/// Activates the testing extension for every language support that claims
Expand All @@ -76,6 +77,13 @@ final class LanguageIntelligenceModuleCoordinator {
testService: LanguageTestService,
supports: @MainActor ([URL]) -> [LanguageSupportDeclaration]
) async -> Bool {
do {
try await awaitShutdown()
try Task.checkCancellation()
} catch {
if !error.isCancellation { notify(error.localizedDescription) }
return false
}
for support in supports(files) where support.testingModuleID != nil {
do {
let value = try await runtime.activateCapability(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ final class ModuleSessionCoordinator {
onModuleReleased(moduleID)
}

var isShuttingDown: Bool { shutdownTask != nil }

func beginShutdown() {
guard shutdownTask == nil else { return }
workbench.endLayoutSession()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,9 @@ extension RustCoreBridge: LanguageServerRuntimeCore {
startPluginLanguageServer(
pluginID: pluginID, providerID: providerID, executableURL: executableURL, arguments: arguments,
environment: environment, rootURL: rootURL, initializationOptions: initializationOptions,
workspaceConfiguration: nil, initializeTimeout: initializeTimeout,
workspaceConfiguration: nil, hostServices: [], hostIntegrationLaunch: nil,
cacheDirectoryURL: nil, workspaceFingerprint: nil, mavenContext: nil, javaGradle: nil,
initializeTimeout: initializeTimeout,
requestTimeout: requestTimeout, shutdownTimeout: shutdownTimeout
)
}
Expand All @@ -264,6 +266,12 @@ extension RustCoreBridge: LanguageServerRuntimeCore {
rootURL: URL,
initializationOptions: ToolingJSONValue?,
workspaceConfiguration: ToolingJSONValue?,
hostServices: [String],
hostIntegrationLaunch: LanguageServerHostIntegrationLaunch?,
cacheDirectoryURL: URL?,
workspaceFingerprint: String?,
mavenContext: MavenLaunchContext?,
javaGradle: JavaGradleImportConfiguration?,
initializeTimeout: TimeInterval,
requestTimeout: TimeInterval,
shutdownTimeout: TimeInterval
Expand All @@ -278,6 +286,13 @@ extension RustCoreBridge: LanguageServerRuntimeCore {
workingDirectoryURL: rootURL,
initializationOptions: initializationOptions,
workspaceConfiguration: workspaceConfiguration,
hostServices: hostServices,
runtimeExecutableURL: hostIntegrationLaunch?.runtimeExecutableURL,
jdtlsLaunchResources: hostIntegrationLaunch?.coreLaunch?.jdtlsLaunchResources,
cacheDirectoryURL: cacheDirectoryURL,
workspaceFingerprint: workspaceFingerprint,
mavenContext: mavenContext,
javaGradle: javaGradle,
initializeTimeout: initializeTimeout,
requestTimeout: requestTimeout,
shutdownTimeout: shutdownTimeout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ extension AppModel {
{
guard let identity = expectedIdentity ?? currentWorkspaceIdentity,
isCurrentWorkspace(identity), !Task.isCancelled else { throw CancellationError() }
let sessions = try await languageIntelligenceModuleCoordinator.sessionsForMaintenance(
current: languageToolingSessionsIfActive
)
let sessions = try await languageIntelligenceModuleCoordinator.sessionsForMaintenance()
try Task.checkCancellation()
guard isCurrentWorkspace(identity) else { throw CancellationError() }
await registerPluginLanguageServers(in: sessions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ extension AppModel {
}

var currentWorkspaceIdentity: WorkspaceIdentity? {
guard let url = workspaceURL?.standardizedFileURL else { return nil }
guard !isProjectSessionClosed, let url = workspaceURL?.standardizedFileURL else { return nil }
return WorkspaceIdentity(url: url, generation: workspaceFeature.workspaceGeneration)
}

Expand Down
23 changes: 20 additions & 3 deletions macos/Sources/Lithe/Models/AppModel/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ final class AppModel: ObservableObject, Identifiable, UnsavedDocumentHandling {
private var documentLanguageCoordinator: DocumentLanguageCoordinator?
private var sidebarRefreshTask: Task<Void, Never>?
private var moduleSessionCoordinator: ModuleSessionCoordinator?
private(set) var isProjectSessionClosed = false
private var featureObservationBinder: AppModelObservationBinder?
private var fileVisibilityRulesObserverID: UUID?
private var searchExclusionsObservation: AnyCancellable?
Expand Down Expand Up @@ -227,6 +228,10 @@ final class AppModel: ObservableObject, Identifiable, UnsavedDocumentHandling {
lazy var languageIntelligenceModuleCoordinator = LanguageIntelligenceModuleCoordinator(
runtime: services.moduleRuntime,
store: moduleCapabilityStore,
awaitShutdown: { [weak self] in
await self?.awaitModuleRuntimeShutdown()
guard self?.isProjectSessionClosed == false else { throw CancellationError() }
},
bind: { [weak self] in self?.bindLanguageIntelligenceCapability($0) },
notify: { [weak self] in self?.showNotification($0) }
)
Expand Down Expand Up @@ -691,6 +696,7 @@ final class AppModel: ObservableObject, Identifiable, UnsavedDocumentHandling {
}

func shutdownProjectSession() async {
isProjectSessionClosed = true
// Native window close can retain this model through Settings without
// closing its workspace. Revoke page requests before teardown can yield.
services.pluginWebPages.closeAll()
Expand Down Expand Up @@ -1359,6 +1365,16 @@ final class AppModel: ObservableObject, Identifiable, UnsavedDocumentHandling {
)
return false
}
if moduleSessionCoordinator?.isShuttingDown == true {
Task { @MainActor [weak self, weak document] in
guard let self else { return }
await self.awaitModuleRuntimeShutdown()
guard let document, !Task.isCancelled, self.isCurrentWorkspace(identity),
self.openDocuments.contains(where: { $0 === document }) else { return }
_ = self.activateLanguageServerIfAvailable(for: document)
}
return false
}
if descriptor.id == "java" {
switch prepareJavaLanguageServerRuntimeIfNeeded(for: document) {
case .ready: break
Expand All @@ -1379,6 +1395,9 @@ final class AppModel: ObservableObject, Identifiable, UnsavedDocumentHandling {
Task { [weak self, weak document] in
guard let self, let document else { return }
do {
await self.awaitModuleRuntimeShutdown()
guard !Task.isCancelled, self.isCurrentWorkspace(identity),
self.openDocuments.contains(where: { $0 === document }) else { return }
_ = try await self.services.moduleRuntime.activateCapability(capabilityID)
guard self.isCurrentWorkspace(identity), !Task.isCancelled,
self.openDocuments.contains(where: { $0 === document }) else { return }
Expand Down Expand Up @@ -1439,9 +1458,7 @@ final class AppModel: ObservableObject, Identifiable, UnsavedDocumentHandling {
Task { [weak self] in
guard let self else { return }
do {
let value = try await self.services.moduleRuntime.activateCapability(.languageIntelligence)
guard let capability = value as? LitheLanguageIntelligenceModule.LanguageIntelligenceCapability else { return }
self.bindLanguageIntelligenceCapability(capability)
_ = try await self.languageIntelligenceModuleCoordinator.activate()
guard self.isCurrentWorkspace(identity), !Task.isCancelled,
self.openDocuments.contains(where: { $0 === document }) else { return }
_ = self.activateLanguageServerIfAvailable(for: document)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,12 @@ package protocol LanguageServerRuntimeCore: Sendable {
rootURL: URL,
initializationOptions: ToolingJSONValue?,
workspaceConfiguration: ToolingJSONValue?,
hostServices: [String],
hostIntegrationLaunch: LanguageServerHostIntegrationLaunch?,
cacheDirectoryURL: URL?,
workspaceFingerprint: String?,
mavenContext: MavenLaunchContext?,
javaGradle: JavaGradleImportConfiguration?,
initializeTimeout: TimeInterval,
requestTimeout: TimeInterval,
shutdownTimeout: TimeInterval
Expand Down Expand Up @@ -712,13 +718,33 @@ package extension LanguageServerRuntimeCore {
rootURL: URL,
initializationOptions: ToolingJSONValue?,
workspaceConfiguration _: ToolingJSONValue?,
hostServices: [String],
hostIntegrationLaunch: LanguageServerHostIntegrationLaunch?,
cacheDirectoryURL: URL?,
workspaceFingerprint: String?,
mavenContext: MavenLaunchContext?,
javaGradle: JavaGradleImportConfiguration?,
initializeTimeout: TimeInterval,
requestTimeout: TimeInterval,
shutdownTimeout: TimeInterval
) -> Result<LanguageServerRuntimeStart, LanguageServerRuntimeFailure> {
startPluginLanguageServer(
if let hostIntegrationLaunch, hostServices.isEmpty {
return startLanguageServer(
providerID: providerID, executableURL: executableURL, arguments: arguments,
environment: environment, rootURL: rootURL, workingDirectoryURL: rootURL,
initializationOptions: initializationOptions,
runtimeExecutableURL: hostIntegrationLaunch.runtimeExecutableURL,
jdtlsLaunchResources: hostIntegrationLaunch.coreLaunch?.jdtlsLaunchResources,
cacheDirectoryURL: cacheDirectoryURL, workspaceFingerprint: workspaceFingerprint,
mavenContext: mavenContext, javaGradle: javaGradle,
initializeTimeout: initializeTimeout, requestTimeout: requestTimeout,
shutdownTimeout: shutdownTimeout
)
}
return startPluginLanguageServer(
pluginID: pluginID, providerID: providerID, executableURL: executableURL, arguments: arguments,
environment: environment, rootURL: rootURL, initializationOptions: initializationOptions,
hostServices: hostServices,
initializeTimeout: initializeTimeout, requestTimeout: requestTimeout, shutdownTimeout: shutdownTimeout
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,21 +201,7 @@ package final class LanguageServerRuntimeSession: LanguageServerSession {
([executableURL.path] + arguments).joined(separator: " "),
nil
)
let started = if let pluginID, !hostServices.isEmpty {
core.startPluginLanguageServer(
pluginID: pluginID,
providerID: providerID,
executableURL: executableURL,
arguments: arguments,
environment: environment,
rootURL: normalizedRoot,
initializationOptions: initializationOptions,
hostServices: hostServices,
initializeTimeout: initializeTimeout,
requestTimeout: requestTimeout,
shutdownTimeout: shutdownTimeout
)
} else if let pluginID {
let started = if let pluginID {
core.startPluginLanguageServer(
pluginID: pluginID,
providerID: providerID,
Expand All @@ -225,6 +211,12 @@ package final class LanguageServerRuntimeSession: LanguageServerSession {
rootURL: normalizedRoot,
initializationOptions: initializationOptions,
workspaceConfiguration: workspaceConfiguration,
hostServices: hostServices,
hostIntegrationLaunch: hostIntegrationLaunch,
cacheDirectoryURL: cacheDirectoryURL,
workspaceFingerprint: workspaceFingerprint,
mavenContext: mavenContext,
javaGradle: javaGradle,
initializeTimeout: initializeTimeout,
requestTimeout: requestTimeout,
shutdownTimeout: shutdownTimeout
Expand Down
Loading
Loading