Added ability for ScriptEngines to allow script dependencies to be tracked and changes to trigger script reloading (#1884)

Signed-off-by: Jonathan Gilbert <jpg@trillica.com>
pull/2161/head
Jonathan Gilbert 2021-01-27 08:23:31 +11:00 committed by GitHub
parent b55933d769
commit f0c9a8434d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 314 additions and 14 deletions

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.module.script.rulesupport.internal.loader;
import java.util.HashSet;
import java.util.Set;
import org.openhab.core.automation.module.script.rulesupport.internal.loader.collection.BidiSetBag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Tracks dependencies between scripts and reloads dependees
*
* @author Jonathan Gilbert
*/
public abstract class DependencyTracker {
private final Logger logger = LoggerFactory.getLogger(DependencyTracker.class);
private final BidiSetBag<String, String> scriptToLibs = new BidiSetBag<>();
private final ScriptLibraryWatcher scriptLibraryWatcher = new ScriptLibraryWatcher() {
@Override
void updateFile(String libraryPath) {
Set<String> scripts;
synchronized (scriptToLibs) {
scripts = new HashSet<>(scriptToLibs.getKeys(libraryPath)); // take a copy as it will change as we
// reimport
}
DependencyTracker.this.logger.debug("Library {} changed; reimporting {} scripts...", libraryPath,
scripts.size());
for (String scriptUrl : scripts) {
reimportScript(scriptUrl);
}
}
};
public void activate() {
scriptLibraryWatcher.activate();
}
public void deactivate() {
scriptLibraryWatcher.deactivate();
}
public abstract void reimportScript(String scriptPath);
public void addLibForScript(String scriptPath, String libPath) {
synchronized (scriptToLibs) {
scriptToLibs.put(scriptPath, libPath);
}
}
public void removeScript(String scriptPath) {
synchronized (scriptToLibs) {
scriptToLibs.removeKey(scriptPath);
}
}
}

View File

@ -75,6 +75,7 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
private final ScriptEngineManager manager;
private final ReadyService readyService;
private @Nullable DependencyTracker dependencyTracker;
private @Nullable ScheduledExecutorService scheduler;
private final Map<String, Set<URL>> urlsByScriptExtension = new ConcurrentHashMap<>();
@ -93,6 +94,18 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
super.activate();
readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE)
.withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL)));
dependencyTracker = new DependencyTracker() {
@Override
public void reimportScript(String scriptPath) {
logger.debug("Reimporting {}...", scriptPath);
try {
importFile(new URL(scriptPath));
} catch (MalformedURLException e) {
logger.warn("Failed to reimport {} as it cannot be parsed as a URL", scriptPath);
}
}
};
dependencyTracker.activate();
}
@Deactivate
@ -104,6 +117,12 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
localScheduler.shutdownNow();
scheduler = null;
}
if (dependencyTracker != null) {
dependencyTracker.deactivate();
dependencyTracker = null;
}
super.deactivate();
}
@ -164,7 +183,9 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
private void removeFile(URL url) {
dequeueUrl(url);
manager.removeEngine(getScriptIdentifier(url));
String scriptIdentifier = getScriptIdentifier(url);
dependencyTracker.removeScript(scriptIdentifier);
manager.removeEngine(scriptIdentifier);
loaded.remove(url);
}
@ -184,11 +205,12 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac
StandardCharsets.UTF_8)) {
logger.info("Loading script '{}'", fileName);
ScriptEngineContainer container = manager.createScriptEngine(scriptType,
getScriptIdentifier(url));
String scriptIdentifier = getScriptIdentifier(url);
ScriptEngineContainer container = manager.createScriptEngine(scriptType, scriptIdentifier);
if (container != null) {
manager.loadScript(container.getIdentifier(), reader);
manager.loadScript(container.getIdentifier(), reader,
dependency -> dependencyTracker.addLibForScript(scriptIdentifier, dependency));
loaded.add(url);
logger.debug("Script loaded: {}", fileName);
} else {

View File

@ -0,0 +1,66 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.module.script.rulesupport.internal.loader;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import org.openhab.core.OpenHAB;
import org.openhab.core.service.AbstractWatchService;
/**
* Listens for changes to script libraries
*
* @author Jonathan Gilbert
*/
abstract class ScriptLibraryWatcher extends AbstractWatchService {
public static final String LIB_PATH = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib",
"javascript");
ScriptLibraryWatcher() {
super(LIB_PATH);
}
@Override
protected boolean watchSubDirectories() {
return true;
}
@Override
protected WatchEvent.Kind<?>[] getWatchEventKinds(Path path) {
return new WatchEvent.Kind<?>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
}
@Override
protected void processWatchEvent(WatchEvent<?> watchEvent, WatchEvent.Kind<?> kind, Path path) {
File file = path.toFile();
if (!file.isHidden()) {
if (kind.equals(ENTRY_DELETE)) {
this.updateFile(file.getPath());
}
if (file.canRead() && (kind.equals(ENTRY_CREATE) || kind.equals(ENTRY_MODIFY))) {
this.updateFile(file.getPath());
}
}
}
abstract void updateFile(String filePath);
}

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.module.script.rulesupport.internal.loader.collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Bidirectional bag of unique elements. A map allowing multiple, unique values to be stored against a single key.
* Provides optimized lookup of values for a key, as well as keys referencing a value.
*
* @author Jonathan Gilbert - Initial contribution
* @param <K> Type of Key
* @param <V> Type of Value
*/
public class BidiSetBag<K, V> {
private Map<K, Set<V>> keyToValues = new HashMap<>();
private Map<V, Set<K>> valueToKeys = new HashMap<>();
public void put(K key, V value) {
addElement(keyToValues, key, value);
addElement(valueToKeys, value, key);
}
public Set<V> getValues(K key) {
Set<V> existing = keyToValues.get(key);
return existing == null ? Collections.emptySet() : Collections.unmodifiableSet(existing);
}
public Set<K> getKeys(V value) {
Set<K> existing = valueToKeys.get(value);
return existing == null ? Collections.emptySet() : Collections.unmodifiableSet(existing);
}
public Set<V> removeKey(K key) {
Set<V> values = keyToValues.remove(key);
if (values != null) {
for (V value : values) {
valueToKeys.computeIfPresent(value, (k, v) -> {
v.remove(key);
return v;
});
}
return values;
} else {
return Collections.emptySet();
}
}
public Set<K> removeValue(V value) {
Set<K> keys = valueToKeys.remove(value);
if (keys != null) {
for (K key : keys) {
keyToValues.computeIfPresent(key, (k, v) -> {
v.remove(value);
return v;
});
}
return keys;
} else {
return Collections.emptySet();
}
}
private static <T, U> void addElement(Map<T, Set<U>> map, T key, U value) {
Set<U> elements = map.compute(key, (k, l) -> l == null ? new HashSet<>() : l);
elements.add(value);
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.module.script;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Interface that allows listener to be notified of script dependencies (libraries)
*
* @author Jonathan Gilbert - Initial contribution
*/
@NonNullByDefault
@FunctionalInterface
public interface ScriptDependencyListener extends Consumer<String> {
void accept(String dependency);
}

View File

@ -45,6 +45,11 @@ public interface ScriptEngineFactory {
*/
String CONTEXT_KEY_EXTENSION_ACCESSOR = "oh.extension-accessor";
/**
* Key to access Dependency Listener {@link ScriptDependencyListener}
*/
String CONTEXT_KEY_DEPENDENCY_LISTENER = "oh.dependency-listener";
/**
* This method returns a list of file extensions and MimeTypes that are supported by the ScriptEngine, e.g. py,
* application/python, js, application/javascript, etc.

View File

@ -45,6 +45,16 @@ public interface ScriptEngineManager {
*/
void loadScript(String engineIdentifier, InputStreamReader scriptData);
/**
* Loads a script and initializes its scope variables
*
* @param engineIdentifier the unique identifier for the ScriptEngine (script file path or UUID)
* @param scriptData the content of the script
* @param scriptDependencyListener listener to be notified of script dependencies
*/
void loadScript(String engineIdentifier, InputStreamReader scriptData,
ScriptDependencyListener scriptDependencyListener);
/**
* Unloads the ScriptEngine loaded with the engineIdentifier
*

View File

@ -12,6 +12,7 @@
*/
package org.openhab.core.automation.module.script.internal;
import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_DEPENDENCY_LISTENER;
import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_ENGINE_IDENTIFIER;
import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_EXTENSION_ACCESSOR;
@ -28,6 +29,7 @@ import javax.script.SimpleScriptContext;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.ScriptDependencyListener;
import org.openhab.core.automation.module.script.ScriptEngineContainer;
import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.openhab.core.automation.module.script.ScriptEngineManager;
@ -136,17 +138,9 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
logger.debug("Added ScriptEngine for language '{}' with identifier: {}", scriptType,
engineIdentifier);
ScriptContext scriptContext = engine.getContext();
addAttributeToScriptContext(engine, CONTEXT_KEY_ENGINE_IDENTIFIER, engineIdentifier);
addAttributeToScriptContext(engine, CONTEXT_KEY_EXTENSION_ACCESSOR, scriptExtensionManager);
if (scriptContext == null) {
scriptContext = new SimpleScriptContext();
engine.setContext(scriptContext);
}
scriptContext.setAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER, engineIdentifier,
ScriptContext.ENGINE_SCOPE);
scriptContext.setAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR, scriptExtensionManager,
ScriptContext.ENGINE_SCOPE);
} else {
logger.error("ScriptEngine for language '{}' could not be created for identifier: {}", scriptType,
engineIdentifier);
@ -161,11 +155,22 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
@Override
public void loadScript(String engineIdentifier, InputStreamReader scriptData) {
loadScript(engineIdentifier, scriptData, null);
}
@Override
public void loadScript(String engineIdentifier, InputStreamReader scriptData,
@Nullable ScriptDependencyListener dependencyListener) {
ScriptEngineContainer container = loadedScriptEngineInstances.get(engineIdentifier);
if (container == null) {
logger.error("Could not load script, as no ScriptEngine has been created");
} else {
ScriptEngine engine = container.getScriptEngine();
if (dependencyListener != null) {
addAttributeToScriptContext(engine, CONTEXT_KEY_DEPENDENCY_LISTENER, dependencyListener);
}
try {
engine.eval(scriptData);
if (engine instanceof Invocable) {
@ -235,4 +240,15 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
public boolean isSupported(String scriptType) {
return findEngineFactory(scriptType) != null;
}
private void addAttributeToScriptContext(ScriptEngine engine, String name, Object value) {
ScriptContext scriptContext = engine.getContext();
if (scriptContext == null) {
scriptContext = new SimpleScriptContext();
engine.setContext(scriptContext);
}
scriptContext.setAttribute(name, value, ScriptContext.ENGINE_SCOPE);
}
}