-
Notifications
You must be signed in to change notification settings - Fork 326
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Execute and debug .enso
files with bin/enso
in VSCode
#8923
Changes from 5 commits
2f37be5
e7c97c5
0f5fb3d
5a44505
642124e
16518d6
1bbc0a3
d3ad80f
2fb11ac
5bce5b5
96a67c0
d87ba6e
a46d055
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
package org.enso.tools.enso4igv; | ||
|
||
import com.sun.jdi.connect.Connector; | ||
import java.io.File; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.concurrent.Callable; | ||
import java.util.concurrent.CancellationException; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.CompletionException; | ||
import java.util.concurrent.Future; | ||
import org.netbeans.api.debugger.jpda.JPDADebugger; | ||
import org.netbeans.api.debugger.jpda.ListeningDICookie; | ||
import org.netbeans.api.extexecution.ExecutionDescriptor; | ||
import org.netbeans.api.extexecution.ExecutionService; | ||
import org.netbeans.api.extexecution.base.ExplicitProcessParameters; | ||
import org.netbeans.spi.project.ActionProvider; | ||
import org.openide.DialogDisplayer; | ||
import org.openide.NotifyDescriptor; | ||
import org.openide.filesystems.FileObject; | ||
import org.openide.util.Lookup; | ||
import org.openide.util.lookup.ServiceProvider; | ||
|
||
import org.netbeans.api.extexecution.base.ProcessBuilder; | ||
import org.netbeans.api.java.classpath.ClassPath; | ||
import org.netbeans.api.java.platform.JavaPlatform; | ||
import org.netbeans.spi.project.ActionProgress; | ||
import org.openide.filesystems.FileUtil; | ||
import org.openide.util.NbBundle; | ||
import org.openide.util.NbPreferences; | ||
import org.openide.util.RequestProcessor; | ||
import org.openide.windows.IOProvider; | ||
|
||
@NbBundle.Messages({ | ||
"CTL_EnsoWhere=Enso Executable Location", | ||
"CTL_EnsoExecutable=enso/enso.bat", | ||
"# {0} - executable file", | ||
"MSG_CannotExecute=Cannot execute {0}", | ||
"# {0} - executable file", | ||
"# {1} - exit code", | ||
"MSG_ExecutionError=Process {0} finished with exit code {1}" | ||
}) | ||
@ServiceProvider(service = ActionProvider.class) | ||
public final class EnsoActionProvider implements ActionProvider { | ||
|
||
@Override | ||
public String[] getSupportedActions() { | ||
return new String[]{ActionProvider.COMMAND_RUN_SINGLE, ActionProvider.COMMAND_DEBUG_SINGLE}; | ||
} | ||
|
||
@Override | ||
public void invokeAction(String action, Lookup lkp) throws IllegalArgumentException { | ||
var process = ActionProgress.start(lkp); | ||
var params = ExplicitProcessParameters.buildExplicitParameters(lkp); | ||
var fo = lkp.lookup(FileObject.class); | ||
var script = FileUtil.toFile(fo); | ||
|
||
var io = IOProvider.getDefault().getIO(script.getName(), false); | ||
var dd = DialogDisplayer.getDefault(); | ||
|
||
var prefs = NbPreferences.forModule(EnsoActionProvider.class); | ||
var exeKey = "enso.executable"; | ||
|
||
var exe = prefs.get(exeKey, ""); | ||
var nd = new NotifyDescriptor.InputLine(Bundle.CTL_EnsoExecutable(), Bundle.CTL_EnsoWhere()); | ||
nd.setInputText(exe); | ||
|
||
var builderFuture = dd.notifyFuture(nd).thenApply(exec -> { | ||
var file = new File(exec.getInputText()); | ||
if (file.canExecute()) { | ||
prefs.put(exeKey, file.getPath()); | ||
|
||
var b = ProcessBuilder.getLocal(); | ||
b.setExecutable(file.getPath()); | ||
b.setArguments(Arrays.asList("--run", script.getPath())); | ||
b.setWorkingDirectory(script.getParent()); | ||
b.setRedirectErrorStream(true); | ||
|
||
var env = b.getEnvironment(); | ||
var path = env.getVariable("PATH"); | ||
var java = JavaPlatform.getDefault().findTool("java"); | ||
if (path != null && java != null) { | ||
var javaBinDir = FileUtil.toFile(java.getParent()); | ||
if (javaBinDir != null) { | ||
var newPath = path + File.pathSeparator + javaBinDir; | ||
env.setVariable("PATH", newPath); | ||
} | ||
} | ||
|
||
return b; | ||
} | ||
var msg = Bundle.MSG_CannotExecute(file.getPath()); | ||
throw new IllegalArgumentException(msg); | ||
}); | ||
|
||
var waitForProcessFuture = builderFuture.thenCompose((builder) -> { | ||
var cf = new CompletableFuture<Integer>(); | ||
var descriptor = new ExecutionDescriptor() | ||
.frontWindow(true).controllable(true) | ||
.inputOutput(io) | ||
.postExecution((exitCode) -> { | ||
cf.complete(exitCode); | ||
}); | ||
var launch = ActionProvider.COMMAND_DEBUG_SINGLE.equals(action) ? | ||
new DebugAndLaunch(fo, builder, params) : builder; | ||
var service = ExecutionService.newService(launch, descriptor, script.getName()); | ||
service.run(); | ||
return cf; | ||
}); | ||
|
||
waitForProcessFuture.thenAcceptBoth(builderFuture, (exitCode, builder) -> { | ||
if (exitCode != 0) { | ||
var msg = Bundle.MSG_ExecutionError(builder.getDescription(), exitCode); | ||
var md = new NotifyDescriptor.Message(msg, NotifyDescriptor.ERROR_MESSAGE); | ||
dd.notifyLater(md); | ||
} | ||
process.finished(exitCode == 0); | ||
}).exceptionally((ex) -> { | ||
process.finished(false); | ||
if (ex instanceof CompletionException && ex.getCause() instanceof CancellationException) { | ||
return null; | ||
} | ||
dd.notifyLater(new NotifyDescriptor.Message(ex.getMessage(), NotifyDescriptor.ERROR_MESSAGE)); | ||
return null; | ||
}); | ||
} | ||
|
||
@Override | ||
public boolean isActionEnabled(String string, Lookup lkp) throws IllegalArgumentException { | ||
if (lkp.lookup(EnsoDataObject.class) != null) { | ||
return true; | ||
} else { | ||
var fo = lkp.lookup(FileObject.class); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd be simpler to do just |
||
return fo != null && fo.getLookup().lookup(EnsoDataObject.class) != null; | ||
} | ||
} | ||
|
||
static final class DebugAndLaunch implements Callable<Process> { | ||
private static final RequestProcessor RP = new RequestProcessor(DebugAndLaunch.class); | ||
private final FileObject script; | ||
private final ProcessBuilder builder; | ||
private final ExplicitProcessParameters params; | ||
private final Future<String> computeAddress; | ||
|
||
DebugAndLaunch(FileObject script, ProcessBuilder builder, ExplicitProcessParameters params) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've got debugging working. Execute VSCode with empty directories: $ code --extensions-dir /tmp/ext --user-data-dir /tmp/usr install the Right now the debugging seems to work when there is no workspace opened - opening root of Enso repository as workspace prevents the debugging to work. Still investigating how to fix that. |
||
this.script = script; | ||
this.builder = builder; | ||
this.computeAddress = RP.submit(this::initAddress); | ||
this.params = params; | ||
} | ||
|
||
@Override | ||
public Process call() throws Exception { | ||
var port = computeAddress.get(); | ||
builder.getEnvironment().setVariable("JAVA_OPTS", "-agentlib:jdwp=transport=dt_socket,address=" + port); | ||
return builder.call(); | ||
} | ||
|
||
private String initAddress() throws Exception { | ||
var lc = ListeningDICookie.create(-1); | ||
var connector = lc.getListeningConnector(); | ||
|
||
var args = lc.getArgs(); | ||
var address = connector.startListening(args); | ||
|
||
var properties = new HashMap<>(); | ||
{ | ||
var sourcePath = ClassPath.getClassPath(script, ClassPath.SOURCE); | ||
properties.put("sourcepath", sourcePath); | ||
properties.put("baseDir", FileUtil.toFile(script.getParent())); | ||
properties.put("name", script.getName()); | ||
} | ||
|
||
var services = new Object[] { properties }; | ||
int port = Integer.parseInt(address.substring(address.indexOf(':') + 1)); | ||
Connector.IntegerArgument portArg = (Connector.IntegerArgument) args.get("port"); | ||
portArg.setValue(port); | ||
|
||
RP.submit(() -> { | ||
JPDADebugger.startListening(connector, args, services); | ||
return null; | ||
}); | ||
|
||
return Integer.toString(port); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While working on this code base, I am trying to make sure the code works as:
The biggest challenge is support for IGV as it is not being updated to run on latest NetBeans version too frequently.
However for this PR I need DialogDisplayer.notifyFuture (thank you, @sdedic) which is only available since NetBeans 14 - e.g. we have to upgrade.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With oracle/graal#8290 the Enso IGV Plugin version 1.31.129 can be installed into IGV:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once the
.nbm
file is installed into IGV, one can press Shift-F6 inside of a.enso
file and get the same behavior as in the VSCode extension: