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
parent
b55933d769
commit
f0c9a8434d
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue