Files
sourcekit-lsp/Sources/LSPTestSupport/TestJSONRPCConnection.swift
Alex Hoppen abd534b584 Fix a race condition in TestMessageHandler
Not entirely sure what introduced the race condition. Found by thread sanitizer. Probably wouldn’t have been an issue if we had migrated this code to strict concurrency checking.
2024-03-05 13:23:25 -08:00

231 lines
6.8 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import LanguageServerProtocol
import LanguageServerProtocolJSONRPC
import SKSupport
import XCTest
import class Foundation.Pipe
public final class TestJSONRPCConnection {
public let clientToServer: Pipe = Pipe()
public let serverToClient: Pipe = Pipe()
public let client: TestMessageHandler
public let clientConnection: JSONRPCConnection
public let server: TestServer
public let serverConnection: JSONRPCConnection
public init(allowUnexpectedNotification: Bool = true) {
clientConnection = JSONRPCConnection(
name: "client",
protocol: testMessageRegistry,
inFD: serverToClient.fileHandleForReading,
outFD: clientToServer.fileHandleForWriting
)
serverConnection = JSONRPCConnection(
name: "server",
protocol: testMessageRegistry,
inFD: clientToServer.fileHandleForReading,
outFD: serverToClient.fileHandleForWriting
)
client = TestMessageHandler(server: clientConnection, allowUnexpectedNotification: allowUnexpectedNotification)
server = TestServer(client: serverConnection)
clientConnection.start(receiveHandler: client) {
// FIXME: keep the pipes alive until we close the connection. This
// should be fixed systemically.
withExtendedLifetime(self) {}
}
serverConnection.start(receiveHandler: server) {
// FIXME: keep the pipes alive until we close the connection. This
// should be fixed systemically.
withExtendedLifetime(self) {}
}
}
public func close() {
clientConnection.close()
serverConnection.close()
}
}
public struct TestLocalConnection {
public let client: TestMessageHandler
public let clientConnection: LocalConnection = .init()
public let server: TestServer
public let serverConnection: LocalConnection = .init()
public init(allowUnexpectedNotification: Bool = true) {
client = TestMessageHandler(server: serverConnection, allowUnexpectedNotification: allowUnexpectedNotification)
server = TestServer(client: clientConnection)
clientConnection.start(handler: client)
serverConnection.start(handler: server)
}
public func close() {
clientConnection.close()
serverConnection.close()
}
}
public actor TestMessageHandler: MessageHandler {
/// The connection to the language client.
public let server: Connection
private let messageHandlingQueue = AsyncQueue<Serial>()
private var oneShotNotificationHandlers: [((Any) -> Void)] = []
private let allowUnexpectedNotification: Bool
public init(server: Connection, allowUnexpectedNotification: Bool = true) {
self.server = server
self.allowUnexpectedNotification = allowUnexpectedNotification
}
public func appendOneShotNotificationHandler<N: NotificationType>(_ handler: @escaping (N) -> Void) {
oneShotNotificationHandlers.append({ anyNote in
guard let note = anyNote as? N else {
fatalError("received notification of the wrong type \(anyNote); expected \(N.self)")
}
handler(note)
})
}
/// The LSP server sent a notification to the client. Handle it.
public nonisolated func handle(_ notification: some NotificationType, from clientID: ObjectIdentifier) {
messageHandlingQueue.async {
await self.handleNotificationImpl(notification)
}
}
public func handleNotificationImpl(_ notification: some NotificationType) {
guard !oneShotNotificationHandlers.isEmpty else {
if allowUnexpectedNotification { return }
fatalError("unexpected notification \(notification)")
}
let handler = oneShotNotificationHandlers.removeFirst()
handler(notification)
}
/// The LSP server sent a request to the client. Handle it.
public nonisolated func handle<Request: RequestType>(
_ request: Request,
id: RequestID,
from clientID: ObjectIdentifier,
reply: @escaping (LSPResult<Request.Response>) -> Void
) {
reply(.failure(.methodNotFound(Request.method)))
}
}
extension TestMessageHandler: Connection {
/// Send a notification to the LSP server.
public nonisolated func send(_ notification: some NotificationType) {
server.send(notification)
}
/// Send a request to the LSP server and (asynchronously) receive a reply.
public nonisolated func send<Request: RequestType>(
_ request: Request,
reply: @escaping (LSPResult<Request.Response>) -> Void
) -> RequestID {
return server.send(request, reply: reply)
}
}
public final class TestServer: MessageHandler {
public let client: Connection
init(client: Connection) {
self.client = client
}
public func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) {
if params is EchoNotification {
self.client.send(params)
} else {
fatalError("Unhandled notification")
}
}
public func handle<R: RequestType>(
_ params: R,
id: RequestID,
from clientID: ObjectIdentifier,
reply: @escaping (LSPResult<R.Response>) -> Void
) {
if let params = params as? EchoRequest {
reply(.success(params.string as! R.Response))
} else if let params = params as? EchoError {
if let code = params.code {
reply(.failure(ResponseError(code: code, message: params.message!)))
} else {
reply(.success(VoidResponse() as! R.Response))
}
} else {
fatalError("Unhandled request")
}
}
}
// MARK: Test requests.
private let testMessageRegistry = MessageRegistry(
requests: [EchoRequest.self, EchoError.self],
notifications: [EchoNotification.self]
)
#if swift(<5.11)
extension String: ResponseType {}
#else
extension String: @retroactive ResponseType {}
#endif
public struct EchoRequest: RequestType {
public static var method: String = "test_server/echo"
public typealias Response = String
public var string: String
public init(string: String) {
self.string = string
}
}
public struct EchoError: RequestType {
public static var method: String = "test_server/echo_error"
public typealias Response = VoidResponse
public var code: ErrorCode?
public var message: String?
public init(code: ErrorCode? = nil, message: String? = nil) {
self.code = code
self.message = message
}
}
public struct EchoNotification: NotificationType {
public static var method: String = "test_server/echo_note"
public var string: String
public init(string: String) {
self.string = string
}
}