Skip to content

Commit d324d71

Browse files
authored
Merge pull request #1500 from ahoppen/fix-index-cancellation
Send `SIGKILL` to `swift-frontend` indexing processes
2 parents 46cc2e6 + 2ba180b commit d324d71

File tree

6 files changed

+90
-17
lines changed

6 files changed

+90
-17
lines changed

Sources/SKSupport/Process+Run.swift

+21-3
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

+8
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
@@ -522,6 +529,7 @@ public final actor SemanticIndexManager {
522529
indexStoreUpToDateTracker: indexStoreUpToDateTracker,
523530
indexFilesWithUpToDateUnit: indexFilesWithUpToDateUnit,
524531
logMessageToIndexLog: logMessageToIndexLog,
532+
timeout: updateIndexStoreTimeout,
525533
testHooks: testHooks
526534
)
527535
)

Sources/SemanticIndex/UpdateIndexStoreTaskDescription.swift

+22-12
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

@@ -120,6 +121,11 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
120121
/// See `SemanticIndexManager.logMessageToIndexLog`.
121122
private let logMessageToIndexLog: @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void
122123

124+
/// How long to wait until we cancel an update indexstore task. This timeout should be long enough that all
125+
/// `swift-frontend` tasks finish within it. It prevents us from blocking the index if the type checker gets stuck on
126+
/// an expression for a long time.
127+
private let timeout: Duration
128+
123129
/// Test hooks that should be called when the index task finishes.
124130
private let testHooks: IndexTestHooks
125131

@@ -147,6 +153,7 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
147153
indexStoreUpToDateTracker: UpToDateTracker<DocumentURI>,
148154
indexFilesWithUpToDateUnit: Bool,
149155
logMessageToIndexLog: @escaping @Sendable (_ taskID: IndexTaskID, _ message: String) -> Void,
156+
timeout: Duration,
150157
testHooks: IndexTestHooks
151158
) {
152159
self.filesToIndex = filesToIndex
@@ -155,6 +162,7 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
155162
self.indexStoreUpToDateTracker = indexStoreUpToDateTracker
156163
self.indexFilesWithUpToDateUnit = indexFilesWithUpToDateUnit
157164
self.logMessageToIndexLog = logMessageToIndexLog
165+
self.timeout = timeout
158166
self.testHooks = testHooks
159167
}
160168

@@ -366,19 +374,21 @@ public struct UpdateIndexStoreTaskDescription: IndexTaskDescription {
366374
let stdoutHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
367375
let stderrHandler = PipeAsStringHandler { logMessageToIndexLog(logID, $0) }
368376

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

Sources/SourceKitLSP/Swift/DocumentFormatting.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ extension SwiftLanguageService {
144144
writeStream.send(snapshot.text)
145145
try writeStream.close()
146146

147-
let result = try await process.waitUntilExitSendingSigIntOnTaskCancellation()
147+
let result = try await process.waitUntilExitStoppingProcessOnTaskCancellation()
148148
guard result.exitStatus == .terminated(code: 0) else {
149149
let swiftFormatErrorMessage: String
150150
switch result.stderrOutput {

Sources/SourceKitLSP/Workspace.swift

+9-1
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

+29
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,35 @@ final class BackgroundIndexingTests: XCTestCase {
13441344
)
13451345
XCTAssertEqual(callHierarchyAfterReindex, [expectedCallHierarchyItem])
13461346
}
1347+
1348+
func testCancelIndexing() async throws {
1349+
try await SkipUnless.swiftPMSupportsExperimentalPrepareForIndexing()
1350+
try SkipUnless.longTestsEnabled()
1351+
1352+
var serverOptions = SourceKitLSPServer.Options.testDefault
1353+
serverOptions.experimentalFeatures.insert(.swiftpmPrepareForIndexing)
1354+
serverOptions.indexOptions.updateIndexStoreTimeout = .seconds(1)
1355+
1356+
let dateStarted = Date()
1357+
_ = try await SwiftPMTestProject(
1358+
files: [
1359+
"Test.swift": """
1360+
func slow(x: Invalid1, y: Invalid2) {
1361+
x / y / x / y / x / y / x / y
1362+
}
1363+
"""
1364+
],
1365+
serverOptions: serverOptions,
1366+
enableBackgroundIndexing: true
1367+
)
1368+
// Creating the `SwiftPMTestProject` implicitly waits for background indexing to finish.
1369+
// Preparation of `Test.swift` should finish instantly because it doesn't type check the function body.
1370+
// Type-checking the body relies on rdar://80582770, which makes the line hard to type check. We should hit the
1371+
// timeout of 1s. Adding another 2s to escalate a SIGINT (to which swift-frontend doesn't respond) to a SIGKILL mean
1372+
// that the initial indexing should be done in ~3s. 30s should be enough to always finish within this time while
1373+
// also testing that we don't wait for type checking of Test.swift to finish.
1374+
XCTAssert(Date().timeIntervalSince(dateStarted) < 30)
1375+
}
13471376
}
13481377

13491378
extension HoverResponseContents {

0 commit comments

Comments
 (0)