Skip to content
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

Add support for the Debug Adapter Protocol #92

Merged
merged 30 commits into from May 16, 2024
Merged

Add support for the Debug Adapter Protocol #92

merged 30 commits into from May 16, 2024

Conversation

agarciadom
Copy link
Contributor

@agarciadom agarciadom commented May 4, 2024

This PR adds support for debugging Epsilon scripts over the network, through the Debug Adapter Protocol. It has been tested with LSP4E and the debug-adapter branch of my fork of the Epsilon VS Code extension:

image

image

The PR includes a new example project with documentation, as well as automated tests for the server and the debug adapter itself. It has required some refactoring of the debugging infrastructure in Epsilon, separating the Eclipse-specific parts out to the EolDebugTarget, and moving all *Debugger classes (which are now Eclipse-agnostic) to their .engine plugins.

The PR allows for using the DAP server from the Ant tasks, and from plain Java. There is an implementation of approximate matching to allow for debugging scripts running from the classpath without requiring configuration, while looking out for ambiguous matches.

In order for the DebugAdapter to be able to use the appropriate debugger
for the given module, we need to move the responsibility of creating the
right debugger to the *Engine class of the language in question.
There are cases where the URLs reported by the module are different than
those sent in DAP requests. This happens, for instance, if we're running
an Epsilon script from the classpath of a Java program, instead of
running it directly from a file in the filesystem.

This commit introduces an approach which will match based on the longest
matching sequence of trailing URI components. Suppose we had set a
breakpoint on this file in our IDE:

file:/your/path/to/sources/a.b.c/epsilon/main/a.eol

If we had modules for these:

platform:/plugin/a.b.c/epsilon/main/a.eol
platform:/plugin/a.b.c/epsilon/util/a.eol

Then it would prefer the main/a.eol option, as it would match a longer
sequence of trailing URI components.
@agarciadom
Copy link
Contributor Author

On second thought, the approximate matching was not a good idea. It would help with the initial setting of breakpoints, but when we actually hit one, we had no reliable way to tell the DAP client the file that should be shown to the user.

I have replaced the approximate matching with an explicit URI-to-path mapping. Before starting the debugging session, we tell the adapter which URI prefixes match to which filesystem paths (e.g. http://foo/bar maps to folder project/foo/bar). These mappings can be then used both ways:

  • When someone sets a breakpoint on project/foo/bar/x.eol, map it back to the module with URI http://foo/bar/x.eol.
  • When the breakpoint on http://foo/bar/x.eol:43 is hit, map it back to line 43 of project/foo/bar/x.eol.

@agarciadom
Copy link
Contributor Author

The PR branch passes the tests on Jenkins:

https://ci.eclipse.org/epsilon/job/interim-kubernetes/job/debug-adapter/21/

This makes the logic easier to understand: if we set a mapping, we
will always try that first. We will only fall back to using the
module's file if we don't have an appropriate URI-to-path mapping.
@agarciadom
Copy link
Contributor Author

agarciadom commented May 10, 2024

In an initial walkthrough over the approach, @kolovos commented that it made more sense to always try to use the URI-to-Path mappings first, and only fall back to the module file if that didn't produce a path. I agree that it makes the user experience more consistent (instead of depending on whether we run a program which uses an Epsilon script from the classpath via Eclipse or via a JAR file), so I have made that change.

@agarciadom
Copy link
Contributor Author

I have also looked a bit more into the VS Code configuration, and it turns out you can use preLaunchTask in the launch.json file to have it start the EOL script in debug mode, wait for the port to open, and then have VS Code connect to it. There is now a 1-click launch.json configuration which will both launch the script and attach to it for debugging.

A future version of the Epsilon VS Code extension could perhaps simplify this a bit, by just retrying the connection a few times before handing over control to VS Code.

VS Code can do the waiting for the server for us, so long as we run the
Gradle task as a background task, and provide a problemMatcher with the
"background" key set up to detect when we are ready to debug.

This requires the latest version of the remote-debug branch of the
Epsilon extension for VS Code, which provides a problemMatcher with the
necessary "background" patterns.
@agarciadom
Copy link
Contributor Author

agarciadom commented May 13, 2024

VS Code already has the necessary functionality to wait until the session is ready to be debugged. You only need to provide a problemMatcher with the appropriate background patterns. My fork of the VS Code extension now contributes this problem matcher, and the VS Code example uses it.

I guess it would be nice if the user didn't have to set up tasks.json for it: there may be a way to have the VS Code extension automate that part.

@Arkaedan
Copy link
Contributor

I guess it would be nice if the user didn't have to set up tasks.json for it: there may be a way to have the VS Code extension automate that part.

https://code.visualstudio.com/api/extension-guides/task-provider
Definitely looks doable

@agarciadom
Copy link
Contributor Author

agarciadom commented May 14, 2024

I guess it would be nice if the user didn't have to set up tasks.json for it: there may be a way to have the VS Code extension automate that part.

https://code.visualstudio.com/api/extension-guides/task-provider Definitely looks doable

I had a look yesterday, and found that Gradle contributes its own kind of task and its own task provider, which does most of what we need and more (except for the background patterns for problem matching). This is what the tasks.json file looks like now:

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "gradle",
            "script": "debugHello",
            "group": "other",
            "buildFile": "${workspaceFolder}/build.gradle",
            "workspaceFolder": "${workspaceFolder}",
            "projectFolder": "${workspaceFolder}",
            "args": "--info",
            "problemMatcher": "$epsilon-debug",
            "label": "epsilonDebug: hello",
            "isBackground": true
        }
    ]
}

Writing the task provider would not be trivial, as we'd have to find out which Gradle tasks are available. It looks like the vscode-gradle extension communicates with the Gradle daemon to do this, and I think it'd be best to avoid complicating the VS Code Epsilon extension too much. It may be best to just explain how to use the Gradle VS Code extension to automate the initial creation of the task, and what to tweak to turn that initial fragment into something suitable for debugging an Epsilon script.

We could also provide the above snippet, and tell users how to tweak the label and script keys to suit their Gradle buildfile.

The existing code would only work for simple variables, but it would
have required having a getReference(...) method for every type of
reference that we would have.

This commit introduces a generic interface (IVariableReference) for
anything that we can reference through a DAP integer, which we could
then use to inspect model element properties, the elements of a
collection, and so on.
This commit allows for inspecting Java collections. For small
collections (below the threshold in SuspendedState, currently 200
elements) we will list each element on its own. For larger collections,
we will initially only show slices (according to the constant in
SuspendedState, currently 100-element long), and then let the user
specify the slice they are interested in.

For slices, there is a minor optimisation where we will use List.subList
instead of creating our own list by iterating to the starting index.
@agarciadom
Copy link
Contributor Author

agarciadom commented May 15, 2024

I've added the ability to inspect collections (per-element for small ones, and per-slice for large ones) and the properties of model elements. I have also tweaked EclipseHost so it will start the DAP server if both debug and debugPort are set: it won't automatically connect to it yet because that would introduce a dependency from Epsilon to LSP4E. Users would have to separately install LSP4E and run an attach LSP4E launch configuration.

I think this is ready for general use. In terms of future improvements, we could do things like showing the type of the variables in addition to their name and value, or perhaps supporting launch requests, but those bits feel outside the scope of this first PR.

Copy link
Contributor

@kolovos kolovos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It all looks good to me!

@agarciadom agarciadom merged commit 9bb2044 into main May 16, 2024
1 check passed
@agarciadom agarciadom added this to the 2.6.0 milestone May 16, 2024
@agarciadom agarciadom added the enhancement New feature or request label May 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants