Monday, November 11, 2013

Debugger 2: The launch framework (not only for debuggers)

In the previous tutorial we created a simple interpreter for a fictional programming language. Now we will add launch support for such scripts. We still will not be debugging yet, but launching is mandatory before we can start a debug session. So be patient, these are the last preparations we need to take.

If launching is something you are already familiar with, you might still want to check out the TextLaunchDelegate implementation, as it provides a convenient multi-purpose launcher.

Debug Framework Tutorials

For a list of all debug related tutorials see Debug Framework Tutorials Overview.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online. 

Launching

Eclipse provides two ways of launching: you may use a LaunchDelegate or a contextual LaunchShortcut. If your launch is about resources in your workspace you will quite probably implement both methods. After this tutorial we should have launch support for our interpreter.


Step 1: Launch Delegates

Create a new Plug-in Project called com.codeandme.debug.textinterpreter.ui. Switch to the Extensions tab and Add... a new launchConfigurationTypes extension. Provide a unique id and a name. The delegate class implements launch instructions, eg. creating and starting an interpreter. We will look at the implementation a bit later. Finally define the modes this LaunchConfiguration applies to. You may provide "run" and/or "debug" as a comma separated list.

Next add a launchConfigurationTypeImages extension to provide a nice icon for our LaunchConfiguration. The configTypeID needs to match the ID we provided for our launchConfigurationType before. Not much we can do wrong here.

Finally create a launchConfigurationTabGroups extension to provide the tabs to be visible when we open the Run Configurations... dialog. The class implementation is really simple as we only need to set an array of available tabs:
package com.codeandme.debugger.textinterpreter.ui.tabs;

import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup;
import org.eclipse.debug.ui.CommonTab;
import org.eclipse.debug.ui.ILaunchConfigurationDialog;
import org.eclipse.debug.ui.ILaunchConfigurationTab;

public class LaunchConfigurationTabGroup extends AbstractLaunchConfigurationTabGroup {

    @Override
    public void createTabs(final ILaunchConfigurationDialog dialog, final String mode) {
        setTabs(new ILaunchConfigurationTab[] { new MainTab(new String[] { "txt" }), new CommonTab() });
    }
}
MainTab is a custom class that lets the user select a project and a file from the workspace. As this is pure UI programming I leave it to you to examine the implementation.

More interesting is the launchMode you may add to the launchConfigurationTabGroup: for one you can provide the mode, which means you may have different  tabs for "run" and "debug". The perspective property allows to set a default perspective when the launch is activated. In debug mode you will typically want to switch to the debug perspective. Right now we will just provide the "run" launchMode.

Step 2: Launch Shortcuts

Launch shortcuts are displayed in the context menu of workspace files and editors. They can be used to quickly launch a file without creating a LaunchConfiguration first.

The launchShortcuts extensions is very similar to the LaunchDelegates we had before. We have modes, an icon and an implementing class. With a contextualLaunch we may provide an enablement. In our case we expect that only one file is selected and its file extension matches "txt".

As we already defined a LaunchConfiguationType before, we may add it to the shortcut by providing a configurationType child node.


Step 3: Launcher implementation

We did not investigate the launcher classes for both launch types yet. As LaunchDelegates and LaunchShortcuts are very similar, I decided to implement both of them in a single class TextLaunchDelegate:
package com.codeandme.debugger.textinterpreter.ui;

public class TextLaunchDelegate implements ILaunchShortcut, ILaunchShortcut2, ILaunchConfigurationDelegate {

 @Override
 public void launch(final ILaunchConfiguration configuration, final String mode, final ILaunch launch, final IProgressMonitor monitor) throws CoreException {
  IFile file = getSourceFile(configuration);
  if (file != null) {
   // we have a valid script, lets feed it to the script engine
   launch(file, configuration, mode, launch, monitor);
  }
 }

 private void launch(final IResource file, final String mode) {

  if (file instanceof IFile) {
   // try to save dirty editors
   PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().saveAllEditors(true);

   try {
    ILaunchConfiguration[] configurations = getLaunchConfgurations(file);
    if (configurations.length == 0) {
     // no configuration found, create new one
     ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
     ILaunchConfigurationType type = manager.getLaunchConfigurationType(TextLaunchConstants.LAUNCH_CONFIGURATION_TYPE_ID);

     ILaunchConfigurationWorkingCopy configuration = type.newInstance(null, file.getName());
     configuration.setAttribute(TextLaunchConstants.PROJECT, file.getProject().getName());
     configuration.setAttribute(TextLaunchConstants.FILE_LOCATION, file.getProjectRelativePath().toPortableString());

     // save and return new configuration
     configuration.doSave();

     configurations = new ILaunchConfiguration[] { configuration };
    }

    // launch
    configurations[0].launch(mode, new NullProgressMonitor());

   } catch (CoreException e) {
    // could not create launch configuration, run file directly
    launch((IFile) file, null, mode, null, new NullProgressMonitor());
   }
  }
 }

 private void launch(final IFile file, final ILaunchConfiguration configuration, final String mode, final ILaunch launch, final IProgressMonitor monitor) {
  // create new interpreter
  TextInterpreter interpreter = new TextInterpreter();

  try {
   interpreter.setCode(toString(file));
   interpreter.schedule();

  } catch (Exception e) {
   // TODO handle this exception (but for now, at least know it
   // happened)
   throw new RuntimeException(e);
  }
 }
}
The main intention here is: no matter how our launch was triggered, try to find/create a valid launch configuration and launch that configuration. This is fairly easy for ILaunchConfigurationDelegate.launch(...) as we already get a valid configuration in the parameters list.

ILaunchShortcut.launch() provides an editor or a selection. In that case we extract the resource file to execute. Afterwards we parse all launch configurations and compare their source file with our resource (line 21). The first existing configuration with a matching source file gets executed (line 38). If no matching configuration exists, we create (and store) a new one (lines 22 - 35).

This matches quite closely the behavior of Java LaunchShortcuts: after such a launch you will find a ready to use LaunchConfiguration.

Optional: Dependencies for Launch Shortcuts

When you are running your own RCP and do not have JDT bundled with your product you might encounter a problem where Run As.../Debug As... context menu entries vanish in the navigator views after using them once. While we wait for bug 415317 to be fixed, you might add org.eclipse.jdt.ui and org.eclipse.jdt.debug.ui plug-ins to your product as a workaround.


Optional: Add Run as... / Debug as... toolbar entries to perspective

Eclipse provides nice run/debug toolbar buttons every Eclipse user knows. If you are using a custom perspective, these entries might not be visible.

To enable them add a perspectiveExtensions extension to your plugin.xml. Define the targetId (which is the id of a perspective) to alter (use * as a wildcard pattern) and provide an actionSet child node pointing its id to "org.eclipse.debug.ui.launchActionSet".

7 comments:

  1. Thanks for great tutorial! It was exactly what I was looking for: short, clear and with working example.

    ReplyDelete
  2. Thank you for great tutorial, it was really helpful. Appreciate if you could share the source code. The links are broken.

    ReplyDelete
  3. Thanks for the tutorial! I am looking for this for months. Can you fix the links for the source code?

    ReplyDelete
  4. hello christian,
    first of all amazing tutorial and thank you for doing it to us, but i have a question.
    The only thing i need is the ui (add breakpoints, step into, step over, etc.) and i wondered do i need the perspective also? and if so how can i see this perspective that you wrote here (tutorial 2) in my eclipse after implementing it?

    ReplyDelete
    Replies
    1. What perspective are you talking about? Perspective has a dedicated meaning in Eclipse and I am not talking abount any perspectives here. To integrate UI debugging support simply follow this series, it is exactly what these tutorials are about.

      Delete
  5. hello Christian,
    can u please help me out to solve below complicated issue.

    right now we are tryng to call one java junit plugin application( test tool) from web application.
    problem is this junit plugin application runs on custom run configuration(project.launch file is already generated) and we are not able to invoke this junit plugin application from web application.
    is there any way for providing custom run configuration to web application.

    ReplyDelete