mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
323 lines
12 KiB
Swift
323 lines
12 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
@_spi(SourceKitLSP) import BuildServerProtocol
|
|
import Foundation
|
|
@_spi(SourceKitLSP) import LanguageServerProtocol
|
|
@_spi(SourceKitLSP) import LanguageServerProtocolExtensions
|
|
@_spi(SourceKitLSP) import LanguageServerProtocolTransport
|
|
@_spi(SourceKitLSP) import SKLogging
|
|
import SKOptions
|
|
import SwiftExtensions
|
|
import TSCExtensions
|
|
|
|
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: URL? = {
|
|
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)?.asURL
|
|
?? lookupExecutablePath(filename: executable("python"), searchPaths: searchPaths)?.asURL
|
|
}()
|
|
|
|
struct ExecutableNotFoundError: Error {
|
|
let executableName: String
|
|
}
|
|
|
|
enum BuildServerNotFoundError: Error {
|
|
case fileNotFound
|
|
}
|
|
|
|
/// BSP configuration
|
|
///
|
|
/// See https://build-server-protocol.github.io/docs/overview/server-discovery#the-bsp-connection-details
|
|
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 server processes to start a BSP server.
|
|
let argv: [String]
|
|
|
|
static func load(from path: URL) throws -> BuildServerConfig {
|
|
let decoder = JSONDecoder()
|
|
let fileData = try Data(contentsOf: path)
|
|
return try decoder.decode(BuildServerConfig.self, from: fileData)
|
|
}
|
|
}
|
|
|
|
/// Launches a subprocess that is a BSP server and manages the process's lifetime.
|
|
actor ExternalBuildServerAdapter {
|
|
/// The root folder of the project. Used to resolve relative server paths.
|
|
private let projectRoot: URL
|
|
|
|
/// The file that specifies the configuration for this build server.
|
|
private let configPath: URL
|
|
|
|
/// The `BuildServerManager` 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 process that runs the external BSP server.
|
|
private var process: Process?
|
|
|
|
/// 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 searchForConfig(
|
|
in workspaceFolder: URL,
|
|
onlyConsiderRoot: Bool,
|
|
options: SourceKitLSPOptions
|
|
) -> BuildServerSpec? {
|
|
guard let configPath = getConfigPath(for: workspaceFolder, onlyConsiderRoot: onlyConsiderRoot) else {
|
|
return nil
|
|
}
|
|
|
|
return BuildServerSpec(kind: .externalBuildServer, projectRoot: workspaceFolder, configPath: configPath)
|
|
}
|
|
|
|
init(
|
|
projectRoot: URL,
|
|
configPath: URL,
|
|
messagesToSourceKitLSPHandler: MessageHandler
|
|
) async throws {
|
|
self.projectRoot = projectRoot
|
|
self.configPath = configPath
|
|
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 `LegacyBuildServer`.
|
|
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 {
|
|
let serverConfig = try BuildServerConfig.load(from: configPath)
|
|
var serverPath = URL(fileURLWithPath: serverConfig.argv[0], relativeTo: projectRoot.ensuringCorrectTrailingSlash)
|
|
var serverArgs = Array(serverConfig.argv[1...])
|
|
|
|
if serverPath.pathExtension == "py" {
|
|
serverArgs = [try serverPath.filePath] + serverArgs
|
|
guard let interpreterPath = python3ExecutablePath else {
|
|
throw ExecutableNotFoundError(executableName: "python3")
|
|
}
|
|
|
|
serverPath = interpreterPath
|
|
}
|
|
|
|
let (connection, process) = try JSONRPCConnection.start(
|
|
executable: serverPath,
|
|
arguments: serverArgs,
|
|
name: "BSP-Server",
|
|
protocol: MessageRegistry.bspProtocol,
|
|
stderrLoggingCategory: "bsp-server-stderr",
|
|
client: messagesToSourceKitLSPHandler,
|
|
terminationHandler: { [weak self] terminationReason in
|
|
guard let self else {
|
|
return
|
|
}
|
|
if terminationReason != .exited(exitCode: 0) {
|
|
Task {
|
|
await orLog("Restarting BSP server") {
|
|
try await self.handleBspServerCrash()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
)
|
|
self.process = process
|
|
return connection
|
|
}
|
|
|
|
private static func getConfigPath(for workspaceFolder: URL? = nil, onlyConsiderRoot: Bool = false) -> URL? {
|
|
var buildServerConfigLocations: [URL?] = []
|
|
if let workspaceFolder = workspaceFolder {
|
|
buildServerConfigLocations.append(workspaceFolder.appending(component: ".bsp"))
|
|
}
|
|
|
|
if !onlyConsiderRoot {
|
|
#if os(Windows)
|
|
if let localAppData = ProcessInfo.processInfo.environment["LOCALAPPDATA"] {
|
|
buildServerConfigLocations.append(URL(fileURLWithPath: localAppData).appending(component: "bsp"))
|
|
}
|
|
if let programData = ProcessInfo.processInfo.environment["PROGRAMDATA"] {
|
|
buildServerConfigLocations.append(URL(fileURLWithPath: programData).appending(component: "bsp"))
|
|
}
|
|
#else
|
|
if let xdgDataHome = ProcessInfo.processInfo.environment["XDG_DATA_HOME"] {
|
|
buildServerConfigLocations.append(URL(fileURLWithPath: xdgDataHome).appending(component: "bsp"))
|
|
}
|
|
|
|
if let libraryUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first {
|
|
buildServerConfigLocations.append(libraryUrl.appending(component: "bsp"))
|
|
}
|
|
|
|
if let xdgDataDirs = ProcessInfo.processInfo.environment["XDG_DATA_DIRS"] {
|
|
buildServerConfigLocations += xdgDataDirs.split(separator: ":").map { xdgDataDir in
|
|
URL(fileURLWithPath: String(xdgDataDir)).appending(component: "bsp")
|
|
}
|
|
}
|
|
|
|
if let libraryUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .systemDomainMask).first {
|
|
buildServerConfigLocations.append(libraryUrl.appending(component: "bsp"))
|
|
}
|
|
#endif
|
|
}
|
|
|
|
for case let buildServerConfigLocation? in buildServerConfigLocations {
|
|
let jsonFiles =
|
|
try? FileManager.default.contentsOfDirectory(at: buildServerConfigLocation, includingPropertiesForKeys: nil)
|
|
.filter {
|
|
guard let config = try? BuildServerConfig.load(from: $0) else {
|
|
return false
|
|
}
|
|
return !Set([Language.c, .cpp, .objective_c, .objective_cpp, .swift].map(\.rawValue))
|
|
.intersection(config.languages).isEmpty
|
|
}
|
|
|
|
if let configFileURL = jsonFiles?.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }).first {
|
|
return configFileURL
|
|
}
|
|
}
|
|
|
|
// 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 buildServerPath = workspaceFolder?.appending(component: "buildServer.json"),
|
|
FileManager.default.isFile(at: buildServerPath)
|
|
{
|
|
return buildServerPath
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
/// If the build server is still running after `duration`, terminate it. Otherwise a no-op.
|
|
package func terminateIfRunning(after duration: Duration) async throws {
|
|
try await process?.terminateIfRunning(after: duration)
|
|
}
|
|
}
|
|
|
|
fileprivate extension URL {
|
|
/// If the path of this URL represents a directory, ensure that it has a trailing slash.
|
|
///
|
|
/// This is important because if we form a file URL relative to eg. file:///tmp/a would assumes that `a` is a file
|
|
/// and use `/tmp` as the base, not `/tmp/a`.
|
|
var ensuringCorrectTrailingSlash: URL {
|
|
guard self.isFileURL else {
|
|
return self
|
|
}
|
|
// `URL(fileURLWithPath:)` checks the file system to decide whether a directory exists at the path.
|
|
return URL(fileURLWithPath: self.path)
|
|
}
|
|
}
|