From 9f8c0f623331ad841fbbf4468f77bc4646949f54 Mon Sep 17 00:00:00 2001 From: Manuel M T Chakravarty Date: Mon, 14 Aug 2023 18:12:42 +0200 Subject: [PATCH] Add user script data channel A user script data channel connects to a process started via an application user script. The crucial difference to a plain local process is that the subprocess does not inherit the sandbox of the application. --- .../DataChannel+UserScript.swift | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Sources/LanguageClient/DataChannel+UserScript.swift diff --git a/Sources/LanguageClient/DataChannel+UserScript.swift b/Sources/LanguageClient/DataChannel+UserScript.swift new file mode 100644 index 0000000..2163acb --- /dev/null +++ b/Sources/LanguageClient/DataChannel+UserScript.swift @@ -0,0 +1,95 @@ +import Foundation +import LanguageServerProtocol +import JSONRPC + + +#if canImport(ProcessEnv) +import ProcessEnv + +#if compiler(>=5.9) + +/// The user script directory for this app. +/// +@available(macOS 12.0, *) +private let userScriptDirectory = try? FileManager.default.url(for: .applicationScriptsDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false) + +extension DataChannel { + + /// Create a `DataChannel` that connects to an application user script in the application scripts directory. + /// + /// - Parameters: + /// - scriptPath: The path of the application user script. + /// - arguments: The script arguments. + /// - terminationHandler: Termination handler to invoke when the user script terminates. + /// + @available(macOS 12.0, *) + public static func userScriptChannel(scriptPath: String, + arguments: [String] = [], + terminationHandler: @escaping @Sendable () -> Void) + throws -> DataChannel + { + guard let scriptURL = userScriptDirectory?.appendingPathComponent(scriptPath) else { + throw CocoaError(.fileNoSuchFile) + } + + // Allocate pipes for the standard handles + let stdinPipe = Pipe() + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + + let (stream, continuation) = DataSequence.makeStream() + + // Forward stdout to the data channel + Task { + let dataStream = stdoutPipe.fileHandleForReading.dataStream + let byteStream = AsyncByteSequence(base: dataStream) + let framedData = AsyncMessageFramingSequence(base: byteStream) + + for try await data in framedData { + continuation.yield(data) + } + + continuation.finish() + } + + // Log stderr + Task { + for try await line in stderrPipe.fileHandleForReading.bytes.lines { + print("stderr: ", line) + } + } + + // Launch the script asynchronously + Task { + + // NB: Needs to happen in the task as `NSUserUnixTask` is not sendable. + let unixTask = try NSUserUnixTask(url: scriptURL) + + unixTask.standardInput = stdinPipe.fileHandleForReading + unixTask.standardOutput = stdoutPipe.fileHandleForWriting + unixTask.standardError = stderrPipe.fileHandleForWriting + + defer { + continuation.finish() + terminationHandler() + } + try await unixTask.execute(withArguments: arguments) + } + + // Forward messages from the data channel into stdin + let handler: DataChannel.WriteHandler = { + let data = MessageFraming.frame($0) + + try stdinPipe.fileHandleForWriting.write(contentsOf: data) + } + + return DataChannel(writeHandler: handler, dataSequence: stream) + } +} + +#endif + +#endif