Skip to content

Commit 57e53e0

Browse files
committed
Add option to generate an output mirror file that contains the exact data sent from SourceKit-LSP to the client
This could help us debug low-level issues of SourceKit-LSP to client communication such as #1890.
1 parent e93b7ed commit 57e53e0

File tree

5 files changed

+67
-28
lines changed

5 files changed

+67
-28
lines changed

Documentation/Configuration File.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The structure of the file is currently not guaranteed to be stable. Options may
4444
- `level: "debug"|"info"|"default"|"error"|"fault"`: The level from which one onwards log messages should be written.
4545
- `privacyLevel: "public"|"private"|"sensitive"`: Whether potentially sensitive information should be redacted. Default is `public`, which redacts potentially sensitive information.
4646
- `inputMirrorDirectory: string`: Write all input received by SourceKit-LSP on stdin to a file in this directory. Useful to record and replay an entire SourceKit-LSP session.
47+
- `outputMirrorDirectory: string`: Write all data sent from SourceKit-LSP to the client to a file in this directory. Useful to record the raw communication between SourceKit-LSP and the client on a low level.
4748
- `defaultWorkspaceType: "buildServer"|"compilationDatabase"|"swiftPM"`: Default workspace type. Overrides workspace type selection logic.
4849
- `generatedFilesPath: string`: Directory in which generated interfaces and macro expansions should be stored.
4950
- `backgroundIndexing: boolean`: Whether background indexing is enabled.

Sources/LanguageServerProtocolJSONRPC/JSONRPCConnection.swift

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ public final class JSONRPCConnection: Connection {
6161
private let sendIO: DispatchIO
6262
private let messageRegistry: MessageRegistry
6363

64+
/// If non-nil, all input received by this `JSONRPCConnection` will be written to the file handle
65+
let inputMirrorFile: FileHandle?
66+
67+
/// If non-nil, all output created by this `JSONRPCConnection` will be written to the file handle
68+
let outputMirrorFile: FileHandle?
69+
6470
enum State {
6571
case created, running, closed
6672
}
@@ -120,9 +126,13 @@ public final class JSONRPCConnection: Connection {
120126
name: String,
121127
protocol messageRegistry: MessageRegistry,
122128
inFD: FileHandle,
123-
outFD: FileHandle
129+
outFD: FileHandle,
130+
inputMirrorFile: FileHandle? = nil,
131+
outputMirrorFile: FileHandle? = nil
124132
) {
125133
self.name = name
134+
self.inputMirrorFile = inputMirrorFile
135+
self.outputMirrorFile = outputMirrorFile
126136
self.receiveHandler = nil
127137
#if os(Linux) || os(Android)
128138
// We receive a `SIGPIPE` if we write to a pipe that points to a crashed process. This in particular happens if the
@@ -268,13 +278,10 @@ public final class JSONRPCConnection: Connection {
268278
/// Start processing `inFD` and send messages to `receiveHandler`.
269279
///
270280
/// - parameter receiveHandler: The message handler to invoke for requests received on the `inFD`.
271-
/// - parameter mirrorFile: If non-nil, all input received by this `JSONRPCConnection` will be written to the file
272-
/// handle
273281
///
274282
/// - Important: `start` must be called before sending any data over the `JSONRPCConnection`.
275283
public func start(
276284
receiveHandler: MessageHandler,
277-
mirrorFile: FileHandle? = nil,
278285
closeHandler: @escaping @Sendable () async -> Void = {}
279286
) {
280287
queue.sync {
@@ -303,8 +310,8 @@ public final class JSONRPCConnection: Connection {
303310
return
304311
}
305312

306-
orLog("Writing data mirror file") {
307-
try mirrorFile?.write(contentsOf: data)
313+
orLog("Writing input mirror file") {
314+
try self.inputMirrorFile?.write(contentsOf: data)
308315
}
309316

310317
// Parse and handle any messages in `buffer + data`, leaving any remaining unparsed bytes in `buffer`.
@@ -538,6 +545,9 @@ public final class JSONRPCConnection: Connection {
538545
dispatchPrecondition(condition: .onQueue(queue))
539546
guard readyToSend() else { return }
540547

548+
orLog("Writing output mirror file") {
549+
try outputMirrorFile?.write(contentsOf: dispatchData)
550+
}
541551
sendIO.write(offset: 0, data: dispatchData, queue: sendQueue) { [weak self] done, _, errorCode in
542552
if errorCode != 0 {
543553
logger.fault("IO error sending message \(errorCode)")

Sources/SKOptions/SourceKitLSPOptions.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,21 +223,29 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable {
223223
/// Useful to record and replay an entire SourceKit-LSP session.
224224
public var inputMirrorDirectory: String?
225225

226+
/// Write all data sent from SourceKit-LSP to the client to a file in this directory.
227+
///
228+
/// Useful to record the raw communication between SourceKit-LSP and the client on a low level.
229+
public var outputMirrorDirectory: String?
230+
226231
public init(
227232
level: String? = nil,
228233
privacyLevel: String? = nil,
229-
inputMirrorDirectory: String? = nil
234+
inputMirrorDirectory: String? = nil,
235+
outputMirrorDirectory: String? = nil
230236
) {
231237
self.level = level
232238
self.privacyLevel = privacyLevel
233239
self.inputMirrorDirectory = inputMirrorDirectory
240+
self.outputMirrorDirectory = outputMirrorDirectory
234241
}
235242

236243
static func merging(base: LoggingOptions, override: LoggingOptions?) -> LoggingOptions {
237244
return LoggingOptions(
238245
level: override?.level ?? base.level,
239246
privacyLevel: override?.privacyLevel ?? base.privacyLevel,
240-
inputMirrorDirectory: override?.inputMirrorDirectory ?? base.inputMirrorDirectory
247+
inputMirrorDirectory: override?.inputMirrorDirectory ?? base.inputMirrorDirectory,
248+
outputMirrorDirectory: override?.outputMirrorDirectory ?? base.outputMirrorDirectory
241249
)
242250
}
243251
}

Sources/sourcekit-lsp/SourceKitLSP.swift

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,22 @@ struct SourceKitLSP: AsyncParsableCommand {
226226
return options
227227
}
228228

229+
/// Create a new file that can be used to use as an input or output mirror file and return a file handle that can be
230+
/// used to write to that file.
231+
private func createMirrorFile(in directory: URL) throws -> FileHandle {
232+
let dateFormatter = ISO8601DateFormatter()
233+
dateFormatter.timeZone = NSTimeZone.local
234+
let date = dateFormatter.string(from: Date()).replacingOccurrences(of: ":", with: "-")
235+
236+
let inputMirrorURL = directory.appendingPathComponent("\(date).log")
237+
238+
logger.log("Mirroring input to \(inputMirrorURL)")
239+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
240+
FileManager.default.createFile(atPath: try inputMirrorURL.filePath, contents: nil)
241+
242+
return try FileHandle(forWritingTo: inputMirrorURL)
243+
}
244+
229245
func run() async throws {
230246
// Dup stdout and redirect the fd to stderr so that a careless print()
231247
// will not break our connection stream.
@@ -262,35 +278,35 @@ struct SourceKitLSP: AsyncParsableCommand {
262278
)
263279
cleanOldLogFiles(logFileDirectory: logFileDirectoryURL, maxAge: 60 * 60 /* 1h */)
264280

281+
let inputMirror = orLog("Setting up input mirror") {
282+
if let inputMirrorDirectory = globalConfigurationOptions.loggingOrDefault.inputMirrorDirectory {
283+
return try createMirrorFile(in: URL(fileURLWithPath: inputMirrorDirectory))
284+
} else {
285+
return nil
286+
}
287+
}
288+
289+
let outputMirror = orLog("Setting up output mirror") {
290+
if let outputMirrorDirectory = globalConfigurationOptions.loggingOrDefault.outputMirrorDirectory {
291+
return try createMirrorFile(in: URL(fileURLWithPath: outputMirrorDirectory))
292+
} else {
293+
return nil
294+
}
295+
}
296+
265297
let clientConnection = JSONRPCConnection(
266298
name: "client",
267299
protocol: MessageRegistry.lspProtocol,
268300
inFD: FileHandle.standardInput,
269-
outFD: realStdoutHandle
301+
outFD: realStdoutHandle,
302+
inputMirrorFile: inputMirror,
303+
outputMirrorFile: outputMirror
270304
)
271305

272306
// For reasons that are completely oblivious to me, `DispatchIO.write`, which is used to write LSP responses to
273307
// stdout fails with error code 5 on Windows unless we call `AbsolutePath(validating:)` on some URL first.
274308
_ = try AbsolutePath(validating: Bundle.main.bundlePath)
275309

276-
var inputMirror: FileHandle? = nil
277-
if let inputMirrorDirectory = globalConfigurationOptions.loggingOrDefault.inputMirrorDirectory {
278-
orLog("Setting up input mirror") {
279-
let dateFormatter = ISO8601DateFormatter()
280-
dateFormatter.timeZone = NSTimeZone.local
281-
let date = dateFormatter.string(from: Date()).replacingOccurrences(of: ":", with: "-")
282-
283-
let inputMirrorDirectory = URL(fileURLWithPath: inputMirrorDirectory)
284-
let inputMirrorURL = inputMirrorDirectory.appendingPathComponent("\(date).log")
285-
286-
logger.log("Mirroring input to \(inputMirrorURL)")
287-
try FileManager.default.createDirectory(at: inputMirrorDirectory, withIntermediateDirectories: true)
288-
FileManager.default.createFile(atPath: try inputMirrorURL.filePath, contents: nil)
289-
290-
inputMirror = try FileHandle(forWritingTo: inputMirrorURL)
291-
}
292-
}
293-
294310
let server = SourceKitLSPServer(
295311
client: clientConnection,
296312
toolchainRegistry: ToolchainRegistry(installPath: Bundle.main.bundleURL),
@@ -302,7 +318,6 @@ struct SourceKitLSP: AsyncParsableCommand {
302318
)
303319
clientConnection.start(
304320
receiveHandler: server,
305-
mirrorFile: inputMirror,
306321
closeHandler: {
307322
await server.prepareForExit()
308323
// Use _Exit to avoid running static destructors due to https://github.com/swiftlang/swift/issues/55112.

config.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@
173173
"markdownDescription" : "The level from which one onwards log messages should be written.",
174174
"type" : "string"
175175
},
176+
"outputMirrorDirectory" : {
177+
"description" : "Write all data sent from SourceKit-LSP to the client to a file in this directory. Useful to record the raw communication between SourceKit-LSP and the client on a low level.",
178+
"markdownDescription" : "Write all data sent from SourceKit-LSP to the client to a file in this directory. Useful to record the raw communication between SourceKit-LSP and the client on a low level.",
179+
"type" : "string"
180+
},
176181
"privacyLevel" : {
177182
"description" : "Whether potentially sensitive information should be redacted. Default is `public`, which redacts potentially sensitive information.",
178183
"enum" : [

0 commit comments

Comments
 (0)