diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index e6292b4087..d2bf8e14df 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -85,6 +85,7 @@ import net.runelite.client.ui.overlay.arrow.ArrowWorldOverlay; import net.runelite.client.ui.overlay.infobox.InfoBoxOverlay; import net.runelite.client.ui.overlay.tooltip.TooltipOverlay; import net.runelite.client.ui.overlay.worldmap.WorldMapOverlay; +import net.runelite.client.util.AppLock; import net.runelite.client.util.WorldUtil; import net.runelite.client.ws.PartyService; import net.runelite.http.api.worlds.World; @@ -201,6 +202,9 @@ public class RuneLite @Inject private Scheduler scheduler; + @Inject + private AppLock appLock; + public static void main(String[] args) throws Exception { Locale.setDefault(Locale.ENGLISH); @@ -368,9 +372,11 @@ public class RuneLite externalPluginManager.startExternalUpdateManager(); externalPluginManager.startExternalPluginManager(); - - RuneLiteSplashScreen.stage(.59, "Updating external plugins"); - externalPluginManager.update(); + if (appLock.lock(this.getClass().getName())) + { + RuneLiteSplashScreen.stage(.59, "Updating external plugins"); + externalPluginManager.update(); + } // Load the plugins, but does not start them yet. // This will initialize configuration @@ -490,5 +496,6 @@ public class RuneLite configManager.sendConfig(); clientSessionManager.shutdown(); discordService.close(); + appLock.release(); } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java index eec6d2bb2e..e9c3afad37 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginManager.java @@ -77,7 +77,7 @@ class ExternalPluginManager { public static ArrayList pluginClassLoaders = new ArrayList<>(); private final PluginManager runelitePluginManager; - private final org.pf4j.PluginManager externalPluginManager; + private org.pf4j.PluginManager externalPluginManager; @Getter(AccessLevel.PUBLIC) private final List repositories = new ArrayList<>(); private final OpenOSRSConfig openOSRSConfig; @@ -104,6 +104,11 @@ class ExternalPluginManager //noinspection ResultOfMethodCallIgnored EXTERNALPLUGIN_DIR.mkdirs(); + initPluginManager(); + } + + private void initPluginManager() + { boolean debug = RuneLiteProperties.getLauncherVersion() == null && RuneLiteProperties.getPluginPath() != null; this.externalPluginManager = new DefaultPluginManager(debug ? Paths.get(RuneLiteProperties.getPluginPath() + File.separator + "release") : EXTERNALPLUGIN_DIR.toPath()) @@ -241,6 +246,8 @@ class ExternalPluginManager { try { + repositories.clear(); + for (String keyval : openOSRSConfig.getExternalRepositories().split(";")) { String id = keyval.substring(0, keyval.lastIndexOf(":https")); @@ -474,6 +481,21 @@ class ExternalPluginManager for (PluginWrapper plugin : startedPlugins) { + boolean depsLoaded = true; + for (PluginDependency dependency : plugin.getDescriptor().getDependencies()) + { + if (startedPlugins.stream().noneMatch(pl -> pl.getPluginId().equals(dependency.getPluginId()))) + { + depsLoaded = false; + } + } + + if (!depsLoaded) + { + // This should never happen but can crash the client + continue; + } + scannedPlugins.addAll(loadPlugin(plugin.getPluginId())); } @@ -673,6 +695,7 @@ class ExternalPluginManager public void update() { + boolean error = false; if (updateManager.hasUpdates()) { List updates = updateManager.getUpdates(); @@ -687,14 +710,24 @@ class ExternalPluginManager if (!updated) { log.warn("Cannot update plugin '{}'", plugin.id); + error = true; } } catch (PluginRuntimeException ex) { log.warn("Cannot update plugin '{}', the user probably has another client open", plugin.id); + error = true; + break; } } } + + if (error) + { + initPluginManager(); + startExternalUpdateManager(); + startExternalPluginManager(); + } } public Set getDependencies() diff --git a/runelite-client/src/main/java/net/runelite/client/util/AppLock.java b/runelite-client/src/main/java/net/runelite/client/util/AppLock.java new file mode 100644 index 0000000000..aca0832439 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/util/AppLock.java @@ -0,0 +1,64 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 by rumatoest at github.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.runelite.client.util; + +/** + * The Class AppLock. + * + * @author Vladislav Zablotsky + */ +public class AppLock +{ + + private static CrossLock lockInstance; + + /** + * Set lock for application instance. + * Method must be run only one time at application start. + * + * @param lockId Unique lock identifiers + * @return true if succeeded + */ + public synchronized boolean lock(String lockId) + { + if (lockInstance == null) + { + lockInstance = new CrossLock("application_" + lockId); + } + return lockInstance.lock(); + } + + /** + * Trying to release application lock. + * Thus another application instances will be able to use lock with current ID. + */ + public synchronized void release() + { + if (lockInstance != null) + { + lockInstance.clear(); + } + lockInstance = null; + } +} \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/util/CrossLock.java b/runelite-client/src/main/java/net/runelite/client/util/CrossLock.java new file mode 100644 index 0000000000..5db7a63432 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/util/CrossLock.java @@ -0,0 +1,228 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2018 by rumatoest at github.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.runelite.client.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import static net.runelite.client.RuneLite.RUNELITE_DIR; + +/** + * Universal cross application instances locker. + * Allow you to create simple lock like object which can be used for + * different application instances. Basic idea is simple - just a simple file lock. + *
+ * All you need is to define unique key for each lock type. + * + * @author Vladislav Zablotsky + */ +public class CrossLock +{ + + private static final HashMap locks = new HashMap<>(); + + private final String id; + + private final File fileToLock; + + private FileOutputStream fileStream; + + private FileChannel fileStreamChannel; + + private FileLock lockOnFile; + + /** + * Will create or retrieve lock instance. + * Each lock id is unique among all you instances, + * thus only one instance can acquire lock for this id. + * + * @param lockId Unique lock identifier + * @return Not null + */ + public static CrossLock get(String lockId) + { + if (locks.containsKey(lockId)) + { + return locks.get(lockId); + } + else + { + synchronized (CrossLock.class) + { + if (locks.containsKey(lockId)) + { + return locks.get(lockId); + } + else + { + CrossLock cl = new CrossLock(lockId); + locks.put(lockId, cl); + return cl; + } + } + } + } + + /** + * Will remove lock object for specific id and release lock if any. + * + * @param lockId Unique lock identifier + */ + public static void remove(String lockId) + { + if (locks.containsKey(lockId)) + { + CrossLock lock = null; + synchronized (CrossLock.class) + { + if (locks.containsKey(lockId)) + { + lock = locks.remove(lockId); + } + } + if (lock != null) + { + lock.release(); + } + } + } + + CrossLock(String lockId) + { + this.id = lockId; + fileToLock = new File(RUNELITE_DIR, lockId + ".app_lock"); + } + + /** + * Return lock instance identifier. + */ + public String id() + { + return this.id; + } + + /** + * Activate lock. + * Note! This is only cross application (cross instances) lock. It will not work + * as lock inside single application instance. + * + * @return true if lock was acquire or false + */ + public synchronized boolean lock() + { + if (lockOnFile != null && lockOnFile.isValid()) + { + return true; + } + else + { + release(); + } + + String lockContent = "#Java AppLock Object\n#Locked by key: " + id() + "\r\n"; + try + { + if (fileToLock.exists()) + { + fileToLock.createNewFile(); + } + fileStream = new FileOutputStream(fileToLock); + fileStreamChannel = fileStream.getChannel(); + lockOnFile = fileStreamChannel.tryLock(); + if (lockOnFile != null) + { + fileStream.write(lockContent.getBytes()); + } + } + catch (Exception ex) + { + if (!(ex instanceof OverlappingFileLockException)) + { + Logger.getLogger(AppLock.class.getName()).log(Level.WARNING, + "Can not get application lock for id=" + id() + "\n" + ex.getMessage(), ex); + } + return false; + } + + return lockOnFile != null; + } + + /** + * Release lock associated with this object. + */ + public synchronized void release() + { + try + { + if (lockOnFile != null && lockOnFile.isValid()) + { + lockOnFile.release(); + } + lockOnFile = null; + + if (fileStream != null) + { + fileStream.close(); + fileStream = null; + } + + if (fileStreamChannel != null && fileStreamChannel.isOpen()) + { + fileStreamChannel.close(); + } + fileStreamChannel = null; + } + catch (IOException ex) + { + Logger.getLogger(AppLock.class.getName()).log(Level.WARNING, + "Can not get application lock for id=" + id() + "\n" + ex.getMessage(), ex); + } + } + + /** + * Release lock and remove lock file. + */ + public synchronized void clear() + { + release(); + if (fileToLock.exists()) + { + fileToLock.delete(); + } + } + + @Override + protected void finalize() throws Throwable + { + this.clear(); + super.finalize(); + } + +} \ No newline at end of file