Skip to content

Commit e00312a

Browse files
authored
Merge pull request #1431 from ahoppen/local-connection-race-condition
Fix a race condition in `LocalConnection`
2 parents 6f7bffa + 3fdaa10 commit e00312a

File tree

1 file changed

+32
-16
lines changed

1 file changed

+32
-16
lines changed

Sources/InProcessClient/LocalConnection.swift

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,47 +26,60 @@ import LanguageServerProtocol
2626
/// conn.send(...) // handled by server
2727
/// conn.close()
2828
/// ```
29-
///
30-
/// - Note: Unchecked sendable conformance because shared state is guarded by `queue`.
31-
public final class LocalConnection: Connection, @unchecked Sendable {
32-
enum State {
29+
public final class LocalConnection: Connection, Sendable {
30+
private enum State {
3331
case ready, started, closed
3432
}
3533

3634
/// A name of the endpoint for this connection, used for logging, e.g. `clangd`.
3735
private let name: String
3836

3937
/// The queue guarding `_nextRequestID`.
40-
let queue: DispatchQueue = DispatchQueue(label: "local-connection-queue")
38+
private let queue: DispatchQueue = DispatchQueue(label: "local-connection-queue")
4139

42-
var _nextRequestID: Int = 0
40+
/// - Important: Must only be accessed from `queue`
41+
nonisolated(unsafe) private var _nextRequestID: Int = 0
4342

44-
var state: State = .ready
43+
/// - Important: Must only be accessed from `queue`
44+
nonisolated(unsafe) private var state: State = .ready
4545

46-
var handler: MessageHandler? = nil
46+
/// - Important: Must only be accessed from `queue`
47+
nonisolated(unsafe) private var handler: MessageHandler? = nil
4748

4849
public init(name: String) {
4950
self.name = name
5051
}
5152

5253
deinit {
53-
if state != .closed {
54-
close()
54+
queue.sync {
55+
if state != .closed {
56+
closeAssumingOnQueue()
57+
}
5558
}
5659
}
5760

5861
public func start(handler: MessageHandler) {
59-
precondition(state == .ready)
60-
state = .started
61-
self.handler = handler
62+
queue.sync {
63+
precondition(state == .ready)
64+
state = .started
65+
self.handler = handler
66+
}
6267
}
6368

64-
public func close() {
69+
/// - Important: Must only be called from `queue`
70+
private func closeAssumingOnQueue() {
71+
dispatchPrecondition(condition: .onQueue(queue))
6572
precondition(state != .closed)
6673
handler = nil
6774
state = .closed
6875
}
6976

77+
public func close() {
78+
queue.sync {
79+
closeAssumingOnQueue()
80+
}
81+
}
82+
7083
func nextRequestID() -> RequestID {
7184
return queue.sync {
7285
_nextRequestID += 1
@@ -81,7 +94,10 @@ public final class LocalConnection: Connection, @unchecked Sendable {
8194
\(notification.forLogging)
8295
"""
8396
)
84-
self.handler?.handle(notification)
97+
guard let handler = queue.sync(execute: { handler }) else {
98+
return
99+
}
100+
handler.handle(notification)
85101
}
86102

87103
public func send<Request: RequestType>(
@@ -97,7 +113,7 @@ public final class LocalConnection: Connection, @unchecked Sendable {
97113
"""
98114
)
99115

100-
guard let handler = self.handler else {
116+
guard let handler = queue.sync(execute: { handler }) else {
101117
logger.info(
102118
"""
103119
Replying to request \(id, privacy: .public) with .serverCancelled because no handler is specified in \(self.name, privacy: .public)

0 commit comments

Comments
 (0)