pf4j-update icon indicating copy to clipboard operation
pf4j-update copied to clipboard

Error updating plugin because file already in use

Open hazemkmammu opened this issue 5 years ago • 8 comments

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.

hazemkmammu avatar May 23 '19 12:05 hazemkmammu

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.

decebals avatar May 23 '19 12:05 decebals

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 avatar May 23 '19 12:05 hazemkmammu

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.

hazemkmammu avatar May 23 '19 12:05 hazemkmammu

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

decebals avatar May 24 '19 20:05 decebals

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 avatar May 27 '19 06:05 hazemkmammu

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();
        }

hazemkmammu avatar May 28 '19 09:05 hazemkmammu

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

decebals avatar May 28 '19 09:05 decebals

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

hazemkmammu avatar May 28 '19 12:05 hazemkmammu