PowerShell / PowerShellEditorServices

A common platform for PowerShell development support in any editor or application!
MIT License
622 stars 213 forks source link

No response from debug service on Unix Domain Sockets #2012

Closed Willem-J-an closed 1 year ago

Willem-J-an commented 1 year ago

Prerequisites

Steps to reproduce

I'm trying to create a simple proxy between neovim dap client and powershell editor services. I created a simple python script that should relay messages over stdin/stdout to Unix Domain Socket.

from time import sleep
from sys import stdin, stdout, argv, stderr
from json import load
import socket
import os, os.path

def get_pipe_name() -> str:
    with open(argv[1], 'r') as session:
        session = load(session)
        return session['debugServicePipeName']

def main() -> None:
    pipe_name = None
    while True:
        buffer = ''
        lines = []
        length = None
        # Read request message from stdin
        while True:
            buffer += stdin.read(1)

            if buffer.endswith('\r\n') and not length:
                lines.append(buffer.rstrip())
                length = int(buffer.rstrip().split(': ')[-1])
                buffer = ''
            if '\r\n' in buffer:
                buffer = buffer.replace('\r\n', '')
            if length and len(buffer) == length:
                lines.append(buffer)
                break

        # Write request to the Unix Domain Socket
        with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
            pipe_name = pipe_name if pipe_name else get_pipe_name()
            s.connect(pipe_name)
            message = '\r\n'.join(lines).encode('UTF-8')
            stderr.write(f"length: {s.send(message)}")
            stderr.flush()

            uds_buffer = b''
            uds_length = None
            uds_lines = []
            # Try to read the response from Unix Domain Socket
            while True:
                # Never gets any response back from PSES
                uds_buffer += s.recv(1024)
                if uds_buffer.endswith(b'\r\n') and not uds_length:
                    decode_buffer = uds_buffer.decode()
                    lines.append(decode_buffer.rstrip())
                    uds_length = int(decode_buffer.rstrip().split(': ')[-1])
                    uds_buffer = b''
                if b'\r\n' in uds_buffer:
                    uds_buffer = uds_buffer.replace(b'\r\n', b'')
                if uds_length and len(uds_buffer) == uds_length:
                    decode_buffer = uds_buffer.decode()
                    uds_lines.append(decode_buffer)
                    break

        stdout.write('\r\n'.join(uds_lines))
        stdout.flush()

        break

if __name__ == '__main__':
    main()

I have the editor services running like so:

& "$PSES_BUNDLE_PATH/PowerShellEditorServices/Start-EditorServices.ps1" `
    -BundledModulesPath $PSES_BUNDLE_PATH `
    -LogPath "$SESSION_TEMP_PATH/logs.log" `
    -SessionDetailsPath "$SESSION_TEMP_PATH/session.json" `
    -FeatureFlags @() `
    -AdditionalModules @() `
    -HostName 'My Client' `
    -HostProfileId 'myclient' `
    -HostVersion 1.0.0 `
    -LogLevel Diagnostic `
    -DebugServiceOnly

I see the server is running, and I validated the request is reaching the Unix Domain Socket. When the request lands, PSES spouts some diagnostics that it's loading a bunch of stuff into context. When I try to listen back for the response, there is nothing. The process hangs on recv.

Expected behavior

recv returns some response that I could relay back through stdout.

Actual behavior

There is no response and process gets stuck on recv.

Error details

No error

Environment data

VERBOSE: 
== PowerShell Details ==
- PowerShell version: 7.3.3
- Language mode:      FullLanguage

VERBOSE: 
== Environment Details ==
 - OS description:  Linux 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21)
 - OS architecture: X64
 - Process bitness: 64

Version

7.3.3

Visuals

No response

andyleejordan commented 1 year ago

What are you actually getting in the $SESSION_TEMP_PATH/session.json file, and what's get_pipe_name() returning?

Willem-J-an commented 1 year ago

The session file contains the debugServicePipeName property. The get_pipe_name function is defined in the code I shared. It parses the session json and returns debugServicePipeName property. Looks something like /tmp/mumbo-jumbo.random.

lsof -U

Shows that the server is indeed listening on that domain socket. My sent request looks like this:

Content-Length: xxx\r\n{initialization request}

It's likely that I'm doing something wrong, but there is no error either. If I shutdown the server while socket.recv is waiting, I do get the server is disconnected message on my client.

andyleejordan commented 1 year ago

The session file contains the debugServicePipeName property. The get_pipe_name function is defined in the code I shared. It parses the session json and returns debugServicePipeName property. Looks something like /tmp/mumbo-jumbo.random.

Thanks, I was trying to confirm that it was parsing the session file and correctly returning the pipe name as I couldn't confirm it from what you'd originally posted.

Content-Length: xxx\r\n{initialization request}

Ah, it looks like your sent request is missing the second \r\n based off the LSP specification docs:

Each header field is terminated by ‘\r\n’. Considering the last header field and the overall header itself are each terminated with ‘\r\n’, and that at least one header is mandatory, this means that two ‘\r\n’ sequences always immediately precede the content part of a message.

It's likely that I'm doing something wrong, but there is no error either. If I shutdown the server while socket.recv is waiting, I do get the server is disconnected message on my client.

It sounds like it's working to spec then.

Until the server has responded to the initialize request with an InitializeResult, the client must not send any additional requests or notifications to the server. In addition the server is not allowed to send any requests or notifications to the client until it has responded with an InitializeResult

Is there anything in the log file? Should be able get diagnostic logs (or attach a debugger to the server) to see if it's processing and discarding what I suspect is a malformed request.

Willem-J-an commented 1 year ago

Ah good spot! If I have double crlf it still does not respond, but when I add a final crlf after the content part I do get a response!

I guess that last crlf is not according to spec? Anyways, I know how to make it work now 😁

andyleejordan commented 1 year ago

Heck yeah! The spec is confusing for sure, and annoying in that the server (which really is O# underneath) is supposed to be silent except to what it sees as a well-formed requests. Sorry for the trouble. Let me know how the rest of it goes!

Willem-J-an commented 1 year ago

I'm back at it again :smile: I can now reliably get a response on the initialize request.

   {
  body = {
    supportsCancelRequest = true,
    supportsConditionalBreakpoints = true,
    supportsConfigurationDoneRequest = true,
    supportsFunctionBreakpoints = true,
    supportsHitConditionalBreakpoints = true,
    supportsLogPoints = true,
    supportsSetVariable = true
  },
  command = "initialize",
  request_seq = 0,
  seq = 1,
  success = true,
  type = "response"
}

I'm now stuck because the server does not wish to reply to my launch request; it looks like this:

# I manually added a few linebreaks here for legibility; they are not there in the request. 
# The real linebreaks are represented by \r\n.
Content-Length: 265\r\n\r\n{
"type":"request",
"seq":1,
"arguments":{
"name":"PowerShell: Current",
"cwd":"/home/abcdefgh/code/pses-dap/test.ps1",
"script":"/home/abcdefgh/code/pses-dap/test.ps1",
"type":"PowerShell",
"createTemporaryIntegratedConsole":true,
"request":"launch"
},"command":"launch"}'

The server gives no response to this. Any ideas?

I notice the VS code implementation adds some extra arguments, not sure if those are required?

"__configurationTarget":6,
"current_document":true

I tried to attach to the PSES DAP server using vs code, which kind of worked, but then it says the code is unknown, so I can't really put breakpoints to see what is going on in the server.

andyleejordan commented 1 year ago

Did you send initialized after initialize? Note the "d", it's the next necessary lifecycle message in the specification: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialized

ghost commented 1 year ago

This issue was closed automatically as author feedback was indicated as needed, but there has been no activity in over a week. Please feel free to reopen with any available information!