skip to Main Content

I want to create a simple LSP server in golang, and so far this is the code I wrote:

package main

import (
    "context"
    "fmt"
    "os"
    "sync"

    "github.com/sourcegraph/jsonrpc2"
)

type LSPServer struct {
    // The symmetric connection
    conn jsonrpc2.Conn

    // Check if the connection is available
    connMutex sync.Mutex

    // shutdown
    shutdown bool
}

func NewLSPServer() *LSPServer {
    return &LSPServer{}
}

func (s *LSPServer) Initialize(ctx context.Context) error {
    // to implement
    return nil
}

func (s *LSPServer) Handle(context.Context, *jsonrpc2.Conn, *jsonrpc2.Request) (result interface{}, err error) {
    fmt.Println("Handling request...")
    // to implement
    return nil, nil
}

func (s *LSPServer) Serve(ctx context.Context) {
    fmt.Println("Starting LSP server...")
    // what port is this server listening on?
    // it is listening on port 4389

    // Create a new jsonrpc2 stream server
    handler := jsonrpc2.HandlerWithError(s.Handle)

    // Create a new jsonrpc2 stream server
    <-jsonrpc2.NewConn(
        context.Background(),
        jsonrpc2.NewBufferedStream(os.Stdin, jsonrpc2.VSCodeObjectCodec{}),
        handler).DisconnectNotify()
}

func main() {

    // Create a new LSP server
    server := NewLSPServer()
    server.Serve(context.Background())

}

It runs, but I don’t know what port it is running on, or how to call it with a client in general. Does someone have some ideas?

I think it should be port 4389, but it is not that one

I am testing with this script:

import json
import requests

def rpc_call(url, method, args):
    headers = {'content-type': 'application/json'}
    payload = {
        "method": method,
        "params": [args],
        "jsonrpc": "2.0",
        "id": 1,
    }
    response = requests.post(url, data=json.dumps(payload), headers=headers).json()
    return response['result']

url = 'http://localhost:4389/'

emailArgs = {'To': '[email protected]','Subject': 'Hello', 'Content': 'Hi!!!'}
smsArgs = {'Number': '381641234567', 'Content': 'Sms!!!'}
print(rpc_call(url, 'email.SendEmail', emailArgs))
print(rpc_call(url, 'sms.SendSMS', smsArgs))

I think it is correct since I took this client from another stackoverflow question

2

Answers


  1. I see:

    HandlerWithError(s.Handle)
    
        // Create a new jsonrpc2 stream server
        <-jsonrpc2.NewConn(
            context.Background(),
            jsonrpc2.NewBufferedStream(os.Stdin, jsonrpc2.VSCodeObjectCodec{}),
            handler).DisconnectNotify()
    }
    

    That means your code is using JSON-RPC over standard input and output (stdin/stdout), not over a network connection.
    When you use os.Stdin as a parameter to jsonrpc2.NewBufferedStream, you are specifying that input should come from the standard input of the process running the server. And the response will be sent to the standard output.

    So, the server is not listening on any network port. It is interacting with data that is sent directly to its standard input and output. That is often used for inter-process communication, for example, when you want one process to call the server process and receive a response.
    See for instance "Go: bidirectional communication with another process?" or davidelorenzoli/stdin-stdout-ipc.


    If you want your JSON-RPC server to listen on a network port, you will need to set up a network connection in Go using the net package. You will also need to modify your client script to send its requests to the correct network port, rather than sending an HTTP request to a URL.

    package main
    
    import (
        "context"
        "net"
        "log"
        "sync"
    
        "github.com/sourcegraph/jsonrpc2"
    )
    
    type LSPServer struct {
        // The symmetric connection
        conn jsonrpc2.Conn
    
        // Check if the connection is available
        connMutex sync.Mutex
    
        // shutdown
        shutdown bool
    }
    
    func NewLSPServer() *LSPServer {
        return &LSPServer{}
    }
    
    func (s *LSPServer) Initialize(ctx context.Context) error {
        // Initialize here if needed
        return nil
    }
    
    func (s *LSPServer) Handle(context.Context, *jsonrpc2.Conn, *jsonrpc2.Request) (result interface{}, err error) {
        fmt.Println("Handling request...")
        // Handle something
        return nil, nil
    }
    
    func (s *LSPServer) Serve(ctx context.Context) {
        fmt.Println("Starting LSP server...")
        
        // Listen on TCP port 4389 on all available unicast and
        // anycast IP addresses of the local system.
        l, err := net.Listen("tcp", "localhost:4389")
        if err != nil {
            log.Fatal(err)
        }
        defer l.Close()
    
        for {
            // Wait for a connection.
            conn, err := l.Accept()
            if err != nil {
                log.Fatal(err)
            }
    
            // Handle the connection in a new goroutine.
            go func(c net.Conn) {
                // Create a new jsonrpc2 stream server
                handler := jsonrpc2.HandlerWithError(s.Handle)
                <-jsonrpc2.NewConn(
                    ctx,
                    jsonrpc2.NewBufferedStream(c, jsonrpc2.VSCodeObjectCodec{}),
                    handler).DisconnectNotify()
                c.Close()
            }(conn)
        }
    }
    
    func main() {
        // Create a new LSP server
        server := NewLSPServer()
        go server.Serve(context.Background()) // run Serve in a separate goroutine
        select {} // wait forever
    }
    

    That is a basic example, where the Serve method creates a TCP listener that listens on port 4389 of the localhost. It then enters a loop where it waits for connections, and when it gets one, it starts a new goroutine to handle that connection using your JSON-RPC server.


    On the client side, you would need to open a TCP connection to the server, write your JSON-RPC request to that connection, then read the response.

    You cannot use the requests library as in your Python script because that is for HTTP requests, not raw TCP connections.
    You will need to use the socket library in Python, or a similar library in your client’s language, to create a TCP connection and send/receive data over it.


    But keep in mind, an LSP (Language Server Protocol) operates over stdin/stdout rather than network sockets.
    That is because LSP servers are typically launched as subprocesses by the editor/IDE and communicate directly through these channels. So depending on your use-case, the original stdin/stdout method might be more appropriate.

    Login or Signup to reply.
  2. This is an addition to @VonC’s excellent answer. This answer provides a stdioReadWriteCloser to make the original stdin/stdout method work. Please read @VonC’s answer first.

    Build the source code in the original question:

    go build -o lspserver .
    

    And then send this message to lspserver‘s stdin:

    echo 'Content-Length: 70rnrn{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}' | ./lspserver
    Starting LSP server...
    Handling request...
    2023/07/16 16:16:37 jsonrpc2 handler: sending response 2: write /dev/stdin: bad file descriptor
    

    (Note: Content-Length: %drnrn is required by the VSCodeObjectCodec)

    Pay attention to the error message. It tries to write to stdin. That’s incorrect. We have to create an io.ReadWriteCloser from stdin and stdout like this:

    type stdioReadWriteCloser struct{}
    
    var _ io.ReadWriteCloser = (*stdioReadWriteCloser)(nil)
    
    func (c stdioReadWriteCloser) Read(p []byte) (n int, err error) {
        return os.Stdin.Read(p)
    }
    
    func (c stdioReadWriteCloser) Write(p []byte) (n int, err error) {
        return os.Stdout.Write(p)
    }
    
    func (c stdioReadWriteCloser) Close() error {
        return nil
    }
    

    And use stdioReadWriteCloser like this:

    <-jsonrpc2.NewConn(
        context.Background(),
        jsonrpc2.NewBufferedStream(stdioReadWriteCloser{}, jsonrpc2.VSCodeObjectCodec{}),
        handler).DisconnectNotify()
    

    Build lspserver and try again:

    echo 'Content-Length: 70rnrn{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 2}' | ./lspserver
    Starting LSP server...
    Handling request...
    Content-Length: 38
    
    {"id":2,"result":null,"jsonrpc":"2.0"}
    

    Now it works as expected!

    It’s weird that github.com/sourcegraph/jsonrpc2 does not provide such a wrapper out of the box.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search