Skip to content

Commit f6d7701

Browse files
committed
[sourcekitd] Add a registry for sourcekitd instances
Protect ourselves from ever having multiple sourcekitd instances for the same path live at once, which is not safe.
1 parent 095ca6c commit f6d7701

File tree

6 files changed

+174
-3
lines changed

6 files changed

+174
-3
lines changed

Package.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ let package = Package(
116116
"SwiftToolsSupport-auto",
117117
]
118118
),
119+
.testTarget(
120+
name: "SourceKitDTests",
121+
dependencies: [
122+
"SourceKitD",
123+
]
124+
),
119125

120126
// Csourcekitd: C modules wrapper for sourcekitd.
121127
.target(

Sources/SourceKit/Swift/SwiftLanguageServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public final class SwiftLanguageServer: ToolchainLanguageServer {
105105
/// Creates a language server for the given client using the sourcekitd dylib at the specified path.
106106
public init(client: Connection, sourcekitd: AbsolutePath, buildSystem: BuildSystem, clientCapabilities: ClientCapabilities, onExit: @escaping () -> Void = {}) throws {
107107
self.client = client
108-
self.sourcekitd = try SourceKitDImpl(dylib: sourcekitd)
108+
self.sourcekitd = try SourceKitDImpl.getOrCreate(dylibPath: sourcekitd)
109109
self.buildSystem = buildSystem
110110
self.clientCapabilities = clientCapabilities
111111
self.documentManager = DocumentManager()

Sources/SourceKitD/SourceKitDImpl.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ public final class SourceKitDImpl: SourceKitD {
5151
}
5252
}
5353

54-
public init(dylib path: AbsolutePath) throws {
54+
public static func getOrCreate(dylibPath: AbsolutePath) throws -> SourceKitD {
55+
try SourceKitDRegistry.shared
56+
.getOrAdd(dylibPath, create: { try SourceKitDImpl(dylib: dylibPath) })
57+
}
58+
59+
init(dylib path: AbsolutePath) throws {
5560
self.path = path
5661
#if os(Windows)
5762
self.dylib = try dlopen(path.pathString, mode: [])
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import TSCBasic
14+
15+
/// The set of known SourceKitD instances, uniqued by path.
16+
///
17+
/// It is not generally safe to have two instances of SourceKitD for the same libsourcekitd, so
18+
/// care is taken to ensure that there is only ever one instance per path.
19+
///
20+
/// * To get a new instance, use `getOrAdd("path", create: { NewSourceKitD() })`.
21+
/// * To remove an existing instance, use `remove("path")`, but be aware that if there are any other
22+
/// references to the instances in the program, it can be resurrected if `getOrAdd` is called with
23+
/// the same path. See note on `remove(_:)`
24+
public final class SourceKitDRegistry {
25+
26+
/// Mutex protecting mutable state in the registry.
27+
let lock: Lock = Lock()
28+
29+
/// Mapping from path to active SourceKitD instance.
30+
var active: [AbsolutePath: SourceKitD] = [:]
31+
32+
/// Instances that have been unregistered, but may be resurrected if accessed before destruction.
33+
var cemetary: [AbsolutePath: WeakSourceKitD] = [:]
34+
35+
/// Initialize an empty registry.
36+
public init() {}
37+
38+
/// The global shared SourceKitD registry.
39+
public static var shared: SourceKitDRegistry = SourceKitDRegistry()
40+
41+
/// Returns the existing SourceKitD for the given path, or creates it and registers it.
42+
public func getOrAdd(
43+
_ key: AbsolutePath,
44+
create: () throws -> SourceKitD
45+
) rethrows -> SourceKitD {
46+
try lock.withLock {
47+
if let existing = active[key] {
48+
return existing
49+
}
50+
if let resurrected = cemetary[key]?.value {
51+
cemetary[key] = nil
52+
active[key] = resurrected
53+
return resurrected
54+
}
55+
let newValue = try create()
56+
active[key] = newValue
57+
return newValue
58+
}
59+
}
60+
61+
/// Removes the SourceKitD instance registered for the given path, if any, from the set of active
62+
/// instances.
63+
///
64+
/// Since it is not generally safe to have two sourcekitd connections at once, the existing value
65+
/// is converted to a weak reference until it is no longer referenced anywhere by the program. If
66+
/// the same path is looked up again before the original service is deinitialized, the original
67+
/// service is resurrected rather than creating a new instance.
68+
public func remove(_ key: AbsolutePath) -> SourceKitD? {
69+
lock.withLock {
70+
let existing = active.removeValue(forKey: key)
71+
if let existing = existing {
72+
assert(self.cemetary[key] == nil)
73+
cemetary[key] = WeakSourceKitD(value: existing)
74+
}
75+
return existing
76+
}
77+
}
78+
}
79+
80+
struct WeakSourceKitD {
81+
weak var value: SourceKitD?
82+
}

Sources/SourceKitD/sourcekitd_functions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Csourcekitd
1414
import SKSupport
1515

1616
extension sourcekitd_functions_t {
17-
init(_ sourcekitd: DLHandle) throws {
17+
public init(_ sourcekitd: DLHandle) throws {
1818
// Zero-initialize
1919
self.init()
2020

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SourceKitD
14+
import TSCBasic
15+
import XCTest
16+
17+
final class SourceKitDRegistryTests: XCTestCase {
18+
19+
func testAdd() {
20+
let registry = SourceKitDRegistry()
21+
22+
let a = FakeSourceKitD.getOrCreate(AbsolutePath("/a"), in: registry)
23+
let b = FakeSourceKitD.getOrCreate(AbsolutePath("/b"), in: registry)
24+
let a2 = FakeSourceKitD.getOrCreate(AbsolutePath("/a"), in: registry)
25+
26+
XCTAssert(a === a2)
27+
XCTAssert(a !== b)
28+
}
29+
30+
func testRemove() {
31+
let registry = SourceKitDRegistry()
32+
33+
let a = FakeSourceKitD.getOrCreate(AbsolutePath("/a"), in: registry)
34+
XCTAssert(registry.remove(AbsolutePath("/a")) === a)
35+
XCTAssertNil(registry.remove(AbsolutePath("/a")))
36+
}
37+
38+
func testRemoveResurrect() {
39+
let registry = SourceKitDRegistry()
40+
41+
@inline(never)
42+
func scope(registry: SourceKitDRegistry) -> Int {
43+
let a = FakeSourceKitD.getOrCreate(AbsolutePath("/a"), in: registry)
44+
45+
XCTAssert(a === FakeSourceKitD.getOrCreate(AbsolutePath("/a"), in: registry))
46+
XCTAssert(registry.remove(AbsolutePath("/a")) === a)
47+
// Resurrected.
48+
XCTAssert(a === FakeSourceKitD.getOrCreate(AbsolutePath("/a"), in: registry))
49+
// Remove again.
50+
XCTAssert(registry.remove(AbsolutePath("/a")) === a)
51+
return (a as! FakeSourceKitD).token
52+
}
53+
54+
let id = scope(registry: registry)
55+
let a2 = FakeSourceKitD.getOrCreate(AbsolutePath("/a"), in: registry)
56+
XCTAssertNotEqual(id, (a2 as! FakeSourceKitD).token)
57+
}
58+
}
59+
60+
private var nextToken = 0
61+
62+
final class FakeSourceKitD: SourceKitD {
63+
let token: Int
64+
var api: sourcekitd_functions_t { fatalError() }
65+
var keys: sourcekitd_keys { fatalError() }
66+
var requests: sourcekitd_requests { fatalError() }
67+
var values: sourcekitd_values { fatalError() }
68+
func addNotificationHandler(_ handler: SKDNotificationHandler) { fatalError() }
69+
func removeNotificationHandler(_ handler: SKDNotificationHandler) { fatalError() }
70+
private init() {
71+
token = nextToken
72+
nextToken += 1
73+
}
74+
75+
static func getOrCreate(_ path: AbsolutePath, in registry: SourceKitDRegistry) -> SourceKitD {
76+
return registry.getOrAdd(path, create: { Self.init() })
77+
}
78+
}

0 commit comments

Comments
 (0)