eclipse-jdtls / eclipse.jdt.ls

Java language server
1.8k stars 401 forks source link

Question: How to connect to the started language server via Java socket? #1129

Open Symbolk opened 5 years ago

Symbolk commented 5 years ago

I am new to LSP. Previously I want to analyze programs with static analysis techniques (not developing a plugin or extension or editor), but I do not want to write one parser for each language, that leads me to the LSP.

Currently I have successfully started this language server with the following command under windows:

set CLIENT_PORT=8679
set CLIENT_HOST=localhost
$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=1044 -Declipse.application=org.eclipse.jdt.ls.core.id1 -Dosgi.bundles.defaultStartLevel=4 -Declipse.product=org.eclipse.jdt.ls.core.product -Dlog.level=ALL -noverify -Xmx1G -jar ./plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.1100.v20190715-0945.jar -configuration ./config_win -data D:\java
Listening for transport dt_socket at address: 1044

Then I write some client code to connect and send request to the server:

public static void main(String[] args) {
        try (Socket socket = new Socket("localhost", 1044)) {
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            sendInitJava(out, in);
//            sendOpen(out, in);
//            sendDefinition(out, in);
            out.close();
            in.close();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
    private static void sendInitJava(PrintWriter out, BufferedReader in) throws IOException {
        JSONObject json = new JSONObject();
        JSONObject params = new JSONObject();
        json.put("jsonrpc", "2.0");
        json.put("id", 0);
        json.put("method", "initialize");

        params.put("rootPath", "D:/java");
        params.put("rootUri", "file:///D:/java");

        JSONObject capabilities = new JSONObject();
        JSONObject textCapabilities = new JSONObject();
        JSONObject ref = new JSONObject();
        ref.put("dynamicRegistration", true);
        textCapabilities.put("references", ref);
        JSONObject def = new JSONObject();
        def.put("dynamicRegistration", true);
        textCapabilities.put("definition", def);
        capabilities.put("textDocument", textCapabilities);
        JSONObject workspaceCapabilities = new JSONObject();
        workspaceCapabilities.put("workspaceFolders", true);

        capabilities.put("workspace", workspaceCapabilities);
        params.put("capabilities", capabilities);

        json.put("params", params);

        String content = json.toString() + "\n";
        String headers = "Content-Length: " + content.length() + "\r\n\r\n";
        String request = headers + content;
        out.write(request);
        out.flush();
        System.out.println("init request:");
        System.out.println(request);

        json = new JSONObject();
        params = new JSONObject();
        json.put("jsonrpc", "2.0");
        json.put("method", "textDocument/didOpen");
        JSONObject textDoc = new JSONObject();
        textDoc.put("uri", "file:///D:/java/jj.java");
        textDoc.put("languageId", "java");
        textDoc.put("version", 0);
        params.put("textDocument", textDoc);
        json.put("params", params);

        content = json.toString() + "\n";
        headers = "Content-Length: " + content.length() + "\r\n\r\n";
        request = headers + content;
        out.write(request);
        out.flush();
        System.out.println("open request:");
        System.out.println(request);

        json.put("jsonrpc", "2.0");
        json.put("id", 1);
        json.put("method", "textDocument/documentSymbol ");
        textDoc = new JSONObject();
        textDoc.put("uri", "file:///D:/java/jj.java");
        params.put("textDocument", textDoc);
        json.put("params", params);

        content = json.toString() + "\n";
        headers = "Content-Length: " + content.length() + "\r\n\r\n";
        request = headers + content;
        out.write(request);
        out.flush();
        System.out.println("symbol request:");
        System.out.println(request);

        json.put("jsonrpc", "2.0");
        json.put("id", 2);
        json.put("method", "textDocument/definition");
        textDoc = new JSONObject();
        textDoc.put("uri", "file:///D:/java/jj.java");
        textDoc.put("languageId", "java");
        params.put("textDocument", textDoc);
        JSONObject pos = new JSONObject();
        pos.put("line", 24);
        pos.put("character", 17);
        params.put("position", pos);
//            params.put(textDoc);
        json.put("params", params);

        content = json.toString() + "\n";
        headers = "Content-Length: " + content.length() + "\r\n\r\n";
        request = headers + content;
        out.write(request);
        out.flush();
        System.out.println("def request:");
        System.out.println(request);

        StringBuilder sb = new StringBuilder();
        String line = "";
        while ((line = in.readLine()) != null) {
            sb.append(line);
            System.out.println(line);
            if(!line.startsWith("Content")&&line.length()>0){
                line = line.substring(0, line.lastIndexOf("}")+1);
                JSONObject response = JSONObject
                        .parseObject(line);
                System.out.println(response.getString("jsonrpc"));
            }
        }
        JSONObject response = JSONObject
                .parseObject(sb.toString());
        String s = JSON.toJSONString(JSON.parseArray(response.getJSONArray("result").toJSONString()));
        System.out.println(s);
    }

Then error happens, the Java console gives the following error:

java.net.SocketException: Software caused connection abort: recv failed

While the server side gives the following error:

Debugger failed to attach: handshake failed - received >Content-Length< - expected >JDWP-Handshake<

I found a related issue #181, but failed to find solution. Do you have any idea? @rcjsuen

rcjsuen commented 5 years ago

@Symbolk In the README.md file in my language server, I have some bits of example code for connecting to a language server using stdio, network sockets, or JSON-RPC that may be helpful to you. They are written in TypeScript but I'm sure you'll be able to transcribe them into Java or JavaScript.

rcjsuen commented 5 years ago
set CLIENT_PORT=8679

@Symbolk It seems to me like you are trying to connect to the debugger on port 1044 in your code. You should be using 8679 instead, no?

Symbolk commented 5 years ago

@rcjsuen I replace the port to connect in Java code from 1044 to 8679, but the connection is refused:

java.net.ConnectException: Connection refused: connect
    at java.net.DualStackPlainSocketImpl.connect0(Native Method)
vasiltabakov commented 5 years ago

Btw, in your previous attempts to analyse the programs have you given semantic a try?

rcjsuen commented 5 years ago

@Symbolk The language server will connect to your client at the specified port. You need to create a java.net.ServerSocket that is listening for connections on that port.

snjeza commented 5 years ago

@Symbolk You can try:

public class Server {
        public static void main(String[] args) throws IOException {
        try (ServerSocket serverSocket = new ServerSocket(8679)) {
                Socket clientSocket = serverSocket.accept();
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // ...initialize
        }
    }
}

$ java Server $ export CLIENT_PORT=8679 $ java -Declipse.application=org.eclipse.jdt.ls.core.id1 -Dosgi.bundles.defaultStartLevel=4 -Declipse.product=org.eclipse.jdt.ls.core.product -Dlog.level=ALL -noverify -Xmx1G -jar ./plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.1100.v20190715-0945.jar -configuration ./config_win -data D:\java

Symbolk commented 5 years ago

Btw, in your previous attempts to analyse the programs have you given semantic a try?

Yeah, once I had a glance at it, but did not try it since I know little about Haskell. For a symbol in code, is it able to find the definition/references?

Symbolk commented 5 years ago

@snjeza Thanks, I encountered some errors when running your command, but I managed to resolve them by changing the jar to run, now the server and the client seem connected.

$ java -Declipse.application=org.eclipse.jdt.ls.core.id1 -Dosgi.bundles.defaultStartLevel=4 -Declipse.product=org.eclipse.jdt.ls.core.product -Dlog.level=ALL -noverify -Xmx1G -jar ./plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.1100.v20190715-0945.jar -configuration ./config_win -data D:\java
no main manifest attribute, in ./plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.1100.v20190715-0945.jar
$ java -Declipse.application=org.eclipse.jdt.ls.core.id1 -Dosgi.bundles.defaultStartLevel=4 -Declipse.product=org.eclipse.jdt.ls.core.product -Dlog.level=ALL -noverify -Xmx1G -jar ./plugins/org.eclipse.equinox.launcher_1.5.500.v20190715-1310.jar -configuration ./config_win -data D:\java
Symbolk commented 5 years ago

@rcjsuen Thanks, by imitating your code, I have successfully worked with the javascript-typescript-langserver (https://github.com/sourcegraph/javascript-typescript-langserver) .. But it seems the eclipse.jdt.ls works in a different way from the javascript-typescript-langserver.

itsaky commented 3 years ago

@Symbolk Could you please share a code snippet (in Java) showing a working example?

What I have tried till now (after reading the comments in this issue)

// 'server' and 'socket' are global variables
String data = "<some_json_string_shown_below>";
server = new ServerSocket(SERVER_PORT);
socket = server.accept();
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.write(data.concat("\n"));
out.flush();

JSON String that I was writing to the server is :

{
     "jsonrpc": "2.0",
     "method": "initialize",
     "params": {
         "rootPath": "<path_of_folder>",
         "rootUri": "<uri_of_folder>",
         "capabilities": {}
     }
 }

But the server doesn't respond. Here is how I read the input stream of the socket (if you're interested). In another thread:

// 'in' is a global variable, initialized in the Thread's constructor
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

// in run() method :
while (true) {
        try {
             String readLine = this.output.readLine();
             if (readLine == null) break;
             // output is empty
    } catch (Throwable th) {}
}
rcjsuen commented 3 years ago

@itsaky I don't think you can just send the JSON string. It sounds to me like you're missing the header bits with Content-Length?

itsaky commented 3 years ago

@rcjsuen Thanks a lot, sir! After I added the Content-Length header in the request, it successfully responded with a language/status method.

goldfita commented 2 years ago

Is it possible to connect to the language server as a server. The docker example says a port has to be opened on the client and then the language server is started, which reverses their roles. I would like to connect to a remote LS using the method described in this post.