File: ExternalBuildSystemAdapter.swift

package info (click to toggle)
swiftlang 6.1.3-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,791,748 kB
  • sloc: cpp: 9,901,738; ansic: 2,201,433; asm: 1,091,827; python: 308,252; objc: 82,166; f90: 80,126; lisp: 38,358; pascal: 25,559; sh: 20,429; ml: 5,058; perl: 4,745; makefile: 4,484; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (312 lines) | stat: -rw-r--r-- 11,708 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
//===----------------------------------------------------------------------===//
//
// 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 LanguageServerProtocolExtensions
import LanguageServerProtocolJSONRPC
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 system 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 ExternalBuildSystemAdapter {
  /// 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 `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 searchForConfig(
    in workspaceFolder: URL,
    onlyConsiderRoot: Bool,
    options: SourceKitLSPOptions
  ) -> BuildSystemSpec? {
    guard let configPath = getConfigPath(for: workspaceFolder, onlyConsiderRoot: onlyConsiderRoot) else {
      return nil
    }

    return BuildSystemSpec(kind: .buildServer, 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 `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 {
    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
    }

    return try JSONRPCConnection.start(
      executable: serverPath,
      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: URL? = nil, onlyConsiderRoot: Bool = false) -> URL? {
    var buildServerConfigLocations: [URL?] = []
    if let workspaceFolder = workspaceFolder {
      buildServerConfigLocations.append(workspaceFolder.appendingPathComponent(".bsp"))
    }

    if !onlyConsiderRoot {
      #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 {
          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?.appendingPathComponent("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))
  }
}

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)
  }
}