Skip to content

Commit e4b2b06

Browse files
authored
Merge pull request #288 from benlangmuir/leaks-and-shutdowns
Fix a number of resource leaks when using SourceKit-LSP in tests or as a library
2 parents 0ea45c7 + 1040621 commit e4b2b06

File tree

11 files changed

+103
-52
lines changed

11 files changed

+103
-52
lines changed

Sources/LSPTestSupport/TestJSONRPCConnection.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -18,7 +18,7 @@ import XCTest
1818
// Workaround ambiguity with Foundation.
1919
public typealias Notification = LanguageServerProtocol.Notification
2020

21-
public struct TestJSONRPCConnection {
21+
public final class TestJSONRPCConnection {
2222
public let clientToServer: Pipe = Pipe()
2323
public let serverToClient: Pipe = Pipe()
2424
public let client: TestClient
@@ -27,11 +27,6 @@ public struct TestJSONRPCConnection {
2727
public let serverConnection: JSONRPCConnection
2828

2929
public init() {
30-
// FIXME: DispatchIO doesn't like when the Pipes close behind its back even after the tests
31-
// finish. Until we fix the lifetime, leak.
32-
_ = Unmanaged.passRetained(clientToServer)
33-
_ = Unmanaged.passRetained(serverToClient)
34-
3530
clientConnection = JSONRPCConnection(
3631
protocol: testMessageRegistry,
3732
inFD: serverToClient.fileHandleForReading.fileDescriptor,
@@ -47,8 +42,16 @@ public struct TestJSONRPCConnection {
4742
client = TestClient(server: clientConnection)
4843
server = TestServer(client: serverConnection)
4944

50-
clientConnection.start(receiveHandler: client)
51-
serverConnection.start(receiveHandler: server)
45+
clientConnection.start(receiveHandler: client) {
46+
// FIXME: keep the pipes alive until we close the connection. This
47+
// should be fixed systemically.
48+
withExtendedLifetime(self) {}
49+
}
50+
serverConnection.start(receiveHandler: server) {
51+
// FIXME: keep the pipes alive until we close the connection. This
52+
// should be fixed systemically.
53+
withExtendedLifetime(self) {}
54+
}
5255
}
5356

5457
public func close() {

Sources/LanguageServerProtocol/Connection.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -104,14 +104,18 @@ public final class LocalConnection {
104104

105105
extension LocalConnection: Connection {
106106
public func send<Notification>(_ notification: Notification) where Notification: NotificationType {
107-
precondition(state == .started)
108-
handler!.handle(notification, from: ObjectIdentifier(self))
107+
handler?.handle(notification, from: ObjectIdentifier(self))
109108
}
110109

111110
public func send<Request>(_ request: Request, queue: DispatchQueue, reply: @escaping (LSPResult<Request.Response>) -> Void) -> RequestID where Request: RequestType {
112-
precondition(state == .started)
113111
let id = nextRequestID()
114-
handler!.handle(request, id: id, from: ObjectIdentifier(self)) { result in
112+
guard let handler = handler else {
113+
queue.async { reply(.failure(.cancelled)) }
114+
return id
115+
}
116+
117+
precondition(state == .started)
118+
handler.handle(request, id: id, from: ObjectIdentifier(self)) { result in
115119
queue.async {
116120
reply(result)
117121
}

Sources/SKCore/BuildServerBuildSystem.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,11 @@ private func makeJSONRPCBuildServer(client: MessageHandler, serverPath: Absolute
254254
outFD: clientToServer.fileHandleForWriting.fileDescriptor
255255
)
256256

257-
connection.start(receiveHandler: client)
257+
connection.start(receiveHandler: client) {
258+
// FIXME: keep the pipes alive until we close the connection. This
259+
// should be fixed systemically.
260+
withExtendedLifetime((clientToServer, serverToClient)) {}
261+
}
258262
let process = Foundation.Process()
259263

260264
if #available(OSX 10.13, *) {

Sources/SKCore/BuildSystemManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public final class BuildSystemManager {
110110
weak var mainFilesProvider: MainFilesProvider?
111111

112112
/// Build system delegate that will receive notifications about setting changes, etc.
113-
var _delegate: BuildSystemDelegate?
113+
weak var _delegate: BuildSystemDelegate?
114114

115115
/// Create a BuildSystemManager that wraps the given build system. The new
116116
/// manager will modify the delegate of the underlying build system.

Sources/SKTestSupport/TestServer.swift

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -19,7 +19,7 @@ import SourceKitLSP
1919
import class Foundation.Pipe
2020
import LSPTestSupport
2121

22-
public struct TestSourceKitServer {
22+
public final class TestSourceKitServer {
2323
public enum ConnectionKind {
2424
case local, jsonrpc
2525
}
@@ -40,6 +40,8 @@ public struct TestSourceKitServer {
4040
public let client: TestClient
4141
let connImpl: ConnectionImpl
4242

43+
public var hasShutdown: Bool = false
44+
4345
/// The server, if it is in the same process.
4446
public let server: SourceKitServer?
4547

@@ -63,11 +65,6 @@ public struct TestSourceKitServer {
6365
let clientToServer: Pipe = Pipe()
6466
let serverToClient: Pipe = Pipe()
6567

66-
// FIXME: DispatchIO doesn't like when the Pipes close behind its back even after the tests
67-
// finish. Until we fix the lifetime, leak.
68-
_ = Unmanaged.passRetained(clientToServer)
69-
_ = Unmanaged.passRetained(serverToClient)
70-
7168
let clientConnection = JSONRPCConnection(
7269
protocol: MessageRegistry.lspProtocol,
7370
inFD: serverToClient.fileHandleForReading.fileDescriptor,
@@ -84,8 +81,16 @@ public struct TestSourceKitServer {
8481
serverConnection.close()
8582
})
8683

87-
clientConnection.start(receiveHandler: client)
88-
serverConnection.start(receiveHandler: server!)
84+
clientConnection.start(receiveHandler: client) {
85+
// FIXME: keep the pipes alive until we close the connection. This
86+
// should be fixed systemically.
87+
withExtendedLifetime((clientToServer, serverToClient)) {}
88+
}
89+
serverConnection.start(receiveHandler: server!) {
90+
// FIXME: keep the pipes alive until we close the connection. This
91+
// should be fixed systemically.
92+
withExtendedLifetime((clientToServer, serverToClient)) {}
93+
}
8994

9095
connImpl = .jsonrpc(
9196
clientToServer: clientToServer,
@@ -95,15 +100,15 @@ public struct TestSourceKitServer {
95100
}
96101
}
97102

103+
deinit {
104+
close()
105+
}
106+
98107
func close() {
99-
switch connImpl {
100-
case .local(clientConnection: let cc, serverConnection: let sc):
101-
cc.close()
102-
sc.close()
103-
104-
case .jsonrpc(clientToServer: _, serverToClient: _, clientConnection: let cc, serverConnection: let sc):
105-
cc.close()
106-
sc.close()
108+
if !hasShutdown {
109+
hasShutdown = true
110+
_ = try! self.client.sendSync(ShutdownRequest())
111+
self.client.send(ExitNotification())
107112
}
108113
}
109114
}

Sources/SourceKitD/SourceKitDRegistry.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,20 @@ public final class SourceKitDRegistry {
6969
lock.withLock {
7070
let existing = active.removeValue(forKey: key)
7171
if let existing = existing {
72-
assert(self.cemetary[key] == nil)
72+
assert(self.cemetary[key]?.value == nil)
7373
cemetary[key] = WeakSourceKitD(value: existing)
7474
}
7575
return existing
7676
}
7777
}
78+
79+
/// Remove all SourceKitD instances, including weak ones.
80+
public func clear() {
81+
lock.withLock {
82+
active.removeAll()
83+
cemetary.removeAll()
84+
}
85+
}
7886
}
7987

8088
struct WeakSourceKitD {

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -26,16 +26,23 @@ final class ClangLanguageServerShim: ToolchainLanguageServer {
2626

2727
let clangd: Connection
2828

29+
let client: LocalConnection
30+
2931
var capabilities: ServerCapabilities? = nil
3032

3133
let buildSystem: BuildSystem
3234

3335
let clang: AbsolutePath?
3436

3537
/// Creates a language server for the given client using the sourcekitd dylib at the specified path.
36-
public init(client: Connection, clangd: Connection, buildSystem: BuildSystem,
37-
clang: AbsolutePath?) throws {
38+
public init(
39+
client: LocalConnection,
40+
clangd: Connection,
41+
buildSystem: BuildSystem,
42+
clang: AbsolutePath?
43+
) throws {
3844
self.clangd = clangd
45+
self.client = client
3946
self.buildSystem = buildSystem
4047
self.clang = clang
4148
}
@@ -82,6 +89,13 @@ extension ClangLanguageServerShim {
8289
clangd.send(initialized)
8390
}
8491

92+
public func shutdown() {
93+
_ = clangd.send(ShutdownRequest(), queue: queue) { [weak self] _ in
94+
self?.clangd.send(ExitNotification())
95+
self?.client.close()
96+
}
97+
}
98+
8599
// MARK: - Text synchronization
86100

87101
public func openDocument(_ note: DidOpenTextDocumentNotification) {
@@ -247,7 +261,11 @@ func makeJSONRPCClangServer(
247261
clang: toolchain.clang)
248262

249263
connectionToClient.start(handler: client)
250-
connection.start(receiveHandler: client)
264+
connection.start(receiveHandler: client) {
265+
// FIXME: keep the pipes alive until we close the connection. This
266+
// should be fixed systemically.
267+
withExtendedLifetime((clientToServer, serverToClient)) {}
268+
}
251269

252270
let process = Foundation.Process()
253271

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,10 @@ extension SourceKitServer {
520520

521521
func shutdown(_ request: Request<ShutdownRequest>) {
522522
_prepareForExit()
523+
for service in languageService.values {
524+
service.shutdown()
525+
}
526+
languageService = [:]
523527
request.reply(VoidResponse())
524528
}
525529

Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -81,7 +81,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
8181
/// The server's request queue, used to serialize requests and responses to `sourcekitd`.
8282
public let queue: DispatchQueue = DispatchQueue(label: "swift-language-server-queue", qos: .userInitiated)
8383

84-
let client: Connection
84+
let client: LocalConnection
8585

8686
let sourcekitd: SourceKitD
8787

@@ -96,20 +96,23 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
9696

9797
var commandsByFile: [DocumentURI: SwiftCompileCommand] = [:]
9898

99-
let onExit: () -> Void
100-
10199
var keys: sourcekitd_keys { return sourcekitd.keys }
102100
var requests: sourcekitd_requests { return sourcekitd.requests }
103101
var values: sourcekitd_values { return sourcekitd.values }
104102

105-
/// Creates a language server for the given client using the sourcekitd dylib at the specified path.
106-
public init(client: Connection, sourcekitd: AbsolutePath, buildSystem: BuildSystem, clientCapabilities: ClientCapabilities, onExit: @escaping () -> Void = {}) throws {
103+
/// Creates a language server for the given client using the sourcekitd dylib at the specified
104+
/// path.
105+
public init(
106+
client: LocalConnection,
107+
sourcekitd: AbsolutePath,
108+
buildSystem: BuildSystem,
109+
clientCapabilities: ClientCapabilities
110+
) throws {
107111
self.client = client
108112
self.sourcekitd = try SourceKitDImpl.getOrCreate(dylibPath: sourcekitd)
109113
self.buildSystem = buildSystem
110114
self.clientCapabilities = clientCapabilities
111115
self.documentManager = DocumentManager()
112-
self.onExit = onExit
113116
}
114117

115118
/// Should be called on self.queue.
@@ -202,12 +205,9 @@ extension SwiftLanguageServer {
202205
// Nothing to do.
203206
}
204207

205-
func shutdown(_ request: Request<ShutdownRequest>) {
208+
public func shutdown() {
206209
sourcekitd.removeNotificationHandler(self)
207-
}
208-
209-
func exit(_ notification: Notification<ExitNotification>) {
210-
onExit()
210+
client.close()
211211
}
212212

213213
// MARK: - Build System Integration

Sources/SourceKitLSP/ToolchainLanguageServer.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -21,6 +21,7 @@ public protocol ToolchainLanguageServer: AnyObject {
2121

2222
func initializeSync(_ initialize: InitializeRequest) throws -> InitializeResult
2323
func clientInitialized(_ initialized: InitializedNotification)
24+
func shutdown()
2425

2526
// MARK: - Text synchronization
2627

Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -237,6 +237,10 @@ class ConnectionTests: XCTestCase {
237237
conn.start(receiveHandler: DummyHandler(), closeHandler: {
238238
// We get an error from XCTest if this is fulfilled more than once.
239239
expectation.fulfill()
240+
241+
// FIXME: keep the pipes alive until we close the connection. This
242+
// should be fixed systemically.
243+
withExtendedLifetime((to, from)) {}
240244
})
241245

242246
to.fileHandleForWriting.closeFile()

0 commit comments

Comments
 (0)