Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 21e3bc3

Browse files
committedMay 7, 2025·
If sourcekitd or clangd don’t respond to a request for 5 minutes, terminate them and use crash recovery to restore behavior
This should be a last stop-gap measure in case sourcekitd or clangd get stuck, don’t respond to any requests anymore and don’t honor cancellation either. In that case we can restore SourceKit-LSP behavior by killing them and using the crash recovery logic to restore functionality. rdar://149492159
1 parent 6a8ea4c commit 21e3bc3

19 files changed

+377
-84
lines changed
 

‎Documentation/Configuration File.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,4 @@ The structure of the file is currently not guaranteed to be stable. Options may
5858
- `swiftPublishDiagnosticsDebounceDuration: number`: The time that `SwiftLanguageService` should wait after an edit before starting to compute diagnostics and sending a `PublishDiagnosticsNotification`.
5959
- `workDoneProgressDebounceDuration: number`: When a task is started that should be displayed to the client as a work done progress, how many milliseconds to wait before actually starting the work done progress. This prevents flickering of the work done progress in the client for short-lived index tasks which end within this duration.
6060
- `sourcekitdRequestTimeout: number`: The maximum duration that a sourcekitd request should be allowed to execute before being declared as timed out. In general, editors should cancel requests that they are no longer interested in, but in case editors don't cancel requests, this ensures that a long-running non-cancelled request is not blocking sourcekitd and thus most semantic functionality. In particular, VS Code does not cancel the semantic tokens request, which can cause a long-running AST build that blocks sourcekitd.
61+
- `semanticServiceRestartTimeout: number`: If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it.

‎Sources/BuildSystemIntegration/ExternalBuildSystemAdapter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ actor ExternalBuildSystemAdapter {
185185
protocol: bspRegistry,
186186
stderrLoggingCategory: "bsp-server-stderr",
187187
client: messagesToSourceKitLSPHandler,
188-
terminationHandler: { [weak self] terminationStatus in
188+
terminationHandler: { [weak self] terminationReason in
189189
guard let self else {
190190
return
191191
}
192-
if terminationStatus != 0 {
192+
if terminationReason != .exited(exitCode: 0) {
193193
Task {
194194
await orLog("Restarting BSP server") {
195195
try await self.handleBspServerCrash()

‎Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ import struct CDispatch.dispatch_fd_t
3131
/// For example, inside a language server, the `JSONRPCConnection` takes the language service implementation as its
3232
// `receiveHandler` and itself provides the client connection for sending notifications and callbacks.
3333
public final class JSONRPCConnection: Connection {
34+
public enum TerminationReason: Sendable, Equatable {
35+
/// The process on the other end of the `JSONRPCConnection` terminated with the given exit code.
36+
case exited(exitCode: Int32)
37+
38+
/// The process on the other end of the `JSONRPCConnection` terminated with a signal. The signal that it terminated
39+
/// with is not known.
40+
case uncaughtSignal
41+
}
3442

3543
/// A name of the endpoint for this connection, used for logging, e.g. `clangd`.
3644
private let name: String
@@ -198,7 +206,7 @@ public final class JSONRPCConnection: Connection {
198206
protocol messageRegistry: MessageRegistry,
199207
stderrLoggingCategory: String,
200208
client: MessageHandler,
201-
terminationHandler: @Sendable @escaping (_ terminationStatus: Int32) -> Void
209+
terminationHandler: @Sendable @escaping (_ terminationReason: TerminationReason) -> Void
202210
) throws -> (connection: JSONRPCConnection, process: Process) {
203211
let clientToServer = Pipe()
204212
let serverToClient = Pipe()
@@ -238,10 +246,22 @@ public final class JSONRPCConnection: Connection {
238246
process.terminationHandler = { process in
239247
logger.log(
240248
level: process.terminationReason == .exit ? .default : .error,
241-
"\(name) exited: \(String(reflecting: process.terminationReason)) \(process.terminationStatus)"
249+
"\(name) exited: \(process.terminationReason.rawValue) \(process.terminationStatus)"
242250
)
243251
connection.close()
244-
terminationHandler(process.terminationStatus)
252+
let terminationReason: TerminationReason
253+
switch process.terminationReason {
254+
case .exit:
255+
terminationReason = .exited(exitCode: process.terminationStatus)
256+
case .uncaughtSignal:
257+
terminationReason = .uncaughtSignal
258+
@unknown default:
259+
logger.fault(
260+
"Process terminated with unknown termination reason: \(process.terminationReason.rawValue, privacy: .public)"
261+
)
262+
terminationReason = .exited(exitCode: 0)
263+
}
264+
terminationHandler(terminationReason)
245265
}
246266
try process.run()
247267

‎Sources/SKOptions/SourceKitLSPOptions.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,17 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
422422
return .seconds(120)
423423
}
424424

425+
/// If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging
426+
/// for some reason and won't recover. To restore semantic functionality, we terminate and restart it.
427+
public var semanticServiceRestartTimeout: Double? = nil
428+
429+
public var semanticServiceRestartTimeoutOrDefault: Duration {
430+
if let semanticServiceRestartTimeout {
431+
return .seconds(semanticServiceRestartTimeout)
432+
}
433+
return .seconds(300)
434+
}
435+
425436
public init(
426437
swiftPM: SwiftPMOptions? = .init(),
427438
fallbackBuildSystem: FallbackBuildSystemOptions? = .init(),
@@ -439,7 +450,8 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
439450
experimentalFeatures: Set<ExperimentalFeature>? = nil,
440451
swiftPublishDiagnosticsDebounceDuration: Double? = nil,
441452
workDoneProgressDebounceDuration: Double? = nil,
442-
sourcekitdRequestTimeout: Double? = nil
453+
sourcekitdRequestTimeout: Double? = nil,
454+
semanticServiceRestartTimeout: Double? = nil
443455
) {
444456
self.swiftPM = swiftPM
445457
self.fallbackBuildSystem = fallbackBuildSystem
@@ -458,6 +470,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
458470
self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration
459471
self.workDoneProgressDebounceDuration = workDoneProgressDebounceDuration
460472
self.sourcekitdRequestTimeout = sourcekitdRequestTimeout
473+
self.semanticServiceRestartTimeout = semanticServiceRestartTimeout
461474
}
462475

463476
public init?(fromLSPAny lspAny: LSPAny?) throws {
@@ -517,7 +530,8 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
517530
?? base.swiftPublishDiagnosticsDebounceDuration,
518531
workDoneProgressDebounceDuration: override?.workDoneProgressDebounceDuration
519532
?? base.workDoneProgressDebounceDuration,
520-
sourcekitdRequestTimeout: override?.sourcekitdRequestTimeout ?? base.sourcekitdRequestTimeout
533+
sourcekitdRequestTimeout: override?.sourcekitdRequestTimeout ?? base.sourcekitdRequestTimeout,
534+
semanticServiceRestartTimeout: override?.semanticServiceRestartTimeout ?? base.semanticServiceRestartTimeout
521535
)
522536
}
523537

‎Sources/SKTestSupport/SkipUnless.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,7 @@ package actor SkipUnless {
265265
sourcekitd.keys.useNewAPI: 1
266266
],
267267
]),
268-
timeout: defaultTimeoutDuration,
269-
fileContents: nil
268+
timeout: defaultTimeoutDuration
270269
)
271270
return response[sourcekitd.keys.useNewAPI] == 1
272271
} catch {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
package import SourceKitD
14+
15+
extension SourceKitD {
16+
/// Convenience overload of the `send` function for testing that doesn't restart sourcekitd if it does not respond
17+
/// and doesn't pass any file contents.
18+
package func send(
19+
_ request: SKDRequestDictionary,
20+
timeout: Duration
21+
) async throws -> SKDResponseDictionary {
22+
return try await self.send(
23+
request,
24+
timeout: timeout,
25+
restartTimeout: .seconds(60 * 60 * 24),
26+
fileContents: nil
27+
)
28+
}
29+
}

‎Sources/SourceKitD/SourceKitD.swift

Lines changed: 79 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,10 @@ package actor SourceKitD {
162162
/// List of notification handlers that will be called for each notification.
163163
private var notificationHandlers: [WeakSKDNotificationHandler] = []
164164

165-
/// List of hooks that should be executed for every request sent to sourcekitd.
165+
/// List of hooks that should be executed and that need to finish executing before a request is sent to sourcekitd.
166+
private var preRequestHandlingHooks: [UUID: @Sendable (SKDRequestDictionary) async -> Void] = [:]
167+
168+
/// List of hooks that should be executed after a request sent to sourcekitd.
166169
private var requestHandlingHooks: [UUID: (SKDRequestDictionary) -> Void] = [:]
167170

168171
package static func getOrCreate(
@@ -261,6 +264,30 @@ package actor SourceKitD {
261264
notificationHandlers.removeAll(where: { $0.value == nil || $0.value === handler })
262265
}
263266

267+
/// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`.
268+
///
269+
/// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd
270+
/// requests that were sent by other clients of the same `DynamicallyLoadedSourceKitD` instance that just happen to
271+
/// send a request during that time.
272+
///
273+
/// This is intended for testing only.
274+
package func withPreRequestHandlingHook(
275+
body: () async throws -> Void,
276+
hook: @escaping @Sendable (SKDRequestDictionary) async -> Void
277+
) async rethrows {
278+
let id = UUID()
279+
preRequestHandlingHooks[id] = hook
280+
defer { preRequestHandlingHooks[id] = nil }
281+
try await body()
282+
}
283+
284+
func willSend(request: SKDRequestDictionary) async {
285+
let request = request
286+
for hook in preRequestHandlingHooks.values {
287+
await hook(request)
288+
}
289+
}
290+
264291
/// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`.
265292
///
266293
/// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd
@@ -293,37 +320,56 @@ package actor SourceKitD {
293320
package func send(
294321
_ request: SKDRequestDictionary,
295322
timeout: Duration,
323+
restartTimeout: Duration,
296324
fileContents: String?
297325
) async throws -> SKDResponseDictionary {
298326
let sourcekitdResponse = try await withTimeout(timeout) {
299-
return try await withCancellableCheckedThrowingContinuation { (continuation) -> SourceKitDRequestHandle? in
300-
logger.info(
301-
"""
302-
Sending sourcekitd request:
303-
\(request.forLogging)
304-
"""
305-
)
306-
var handle: sourcekitd_api_request_handle_t? = nil
307-
self.api.send_request(request.dict, &handle) { response in
308-
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
309-
}
310-
Task {
311-
await self.didSend(request: request)
312-
}
313-
if let handle {
314-
return SourceKitDRequestHandle(handle: handle)
327+
let restartTimeoutHandle = TimeoutHandle()
328+
do {
329+
return try await withTimeout(restartTimeout, handle: restartTimeoutHandle) {
330+
await self.willSend(request: request)
331+
return try await withCancellableCheckedThrowingContinuation { (continuation) -> SourceKitDRequestHandle? in
332+
logger.info(
333+
"""
334+
Sending sourcekitd request:
335+
\(request.forLogging)
336+
"""
337+
)
338+
var handle: sourcekitd_api_request_handle_t? = nil
339+
self.api.send_request(request.dict, &handle) { response in
340+
continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
341+
}
342+
Task {
343+
await self.didSend(request: request)
344+
}
345+
if let handle {
346+
return SourceKitDRequestHandle(handle: handle)
347+
}
348+
return nil
349+
} cancel: { (handle: SourceKitDRequestHandle?) in
350+
if let handle {
351+
logger.info(
352+
"""
353+
Cancelling sourcekitd request:
354+
\(request.forLogging)
355+
"""
356+
)
357+
self.api.cancel_request(handle.handle)
358+
}
359+
}
315360
}
316-
return nil
317-
} cancel: { (handle: SourceKitDRequestHandle?) in
318-
if let handle {
319-
logger.info(
320-
"""
321-
Cancelling sourcekitd request:
322-
\(request.forLogging)
323-
"""
361+
} catch let error as TimeoutError where error.handle == restartTimeoutHandle {
362+
if !self.path.lastPathComponent.contains("InProc") {
363+
logger.fault(
364+
"Did not receive reply from sourcekitd after \(restartTimeout, privacy: .public). Terminating and restarting sourcekitd."
365+
)
366+
await self.crash()
367+
} else {
368+
logger.fault(
369+
"Did not receive reply from sourcekitd after \(restartTimeout, privacy: .public). Not terminating sourcekitd because it is run in-process."
324370
)
325-
self.api.cancel_request(handle.handle)
326371
}
372+
throw error
327373
}
328374
}
329375

@@ -362,6 +408,13 @@ package actor SourceKitD {
362408

363409
return dict
364410
}
411+
412+
package func crash() async {
413+
let req = dictionary([
414+
keys.request: requests.crashWithExit
415+
])
416+
_ = try? await send(req, timeout: .seconds(60), restartTimeout: .seconds(24 * 60 * 60), fileContents: nil)
417+
}
365418
}
366419

367420
/// A sourcekitd notification handler in a class to allow it to be uniquely referenced.

‎Sources/SourceKitD/SourceKitDRegistry.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ package actor SourceKitDRegistry<SourceKitDType: AnyObject> {
3030
private var active: [URL: (pluginPaths: PluginPaths?, sourcekitd: SourceKitDType)] = [:]
3131

3232
/// Instances that have been unregistered, but may be resurrected if accessed before destruction.
33-
private var cemetary: [URL: (pluginPaths: PluginPaths?, sourcekitd: WeakSourceKitD<SourceKitDType>)] = [:]
33+
private var cemetery: [URL: (pluginPaths: PluginPaths?, sourcekitd: WeakSourceKitD<SourceKitDType>)] = [:]
3434

3535
/// Initialize an empty registry.
3636
package init() {}
@@ -49,8 +49,8 @@ package actor SourceKitDRegistry<SourceKitDType: AnyObject> {
4949
}
5050
return existing.sourcekitd
5151
}
52-
if let resurrected = cemetary[key], let resurrectedSourcekitD = resurrected.sourcekitd.value {
53-
cemetary[key] = nil
52+
if let resurrected = cemetery[key], let resurrectedSourcekitD = resurrected.sourcekitd.value {
53+
cemetery[key] = nil
5454
if resurrected.pluginPaths != pluginPaths {
5555
logger.fault(
5656
"Already created SourceKitD with plugin paths \(resurrected.pluginPaths?.forLogging), now requesting incompatible plugin paths \(pluginPaths.forLogging)"
@@ -74,8 +74,8 @@ package actor SourceKitDRegistry<SourceKitDType: AnyObject> {
7474
package func remove(_ key: URL) -> SourceKitDType? {
7575
let existing = active.removeValue(forKey: key)
7676
if let existing = existing {
77-
assert(self.cemetary[key]?.sourcekitd.value == nil)
78-
cemetary[key] = (existing.pluginPaths, WeakSourceKitD(value: existing.sourcekitd))
77+
assert(self.cemetery[key]?.sourcekitd.value == nil)
78+
cemetery[key] = (existing.pluginPaths, WeakSourceKitD(value: existing.sourcekitd))
7979
}
8080
return existing?.sourcekitd
8181
}

‎Sources/SourceKitLSP/Clang/ClangLanguageService.swift

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ actor ClangLanguageService: LanguageService, MessageHandler {
6262
/// Path to the `clangd` binary.
6363
let clangdPath: URL
6464

65-
let clangdOptions: [String]
65+
let options: SourceKitLSPOptions
6666

6767
/// The current state of the `clangd` language server.
6868
/// Changing the property automatically notified the state change handlers.
@@ -117,7 +117,7 @@ actor ClangLanguageService: LanguageService, MessageHandler {
117117
}
118118
self.clangPath = toolchain.clang
119119
self.clangdPath = clangdPath
120-
self.clangdOptions = options.clangdOptions ?? []
120+
self.options = options
121121
self.workspace = WeakWorkspace(workspace)
122122
self.state = .connected
123123
self.sourceKitLSPServer = sourceKitLSPServer
@@ -154,10 +154,11 @@ actor ClangLanguageService: LanguageService, MessageHandler {
154154
/// Restarts `clangd` if it has crashed.
155155
///
156156
/// - Parameter terminationStatus: The exit code of `clangd`.
157-
private func handleClangdTermination(terminationStatus: Int32) {
157+
private func handleClangdTermination(terminationReason: JSONRPCConnection.TerminationReason) {
158158
self.clangdProcess = nil
159-
if terminationStatus != 0 {
159+
if terminationReason != .exited(exitCode: 0) {
160160
self.state = .connectionInterrupted
161+
logger.info("clangd crashed. Restarting it.")
161162
self.restartClangd()
162163
}
163164
}
@@ -173,15 +174,15 @@ actor ClangLanguageService: LanguageService, MessageHandler {
173174
"-compile_args_from=lsp", // Provide compiler args programmatically.
174175
"-background-index=false", // Disable clangd indexing, we use the build
175176
"-index=false", // system index store instead.
176-
] + clangdOptions,
177+
] + (options.clangdOptions ?? []),
177178
name: "clangd",
178179
protocol: MessageRegistry.lspProtocol,
179180
stderrLoggingCategory: "clangd-stderr",
180181
client: self,
181-
terminationHandler: { [weak self] terminationStatus in
182+
terminationHandler: { [weak self] terminationReason in
182183
guard let self = self else { return }
183184
Task {
184-
await self.handleClangdTermination(terminationStatus: terminationStatus)
185+
await self.handleClangdTermination(terminationReason: terminationReason)
185186
}
186187

187188
}
@@ -291,14 +292,20 @@ actor ClangLanguageService: LanguageService, MessageHandler {
291292
}
292293

293294
/// Forward the given request to `clangd`.
294-
///
295-
/// This method calls `readyToHandleNextRequest` once the request has been
296-
/// transmitted to `clangd` and another request can be safely transmitted to
297-
/// `clangd` while guaranteeing ordering.
298-
///
299-
/// The response of the request is returned asynchronously as the return value.
300295
func forwardRequestToClangd<R: RequestType>(_ request: R) async throws -> R.Response {
301-
return try await clangd.send(request)
296+
let timeoutHandle = TimeoutHandle()
297+
do {
298+
return try await withTimeout(options.semanticServiceRestartTimeoutOrDefault, handle: timeoutHandle) {
299+
await self.sourceKitLSPServer?.hooks.preForwardRequestToClangd?(request)
300+
return try await self.clangd.send(request)
301+
}
302+
} catch let error as TimeoutError where error.handle == timeoutHandle {
303+
logger.fault(
304+
"Did not receive reply from clangd after \(options.semanticServiceRestartTimeoutOrDefault, privacy: .public). Terminating and restarting clangd."
305+
)
306+
self.crash()
307+
throw error
308+
}
302309
}
303310

304311
package func canonicalDeclarationPosition(of position: Position, in uri: DocumentURI) async -> Position? {

‎Sources/SourceKitLSP/Hooks.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,24 @@ public struct Hooks: Sendable {
3030
/// This allows requests to be artificially delayed.
3131
package var preHandleRequest: (@Sendable (any RequestType) async -> Void)?
3232

33+
/// Closure that is executed before a request is forwarded to clangd.
34+
///
35+
/// This allows tests to simulate a `clangd` process that's unresponsive.
36+
package var preForwardRequestToClangd: (@Sendable (any RequestType) async -> Void)?
37+
3338
public init() {
3439
self.init(indexHooks: IndexHooks(), buildSystemHooks: BuildSystemHooks())
3540
}
3641

3742
package init(
3843
indexHooks: IndexHooks = IndexHooks(),
3944
buildSystemHooks: BuildSystemHooks = BuildSystemHooks(),
40-
preHandleRequest: (@Sendable (any RequestType) async -> Void)? = nil
45+
preHandleRequest: (@Sendable (any RequestType) async -> Void)? = nil,
46+
preForwardRequestToClangd: (@Sendable (any RequestType) async -> Void)? = nil
4147
) {
4248
self.indexHooks = indexHooks
4349
self.buildSystemHooks = buildSystemHooks
4450
self.preHandleRequest = preHandleRequest
51+
self.preForwardRequestToClangd = preForwardRequestToClangd
4552
}
4653
}

‎Sources/SourceKitLSP/Swift/CodeCompletionSession.swift

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ class CodeCompletionSession {
201201
return await Self.resolveDocumentation(
202202
in: item,
203203
timeout: session.options.sourcekitdRequestTimeoutOrDefault,
204+
restartTimeout: session.options.semanticServiceRestartTimeoutOrDefault,
204205
sourcekitd: sourcekitd
205206
)
206207
}
@@ -287,11 +288,7 @@ class CodeCompletionSession {
287288
keys.codeCompleteOptions: optionsDictionary(filterText: filterText),
288289
])
289290

290-
let dict = try await sourcekitd.send(
291-
req,
292-
timeout: options.sourcekitdRequestTimeoutOrDefault,
293-
fileContents: snapshot.text
294-
)
291+
let dict = try await sendSourceKitdRequest(req, snapshot: snapshot)
295292
self.state = .open
296293

297294
guard let completions: SKDResponseArray = dict[keys.results] else {
@@ -325,11 +322,7 @@ class CodeCompletionSession {
325322
keys.codeCompleteOptions: optionsDictionary(filterText: filterText),
326323
])
327324

328-
let dict = try await sourcekitd.send(
329-
req,
330-
timeout: options.sourcekitdRequestTimeoutOrDefault,
331-
fileContents: snapshot.text
332-
)
325+
let dict = try await sendSourceKitdRequest(req, snapshot: snapshot)
333326
guard let completions: SKDResponseArray = dict[keys.results] else {
334327
return CompletionList(isIncomplete: false, items: [])
335328
}
@@ -378,13 +371,25 @@ class CodeCompletionSession {
378371
keys.codeCompleteOptions: [keys.useNewAPI: 1],
379372
])
380373
logger.info("Closing code completion session: \(self.description)")
381-
_ = try? await sourcekitd.send(req, timeout: options.sourcekitdRequestTimeoutOrDefault, fileContents: nil)
374+
_ = try? await sendSourceKitdRequest(req, snapshot: nil)
382375
self.state = .closed
383376
}
384377
}
385378

386379
// MARK: - Helpers
387380

381+
private func sendSourceKitdRequest(
382+
_ request: SKDRequestDictionary,
383+
snapshot: DocumentSnapshot?
384+
) async throws -> SKDResponseDictionary {
385+
try await sourcekitd.send(
386+
request,
387+
timeout: options.sourcekitdRequestTimeoutOrDefault,
388+
restartTimeout: options.semanticServiceRestartTimeoutOrDefault,
389+
fileContents: snapshot?.text
390+
)
391+
}
392+
388393
private func expandClosurePlaceholders(insertText: String) -> String? {
389394
guard insertText.contains("<#") && insertText.contains("->") else {
390395
// Fast path: There is no closure placeholder to expand
@@ -532,8 +537,14 @@ class CodeCompletionSession {
532537
}
533538

534539
if !clientSupportsDocumentationResolve {
540+
let semanticServiceRestartTimeoutOrDefault = self.options.semanticServiceRestartTimeoutOrDefault
535541
completionItems = await completionItems.asyncMap { item in
536-
return await Self.resolveDocumentation(in: item, timeout: .seconds(1), sourcekitd: sourcekitd)
542+
return await Self.resolveDocumentation(
543+
in: item,
544+
timeout: .seconds(1),
545+
restartTimeout: semanticServiceRestartTimeoutOrDefault,
546+
sourcekitd: sourcekitd
547+
)
537548
}
538549
}
539550

@@ -543,6 +554,7 @@ class CodeCompletionSession {
543554
private static func resolveDocumentation(
544555
in item: CompletionItem,
545556
timeout: Duration,
557+
restartTimeout: Duration,
546558
sourcekitd: SourceKitD
547559
) async -> CompletionItem {
548560
var item = item
@@ -552,7 +564,7 @@ class CodeCompletionSession {
552564
sourcekitd.keys.identifier: itemId,
553565
])
554566
let documentationResponse = await orLog("Retrieving documentation for completion item") {
555-
try await sourcekitd.send(req, timeout: timeout, fileContents: nil)
567+
try await sourcekitd.send(req, timeout: timeout, restartTimeout: restartTimeout, fileContents: nil)
556568
}
557569
if let docString: String = documentationResponse?[sourcekitd.keys.docBrief] {
558570
item.documentation = .markupContent(MarkupContent(kind: .markdown, value: docString))

‎Sources/SourceKitLSP/Swift/DiagnosticReportManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ actor DiagnosticReportManager {
124124
dict = try await self.sourcekitd.send(
125125
skreq,
126126
timeout: options.sourcekitdRequestTimeoutOrDefault,
127+
restartTimeout: options.semanticServiceRestartTimeoutOrDefault,
127128
fileContents: snapshot.text
128129
)
129130
} catch SKDError.requestFailed(let sourcekitdError) {

‎Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ package actor SwiftLanguageService: LanguageService, Sendable {
101101

102102
private let sourcekitdPath: URL
103103

104-
let sourcekitd: SourceKitD
104+
package let sourcekitd: SourceKitD
105105

106106
/// Path to the swift-format executable if it exists in the toolchain.
107107
let swiftFormat: URL?
@@ -307,6 +307,7 @@ package actor SwiftLanguageService: LanguageService, Sendable {
307307
try await sourcekitd.send(
308308
request,
309309
timeout: options.sourcekitdRequestTimeoutOrDefault,
310+
restartTimeout: options.semanticServiceRestartTimeoutOrDefault,
310311
fileContents: fileContents
311312
)
312313
}

‎Sources/SwiftExtensions/AsyncUtils.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,30 @@ extension Collection where Element: Sendable {
181181
package struct TimeoutError: Error, CustomStringConvertible {
182182
package var description: String { "Timed out" }
183183

184+
package let handle: TimeoutHandle?
185+
186+
package init(handle: TimeoutHandle?) {
187+
self.handle = handle
188+
}
189+
}
190+
191+
package final class TimeoutHandle: Equatable, Sendable {
184192
package init() {}
193+
194+
static package func == (_ lhs: TimeoutHandle, _ rhs: TimeoutHandle) -> Bool {
195+
return lhs === rhs
196+
}
185197
}
186198

187-
/// Executes `body`. If it doesn't finish after `duration`, throws a `TimeoutError`.
199+
/// Executes `body`. If it doesn't finish after `duration`, throws a `TimeoutError` and cancels `body`.
200+
///
201+
/// `TimeoutError` is thrown immediately an the function does not wait for `body` to honor the cancellation.
202+
///
203+
/// If a `handle` is passed in and this `withTimeout` call times out, the thrown `TimeoutError` contains this handle.
204+
/// This way a caller can identify whether this call to `withTimeout` timed out or if a nested call timed out.
188205
package func withTimeout<T: Sendable>(
189206
_ duration: Duration,
207+
handle: TimeoutHandle? = nil,
190208
_ body: @escaping @Sendable () async throws -> T
191209
) async throws -> T {
192210
// Get the priority with which to launch the body task here so that we can pass the same priority as the initial
@@ -207,7 +225,7 @@ package func withTimeout<T: Sendable>(
207225

208226
let timeoutTask = Task(priority: priority) {
209227
try await Task.sleep(for: duration)
210-
continuation.yield(with: .failure(TimeoutError()))
228+
continuation.yield(with: .failure(TimeoutError(handle: handle)))
211229
bodyTask.cancel()
212230
}
213231
mutableTasks = [bodyTask, timeoutTask]

‎Tests/SourceKitDTests/SourceKitDTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ final class SourceKitDTests: XCTestCase {
8484
keys.compilerArgs: args,
8585
])
8686

87-
_ = try await sourcekitd.send(req, timeout: defaultTimeoutDuration, fileContents: nil)
87+
_ = try await sourcekitd.send(req, timeout: defaultTimeoutDuration)
8888

8989
try await fulfillmentOfOrThrow(expectation1, expectation2)
9090

9191
let close = sourcekitd.dictionary([
9292
keys.request: sourcekitd.requests.editorClose,
9393
keys.name: path,
9494
])
95-
_ = try await sourcekitd.send(close, timeout: defaultTimeoutDuration, fileContents: nil)
95+
_ = try await sourcekitd.send(close, timeout: defaultTimeoutDuration)
9696
}
9797
}
9898

‎Tests/SourceKitLSPTests/ClangdTests.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import LanguageServerProtocol
14+
import SKLogging
15+
import SKOptions
1416
import SKTestSupport
17+
import SourceKitLSP
18+
import SwiftExtensions
19+
import TSCBasic
1520
import XCTest
1621

1722
final class ClangdTests: XCTestCase {
@@ -127,4 +132,58 @@ final class ClangdTests: XCTestCase {
127132
}
128133
}
129134
}
135+
136+
func testRestartClangdIfItDoesntReply() async throws {
137+
// We simulate clangd not replying until it is restarted using a hook.
138+
let clangdRestarted = AtomicBool(initialValue: false)
139+
let clangdRestartedExpectation = self.expectation(description: "clangd restarted")
140+
let hooks = Hooks(preForwardRequestToClangd: { request in
141+
if !clangdRestarted.value {
142+
try? await Task.sleep(for: .seconds(60 * 60))
143+
}
144+
})
145+
146+
let testClient = try await TestSourceKitLSPClient(
147+
options: SourceKitLSPOptions(semanticServiceRestartTimeout: 1),
148+
hooks: hooks
149+
)
150+
let uri = DocumentURI(for: .c)
151+
let positions = testClient.openDocument(
152+
"""
153+
void test() {
154+
int x1️⃣;
155+
}
156+
""",
157+
uri: uri
158+
)
159+
160+
// Monitor clangd to notice when it gets restarted
161+
let clangdServer = try await unwrap(
162+
testClient.server.languageService(for: uri, .c, in: unwrap(testClient.server.workspaceForDocument(uri: uri)))
163+
)
164+
await clangdServer.addStateChangeHandler { oldState, newState in
165+
if oldState == .connectionInterrupted, newState == .connected {
166+
clangdRestarted.value = true
167+
clangdRestartedExpectation.fulfill()
168+
}
169+
}
170+
171+
// The first hover request should get cancelled by `semanticServiceRestartTimeout`
172+
await assertThrowsError(
173+
try await testClient.send(HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"]))
174+
) { error in
175+
XCTAssert(
176+
(error as? ResponseError)?.message.contains("Timed out") ?? false,
177+
"Received unexpected error: \(error)"
178+
)
179+
}
180+
181+
try await fulfillmentOfOrThrow(clangdRestartedExpectation)
182+
183+
// After clangd gets restarted
184+
let hover = try await testClient.send(
185+
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
186+
)
187+
assertContains(hover?.contents.markupContent?.value ?? "", "Type: int")
188+
}
130189
}

‎Tests/SourceKitDTests/CrashRecoveryTests.swift renamed to ‎Tests/SourceKitLSPTests/CrashRecoveryTests.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import LanguageServerProtocol
1414
import LanguageServerProtocolExtensions
1515
import SKLogging
16+
import SKOptions
1617
import SKTestSupport
1718
import SourceKitD
1819
@_spi(Testing) import SourceKitLSP
@@ -349,4 +350,70 @@ final class CrashRecoveryTests: XCTestCase {
349350
return true
350351
}
351352
}
353+
354+
func testRestartSourceKitDIfItDoesntReply() async throws {
355+
try SkipUnless.longTestsEnabled()
356+
try SkipUnless.platformIsDarwin("Linux and Windows use in-process sourcekitd")
357+
358+
let sourcekitdTerminatedExpectation = self.expectation(description: "sourcekitd terminated")
359+
360+
let testClient = try await TestSourceKitLSPClient(options: SourceKitLSPOptions(semanticServiceRestartTimeout: 2))
361+
let uri = DocumentURI(for: .swift)
362+
let positions = testClient.openDocument(
363+
"""
364+
func test() {
365+
let 1️⃣x = 1
366+
}
367+
""",
368+
uri: uri
369+
)
370+
371+
// Monitor sourcekitd to notice when it gets terminated
372+
let swiftService = try await unwrap(
373+
testClient.server.languageService(for: uri, .swift, in: unwrap(testClient.server.workspaceForDocument(uri: uri)))
374+
as? SwiftLanguageService
375+
)
376+
await swiftService.addStateChangeHandler { oldState, newState in
377+
logger.debug("sourcekitd changed state: \(String(describing: oldState)) -> \(String(describing: newState))")
378+
if newState == .connectionInterrupted {
379+
sourcekitdTerminatedExpectation.fulfill()
380+
}
381+
}
382+
383+
try await swiftService.sourcekitd.withPreRequestHandlingHook {
384+
// The first hover request should get cancelled by `semanticServiceRestartTimeout`
385+
await assertThrowsError(
386+
try await testClient.send(HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"]))
387+
) { error in
388+
XCTAssert(
389+
(error as? ResponseError)?.message.contains("Timed out") ?? false,
390+
"Received unexpected error: \(error)"
391+
)
392+
}
393+
} hook: { request in
394+
// Simulate a stuck sourcekitd that only gets unstuck when sourcekitd is terminated.
395+
if request.description.contains("cursorinfo") {
396+
// Use a detached task here so that a cancellation of the Task that runs this doesn't cancel the await of
397+
// sourcekitdTerminatedExpectation. We want to simulate a sourecekitd that is stuck and doesn't listen to
398+
// cancellation.
399+
await Task.detached {
400+
await orLog("awaiting sourcekitdTerminatedExpectation") {
401+
try await fulfillmentOfOrThrow(sourcekitdTerminatedExpectation)
402+
}
403+
}.value
404+
}
405+
}
406+
407+
// After sourcekitd is restarted, we should get a hover result once the semantic editor is enabled again.
408+
try await repeatUntilExpectedResult {
409+
do {
410+
let hover = try await testClient.send(
411+
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
412+
)
413+
return hover?.contents.markupContent?.value != nil
414+
} catch {
415+
return false
416+
}
417+
}
418+
}
352419
}

‎Tests/SwiftSourceKitPluginTests/SwiftSourceKitPluginTests.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,7 +1830,7 @@ fileprivate extension SourceKitD {
18301830
keys.syntacticOnly: 1,
18311831
keys.compilerArgs: compilerArguments as [SKDRequestValue],
18321832
])
1833-
_ = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1833+
_ = try await send(req, timeout: defaultTimeoutDuration)
18341834
return DocumentPositions(markers: markers, textWithoutMarkers: textWithoutMarkers)
18351835
}
18361836

@@ -1844,7 +1844,7 @@ fileprivate extension SourceKitD {
18441844
keys.syntacticOnly: 1,
18451845
])
18461846

1847-
_ = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1847+
_ = try await send(req, timeout: defaultTimeoutDuration)
18481848
}
18491849

18501850
nonisolated func closeDocument(_ name: String) async throws {
@@ -1853,7 +1853,7 @@ fileprivate extension SourceKitD {
18531853
keys.name: name,
18541854
])
18551855

1856-
_ = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1856+
_ = try await send(req, timeout: defaultTimeoutDuration)
18571857
}
18581858

18591859
nonisolated func completeImpl(
@@ -1888,7 +1888,7 @@ fileprivate extension SourceKitD {
18881888
keys.compilerArgs: compilerArguments as [SKDRequestValue]?,
18891889
])
18901890

1891-
let res = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1891+
let res = try await send(req, timeout: defaultTimeoutDuration)
18921892
return try CompletionResultSet(res)
18931893
}
18941894

@@ -1946,7 +1946,7 @@ fileprivate extension SourceKitD {
19461946
keys.codeCompleteOptions: dictionary([keys.useNewAPI: 1]),
19471947
])
19481948

1949-
_ = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1949+
_ = try await send(req, timeout: defaultTimeoutDuration)
19501950
}
19511951

19521952
nonisolated func completeDocumentation(id: Int) async throws -> CompletionDocumentation {
@@ -1955,7 +1955,7 @@ fileprivate extension SourceKitD {
19551955
keys.identifier: id,
19561956
])
19571957

1958-
let resp = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1958+
let resp = try await send(req, timeout: defaultTimeoutDuration)
19591959
return CompletionDocumentation(resp)
19601960
}
19611961

@@ -1964,7 +1964,7 @@ fileprivate extension SourceKitD {
19641964
keys.request: requests.codeCompleteDiagnostic,
19651965
keys.identifier: id,
19661966
])
1967-
let resp = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1967+
let resp = try await send(req, timeout: defaultTimeoutDuration)
19681968

19691969
return CompletionDiagnostic(resp)
19701970
}
@@ -1973,7 +1973,7 @@ fileprivate extension SourceKitD {
19731973
let req = dictionary([
19741974
keys.request: requests.dependencyUpdated
19751975
])
1976-
_ = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1976+
_ = try await send(req, timeout: defaultTimeoutDuration)
19771977
}
19781978

19791979
nonisolated func setPopularAPI(popular: [String], unpopular: [String]) async throws {
@@ -1984,7 +1984,7 @@ fileprivate extension SourceKitD {
19841984
keys.unpopular: unpopular as [SKDRequestValue],
19851985
])
19861986

1987-
let resp = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
1987+
let resp = try await send(req, timeout: defaultTimeoutDuration)
19881988
XCTAssertEqual(resp[keys.useNewAPI], 1)
19891989
}
19901990

@@ -2001,7 +2001,7 @@ fileprivate extension SourceKitD {
20012001
keys.notoriousModules: notoriousModules as [SKDRequestValue],
20022002
])
20032003

2004-
let resp = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
2004+
let resp = try await send(req, timeout: defaultTimeoutDuration)
20052005
XCTAssertEqual(resp[keys.useNewAPI], 1)
20062006
}
20072007

@@ -2025,7 +2025,7 @@ fileprivate extension SourceKitD {
20252025
keys.modulePopularity: modulePopularity as [SKDRequestValue],
20262026
])
20272027

2028-
let resp = try await send(req, timeout: defaultTimeoutDuration, fileContents: nil)
2028+
let resp = try await send(req, timeout: defaultTimeoutDuration)
20292029
XCTAssertEqual(resp[keys.useNewAPI], 1)
20302030
}
20312031

‎config.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@
181181
},
182182
"type" : "object"
183183
},
184+
"semanticServiceRestartTimeout" : {
185+
"description" : "If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it.",
186+
"markdownDescription" : "If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it.",
187+
"type" : "number"
188+
},
184189
"sourcekitd" : {
185190
"description" : "Options modifying the behavior of sourcekitd.",
186191
"markdownDescription" : "Options modifying the behavior of sourcekitd.",

0 commit comments

Comments
 (0)
Please sign in to comment.