[jsscripting] Extend synchronization to common ScriptEngine methods (#13924)
* [jsscripting] Extend synchronization to common ScriptEngine methods This extends the multi-thread synchronization to "eval" and "invokeMethod" and moves synchronization for "invokeFunction" to the DelegatingScriptEngineWithInvocableAndAutocloseableAndSynchronization class. Fixes the multi-thread access requested warnings described in the community (https://community.openhab.org/t/openhab-3-4-milestone-discussion/138093/130) and related to https://github.com/openhab/openhab-core/pull/3180. * Revert "[jsscripting] Extend synchronization to common ScriptEngine methods" This reverts commit aadd21e45879c10aad29bf279ddbb0afd789b0aa. * [jsscripting] Extend synchronization to common ScriptEngine methods & Switch to ReentrantLock This extends the multi-thread synchronization to "eval" and "invokeMethod" and moves synchronization for "invokeFunction" to the InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable class. The synchronization mechanism changed from using synchronized to using a ReentrantLock together with catch_finally to avoid having deadlocks when an exception is thrown. Fixes the multi-thread access requested warnings described in the community (https://community.openhab.org/t/openhab-3-4-milestone-discussion/138093/130) and related to https://github.com/openhab/openhab-core/pull/3180. * [jsscripting] Reduce compiler warnings * [jsscripting] Replace finally blocks & Wrap returns in afterInvocation * [jsscripting] Fix deadlock caused by NoSuchMethodException in Invocable interface methods During testing my latest changes, I noticed that there is a deadlock when invokeFunction or invokeMethod are called on a non-existing method. This happens because the NoSuchMethodException keeps afterInvocation from running and therefore the lock never gets released. * [jsscripting] Also rethrow NPE & Fix PMD warnings/errors * [jsscripting] Wrap and rethrow other exceptions instead of returning them * [jsscripting] Address review comment from @jpg0 Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
This commit is contained in:
parent
1ca9baf157
commit
d4ec220925
@ -101,6 +101,13 @@
|
|||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.openhab.tools.sat</groupId>
|
||||||
|
<artifactId>sat-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<pmdFilter>${project.basedir}/suppressions.properties</pmdFilter>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@ package org.openhab.automation.jsscripting.internal;
|
|||||||
|
|
||||||
import javax.script.Invocable;
|
import javax.script.Invocable;
|
||||||
import javax.script.ScriptEngine;
|
import javax.script.ScriptEngine;
|
||||||
import javax.script.ScriptException;
|
|
||||||
|
|
||||||
import org.graalvm.polyglot.PolyglotException;
|
import org.graalvm.polyglot.PolyglotException;
|
||||||
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable;
|
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable;
|
||||||
@ -38,11 +37,11 @@ class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable & AutoClosea
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ScriptException afterThrowsInvocation(ScriptException se) {
|
public Exception afterThrowsInvocation(Exception e) {
|
||||||
Throwable cause = se.getCause();
|
Throwable cause = e.getCause();
|
||||||
if (cause instanceof PolyglotException) {
|
if (cause instanceof PolyglotException) {
|
||||||
STACK_LOGGER.error("Failed to execute script:", cause);
|
STACK_LOGGER.error("Failed to execute script:", cause);
|
||||||
}
|
}
|
||||||
return se;
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import java.util.Map;
|
|||||||
|
|
||||||
import javax.script.ScriptEngine;
|
import javax.script.ScriptEngine;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
|
import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
|
||||||
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
|
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
|
||||||
@ -37,6 +38,7 @@ import org.osgi.service.component.annotations.Reference;
|
|||||||
@Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.jsscripting", property = Constants.SERVICE_PID
|
@Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.jsscripting", property = Constants.SERVICE_PID
|
||||||
+ "=org.openhab.jsscripting")
|
+ "=org.openhab.jsscripting")
|
||||||
@ConfigurableService(category = "automation", label = "JS Scripting", description_uri = "automation:jsscripting")
|
@ConfigurableService(category = "automation", label = "JS Scripting", description_uri = "automation:jsscripting")
|
||||||
|
@NonNullByDefault
|
||||||
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
||||||
private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
|
private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
|
||||||
private static final String INJECTION_CODE = "Object.assign(this, require('openhab'));";
|
private static final String INJECTION_CODE = "Object.assign(this, require('openhab'));";
|
||||||
@ -80,7 +82,7 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ScriptEngine createScriptEngine(String scriptType) {
|
public @Nullable ScriptEngine createScriptEngine(String scriptType) {
|
||||||
return new DebuggingGraalScriptEngine<>(
|
return new DebuggingGraalScriptEngine<>(
|
||||||
new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null, jsScriptServiceUtil));
|
new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null, jsScriptServiceUtil));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ package org.openhab.automation.jsscripting.internal;
|
|||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers;
|
import org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers;
|
||||||
@ -31,7 +32,7 @@ public class JSRuntimeFeatures {
|
|||||||
private final Map<String, Object> features = new HashMap<>();
|
private final Map<String, Object> features = new HashMap<>();
|
||||||
public final ThreadsafeTimers threadsafeTimers;
|
public final ThreadsafeTimers threadsafeTimers;
|
||||||
|
|
||||||
JSRuntimeFeatures(Object lock, JSScriptServiceUtil jsScriptServiceUtil) {
|
JSRuntimeFeatures(Lock lock, JSScriptServiceUtil jsScriptServiceUtil) {
|
||||||
this.threadsafeTimers = new ThreadsafeTimers(lock, jsScriptServiceUtil.getScriptExecution(),
|
this.threadsafeTimers = new ThreadsafeTimers(lock, jsScriptServiceUtil.getScriptExecution(),
|
||||||
jsScriptServiceUtil.getScheduler());
|
jsScriptServiceUtil.getScheduler());
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.automation.jsscripting.internal;
|
package org.openhab.automation.jsscripting.internal;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.core.automation.module.script.action.ScriptExecution;
|
import org.openhab.core.automation.module.script.action.ScriptExecution;
|
||||||
import org.openhab.core.scheduler.Scheduler;
|
import org.openhab.core.scheduler.Scheduler;
|
||||||
@ -44,7 +46,7 @@ public class JSScriptServiceUtil {
|
|||||||
return scriptExecution;
|
return scriptExecution;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JSRuntimeFeatures getJSRuntimeFeatures(Object lock) {
|
public JSRuntimeFeatures getJSRuntimeFeatures(Lock lock) {
|
||||||
return new JSRuntimeFeatures(lock, this);
|
return new JSRuntimeFeatures(lock, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import java.time.ZonedDateTime;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@ -58,7 +60,8 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
|
|||||||
* @author Jonathan Gilbert - Initial contribution
|
* @author Jonathan Gilbert - Initial contribution
|
||||||
* @author Dan Cunningham - Script injections
|
* @author Dan Cunningham - Script injections
|
||||||
* @author Florian Hotze - Create lock object for multi-thread synchronization; Inject the {@link JSRuntimeFeatures}
|
* @author Florian Hotze - Create lock object for multi-thread synchronization; Inject the {@link JSRuntimeFeatures}
|
||||||
* into the JS context; Fix memory leak caused by HostObject by making HostAccess reference static
|
* into the JS context; Fix memory leak caused by HostObject by making HostAccess reference static; Switch to
|
||||||
|
* {@link Lock} for multi-thread synchronization
|
||||||
*/
|
*/
|
||||||
public class OpenhabGraalJSScriptEngine
|
public class OpenhabGraalJSScriptEngine
|
||||||
extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<GraalJSScriptEngine> {
|
extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<GraalJSScriptEngine> {
|
||||||
@ -83,13 +86,13 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
}, HostAccess.TargetMappingPrecedence.LOW)
|
}, HostAccess.TargetMappingPrecedence.LOW)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
/** Shared lock object for synchronization of multi-thread access */
|
/** {@link Lock} synchronization of multi-thread access */
|
||||||
private final Object lock = new Object();
|
private final Lock lock = new ReentrantLock();
|
||||||
private final JSRuntimeFeatures jsRuntimeFeatures;
|
private final JSRuntimeFeatures jsRuntimeFeatures;
|
||||||
|
|
||||||
// these fields start as null because they are populated on first use
|
// these fields start as null because they are populated on first use
|
||||||
private String engineIdentifier;
|
private String engineIdentifier;
|
||||||
private Consumer<String> scriptDependencyListener;
|
private @Nullable Consumer<String> scriptDependencyListener;
|
||||||
|
|
||||||
private boolean initialized = false;
|
private boolean initialized = false;
|
||||||
private final String globalScript;
|
private final String globalScript;
|
||||||
@ -119,8 +122,9 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
@Override
|
@Override
|
||||||
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
|
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
|
||||||
FileAttribute<?>... attrs) throws IOException {
|
FileAttribute<?>... attrs) throws IOException {
|
||||||
if (scriptDependencyListener != null) {
|
Consumer<String> localScriptDependencyListener = scriptDependencyListener;
|
||||||
scriptDependencyListener.accept(path.toString());
|
if (localScriptDependencyListener != null) {
|
||||||
|
localScriptDependencyListener.accept(path.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.toString().endsWith(".js")) {
|
if (path.toString().endsWith(".js")) {
|
||||||
@ -174,6 +178,8 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void beforeInvocation() {
|
protected void beforeInvocation() {
|
||||||
|
lock.lock();
|
||||||
|
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -227,11 +233,15 @@ public class OpenhabGraalJSScriptEngine
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException {
|
protected Object afterInvocation(Object obj) {
|
||||||
// Synchronize multi-thread access to avoid exceptions when reloading a script file while the script is running
|
lock.unlock();
|
||||||
synchronized (lock) {
|
return obj;
|
||||||
return super.invokeFunction(s, objects);
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
protected Exception afterThrowsInvocation(Exception e) {
|
||||||
|
lock.unlock();
|
||||||
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import java.io.IOException;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.graalvm.polyglot.Context;
|
import org.graalvm.polyglot.Context;
|
||||||
@ -29,7 +30,8 @@ import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAuto
|
|||||||
* Class providing script extensions via CommonJS modules.
|
* Class providing script extensions via CommonJS modules.
|
||||||
*
|
*
|
||||||
* @author Jonathan Gilbert - Initial contribution
|
* @author Jonathan Gilbert - Initial contribution
|
||||||
* @author Florian Hotze - Pass in lock object for multi-thread synchronization
|
* @author Florian Hotze - Pass in lock object for multi-thread synchronization; Switch to {@link Lock} for multi-thread
|
||||||
|
* synchronization
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@ -37,11 +39,11 @@ public class ScriptExtensionModuleProvider {
|
|||||||
|
|
||||||
private static final String RUNTIME_MODULE_PREFIX = "@runtime";
|
private static final String RUNTIME_MODULE_PREFIX = "@runtime";
|
||||||
private static final String DEFAULT_MODULE_NAME = "Defaults";
|
private static final String DEFAULT_MODULE_NAME = "Defaults";
|
||||||
private final Object lock;
|
private final Lock lock;
|
||||||
|
|
||||||
private final ScriptExtensionAccessor scriptExtensionAccessor;
|
private final ScriptExtensionAccessor scriptExtensionAccessor;
|
||||||
|
|
||||||
public ScriptExtensionModuleProvider(ScriptExtensionAccessor scriptExtensionAccessor, Object lock) {
|
public ScriptExtensionModuleProvider(ScriptExtensionAccessor scriptExtensionAccessor, Lock lock) {
|
||||||
this.scriptExtensionAccessor = scriptExtensionAccessor;
|
this.scriptExtensionAccessor = scriptExtensionAccessor;
|
||||||
this.lock = lock;
|
this.lock = lock;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,11 @@
|
|||||||
|
|
||||||
package org.openhab.automation.jsscripting.internal.scope;
|
package org.openhab.automation.jsscripting.internal.scope;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@ -28,7 +32,7 @@ import org.osgi.service.component.annotations.Activate;
|
|||||||
* @author Jonathan Gilbert - Initial contribution
|
* @author Jonathan Gilbert - Initial contribution
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractScriptExtensionProvider implements ScriptExtensionProvider {
|
public abstract class AbstractScriptExtensionProvider implements ScriptExtensionProvider {
|
||||||
private Map<String, Function<String, Object>> types;
|
private Map<String, Function<String, Object>> types = new HashMap<>();
|
||||||
private Map<String, Map<String, Object>> idToTypes = new ConcurrentHashMap<>();
|
private Map<String, Map<String, Object>> idToTypes = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
protected abstract String getPresetName();
|
protected abstract String getPresetName();
|
||||||
@ -41,7 +45,7 @@ public abstract class AbstractScriptExtensionProvider implements ScriptExtension
|
|||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
public void activate(final BundleContext context) {
|
public void activate(final BundleContext context) {
|
||||||
types = new HashMap<>();
|
types.clear();
|
||||||
initializeTypes(context);
|
initializeTypes(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,11 @@
|
|||||||
|
|
||||||
package org.openhab.automation.jsscripting.internal.scope;
|
package org.openhab.automation.jsscripting.internal.scope;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
package org.openhab.automation.jsscripting.internal.scriptengine;
|
package org.openhab.automation.jsscripting.internal.scriptengine;
|
||||||
|
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import javax.script.Bindings;
|
import javax.script.Bindings;
|
||||||
import javax.script.Invocable;
|
import javax.script.Invocable;
|
||||||
@ -23,7 +22,7 @@ import javax.script.ScriptEngine;
|
|||||||
import javax.script.ScriptEngineFactory;
|
import javax.script.ScriptEngineFactory;
|
||||||
import javax.script.ScriptException;
|
import javax.script.ScriptException;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.NonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ScriptEngine} implementation that delegates to a supplied ScriptEngine instance. Allows overriding specific
|
* {@link ScriptEngine} implementation that delegates to a supplied ScriptEngine instance. Allows overriding specific
|
||||||
@ -33,94 +32,91 @@ import org.eclipse.jdt.annotation.Nullable;
|
|||||||
*/
|
*/
|
||||||
public abstract class DelegatingScriptEngineWithInvocableAndAutocloseable<T extends ScriptEngine & Invocable & AutoCloseable>
|
public abstract class DelegatingScriptEngineWithInvocableAndAutocloseable<T extends ScriptEngine & Invocable & AutoCloseable>
|
||||||
implements ScriptEngine, Invocable, AutoCloseable {
|
implements ScriptEngine, Invocable, AutoCloseable {
|
||||||
protected T delegate;
|
protected @NonNull T delegate;
|
||||||
|
|
||||||
public DelegatingScriptEngineWithInvocableAndAutocloseable(T delegate) {
|
public DelegatingScriptEngineWithInvocableAndAutocloseable(@NonNull T delegate) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object eval(String s, ScriptContext scriptContext) throws ScriptException {
|
public Object eval(String s, ScriptContext scriptContext) throws ScriptException {
|
||||||
return Objects.nonNull(delegate) ? delegate.eval(s, scriptContext) : null;
|
return delegate.eval(s, scriptContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException {
|
public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException {
|
||||||
return Objects.nonNull(delegate) ? delegate.eval(reader, scriptContext) : null;
|
return delegate.eval(reader, scriptContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object eval(String s) throws ScriptException {
|
public Object eval(String s) throws ScriptException {
|
||||||
return Objects.nonNull(delegate) ? delegate.eval(s) : null;
|
return delegate.eval(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object eval(Reader reader) throws ScriptException {
|
public Object eval(Reader reader) throws ScriptException {
|
||||||
return Objects.nonNull(delegate) ? delegate.eval(reader) : null;
|
return delegate.eval(reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object eval(String s, Bindings bindings) throws ScriptException {
|
public Object eval(String s, Bindings bindings) throws ScriptException {
|
||||||
return Objects.nonNull(delegate) ? delegate.eval(s, bindings) : null;
|
return delegate.eval(s, bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object eval(Reader reader, Bindings bindings) throws ScriptException {
|
public Object eval(Reader reader, Bindings bindings) throws ScriptException {
|
||||||
return Objects.nonNull(delegate) ? delegate.eval(reader, bindings) : null;
|
return delegate.eval(reader, bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void put(String s, Object o) {
|
public void put(String s, Object o) {
|
||||||
if (Objects.nonNull(delegate))
|
delegate.put(s, o);
|
||||||
delegate.put(s, o);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object get(String s) {
|
public Object get(String s) {
|
||||||
return Objects.nonNull(delegate) ? delegate.get(s) : null;
|
return delegate.get(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Bindings getBindings(int i) {
|
public Bindings getBindings(int i) {
|
||||||
return Objects.nonNull(delegate) ? delegate.getBindings(i) : null;
|
return delegate.getBindings(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setBindings(Bindings bindings, int i) {
|
public void setBindings(Bindings bindings, int i) {
|
||||||
if (Objects.nonNull(delegate))
|
delegate.setBindings(bindings, i);
|
||||||
delegate.setBindings(bindings, i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Bindings createBindings() {
|
public Bindings createBindings() {
|
||||||
return Objects.nonNull(delegate) ? delegate.createBindings() : null;
|
return delegate.createBindings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable ScriptContext getContext() {
|
public ScriptContext getContext() {
|
||||||
return Objects.nonNull(delegate) ? delegate.getContext() : null;
|
return delegate.getContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setContext(ScriptContext scriptContext) {
|
public void setContext(ScriptContext scriptContext) {
|
||||||
if (Objects.nonNull(delegate))
|
delegate.setContext(scriptContext);
|
||||||
delegate.setContext(scriptContext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable ScriptEngineFactory getFactory() {
|
public ScriptEngineFactory getFactory() {
|
||||||
return Objects.nonNull(delegate) ? delegate.getFactory() : null;
|
return delegate.getFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object invokeMethod(Object o, String s, Object... objects)
|
public Object invokeMethod(Object o, String s, Object... objects)
|
||||||
throws ScriptException, NoSuchMethodException {
|
throws ScriptException, NoSuchMethodException, IllegalArgumentException {
|
||||||
return Objects.nonNull(delegate) ? delegate.invokeMethod(o, s, objects) : null;
|
return delegate.invokeMethod(o, s, objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException {
|
public Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException {
|
||||||
return Objects.nonNull(delegate) ? delegate.invokeFunction(s, objects) : null;
|
return delegate.invokeFunction(s, objects);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -135,7 +131,6 @@ public abstract class DelegatingScriptEngineWithInvocableAndAutocloseable<T exte
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
if (Objects.nonNull(delegate))
|
delegate.close();
|
||||||
delegate.close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
package org.openhab.automation.jsscripting.internal.scriptengine;
|
package org.openhab.automation.jsscripting.internal.scriptengine;
|
||||||
|
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.lang.reflect.UndeclaredThrowableException;
|
||||||
|
|
||||||
import javax.script.Bindings;
|
import javax.script.Bindings;
|
||||||
import javax.script.Invocable;
|
import javax.script.Invocable;
|
||||||
@ -38,17 +39,21 @@ public abstract class InvocationInterceptingScriptEngineWithInvocableAndAutoClos
|
|||||||
protected void beforeInvocation() {
|
protected void beforeInvocation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ScriptException afterThrowsInvocation(ScriptException se) {
|
protected Object afterInvocation(Object obj) {
|
||||||
return se;
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Exception afterThrowsInvocation(Exception e) {
|
||||||
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object eval(String s, ScriptContext scriptContext) throws ScriptException {
|
public Object eval(String s, ScriptContext scriptContext) throws ScriptException {
|
||||||
try {
|
try {
|
||||||
beforeInvocation();
|
beforeInvocation();
|
||||||
return super.eval(s, scriptContext);
|
return afterInvocation(super.eval(s, scriptContext));
|
||||||
} catch (ScriptException se) {
|
} catch (ScriptException se) {
|
||||||
throw afterThrowsInvocation(se);
|
throw (ScriptException) afterThrowsInvocation(se);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,9 +61,9 @@ public abstract class InvocationInterceptingScriptEngineWithInvocableAndAutoClos
|
|||||||
public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException {
|
public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException {
|
||||||
try {
|
try {
|
||||||
beforeInvocation();
|
beforeInvocation();
|
||||||
return super.eval(reader, scriptContext);
|
return afterInvocation(super.eval(reader, scriptContext));
|
||||||
} catch (ScriptException se) {
|
} catch (ScriptException se) {
|
||||||
throw afterThrowsInvocation(se);
|
throw (ScriptException) afterThrowsInvocation(se);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,9 +71,9 @@ public abstract class InvocationInterceptingScriptEngineWithInvocableAndAutoClos
|
|||||||
public Object eval(String s) throws ScriptException {
|
public Object eval(String s) throws ScriptException {
|
||||||
try {
|
try {
|
||||||
beforeInvocation();
|
beforeInvocation();
|
||||||
return super.eval(s);
|
return afterInvocation(super.eval(s));
|
||||||
} catch (ScriptException se) {
|
} catch (ScriptException se) {
|
||||||
throw afterThrowsInvocation(se);
|
throw (ScriptException) afterThrowsInvocation(se);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,9 +81,9 @@ public abstract class InvocationInterceptingScriptEngineWithInvocableAndAutoClos
|
|||||||
public Object eval(Reader reader) throws ScriptException {
|
public Object eval(Reader reader) throws ScriptException {
|
||||||
try {
|
try {
|
||||||
beforeInvocation();
|
beforeInvocation();
|
||||||
return super.eval(reader);
|
return afterInvocation(super.eval(reader));
|
||||||
} catch (ScriptException se) {
|
} catch (ScriptException se) {
|
||||||
throw afterThrowsInvocation(se);
|
throw (ScriptException) afterThrowsInvocation(se);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,9 +91,9 @@ public abstract class InvocationInterceptingScriptEngineWithInvocableAndAutoClos
|
|||||||
public Object eval(String s, Bindings bindings) throws ScriptException {
|
public Object eval(String s, Bindings bindings) throws ScriptException {
|
||||||
try {
|
try {
|
||||||
beforeInvocation();
|
beforeInvocation();
|
||||||
return super.eval(s, bindings);
|
return afterInvocation(super.eval(s, bindings));
|
||||||
} catch (ScriptException se) {
|
} catch (ScriptException se) {
|
||||||
throw afterThrowsInvocation(se);
|
throw (ScriptException) afterThrowsInvocation(se);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,29 +101,47 @@ public abstract class InvocationInterceptingScriptEngineWithInvocableAndAutoClos
|
|||||||
public Object eval(Reader reader, Bindings bindings) throws ScriptException {
|
public Object eval(Reader reader, Bindings bindings) throws ScriptException {
|
||||||
try {
|
try {
|
||||||
beforeInvocation();
|
beforeInvocation();
|
||||||
return super.eval(reader, bindings);
|
return afterInvocation(super.eval(reader, bindings));
|
||||||
} catch (ScriptException se) {
|
} catch (ScriptException se) {
|
||||||
throw afterThrowsInvocation(se);
|
throw (ScriptException) afterThrowsInvocation(se);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object invokeMethod(Object o, String s, Object... objects) throws ScriptException, NoSuchMethodException {
|
public Object invokeMethod(Object o, String s, Object... objects)
|
||||||
|
throws ScriptException, NoSuchMethodException, NullPointerException, IllegalArgumentException {
|
||||||
try {
|
try {
|
||||||
beforeInvocation();
|
beforeInvocation();
|
||||||
return super.invokeMethod(o, s, objects);
|
return afterInvocation(super.invokeMethod(o, s, objects));
|
||||||
} catch (ScriptException se) {
|
} catch (ScriptException se) {
|
||||||
throw afterThrowsInvocation(se);
|
throw (ScriptException) afterThrowsInvocation(se);
|
||||||
|
} catch (NoSuchMethodException e) { // Make sure to unlock on exceptions from Invocable.invokeMethod to avoid
|
||||||
|
// deadlocks
|
||||||
|
throw (NoSuchMethodException) afterThrowsInvocation(e);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
throw (NullPointerException) afterThrowsInvocation(e);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw (IllegalArgumentException) afterThrowsInvocation(e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new UndeclaredThrowableException(afterThrowsInvocation(e)); // Wrap and rethrow other exceptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException {
|
public Object invokeFunction(String s, Object... objects)
|
||||||
|
throws ScriptException, NoSuchMethodException, NullPointerException {
|
||||||
try {
|
try {
|
||||||
beforeInvocation();
|
beforeInvocation();
|
||||||
return super.invokeFunction(s, objects);
|
return afterInvocation(super.invokeFunction(s, objects));
|
||||||
} catch (ScriptException se) {
|
} catch (ScriptException se) {
|
||||||
throw afterThrowsInvocation(se);
|
throw (ScriptException) afterThrowsInvocation(se);
|
||||||
|
} catch (NoSuchMethodException e) { // Make sure to unlock on exceptions from Invocable.invokeFunction to avoid
|
||||||
|
// deadlocks
|
||||||
|
throw (NoSuchMethodException) afterThrowsInvocation(e);
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
throw (NullPointerException) afterThrowsInvocation(e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new UndeclaredThrowableException(afterThrowsInvocation(e)); // Wrap and rethrow other exceptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ package org.openhab.automation.jsscripting.internal.threading;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
@ -38,7 +39,7 @@ import org.openhab.core.config.core.Configuration;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
class ThreadsafeSimpleRuleDelegate implements Rule, SimpleRuleActionHandler {
|
class ThreadsafeSimpleRuleDelegate implements Rule, SimpleRuleActionHandler {
|
||||||
|
|
||||||
private final Object lock;
|
private final Lock lock;
|
||||||
private final SimpleRule delegate;
|
private final SimpleRule delegate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,7 +48,7 @@ class ThreadsafeSimpleRuleDelegate implements Rule, SimpleRuleActionHandler {
|
|||||||
* @param lock rule executions will synchronize on this object
|
* @param lock rule executions will synchronize on this object
|
||||||
* @param delegate the delegate to forward invocations to
|
* @param delegate the delegate to forward invocations to
|
||||||
*/
|
*/
|
||||||
ThreadsafeSimpleRuleDelegate(Object lock, SimpleRule delegate) {
|
ThreadsafeSimpleRuleDelegate(Lock lock, SimpleRule delegate) {
|
||||||
this.lock = lock;
|
this.lock = lock;
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
}
|
}
|
||||||
@ -55,8 +56,11 @@ class ThreadsafeSimpleRuleDelegate implements Rule, SimpleRuleActionHandler {
|
|||||||
@Override
|
@Override
|
||||||
@NonNullByDefault({})
|
@NonNullByDefault({})
|
||||||
public Object execute(Action module, Map<String, ?> inputs) {
|
public Object execute(Action module, Map<String, ?> inputs) {
|
||||||
synchronized (lock) {
|
lock.lock();
|
||||||
|
try {
|
||||||
return delegate.execute(module, inputs);
|
return delegate.execute(module, inputs);
|
||||||
|
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid deadlocks
|
||||||
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import java.time.temporal.Temporal;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.core.automation.module.script.action.ScriptExecution;
|
import org.openhab.core.automation.module.script.action.ScriptExecution;
|
||||||
@ -35,7 +36,7 @@ import org.openhab.core.scheduler.SchedulerTemporalAdjuster;
|
|||||||
* Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
|
* Threadsafe reimplementation of the timer creation methods of {@link ScriptExecution}
|
||||||
*/
|
*/
|
||||||
public class ThreadsafeTimers {
|
public class ThreadsafeTimers {
|
||||||
private final Object lock;
|
private final Lock lock;
|
||||||
private final Scheduler scheduler;
|
private final Scheduler scheduler;
|
||||||
private final ScriptExecution scriptExecution;
|
private final ScriptExecution scriptExecution;
|
||||||
// Mapping of positive, non-zero integer values (used as timeoutID or intervalID) and the Scheduler
|
// Mapping of positive, non-zero integer values (used as timeoutID or intervalID) and the Scheduler
|
||||||
@ -43,7 +44,7 @@ public class ThreadsafeTimers {
|
|||||||
private AtomicLong lastId = new AtomicLong();
|
private AtomicLong lastId = new AtomicLong();
|
||||||
private String identifier = "noIdentifier";
|
private String identifier = "noIdentifier";
|
||||||
|
|
||||||
public ThreadsafeTimers(Object lock, ScriptExecution scriptExecution, Scheduler scheduler) {
|
public ThreadsafeTimers(Lock lock, ScriptExecution scriptExecution, Scheduler scheduler) {
|
||||||
this.lock = lock;
|
this.lock = lock;
|
||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
this.scriptExecution = scriptExecution;
|
this.scriptExecution = scriptExecution;
|
||||||
@ -79,8 +80,12 @@ public class ThreadsafeTimers {
|
|||||||
*/
|
*/
|
||||||
public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
|
public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable closure) {
|
||||||
return scriptExecution.createTimer(identifier, instant, () -> {
|
return scriptExecution.createTimer(identifier, instant, () -> {
|
||||||
synchronized (lock) {
|
lock.lock();
|
||||||
|
try {
|
||||||
closure.run();
|
closure.run();
|
||||||
|
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
||||||
|
// deadlocks
|
||||||
|
lock.unlock();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -108,12 +113,16 @@ public class ThreadsafeTimers {
|
|||||||
* @return Positive integer value which identifies the timer created; this value can be passed to
|
* @return Positive integer value which identifies the timer created; this value can be passed to
|
||||||
* <code>clearTimeout()</code> to cancel the timeout.
|
* <code>clearTimeout()</code> to cancel the timeout.
|
||||||
*/
|
*/
|
||||||
public long setTimeout(Runnable callback, Long delay, Object... args) {
|
public long setTimeout(Runnable callback, Long delay, @Nullable Object... args) {
|
||||||
long id = lastId.incrementAndGet();
|
long id = lastId.incrementAndGet();
|
||||||
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
||||||
synchronized (lock) {
|
lock.lock();
|
||||||
|
try {
|
||||||
callback.run();
|
callback.run();
|
||||||
idSchedulerMapping.remove(id);
|
idSchedulerMapping.remove(id);
|
||||||
|
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
||||||
|
// deadlocks
|
||||||
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
|
}, identifier + ".timeout." + id, Instant.now().plusMillis(delay));
|
||||||
idSchedulerMapping.put(id, future);
|
idSchedulerMapping.put(id, future);
|
||||||
@ -157,11 +166,15 @@ public class ThreadsafeTimers {
|
|||||||
* @return Numeric, non-zero value which identifies the timer created; this value can be passed to
|
* @return Numeric, non-zero value which identifies the timer created; this value can be passed to
|
||||||
* <code>clearInterval()</code> to cancel the interval.
|
* <code>clearInterval()</code> to cancel the interval.
|
||||||
*/
|
*/
|
||||||
public long setInterval(Runnable callback, Long delay, Object... args) {
|
public long setInterval(Runnable callback, Long delay, @Nullable Object... args) {
|
||||||
long id = lastId.incrementAndGet();
|
long id = lastId.incrementAndGet();
|
||||||
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
ScheduledCompletableFuture<Object> future = scheduler.schedule(() -> {
|
||||||
synchronized (lock) {
|
lock.lock();
|
||||||
|
try {
|
||||||
callback.run();
|
callback.run();
|
||||||
|
} finally { // Make sure that Lock is unlocked regardless of an exception is thrown or not to avoid
|
||||||
|
// deadlocks
|
||||||
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
|
}, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay)));
|
||||||
idSchedulerMapping.put(id, future);
|
idSchedulerMapping.put(id, future);
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
package org.openhab.automation.jsscripting.internal.threading;
|
package org.openhab.automation.jsscripting.internal.threading;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.core.automation.Rule;
|
import org.openhab.core.automation.Rule;
|
||||||
import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager;
|
import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager;
|
||||||
@ -31,15 +33,16 @@ import org.openhab.core.automation.type.TriggerType;
|
|||||||
* instance of this class that they are registered with.
|
* instance of this class that they are registered with.
|
||||||
*
|
*
|
||||||
* @author Jonathan Gilbert - Initial contribution
|
* @author Jonathan Gilbert - Initial contribution
|
||||||
* @author Florian Hotze - Pass in lock object for multi-thread synchronization
|
* @author Florian Hotze - Pass in lock object for multi-thread synchronization; Switch to {@link Lock} for multi-thread
|
||||||
|
* synchronization
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class ThreadsafeWrappingScriptedAutomationManagerDelegate {
|
public class ThreadsafeWrappingScriptedAutomationManagerDelegate {
|
||||||
|
|
||||||
private ScriptedAutomationManager delegate;
|
private ScriptedAutomationManager delegate;
|
||||||
private final Object lock;
|
private final Lock lock;
|
||||||
|
|
||||||
public ThreadsafeWrappingScriptedAutomationManagerDelegate(ScriptedAutomationManager delegate, Object lock) {
|
public ThreadsafeWrappingScriptedAutomationManagerDelegate(ScriptedAutomationManager delegate, Lock lock) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
this.lock = lock;
|
this.lock = lock;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
# Please check here how to add suppressions https://maven.apache.org/plugins/maven-pmd-plugin/examples/violation-exclusions.html
|
||||||
|
org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable=AvoidThrowingNullPointerException,AvoidCatchingNPE
|
||||||
Loading…
x
Reference in New Issue
Block a user