blob: 017e08c6f77df0b138a62e1e6ce4736c03155d73 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2018 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lucas Bullen (Red Hat Inc.) - initial implementation
*******************************************************************************/
package org.eclipse.lsp4e;
import java.io.File;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.lsp4e.server.StreamConnectionProvider;
import org.eclipse.lsp4j.jsonrpc.messages.Message;
import org.eclipse.lsp4j.services.LanguageServer;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.IConsoleManager;
import org.eclipse.ui.console.MessageConsole;
import org.eclipse.ui.console.MessageConsoleStream;
public class LoggingStreamConnectionProviderProxy implements StreamConnectionProvider {
public static File getLogDirectory() {
IPath root = ResourcesPlugin.getWorkspace().getRoot().getLocation();
if (root == null) {
return null;
}
File logFolder = new File(root.addTrailingSeparator().toPortableString(), "languageServers-log"); //$NON-NLS-1$
if (!(logFolder.exists() || logFolder.mkdirs()) || !logFolder.isDirectory() || !logFolder.canWrite()) {
return null;
}
return logFolder;
}
private static final String FILE_KEY = "file.logging.enabled"; //$NON-NLS-1$
private static final String STDERR_KEY = "stderr.logging.enabled"; //$NON-NLS-1$
private StreamConnectionProvider provider;
private InputStream inputStream;
private OutputStream outputStream;
private InputStream errorStream;
private String id;
private final File logFile;
private boolean logToFile;
private boolean logToConsole;
/**
* Converts a language server ID to the preference ID for logging communications
* to file from the language server
*
* @return language server's preference ID for file logging
*/
public static String lsToFileLoggingId(String serverId) {
return serverId + "." + FILE_KEY;//$NON-NLS-1$
}
/**
* Converts a language server ID to the preference ID for logging communications
* to console from the language server
*
* @return language server's preference ID for console logging
*/
public static String lsToConsoleLoggingId(String serverId) {
return serverId + "." + STDERR_KEY;//$NON-NLS-1$
}
/**
* Returns whether currently created connections should be logged to file or the
* standard error stream.
*
* @return If connections should be logged
*/
public static boolean shouldLog(String serverId) {
IPreferenceStore store = LanguageServerPlugin.getDefault().getPreferenceStore();
return store.getBoolean(lsToFileLoggingId(serverId)) || store.getBoolean(lsToConsoleLoggingId(serverId));
}
public LoggingStreamConnectionProviderProxy(StreamConnectionProvider provider, String serverId) {
this.id = serverId;
this.provider = provider;
IPreferenceStore store = LanguageServerPlugin.getDefault().getPreferenceStore();
logToFile = store.getBoolean(lsToFileLoggingId(serverId));
logToConsole = store.getBoolean(lsToConsoleLoggingId(serverId));
store.addPropertyChangeListener(event -> {
if (event.getProperty().equals(FILE_KEY)) {
logToFile = (boolean) event.getNewValue();
} else if (event.getProperty().equals(STDERR_KEY)) {
logToConsole = (boolean) event.getNewValue();
}
});
this.logFile = getLogFile();
}
@Override
public InputStream getInputStream() {
if (inputStream != null) {
return inputStream;
}
if (provider.getInputStream() != null) {
inputStream = new FilterInputStream(provider.getInputStream()) {
@Override
public int read(byte[] b, int off, int len) throws IOException {
int bytes = super.read(b, off, len);
byte[] payload = new byte[bytes];
System.arraycopy(b, off, payload, 0, bytes);
if (logToConsole || logToFile) {
String s = "\n[t=" + System.currentTimeMillis() + "] " + id + " to LSP4E:\n" + new String(payload); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
if (logToConsole) {
logToConsole(s);
}
if (logToFile) {
logToFile(s);
}
}
return bytes;
}
};
}
return inputStream;
}
@Override
public InputStream getErrorStream() {
if (errorStream != null) {
return errorStream;
}
if (provider.getErrorStream() != null) {
errorStream = new FilterInputStream(provider.getErrorStream()) {
@Override
public int read(byte[] b, int off, int len) throws IOException {
int bytes = super.read(b, off, len);
byte[] payload = new byte[bytes];
System.arraycopy(b, off, payload, 0, bytes);
if (logToConsole || logToFile) {
String s = "\n[t=" + System.currentTimeMillis() + "] Error from " + id + ":\n" + new String(payload); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
if (logToConsole) {
logToConsole(s);
}
if (logToFile) {
logToFile(s);
}
}
return bytes;
}
};
}
return errorStream;
}
@Override
public OutputStream getOutputStream() {
if (outputStream != null) {
return outputStream;
}
if (provider.getOutputStream() != null) {
outputStream = new FilterOutputStream(provider.getOutputStream()) {
@Override
public void write(byte[] b) throws IOException {
if (logToConsole || logToFile) {
String s = "\n[t=" + System.currentTimeMillis() + "] LSP4E to " + id + ":\n" + new String(b); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
if (logToConsole) {
logToConsole(s);
}
if (logToFile) {
logToFile(s);
}
}
super.write(b);
}
};
}
return outputStream;
}
@Override
public void start() throws IOException {
provider.start();
}
@Override
public InputStream forwardCopyTo(InputStream input, OutputStream output) {
return provider.forwardCopyTo(input, output);
}
@Override
public Object getInitializationOptions(URI rootUri) {
return provider.getInitializationOptions(rootUri);
}
@Override
public String getTrace(URI rootUri) {
return provider.getTrace(rootUri);
}
@Override
public void handleMessage(Message message, LanguageServer languageServer, URI rootURI) {
provider.handleMessage(message, languageServer, rootURI);
}
@Override
public void stop() {
provider.stop();
try {
if (outputStream != null) {
outputStream.close();
outputStream = null;
}
if (inputStream != null) {
inputStream.close();
inputStream = null;
}
if (errorStream != null) {
errorStream.close();
errorStream = null;
}
} catch (IOException e) {
LanguageServerPlugin.logError(e);
}
}
private void logToConsole(String string) {
if (consoleStream == null || consoleStream.isClosed()) {
consoleStream = findConsole().newMessageStream();
}
consoleStream.println(string);
}
private MessageConsoleStream consoleStream;
private MessageConsole findConsole() {
ConsolePlugin plugin = ConsolePlugin.getDefault();
IConsoleManager conMan = plugin.getConsoleManager();
IConsole[] existing = conMan.getConsoles();
for (int i = 0; i < existing.length; i++)
if (LanguageServerPlugin.PLUGIN_ID.equals(existing[i].getName()))
return (MessageConsole) existing[i];
// no console found, so create a new one
MessageConsole myConsole = new MessageConsole(LanguageServerPlugin.PLUGIN_ID, null);
conMan.addConsoles(new IConsole[] { myConsole });
return myConsole;
}
private void logToFile(String string) {
if (logFile == null) {
return;
}
if (!logFile.exists()) {
try {
if (!logFile.createNewFile()) {
throw new IOException(String.format("Failed to create file %s", logFile.toString())); //$NON-NLS-1$
}
} catch (IOException e) {
LanguageServerPlugin.logError(e);
}
}
try {
Files.write(logFile.toPath(), string.getBytes(), StandardOpenOption.APPEND);
} catch (IOException e) {
LanguageServerPlugin.logError(e);
}
}
private File getLogFile() {
if (logFile != null) {
return logFile;
}
File logFolder = getLogDirectory();
if (logFolder == null) {
return null;
}
File file = new File(logFolder, id + ".log"); //$NON-NLS-1$
if (file.exists() && !(file.isFile() && file.canWrite())) {
return null;
}
return file;
}
}