/*
* Copyright 2016 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.plugin.infra.monitor;
import com.thoughtworks.go.util.SystemEnvironment;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import static com.thoughtworks.go.util.SystemEnvironment.*;
@Component
public class DefaultPluginJarLocationMonitor implements PluginJarLocationMonitor {
private static final Logger LOGGER = Logger.getLogger(DefaultPluginJarLocationMonitor.class);
private List<WeakReference<PluginJarChangeListener>> pluginJarChangeListener = new CopyOnWriteArrayList<>();
private List<WeakReference<PluginsFolderChangeListener>> pluginsFolderChangeListener = new CopyOnWriteArrayList<>();
private File bundledPluginDirectory;
private final File externalPluginDirectory;
private PluginLocationMonitorThread monitorThread;
private SystemEnvironment systemEnvironment;
@Autowired
public DefaultPluginJarLocationMonitor(SystemEnvironment systemEnvironment) {
this.systemEnvironment = systemEnvironment;
this.bundledPluginDirectory = new File(this.systemEnvironment.get(PLUGIN_GO_PROVIDED_PATH));
this.externalPluginDirectory = new File(this.systemEnvironment.get(PLUGIN_EXTERNAL_PROVIDED_PATH));
}
public void initialize() {
validateBundledPluginDirectory();
validateExternalPluginDirectory();
}
public void addPluginsFolderChangeListener(PluginsFolderChangeListener pluginsFolderChangeListener) {
this.pluginsFolderChangeListener.add(new WeakReference<>(pluginsFolderChangeListener));
removeClearedWeakReferencesForFolder();
}
@Override
public void addPluginJarChangeListener(PluginJarChangeListener listener) {
pluginJarChangeListener.add(new WeakReference<>(listener));
removeClearedWeakReferences();
}
@Override
public void removePluginJarChangeListener(final PluginJarChangeListener listener) {
Object referenceOfListenerToBeRemoved = CollectionUtils.find(pluginJarChangeListener, new Predicate() {
@Override
public boolean evaluate(Object object) {
WeakReference<PluginJarChangeListener> listenerWeakReference = (WeakReference<PluginJarChangeListener>) object;
PluginJarChangeListener registeredListener = listenerWeakReference.get();
return registeredListener != null && registeredListener == listener;
}
});
pluginJarChangeListener.remove(referenceOfListenerToBeRemoved);
removeClearedWeakReferences();
}
@Override
public void start() {
initializeMonitorThread();
monitorThread.start();
}
private void initializeMonitorThread(){
if (monitorThread != null) {
throw new IllegalStateException("Cannot start the monitor multiple times.");
}
monitorThread = new PluginLocationMonitorThread(bundledPluginDirectory, externalPluginDirectory, pluginJarChangeListener, pluginsFolderChangeListener, systemEnvironment);
monitorThread.setDaemon(true);
}
@Override
public void oneShot() {
initializeMonitorThread();
monitorThread.oneShot();
}
@Override
public void stop() {
if (monitorThread == null) {
return;
}
monitorThread.interrupt();
try {
monitorThread.join();
} catch (InterruptedException e) {
}
monitorThread = null;
}
public void validateBundledPluginDirectory() {
if (bundledPluginDirectory.exists()) {
return;
}
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Force creating the plugins jar directory as it does not exist " + bundledPluginDirectory.getAbsolutePath());
}
FileUtils.forceMkdir(bundledPluginDirectory);
} catch (IOException e) {
String message = "Failed to create plugins folder in location " + bundledPluginDirectory.getAbsolutePath();
LOGGER.warn(message, e);
throw new RuntimeException(message, e);
}
}
private void validateExternalPluginDirectory() {
if (externalPluginDirectory.exists()) {
return;
}
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Force creating the plugins jar directory as it does not exist " + externalPluginDirectory.getAbsolutePath());
}
FileUtils.forceMkdir(externalPluginDirectory);
} catch (IOException e) {
String message = "Failed to create external plugins folder in location " + externalPluginDirectory.getAbsolutePath();
LOGGER.warn(message, e);
throw new RuntimeException(message, e);
}
}
private void removeClearedWeakReferences() {
Iterator<WeakReference<PluginJarChangeListener>> iterator = pluginJarChangeListener.iterator();
while (iterator.hasNext()) {
WeakReference<PluginJarChangeListener> next = iterator.next();
if (next.get() == null) {
iterator.remove();
}
}
}
private void removeClearedWeakReferencesForFolder() {
Iterator<WeakReference<PluginsFolderChangeListener>> iterator = pluginsFolderChangeListener.iterator();
while (iterator.hasNext()) {
WeakReference<PluginsFolderChangeListener> next = iterator.next();
if (next.get() == null) {
iterator.remove();
}
}
}
private static class PluginLocationMonitorThread extends Thread {
private Set<PluginFileDetails> knownBundledPluginFileDetails = new HashSet<>();
private Set<PluginFileDetails> knownExternalPluginFileDetails = new HashSet<>();
private File bundledPluginDirectory;
private File externalPluginDirectory;
private List<WeakReference<PluginJarChangeListener>> pluginJarChangeListener;
private List<WeakReference<PluginsFolderChangeListener>> pluginsFolderChangeListener;
private SystemEnvironment systemEnvironment;
public PluginLocationMonitorThread(File bundledPluginDirectory, File externalPluginDirectory, List<WeakReference<PluginJarChangeListener>> pluginJarChangeListener, List<WeakReference<PluginsFolderChangeListener>> pluginsFolderChangeListener, SystemEnvironment systemEnvironment) {
this.bundledPluginDirectory = bundledPluginDirectory;
this.externalPluginDirectory = externalPluginDirectory;
this.pluginJarChangeListener = pluginJarChangeListener;
this.pluginsFolderChangeListener = pluginsFolderChangeListener;
this.systemEnvironment = systemEnvironment;
}
@Override
public void run() {
do {
oneShot();
int interval = systemEnvironment.get(PLUGIN_LOCATION_MONITOR_INTERVAL_IN_SECONDS);
if (interval <= 0) {
break;
}
waitForMonitorInterval(interval);
} while (!Thread.currentThread().isInterrupted());
}
public void oneShot() {
knownBundledPluginFileDetails = loadAndNotifyPluginsFrom(bundledPluginDirectory, knownBundledPluginFileDetails, true);
knownExternalPluginFileDetails = loadAndNotifyPluginsFrom(externalPluginDirectory, knownExternalPluginFileDetails, false);
}
private Set<PluginFileDetails> loadAndNotifyPluginsFrom(File pluginDirectory, Set<PluginFileDetails> knownPluginFiles, boolean isBundledPluginsLocation) {
Set<PluginFileDetails> currentPluginFiles = getDetailsOfCurrentPluginFilesFrom(pluginDirectory, isBundledPluginsLocation);
boolean notifyListenersOfRemovedPlugins = notifyListenersOfRemovedPlugins(currentPluginFiles, knownPluginFiles);
boolean notifyListenersOfUpdatedPlugins = notifyListenersOfUpdatedPlugins(currentPluginFiles, knownPluginFiles);
boolean notifyListenersOfAddedPlugins = notifyListenersOfAddedPlugins(currentPluginFiles, knownPluginFiles);
if (notifyListenersOfRemovedPlugins || notifyListenersOfUpdatedPlugins || notifyListenersOfAddedPlugins) {
doOnPluginsFolderChangeListener().handle();
}
return currentPluginFiles;
}
private boolean notifyListenersOfAddedPlugins(Set<PluginFileDetails> currentPluginFiles, Set<PluginFileDetails> previouslyKnownPluginFiles) {
HashSet<PluginFileDetails> currentPlugins = new HashSet<>(currentPluginFiles);
currentPlugins.removeAll(previouslyKnownPluginFiles);
for (PluginFileDetails newlyAddedPluginFile : currentPlugins) {
doOnAllListeners().pluginJarAdded(newlyAddedPluginFile);
}
return !currentPlugins.isEmpty();
}
private boolean notifyListenersOfRemovedPlugins(Set<PluginFileDetails> currentPluginFiles, Set<PluginFileDetails> previouslyKnownPluginFiles) {
HashSet<PluginFileDetails> previouslyKnownPlugins = new HashSet<>(previouslyKnownPluginFiles);
previouslyKnownPlugins.removeAll(currentPluginFiles);
for (PluginFileDetails removedPluginFile : previouslyKnownPlugins) {
doOnAllListeners().pluginJarRemoved(removedPluginFile);
}
return !previouslyKnownPlugins.isEmpty();
}
private boolean notifyListenersOfUpdatedPlugins(Set<PluginFileDetails> currentPluginFiles, Set<PluginFileDetails> knownPluginFileDetails) {
final ArrayList<PluginFileDetails> updatedPlugins = findUpdatedPlugins(currentPluginFiles, knownPluginFileDetails);
for (PluginFileDetails updatedPlugin : updatedPlugins) {
doOnAllListeners().pluginJarUpdated(updatedPlugin);
}
return !updatedPlugins.isEmpty();
}
private PluginJarChangeListener doOnAllListeners() {
return new DoOnAllListeners(pluginJarChangeListener);
}
private DoOnPluginsFolderChangeListener doOnPluginsFolderChangeListener() {
return new DoOnPluginsFolderChangeListener(pluginsFolderChangeListener);
}
private void waitForMonitorInterval(int interval) {
try {
Thread.sleep(interval * 1000);
} catch (InterruptedException e) {
this.interrupt();
}
}
private Set<PluginFileDetails> getDetailsOfCurrentPluginFilesFrom(File directory, boolean isBundledPluginsLocation) {
Set<PluginFileDetails> currentPluginFileDetails = new HashSet<>();
for (Object fileOfPlugin : FileUtils.listFiles(directory, new String[]{"jar"}, false)) {
currentPluginFileDetails.add(new PluginFileDetails((File) fileOfPlugin, isBundledPluginsLocation));
}
return currentPluginFileDetails;
}
private ArrayList<PluginFileDetails> findUpdatedPlugins(Set<PluginFileDetails> currentPluginFiles, Set<PluginFileDetails> knownPluginFileDetails) {
final ArrayList<PluginFileDetails> currentPlugins = new ArrayList<>(currentPluginFiles);
final ArrayList<PluginFileDetails> knownPlugins = new ArrayList<>(knownPluginFileDetails);
CollectionUtils.filter(knownPlugins, new Predicate() {
@Override
public boolean evaluate(Object object) {
PluginFileDetails knownPlugin = (PluginFileDetails) object;
int i = currentPlugins.indexOf(knownPlugin);
if (i == -1) {
return false;
}
PluginFileDetails plugin = currentPlugins.get(i);
return plugin.doesTimeStampDiffer(knownPlugin);
}
});
return knownPlugins;
}
public static class DoOnAllListeners implements PluginJarChangeListener {
private List<WeakReference<PluginJarChangeListener>> listeners;
public DoOnAllListeners(List<WeakReference<PluginJarChangeListener>> listeners) {
this.listeners = listeners;
}
@Override
public void pluginJarAdded(final PluginFileDetails pluginFileDetails) {
doOnAllPluginJarChangeListener(new Closure() {
public void execute(Object o) {
((PluginJarChangeListener) o).pluginJarAdded(pluginFileDetails);
}
});
}
@Override
public void pluginJarUpdated(final PluginFileDetails pluginFileDetails) {
doOnAllPluginJarChangeListener(new Closure() {
public void execute(Object o) {
((PluginJarChangeListener) o).pluginJarUpdated(pluginFileDetails);
}
});
}
@Override
public void pluginJarRemoved(final PluginFileDetails pluginFileDetails) {
doOnAllPluginJarChangeListener(new Closure() {
public void execute(Object o) {
((PluginJarChangeListener) o).pluginJarRemoved(pluginFileDetails);
}
});
}
private void doOnAllPluginJarChangeListener(Closure closure) {
for (WeakReference<PluginJarChangeListener> listener : listeners) {
PluginJarChangeListener changeListener = listener.get();
if (changeListener == null) {
continue;
}
try {
closure.execute(changeListener);
} catch (Exception e) {
LOGGER.warn("Plugin listener failed", e);
}
}
}
}
public static class DoOnPluginsFolderChangeListener implements PluginsFolderChangeListener {
private List<WeakReference<PluginsFolderChangeListener>> listeners;
public DoOnPluginsFolderChangeListener(List<WeakReference<PluginsFolderChangeListener>> listeners) {
this.listeners = listeners;
}
@Override
public void handle() {
for (WeakReference<PluginsFolderChangeListener> listener : listeners) {
PluginsFolderChangeListener changeListener = listener.get();
if (changeListener == null) {
continue;
}
try {
new Closure() {
public void execute(Object o) {
((PluginsFolderChangeListener) o).handle();
}
}.execute(changeListener);
} catch (Exception e) {
LOGGER.warn("Plugin listener failed", e);
}
}
}
}
}
}