JavaScript Sandbox Service improvements.

This commit is contained in:
Igor Kulikov 2018-05-18 16:55:26 +03:00
parent 04432ee73f
commit f73fbc5ee9
22 changed files with 302 additions and 136 deletions

View File

@ -44,7 +44,7 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.script.JsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import scala.concurrent.duration.Duration; import scala.concurrent.duration.Duration;
import java.util.Collections; import java.util.Collections;
@ -151,8 +151,8 @@ class DefaultTbContext implements TbContext {
} }
@Override @Override
public ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames) { public ScriptEngine createJsScriptEngine(String script, String... argNames) {
return new JsScriptEngine(mainCtx.getJsSandbox(), script, functionName, argNames); return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), script, argNames);
} }
@Override @Override

View File

@ -50,9 +50,8 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.service.script.JsExecutorService;
import org.thingsboard.server.service.script.JsSandboxService; import org.thingsboard.server.service.script.JsSandboxService;
import org.thingsboard.server.service.script.JsScriptEngine; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -266,7 +265,6 @@ public class RuleChainController extends BaseController {
try { try {
String script = inputParams.get("script").asText(); String script = inputParams.get("script").asText();
String scriptType = inputParams.get("scriptType").asText(); String scriptType = inputParams.get("scriptType").asText();
String functionName = inputParams.get("functionName").asText();
JsonNode argNamesJson = inputParams.get("argNames"); JsonNode argNamesJson = inputParams.get("argNames");
String[] argNames = objectMapper.treeToValue(argNamesJson, String[].class); String[] argNames = objectMapper.treeToValue(argNamesJson, String[].class);
@ -278,7 +276,7 @@ public class RuleChainController extends BaseController {
String errorText = ""; String errorText = "";
ScriptEngine engine = null; ScriptEngine engine = null;
try { try {
engine = new JsScriptEngine(jsSandboxService, script, functionName, argNames); engine = new RuleNodeJsScriptEngine(jsSandboxService, script, argNames);
TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L); TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L);
switch (scriptType) { switch (scriptType) {
case "update": case "update":

View File

@ -0,0 +1,133 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* 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 org.thingsboard.server.service.script;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.script.ScriptException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public abstract class AbstractNashornJsSandboxService implements JsSandboxService {
private NashornSandbox sandbox = NashornSandboxes.create();
private ExecutorService monitorExecutorService;
private Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
private Map<UUID,AtomicInteger> blackListedFunctions = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
sandbox.setExecutor(monitorExecutorService);
sandbox.setMaxCPUTime(getMaxCpuTime());
sandbox.allowNoBraces(false);
sandbox.setMaxPreparedStatements(30);
}
@PreDestroy
public void stop() {
if (monitorExecutorService != null) {
monitorExecutorService.shutdownNow();
}
}
protected abstract int getMonitorThreadPoolSize();
protected abstract long getMaxCpuTime();
protected abstract int getMaxErrors();
@Override
public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) {
UUID scriptId = UUID.randomUUID();
String functionName = "invokeInternal_" + scriptId.toString().replace('-','_');
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
try {
sandbox.eval(jsScript);
functionsMap.put(scriptId, functionName);
} catch (Exception e) {
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
return Futures.immediateFailedFuture(e);
}
return Futures.immediateFuture(scriptId);
}
@Override
public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) {
String functionName = functionsMap.get(scriptId);
if (functionName == null) {
return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
}
if (!isBlackListed(scriptId)) {
try {
return Futures.immediateFuture(sandbox.getSandboxedInvocable().invokeFunction(functionName, args));
} catch (Exception e) {
blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
return Futures.immediateFailedFuture(e);
}
} else {
return Futures.immediateFailedFuture(
new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!"));
}
}
@Override
public ListenableFuture<Void> release(UUID scriptId) {
String functionName = functionsMap.get(scriptId);
if (functionName != null) {
try {
sandbox.eval(functionName + " = undefined;");
functionsMap.remove(scriptId);
blackListedFunctions.remove(scriptId);
} catch (ScriptException e) {
return Futures.immediateFailedFuture(e);
}
}
return Futures.immediateFuture(null);
}
private boolean isBlackListed(UUID scriptId) {
if (blackListedFunctions.containsKey(scriptId)) {
AtomicInteger errorCount = blackListedFunctions.get(scriptId);
return errorCount.get() >= getMaxErrors();
} else {
return false;
}
}
private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
switch (scriptType) {
case RULE_NODE_SCRIPT:
return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames);
default:
throw new RuntimeException("No script factory implemented for scriptType: " + scriptType);
}
}
}

View File

@ -16,12 +16,16 @@
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import javax.script.ScriptException; import com.google.common.util.concurrent.ListenableFuture;
import java.util.UUID;
public interface JsSandboxService { public interface JsSandboxService {
Object eval(String js) throws ScriptException; ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames);
Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException; ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args);
ListenableFuture<Void> release(UUID scriptId);
} }

View File

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* 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 org.thingsboard.server.service.script;
public enum JsScriptType {
RULE_NODE_SCRIPT
}

View File

@ -16,21 +16,13 @@
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.script.ScriptException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j @Slf4j
@Service @Service
public class NashornJsSandboxService implements JsSandboxService { public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
@Value("${actors.rule.js_sandbox.monitor_thread_pool_size}") @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}")
private int monitorThreadPoolSize; private int monitorThreadPoolSize;
@ -38,33 +30,21 @@ public class NashornJsSandboxService implements JsSandboxService {
@Value("${actors.rule.js_sandbox.max_cpu_time}") @Value("${actors.rule.js_sandbox.max_cpu_time}")
private long maxCpuTime; private long maxCpuTime;
private NashornSandbox sandbox = NashornSandboxes.create(); @Value("${actors.rule.js_sandbox.max_errors}")
private ExecutorService monitorExecutorService; private int maxErrors;
@PostConstruct @Override
public void init() { protected int getMonitorThreadPoolSize() {
monitorExecutorService = Executors.newFixedThreadPool(monitorThreadPoolSize); return monitorThreadPoolSize;
sandbox.setExecutor(monitorExecutorService);
sandbox.setMaxCPUTime(maxCpuTime);
sandbox.allowNoBraces(false);
sandbox.setMaxPreparedStatements(30);
}
@PreDestroy
public void stop() {
if (monitorExecutorService != null) {
monitorExecutorService.shutdownNow();
}
} }
@Override @Override
public Object eval(String js) throws ScriptException { protected long getMaxCpuTime() {
return sandbox.eval(js); return maxCpuTime;
} }
@Override @Override
public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { protected int getMaxErrors() {
return sandbox.getSandboxedInvocable().invokeFunction(name, args); return maxErrors;
} }
} }

View File

@ -28,56 +28,23 @@ import javax.script.ScriptException;
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.UUID;
import java.util.concurrent.ExecutionException;
@Slf4j @Slf4j
public class JsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEngine { public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEngine {
public static final String MSG = "msg";
public static final String METADATA = "metadata";
public static final String MSG_TYPE = "msgType";
private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " +
" var msg = JSON.parse(msgStr); " +
" var metadata = JSON.parse(metadataStr); " +
" return JSON.stringify(%s(msg, metadata, msgType));" +
" function %s(%s, %s, %s) {";
private static final String JS_WRAPPER_SUFFIX = "}" +
"\n}";
private static final ObjectMapper mapper = new ObjectMapper(); private static final ObjectMapper mapper = new ObjectMapper();
// private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
// private ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"});
private final JsSandboxService sandboxService; private final JsSandboxService sandboxService;
private final String invokeFunctionName; private final UUID scriptId;
public JsScriptEngine(JsSandboxService sandboxService, String script, String functionName, String... argNames) { public RuleNodeJsScriptEngine(JsSandboxService sandboxService, String script, String... argNames) {
this.sandboxService = sandboxService; this.sandboxService = sandboxService;
this.invokeFunctionName = "invokeInternal" + this.hashCode();
String msgArg;
String metadataArg;
String msgTypeArg;
if (argNames != null && argNames.length == 3) {
msgArg = argNames[0];
metadataArg = argNames[1];
msgTypeArg = argNames[2];
} else {
msgArg = MSG;
metadataArg = METADATA;
msgTypeArg = MSG_TYPE;
}
String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, this.invokeFunctionName,
functionName, functionName, msgArg, metadataArg, msgTypeArg);
compileScript(jsWrapperPrefix + script + JS_WRAPPER_SUFFIX);
}
private void compileScript(String script) {
try { try {
//engine.eval(script); this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get();
sandboxService.eval(script); } catch (Exception e) {
} catch (ScriptException e) {
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
throw new IllegalArgumentException("Can't compile script: " + e.getMessage()); throw new IllegalArgumentException("Can't compile script: " + e.getMessage());
} }
} }
@ -103,17 +70,17 @@ public class JsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEng
String data = null; String data = null;
Map<String, String> metadata = null; Map<String, String> metadata = null;
String messageType = null; String messageType = null;
if (msgData.has(MSG)) { if (msgData.has(RuleNodeScriptFactory.MSG)) {
JsonNode msgPayload = msgData.get(MSG); JsonNode msgPayload = msgData.get(RuleNodeScriptFactory.MSG);
data = mapper.writeValueAsString(msgPayload); data = mapper.writeValueAsString(msgPayload);
} }
if (msgData.has(METADATA)) { if (msgData.has(RuleNodeScriptFactory.METADATA)) {
JsonNode msgMetadata = msgData.get(METADATA); JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA);
metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() { metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() {
}); });
} }
if (msgData.has(MSG_TYPE)) { if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) {
messageType = msgData.get(MSG_TYPE).asText(); messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText();
} }
String newData = data != null ? data : msg.getData(); String newData = data != null ? data : msg.getData();
TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy(); TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy();
@ -195,18 +162,20 @@ public class JsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEng
private JsonNode executeScript(TbMsg msg) throws ScriptException { private JsonNode executeScript(TbMsg msg) throws ScriptException {
try { try {
String[] inArgs = prepareArgs(msg); String[] inArgs = prepareArgs(msg);
//String eval = ((Invocable)engine).invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString(); String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString();
String eval = sandboxService.invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString();
return mapper.readTree(eval); return mapper.readTree(eval);
} catch (ScriptException | IllegalArgumentException th) { } catch (ExecutionException e) {
throw th; if (e.getCause() instanceof ScriptException) {
} catch (Throwable th) { throw (ScriptException)e.getCause();
th.printStackTrace(); } else {
throw new RuntimeException("Failed to execute js script", th); throw new ScriptException("Failed to execute js script: " + e.getMessage());
}
} catch (Exception e) {
throw new ScriptException("Failed to execute js script: " + e.getMessage());
} }
} }
public void destroy() { public void destroy() {
//engine = null; sandboxService.release(this.scriptId);
} }
} }

View File

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* 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 org.thingsboard.server.service.script;
public class RuleNodeScriptFactory {
public static final String MSG = "msg";
public static final String METADATA = "metadata";
public static final String MSG_TYPE = "msgType";
public static final String RULE_NODE_FUNCTION_NAME = "ruleNodeFunc";
private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " +
" var msg = JSON.parse(msgStr); " +
" var metadata = JSON.parse(metadataStr); " +
" return JSON.stringify(%s(msg, metadata, msgType));" +
" function %s(%s, %s, %s) {";
private static final String JS_WRAPPER_SUFFIX = "}" +
"\n}";
public static String generateRuleNodeScript(String functionName, String scriptBody, String... argNames) {
String msgArg;
String metadataArg;
String msgTypeArg;
if (argNames != null && argNames.length == 3) {
msgArg = argNames[0];
metadataArg = argNames[1];
msgTypeArg = argNames[2];
} else {
msgArg = MSG;
metadataArg = METADATA;
msgTypeArg = MSG_TYPE;
}
String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName,
RULE_NODE_FUNCTION_NAME, RULE_NODE_FUNCTION_NAME, msgArg, metadataArg, msgTypeArg);
return jsWrapperPrefix + scriptBody + JS_WRAPPER_SUFFIX;
}
}

View File

@ -243,6 +243,8 @@ actors:
monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
# Maximum CPU time in milliseconds allowed for script execution # Maximum CPU time in milliseconds allowed for script execution
max_cpu_time: "${ACTORS_RULE_JS_SANDBOX_MAX_CPU_TIME:100}" max_cpu_time: "${ACTORS_RULE_JS_SANDBOX_MAX_CPU_TIME:100}"
# Maximum allowed JavaScript execution errors before JavaScript will be blacklisted
max_errors: "${ACTORS_RULE_JS_SANDBOX_MAX_ERRORS:3}"
chain: chain:
# Errors for particular actor are persisted once per specified amount of milliseconds # Errors for particular actor are persisted once per specified amount of milliseconds
error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}" error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}"

View File

@ -27,30 +27,28 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import javax.script.ScriptException; import javax.script.ScriptException;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class JsScriptEngineTest { public class RuleNodeJsScriptEngineTest {
private ScriptEngine scriptEngine; private ScriptEngine scriptEngine;
private TestNashornJsSandboxService jsSandboxService; private TestNashornJsSandboxService jsSandboxService;
@Before @Before
public void beforeTest() throws Exception { public void beforeTest() throws Exception {
jsSandboxService = new TestNashornJsSandboxService(1, 100); jsSandboxService = new TestNashornJsSandboxService(1, 100, 3);
} }
@After @After
public void afterTest() throws Exception { public void afterTest() throws Exception {
jsSandboxService.destroy(); jsSandboxService.stop();
} }
@Test @Test
public void msgCanBeUpdated() throws ScriptException { public void msgCanBeUpdated() throws ScriptException {
String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};";
scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
@ -61,12 +59,13 @@ public class JsScriptEngineTest {
TbMsg actual = scriptEngine.executeUpdate(msg); TbMsg actual = scriptEngine.executeUpdate(msg);
assertEquals("70", actual.getMetaData().getValue("temp")); assertEquals("70", actual.getMetaData().getValue("temp"));
scriptEngine.destroy();
} }
@Test @Test
public void newAttributesCanBeAddedInMsg() throws ScriptException { public void newAttributesCanBeAddedInMsg() throws ScriptException {
String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};"; String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};";
scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -76,12 +75,13 @@ public class JsScriptEngineTest {
TbMsg actual = scriptEngine.executeUpdate(msg); TbMsg actual = scriptEngine.executeUpdate(msg);
assertEquals("94", actual.getMetaData().getValue("newAttr")); assertEquals("94", actual.getMetaData().getValue("newAttr"));
scriptEngine.destroy();
} }
@Test @Test
public void payloadCanBeUpdated() throws ScriptException { public void payloadCanBeUpdated() throws ScriptException {
String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};"; String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};";
scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -93,12 +93,13 @@ public class JsScriptEngineTest {
String expectedJson = "{\"name\":\"Vit\",\"passed\":35,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}"; String expectedJson = "{\"name\":\"Vit\",\"passed\":35,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}";
assertEquals(expectedJson, actual.getData()); assertEquals(expectedJson, actual.getData());
scriptEngine.destroy();
} }
@Test @Test
public void metadataAccessibleForFilter() throws ScriptException { public void metadataAccessibleForFilter() throws ScriptException {
String function = "return metadata.humidity < 15;"; String function = "return metadata.humidity < 15;";
scriptEngine = new JsScriptEngine(jsSandboxService, function, "Filter"); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -106,12 +107,13 @@ public class JsScriptEngineTest {
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
assertFalse(scriptEngine.executeFilter(msg)); assertFalse(scriptEngine.executeFilter(msg));
scriptEngine.destroy();
} }
@Test @Test
public void dataAccessibleForFilter() throws ScriptException { public void dataAccessibleForFilter() throws ScriptException {
String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;"; String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;";
scriptEngine = new JsScriptEngine(jsSandboxService, function, "Filter"); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "7"); metaData.putValue("temp", "7");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -119,6 +121,7 @@ public class JsScriptEngineTest {
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
assertTrue(scriptEngine.executeFilter(msg)); assertTrue(scriptEngine.executeFilter(msg));
scriptEngine.destroy();
} }
@Test @Test
@ -131,7 +134,7 @@ public class JsScriptEngineTest {
"};\n" + "};\n" +
"\n" + "\n" +
"return nextRelation(metadata, msg);"; "return nextRelation(metadata, msg);";
scriptEngine = new JsScriptEngine(jsSandboxService, jsCode, "Switch"); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -140,6 +143,7 @@ public class JsScriptEngineTest {
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
Set<String> actual = scriptEngine.executeSwitch(msg); Set<String> actual = scriptEngine.executeSwitch(msg);
assertEquals(Sets.newHashSet("one"), actual); assertEquals(Sets.newHashSet("one"), actual);
scriptEngine.destroy();
} }
@Test @Test
@ -152,7 +156,7 @@ public class JsScriptEngineTest {
"};\n" + "};\n" +
"\n" + "\n" +
"return nextRelation(metadata, msg);"; "return nextRelation(metadata, msg);";
scriptEngine = new JsScriptEngine(jsSandboxService, jsCode, "Switch"); scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode);
TbMsgMetaData metaData = new TbMsgMetaData(); TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("temp", "10"); metaData.putValue("temp", "10");
metaData.putValue("humidity", "99"); metaData.putValue("humidity", "99");
@ -161,6 +165,7 @@ public class JsScriptEngineTest {
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L); TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L);
Set<String> actual = scriptEngine.executeSwitch(msg); Set<String> actual = scriptEngine.executeSwitch(msg);
assertEquals(Sets.newHashSet("one", "three"), actual); assertEquals(Sets.newHashSet("one", "three"), actual);
scriptEngine.destroy();
} }
} }

View File

@ -16,39 +16,40 @@
package org.thingsboard.server.service.script; package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.ListenableFuture;
import delight.nashornsandbox.NashornSandbox; import delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes; import delight.nashornsandbox.NashornSandboxes;
import javax.script.ScriptException; import javax.script.ScriptException;
import java.util.UUID;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
public class TestNashornJsSandboxService implements JsSandboxService { public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService {
private NashornSandbox sandbox = NashornSandboxes.create(); private final int monitorThreadPoolSize;
private ExecutorService monitorExecutorService; private final long maxCpuTime;
private final int maxErrors;
public TestNashornJsSandboxService(int monitorThreadPoolSize, long maxCpuTime) { public TestNashornJsSandboxService(int monitorThreadPoolSize, long maxCpuTime, int maxErrors) {
monitorExecutorService = Executors.newFixedThreadPool(monitorThreadPoolSize); this.monitorThreadPoolSize = monitorThreadPoolSize;
sandbox.setExecutor(monitorExecutorService); this.maxCpuTime = maxCpuTime;
sandbox.setMaxCPUTime(maxCpuTime); this.maxErrors = maxErrors;
sandbox.allowNoBraces(false); init();
sandbox.setMaxPreparedStatements(30);
} }
@Override @Override
public Object eval(String js) throws ScriptException { protected int getMonitorThreadPoolSize() {
return sandbox.eval(js); return monitorThreadPoolSize;
} }
@Override @Override
public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { protected long getMaxCpuTime() {
return sandbox.getSandboxedInvocable().invokeFunction(name, args); return maxCpuTime;
} }
public void destroy() { @Override
if (monitorExecutorService != null) { protected int getMaxErrors() {
monitorExecutorService.shutdownNow(); return maxErrors;
}
} }
} }

View File

@ -90,6 +90,6 @@ public interface TbContext {
MailService getMailService(); MailService getMailService();
ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames); ScriptEngine createJsScriptEngine(String script, String... argNames);
} }

View File

@ -43,7 +43,7 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadAlarmNodeConfig(configuration); this.config = loadAlarmNodeConfig(configuration);
this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details"); this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs());
} }
protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException; protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;

View File

@ -46,7 +46,7 @@ public class TbLogNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class);
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "ToString"); this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
} }
@Override @Override

View File

@ -65,7 +65,7 @@ public class TbMsgGeneratorNode implements TbNode {
} else { } else {
originatorId = ctx.getSelfId(); originatorId = ctx.getSelfId();
} }
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Generate", "prevMsg", "prevMetadata", "prevMsgType"); this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType");
sentTickMsg(ctx); sentTickMsg(ctx);
} }

View File

@ -45,7 +45,7 @@ public class TbJsFilterNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class);
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Filter"); this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
} }
@Override @Override

View File

@ -47,7 +47,7 @@ public class TbJsSwitchNode implements TbNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class);
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Switch"); this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
} }
@Override @Override

View File

@ -43,7 +43,7 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Transform"); this.jsEngine = ctx.createJsScriptEngine(config.getJsScript());
setConfig(config); setConfig(config);
} }

View File

@ -152,7 +152,7 @@ public class TbAlarmNodeTest {
verifyError(msg, "message", NotImplementedException.class); verifyError(msg, "message", NotImplementedException.class);
verify(ctx).createJsScriptEngine("DETAILS", "Details"); verify(ctx).createJsScriptEngine("DETAILS");
verify(ctx, times(1)).getJsExecutor(); verify(ctx, times(1)).getJsExecutor();
verify(ctx).getAlarmService(); verify(ctx).getAlarmService();
verify(ctx, times(2)).getDbCallbackExecutor(); verify(ctx, times(2)).getDbCallbackExecutor();
@ -314,7 +314,7 @@ public class TbAlarmNodeTest {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs); when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs);
when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getTenantId()).thenReturn(tenantId);
when(ctx.getJsExecutor()).thenReturn(executor); when(ctx.getJsExecutor()).thenReturn(executor);
@ -338,7 +338,7 @@ public class TbAlarmNodeTest {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs); when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs);
when(ctx.getTenantId()).thenReturn(tenantId); when(ctx.getTenantId()).thenReturn(tenantId);
when(ctx.getJsExecutor()).thenReturn(executor); when(ctx.getJsExecutor()).thenReturn(executor);

View File

@ -97,7 +97,7 @@ public class TbJsFilterNodeTest {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
when(ctx.createJsScriptEngine("scr", "Filter")).thenReturn(scriptEngine); when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
node = new TbJsFilterNode(); node = new TbJsFilterNode();
node.init(ctx, nodeConfiguration); node.init(ctx, nodeConfiguration);

View File

@ -79,7 +79,7 @@ public class TbJsSwitchNodeTest {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
when(ctx.createJsScriptEngine("scr", "Switch")).thenReturn(scriptEngine); when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
node = new TbJsSwitchNode(); node = new TbJsSwitchNode();
node.init(ctx, nodeConfiguration); node.init(ctx, nodeConfiguration);

View File

@ -97,7 +97,7 @@ public class TbTransformMsgNodeTest {
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
when(ctx.createJsScriptEngine("scr", "Transform")).thenReturn(scriptEngine); when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine);
node = new TbTransformMsgNode(); node = new TbTransformMsgNode();
node.init(ctx, nodeConfiguration); node.init(ctx, nodeConfiguration);