ChimeHQ / LanguageClient

Language Server Protocol (LSP) client for Swift
BSD 3-Clause "New" or "Revised" License
94 stars 10 forks source link

Calling diagnostics from within Xcode command plugin terminates the server #20

Closed steprescott closed 1 month ago

steprescott commented 1 month ago

Thank you for your work over sourcekit-lsp. I did start at writing my own client but then came across this and it really accelerated my development!

I have come into an issue when calling try await server.diagnostics(diagnosticParams) when the executable is ran from an Xcode command plugin.

In the video below you can see that when I run the Xcode command plugin, by right mouse clicking on the App project and selecting my LSP plugin, the lsp executable is triggered but the server is terminated as soon as diagnostics is called. However, the server does respond to the init message from within the plugin so I know it's not due to incorrect configuration of the plugin.

When I look to the termination reason I see it is 2 aka, uncaughtSignal with a status of 5. But that is as far as I have been able to debug so far. Within the same video you can see when I run the lsp command from Xcode or via terminal it correctly outputs the errors found in the textDocument.

I would appreciate your help and happy to pay for your time too.

I have attached a .zip file that has an example app that is just an unmodified File > New Project that links the local Swift Package of LSP. The LSP package holds the command that when ran calls diagnostics and a plugin that calls the lsp binary.

Happy to answer any questions or do any further tasks that might support the investigation.

https://github.com/user-attachments/assets/13012c48-c9fa-48a3-ab82-ed380f6b097e

Example.zip

mattmassicotte commented 1 month ago

Hello!

First, regular support for my packages is completely free! But, if you are looking for more dedicated blocks of time, I absolutely do that as well.

I have come into an issue when calling try await server.diagnostics(diagnosticParams) when the executable is ran from an Xcode command plugin.

Hmm, I would have expected plugins like this to be sandboxed. If the execution environment is the only thing you are changing, I'm worried about that. I would be surprised if any LSP server could run sandboxed, even sourcekit-lsp. But maybe!

However, the server does respond to the init message from within the plugin so I know it's not due to incorrect configuration of the plugin.

Let's dig into this as well, to be sure we are sure this is happening. LSPClient does a lot of stuff lazily, so it can be hard sometimes to understand what it is doing, when.

Also, not that it's relevent to the problem at hand, but I did glance and see some semaphore usage that could be problematic in the example.

steprescott commented 1 month ago

Hey. Thanks for your quick reply.

The way I validated that the init message was received was by adding a print into the initializeIfNeeded function.

extension InitializingServer {
    /// Run the initialization sequence with the server, if it has not already happened.
    public func initializeIfNeeded() async throws -> InitializationResponse {
        switch state {
        case .initialized(let initResp):
            return initResp
        case .uninitialized, .shutdown:
            try await semaphore.waitUnlessCancelled()
        }

        defer { semaphore.signal() }

        let params = try await initializeParamsProvider()
        let initResponse = try await channel.initialize(params)

                print("💻 INIT:", initResponse)

        try await channel.initialized(InitializedParams())
        self.state = .initialized(initResponse)

        capabilitiesContinuation.yield(initResponse.capabilities)

        return initResponse
    }
...
}

Yer semaphores can be run but I've used this in the past to move halt the process. Maybe I'll change that to use https://github.com/groue/Semaphore but it's very basic usage.

I have other operations that occur within the Xcode command plugin that operate over files and I can mutate them. I'll do some more verification on the question about sandboxing. However the ability to change files and the init call working leads me to think it is not sandboxed.

image
mattmassicotte commented 1 month ago

No worries!

Ok, good that was smart. I'm still very suspicious of the server's runtime environment, though. Especially since a) I know that the LSP protocol wasn't built to support sandboxing and b) pretty much ever server I have tried explodes when run that way.

Sourcekit-lsp might be different though, I honestly don't know. It is possible it does not have external dependencies, and the plugin system probably does forward along permissions to access opened files. Or maybe these things aren't sandboxed?

Can you post some of the crash report?

mattmassicotte commented 1 month ago

Oh, also DispatchSemaphore and https://github.com/groue/Semaphore have similar names but are actually not interchangeable. You were using DispatchSemaphore to synchronously wait for an async operation. Very common problem, but unfortunately this is something the concurrency system explicitly does not support. I've had to deal with this too, occasionally, and it is a very challenging problem that does not always have viable solutions...

steprescott commented 1 month ago

I don't get a crash report unfortunately. I've just noticed that main has more commits than the latest release and will try that. I will also try linking your package locally to the example project.

mattmassicotte commented 1 month ago

Ohh, sorry, I assumed that uncaught signal was a crash somewhere. Can you just double-check that there's nothing being generated in Console.app > Crash Reports?

steprescott commented 1 month ago

Great advice!

So there does seem to be an error in the console. Does suggest sandboxing.

image
mattmassicotte commented 1 month ago

Sandbox: sourcekit-lsp(15080) deny(1) mach-lookup com.apple.CoreServices.coreservicesd

Pretty sure sourcekit-lsp is being run in a sandbox here and it cannot tolerate that...

I haevn't be able to put too much time into this recently, but I've done quite a lot of work on running servers in a sandboxed environment. Here's one effort: https://github.com/ChimeHQ/LanguageServerScripts

steprescott commented 1 month ago

~Might be out of the scope of this issue, but would perhaps notarizing the binary help?~ I don't think it will.

I'll have a look at your linked repo.

It's so strange that the init is allowed or have I misunderstood that?

mattmassicotte commented 1 month ago

In my experience, this is kind of how it goes. They are kinda functional, but they have some external dependencies that don't work. So the failures only occur once you ask them to do something that isn't allowed by the sandbox.

steprescott commented 1 month ago

I'll close the issue. Thanks for your help. If you do have other ideas or progress with ways that can run outside the sandbox please do let this issue know.

steprescott commented 1 month ago

Yup, process is sandboxed.

image
steprescott commented 1 month ago

I have been able to get the results from sourcekit-lsp by using https://github.com/codeface-io/LSPService. So having a local server that proxies sourcekit-lsp

mattmassicotte commented 1 month ago

Ahh yes! I've worked with the author a bit on this exact problem. We were hoping to collaborate some more, but so far, it hasn't happened.