mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
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.
231 lines
6.8 KiB
Swift
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
|
|
}
|
|
}
|