Skip to content

Commit f33ea39

Browse files
authored
Merge pull request #1525 from ahoppen/6.0/fix-index-cancellation
[6.0] Send `SIGKILL` to `swift-frontend` indexing processes
2 parents 252d5be + 2bcd145 commit f33ea39

File tree

6 files changed

+90
-17
lines changed

6 files changed

+90
-17
lines changed

Sources/SKSupport/Process+Run.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,30 @@ import WinSDK
2626

2727
extension Process {
2828
/// Wait for the process to exit. If the task gets cancelled, during this time, send a `SIGINT` to the process.
29+
/// Should the process not terminate on SIGINT after 2 seconds, it is killed using `SIGKILL`.
2930
@discardableResult
30-
public func waitUntilExitSendingSigIntOnTaskCancellation() async throws -> ProcessResult {
31+
public func waitUntilExitStoppingProcessOnTaskCancellation() async throws -> ProcessResult {
32+
let hasExited = AtomicBool(initialValue: false)
3133
return try await withTaskCancellationHandler {
32-
try await waitUntilExit()
34+
defer {
35+
hasExited.value = true
36+
}
37+
return try await waitUntilExit()
3338
} onCancel: {
3439
signal(SIGINT)
40+
Task {
41+
// Give the process 2 seconds to react to a SIGINT. If that doesn't work, kill the process.
42+
try await Task.sleep(for: .seconds(2))
43+
if !hasExited.value {
44+
#if os(Windows)
45+
// Windows does not define SIGKILL. Process.signal sends a `terminate` to the underlying Foundation process
46+
// for any signal that is not SIGINT. Use `SIGABRT` to terminate the process.
47+
signal(SIGABRT)
48+
#else
49+
signal(SIGKILL)
50+
#endif
51+
}
52+
}
3553
}
3654
}
3755

@@ -138,7 +156,7 @@ extension Process {
138156
)
139157
return try await withTaskPriorityChangedHandler(initialPriority: Task.currentPriority) { @Sendable in
140158
setProcessPriority(pid: process.processID, newPriority: Task.currentPriority)
141-
return try await process.waitUntilExitSendingSigIntOnTaskCancellation()
159+
return try await process.waitUntilExitStoppingProcessOnTaskCancellation()
142160
} taskPriorityChanged: {
143161
setProcessPriority(pid: process.processID, newPriority: Task.currentPriority)
144162
}

Sources/SemanticIndex/SemanticIndexManager.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ public final actor SemanticIndexManager {
133133
/// The build system manager that is used to get compiler arguments for a file.
134134
private let buildSystemManager: BuildSystemManager
135135

136+
/// How long to wait until we cancel an update indexstore task. This timeout should be long enough that all
137+
/// `swift-frontend` tasks finish within it. It prevents us from blocking the index if the type checker gets stuck on
138+
/// an expression for a long time.
139+
public var updateIndexStoreTimeout: Duration
140+
136141
private let testHooks: IndexTestHooks
137142

138143
/// The task to generate the build graph (resolving package dependencies, generating the build description,
@@ -209,6 +214,7 @@ public final actor SemanticIndexManager {
209214
public init(
210215
index: UncheckedIndex,
211216
buildSystemManager: BuildSystemManager,
217+
updateIndexStoreTimeout: Duration,
212218
testHooks: IndexTestHooks,
213219
indexTaskScheduler: TaskScheduler<AnyIndexTaskDescription>,
214220
logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void,
@@ -217,6 +223,7 @@ public final actor SemanticIndexManager {
217223
) {
218224
self.index = index
219225
self.buildSystemManager = buildSystemManager
226+
self.updateIndexStoreTimeout = updateIndexStoreTimeout
220227
self.testHooks = testHooks
221228
self.indexTaskScheduler = indexTaskScheduler
222229
self.logMessageToIndexLog = logMessageToIndexLog
@@ -510,6 +517,7 @@ public final actor SemanticIndexManager {
510517
index: index,
511518
indexStoreUpToDateTracker: indexStoreUpToDateTracker,
512519
logMessageToIndexLog: logMessageToIndexLog,
520+
timeout: updateIndexStoreTimeout,
513521
testHooks: testHooks
514522
)
515523
)

Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import SwiftExtensions
1919

2020
import struct TSCBasic.AbsolutePath
2121
import class TSCBasic.Process
22+
import struct TSCBasic.ProcessResult
2223

2324
private let updateIndexStoreIDForLogging = AtomicUInt32(initialValue: 1)
2425

@@ -114,6 +115,11 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
114115
/// See `SemanticIndexManager.logMessageToIndexLog`.
115116
private let logMessageToIndexLog: @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void
116117

118+
/// How long to wait until we cancel an update indexstore task. This timeout should be long enough that all
119+
/// `swift-frontend` tasks finish within it. It prevents us from blocking the index if the type checker gets stuck on
120+
/// an expression for a long time.
121+
private let timeout: Duration
122+
117123
/// Test hooks that should be called when the index task finishes.
118124
private let testHooks: IndexTestHooks
119125

@@ -140,13 +146,15 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
140146
index: UncheckedIndex,
141147
indexStoreUpToDateTracker: UpToDateTracker<DocumentURI>,
142148
logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void,
149+
timeout: Duration,
143150
testHooks: IndexTestHooks
144151
) {
145152
self.filesToIndex = filesToIndex
146153
self.buildSystemManager = buildSystemManager
147154
self.index = index
148155
self.indexStoreUpToDateTracker = indexStoreUpToDateTracker
149156
self.logMessageToIndexLog = logMessageToIndexLog
157+
self.timeout = timeout
150158
self.testHooks = testHooks
151159
}
152160

@@ -356,19 +364,21 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
356364
let stdoutHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
357365
let stderrHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
358366

359-
// Time out updating of the index store after 2 minutes. We don't expect any single file compilation to take longer
360-
// than 2 minutes in practice, so this indicates that the compiler has entered a loop and we probably won't make any
361-
// progress here. We will try indexing the file again when it is edited or when the project is re-opened.
362-
// 2 minutes have been chosen arbitrarily.
363-
let result = try await withTimeout(.seconds(120)) {
364-
try await Process.run(
365-
arguments: processArguments,
366-
workingDirectory: workingDirectory,
367-
outputRedirection: .stream(
368-
stdout: { stdoutHandler.handleDataFromPipe(Data($0)) },
369-
stderr: { stderrHandler.handleDataFromPipe(Data($0)) }
367+
let result: ProcessResult
368+
do {
369+
result = try await withTimeout(timeout) {
370+
try await Process.run(
371+
arguments: processArguments,
372+
workingDirectory: workingDirectory,
373+
outputRedirection: .stream(
374+
stdout: { stdoutHandler.handleDataFromPipe(Data($0)) },
375+
stderr: { stderrHandler.handleDataFromPipe(Data($0)) }
376+
)
370377
)
371-
)
378+
}
379+
} catch {
380+
logMessageToIndexLog(logID, "Finished error in \(start.duration(to: .now)): \(error)")
381+
throw error
372382
}
373383
let exitStatus = result.exitStatus.exhaustivelySwitchable
374384
logMessageToIndexLog(logID, "Finished with \(exitStatus.description) in \(start.duration(to: .now))")

Sources/SourceKitLSP/Swift/DocumentFormatting.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ extension SwiftLanguageService {
152152
writeStream.send(snapshot.text)
153153
try writeStream.close()
154154

155-
let result = try await process.waitUntilExitSendingSigIntOnTaskCancellation()
155+
let result = try await process.waitUntilExitStoppingProcessOnTaskCancellation()
156156
guard result.exitStatus == .terminated(code: 0) else {
157157
let swiftFormatErrorMessage: String
158158
switch result.stderrOutput {

Sources/SourceKitLSP/Workspace.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public final class Workspace: Sendable {
116116
self.semanticIndexManager = SemanticIndexManager(
117117
index: uncheckedIndex,
118118
buildSystemManager: buildSystemManager,
119+
updateIndexStoreTimeout: options.indexOptions.updateIndexStoreTimeout,
119120
testHooks: options.indexTestHooks,
120121
indexTaskScheduler: indexTaskScheduler,
121122
logMessageToIndexLog: logMessageToIndexLog,
@@ -275,17 +276,24 @@ public struct IndexOptions: Sendable {
275276
/// Setting this to a value < 1 ensures that background indexing doesn't use all CPU resources.
276277
public var maxCoresPercentageToUseForBackgroundIndexing: Double
277278

279+
/// How long to wait until we cancel an update indexstore task. This timeout should be long enough that all
280+
/// `swift-frontend` tasks finish within it. It prevents us from blocking the index if the type checker gets stuck on
281+
/// an expression for a long time.
282+
public var updateIndexStoreTimeout: Duration
283+
278284
public init(
279285
indexStorePath: AbsolutePath? = nil,
280286
indexDatabasePath: AbsolutePath? = nil,
281287
indexPrefixMappings: [PathPrefixMapping]? = nil,
282288
listenToUnitEvents: Bool = true,
283-
maxCoresPercentageToUseForBackgroundIndexing: Double = 1
289+
maxCoresPercentageToUseForBackgroundIndexing: Double = 1,
290+
updateIndexStoreTimeout: Duration = .seconds(120)
284291
) {
285292
self.indexStorePath = indexStorePath
286293
self.indexDatabasePath = indexDatabasePath
287294
self.indexPrefixMappings = indexPrefixMappings
288295
self.listenToUnitEvents = listenToUnitEvents
289296
self.maxCoresPercentageToUseForBackgroundIndexing = maxCoresPercentageToUseForBackgroundIndexing
297+
self.updateIndexStoreTimeout = updateIndexStoreTimeout
290298
}
291299
}

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,35 @@ final class BackgroundIndexingTests: XCTestCase {
12461246
)
12471247
_ = try await project.testClient.send(PollIndexRequest())
12481248
}
1249+
1250+
func testCancelIndexing() async throws {
1251+
try await SkipUnless.swiftPMSupportsExperimentalPrepareForIndexing()
1252+
try SkipUnless.longTestsEnabled()
1253+
1254+
var serverOptions = SourceKitLSPServer.Options.testDefault
1255+
serverOptions.experimentalFeatures.insert(.swiftpmPrepareForIndexing)
1256+
serverOptions.indexOptions.updateIndexStoreTimeout = .seconds(1)
1257+
1258+
let dateStarted = Date()
1259+
_ = try await SwiftPMTestProject(
1260+
files: [
1261+
"Test.swift": """
1262+
func slow(x: Invalid1, y: Invalid2) {
1263+
x / y / x / y / x / y / x / y
1264+
}
1265+
"""
1266+
],
1267+
serverOptions: serverOptions,
1268+
enableBackgroundIndexing: true
1269+
)
1270+
// Creating the `SwiftPMTestProject` implicitly waits for background indexing to finish.
1271+
// Preparation of `Test.swift` should finish instantly because it doesn't type check the function body.
1272+
// Type-checking the body relies on rdar://80582770, which makes the line hard to type check. We should hit the
1273+
// timeout of 1s. Adding another 2s to escalate a SIGINT (to which swift-frontend doesn't respond) to a SIGKILL mean
1274+
// that the initial indexing should be done in ~3s. 30s should be enough to always finish within this time while
1275+
// also testing that we don't wait for type checking of Test.swift to finish.
1276+
XCTAssert(Date().timeIntervalSince(dateStarted) < 30)
1277+
}
12491278
}
12501279

12511280
extension HoverResponseContents {

0 commit comments

Comments
 (0)