mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
284 lines
10 KiB
Swift
284 lines
10 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2024 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 BuildServerProtocol
|
|
import Foundation
|
|
import LanguageServerProtocol
|
|
import LanguageServerProtocolJSONRPC
|
|
import SKLogging
|
|
import SKOptions
|
|
import SKSupport
|
|
import SwiftExtensions
|
|
import TSCExtensions
|
|
|
|
import struct TSCBasic.AbsolutePath
|
|
import func TSCBasic.getEnvSearchPaths
|
|
import var TSCBasic.localFileSystem
|
|
import func TSCBasic.lookupExecutablePath
|
|
|
|
private func executable(_ name: String) -> String {
|
|
#if os(Windows)
|
|
guard !name.hasSuffix(".exe") else { return name }
|
|
return "\(name).exe"
|
|
#else
|
|
return name
|
|
#endif
|
|
}
|
|
|
|
private let python3ExecutablePath: AbsolutePath? = {
|
|
let pathVariable: String
|
|
#if os(Windows)
|
|
pathVariable = "Path"
|
|
#else
|
|
pathVariable = "PATH"
|
|
#endif
|
|
let searchPaths =
|
|
getEnvSearchPaths(
|
|
pathString: ProcessInfo.processInfo.environment[pathVariable],
|
|
currentWorkingDirectory: localFileSystem.currentWorkingDirectory
|
|
)
|
|
|
|
return lookupExecutablePath(filename: executable("python3"), searchPaths: searchPaths)
|
|
?? lookupExecutablePath(filename: executable("python"), searchPaths: searchPaths)
|
|
}()
|
|
|
|
struct ExecutableNotFoundError: Error {
|
|
let executableName: String
|
|
}
|
|
|
|
enum BuildServerNotFoundError: Error {
|
|
case fileNotFound
|
|
}
|
|
|
|
private struct BuildServerConfig: Codable {
|
|
/// The name of the build tool.
|
|
let name: String
|
|
|
|
/// The version of the build tool.
|
|
let version: String
|
|
|
|
/// The bsp version of the build tool.
|
|
let bspVersion: String
|
|
|
|
/// A collection of languages supported by this BSP server.
|
|
let languages: [String]
|
|
|
|
/// Command arguments runnable via system processes to start a BSP server.
|
|
let argv: [String]
|
|
|
|
static func load(from path: AbsolutePath) throws -> BuildServerConfig {
|
|
let decoder = JSONDecoder()
|
|
let fileData = try localFileSystem.readFileContents(path).contents
|
|
return try decoder.decode(BuildServerConfig.self, from: Data(fileData))
|
|
}
|
|
}
|
|
|
|
/// Launches a subprocess that is a BSP server and manages the process's lifetime.
|
|
actor ExternalBuildSystemAdapter {
|
|
private let projectRoot: AbsolutePath
|
|
|
|
/// The `BuildSystemManager` that handles messages from the BSP server to SourceKit-LSP.
|
|
var messagesToSourceKitLSPHandler: MessageHandler
|
|
|
|
/// The JSON-RPC connection between SourceKit-LSP and the BSP server.
|
|
private(set) var connectionToBuildServer: JSONRPCConnection?
|
|
|
|
/// After a `build/initialize` request has been sent to the BSP server, that request, so we can replay it in case the
|
|
/// server crashes.
|
|
private var initializeRequest: InitializeBuildRequest?
|
|
|
|
/// The date at which `clangd` was last restarted.
|
|
/// Used to delay restarting in case of a crash loop.
|
|
private var lastRestart: Date?
|
|
|
|
static package func projectRoot(for workspaceFolder: AbsolutePath, options: SourceKitLSPOptions) -> AbsolutePath? {
|
|
guard getConfigPath(for: workspaceFolder) != nil else {
|
|
return nil
|
|
}
|
|
return workspaceFolder
|
|
}
|
|
|
|
init(
|
|
projectRoot: AbsolutePath,
|
|
messagesToSourceKitLSPHandler: MessageHandler
|
|
) async throws {
|
|
self.projectRoot = projectRoot
|
|
self.messagesToSourceKitLSPHandler = messagesToSourceKitLSPHandler
|
|
self.connectionToBuildServer = try await self.createConnectionToBspServer()
|
|
}
|
|
|
|
/// Change the handler that handles messages from the build server.
|
|
///
|
|
/// The intended use of this is to intercept messages from the build server by `LegacyBuildServerBuildSystem`.
|
|
func changeMessageToSourceKitLSPHandler(to newHandler: MessageHandler) {
|
|
messagesToSourceKitLSPHandler = newHandler
|
|
connectionToBuildServer?.changeReceiveHandler(messagesToSourceKitLSPHandler)
|
|
}
|
|
|
|
/// Send a notification to the build server.
|
|
func send(_ notification: some NotificationType) {
|
|
guard let connectionToBuildServer else {
|
|
logger.error("Dropping notification because BSP server has crashed: \(notification.forLogging)")
|
|
return
|
|
}
|
|
connectionToBuildServer.send(notification)
|
|
}
|
|
|
|
/// Send a request to the build server.
|
|
func send<Request: RequestType>(_ request: Request) async throws -> Request.Response {
|
|
guard let connectionToBuildServer else {
|
|
throw ResponseError.internalError("BSP server has crashed")
|
|
}
|
|
if let request = request as? InitializeBuildRequest {
|
|
if initializeRequest != nil {
|
|
logger.error("BSP server was initialized multiple times")
|
|
}
|
|
self.initializeRequest = request
|
|
}
|
|
return try await connectionToBuildServer.send(request)
|
|
}
|
|
|
|
/// Create a new JSONRPCConnection to the build server.
|
|
private func createConnectionToBspServer() async throws -> JSONRPCConnection {
|
|
guard let configPath = ExternalBuildSystemAdapter.getConfigPath(for: self.projectRoot) else {
|
|
throw BuildServerNotFoundError.fileNotFound
|
|
}
|
|
|
|
let serverConfig = try BuildServerConfig.load(from: configPath)
|
|
var serverPath = try AbsolutePath(validating: serverConfig.argv[0], relativeTo: projectRoot)
|
|
var serverArgs = Array(serverConfig.argv[1...])
|
|
|
|
if serverPath.suffix == ".py" {
|
|
serverArgs = [serverPath.pathString] + serverArgs
|
|
guard let interpreterPath = python3ExecutablePath else {
|
|
throw ExecutableNotFoundError(executableName: "python3")
|
|
}
|
|
|
|
serverPath = interpreterPath
|
|
}
|
|
|
|
return try JSONRPCConnection.start(
|
|
executable: serverPath.asURL,
|
|
arguments: serverArgs,
|
|
name: "BSP-Server",
|
|
protocol: bspRegistry,
|
|
stderrLoggingCategory: "bsp-server-stderr",
|
|
client: messagesToSourceKitLSPHandler,
|
|
terminationHandler: { [weak self] terminationStatus in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if terminationStatus != 0 {
|
|
Task {
|
|
await orLog("Restarting BSP server") {
|
|
try await self.handleBspServerCrash()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
).connection
|
|
}
|
|
|
|
private static func getConfigPath(for workspaceFolder: AbsolutePath? = nil) -> AbsolutePath? {
|
|
var buildServerConfigLocations: [URL?] = []
|
|
if let workspaceFolder = workspaceFolder {
|
|
buildServerConfigLocations.append(workspaceFolder.appending(component: ".bsp").asURL)
|
|
}
|
|
|
|
#if os(Windows)
|
|
if let localAppData = ProcessInfo.processInfo.environment["LOCALAPPDATA"] {
|
|
buildServerConfigLocations.append(URL(fileURLWithPath: localAppData).appendingPathComponent("bsp"))
|
|
}
|
|
if let programData = ProcessInfo.processInfo.environment["PROGRAMDATA"] {
|
|
buildServerConfigLocations.append(URL(fileURLWithPath: programData).appendingPathComponent("bsp"))
|
|
}
|
|
#else
|
|
if let xdgDataHome = ProcessInfo.processInfo.environment["XDG_DATA_HOME"] {
|
|
buildServerConfigLocations.append(URL(fileURLWithPath: xdgDataHome).appendingPathComponent("bsp"))
|
|
}
|
|
|
|
if let libraryUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
|
|
buildServerConfigLocations.append(libraryUrl.appendingPathComponent("bsp"))
|
|
}
|
|
|
|
if let xdgDataDirs = ProcessInfo.processInfo.environment["XDG_DATA_DIRS"] {
|
|
buildServerConfigLocations += xdgDataDirs.split(separator: ":").map { xdgDataDir in
|
|
URL(fileURLWithPath: String(xdgDataDir)).appendingPathComponent("bsp")
|
|
}
|
|
}
|
|
|
|
if let libraryUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .systemDomainMask).first {
|
|
buildServerConfigLocations.append(libraryUrl.appendingPathComponent("bsp"))
|
|
}
|
|
#endif
|
|
|
|
for case let buildServerConfigLocation? in buildServerConfigLocations {
|
|
let jsonFiles =
|
|
try? FileManager.default.contentsOfDirectory(at: buildServerConfigLocation, includingPropertiesForKeys: nil)
|
|
.filter { $0.pathExtension == "json" }
|
|
|
|
if let configFileURL = jsonFiles?.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }).first,
|
|
let configFilePath = AbsolutePath(validatingOrNil: try? configFileURL.filePath)
|
|
{
|
|
return configFilePath
|
|
}
|
|
}
|
|
|
|
// Pre Swift 6.1 SourceKit-LSP looked for `buildServer.json` in the project root. Maintain this search location for
|
|
// compatibility even though it's not a standard BSP search location.
|
|
if let workspaceFolder = workspaceFolder,
|
|
localFileSystem.isFile(workspaceFolder.appending(component: "buildServer.json"))
|
|
{
|
|
return workspaceFolder.appending(component: "buildServer.json")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Restart the BSP server after it has crashed.
|
|
private func handleBspServerCrash() async throws {
|
|
// Set `connectionToBuildServer` to `nil` to indicate that there is currently no BSP server running.
|
|
connectionToBuildServer = nil
|
|
|
|
guard let initializeRequest else {
|
|
logger.error("BSP server crashed before it was sent an initialize request. Not restarting.")
|
|
return
|
|
}
|
|
|
|
logger.error("The BSP server has crashed. Restarting.")
|
|
let restartDelay: Duration
|
|
if let lastClangdRestart = self.lastRestart, Date().timeIntervalSince(lastClangdRestart) < 30 {
|
|
logger.log("BSP server has been restarted in the last 30 seconds. Delaying another restart by 10 seconds.")
|
|
restartDelay = .seconds(10)
|
|
} else {
|
|
restartDelay = .zero
|
|
}
|
|
self.lastRestart = Date()
|
|
|
|
try await Task.sleep(for: restartDelay)
|
|
|
|
let restartedConnection = try await self.createConnectionToBspServer()
|
|
|
|
// We assume that the server returns the same initialize response after being restarted.
|
|
// BSP does not set any state from the client to the server, so there are no other requests we need to replay
|
|
// (other than `textDocument/registerForChanges`, which is only used by the legacy BSP protocol, which didn't have
|
|
// crash recovery and doesn't need to gain it because it is deprecated).
|
|
_ = try await restartedConnection.send(initializeRequest)
|
|
restartedConnection.send(OnBuildInitializedNotification())
|
|
self.connectionToBuildServer = restartedConnection
|
|
|
|
// The build targets might have changed after the restart. Send a `buildTarget/didChange` notification to
|
|
// SourceKit-LSP to discard cached information.
|
|
self.messagesToSourceKitLSPHandler.handle(OnBuildTargetDidChangeNotification(changes: nil))
|
|
}
|
|
}
|