Skip to content

Commit fae2fe8

Browse files
committed
[JSONRPC] Do not exit until outstanding I/O has finished
Specifically, we care that all outstanding **writes** are finished before we call the close handler, because otherwise we may (a) send corrupted output during shutdown, or (b) drop notifications and replies sent during the shutdown process. The former is a potential issue for clients that are not robust about parse failures, and the latter is an issue for reproducibility and robustness during testing/debugging - in particular, we have some integration tests that send data without waiting for individual replies and they need to finish outstanding replies before exiting. rdar://60159448
1 parent b00cff1 commit fae2fe8

File tree

3 files changed

+36
-4
lines changed

3 files changed

+36
-4
lines changed

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,28 @@ public final class JSONRPCConnection {
6464
self.messageRegistry = messageRegistry
6565
self.syncRequests = syncRequests
6666

67+
let ioGroup = DispatchGroup()
68+
69+
ioGroup.enter()
6770
receiveIO = DispatchIO(type: .stream, fileDescriptor: inFD, queue: queue) { (error: Int32) in
6871
if error != 0 {
6972
log("IO error \(error)", level: .error)
7073
}
74+
ioGroup.leave()
7175
}
7276

77+
ioGroup.enter()
7378
sendIO = DispatchIO(type: .stream, fileDescriptor: outFD, queue: sendQueue) { (error: Int32) in
7479
if error != 0 {
7580
log("IO error \(error)", level: .error)
7681
}
82+
ioGroup.leave()
83+
}
84+
85+
ioGroup.notify(queue: queue) { [weak self] in
86+
guard let self = self else { return }
87+
self.closeHandler()
88+
self.receiveHandler = nil // break retain cycle
7789
}
7890

7991
// We cannot assume the client will send us bytes in packets of any particular size, so set the lower limit to 1.
@@ -99,7 +111,9 @@ public final class JSONRPCConnection {
99111

100112
receiveIO.read(offset: 0, length: Int.max, queue: queue) { done, data, errorCode in
101113
guard errorCode == 0 else {
102-
log("IO error \(errorCode)", level: .error)
114+
if errorCode != POSIXError.ECANCELED.rawValue {
115+
log("IO error reading \(errorCode)", level: .error)
116+
}
103117
if done { self._close() }
104118
return
105119
}
@@ -287,6 +301,9 @@ public final class JSONRPCConnection {
287301
}
288302

289303
/// Close the connection.
304+
///
305+
/// The user-provided close handler will be called *asynchronously* when all outstanding I/O
306+
/// operations have completed. No new I/O will be accepted after `close` returns.
290307
public func close() {
291308
queue.sync { _close() }
292309
}
@@ -298,10 +315,10 @@ public final class JSONRPCConnection {
298315
state = .closed
299316

300317
log("\(JSONRPCConnection.self): closing...")
318+
// Attempt to close the reader immediately; we do not need to accept remaining inputs.
301319
receiveIO.close(flags: .stop)
302-
sendIO.close(flags: .stop)
303-
receiveHandler = nil // break retain cycle
304-
closeHandler()
320+
// Close the writer after it finishes outstanding work.
321+
sendIO.close()
305322
}
306323
}
307324

Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,20 @@ class ConnectionTests: XCTestCase {
197197
waitForExpectations(timeout: 10)
198198
}
199199

200+
func testSendBeforeClose() {
201+
let client = connection.client
202+
let server = connection.server
203+
204+
let expectation = self.expectation(description: "received notification")
205+
client.handleNextNotification { (note: Notification<EchoNotification>) in
206+
expectation.fulfill()
207+
}
208+
209+
server.client.send(EchoNotification(string: "about to close!"))
210+
connection.serverConnection.close()
211+
212+
waitForExpectations(timeout: 10)
213+
}
200214

201215
/// We can explicitly close a connection, but the connection also
202216
/// automatically closes itself if the pipe is closed (or has an error).

Tests/LanguageServerProtocolJSONRPCTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ extension ConnectionTests {
3434
("testMessageBuffer", testMessageBuffer),
3535
("testRound", testRound),
3636
("testSendAfterClose", testSendAfterClose),
37+
("testSendBeforeClose", testSendBeforeClose),
3738
("testUnexpectedResponse", testUnexpectedResponse),
3839
("testUnknownNotification", testUnknownNotification),
3940
("testUnknownRequest", testUnknownRequest),

0 commit comments

Comments
 (0)