Skip to content

[JSONRPC] Do not exit until outstanding I/O has finished #254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,28 @@ public final class JSONRPCConnection {
self.messageRegistry = messageRegistry
self.syncRequests = syncRequests

let ioGroup = DispatchGroup()

ioGroup.enter()
receiveIO = DispatchIO(type: .stream, fileDescriptor: inFD, queue: queue) { (error: Int32) in
if error != 0 {
log("IO error \(error)", level: .error)
}
ioGroup.leave()
}

ioGroup.enter()
sendIO = DispatchIO(type: .stream, fileDescriptor: outFD, queue: sendQueue) { (error: Int32) in
if error != 0 {
log("IO error \(error)", level: .error)
}
ioGroup.leave()
}

ioGroup.notify(queue: queue) { [weak self] in
guard let self = self else { return }
self.closeHandler()
self.receiveHandler = nil // break retain cycle
}

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

receiveIO.read(offset: 0, length: Int.max, queue: queue) { done, data, errorCode in
guard errorCode == 0 else {
log("IO error \(errorCode)", level: .error)
if errorCode != POSIXError.ECANCELED.rawValue {
log("IO error reading \(errorCode)", level: .error)
}
if done { self._close() }
return
}
Expand Down Expand Up @@ -287,6 +301,9 @@ public final class JSONRPCConnection {
}

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

log("\(JSONRPCConnection.self): closing...")
// Attempt to close the reader immediately; we do not need to accept remaining inputs.
receiveIO.close(flags: .stop)
sendIO.close(flags: .stop)
receiveHandler = nil // break retain cycle
closeHandler()
// Close the writer after it finishes outstanding work.
sendIO.close()
}
}

Expand Down
14 changes: 14 additions & 0 deletions Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,20 @@ class ConnectionTests: XCTestCase {
waitForExpectations(timeout: 10)
}

func testSendBeforeClose() {
let client = connection.client
let server = connection.server

let expectation = self.expectation(description: "received notification")
client.handleNextNotification { (note: Notification<EchoNotification>) in
expectation.fulfill()
}

server.client.send(EchoNotification(string: "about to close!"))
connection.serverConnection.close()

waitForExpectations(timeout: 10)
}

/// We can explicitly close a connection, but the connection also
/// automatically closes itself if the pipe is closed (or has an error).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension ConnectionTests {
("testMessageBuffer", testMessageBuffer),
("testRound", testRound),
("testSendAfterClose", testSendAfterClose),
("testSendBeforeClose", testSendBeforeClose),
("testUnexpectedResponse", testUnexpectedResponse),
("testUnknownNotification", testUnknownNotification),
("testUnknownRequest", testUnknownRequest),
Expand Down