Skip to content

Commit 42f15b8

Browse files
authored
Merge pull request #196 from benlangmuir/shutdown-flush
Attempt to save index on shutdown
2 parents 72aefff + 3e9348f commit 42f15b8

File tree

7 files changed

+118
-21
lines changed

7 files changed

+118
-21
lines changed

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,15 @@ public final class JSONRPCConection {
5252
/// The set of currently outstanding outgoing requests along with information about how to decode and handle their responses.
5353
var outstandingRequests: [RequestID: OutstandingRequest] = [:]
5454

55-
var closeHandler: () -> Void
55+
var closeHandler: (() -> Void)! = nil
5656

5757
public init(
5858
protocol messageRegistry: MessageRegistry,
5959
inFD: Int32,
6060
outFD: Int32,
61-
syncRequests: Bool = false,
62-
closeHandler: @escaping () -> Void = {})
61+
syncRequests: Bool = false)
6362
{
6463
state = .created
65-
self.closeHandler = closeHandler
6664
self.messageRegistry = messageRegistry
6765
self.syncRequests = syncRequests
6866

@@ -93,10 +91,11 @@ public final class JSONRPCConection {
9391
/// Start processing `inFD` and send messages to `receiveHandler`.
9492
///
9593
/// - parameter receiveHandler: The message handler to invoke for requests received on the `inFD`.
96-
public func start(receiveHandler: MessageHandler) {
94+
public func start(receiveHandler: MessageHandler, closeHandler: @escaping () -> Void = {}) {
9795
precondition(state == .created)
9896
state = .running
9997
self.receiveHandler = receiveHandler
98+
self.closeHandler = closeHandler
10099

101100
receiveIO.read(offset: 0, length: Int.max, queue: queue) { done, data, errorCode in
102101
guard errorCode == 0 else {

Sources/SKTestSupport/SKTibsTestWorkspace.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,15 @@ public final class SKTibsTestWorkspace {
3838
immutableProjectDir: URL,
3939
persistentBuildDir: URL,
4040
tmpDir: URL,
41+
removeTmpDir: Bool,
4142
toolchain: Toolchain,
4243
clientCapabilities: ClientCapabilities) throws
4344
{
4445
self.tibsWorkspace = try TibsTestWorkspace(
4546
immutableProjectDir: immutableProjectDir,
4647
persistentBuildDir: persistentBuildDir,
4748
tmpDir: tmpDir,
49+
removeTmpDir: removeTmpDir,
4850
toolchain: TibsToolchain(toolchain))
4951

5052
initWorkspace(clientCapabilities: clientCapabilities)
@@ -91,15 +93,22 @@ extension SKTibsTestWorkspace {
9193

9294
extension XCTestCase {
9395

94-
public func staticSourceKitTibsWorkspace(name: String, clientCapabilities: ClientCapabilities = .init(), testFile: String = #file) throws -> SKTibsTestWorkspace? {
96+
public func staticSourceKitTibsWorkspace(
97+
name: String,
98+
clientCapabilities: ClientCapabilities = .init(),
99+
tmpDir: URL? = nil,
100+
removeTmpDir: Bool = true,
101+
testFile: String = #file
102+
) throws -> SKTibsTestWorkspace? {
95103
let testDirName = testDirectoryName
96104
let workspace = try SKTibsTestWorkspace(
97105
immutableProjectDir: inputsDirectory(testFile: testFile)
98106
.appendingPathComponent(name, isDirectory: true),
99107
persistentBuildDir: XCTestCase.productsDirectory
100108
.appendingPathComponent("sk-tests/\(testDirName)", isDirectory: true),
101-
tmpDir: URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
109+
tmpDir: tmpDir ?? URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
102110
.appendingPathComponent("sk-test-data/\(testDirName)", isDirectory: true),
111+
removeTmpDir: removeTmpDir,
103112
toolchain: ToolchainRegistry.shared.default!,
104113
clientCapabilities: clientCapabilities)
105114

Sources/SourceKit/SourceKitServer.swift

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public final class SourceKitServer: LanguageServer {
4646

4747
let fs: FileSystem
4848

49-
let onExit: () -> Void
49+
var onExit: () -> Void
5050

5151
/// Creates a language server for the given client.
5252
public init(client: Connection, fileSystem: FileSystem = localFileSystem, options: Options, onExit: @escaping () -> Void = {}) {
@@ -351,13 +351,47 @@ extension SourceKitServer {
351351
requestCancellation[key]?.cancel()
352352
}
353353

354+
/// The server is about to exit, and the server should flush any buffered state.
355+
///
356+
/// The server shall not be used to handle more requests (other than possibly
357+
/// `shutdown` and `exit`) and should attempt to flush any buffered state
358+
/// immediately, such as sending index changes to disk.
359+
public func prepareForExit() {
360+
// Note: this method should be safe to call multiple times, since we want to
361+
// be resilient against multiple possible shutdown sequences, including
362+
// pipe failure.
363+
364+
// Close the index, which will flush to disk.
365+
self.queue.sync {
366+
self._prepareForExit()
367+
}
368+
}
369+
370+
func _prepareForExit() {
371+
// Note: this method should be safe to call multiple times, since we want to
372+
// be resilient against multiple possible shutdown sequences, including
373+
// pipe failure.
374+
375+
// Close the index, which will flush to disk.
376+
self.workspace?.index = nil
377+
}
378+
379+
354380
func shutdown(_ request: Request<Shutdown>) {
355-
// Nothing to do yet.
381+
_prepareForExit()
356382
request.reply(VoidResponse())
357383
}
358384

359385
func exit(_ notification: Notification<Exit>) {
360-
onExit()
386+
// Should have been called in shutdown, but allow misbehaving clients.
387+
_prepareForExit()
388+
389+
// Call onExit only once, and hop off queue to allow the handler to call us back.
390+
let onExit = self.onExit
391+
self.onExit = {}
392+
DispatchQueue.global().async {
393+
onExit()
394+
}
361395
}
362396

363397
// MARK: - Text synchronization

Sources/sourcekit-lsp/main.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,18 +102,18 @@ let clientConnection = JSONRPCConection(
102102
protocol: MessageRegistry.lspProtocol,
103103
inFD: STDIN_FILENO,
104104
outFD: STDOUT_FILENO,
105-
syncRequests: options.syncRequests,
106-
closeHandler: {
107-
exit(0)
108-
})
105+
syncRequests: options.syncRequests)
109106

110107
let installPath = AbsolutePath(Bundle.main.bundlePath)
111108
ToolchainRegistry.shared = ToolchainRegistry(installPath: installPath, localFileSystem)
112109

113110
let server = SourceKitServer(client: clientConnection, options: options.serverOptions, onExit: {
114111
clientConnection.close()
115112
})
116-
clientConnection.start(receiveHandler: server)
113+
clientConnection.start(receiveHandler: server, closeHandler: {
114+
server.prepareForExit()
115+
exit(0)
116+
})
117117

118118
Logger.shared.addLogHandler { message, _ in
119119
clientConnection.send(LogMessage(type: .log, message: message))

Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,18 +213,17 @@ class ConnectionTests: XCTestCase {
213213
let conn = JSONRPCConection(
214214
protocol: MessageRegistry(requests: [], notifications: []),
215215
inFD: to.fileHandleForReading.fileDescriptor,
216-
outFD: from.fileHandleForWriting.fileDescriptor,
217-
closeHandler: {
218-
// We get an error from XCTest if this is fulfilled more than once.
219-
expectation.fulfill()
220-
})
216+
outFD: from.fileHandleForWriting.fileDescriptor)
221217

222218
final class DummyHandler: MessageHandler {
223219
func handle<N: NotificationType>(_: N, from: ObjectIdentifier) {}
224220
func handle<R: RequestType>(_: R, id: RequestID, from: ObjectIdentifier, reply: @escaping (LSPResult<R.Response>) -> Void) {}
225221
}
226222

227-
conn.start(receiveHandler: DummyHandler())
223+
conn.start(receiveHandler: DummyHandler(), closeHandler: {
224+
// We get an error from XCTest if this is fulfilled more than once.
225+
expectation.fulfill()
226+
})
228227

229228

230229
close(to.fileHandleForWriting.fileDescriptor)

Tests/SourceKitTests/SourceKitTests.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,61 @@ final class SKTests: XCTestCase {
8787
])
8888
}
8989

90+
func testIndexShutdown() throws {
91+
92+
let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
93+
.appendingPathComponent("sk-test-data/\(testDirectoryName)", isDirectory: true)
94+
95+
func listdir(_ url: URL) throws -> [URL] {
96+
try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
97+
}
98+
99+
func checkRunningIndex(build: Bool) throws -> URL? {
100+
guard let ws = try staticSourceKitTibsWorkspace(
101+
name: "SwiftModules", tmpDir: tmpDir, removeTmpDir: false)
102+
else {
103+
return nil
104+
}
105+
106+
if build {
107+
try ws.buildAndIndex()
108+
}
109+
110+
let locDef = ws.testLoc("aaa:def")
111+
let locRef = ws.testLoc("aaa:call:c")
112+
try ws.openDocument(locRef.url, language: .swift)
113+
let jump = try ws.sk.sendSync(DefinitionRequest(
114+
textDocument: locRef.docIdentifier,
115+
position: locRef.position))
116+
XCTAssertEqual(jump.count, 1)
117+
XCTAssertEqual(jump.first?.uri, DocumentURI(locDef.url))
118+
XCTAssertEqual(jump.first?.range.lowerBound, locDef.position)
119+
120+
let tmpContents = try listdir(tmpDir)
121+
guard let versionedPath = tmpContents.filter({ $0.lastPathComponent.starts(with: "v") }).spm_only else {
122+
XCTFail("expected one version path 'v[0-9]*', found \(tmpContents)")
123+
return nil
124+
}
125+
126+
let versionContentsBefore = try listdir(versionedPath)
127+
XCTAssertEqual(versionContentsBefore.count, 1)
128+
XCTAssert(versionContentsBefore.first?.lastPathComponent.starts(with: "p") ?? false)
129+
130+
_ = try ws.sk.sendSync(Shutdown())
131+
return versionedPath
132+
}
133+
134+
guard let versionedPath = try checkRunningIndex(build: true) else { return }
135+
136+
let versionContentsAfter = try listdir(versionedPath)
137+
XCTAssertEqual(versionContentsAfter.count, 1)
138+
XCTAssertEqual(versionContentsAfter.first?.lastPathComponent, "saved")
139+
140+
_ = try checkRunningIndex(build: true)
141+
142+
try FileManager.default.removeItem(atPath: tmpDir.path)
143+
}
144+
90145
func testCodeCompleteSwiftTibs() throws {
91146
guard let ws = try staticSourceKitTibsWorkspace(name: "CodeCompleteSingleModule") else { return }
92147
let loc = ws.testLoc("cc:A")

Tests/SourceKitTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ extension SKTests {
122122
// to regenerate.
123123
static let __allTests__SKTests = [
124124
("testCodeCompleteSwiftTibs", testCodeCompleteSwiftTibs),
125+
("testIndexShutdown", testIndexShutdown),
125126
("testIndexSwiftModules", testIndexSwiftModules),
126127
("testInitJSON", testInitJSON),
127128
("testInitLocal", testInitLocal),

0 commit comments

Comments
 (0)