Skip to content

Commit b91f72f

Browse files
authored
Merge pull request #1700 from ahoppen/build-settings-timeout
Use fallback build settings if build system doesn’t provide build settings within a timeout
2 parents 5f6e58a + 78217ec commit b91f72f

File tree

9 files changed

+157
-19
lines changed

9 files changed

+157
-19
lines changed

Documentation/Configuration File.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ The structure of the file is currently not guaranteed to be stable. Options may
3131
- `cxxCompilerFlags: string[]`: Extra arguments passed to the compiler for C++ files
3232
- `swiftCompilerFlags: string[]`: Extra arguments passed to the compiler for Swift files
3333
- `sdk: string`: The SDK to use for fallback arguments. Default is to infer the SDK using `xcrun`.
34+
- `buildSettingsTimeout: int`: Number of milliseconds to wait for build settings from the build system before using fallback build settings.
3435
- `clangdOptions: string[]`: Extra command line arguments passed to `clangd` when launching it
3536
- `index`: Dictionary with the following keys, defining options related to indexing
3637
- `indexStorePath: string`: Directory in which a separate compilation stores the index store. By default, inferred from the build system.

Sources/BuildSystemIntegration/BuildSystemManager.swift

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ private extension BuildSystemKind {
137137
private static func createBuiltInBuildSystemAdapter(
138138
projectRoot: AbsolutePath,
139139
messagesToSourceKitLSPHandler: any MessageHandler,
140+
buildSystemTestHooks: BuildSystemTestHooks,
140141
_ createBuildSystem: @Sendable (_ connectionToSourceKitLSP: any Connection) async throws -> BuiltInBuildSystem?
141142
) async -> BuildSystemAdapter? {
142143
let connectionToSourceKitLSP = LocalConnection(receiverName: "BuildSystemManager")
@@ -152,7 +153,8 @@ private extension BuildSystemKind {
152153
logger.log("Created \(type(of: buildSystem), privacy: .public) at \(projectRoot.pathString)")
153154
let buildSystemAdapter = BuiltInBuildSystemAdapter(
154155
underlyingBuildSystem: buildSystem,
155-
connectionToSourceKitLSP: connectionToSourceKitLSP
156+
connectionToSourceKitLSP: connectionToSourceKitLSP,
157+
buildSystemTestHooks: buildSystemTestHooks
156158
)
157159
let connectionToBuildSystem = LocalConnection(receiverName: "Build system")
158160
connectionToBuildSystem.start(handler: buildSystemAdapter)
@@ -184,7 +186,8 @@ private extension BuildSystemKind {
184186
case .compilationDatabase(projectRoot: let projectRoot):
185187
return await Self.createBuiltInBuildSystemAdapter(
186188
projectRoot: projectRoot,
187-
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
189+
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
190+
buildSystemTestHooks: testHooks
188191
) { connectionToSourceKitLSP in
189192
CompilationDatabaseBuildSystem(
190193
projectRoot: projectRoot,
@@ -197,7 +200,8 @@ private extension BuildSystemKind {
197200
case .swiftPM(projectRoot: let projectRoot):
198201
return await Self.createBuiltInBuildSystemAdapter(
199202
projectRoot: projectRoot,
200-
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
203+
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
204+
buildSystemTestHooks: testHooks
201205
) { connectionToSourceKitLSP in
202206
try await SwiftPMBuildSystem(
203207
projectRoot: projectRoot,
@@ -210,7 +214,8 @@ private extension BuildSystemKind {
210214
case .testBuildSystem(projectRoot: let projectRoot):
211215
return await Self.createBuiltInBuildSystemAdapter(
212216
projectRoot: projectRoot,
213-
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler
217+
messagesToSourceKitLSPHandler: messagesToSourceKitLSPHandler,
218+
buildSystemTestHooks: testHooks
214219
) { connectionToSourceKitLSP in
215220
TestBuildSystem(projectRoot: projectRoot, connectionToSourceKitLSP: connectionToSourceKitLSP)
216221
}
@@ -399,7 +404,8 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
399404
)
400405
let adapter = BuiltInBuildSystemAdapter(
401406
underlyingBuildSystem: legacyBuildServer,
402-
connectionToSourceKitLSP: legacyBuildServer.connectionToSourceKitLSP
407+
connectionToSourceKitLSP: legacyBuildServer.connectionToSourceKitLSP,
408+
buildSystemTestHooks: buildSystemTestHooks
403409
)
404410
let connectionToBuildSystem = LocalConnection(receiverName: "Legacy BSP server")
405411
connectionToBuildSystem.start(handler: adapter)
@@ -705,14 +711,17 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
705711
in target: BuildTargetIdentifier?,
706712
language: Language
707713
) async -> FileBuildSettings? {
708-
do {
709-
if let target,
710-
let buildSettings = try await buildSettingsFromBuildSystem(for: document, in: target, language: language)
711-
{
712-
return buildSettings
714+
if let target {
715+
let buildSettingsFromBuildSystem = await orLog("Getting build settings") {
716+
try await withTimeout(options.buildSettingsTimeoutOrDefault) {
717+
try await self.buildSettingsFromBuildSystem(for: document, in: target, language: language)
718+
} resultReceivedAfterTimeout: {
719+
await self.delegate?.fileBuildSettingsChanged([document])
720+
}
721+
}
722+
if let buildSettingsFromBuildSystem {
723+
return buildSettingsFromBuildSystem
713724
}
714-
} catch {
715-
logger.error("Getting build settings failed: \(error.forLogging)")
716725
}
717726

718727
guard
@@ -746,8 +755,15 @@ package actor BuildSystemManager: QueueBasedMessageHandler {
746755
basedOn document: DocumentURI
747756
) async -> (mainFile: DocumentURI, settings: FileBuildSettings)? {
748757
let mainFile = await self.mainFile(for: document, language: language)
749-
let target = await canonicalTarget(for: mainFile)
750-
guard let settings = await buildSettings(for: mainFile, in: target, language: language) else {
758+
let settings = await orLog("Getting build settings") {
759+
let target = try await withTimeout(options.buildSettingsTimeoutOrDefault) {
760+
await self.canonicalTarget(for: mainFile)
761+
} resultReceivedAfterTimeout: {
762+
await self.delegate?.fileBuildSettingsChanged([document])
763+
}
764+
return await self.buildSettings(for: mainFile, in: target, language: language)
765+
}
766+
guard let settings else {
751767
return nil
752768
}
753769
return (mainFile, settings)

Sources/BuildSystemIntegration/BuildSystemTestHooks.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,21 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import LanguageServerProtocol
14+
1315
package struct BuildSystemTestHooks: Sendable {
1416
package var swiftPMTestHooks: SwiftPMTestHooks
1517

16-
package init(swiftPMTestHooks: SwiftPMTestHooks = SwiftPMTestHooks()) {
18+
/// A hook that will be executed before a request is handled by a `BuiltInBuildSystem`.
19+
///
20+
/// This allows requests to be artificially delayed.
21+
package var handleRequest: (@Sendable (any RequestType) async -> Void)?
22+
23+
package init(
24+
swiftPMTestHooks: SwiftPMTestHooks = SwiftPMTestHooks(),
25+
handleRequest: (@Sendable (any RequestType) async -> Void)? = nil
26+
) {
1727
self.swiftPMTestHooks = swiftPMTestHooks
28+
self.handleRequest = handleRequest
1829
}
1930
}

Sources/BuildSystemIntegration/BuiltInBuildSystemAdapter.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler {
5151
/// The connection with which messages are sent to `BuildSystemManager`.
5252
private let connectionToSourceKitLSP: LocalConnection
5353

54+
private let buildSystemTestHooks: BuildSystemTestHooks
55+
5456
/// If the underlying build system is a `TestBuildSystem`, return it. Otherwise, `nil`
5557
///
5658
/// - Important: For testing purposes only.
@@ -62,10 +64,12 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler {
6264
/// from the build system to SourceKit-LSP.
6365
init(
6466
underlyingBuildSystem: BuiltInBuildSystem,
65-
connectionToSourceKitLSP: LocalConnection
67+
connectionToSourceKitLSP: LocalConnection,
68+
buildSystemTestHooks: BuildSystemTestHooks
6669
) {
6770
self.underlyingBuildSystem = underlyingBuildSystem
6871
self.connectionToSourceKitLSP = connectionToSourceKitLSP
72+
self.buildSystemTestHooks = buildSystemTestHooks
6973
}
7074

7175
deinit {
@@ -102,6 +106,7 @@ actor BuiltInBuildSystemAdapter: QueueBasedMessageHandler {
102106
}
103107

104108
package func handleImpl<Request: RequestType>(_ request: RequestAndReply<Request>) async {
109+
await buildSystemTestHooks.handleRequest?(request.params)
105110
switch request {
106111
case let request as RequestAndReply<BuildShutdownRequest>:
107112
await request.reply { VoidResponse() }

Sources/SKOptions/SourceKitLSPOptions.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,13 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable, CustomLogString
241241
set { fallbackBuildSystem = newValue }
242242
}
243243

244+
/// Number of milliseconds to wait for build settings from the build system before using fallback build settings.
245+
public var buildSettingsTimeout: Int?
246+
public var buildSettingsTimeoutOrDefault: Duration {
247+
// The default timeout of 500ms was chosen arbitrarily without any measurements.
248+
get { .milliseconds(buildSettingsTimeout ?? 500) }
249+
}
250+
244251
public var clangdOptions: [String]?
245252

246253
private var index: IndexOptions?

Sources/SKTestSupport/WrappedSemaphore.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,16 @@ package struct WrappedSemaphore: Sendable {
5151
}
5252

5353
/// Wait for a signal and emit an XCTFail if the semaphore is not signaled within `timeout`.
54-
package func waitOrXCTFail(timeout: DispatchTime = DispatchTime.now() + .seconds(Int(defaultTimeout))) {
54+
package func waitOrXCTFail(
55+
timeout: DispatchTime = DispatchTime.now() + .seconds(Int(defaultTimeout)),
56+
file: StaticString = #filePath,
57+
line: UInt = #line
58+
) {
5559
switch self.wait(timeout: timeout) {
5660
case .success:
5761
break
5862
case .timedOut:
59-
XCTFail("\(name) timed out")
63+
XCTFail("\(name) timed out", file: file, line: line)
6064
}
6165
}
6266
}

Sources/SourceKitLSP/Swift/SwiftLanguageService.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,7 @@ extension SwiftLanguageService {
989989
)
990990
let snapshot = try await self.latestSnapshot(for: req.textDocument.uri)
991991
let buildSettings = await self.buildSettings(for: req.textDocument.uri)
992+
try Task.checkCancellation()
992993
let diagnosticReport = try await self.diagnosticReportManager.diagnosticReport(
993994
for: snapshot,
994995
buildSettings: buildSettings

Sources/SwiftExtensions/AsyncUtils.swift

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ extension Collection where Element: Sendable {
169169

170170
package struct TimeoutError: Error, CustomStringConvertible {
171171
package var description: String { "Timed out" }
172+
173+
package init() {}
172174
}
173175

174176
/// Executes `body`. If it doesn't finish after `duration`, throws a `TimeoutError`.
@@ -184,10 +186,56 @@ package func withTimeout<T: Sendable>(
184186
taskGroup.addTask {
185187
return try await body()
186188
}
187-
for try await value in taskGroup {
189+
defer {
188190
taskGroup.cancelAll()
191+
}
192+
for try await value in taskGroup {
189193
return value
190194
}
191195
throw CancellationError()
192196
}
193197
}
198+
199+
/// Executes `body`. If it doesn't finish after `duration`, return `nil` and continue running body. When `body` returns
200+
/// a value after the timeout, `resultReceivedAfterTimeout` is called.
201+
///
202+
/// - Important: `body` will not be cancelled when the timeout is received. Use the other overload of `withTimeout` if
203+
/// `body` should be cancelled after `timeout`.
204+
package func withTimeout<T: Sendable>(
205+
_ timeout: Duration,
206+
body: @escaping @Sendable () async throws -> T?,
207+
resultReceivedAfterTimeout: @escaping @Sendable () async -> Void
208+
) async throws -> T? {
209+
let didHitTimeout = AtomicBool(initialValue: false)
210+
211+
let stream = AsyncThrowingStream<T?, Error> { continuation in
212+
Task {
213+
try await Task.sleep(for: timeout)
214+
didHitTimeout.value = true
215+
continuation.yield(nil)
216+
}
217+
218+
Task {
219+
do {
220+
let result = try await body()
221+
if didHitTimeout.value {
222+
await resultReceivedAfterTimeout()
223+
}
224+
continuation.yield(result)
225+
} catch {
226+
continuation.yield(with: .failure(error))
227+
}
228+
}
229+
}
230+
231+
for try await value in stream {
232+
return value
233+
}
234+
// The only reason for the loop above to terminate is if the Task got cancelled or if the continuation finishes
235+
// (which it never does).
236+
if Task.isCancelled {
237+
throw CancellationError()
238+
} else {
239+
preconditionFailure("Continuation never finishes")
240+
}
241+
}

Tests/BuildSystemIntegrationTests/FallbackBuildSettingsTests.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import BuildServerProtocol
1314
@_spi(Testing) import BuildSystemIntegration
1415
import LanguageServerProtocol
1516
import SKOptions
1617
import SKTestSupport
18+
import SourceKitLSP
1719
import TSCBasic
1820
import XCTest
1921

@@ -160,4 +162,47 @@ final class FallbackBuildSystemTests: XCTestCase {
160162

161163
XCTAssertNil(fallbackBuildSettings(for: source, language: Language(rawValue: "unknown"), options: .init()))
162164
}
165+
166+
func testFallbackBuildSettingsWhileBuildSystemIsComputingBuildSettings() async throws {
167+
let fallbackResultsReceived = WrappedSemaphore(name: "Fallback results received")
168+
let project = try await SwiftPMTestProject(
169+
files: [
170+
"Test.swift": """
171+
let x: 1️⃣String = 1
172+
"""
173+
],
174+
testHooks: TestHooks(
175+
buildSystemTestHooks: BuildSystemTestHooks(
176+
handleRequest: { request in
177+
if request is TextDocumentSourceKitOptionsRequest {
178+
fallbackResultsReceived.waitOrXCTFail()
179+
}
180+
}
181+
)
182+
)
183+
)
184+
185+
let (uri, positions) = try project.openDocument("Test.swift")
186+
187+
let diagsBeforeBuildSettings = try await project.testClient.send(
188+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
189+
)
190+
XCTAssertEqual(diagsBeforeBuildSettings.fullReport?.items, [])
191+
192+
let hoverBeforeBuildSettings = try await project.testClient.send(
193+
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
194+
)
195+
XCTAssertNotNil(hoverBeforeBuildSettings?.contents)
196+
197+
fallbackResultsReceived.signal()
198+
199+
try await repeatUntilExpectedResult {
200+
let diagsAfterBuildSettings = try await project.testClient.send(
201+
DocumentDiagnosticsRequest(textDocument: TextDocumentIdentifier(uri))
202+
)
203+
return diagsAfterBuildSettings.fullReport?.items.map(\.message) == [
204+
"Cannot convert value of type 'Int' to specified type 'String'"
205+
]
206+
}
207+
}
163208
}

0 commit comments

Comments
 (0)