Using Custom System Checkers for finding unresolved bundles

By Rafał Pydyniak | 2022-07-22

In this post I would like to explain what System Checkers in Liferay are, how you can implement one and what you can achieve using it. As an example I will use a real life scenario - what if you have a fragment which depends on another OSGi module and this module stops exporting the package you need in your fragment. In such scenario Liferay will sadly not report any error even though your fragment basically stops working. Lets find out how we can use system checkers to find such situations or how you can use them for any other checks you need.

Some introduction

So the situation I described at the beginning is the real issue I've experienced. In a project I work on we have quite a lot of different fragments. Some of these fragments make some huge changes in the basic views while others do only a tiny changes like adding a field, button or some text. Sometimes these fragments use constants from another API modules.

In my blog samples project I created such fragment as an example. This fragment is meant to add a simple text in the "Password" page of editing user view (Under Control Panel -> Users and Organizations -> Edit one of the users).

This fragment uses the API module for a constant text (lets skip translations in this example). The added code looks like this:

<p>
    <%=FakeConstants.FAKE_CONSTANT%>
</p>

Nothing really fancy here if you are familiar with fragments modules.

Anyway the issue with this fragment is that the FakeConstants class package is not exported by the API module's bnd.bnd file. Because of this the fragment will not start. It will stay in the Installed OSGi state

But hey - developer would notice that the fragment has not started

Of course. He would. And even if he pushed that without testing someone else would. For example tester or project manager or even a client. That's true.

But what if the module has been created few months ago, everything was working but then someone moved our class to another package because he thought it's only used internally in the same module? Or even removed the Export-Package line from bnd.bnd?

Well in such scenario the edit password page, which has worked for long time, might not be retested and the issue could've been overseen. Of course the page would still work but without changes from our fragment module. It would just use the default Liferay's view. This can be fine in some cases but maybe in some cases it's a legal requirement which your fragments fulfills for example adds some specific warning text required in your country. Or maybe your fragment adds some extra fields to the form and another module you use needs these fields otherwise the form won't work.

Doesn't Liferay give you some error on startup if the bundle couldn't be resolved?

Well actually it does but only for normal modules. In order to show this I created another module: not-resolved-bundles-web which is a basic portlet and it also uses the same, not exported API. In this case on startup there are errors in the logs:

2022-07-22 07:32:59.859 ERROR [fileinstall-directory-watcher][DirectoryWatcher:1173] Unable to start bundle: file:/opt/liferay/osgi/modules/com.pydyniak.blog.samples.not.resolved.bundles.web.jar
2022-07-22T07:32:59.860166363Z com.liferay.portal.kernel.log.LogSanitizerException: org.osgi.framework.BundleException: Could not resolve module: com.pydyniak.blog.samples.not.resolved.bundles.web [1503]_  Unresolved requirement: Import-Package: com.pydyniak.blog.samples.not.resolved.bundles.api_ [Sanitized]
2022-07-22T07:32:59.860172750Z 	at org.eclipse.osgi.container.Module.start(Module.java:444) ~[org.eclipse.osgi.jar:?]
2022-07-22T07:32:59.860182947Z 	at org.eclipse.osgi.internal.framework.EquinoxBundle.start(EquinoxBundle.java:428) ~[org.eclipse.osgi.jar:?]
2022-07-22T07:32:59.860184568Z 	at com.liferay.portal.file.install.internal.DirectoryWatcher._startBundle(DirectoryWatcher.java:1156) [bundleFile:?]
2022-07-22T07:32:59.860186144Z 	at com.liferay.portal.file.install.internal.DirectoryWatcher._startBundles(DirectoryWatcher.java:1189) [bundleFile:?]
2022-07-22T07:32:59.860187736Z 	at com.liferay.portal.file.install.internal.DirectoryWatcher._process(DirectoryWatcher.java:1046) [bundleFile:?]
2022-07-22T07:32:59.860189692Z 	at com.liferay.portal.file.install.internal.DirectoryWatcher.run(DirectoryWatcher.java:221) [bundleFile:?]

But the fragment doesn't give you the same errors. In fact Liferay doesn't give you any message in such case.

The issue is that the DirectoryWatcher which is responsible for starting modules works differently for fragments and for other modules. The fragments modules as you might now cannot be started, they can only be resolved (meaning all dependencies have been met) and then they works.

Then how can we find out that the fragment has not resolved?

Of course we still can find out that the fragment has not been resolved by using gogo shell. We can use the lb command which lists all the bundle. Then we can find bundles which are in "Installed" state and not resolved or active. Using this method we can find our two bundles:

Our two bundles in installed state
Our two bundles in installed state

Tip In the Gogo Shell we can also use grep to filter the results:

lb | grep "Installed"

Then we can use the diag command to find out why particular bundle hasn't started:

Example of using diag command
Example of using diag command

The main issue I have with that approach is that you have to manully use lb. In fact there is even an OSGi command called system:check which is supposed to find all the issues in the deployed bundles but it just doesn't show you any issues in our scenario:

No errors using system:check command
No errors using system:check command

And this is what we're going to change

System Checkers

What system checkers are

System checkers are the components which do different checks of your OSGi bundles. The idea is that you can run system:check and find out if everything is fine.

Also the system check is run automatically on every startup if you have a following portal properties entry:

module.framework.properties.initial.system.check.enabled=true

It works quite well for normal modules but like shown before you can't fully rely on the default system checkers if you're using fragments.

How we can implement a custom checker - example of creating one which finds installed (but not resolved) bundles

So we know that the default system checkers are not reliable in all situations but we can fix that with custom system checker which we will create on our own. The idea is really simple. We have to:

  • Create a class implementing SystemChecker interface
  • Register our class as a component
  • Implement all methods from the interface

So lets start with an empty class:

public class UnresolvedBundlesSystemChecker implements SystemChecker {

}

Then add a Component annotation:

@Component(
    immediate = true,
    property = {},
    service = SystemChecker.class
)
public class UnresolvedBundlesSystemChecker implements SystemChecker {

}

This interface has three methods we have to implement

public interface SystemChecker {

    public String check();

    public String getName();

    public String getOSGiCommand();

}

Lets start with the second and third ones. The getName one is supposed to just return name of the checker:

@Override
public String getName() {
    return "Unresolved Requirement System Checker";
}

The getOSGiCommand should return a OSGi command which can be run to execute this system check on its own. This command name is later logged in the system:check result.

@Override
public String getOSGiCommand() {
    return "pydyniak:unresolved";
}

You can name the command as you want. I will show you the implementation of the command component later.

Now lets go to the check method. This is the method that is being called when you run the system:check and it's the one where you should implement all your logic. In the end you have to return a String which will be displayed by system:check but of course you can add some extra logic there for example use Log to also log the issues to the log files.

How the logic should look like in our example? Well lets start with the fact that you can get information about bundles in OSGi by using BundleContext which you can obtain in the activate method of your component. So lets get the BundleContext and save it:

private BundleContext bundleContext;

@Activate
protected void activate(BundleContext bundleContext) {
    this.bundleContext = bundleContext;
}

Once we have that we can use that in our check method. I will first create an Util class which will handle our logic:

public class UnresolvedBundlesUtil {
    private UnresolvedBundlesUtil(){}

    public static String listUnresolvedBundles(BundleContext bundleContext) {
        return Arrays.stream(bundleContext.getBundles())
                .filter(UnresolvedBundlesUtil::isInInstalledState)
                .map(UnresolvedBundlesUtil::getMessage)
                .collect(Collectors.joining());
    }

    /**
     * We only look for installed bundles. This is the states in which bundles might have unresolved requirements
     * @param bundle
     * @return
     */
    private static boolean isInInstalledState(Bundle bundle) {
        return Bundle.INSTALLED == bundle.getState();
    }

    private static String getMessage(Bundle bundle) {
        StringBuilder message = new StringBuilder();
        message.append("\n\tBundle {id: ");
        message.append(bundle.getBundleId());
        message.append(", symbolicName: ");
        message.append(bundle.getSymbolicName());
        message.append("} is unresolved");
        return message.toString();
    }
}

So what are we doing here - the listUnresolvedBundles method take BundleContext object as a parameter. Then it uses it to obtain all the bundles in your OSGi container. In our example we want to find bundles that are not resolved. In OSGi world the bundle which couldn't be resolved is a bundle for which not all dependencies have been met. In such scenario it stays in Installed state. When new bundles are deployed our installed bundles are checked again in order to verify if the requirements have been met or not. If yes then the installed bundle gets resolved as well.

Note if you're not familiar with OSGi states you can read some more about them for example in JavaDocs of Bundle or in Liferay docs (they have some nice pictures) or in many different places across the Internet.

Checking the bundle state is quite easy and can be seen in isInInstalledState method:

private static boolean isInInstalledState(Bundle bundle) {
    return Bundle.INSTALLED == bundle.getState();
}

Then once we find bundles that are installed we can create some basic message for each of them:

private static String getMessage(Bundle bundle) {
    StringBuilder message = new StringBuilder();
    message.append("\n\tBundle {id: ");
    message.append(bundle.getBundleId());
    message.append(", symbolicName: ");
    message.append(bundle.getSymbolicName());
    message.append("} is unresolved");
    return message.toString();
}

Both methods mentioned above are used in the listUnresolvedBundles which uses Lambda to do all the work:

public static String listUnresolvedBundles(BundleContext bundleContext) {
    return Arrays.stream(bundleContext.getBundles())
        .filter(UnresolvedBundlesUtil::isInInstalledState)
        .map(UnresolvedBundlesUtil::getMessage)
        .collect(Collectors.joining());
}

Going back to our check method in UnresolvedBundlesSystemChecker class - we simply call our listUnresolvedBundles method and return the result:

@Override
public String check() {
    return UnresolvedBundlesUtil.listUnresolvedBundles(bundleContext);
}

Creating a OSGi command

In the beginning we defined custom OSGi command called pydyniak:unresolved which can be executed to run the system checker on its own. Sadly this won't call our check method by default. Luckily we can easily create the OSGi command on our own. To do that we have to create a simple component which again calls our UnresolvedBundlesUtil class and then simply uses System.out.println to log the result in the gogo shell:

@Component(
    immediate = true,
    property = {"osgi.command.function=unresolved", "osgi.command.scope=pydyniak"},
    service = UnresolvedBundlesOSGICommand.class
)
public class UnresolvedBundlesOSGICommand {
    private BundleContext bundleContext;

    @Activate
    protected void activate(BundleContext bundleContext) {
        this.bundleContext = bundleContext;
    }


    public void unresolved() {
        System.out.println(UnresolvedBundlesUtil.listUnresolvedBundles(bundleContext));
    }
}

If you have created OSGi commands before then it should be simple. If not then it's enough to understand that the osgi.command.function and osgi.command.scope define how our command can be run and then we just need a function named exactly the same as the osgi.command.function in our case it's unresolved.

Testing

Once we have everything we can get to the testing. First we must deploy our modules. Then we should go to the Control Panel -> Gogo Shell and execute system:check command (it takes a while). In the result we can see our custom system checker being run and its result:

Our custom system checker results
Our custom system checker results

We can also test our custom OSGi command, which is mentioned in the system:check result:

Testing the pydyniak:unresolved command
Testing the pydyniak:unresolved command

Now we can be sure that our code works just fine.

Final thoughts

With this quite simple module we can be more certain that if we run system:check we get all information about the errors.

As far as I'm concerned there are no other possible errors in bundles which could not be found with system:check (but I might be wrong ;)).

System checkers of course could be use for different things, even not related to the bundles or components states. Same for OSGi command - it's really cool feature in my opinion which I have used in few occasions and I really like them. They're quite well documented so if you have any doubts you can read the Liferay documentation.

On the other hand, the System Checkers are not documented at all, at least not for the moment. You can only find information about how to run the system check but not how to implement custom ones. That's why I decided to create this blog post.

The whole example (API, Service, Custom Portlet and Fragment) can be found on my Github: https://github.com/RafalPydyniak/liferay-blog-samples/tree/master/modules/not-resolved-bundles

Of course feel free to use the code in any way you want.

Like always I hope you learned something and of course if you have any questions just email me.

Copyright: Rafał Pydyniak