Skip to content

Commit b41f6af

Browse files
committed
Avoid a potential race condition in which a sourcekitd/clangd request wouldn't get cancelled
1 parent 71dfd48 commit b41f6af

File tree

3 files changed

+49
-12
lines changed

3 files changed

+49
-12
lines changed

Sources/LanguageServerProtocol/Connection.swift

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,18 +157,32 @@ extension Connection {
157157
/// use the version with a completion handler.
158158
public func send<R: RequestType>(_ request: R) async throws -> R.Response {
159159
let requestIDWrapper = ThreadSafeBox<RequestID?>(initialValue: nil)
160-
return try await withTaskCancellationHandler {
160+
161+
@Sendable
162+
func sendCancelNotification() {
163+
/// Take the request ID out of the box. This ensures that we only send the
164+
/// cancel notification once in case the `Task.isCancelled` and the
165+
/// `onCancel` check race.
166+
if let requestID = requestIDWrapper.takeValue() {
167+
self.send(CancelRequestNotification(id: requestID))
168+
}
169+
}
170+
171+
return try await withTaskCancellationHandler(operation: {
161172
try Task.checkCancellation()
162173
return try await withCheckedThrowingContinuation { continuation in
163174
let requestID = self.send(request) { result in
164175
continuation.resume(with: result)
165176
}
166177
requestIDWrapper.value = requestID
178+
179+
// Check if the task was cancelled. This ensures we send a
180+
// CancelNotification even if the task gets cancelled after we register
181+
// the cancellation handler but before we set the `requestID`.
182+
if Task.isCancelled {
183+
sendCancelNotification()
184+
}
167185
}
168-
} onCancel: {
169-
if let requestID = requestIDWrapper.value {
170-
self.send(CancelRequestNotification(id: requestID))
171-
}
172-
}
186+
}, onCancel: sendCancelNotification)
173187
}
174188
}

Sources/SKSupport/ThreadSafeBox.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,14 @@ public class ThreadSafeBox<T> {
4444
public init(initialValue: T) {
4545
_value = initialValue
4646
}
47+
48+
/// If the value in the box is an optional, return it and reset it to `nil`
49+
/// in an atomic operation.
50+
public func takeValue<U>() -> T where U? == T {
51+
lock.withLock {
52+
guard let value = self._value else { return nil }
53+
self._value = nil
54+
return value
55+
}
56+
}
4757
}

Sources/SourceKitD/SourceKitD.swift

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,35 @@ extension SourceKitD {
8383
public func send(_ req: SKDRequestDictionary) async throws -> SKDResponseDictionary {
8484
logRequest(req)
8585

86+
@Sendable
87+
func cancelSourcekitdRequest() {
88+
/// Take the request ID out of the box. This ensures that we only send the
89+
/// cancel notification once in case the `Task.isCancelled` and the
90+
/// `onCancel` check race.
91+
if let handle = handleWrapper.takeValue() {
92+
api.cancel_request(handle)
93+
}
94+
}
95+
8696
let handleWrapper = ThreadSafeBox<sourcekitd_request_handle_t?>(initialValue: nil)
87-
let sourcekitdResponse: SKDResponse = try await withTaskCancellationHandler {
97+
let sourcekitdResponse: SKDResponse = try await withTaskCancellationHandler(operation: {
8898
try Task.checkCancellation()
8999
return await withCheckedContinuation { continuation in
90100
var handle: sourcekitd_request_handle_t? = nil
91101

92102
api.send_request(req.dict, &handle) { _resp in
93103
continuation.resume(returning: SKDResponse(_resp, sourcekitd: self))
94104
}
105+
95106
handleWrapper.value = handle
107+
// Check if the task was cancelled. This ensures we send a
108+
// CancelNotification even if the task gets cancelled after we register
109+
// the cancellation handler but before we set the request handle.
110+
if Task.isCancelled {
111+
cancelSourcekitdRequest()
112+
}
96113
}
97-
} onCancel: {
98-
if let handle = handleWrapper.value {
99-
api.cancel_request(handle)
100-
}
101-
}
114+
}, onCancel: cancelSourcekitdRequest)
102115

103116
logResponse(sourcekitdResponse)
104117

0 commit comments

Comments
 (0)