Skip to content

Commit c8d0977

Browse files
committed
Restart sourcekitd and clangd after they have crashed
1 parent 94585a7 commit c8d0977

File tree

12 files changed

+425
-75
lines changed

12 files changed

+425
-75
lines changed

Sources/LanguageServerProtocol/Error.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public struct ErrorCode: RawRepresentable, Codable, Hashable {
3535

3636
// LSP
3737
public static let cancelled: ErrorCode = ErrorCode(rawValue: -32800)
38+
39+
// sourcekitd
40+
public static let connectionInterrupted = ErrorCode(rawValue: -13000)
3841
}
3942

4043
/// An error response represented by a code and message.
@@ -63,6 +66,10 @@ extension ResponseError {
6366
public static func unknown(_ message: String) -> ResponseError {
6467
return ResponseError(code: .unknownErrorCode, message: message)
6568
}
69+
70+
public static func connectionInterrupted(_ message: String) -> ResponseError {
71+
return ResponseError(code: .connectionInterrupted, message: message)
72+
}
6673
}
6774

6875
/// An error during message decoding.

Sources/SKTestSupport/SKTibsTestWorkspace.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,5 @@ extension TibsToolchain {
200200

201201
extension TestLocation {
202202
public var docIdentifier: TextDocumentIdentifier { TextDocumentIdentifier(url) }
203+
public var docUri: DocumentURI { DocumentURI(url) }
203204
}

Sources/SourceKit/SourceKitServer.swift

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,36 @@ public final class SourceKitServer: LanguageServer {
185185
return nil
186186
}
187187

188+
func reopenDocuments(for languages: Set<Language>) {
189+
guard let workspace = workspace else {
190+
return
191+
}
192+
193+
for documentUri in workspace.documentManager.openDocuments {
194+
guard let snapshot = workspace.documentManager.latestSnapshot(documentUri) else {
195+
// The document has been closed since we retrieved its URI. We don't care about it anymore.
196+
continue
197+
}
198+
guard languages.contains(snapshot.document.language) else {
199+
// The document is a different language
200+
continue
201+
}
202+
203+
do {
204+
// Close the document in the document manager so we can reopen it. If the document has been
205+
// closed since, the try will fail and we won't try to reopen it.
206+
try workspace.documentManager.close(documentUri)
207+
let textDocument = TextDocumentItem(uri: documentUri,
208+
language: snapshot.document.language,
209+
version: snapshot.version,
210+
text: snapshot.text)
211+
openDocument(DidOpenTextDocumentNotification(textDocument: textDocument), workspace: workspace)
212+
} catch {
213+
// The document was no longer open. Ignore it
214+
}
215+
}
216+
}
217+
188218
func languageService(for toolchain: Toolchain, _ language: Language) -> ToolchainLanguageServer? {
189219
let key = LanguageServiceKey(toolchain: toolchain.identifier, language: language)
190220
if let service = languageService[key] {
@@ -193,7 +223,16 @@ public final class SourceKitServer: LanguageServer {
193223

194224
// Start a new service.
195225
return orLog("failed to start language service", level: .error) {
196-
guard let service = try SourceKit.languageService(for: toolchain, language, options: options, client: self) else {
226+
let reopenDocuments = {
227+
switch language {
228+
case .c, .cpp, .objective_c, .objective_cpp:
229+
// Re-open all C-based languages if clangd crashed
230+
self.reopenDocuments(for: [.c, .cpp, .objective_c, .objective_cpp])
231+
default:
232+
self.reopenDocuments(for: [language])
233+
}
234+
}
235+
guard let service = try SourceKit.languageService(for: toolchain, language, options: options, client: self, reopenDocuments: reopenDocuments) else {
197236
return nil
198237
}
199238

@@ -220,7 +259,8 @@ public final class SourceKitServer: LanguageServer {
220259
}
221260
}
222261

223-
func languageService(for uri: DocumentURI, _ language: Language, in workspace: Workspace) -> ToolchainLanguageServer? {
262+
/// **Public for testing purposes only**
263+
public func languageService(for uri: DocumentURI, _ language: Language, in workspace: Workspace) -> ToolchainLanguageServer? {
224264
if let service = workspace.documentService[uri] {
225265
return service
226266
}
@@ -418,13 +458,17 @@ extension SourceKitServer {
418458
// MARK: - Text synchronization
419459

420460
func openDocument(_ note: Notification<DidOpenTextDocumentNotification>, workspace: Workspace) {
421-
workspace.documentManager.open(note.params)
461+
openDocument(note.params, workspace: workspace)
462+
}
463+
464+
private func openDocument(_ note: DidOpenTextDocumentNotification, workspace: Workspace) {
465+
workspace.documentManager.open(note)
422466

423-
let textDocument = note.params.textDocument
467+
let textDocument = note.textDocument
424468
workspace.buildSettings.registerForChangeNotifications(for: textDocument.uri)
425469

426470
if let service = languageService(for: textDocument.uri, textDocument.language, in: workspace) {
427-
service.openDocument(note.params)
471+
service.openDocument(note)
428472
}
429473
}
430474

@@ -762,17 +806,18 @@ public func languageService(
762806
for toolchain: Toolchain,
763807
_ language: Language,
764808
options: SourceKitServer.Options,
765-
client: MessageHandler) throws -> ToolchainLanguageServer?
809+
client: MessageHandler,
810+
reopenDocuments: @escaping () -> Void) throws -> ToolchainLanguageServer?
766811
{
767812
switch language {
768813

769814
case .c, .cpp, .objective_c, .objective_cpp:
770815
guard toolchain.clangd != nil else { return nil }
771-
return try makeJSONRPCClangServer(client: client, toolchain: toolchain, buildSettings: (client as? SourceKitServer)?.workspace?.buildSettings, clangdOptions: options.clangdOptions)
816+
return try makeJSONRPCClangServer(client: client, toolchain: toolchain, buildSettings: (client as? SourceKitServer)?.workspace?.buildSettings, clangdOptions: options.clangdOptions, reopenDocuments: reopenDocuments)
772817

773818
case .swift:
774819
guard let sourcekitd = toolchain.sourcekitd else { return nil }
775-
return try makeLocalSwiftServer(client: client, sourcekitd: sourcekitd, buildSettings: (client as? SourceKitServer)?.workspace?.buildSettings, clientCapabilities: (client as? SourceKitServer)?.workspace?.clientCapabilities)
820+
return try makeLocalSwiftServer(client: client, sourcekitd: sourcekitd, buildSettings: (client as? SourceKitServer)?.workspace?.buildSettings, clientCapabilities: (client as? SourceKitServer)?.workspace?.clientCapabilities, reopenDocuments: reopenDocuments)
776821

777822
default:
778823
return nil

Sources/SourceKit/ToolchainLanguageServer.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@
1313
import Foundation
1414
import LanguageServerProtocol
1515

16+
/// The state of a `ToolchainLanguageServer`
17+
public enum LanguageServerState {
18+
/// The language server is running with semantic functionality enabled
19+
case connected
20+
/// The language server server has crashed and we are waiting for it to relaunch
21+
case connectionInterrupted
22+
/// The language server has relaunched but semantic functionality is currently disabled
23+
case semanticFunctionalityDisabled
24+
/// The langauge server has been shut down gracefully
25+
case shutDown
26+
}
27+
1628
/// A `LanguageServer` that exists within the context of the current process.
1729
public protocol ToolchainLanguageServer: AnyObject {
1830

@@ -21,6 +33,9 @@ public protocol ToolchainLanguageServer: AnyObject {
2133
func initializeSync(_ initialize: InitializeRequest) throws -> InitializeResult
2234
func clientInitialized(_ initialized: InitializedNotification)
2335

36+
/// Add a handler that is called whenever the state of the language server changes.
37+
func addStateChangeHandler(handler: @escaping (_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void)
38+
2439
// MARK: - Text synchronization
2540

2641
func openDocument(_ note: DidOpenTextDocumentNotification)

Sources/SourceKit/clangd/ClangLanguageServer.swift

Lines changed: 124 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,136 @@ import LSPLogging
1717
import SKCore
1818
import SKSupport
1919
import TSCBasic
20+
import Dispatch
2021

2122
/// A thin wrapper over a connection to a clangd server providing build setting handling.
2223
final class ClangLanguageServerShim: ToolchainLanguageServer {
2324

2425
/// The server's request queue, used to serialize requests and responses to `clangd`.
2526
public let queue: DispatchQueue = DispatchQueue(label: "clangd-language-server-queue", qos: .userInitiated)
2627

27-
let clangd: Connection
28+
/// The connection to `clangd`. `nil` before `initialize` has been called.
29+
private var clangd: Connection!
2830

29-
var capabilities: ServerCapabilities? = nil
31+
private var capabilities: ServerCapabilities? = nil
3032

31-
let buildSystem: BuildSystem
33+
private let buildSystem: BuildSystem
3234

33-
let clang: AbsolutePath?
35+
private let clangdPath: AbsolutePath
3436

35-
/// 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-
self.clangd = clangd
39-
self.buildSystem = buildSystem
40-
self.clang = clang
37+
private let clangdOptions: [String]
38+
39+
private let client: MessageHandler
40+
41+
private var state: LanguageServerState {
42+
didSet {
43+
for handler in stateChangeHandlers {
44+
handler(oldValue, state)
45+
}
46+
}
47+
}
48+
49+
private var stateChangeHandlers: [(_ oldState: LanguageServerState, _ newState: LanguageServerState) -> Void] = []
50+
51+
/// Ask the parent of this language server to re-open all documents in this language server.
52+
private let reopenDocuments: () -> Void
53+
54+
/// The `InitializeRequest` with which `clangd` was originally initialized.
55+
/// Stored so we can replay the initialization when clangd crashes.
56+
private var initializeRequest: InitializeRequest?
57+
58+
public init(client: MessageHandler,
59+
clangdPath: AbsolutePath,
60+
buildSettings: BuildSystem?,
61+
clangdOptions: [String],
62+
reopenDocuments: @escaping () -> Void
63+
) throws {
64+
self.client = client
65+
self.buildSystem = buildSettings ?? BuildSystemList()
66+
self.clangdPath = clangdPath
67+
self.clangdOptions = clangdOptions
68+
self.state = .connected
69+
self.reopenDocuments = reopenDocuments
70+
}
71+
72+
func initialize() throws {
73+
let clientToServer: Pipe = Pipe()
74+
let serverToClient: Pipe = Pipe()
75+
76+
let connection = JSONRPCConnection(
77+
protocol: MessageRegistry.lspProtocol,
78+
inFD: serverToClient.fileHandleForReading.fileDescriptor,
79+
outFD: clientToServer.fileHandleForWriting.fileDescriptor
80+
)
81+
82+
self.clangd = connection
83+
84+
connection.start(receiveHandler: client)
85+
86+
let process = Foundation.Process()
87+
88+
if #available(OSX 10.13, *) {
89+
process.executableURL = clangdPath.asURL
90+
} else {
91+
process.launchPath = clangdPath.pathString
92+
}
93+
94+
process.arguments = [
95+
"-compile_args_from=lsp", // Provide compiler args programmatically.
96+
"-background-index=false", // Disable clangd indexing, we use the build
97+
"-index=false", // system index store instead.
98+
] + clangdOptions
99+
100+
process.standardOutput = serverToClient
101+
process.standardInput = clientToServer
102+
process.terminationHandler = { [weak self] process in
103+
log("clangd exited: \(process.terminationReason) \(process.terminationStatus)")
104+
connection.close()
105+
if process.terminationStatus != 0 {
106+
if let self = self {
107+
self.state = .connectionInterrupted
108+
self.restartClangd()
109+
}
110+
}
111+
}
112+
113+
if #available(OSX 10.13, *) {
114+
try process.run()
115+
} else {
116+
process.launch()
117+
}
118+
}
119+
120+
private func restartClangd() {
121+
precondition(self.state == .connectionInterrupted)
122+
123+
guard let initializeRequest = initializeRequest else {
124+
log("clangd crashed before it was sent an InitializeRequest. *Not* trying to restart.", level: .error)
125+
return
126+
}
127+
128+
do {
129+
try self.initialize()
130+
// FIXME: We assume that clangd will return the same capabilites after restarting.
131+
// Theoretically they could have changed and we would need to inform SourceKitServer about them.
132+
// But since SourceKitServer more or less ignores them right now anyway, this should be fine for now.
133+
_ = try self.initializeSync(initializeRequest)
134+
self.clientInitialized(InitializedNotification())
135+
self.reopenDocuments()
136+
self.state = .connected
137+
} catch {
138+
log("Failed to restart clangd after a crash, retrying in 5 seconds: \(error)", level: .error)
139+
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak self] in
140+
self?.restartClangd()
141+
}
142+
}
41143
}
42144

145+
func addStateChangeHandler(handler: @escaping (LanguageServerState, LanguageServerState) -> Void) {
146+
stateChangeHandlers.append(handler)
147+
}
148+
149+
43150
/// Forwards a request to the given connection, taking care of replying to the original request
44151
/// and cancellation, while providing a callback with the response for additional processing.
45152
///
@@ -73,6 +180,7 @@ final class ClangLanguageServerShim: ToolchainLanguageServer {
73180
extension ClangLanguageServerShim {
74181

75182
func initializeSync(_ initialize: InitializeRequest) throws -> InitializeResult {
183+
self.initializeRequest = initialize
76184
let result = try clangd.sendSync(initialize)
77185
self.capabilities = result.capabilities
78186
return result
@@ -124,7 +232,7 @@ extension ClangLanguageServerShim {
124232
if let settings = settings {
125233
clangd.send(DidChangeConfigurationNotification(settings: .clangd(
126234
ClangWorkspaceSettings(
127-
compilationDatabaseChanges: [url.path: ClangCompileCommand(settings, clang: clang)]))))
235+
compilationDatabaseChanges: [url.path: ClangCompileCommand(settings, clang: clangdPath)]))))
128236
}
129237
}
130238

@@ -191,60 +299,16 @@ func makeJSONRPCClangServer(
191299
client: MessageHandler,
192300
toolchain: Toolchain,
193301
buildSettings: BuildSystem?,
194-
clangdOptions: [String]
302+
clangdOptions: [String],
303+
reopenDocuments: @escaping () -> Void
195304
) throws -> ToolchainLanguageServer {
196305
guard let clangd = toolchain.clangd else {
197306
preconditionFailure("missing clang from toolchain \(toolchain.identifier)")
198307
}
199308

200-
let clientToServer: Pipe = Pipe()
201-
let serverToClient: Pipe = Pipe()
202-
203-
let connection = JSONRPCConnection(
204-
protocol: MessageRegistry.lspProtocol,
205-
inFD: serverToClient.fileHandleForReading.fileDescriptor,
206-
outFD: clientToServer.fileHandleForWriting.fileDescriptor
207-
)
208-
209-
let connectionToClient = LocalConnection()
210-
211-
let shim = try ClangLanguageServerShim(
212-
client: connectionToClient,
213-
clangd: connection,
214-
buildSystem: buildSettings ?? BuildSystemList(),
215-
clang: toolchain.clang)
216-
217-
connectionToClient.start(handler: client)
218-
connection.start(receiveHandler: client)
219-
220-
let process = Foundation.Process()
221-
222-
if #available(OSX 10.13, *) {
223-
process.executableURL = clangd.asURL
224-
} else {
225-
process.launchPath = clangd.pathString
226-
}
227-
228-
process.arguments = [
229-
"-compile_args_from=lsp", // Provide compiler args programmatically.
230-
"-background-index=false", // Disable clangd indexing, we use the build
231-
"-index=false", // system index store instead.
232-
] + clangdOptions
233-
234-
process.standardOutput = serverToClient
235-
process.standardInput = clientToServer
236-
process.terminationHandler = { process in
237-
log("clangd exited: \(process.terminationReason) \(process.terminationStatus)")
238-
connection.close()
239-
}
240-
241-
if #available(OSX 10.13, *) {
242-
try process.run()
243-
} else {
244-
process.launch()
245-
}
246-
247-
return shim
309+
let server = try ClangLanguageServerShim(client: client, clangdPath: clangd, buildSettings: buildSettings, clangdOptions: clangdOptions, reopenDocuments: reopenDocuments)
310+
try server.initialize()
311+
return server
248312
}
249313

250314
extension ClangCompileCommand {

0 commit comments

Comments
 (0)