Files
sourcekit-lsp/Sources/SourceKitD/SourceKitD.swift
T
Rintaro Ishizaki 4b2fa3193a Allow injection of a pre-initialized sourcekitd connection
Introduce `SourceKitDCore` as the protocol boundary between dylib
lifecycle management and the high-level `SourceKitD` API. Its single
lifecycle entry point, `initializeService(api:notificationCallback:)`,
receives the already-loaded `sourcekitd_api_functions_t` from
`SourceKitD.init(core:)`.

`SourceKitDCoreImpl` is the standard implementation: `init` opens the
dylib; `initializeService` registers any plugin paths, calls
`api.initialize()`, and wires the notification handler; `deinit` calls
`shutdown()` and closes the handle. Pre-initialized conformances
implement `initializeService` as a no-op.

Wire a `sourcekitdCoreInjector` hook through `Hooks` so an embedding
host can return a pre-initialized `SourceKitDCore` for a given toolchain
path, preventing `sourcekitd_initialize()` from being called a second
time.

Declare `SourceKitDCoreForPlugin` at its use sites so each call site
can express the exact deinit behavior it needs: `dlclose` for handles
acquired via `RTLD_NOLOAD`, and `leak` for externally-owned handles.
2026-05-07 09:58:47 -07:00

446 lines
17 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
package import Csourcekitd
package import Foundation
@_spi(SourceKitLSP) import SKLogging
import SwiftExtensions
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions
extension sourcekitd_api_keys: @unchecked Sendable {}
extension sourcekitd_api_requests: @unchecked Sendable {}
extension sourcekitd_api_values: @unchecked Sendable {}
fileprivate extension ThreadSafeBox {
/// If the wrapped value is `nil`, run `compute` and store the computed value. If it is not `nil`, return the stored
/// value.
func computeIfNil<WrappedValue: Sendable>(compute: () -> WrappedValue) -> WrappedValue where T == WrappedValue? {
return withLock { (value: inout WrappedValue?) -> WrappedValue in
if let value {
return value
}
let computed = compute()
value = computed
return computed
}
}
}
#if canImport(Darwin)
private func setenv(name: String, value: String, override: Bool) throws {
struct FailedToSetEnvError: Error {
let errorCode: Int32
}
try name.withCString { name in
try value.withCString { value in
let result = setenv(name, value, override ? 0 : 1)
if result != 0 {
throw FailedToSetEnvError(errorCode: result)
}
}
}
}
#endif
private struct SourceKitDRequestHandle: Sendable {
/// `nonisolated(unsafe)` is fine because we just use the handle as an opaque value.
nonisolated(unsafe) let handle: sourcekitd_api_request_handle_t
}
package struct PluginPaths: Equatable, CustomLogStringConvertible {
package let clientPlugin: URL
package let servicePlugin: URL
package init(clientPlugin: URL, servicePlugin: URL) {
self.clientPlugin = clientPlugin
self.servicePlugin = servicePlugin
}
package var description: String {
"(client: \(clientPlugin), service: \(servicePlugin))"
}
var redactedDescription: String {
"(client: \(clientPlugin.description.hashForLogging), service: \(servicePlugin.description.hashForLogging))"
}
}
package enum SKDError: Error, Equatable {
/// The service has crashed.
case connectionInterrupted
/// The request was unknown or had an invalid or missing parameter.
case requestInvalid(String)
/// The request failed.
case requestFailed(String)
/// The request was cancelled.
case requestCancelled
/// The request exceeded the maximum allowed duration.
case timedOut
/// Loading a required symbol from the sourcekitd library failed.
case missingRequiredSymbol(String)
}
/// Wrapper for sourcekitd that provides the high-level `send` API, notification handler
/// multiplexing, and convenience UID accessors.
///
/// Dylib lifecycle (`initialize`, `shutdown`, `set_notification_handler`) is managed by the
/// underlying `SourceKitDCore` implementation, not by this type directly.
package actor SourceKitD {
/// The underlying dylib connection (owns the handle, initialize/shutdown, and raw notification
/// callback registration).
nonisolated package let core: any SourceKitDCore
/// Path to the sourcekitd dylib.
nonisolated package var path: URL { core.path }
/// The sourcekitd API functions loaded from `core.dlHandle`.
nonisolated package let api: sourcekitd_api_functions_t
/// General API for the SourceKit service and client framework, eg. for plugin initialization and to set up custom
/// variant functions.
///
/// This must not be referenced outside of `SwiftSourceKitPlugin`, `SwiftSourceKitPluginCommon`, or
/// `SwiftSourceKitClientPlugin`.
package nonisolated var pluginApi: sourcekitd_plugin_api_functions_t { try! pluginApiResult.get() }
private let pluginApiResult: Result<sourcekitd_plugin_api_functions_t, any Error>
/// The API with which the SourceKit plugin handles requests.
///
/// This must not be referenced outside of `SwiftSourceKitPlugin`.
package nonisolated var servicePluginApi: sourcekitd_service_plugin_api_functions_t {
do {
return try servicePluginApiResult.get()
} catch {
fatalError("failed to get service plugin api, error: \(error)")
}
}
private let servicePluginApiResult: Result<sourcekitd_service_plugin_api_functions_t, any Error>
/// The API with which the SourceKit plugin communicates with the type-checker in-process.
///
/// This must not be referenced outside of `SwiftSourceKitPlugin`.
package nonisolated var ideApi: sourcekitd_ide_api_functions_t { try! ideApiResult.get() }
private let ideApiResult: Result<sourcekitd_ide_api_functions_t, any Error>
/// Convenience for accessing known keys.
///
/// These need to be computed dynamically so that a client has the chance to register a UID handler between the
/// initialization of the SourceKit plugin and the first request being handled by it.
private let _keys: ThreadSafeBox<sourcekitd_api_keys?> = ThreadSafeBox(initialValue: nil)
package nonisolated var keys: sourcekitd_api_keys {
_keys.computeIfNil { sourcekitd_api_keys(api: self.api) }
}
/// Convenience for accessing known request names.
///
/// These need to be computed dynamically so that a client has the chance to register a UID handler between the
/// initialization of the SourceKit plugin and the first request being handled by it.
private let _requests: ThreadSafeBox<sourcekitd_api_requests?> = ThreadSafeBox(initialValue: nil)
package nonisolated var requests: sourcekitd_api_requests {
_requests.computeIfNil { sourcekitd_api_requests(api: self.api) }
}
/// Convenience for accessing known request/response values.
///
/// These need to be computed dynamically so that a client has the chance to register a UID handler between the
/// initialization of the SourceKit plugin and the first request being handled by it.
private let _values: ThreadSafeBox<sourcekitd_api_values?> = ThreadSafeBox(initialValue: nil)
package nonisolated var values: sourcekitd_api_values {
_values.computeIfNil { sourcekitd_api_values(api: self.api) }
}
private nonisolated let notificationHandlingQueue = AsyncQueue<Serial>()
/// List of notification handlers that will be called for each notification.
private var notificationHandlers: [WeakSKDNotificationHandler] = []
/// List of hooks that should be executed and that need to finish executing before a request is sent to sourcekitd.
private var preRequestHandlingHooks: [UUID: @Sendable (SKDRequestDictionary) async -> Void] = [:]
/// List of hooks that should be executed after a request sent to sourcekitd.
private var requestHandlingHooks: [UUID: (SKDRequestDictionary) -> Void] = [:]
package static func getOrCreate(
dylibPath: URL,
pluginPaths: PluginPaths?
) async throws -> SourceKitD {
try await SourceKitDRegistry.shared.getOrAdd(dylibPath, pluginPaths: pluginPaths) {
return try SourceKitD(dylib: dylibPath, pluginPaths: pluginPaths)
}
}
package static func getOrCreate(core: any SourceKitDCore) async throws -> SourceKitD {
try await SourceKitDRegistry.shared.getOrAdd(core.path, pluginPaths: nil) {
return try SourceKitD(core: core)
}
}
/// Create a `SourceKitD` wrapping an existing `SourceKitDCore` connection.
package init(core: any SourceKitDCore) throws {
self.core = core
let api = try sourcekitd_api_functions_t(core.dlHandle)
self.api = api
// We load the plugin-related functions eagerly so the members are initialized and we don't have data races on first
// access to eg. `pluginApi`. But if one of the functions is missing, we will only emit that error when that family
// of functions is being used. For example, it is expected that the plugin functions are not available in
// SourceKit-LSP.
self.ideApiResult = Result(catching: { try sourcekitd_ide_api_functions_t(core.dlHandle) })
self.pluginApiResult = Result(catching: { try sourcekitd_plugin_api_functions_t(core.dlHandle) })
self.servicePluginApiResult = Result(catching: { try sourcekitd_service_plugin_api_functions_t(core.dlHandle) })
core.initializeService(
api: api,
notificationCallback: { [weak self] rawResponse in
guard let self else { return }
let response = SKDResponse(rawResponse, sourcekitd: self)
self.notificationHandlingQueue.async {
let handlers = await self.notificationHandlers.compactMap(\.value)
for handler in handlers {
handler.notification(response)
}
}
}
)
}
package init(dylib path: URL, pluginPaths: PluginPaths?) throws {
try self.init(core: SourceKitDCoreImpl(dylib: path, pluginPaths: pluginPaths))
}
/// Adds a new notification handler (referenced weakly).
package func addNotificationHandler(_ handler: any SKDNotificationHandler) {
notificationHandlers.removeAll(where: { $0.value == nil })
notificationHandlers.append(.init(handler))
}
/// Removes a previously registered notification handler.
package func removeNotificationHandler(_ handler: any SKDNotificationHandler) {
notificationHandlers.removeAll(where: { $0.value == nil || $0.value === handler })
}
/// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`.
///
/// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd
/// requests that were sent by other clients of the same `DynamicallyLoadedSourceKitD` instance that just happen to
/// send a request during that time.
///
/// This is intended for testing only.
package func withPreRequestHandlingHook(
body: () async throws -> Void,
hook: @escaping @Sendable (SKDRequestDictionary) async -> Void
) async rethrows {
let id = UUID()
preRequestHandlingHooks[id] = hook
defer { preRequestHandlingHooks[id] = nil }
try await body()
}
func willSend(request: SKDRequestDictionary) async {
let request = request
for hook in preRequestHandlingHooks.values {
await hook(request)
}
}
/// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`.
///
/// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd
/// requests that were sent by other clients of the same `DynamicallyLoadedSourceKitD` instance that just happen to
/// send a request during that time.
///
/// This is intended for testing only.
package func withRequestHandlingHook(
body: () async throws -> Void,
hook: @escaping (SKDRequestDictionary) -> Void
) async rethrows {
let id = UUID()
requestHandlingHooks[id] = hook
defer { requestHandlingHooks[id] = nil }
try await body()
}
func didSend(request: SKDRequestDictionary) {
for hook in requestHandlingHooks.values {
hook(request)
}
}
private struct ContextualRequest {
enum Kind {
case editorOpen
case codeCompleteOpen
}
let kind: Kind
let request: SKDRequestDictionary
}
private var contextualRequests: [URL: [ContextualRequest]] = [:]
private func recordContextualRequest(
requestUid: sourcekitd_api_uid_t,
request: SKDRequestDictionary,
documentUrl: URL?
) {
guard let documentUrl else {
return
}
switch requestUid {
case requests.editorOpen:
contextualRequests[documentUrl] = [ContextualRequest(kind: .editorOpen, request: request)]
case requests.editorClose:
contextualRequests[documentUrl] = nil
case requests.codeCompleteOpen:
contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen })
contextualRequests[documentUrl, default: []].append(ContextualRequest(kind: .codeCompleteOpen, request: request))
case requests.codeCompleteClose:
contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen })
if contextualRequests[documentUrl]?.isEmpty ?? false {
// This should never happen because we should still have an active `.editorOpen` contextual request but just be
// safe in case we don't.
contextualRequests[documentUrl] = nil
}
default:
break
}
}
/// - Parameters:
/// - request: The request to send to sourcekitd.
/// - timeout: The maximum duration how long to wait for a response. If no response is returned within this time,
/// declare the request as having timed out.
/// - fileContents: The contents of the file that the request operates on. If sourcekitd crashes, the file contents
/// will be logged.
package func send(
_ requestUid: KeyPath<sourcekitd_api_requests, sourcekitd_api_uid_t>,
_ request: SKDRequestDictionary,
timeout: Duration,
restartTimeout: Duration,
documentUrl: URL?,
fileContents: String?
) async throws -> SKDResponseDictionary {
request.set(keys.request, to: requests[keyPath: requestUid])
recordContextualRequest(requestUid: requests[keyPath: requestUid], request: request, documentUrl: documentUrl)
let sourcekitdResponse = try await withTimeout(timeout) {
let restartTimeoutHandle = TimeoutHandle()
do {
return try await withTimeout(restartTimeout, handle: restartTimeoutHandle) {
await self.willSend(request: request)
return try await withCancellableCheckedThrowingContinuation { (continuation) -> SourceKitDRequestHandle? in
logger.info(
"""
Sending sourcekitd request:
\(request.forLogging)
"""
)
var handle: sourcekitd_api_request_handle_t? = nil
self.api.send_request(request.dict, &handle) { response in
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
}
Task {
await self.didSend(request: request)
}
if let handle {
return SourceKitDRequestHandle(handle: handle)
}
return nil
} cancel: { (handle: SourceKitDRequestHandle?) in
if let handle {
logger.info(
"""
Cancelling sourcekitd request:
\(request.forLogging)
"""
)
self.api.cancel_request(handle.handle)
}
}
}
} catch let error as TimeoutError where error.handle == restartTimeoutHandle {
if !self.path.lastPathComponent.contains("InProc") {
logger.fault(
"Did not receive reply from sourcekitd after \(restartTimeout, privacy: .public). Terminating and restarting sourcekitd."
)
await self.crash()
} else {
logger.fault(
"Did not receive reply from sourcekitd after \(restartTimeout, privacy: .public). Not terminating sourcekitd because it is run in-process."
)
}
throw error
}
}
logger.log(
level: (sourcekitdResponse.error == nil || sourcekitdResponse.error == .requestCancelled) ? .debug : .error,
"""
Received sourcekitd response:
\(sourcekitdResponse.forLogging)
"""
)
guard let dict = sourcekitdResponse.value else {
if sourcekitdResponse.error == .connectionInterrupted {
var log = """
Request:
\(request.description)
File contents:
\(fileContents ?? "<nil>")
"""
if let documentUrl {
let contextualRequests = (contextualRequests[documentUrl] ?? []).filter { $0.request !== request }
for (index, contextualRequest) in contextualRequests.enumerated() {
log += """
Contextual request \(index + 1) / \(contextualRequests.count):
\(contextualRequest.request.description)
"""
}
}
let chunks = splitLongMultilineMessage(message: log)
for (index, chunk) in chunks.enumerated() {
logger.fault(
"""
sourcekitd crashed (\(index + 1)/\(chunks.count))
\(chunk)
"""
)
}
}
if sourcekitdResponse.error == .requestCancelled && !Task.isCancelled {
throw SKDError.timedOut
}
throw sourcekitdResponse.error!
}
return dict
}
package func crash() async {
_ = try? await send(
\.crashWithExit,
dictionary([:]),
timeout: .seconds(60),
restartTimeout: .seconds(24 * 60 * 60),
documentUrl: nil,
fileContents: nil
)
}
}