/************************************************************************************ * This file is part of AndroidIDE. * * AndroidIDE is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * AndroidIDE is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with AndroidIDE. If not, see . * **************************************************************************************/ package com.itsaky.androidide.lsp; import com.itsaky.androidide.utils.Logger; import com.itsaky.lsp.services.IDELanguageServer; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import org.eclipse.lsp4j.jsonrpc.Launcher; import java.util.Locale; import com.itsaky.androidide.app.StudioApp; /** * Launches a client for a Language server and handles connection. * A callback is sent to the IDELanguageClient to tell the client to start the server if necessary. */ public abstract class LSPClientLauncher extends Thread { protected final IDELanguageClientImpl languageClient; protected IDELanguageServer server; protected LaunchListener mLaunchListener; public LSPClientLauncher(IDELanguageClientImpl client) { Objects.requireNonNull(client); setDaemon(true); this.languageClient = client; } /** * Set the listener to get notified about language server start * event. * * @param listener The listener. */ public void setLaunchListener (LaunchListener listener) { this.mLaunchListener = listener; } /** * Returns the connected LanguageServer, or {@code null} * * @return The LanguageServer or {@code null} */ public IDELanguageServer getServer() { return this.server; } /** * Are we connected to the server? */ public boolean isConnected() { return languageClient != null && languageClient.isConnected(); } /** * Actual logic to start the server */ protected abstract void launch() throws Exception; @Override public void run() { try { launch(); } catch (Throwable th) { LOG.error(StudioApp.getInstance().getString(com.itsaky.androidide.R.string.err_cannot_start_server, getLanguageCode()), th); } } protected Launcher createClientLauncher(IDELanguageClientImpl client, InputStream in, OutputStream out) { return new Launcher.Builder () .setLocalService(client) .setRemoteInterface(IDELanguageServer.class) .setInput(in) .setOutput(out) .create(); } /** * Notify the {@link LaunchListener} instance that the server is started. * Does nothing if the listener instance is {@code null}. */ protected void notifyServerStarted () { if (mLaunchListener != null) { mLaunchListener.onStarted(this.server); } } /** * Shut down the language server */ public abstract void shutdown(); /** * Get the language code for the server */ public abstract String getLanguageCode(); /** * A listener to listen for language server start event. */ public static interface LaunchListener { /** * Called when the language server is started. * * @param server The language server instance. */ void onStarted (IDELanguageServer server); } /** * Wraps an output stream to make sure that we don't write on UI Thread * Writing to a socket's output stream is considered as network operation * Trying to write to a socket on UI Thread will result in NetworkOnMainThreadExeption */ protected class OutputStreamWrapper extends OutputStream { private final AsyncWriter writer; private final Thread writerThread; private final OutputStream actualStream; public OutputStreamWrapper(OutputStream actualStream) { this.writer = new AsyncWriter(actualStream); this.writerThread = new Thread(writer); this.actualStream = actualStream; this.writerThread.start(); } @Override public void write(int p1) throws IOException { // Not needed } /** * LSP4J always calls this method to write to remote proxy. * See https://github.com/eclipse/lsp4j/blob/253aef9a702659f2524ecefaab7e829278c2ffd3/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageConsumer.java#L67 */ @Override public void write(final byte[] b) throws IOException { // Do not call super.write(b); // If done, it will further call write(int) this.writer.write(b); } @Override public void close() throws IOException { super.close(); this.writer.close(); } } /** * Actual implementation of the writer thread */ private class AsyncWriter implements Runnable, Closeable { private final OutputStream out; private boolean closed; private final BlockingQueue writeQueue = new ArrayBlockingQueue<>(10); public AsyncWriter(OutputStream out) { this.out = out; this.closed = false; } public void write(byte[] b) { this.writeQueue.add(new Writable(b)); } @Override public void run() { while (!closed) { try { Writable data = writeQueue.take(); this.out.write(data.data); this.out.flush(); } catch (Throwable e) { LOG.error (StudioApp.getInstance().getString(com.itsaky.androidide.R.string.err_cannot_write_socket), e); } } } @Override public void close() throws IOException { this.out.close(); this.closed = true; } class Writable { byte[] data; public Writable(byte[] data) { this.data = data; } } } protected static final Logger LOG = Logger.instance("LSPClientLauncher"); }