Merge pull request #2400 from Owain94/externalscrash

externals: Don't crash the client if updating a plugin fails
This commit is contained in:
Owain van Brakel
2020-03-03 11:17:57 +01:00
committed by GitHub
4 changed files with 336 additions and 4 deletions

View File

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

View File

@@ -77,7 +77,7 @@ class ExternalPluginManager
{
public static ArrayList<ClassLoader> pluginClassLoaders = new ArrayList<>();
private final PluginManager runelitePluginManager;
private final org.pf4j.PluginManager externalPluginManager;
private org.pf4j.PluginManager externalPluginManager;
@Getter(AccessLevel.PUBLIC)
private final List<UpdateRepository> 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<PluginInfo> 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<String> getDependencies()

View File

@@ -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;
}
}

View File

@@ -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.
* <br />
* All you need is to define unique key for each lock type.
*
* @author Vladislav Zablotsky
*/
public class CrossLock
{
private static final HashMap<String, CrossLock> 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();
}
}