improved feature installer logic (#238)

- upgraded to Java 8
- reduced calls to listInstalledFeatures, which is very expensive
- properly do the calculation of current/target/diff list of addons, when the config changes
- don't log errors when uninstallation fails as this can happen if it is a dependent feature that must not be uninstalled

Signed-off-by: Kai Kreuzer <kai@openhab.org>
pull/239/head
Kai Kreuzer 2017-11-28 09:30:58 +01:00 committed by GitHub
parent 0c7e762c03
commit 3f1af862bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 125 additions and 103 deletions

View File

@ -12,6 +12,6 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@ -1,8 +1,2 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.7

View File

@ -25,8 +25,8 @@ import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.karaf.features.Feature;
import org.apache.karaf.features.FeaturesService;
@ -109,47 +109,58 @@ public class FeatureInstaller implements ConfigurationListener {
}
protected void modified(final Map<String, Object> config) {
if (config.get(CFG_REMOTE) == null && getOnlineStatus() == true) {
// we seem to have an online distro and no configuration set
updateOnlineConfigFlag(true);
return;
} else {
boolean online = config.get(CFG_REMOTE) != null && "true".equals(config.get(CFG_REMOTE).toString());
if (getOnlineStatus() != online) {
setOnlineStatus(online);
}
boolean changed = false;
boolean online = (config.get(CFG_REMOTE) == null && getOnlineStatus())
|| (config.get(CFG_REMOTE) != null && "true".equals(config.get(CFG_REMOTE).toString()));
if (getOnlineStatus() != online) {
setOnlineStatus(online);
}
final boolean configChanged = changed;
if (configChanged) {
// let's set the flag immediately, so that we do not miss the event
paxCfgUpdated = false;
}
final FeaturesService service = featuresService;
ExecutorService scheduler = Executors.newSingleThreadExecutor();
scheduler.execute(new Runnable() {
@Override
public void run() {
int counter = 0;
// wait up to 5 seconds for the config update event
while (!paxCfgUpdated && counter++ < 50) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
setLegacyExtensions(service, config);
installPackage(service, config);
installAddons(service, config);
scheduler.execute(() -> {
if (configChanged) {
waitForConfigUpdateEvent();
}
setLegacyExtensions(service, config);
if (installPackage(service, config)) {
// our package selection has changed, so let's wait for the values to be available in config admin
// which we will receive as another call to modified
return;
}
installAddons(service, config);
});
}
private void waitForConfigUpdateEvent() {
int counter = 0;
// wait up to 5 seconds for the config update event
while (!paxCfgUpdated && counter++ < 50) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
}
public boolean addAddon(String type, String id) {
try {
Configuration cfg = configurationAdmin.getConfiguration(OpenHAB.ADDONS_SERVICE_PID, null);
Dictionary<String, Object> props = cfg.getProperties();
Object typeProp = props.get(type);
String[] addonIds = typeProp != null ? typeProp.toString().split(",") : new String[0];
if (!ArrayUtils.contains(addonIds, id)) {
ArrayList<String> newAddonIds = new ArrayList<>(addonIds.length + 1);
newAddonIds.addAll(Arrays.asList(addonIds));
List<String> trimmedAddonIds = Arrays.stream(addonIds).map(addonId -> addonId.trim())
.collect(Collectors.toList());
if (!trimmedAddonIds.contains(id)) {
List<String> newAddonIds = new ArrayList<>(trimmedAddonIds.size() + 1);
newAddonIds.addAll(trimmedAddonIds);
newAddonIds.add(id);
props.put(type, StringUtils.join(newAddonIds, ','));
cfg.update(props);
@ -170,8 +181,10 @@ public class FeatureInstaller implements ConfigurationListener {
Dictionary<String, Object> props = cfg.getProperties();
Object typeProp = props.get(type);
String[] addonIds = typeProp != null ? typeProp.toString().split(",") : new String[0];
if (ArrayUtils.contains(addonIds, id)) {
ArrayList<String> newAddonIds = new ArrayList<>(Arrays.asList(addonIds));
List<String> trimmedAddonIds = Arrays.stream(addonIds).map(addonId -> addonId.trim())
.collect(Collectors.toList());
if (trimmedAddonIds.contains(id)) {
List<String> newAddonIds = new ArrayList<>(trimmedAddonIds);
boolean success = newAddonIds.remove(id);
props.put(type, StringUtils.join(newAddonIds, ','));
cfg.update(props);
@ -267,10 +280,10 @@ public class FeatureInstaller implements ConfigurationListener {
}
}
return false;
}
private void setOnlineStatus(boolean status) {
private boolean setOnlineStatus(boolean status) {
boolean changed = false;
if (online_repo_url != null) {
try {
Configuration paxCfg = configurationAdmin.getConfiguration(PAX_URL_PID, null);
@ -288,84 +301,74 @@ public class FeatureInstaller implements ConfigurationListener {
if (status) {
if (!repoCfg.contains(online_repo_url)) {
repoCfg.add(online_repo_url);
changed = true;
}
} else {
if (repoCfg.contains(online_repo_url)) {
repoCfg.remove(online_repo_url);
changed = true;
}
}
properties.put(PROPERTY_MVN_REPOS, StringUtils.join(repoCfg.toArray(), ","));
paxCfgUpdated = false;
paxCfg.update(properties);
if (changed) {
properties.put(PROPERTY_MVN_REPOS, StringUtils.join(repoCfg.toArray(), ","));
paxCfg.update(properties);
}
} catch (IOException e) {
logger.error("Failed setting the add-on management online/offline mode: {}", e.getMessage());
}
}
}
private void updateOnlineConfigFlag(final Boolean online) {
// let's do this asynchronous to not block the current config dispatching
ExecutorService scheduler = Executors.newSingleThreadExecutor();
final ConfigurationAdmin service = configurationAdmin;
scheduler.execute(new Runnable() {
@Override
public void run() {
try {
Configuration cfg = service.getConfiguration(OpenHAB.ADDONS_SERVICE_PID);
Dictionary<String, Object> properties = cfg.getProperties();
if (properties == null) {
properties = new Hashtable<>();
}
if (properties.get(CFG_REMOTE) == null
|| !online.toString().equals(properties.get(CFG_REMOTE).toString())) {
// configuration is out of sync, so let's update it
properties.put(CFG_REMOTE, online);
cfg.update(properties);
}
} catch (IOException e) {
logger.error("Failed updating the remote configuration: {}", e.getMessage());
}
}
});
return changed;
}
private synchronized void installAddons(final FeaturesService service, final Map<String, Object> config) {
Set<String> installAddons = new HashSet<>();
Set<String> installedAddons = new HashSet<>();
Set<String> uninstallAddons = new HashSet<>();
final Set<String> currentAddons = new HashSet<>(); // the currently installed ones
final Set<String> targetAddons = new HashSet<>(); // the target we want to have installed afterwards
final Set<String> installAddons = new HashSet<>(); // the ones to be installed (the diff)
for (String type : addonTypes) {
Object install = config.get(type);
if (install instanceof String) {
String[] entries = ((String) install).split(",");
for (String addon : entries) {
if (!StringUtils.isEmpty(addon)) {
String id = PREFIX + type + "-" + addon.trim();
installedAddons.add(id);
if (!isInstalled(service, id)) {
installAddons.add(id);
try {
Feature[] features = featuresService.listInstalledFeatures();
for (String addon : entries) {
if (!StringUtils.isEmpty(addon)) {
String id = PREFIX + type + "-" + addon.trim();
targetAddons.add(id);
if (!isInstalled(features, id)) {
installAddons.add(id);
}
}
}
}
// we collect all possible addons first
for (String addon : getAllAddonsOfType(type)) {
if (!StringUtils.isEmpty(addon)) {
uninstallAddons.add(PREFIX + type + "-" + addon.trim());
// we collect all installed addons
for (String addon : getAllAddonsOfType(type)) {
if (!StringUtils.isEmpty(addon)) {
String id = PREFIX + type + "-" + addon.trim();
if (isInstalled(features, id)) {
currentAddons.add(id);
}
}
}
} catch (Exception e) {
logger.error("Failed retrieving features: {}", e.getMessage());
}
}
}
// now remove everything from the list that we want to keep installed
for (String addon : installedAddons) {
uninstallAddons.remove(addon);
}
// now calculate what we have to uninstall: all current ones that are not part of the target anymore
Set<String> uninstallAddons = currentAddons.stream().filter(addon -> !targetAddons.contains(addon))
.collect(Collectors.toSet());
// do the installation
if (!installAddons.isEmpty()) {
installFeatures(service, installAddons);
}
// do the de-installation
if (!uninstallAddons.isEmpty()) {
for (String addon : uninstallAddons) {
uninstallFeature(service, addon);
}
uninstallFeatures(service, uninstallAddons);
}
}
@ -389,65 +392,90 @@ public class FeatureInstaller implements ConfigurationListener {
featuresService.installFeatures(addons,
EnumSet.of(FeaturesService.Option.Upgrade, FeaturesService.Option.NoFailOnFeatureNotFound));
logger.debug("Installed '{}'", StringUtils.join(addons, ", "));
for (String addon : addons) {
if (isInstalled(featuresService, addon)) {
postInstalledEvent(addon);
try {
Feature[] features = featuresService.listInstalledFeatures();
for (String addon : addons) {
if (isInstalled(features, addon)) {
postInstalledEvent(addon);
}
}
} catch (Exception e) {
logger.error("Failed retrieving features: {}", e.getMessage());
}
} catch (Exception e) {
logger.error("Failed installing '{}': {}", StringUtils.join(addons, ", "), e.getMessage());
}
}
private static synchronized void installFeature(FeaturesService featuresService, String name) {
private synchronized void uninstallFeatures(FeaturesService featuresService, Set<String> addons) {
for (String addon : addons) {
uninstallFeature(featuresService, addon);
}
}
private static synchronized boolean installFeature(FeaturesService featuresService, String name) {
try {
if (!isInstalled(featuresService, name)) {
Feature[] features = featuresService.listInstalledFeatures();
if (!isInstalled(features, name)) {
featuresService.installFeature(name);
if (isInstalled(featuresService, name)) {
features = featuresService.listInstalledFeatures();
if (isInstalled(features, name)) {
logger.debug("Installed '{}'", name);
postInstalledEvent(name);
return true;
}
}
} catch (Exception e) {
logger.error("Failed installing '{}': {}", name, e.getMessage());
}
return false;
}
private static synchronized void uninstallFeature(FeaturesService featuresService, String name) {
private static synchronized boolean uninstallFeature(FeaturesService featuresService, String name) {
try {
if (isInstalled(featuresService, name)) {
Feature[] features = featuresService.listInstalledFeatures();
if (isInstalled(features, name)) {
featuresService.uninstallFeature(name);
logger.info("Uninstalled '{}'", name);
postUninstalledEvent(name);
return true;
}
} catch (Exception e) {
logger.error("Failed uninstalling '{}': {}", name, e.getMessage());
logger.debug("Failed uninstalling '{}': {}", name, e.getMessage());
}
return false;
}
private static synchronized void installPackage(FeaturesService featuresService, final Map<String, Object> config) {
private static synchronized boolean installPackage(FeaturesService featuresService,
final Map<String, Object> config) {
boolean configChanged = false;
Object packageName = config.get(OpenHAB.CFG_PACKAGE);
if (packageName instanceof String) {
currentPackage = (String) packageName;
String name = PREFIX + PREFIX_PACKAGE + ((String) packageName).trim();
installFeature(featuresService, name);
if (installFeature(featuresService, name)) {
configChanged = true;
}
// uninstall all other packages
try {
for (Feature feature : featuresService.listFeatures()) {
if (feature.getName().startsWith(PREFIX + PREFIX_PACKAGE) && !feature.getName().equals(name)) {
uninstallFeature(featuresService, feature.getName());
if (uninstallFeature(featuresService, feature.getName()) && !configChanged) {
configChanged = true;
}
}
}
} catch (Exception e) {
logger.error("Failed retrieving features: {}", e.getMessage());
}
}
return configChanged;
}
private static boolean isInstalled(FeaturesService featuresService, String name) {
private static boolean isInstalled(Feature[] features, String name) {
try {
for (Feature feature : featuresService.listInstalledFeatures()) {
for (Feature feature : features) {
if (feature.getName().equals(name)) {
return true;
}