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 debugging session. So be patient, these are the last preparations we need to take.
  1. A fictional interpreter
  2. The launch framework
  3. A tale of debuggers, processes and threads
  4. Stepping, suspending and other actions
  5. UI breakpoint integration
  6. Debugger breakpoint integration
  7. Display current source code - source lookup
  8. Run to line support
  9. Displaying variables
  10. Displaying memory areas
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.

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.


Source code for this tutorial is available on googlecode as a single zip archive, as a Team Project Set or you can checkout the SVN projects directly.


Step 1: Launch Delegates

Create a new Plug-in Project called com.codeandme.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. 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.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.


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.textinterpreter.ui;

public class TextLaunchDelegate implements ILaunchShortcut, ILaunchShortcut2, ILaunchConfigurationDelegate {

 // FIXME for full code see http://codeandme.googlecode.com/svn/trunk/blog/debugger/02/com.codeandme.textinterpreter.ui/src/com/codeandme/textinterpreter/ui/TextLaunchDelegate.java

 @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 23). The first existing configuration with a matching source file gets executed (line 40). If no matching configuration exists, we create (and store) a new one (lines 24 - 37).

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".

2 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