IDEA: run debug server when running dev server in debug mode, connect to this server automatically from the web page

This commit is contained in:
Alexey Andreev 2018-12-17 19:31:53 +03:00
parent b1e04da597
commit 66126856a2
15 changed files with 343 additions and 87 deletions

View File

@ -54,7 +54,6 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

View File

@ -0,0 +1,27 @@
/*
* Copyright 2018 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
window.addEventListener("message", function(event) {
if (event.source !== window) {
return;
}
var data = event.data;
if (typeof data.teavmDebugger !== "undefined") {
chrome.runtime.sendMessage({ command: "debug", port: data.teavmDebugger.port });
}
}, false);
window.postMessage({ teavmDebuggerRequest: true }, "*");

View File

@ -1,15 +1,16 @@
debuggerAgentMap = {};
chrome.browserAction.onClicked.addListener(function(tab) {
new DebuggerAgent(tab).attach();
new DebuggerAgent(tab, 2357).attach();
});
function DebuggerAgent(tab) {
function DebuggerAgent(tab, port) {
this.pendingMessages = [];
this.connection = null;
this.tab = null;
this.debuggee = { tabId : tab.id };
this.attachedToDebugger = false;
this.messageBuffer = "";
this.port = port;
debuggerAgentMap[tab.id] = this;
}
DebuggerAgent.MAX_MESSAGE_SIZE = 65534;
@ -20,12 +21,12 @@ DebuggerAgent.prototype.attach = function() {
}.bind(this, this.connectToServer.bind(this)));
};
DebuggerAgent.prototype.connectToServer = function() {
this.connection = new WebSocket("ws://localhost:2357/");
this.connection = new WebSocket("ws://localhost:" + this.port + "/");
this.connection.onmessage = function(event) {
var str = event.data;
var ctl = str.substring(0, 1);
this.messageBuffer += str.substring(1);
if (ctl == '.') {
if (ctl === '.') {
this.receiveMessage(JSON.parse(this.messageBuffer));
this.messageBuffer = "";
}
@ -95,4 +96,10 @@ chrome.debugger.onDetach.addListener(function(source) {
agent.attachedToDebugger = false;
agent.disconnect();
}
});
chrome.runtime.onMessage.addListener(function(message, sender, callback) {
if (message.command === "debug") {
new DebuggerAgent(sender.tab, message.port).attach();
}
});

View File

@ -3,9 +3,9 @@
"name": "TeaVM debugger agent",
"description": "TeaVM debugger agent, that sends RDP commands over WebSocket",
"version": "0.2",
"version": "0.6.0",
"permissions" : ["debugger", "activeTab", "tabs"],
"permissions" : ["debugger", "activeTab", "tabs", "*://*/*"],
"browser_action" : {
"default_icon": "teavm-16.png",
@ -14,5 +14,12 @@
"background": {
"scripts": ["main.js"]
}
},
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "file://*/*"],
"js": ["contentscript.js"]
}
]
}

View File

@ -79,6 +79,7 @@ public class CodeServlet extends HttpServlet {
private boolean indicator;
private boolean automaticallyReloaded;
private int port;
private int debugPort;
private Map<String, Supplier<InputStream>> sourceFileCache = new HashMap<>();
@ -138,6 +139,10 @@ public class CodeServlet extends HttpServlet {
this.port = port;
}
public void setDebugPort(int debugPort) {
this.debugPort = debugPort;
}
public void setAutomaticallyReloaded(boolean automaticallyReloaded) {
this.automaticallyReloaded = automaticallyReloaded;
}
@ -450,10 +455,6 @@ public class CodeServlet extends HttpServlet {
}
private void addIndicator() {
if (!indicator) {
return;
}
String script = getIndicatorScript(false);
try (Writer writer = new OutputStreamWriter(buildTarget.appendToResource(fileName), StandardCharsets.UTF_8)) {
writer.append("\n");
@ -472,6 +473,8 @@ public class CodeServlet extends HttpServlet {
script = script.replace("BOOT_FLAG", Boolean.toString(boot));
script = script.replace("RELOAD_FLAG", Boolean.toString(automaticallyReloaded));
script = script.replace("FILE_NAME", "http://localhost:" + port + pathToFile + fileName);
script = script.replace("INDICATOR_FLAG", Boolean.toString(indicator));
script = script.replace("DEBUG_PORT", Integer.toString(debugPort));
return script;
} catch (IOException e) {
throw new RuntimeException("IO error occurred writing debug information", e);

View File

@ -46,6 +46,7 @@ public class DevServer {
private Server server;
private int port = 9090;
private int debugPort;
public void setMainClass(String mainClass) {
this.mainClass = mainClass;
@ -59,6 +60,10 @@ public class DevServer {
this.port = port;
}
public void setDebugPort(int debugPort) {
this.debugPort = debugPort;
}
public void setPathToFile(String pathToFile) {
if (!pathToFile.startsWith("/")) {
pathToFile = "/" + pathToFile;
@ -122,6 +127,7 @@ public class DevServer {
servlet.setIndicator(indicator);
servlet.setAutomaticallyReloaded(reloadedAutomatically);
servlet.setPort(port);
servlet.setDebugPort(debugPort);
for (DevServerListener listener : listeners) {
servlet.addListener(listener);
}

View File

@ -16,6 +16,8 @@
(function () {
var boot = BOOT_FLAG;
var reload = RELOAD_FLAG;
var indicatorVisible = INDICATOR_FLAG;
var debugPort = DEBUG_PORT;
function createWebSocket() {
var loc = window.location;
@ -121,10 +123,12 @@
document.body.appendChild(indicator.container);
}
if (document.body) {
onLoad();
} else {
window.addEventListener("load", onLoad);
if (indicatorVisible) {
if (document.body) {
onLoad();
} else {
window.addEventListener("load", onLoad);
}
}
function startMain() {
@ -164,4 +168,17 @@
if (boot) {
indicator.show("File is not ready, about to compile");
}
if (debugPort > 0) {
function connectDebugAgent(event) {
if (event.source !== window) {
return;
}
var data = event.data;
if (typeof data.teavmDebuggerRequest !== "undefined") {
window.postMessage({teavmDebugger: {port: debugPort}}, "*");
}
}
window.addEventListener("message", connectDebugAgent);
}
})();

View File

@ -94,7 +94,7 @@
<target>
<mkdir dir="${basedir}/dependencies/"/>
<get src="https://plugins.jetbrains.com/plugin/download?updateId=42362"
dest="${basedir}/dependencies/scala.zip"/>
dest="${basedir}/dependencies/scala.zip" skipexisting="true"/>
<unzip src="${basedir}/dependencies/scala.zip" dest="${basedir}/dependencies"/>
</target>
</configuration>

View File

@ -16,6 +16,8 @@
package org.teavm.idea.debug;
import com.intellij.debugger.ui.breakpoints.JavaLineBreakpointType;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.ui.ExecutionConsole;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.extensions.ExtensionPoint;
import com.intellij.openapi.extensions.Extensions;
@ -49,6 +51,7 @@ public class TeaVMDebugProcess extends XDebugProcess {
private final int port;
private ChromeRDPServer debugServer;
ConcurrentMap<Breakpoint, XBreakpoint<?>> breakpointMap = new ConcurrentHashMap<>();
public ExecutionConsole console;
public TeaVMDebugProcess(@NotNull XDebugSession session, int port) {
super(session);
@ -188,4 +191,10 @@ public class TeaVMDebugProcess extends XDebugProcess {
public XBreakpointHandler<?>[] getBreakpointHandlers() {
return breakpointHandlers.toArray(new XBreakpointHandler<?>[0]);
}
@NotNull
@Override
public ExecutionConsole createConsole() {
return console != null ? console : super.createConsole();
}
}

View File

@ -26,4 +26,5 @@ public class DevServerConfiguration {
public int port;
public String pathToFile;
public String fileName;
public int debugPort;
}

View File

@ -125,6 +125,9 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
case "-f":
server.setFileName(args[++i]);
break;
case "-P":
server.setDebugPort(Integer.parseInt(args[++i]));
break;
}
}
server.setClassPath(classPath.toArray(new String[0]));
@ -179,6 +182,11 @@ public class DevServerRunner extends UnicastRemoteObject implements DevServerMan
arguments.add(entry);
}
if (options.debugPort > 0) {
arguments.add("-P");
arguments.add(Integer.toString(options.debugPort));
}
ProcessBuilder builder = new ProcessBuilder(arguments.toArray(new String[0]));
Process process = builder.start();
BufferedReader stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream(),

View File

@ -21,12 +21,11 @@ import com.intellij.execution.ExecutionResult;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.RunProfileState;
import com.intellij.execution.configurations.SearchScopeProvider;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.filters.TextConsoleBuilder;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ProgramRunner;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.execution.util.JavaParametersUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
@ -36,24 +35,24 @@ import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.search.GlobalSearchScope;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.Objects;
import java.util.Random;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.teavm.idea.DaemonUtil;
import org.teavm.idea.DevServerRunnerListener;
import org.teavm.idea.devserver.ui.TeaVMDevServerConsole;
public class TeaVMDevServerRunState implements RunProfileState {
private final TeaVMDevServerConfiguration configuration;
private final TextConsoleBuilder consoleBuilder;
private final Project project;
public TeaVMDevServerRunState(@NotNull ExecutionEnvironment environment,
@NotNull TeaVMDevServerConfiguration configuration) {
this.configuration = configuration;
Project project = environment.getProject();
project = environment.getProject();
GlobalSearchScope searchScope = SearchScopeProvider.createSearchScope(project, environment.getRunProfile());
consoleBuilder = TextConsoleBuilderFactory.getInstance().createBuilder(project, searchScope);
}
@ -83,15 +82,44 @@ public class TeaVMDevServerRunState implements RunProfileState {
config.mainClass = configuration.getMainClass();
config.maxHeap = configuration.getMaxHeap();
if (executor.getId().equals(DefaultDebugExecutor.EXECUTOR_ID)) {
config.debugPort = choosePort();
}
TeaVMProcessHandler processHandler;
ExecutionResult executionResult;
try {
TeaVMDevServerConsole console = new TeaVMDevServerConsole(consoleBuilder.getConsole());
ProcessHandlerImpl processHandler = new ProcessHandlerImpl(config, console);
processHandler = new TeaVMProcessHandler(config, console);
console.getUnderlyingConsole().attachToProcess(processHandler);
processHandler.start();
return new DefaultExecutionResult(console, processHandler);
executionResult = new DefaultExecutionResult(console, processHandler);
} catch (IOException e) {
throw new ExecutionException(e);
}
return executionResult;
}
private int choosePort() {
Random random = new Random();
int minPort = 10000;
int maxPort = 1 << 16;
for (int i = 0; i < 20; ++i) {
int port = minPort + random.nextInt(maxPort - minPort);
if (isPortAvailable(port)) {
return port;
}
}
throw new RuntimeException("Could not find available port");
}
private boolean isPortAvailable(int port) {
try (Socket ignored = new Socket("localhost", port)) {
return false;
} catch (IOException ignored) {
return true;
}
}
private String path(VirtualFile file) {
@ -103,63 +131,4 @@ public class TeaVMDevServerRunState implements RunProfileState {
}
return file.getCanonicalPath();
}
class ProcessHandlerImpl extends ProcessHandler implements DevServerRunnerListener {
private DevServerConfiguration config;
private TeaVMDevServerConsole console;
private DevServerInfo info;
ProcessHandlerImpl(DevServerConfiguration config, TeaVMDevServerConsole console) {
this.config = config;
this.console = console;
}
void start() throws IOException {
info = DevServerRunner.start(DaemonUtil.detectClassPath().toArray(new String[0]), config, this);
console.setServerManager(info.server);
startNotify();
}
@Override
protected void destroyProcessImpl() {
info.process.destroy();
}
@Override
protected void detachProcessImpl() {
try {
info.server.stop();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public boolean detachIsDefault() {
return true;
}
@Nullable
@Override
public OutputStream getProcessInput() {
return null;
}
@Override
public void error(String text) {
console.getUnderlyingConsole().print(text + System.lineSeparator(), ConsoleViewContentType.ERROR_OUTPUT);
}
@Override
public void info(String text) {
console.getUnderlyingConsole().print(text + System.lineSeparator(), ConsoleViewContentType.NORMAL_OUTPUT);
}
@Override
public void stopped(int code) {
console.stop();
notifyProcessTerminated(code);
}
}
}

View File

@ -20,11 +20,21 @@ import com.intellij.execution.ExecutionResult;
import com.intellij.execution.configurations.RunProfile;
import com.intellij.execution.configurations.RunProfileState;
import com.intellij.execution.configurations.RunnerSettings;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.GenericProgramRunner;
import com.intellij.execution.runners.RunContentBuilder;
import com.intellij.execution.ui.ExecutionConsole;
import com.intellij.execution.ui.RunContentDescriptor;
import com.intellij.xdebugger.XDebugProcess;
import com.intellij.xdebugger.XDebugProcessStarter;
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.XDebuggerManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.teavm.idea.debug.TeaVMDebugProcess;
public class TeaVMDevServerRunner extends GenericProgramRunner<RunnerSettings> {
@NotNull
@ -47,6 +57,32 @@ public class TeaVMDevServerRunner extends GenericProgramRunner<RunnerSettings> {
return null;
}
return null;
RunContentDescriptor runContent = new RunContentBuilder(executionResult, environment).showRunContent(null);
int debugPort = ((TeaVMProcessHandler) executionResult.getProcessHandler()).config.debugPort;
ExecutionConsole console = runContent.getExecutionConsole();
ProcessHandler processHandler = runContent.getProcessHandler();
if (debugPort > 0) {
XDebuggerManager debuggerManager = XDebuggerManager.getInstance(environment.getProject());
XDebugSession debugSession = debuggerManager.startSession(environment, new XDebugProcessStarter() {
@NotNull
@Override
public XDebugProcess start(@NotNull XDebugSession session) {
TeaVMDebugProcess debugProcess = new TeaVMDebugProcess(session, debugPort);
debugProcess.console = console;
return debugProcess;
}
});
runContent = debugSession.getRunContentDescriptor();
ProcessHandler debugProcessHandler = debugSession.getDebugProcess().getProcessHandler();
debugProcessHandler.addProcessListener(new ProcessAdapter() {
@Override
public void processTerminated(@NotNull ProcessEvent event) {
processHandler.destroyProcess();
}
});
}
return runContent;
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2018 Alexey Andreev.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.teavm.idea.devserver;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.ui.ConsoleViewContentType;
import java.io.IOException;
import java.io.OutputStream;
import org.jetbrains.annotations.Nullable;
import org.teavm.idea.DaemonUtil;
import org.teavm.idea.DevServerRunnerListener;
import org.teavm.idea.devserver.ui.TeaVMDevServerConsole;
class TeaVMProcessHandler extends ProcessHandler implements DevServerRunnerListener {
DevServerConfiguration config;
private TeaVMDevServerConsole console;
private DevServerInfo info;
TeaVMProcessHandler(DevServerConfiguration config, TeaVMDevServerConsole console) {
this.config = config;
this.console = console;
}
void start() throws IOException {
info = DevServerRunner.start(DaemonUtil.detectClassPath().toArray(new String[0]), config, this);
console.setServerManager(info.server);
startNotify();
Runtime.getRuntime().addShutdownHook(new Thread(() -> info.process.destroy()));
}
@Override
protected void destroyProcessImpl() {
info.process.destroy();
}
@Override
protected void detachProcessImpl() {
try {
info.server.stop();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public boolean detachIsDefault() {
return true;
}
@Nullable
@Override
public OutputStream getProcessInput() {
return null;
}
@Override
public void error(String text) {
console.getUnderlyingConsole().print(text + System.lineSeparator(), ConsoleViewContentType.ERROR_OUTPUT);
}
@Override
public void info(String text) {
console.getUnderlyingConsole().print(text + System.lineSeparator(), ConsoleViewContentType.NORMAL_OUTPUT);
}
@Override
public void stopped(int code) {
console.stop();
notifyProcessTerminated(code);
}
}

View File

@ -15,8 +15,12 @@
*/
package org.teavm.idea.devserver.ui;
import com.intellij.execution.filters.Filter;
import com.intellij.execution.filters.HyperlinkInfo;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ExecutionConsole;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.actionSystem.AnAction;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.rmi.RemoteException;
@ -26,11 +30,13 @@ import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.teavm.idea.devserver.DevServerBuildResult;
import org.teavm.idea.devserver.DevServerManager;
import org.teavm.idea.devserver.DevServerManagerListener;
public class TeaVMDevServerConsole extends JPanel implements ExecutionConsole {
public class TeaVMDevServerConsole extends JPanel implements ConsoleView {
private ConsoleView underlyingConsole;
private DevServerManager serverManager;
private ServerListenerImpl serverListener;
@ -135,6 +141,82 @@ public class TeaVMDevServerConsole extends JPanel implements ExecutionConsole {
rebuildButton.setEnabled(false);
}
@Override
public void print(@NotNull String s, @NotNull ConsoleViewContentType consoleViewContentType) {
underlyingConsole.print(s, consoleViewContentType);
}
@Override
public void clear() {
underlyingConsole.clear();
}
@Override
public void scrollTo(int i) {
underlyingConsole.scrollTo(i);
}
@Override
public void attachToProcess(ProcessHandler processHandler) {
underlyingConsole.attachToProcess(processHandler);
}
@Override
public void setOutputPaused(boolean b) {
underlyingConsole.setOutputPaused(b);
}
@Override
public boolean isOutputPaused() {
return underlyingConsole.isOutputPaused();
}
@Override
public boolean hasDeferredOutput() {
return underlyingConsole.hasDeferredOutput();
}
@Override
public void performWhenNoDeferredOutput(@NotNull Runnable runnable) {
underlyingConsole.performWhenNoDeferredOutput(runnable);
}
@Override
public void setHelpId(@NotNull String s) {
underlyingConsole.setHelpId(s);
}
@Override
public void addMessageFilter(@NotNull Filter filter) {
underlyingConsole.addMessageFilter(filter);
}
@Override
public void printHyperlink(@NotNull String s, @Nullable HyperlinkInfo hyperlinkInfo) {
underlyingConsole.printHyperlink(s, hyperlinkInfo);
}
@Override
public int getContentSize() {
return underlyingConsole.getContentSize();
}
@Override
public boolean canPause() {
return underlyingConsole.canPause();
}
@NotNull
@Override
public AnAction[] createConsoleActions() {
return underlyingConsole.createConsoleActions();
}
@Override
public void allowHeavyFilters() {
underlyingConsole.allowHeavyFilters();
}
class ServerListenerImpl extends UnicastRemoteObject implements DevServerManagerListener {
private NumberFormat percentFormat = NumberFormat.getPercentInstance();