pf4j / pf4j-update

Update mechanism for PF4J
Apache License 2.0
69 stars 39 forks source link

Error updating plugin because file already in use #39

Open hazemkmammu opened 5 years ago

hazemkmammu commented 5 years ago

I tried running the example in the 'How to use' section of README.md. The update manager failed to update the plugin to the latest version because it could not delete the old version. The old version was already loaded and started before the update call.

So I unloaded the plugin being updated before calling update. This threw an exception "Plugin {} cannot be updated since it is not installed" because the plugin was unloaded. The method org.pf4j.update.UpdateManager.updatePlugin(String, String) is checking pluginManager.getPlugin(id) == null, shouldn't it instead check if the Jar/Zip exists in the pluginRoot directory. It is installed, it's just not loaded.

Right now it seems impossible to update a plugin using the default update manager.

decebals commented 5 years ago

Thanks for reporting the issue! Can you add more details (pf4j version, pf4j-update version, operating system. ...)? If it's not difficult for you, a tiny project that replicates the problem is very welcome and it will speed up the solution/fix.

hazemkmammu commented 5 years ago

org.pf4j:pf4j:2.6.0 org.pf4j:pf4j-update:2.0.0 Windows 10

Path pluginCacheDir = Paths.get( "D:\\plugincache");
PluginManager pluginManager = new DefaultPluginManager( pluginCacheDir);
pluginManager.loadPlugins();
pluginManager.startPlugins();

UpdateManager updateManager = new UpdateManager( pluginManager,
        Paths.get( "D:/plugin_repositories/repositories.json"));
// >> keep system up-to-date <<
boolean systemUpToDate = true;

// check for updates
if (updateManager.hasUpdates())
{
    List<PluginInfo> updates = updateManager.getUpdates();
    LOGGER.debug( "Found {} updates", updates.size());
    for (PluginInfo plugin : updates)
    {
        LOGGER.debug( "Found update for plugin '{}'", plugin.id);
        PluginInfo.PluginRelease lastRelease = plugin
                .getLastRelease( pluginManager.getSystemVersion(), pluginManager.getVersionManager());
        String lastVersion = lastRelease.version;
        String installedVersion = pluginManager.getPlugin( plugin.id).getDescriptor().getVersion();
//                pluginManager.stopPlugin( plugin.id);
        pluginManager.unloadPlugin( plugin.id);
        LOGGER.debug( "Update plugin '{}' from version {} to version {}", plugin.id, installedVersion,
                lastVersion);
        boolean updated = updateManager.updatePlugin( plugin.id, lastVersion);
        if (updated)
        {
            LOGGER.debug( "Updated plugin '{}'", plugin.id);
        }
        else
        {
            LOGGER.error( "Cannot update plugin '{}'", plugin.id);
            systemUpToDate = false;
        }
    }
}
else
{
    LOGGER.debug( "No updates found");
}
hazemkmammu commented 5 years ago

Also, please note the PluginInfo.PluginRelease lastRelease = plugin .getLastRelease( pluginManager.getSystemVersion(), pluginManager.getVersionManager());. The version 2.0.0 does not have a updateManager.getLastPluginRelease(plugin.id) method like in the example in README.md.

decebals commented 5 years ago

@hazemkmammu Can you create a PR (pull request) please? In a PR are very clear (visible) the modifications.

hazemkmammu commented 5 years ago

My earlier proposal (deleted comment) to fix the issue was incorrect. The actual issue is org.pf4j.DefaultPluginRepository.deletePluginPath(Path) throwing "java.nio.file.FileSystemException: XXX.jar: The process cannot access the file because it is being used by another process.". I am not sure why. I will update if I can figure out. Closing this issue. Sorry for the confusion.

hazemkmammu commented 5 years ago

I believe I have isolated the cause of the issue. It seems to be somehow related to org.pf4j.LegacyExtensionFinder.readPluginsStorages(). Please see the following code snippets.

Case# 1 Throws java.nio.file.FileSystemException: D:\plugincache\SpanishGreetingPlugin.jar: The process cannot access the file because it is being used by another process..

try {
            URL jarUrl = new File("D:\\plugincache\\SpanishGreetingPlugin.jar").getCanonicalFile().toURI().toURL();
            URL[] urls = new URL[] { jarUrl };
            URLClassLoader ucl = new URLClassLoader(urls);
            Enumeration<URL> extensionResources = ucl.findResources("META-INF/extensions.idx");
            while (extensionResources.hasMoreElements()) {
                URL extensionResource = extensionResources.nextElement();
                try (Reader reader = new InputStreamReader(extensionResource.openStream(), StandardCharsets.UTF_8)) {
                }
            }
            Class<?> pluginClass = ucl.loadClass("com.fisc.pf4jdemo.SpanishGreetingPlugin$SpanishGreeting");
            GreetingExtensionPoint plugin = (GreetingExtensionPoint) pluginClass.newInstance();
            System.out.println("Greeting:" + plugin.greeting());
            ucl.close();
            FileUtils.delete(Paths.get("D:\\plugincache\\SpanishGreetingPlugin.jar"));
        } catch (ClassNotFoundException | IOException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }

Case# 2 Works fine. No errors.

        try {
            URL jarUrl = new File("D:\\plugincache\\SpanishGreetingPlugin.jar").getCanonicalFile().toURI().toURL();
            URL[] urls = new URL[] { jarUrl };
            URLClassLoader ucl = new URLClassLoader(urls);
            Enumeration<URL> extensionResources = ucl.findResources("META-INF/extensions.idx");
            while (extensionResources.hasMoreElements()) {
                URL extensionResource = extensionResources.nextElement();
//                try (Reader reader = new InputStreamReader(extensionResource.openStream(), StandardCharsets.UTF_8)) {
//                }
            }
            Class<?> pluginClass = ucl.loadClass("com.fisc.pf4jdemo.SpanishGreetingPlugin$SpanishGreeting");
            GreetingExtensionPoint plugin = (GreetingExtensionPoint) pluginClass.newInstance();
            System.out.println("Greeting:" + plugin.greeting());
            ucl.close();
            FileUtils.delete(Paths.get("D:\\plugincache\\SpanishGreetingPlugin.jar"));
        } catch (ClassNotFoundException | IOException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
decebals commented 5 years ago

@hazemkmammu Maybe is something related with https://github.com/pf4j/pf4j/issues/217? The error message looks the same.

I believe I have isolated the cause of the issue.

You help a lot with your analyse. I really like that you try to find where is the problem in code and how can it be solved.

hazemkmammu commented 5 years ago

@decebals Thank you. You created PF4J in a simple and clean way such that it encourages contribution. I am glad I have been of help. Yes, #217 appears to be related.

I observed that using java.net.URLClassLoader.getResourceAsStream(String) instead of java.net.URLClassLoader.findResources(String) fixes this in my test code.

Case# 3 Works without errors.

try {
    URL jarUrl = new File("D:\\plugincache\\SpanishGreetingPlugin.jar").getCanonicalFile().toURI().toURL();
    URL[] urls = new URL[] { jarUrl };
    URLClassLoader ucl = new URLClassLoader(urls);

    Set<String> entries = new HashSet<>();
    try (Reader reader = new InputStreamReader(ucl.getResourceAsStream("META-INF/extensions.idx"))) {
        LegacyExtensionStorage.read(reader, entries);
    }
    Class<?> pluginClass = ucl.loadClass("com.fisc.pf4jdemo.SpanishGreetingPlugin$SpanishGreeting");
    GreetingExtensionPoint plugin = (GreetingExtensionPoint) pluginClass.newInstance();
    System.out.println("Greeting:" + plugin.greeting());
    ucl.close();
    FileUtils.delete(Paths.get("D:\\plugincache\\SpanishGreetingPlugin.jar"));
} catch (ClassNotFoundException | IOException | InstantiationException | IllegalAccessException e) {
    e.printStackTrace();
}

The java.net.URLClassLoader.getResourceAsStream(String) collects the input streams returned (for files and jar files) internally and closes them when java.net.URLClassLoader.close() is called. I am not sure how that makes a difference because Case# 1 also closes the stream.

I will create a PR so that you can understand this better.