Overview

Add p2 update functionality to an Eclipse 4 application – Eclipse RCP Cookbook

No Comments

The Equinox p2 project provides a provisioning infrastructure that can be used to update or install features into an OSGi application.

This recipe will explain and show how to add an update mechanism to an Eclipse 4 application.

Ingredients

To get started fast with this recipe, you can use the Eclipse RCP Cookbook – Basic Recipe we have prepared for you on GitHub.

To use the prepared basic recipe, import the project by cloning the Git repository:

  • File → Import → Git → Projects from Git
  • Click Next
  • Select Clone URI
  • Enter URI https://github.com/fipro78/e4-cookbook-basic-recipe.git
  • Click Next
  • Select the master branch (or the services branch which adds OSGi declarative services)
  • Click Next
  • Choose a directory where you want to store the checked out sources
  • Click Next
  • Select Import existing projects
  • Click Next
  • Click Finish

Preparation

Step 1: Update the Target Platform

  • Open the target definition de.codecentric.eclipse.tutorial.target.target in the project de.codecentric.eclipse.tutorial.target
  • Edit the Software Site http://download.eclipse.org/releases/luna/ by selecting it in the Locations section and then clicking Edit…
    • Disable Group by category
    • Filter for Equinox
    • Select the following features additionally to the already selected
      (Note: in the current state of the Target Editor you need to manually add the previously selected features too)

      • Equinox Core Function
      • Equinox p2, headless functionalities
  • Activate the target platform by clicking Set as Target Platform in the upper right corner of the Target Definition Editor

Eclipse4_p2_target_definition

Step 2: Prepare the application plug-in

To keep it simple, an update handler will be added to the application plug-in.

  • Update the bundle dependencies
    • Open the file META-INF/MANIFEST.MF in the project de.codecentric.eclipse.tutorial.app
    • Switch to the Dependencies tab
    • Add the following bundles to the Required Plug-ins
      • org.eclipse.equinox.p2.core
      • org.eclipse.equinox.p2.engine
      • org.eclipse.equinox.p2.operations
      • org.eclipse.equinox.p2.metadata.repository
  • Update the application model
    • Open the file Application.e4xmi in the project de.codecentric.eclipse.tutorial.app
    • Add a command
      • Application → Commands → Add
      • Set Name to Update
      • Set ID to de.codecentric.eclipse.tutorial.app.command.update
    • Add a handler
      • Application → Handlers → Add
      • Set ID to de.codecentric.eclipse.tutorial.app.handler.update
      • Set the Command reference to de.codecentric.eclipse.tutorial.app.command.update via Find… dialog
      • Create a handler implementation by clicking on the Class URI link
        • Set Package to de.codecentric.eclipse.tutorial.app.handler
        • Set Name to UpdateHandler
    • Add a main menu to the application for making the update command accessible for a user
      • Application → Windows and Dialogs → Trimmed Window
      • Check Main Menu in the details view of the Trimmed Window
      • Select the now visible Main Menu in the Application Model tree below the Trimmed Window
        • Set ID to org.eclipse.ui.main.menu
      • Add a Menu
        • Set ID to org.eclipse.ui.file.menu
        • Set Label to File
      • Add a Handled Menu Item to the File Menu
        • Set the label to Update
        • Set the Command reference to the Update command via Find … dialog

Step 3: Update the Product Configuration

  • Open the file de.codecentric.eclipse.tutorial.app.product in the project de.codecentric.eclipse.tutorial.product
  • Switch to the Dependencies tab
    •  Add
      • org.eclipse.equinox.p2.core.feature
    • Click Add Required to add the ECF features required by the p2 core feature
  • Switch to the Overview tab
    • Ensure that a proper version is set to the Version field, e.g. 1.0.0

Step 4: Implement the update handler

Performing a p2 update basically consists of three steps:

  1. Create the update operation that should be performed
  2. Check if there are available updates for the specified update operation
  3. Perform the update by executing a provisioning job if updates are available

For a good user experience it is best practice to execute those operations in background threads to keep the UI responsive. To keep the focus on p2 updates in this recipe, I will not go into details of background processing via the Eclipse Jobs API here.

You should also consider giving feedback to the user about the update operation results between the steps. For example, if updates are available, the user should be asked whether to perform the update or not.

4.1 Update handler preparation

  • Get the following values injected in UpdateHandler#execute()
    • IProvisioningAgent
      OSGi service needed for creating an update operation
    • UISynchronize
      Helper class for executing code in the UI thread
    • IWorkbench
      The current workbench. Will be needed to restart the application.
public class UpdateHandler {
 
	@Execute
	public void execute(
            IProvisioningAgent agent, 
            UISynchronize sync, 
            IWorkbench workbench) {
		...
	}
 
}

4.2 Create the update operation

p2 operations are performed via an UpdateOperation instance. To create an UpdateOperation, a ProvisioningSession is needed, which can be created by providing an IProvisioningAgent. As the IProvisioningAgent is available as an OSGi service if the necessary bundle was started, it can be simply retrieved via dependency injection.

ProvisioningSession session = new ProvisioningSession(agent);
// update all user-visible installable units
UpdateOperation operation = new UpdateOperation(session);

Note: The UpdateOperation can also be configured to only update selected installable units.

4.3 Check if there are updates available

Via UpdateOperation#resolveModal(IProgressMonitor) you can check whether updates for the installable units are available or not.  It will return an IStatus which can be inspected to give feedback to the user.

IStatus status = operation.resolveModal(null);
if (status.getCode() == UpdateOperation.STATUS_NOTHING_TO_UPDATE) {
	MessageDialog.openInformation(
            null, 
            "Information", 
            "Nothing to update");
}

Note: Using null as IProgressMonitor will simply avoid progress reporting. Use an appropriate value if progress reporting should be performed, e.g. via JFace ProgressMonitorDialog.

4.4 Check if an update can be performed

Via UpdateOperation#getProvisioningJob(IProgressMonitor) the provisioning job can be retrieved to perform the resolved operation. Since it can be null under several circumstances, it needs to be checked prior to scheduling.

If a ProvisioningJob could be created, it is possible to perform an update. For good UX ask the user whether the update should be performed or not. If yes, start the ProvisioningJob. This can either be done via ProvisioningJob#runModal(IProgressMonitor) if the job should be executed in the current thread, or ProvisioningJob#schedule() if it should be executed asynchronously.

ProvisioningJob provisioningJob = operation.getProvisioningJob(null);
if (provisioningJob != null) {
       	sync.syncExec(new Runnable() {
 
                @Override
                public void run() {
                    boolean performUpdate = MessageDialog.openQuestion(
                            null,
                            "Updates available",
                            "There are updates available. Do you want to install them now?");
                    if (performUpdate) {
                        ...
                        provisioningJob.schedule();
                    }
                }
            });	
}
else {
        if (operation.hasResolved()) {
                MessageDialog.openError(
                    null, 
                    "Error""Couldn't get provisioning job: " + operation.getResolutionResult());
        }
        else {
                MessageDialog.openError(
                    null, 
                    "Error""Couldn't resolve provisioning job");
        }
}

4.5 Restart the application after the update is finished

After an update was performed, it is good practice to restart the application so the updates are applied correctly. This can be done using IWorkbench#restart().

boolean restart = MessageDialog.openQuestion(null,
    "Updates installed, restart?",
    "Updates have been installed successfully, do you want to restart?");
if (restart) {
    workbench.restart();
}

Note: If the ProvisioningJob was executed asynchronously via ProvisioningJob#schedule(), you need to perform this operation via IJobChangeListener attached to the ProvisioningJob.

Note: If the update made changes to the application model, it is necessary to clear the persisted state. Otherwise the updates won’t be visible to the user. In the example application an update of the application model works because the -clearPersistedState flag is set. Typically this is not the case for productive environments. At the time of writing this blog post, it is only possible to solve this via workarounds, e.g using the Tom Schindls RestartService. Hopefully such a service will soon make it into the Eclipse platform itself!

The complete UpdateHandler using a JFace ProgressMonitor can be found on GitHub.

Step 5: Configure the repository location

To perform update operations, it is necessary to configure the repositories to check against. You need to specify the artifact repositories, which contain the actual content being installed or managed, and the metadata repositories, which contain the installable units (IUs) that describe things that can be installed, the capabilities they provide, and the requirements they have.

Note: The artifact and metadata repositories don’t need to be at the same locations, but typically they are.

It is possible to configure the repositories programmatically via UpdateOperation#getProvisioningContext().setArtifactRepositories(URI[]) and UpdateOperation#getProvisioningContext().setMetadataRepositories(URI[]). But the best practice is to configure them via p2.inf configuration files.

  • Create the file p2.inf in the project de.codecentric.eclipse.tutorial.product
  • Add the following lines to configure the repository locations (e.g. C:/Development/tmp/repository)
    • ${#58} is the variable for ‘:’
instructions.configure=\
  addRepository(type:0,location:file${#58}/C${#58}/Development/tmp/repository);\
  addRepository(type:1,location:file${#58}/C${#58}/Development/tmp/repository/);

Further information on how to create and configure a p2.inf file can be found here:

Step 6: Export the product

  • Open the Product Configuration in the de.codecentric.eclipse.tutorial.product project
  • Select the Overview tab
  • Click Eclipse Product export wizard in the Exporting section
  • Select a directory to export to in the Destination section of the export wizard (e.g. C:/Development/tmp/base_export)
  • Ensure Generate p2 repository is checked
    • This also creates the p2 cache that is necessary to make updates work
    • Copy the export to another directory (e.g. C:/Development/tmp/app)
  • Leave the other options unchanged
  • Click Finish

Note: If you face issues on exporting regarding a missing plug-in dependency, try to add org.eclipse.equinox.concurrent to the Plug-ins section of de.codecentric.eclipse.tutorial.feature. This is due to platform changes explained here.

Step 7: Create an application update

  • Open the file Application.e4xmi in the project de.codecentric.eclipse.tutorial.app
  • Add an exit command
    • Application → Commands → Add
    • Set Name to Exit
    • Set ID to de.codecentric.eclipse.tutorial.app.command.exit
  • Add a handler
    • Application → Handlers → Add
    • Set ID to de.codecentric.eclipse.tutorial.app.handler.exit
    • Set the Command reference to de.codecentric.eclipse.tutorial.app.command.exit via Find… dialog
    • Create a handler implementation by clicking on the Class URI link
      • Set Package to de.codecentric.eclipse.tutorial.app.handler
      • Set Name to ExitHandler
      @Execute
      public void execute(IWorkbench workbench) {
          workbench.close();
      }
  • Add a Handled Menu Item to the File Menu
    • Set the label to Exit
    • Set the Command reference to the Exit command via Find … dialog
  • Increase the version of the plug-in de.codecentric.eclipse.tutorial.app
    • 1.0.1.qualifier
  • Increase the version of the feature de.codecentric.eclipse.tutorial.feature
    • 1.0.1.qualifier
  • Update the product configuration in de.codecentric.eclipse.tutorial.product
    • Increase the version of the product to 1.0.1
    • Update product feature dependencies
      • Edit Properties of the feature de.codecentric.eclipse.tutorial.feature and set the version to 1.0.1.qualifier
  • Export the updated product
    • Open the Product Configuration in the de.codecentric.eclipse.tutorial.product project
    • Select the Overview tab
    • Click Eclipse Product export wizard in the Exporting section
    • Select a directory to export to in the Destination section of the export wizard (e.g. C:/Development/tmp/update_export)
    • Ensure Generate p2 repository is checked
    • Click Finish
  • Copy the generated p2 repository located at C:/Development/tmp/update_export/repository to (e.g. C:/Development/tmp/repository)

Step 8: Taste

  • Start the application that was exported first via C:/Development/tmp/app/eclipse/eclipse.exe
  • Execute the update via File → Update
  • Check that an update is performed and that after the restart an Exit menu item is available in the File menu

Following the above steps, it is possible to add a simple update mechanism to an existing Eclipse 4-based application, using plain Equinox p2. With the exception of the JFace dialogs, the above approach can also be used in an e(fx)clipse application. If you are interested in using the e(fx)clipse UpdateService for performing p2 updates, stay tuned for the follow-up recipe!

Comment

Your email address will not be published. Required fields are marked *