Commit f73fbc5ee94b64874f5b329c85a28b686204797e
1 parent
04432ee7
JavaScript Sandbox Service improvements.
Showing
22 changed files
with
302 additions
and
136 deletions
... | ... | @@ -44,7 +44,7 @@ import org.thingsboard.server.dao.relation.RelationService; |
44 | 44 | import org.thingsboard.server.dao.rule.RuleChainService; |
45 | 45 | import org.thingsboard.server.dao.timeseries.TimeseriesService; |
46 | 46 | import org.thingsboard.server.dao.user.UserService; |
47 | -import org.thingsboard.server.service.script.JsScriptEngine; | |
47 | +import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; | |
48 | 48 | import scala.concurrent.duration.Duration; |
49 | 49 | |
50 | 50 | import java.util.Collections; |
... | ... | @@ -151,8 +151,8 @@ class DefaultTbContext implements TbContext { |
151 | 151 | } |
152 | 152 | |
153 | 153 | @Override |
154 | - public ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames) { | |
155 | - return new JsScriptEngine(mainCtx.getJsSandbox(), script, functionName, argNames); | |
154 | + public ScriptEngine createJsScriptEngine(String script, String... argNames) { | |
155 | + return new RuleNodeJsScriptEngine(mainCtx.getJsSandbox(), script, argNames); | |
156 | 156 | } |
157 | 157 | |
158 | 158 | @Override | ... | ... |
... | ... | @@ -50,9 +50,8 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; |
50 | 50 | import org.thingsboard.server.common.msg.TbMsg; |
51 | 51 | import org.thingsboard.server.common.msg.TbMsgMetaData; |
52 | 52 | import org.thingsboard.server.dao.event.EventService; |
53 | -import org.thingsboard.server.service.script.JsExecutorService; | |
54 | 53 | import org.thingsboard.server.service.script.JsSandboxService; |
55 | -import org.thingsboard.server.service.script.JsScriptEngine; | |
54 | +import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; | |
56 | 55 | |
57 | 56 | import java.util.List; |
58 | 57 | import java.util.Map; |
... | ... | @@ -266,7 +265,6 @@ public class RuleChainController extends BaseController { |
266 | 265 | try { |
267 | 266 | String script = inputParams.get("script").asText(); |
268 | 267 | String scriptType = inputParams.get("scriptType").asText(); |
269 | - String functionName = inputParams.get("functionName").asText(); | |
270 | 268 | JsonNode argNamesJson = inputParams.get("argNames"); |
271 | 269 | String[] argNames = objectMapper.treeToValue(argNamesJson, String[].class); |
272 | 270 | |
... | ... | @@ -278,7 +276,7 @@ public class RuleChainController extends BaseController { |
278 | 276 | String errorText = ""; |
279 | 277 | ScriptEngine engine = null; |
280 | 278 | try { |
281 | - engine = new JsScriptEngine(jsSandboxService, script, functionName, argNames); | |
279 | + engine = new RuleNodeJsScriptEngine(jsSandboxService, script, argNames); | |
282 | 280 | TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L); |
283 | 281 | switch (scriptType) { |
284 | 282 | case "update": | ... | ... |
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsSandboxService.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2018 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package org.thingsboard.server.service.script; | |
18 | + | |
19 | +import com.google.common.util.concurrent.Futures; | |
20 | +import com.google.common.util.concurrent.ListenableFuture; | |
21 | +import delight.nashornsandbox.NashornSandbox; | |
22 | +import delight.nashornsandbox.NashornSandboxes; | |
23 | +import lombok.extern.slf4j.Slf4j; | |
24 | + | |
25 | +import javax.annotation.PostConstruct; | |
26 | +import javax.annotation.PreDestroy; | |
27 | +import javax.script.ScriptException; | |
28 | +import java.util.Map; | |
29 | +import java.util.UUID; | |
30 | +import java.util.concurrent.ConcurrentHashMap; | |
31 | +import java.util.concurrent.ExecutorService; | |
32 | +import java.util.concurrent.Executors; | |
33 | +import java.util.concurrent.atomic.AtomicInteger; | |
34 | + | |
35 | +@Slf4j | |
36 | +public abstract class AbstractNashornJsSandboxService implements JsSandboxService { | |
37 | + | |
38 | + private NashornSandbox sandbox = NashornSandboxes.create(); | |
39 | + private ExecutorService monitorExecutorService; | |
40 | + | |
41 | + private Map<UUID, String> functionsMap = new ConcurrentHashMap<>(); | |
42 | + | |
43 | + private Map<UUID,AtomicInteger> blackListedFunctions = new ConcurrentHashMap<>(); | |
44 | + | |
45 | + @PostConstruct | |
46 | + public void init() { | |
47 | + monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize()); | |
48 | + sandbox.setExecutor(monitorExecutorService); | |
49 | + sandbox.setMaxCPUTime(getMaxCpuTime()); | |
50 | + sandbox.allowNoBraces(false); | |
51 | + sandbox.setMaxPreparedStatements(30); | |
52 | + } | |
53 | + | |
54 | + @PreDestroy | |
55 | + public void stop() { | |
56 | + if (monitorExecutorService != null) { | |
57 | + monitorExecutorService.shutdownNow(); | |
58 | + } | |
59 | + } | |
60 | + | |
61 | + protected abstract int getMonitorThreadPoolSize(); | |
62 | + | |
63 | + protected abstract long getMaxCpuTime(); | |
64 | + | |
65 | + protected abstract int getMaxErrors(); | |
66 | + | |
67 | + @Override | |
68 | + public ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames) { | |
69 | + UUID scriptId = UUID.randomUUID(); | |
70 | + String functionName = "invokeInternal_" + scriptId.toString().replace('-','_'); | |
71 | + String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); | |
72 | + try { | |
73 | + sandbox.eval(jsScript); | |
74 | + functionsMap.put(scriptId, functionName); | |
75 | + } catch (Exception e) { | |
76 | + log.warn("Failed to compile JS script: {}", e.getMessage(), e); | |
77 | + return Futures.immediateFailedFuture(e); | |
78 | + } | |
79 | + return Futures.immediateFuture(scriptId); | |
80 | + } | |
81 | + | |
82 | + @Override | |
83 | + public ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args) { | |
84 | + String functionName = functionsMap.get(scriptId); | |
85 | + if (functionName == null) { | |
86 | + return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!")); | |
87 | + } | |
88 | + if (!isBlackListed(scriptId)) { | |
89 | + try { | |
90 | + return Futures.immediateFuture(sandbox.getSandboxedInvocable().invokeFunction(functionName, args)); | |
91 | + } catch (Exception e) { | |
92 | + blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet(); | |
93 | + return Futures.immediateFailedFuture(e); | |
94 | + } | |
95 | + } else { | |
96 | + return Futures.immediateFailedFuture( | |
97 | + new RuntimeException("Script is blacklisted due to maximum error count " + getMaxErrors() + "!")); | |
98 | + } | |
99 | + } | |
100 | + | |
101 | + @Override | |
102 | + public ListenableFuture<Void> release(UUID scriptId) { | |
103 | + String functionName = functionsMap.get(scriptId); | |
104 | + if (functionName != null) { | |
105 | + try { | |
106 | + sandbox.eval(functionName + " = undefined;"); | |
107 | + functionsMap.remove(scriptId); | |
108 | + blackListedFunctions.remove(scriptId); | |
109 | + } catch (ScriptException e) { | |
110 | + return Futures.immediateFailedFuture(e); | |
111 | + } | |
112 | + } | |
113 | + return Futures.immediateFuture(null); | |
114 | + } | |
115 | + | |
116 | + private boolean isBlackListed(UUID scriptId) { | |
117 | + if (blackListedFunctions.containsKey(scriptId)) { | |
118 | + AtomicInteger errorCount = blackListedFunctions.get(scriptId); | |
119 | + return errorCount.get() >= getMaxErrors(); | |
120 | + } else { | |
121 | + return false; | |
122 | + } | |
123 | + } | |
124 | + | |
125 | + private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) { | |
126 | + switch (scriptType) { | |
127 | + case RULE_NODE_SCRIPT: | |
128 | + return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames); | |
129 | + default: | |
130 | + throw new RuntimeException("No script factory implemented for scriptType: " + scriptType); | |
131 | + } | |
132 | + } | |
133 | +} | ... | ... |
... | ... | @@ -16,12 +16,16 @@ |
16 | 16 | |
17 | 17 | package org.thingsboard.server.service.script; |
18 | 18 | |
19 | -import javax.script.ScriptException; | |
19 | +import com.google.common.util.concurrent.ListenableFuture; | |
20 | + | |
21 | +import java.util.UUID; | |
20 | 22 | |
21 | 23 | public interface JsSandboxService { |
22 | 24 | |
23 | - Object eval(String js) throws ScriptException; | |
25 | + ListenableFuture<UUID> eval(JsScriptType scriptType, String scriptBody, String... argNames); | |
26 | + | |
27 | + ListenableFuture<Object> invokeFunction(UUID scriptId, Object... args); | |
24 | 28 | |
25 | - Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException; | |
29 | + ListenableFuture<Void> release(UUID scriptId); | |
26 | 30 | |
27 | 31 | } | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2018 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package org.thingsboard.server.service.script; | |
18 | + | |
19 | +public enum JsScriptType { | |
20 | + RULE_NODE_SCRIPT | |
21 | +} | ... | ... |
... | ... | @@ -16,21 +16,13 @@ |
16 | 16 | |
17 | 17 | package org.thingsboard.server.service.script; |
18 | 18 | |
19 | -import delight.nashornsandbox.NashornSandbox; | |
20 | -import delight.nashornsandbox.NashornSandboxes; | |
21 | 19 | import lombok.extern.slf4j.Slf4j; |
22 | 20 | import org.springframework.beans.factory.annotation.Value; |
23 | 21 | import org.springframework.stereotype.Service; |
24 | 22 | |
25 | -import javax.annotation.PostConstruct; | |
26 | -import javax.annotation.PreDestroy; | |
27 | -import javax.script.ScriptException; | |
28 | -import java.util.concurrent.ExecutorService; | |
29 | -import java.util.concurrent.Executors; | |
30 | - | |
31 | 23 | @Slf4j |
32 | 24 | @Service |
33 | -public class NashornJsSandboxService implements JsSandboxService { | |
25 | +public class NashornJsSandboxService extends AbstractNashornJsSandboxService { | |
34 | 26 | |
35 | 27 | @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}") |
36 | 28 | private int monitorThreadPoolSize; |
... | ... | @@ -38,33 +30,21 @@ public class NashornJsSandboxService implements JsSandboxService { |
38 | 30 | @Value("${actors.rule.js_sandbox.max_cpu_time}") |
39 | 31 | private long maxCpuTime; |
40 | 32 | |
41 | - private NashornSandbox sandbox = NashornSandboxes.create(); | |
42 | - private ExecutorService monitorExecutorService; | |
43 | - | |
44 | - @PostConstruct | |
45 | - public void init() { | |
46 | - monitorExecutorService = Executors.newFixedThreadPool(monitorThreadPoolSize); | |
47 | - sandbox.setExecutor(monitorExecutorService); | |
48 | - sandbox.setMaxCPUTime(maxCpuTime); | |
49 | - sandbox.allowNoBraces(false); | |
50 | - sandbox.setMaxPreparedStatements(30); | |
51 | - } | |
33 | + @Value("${actors.rule.js_sandbox.max_errors}") | |
34 | + private int maxErrors; | |
52 | 35 | |
53 | - @PreDestroy | |
54 | - public void stop() { | |
55 | - if (monitorExecutorService != null) { | |
56 | - monitorExecutorService.shutdownNow(); | |
57 | - } | |
36 | + @Override | |
37 | + protected int getMonitorThreadPoolSize() { | |
38 | + return monitorThreadPoolSize; | |
58 | 39 | } |
59 | 40 | |
60 | 41 | @Override |
61 | - public Object eval(String js) throws ScriptException { | |
62 | - return sandbox.eval(js); | |
42 | + protected long getMaxCpuTime() { | |
43 | + return maxCpuTime; | |
63 | 44 | } |
64 | 45 | |
65 | 46 | @Override |
66 | - public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { | |
67 | - return sandbox.getSandboxedInvocable().invokeFunction(name, args); | |
47 | + protected int getMaxErrors() { | |
48 | + return maxErrors; | |
68 | 49 | } |
69 | - | |
70 | 50 | } | ... | ... |
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
renamed from
application/src/main/java/org/thingsboard/server/service/script/JsScriptEngine.java
... | ... | @@ -28,56 +28,23 @@ import javax.script.ScriptException; |
28 | 28 | import java.util.Collections; |
29 | 29 | import java.util.Map; |
30 | 30 | import java.util.Set; |
31 | +import java.util.UUID; | |
32 | +import java.util.concurrent.ExecutionException; | |
31 | 33 | |
32 | 34 | |
33 | 35 | @Slf4j |
34 | -public class JsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEngine { | |
35 | - | |
36 | - public static final String MSG = "msg"; | |
37 | - public static final String METADATA = "metadata"; | |
38 | - public static final String MSG_TYPE = "msgType"; | |
39 | - | |
40 | - private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " + | |
41 | - " var msg = JSON.parse(msgStr); " + | |
42 | - " var metadata = JSON.parse(metadataStr); " + | |
43 | - " return JSON.stringify(%s(msg, metadata, msgType));" + | |
44 | - " function %s(%s, %s, %s) {"; | |
45 | - private static final String JS_WRAPPER_SUFFIX = "}" + | |
46 | - "\n}"; | |
36 | +public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEngine { | |
47 | 37 | |
48 | 38 | private static final ObjectMapper mapper = new ObjectMapper(); |
49 | -// private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); | |
50 | -// private ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"}); | |
51 | 39 | private final JsSandboxService sandboxService; |
52 | 40 | |
53 | - private final String invokeFunctionName; | |
41 | + private final UUID scriptId; | |
54 | 42 | |
55 | - public JsScriptEngine(JsSandboxService sandboxService, String script, String functionName, String... argNames) { | |
43 | + public RuleNodeJsScriptEngine(JsSandboxService sandboxService, String script, String... argNames) { | |
56 | 44 | this.sandboxService = sandboxService; |
57 | - this.invokeFunctionName = "invokeInternal" + this.hashCode(); | |
58 | - String msgArg; | |
59 | - String metadataArg; | |
60 | - String msgTypeArg; | |
61 | - if (argNames != null && argNames.length == 3) { | |
62 | - msgArg = argNames[0]; | |
63 | - metadataArg = argNames[1]; | |
64 | - msgTypeArg = argNames[2]; | |
65 | - } else { | |
66 | - msgArg = MSG; | |
67 | - metadataArg = METADATA; | |
68 | - msgTypeArg = MSG_TYPE; | |
69 | - } | |
70 | - String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, this.invokeFunctionName, | |
71 | - functionName, functionName, msgArg, metadataArg, msgTypeArg); | |
72 | - compileScript(jsWrapperPrefix + script + JS_WRAPPER_SUFFIX); | |
73 | - } | |
74 | - | |
75 | - private void compileScript(String script) { | |
76 | 45 | try { |
77 | - //engine.eval(script); | |
78 | - sandboxService.eval(script); | |
79 | - } catch (ScriptException e) { | |
80 | - log.warn("Failed to compile JS script: {}", e.getMessage(), e); | |
46 | + this.scriptId = this.sandboxService.eval(JsScriptType.RULE_NODE_SCRIPT, script, argNames).get(); | |
47 | + } catch (Exception e) { | |
81 | 48 | throw new IllegalArgumentException("Can't compile script: " + e.getMessage()); |
82 | 49 | } |
83 | 50 | } |
... | ... | @@ -103,17 +70,17 @@ public class JsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEng |
103 | 70 | String data = null; |
104 | 71 | Map<String, String> metadata = null; |
105 | 72 | String messageType = null; |
106 | - if (msgData.has(MSG)) { | |
107 | - JsonNode msgPayload = msgData.get(MSG); | |
73 | + if (msgData.has(RuleNodeScriptFactory.MSG)) { | |
74 | + JsonNode msgPayload = msgData.get(RuleNodeScriptFactory.MSG); | |
108 | 75 | data = mapper.writeValueAsString(msgPayload); |
109 | 76 | } |
110 | - if (msgData.has(METADATA)) { | |
111 | - JsonNode msgMetadata = msgData.get(METADATA); | |
77 | + if (msgData.has(RuleNodeScriptFactory.METADATA)) { | |
78 | + JsonNode msgMetadata = msgData.get(RuleNodeScriptFactory.METADATA); | |
112 | 79 | metadata = mapper.convertValue(msgMetadata, new TypeReference<Map<String, String>>() { |
113 | 80 | }); |
114 | 81 | } |
115 | - if (msgData.has(MSG_TYPE)) { | |
116 | - messageType = msgData.get(MSG_TYPE).asText(); | |
82 | + if (msgData.has(RuleNodeScriptFactory.MSG_TYPE)) { | |
83 | + messageType = msgData.get(RuleNodeScriptFactory.MSG_TYPE).asText(); | |
117 | 84 | } |
118 | 85 | String newData = data != null ? data : msg.getData(); |
119 | 86 | TbMsgMetaData newMetadata = metadata != null ? new TbMsgMetaData(metadata) : msg.getMetaData().copy(); |
... | ... | @@ -195,18 +162,20 @@ public class JsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEng |
195 | 162 | private JsonNode executeScript(TbMsg msg) throws ScriptException { |
196 | 163 | try { |
197 | 164 | String[] inArgs = prepareArgs(msg); |
198 | - //String eval = ((Invocable)engine).invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString(); | |
199 | - String eval = sandboxService.invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString(); | |
165 | + String eval = sandboxService.invokeFunction(this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString(); | |
200 | 166 | return mapper.readTree(eval); |
201 | - } catch (ScriptException | IllegalArgumentException th) { | |
202 | - throw th; | |
203 | - } catch (Throwable th) { | |
204 | - th.printStackTrace(); | |
205 | - throw new RuntimeException("Failed to execute js script", th); | |
167 | + } catch (ExecutionException e) { | |
168 | + if (e.getCause() instanceof ScriptException) { | |
169 | + throw (ScriptException)e.getCause(); | |
170 | + } else { | |
171 | + throw new ScriptException("Failed to execute js script: " + e.getMessage()); | |
172 | + } | |
173 | + } catch (Exception e) { | |
174 | + throw new ScriptException("Failed to execute js script: " + e.getMessage()); | |
206 | 175 | } |
207 | 176 | } |
208 | 177 | |
209 | 178 | public void destroy() { |
210 | - //engine = null; | |
179 | + sandboxService.release(this.scriptId); | |
211 | 180 | } |
212 | 181 | } | ... | ... |
application/src/main/java/org/thingsboard/server/service/script/RuleNodeScriptFactory.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2018 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +package org.thingsboard.server.service.script; | |
18 | + | |
19 | +public class RuleNodeScriptFactory { | |
20 | + | |
21 | + public static final String MSG = "msg"; | |
22 | + public static final String METADATA = "metadata"; | |
23 | + public static final String MSG_TYPE = "msgType"; | |
24 | + public static final String RULE_NODE_FUNCTION_NAME = "ruleNodeFunc"; | |
25 | + | |
26 | + private static final String JS_WRAPPER_PREFIX_TEMPLATE = "function %s(msgStr, metadataStr, msgType) { " + | |
27 | + " var msg = JSON.parse(msgStr); " + | |
28 | + " var metadata = JSON.parse(metadataStr); " + | |
29 | + " return JSON.stringify(%s(msg, metadata, msgType));" + | |
30 | + " function %s(%s, %s, %s) {"; | |
31 | + private static final String JS_WRAPPER_SUFFIX = "}" + | |
32 | + "\n}"; | |
33 | + | |
34 | + | |
35 | + public static String generateRuleNodeScript(String functionName, String scriptBody, String... argNames) { | |
36 | + String msgArg; | |
37 | + String metadataArg; | |
38 | + String msgTypeArg; | |
39 | + if (argNames != null && argNames.length == 3) { | |
40 | + msgArg = argNames[0]; | |
41 | + metadataArg = argNames[1]; | |
42 | + msgTypeArg = argNames[2]; | |
43 | + } else { | |
44 | + msgArg = MSG; | |
45 | + metadataArg = METADATA; | |
46 | + msgTypeArg = MSG_TYPE; | |
47 | + } | |
48 | + String jsWrapperPrefix = String.format(JS_WRAPPER_PREFIX_TEMPLATE, functionName, | |
49 | + RULE_NODE_FUNCTION_NAME, RULE_NODE_FUNCTION_NAME, msgArg, metadataArg, msgTypeArg); | |
50 | + return jsWrapperPrefix + scriptBody + JS_WRAPPER_SUFFIX; | |
51 | + } | |
52 | + | |
53 | +} | ... | ... |
... | ... | @@ -243,6 +243,8 @@ actors: |
243 | 243 | monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" |
244 | 244 | # Maximum CPU time in milliseconds allowed for script execution |
245 | 245 | max_cpu_time: "${ACTORS_RULE_JS_SANDBOX_MAX_CPU_TIME:100}" |
246 | + # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted | |
247 | + max_errors: "${ACTORS_RULE_JS_SANDBOX_MAX_ERRORS:3}" | |
246 | 248 | chain: |
247 | 249 | # Errors for particular actor are persisted once per specified amount of milliseconds |
248 | 250 | error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}" | ... | ... |
application/src/test/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngineTest.java
renamed from
application/src/test/java/org/thingsboard/server/service/script/JsScriptEngineTest.java
... | ... | @@ -27,30 +27,28 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; |
27 | 27 | import javax.script.ScriptException; |
28 | 28 | |
29 | 29 | import java.util.Set; |
30 | -import java.util.concurrent.ExecutorService; | |
31 | -import java.util.concurrent.Executors; | |
32 | 30 | |
33 | 31 | import static org.junit.Assert.*; |
34 | 32 | |
35 | -public class JsScriptEngineTest { | |
33 | +public class RuleNodeJsScriptEngineTest { | |
36 | 34 | |
37 | 35 | private ScriptEngine scriptEngine; |
38 | 36 | private TestNashornJsSandboxService jsSandboxService; |
39 | 37 | |
40 | 38 | @Before |
41 | 39 | public void beforeTest() throws Exception { |
42 | - jsSandboxService = new TestNashornJsSandboxService(1, 100); | |
40 | + jsSandboxService = new TestNashornJsSandboxService(1, 100, 3); | |
43 | 41 | } |
44 | 42 | |
45 | 43 | @After |
46 | 44 | public void afterTest() throws Exception { |
47 | - jsSandboxService.destroy(); | |
45 | + jsSandboxService.stop(); | |
48 | 46 | } |
49 | 47 | |
50 | 48 | @Test |
51 | 49 | public void msgCanBeUpdated() throws ScriptException { |
52 | 50 | String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; |
53 | - scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); | |
51 | + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); | |
54 | 52 | |
55 | 53 | TbMsgMetaData metaData = new TbMsgMetaData(); |
56 | 54 | metaData.putValue("temp", "7"); |
... | ... | @@ -61,12 +59,13 @@ public class JsScriptEngineTest { |
61 | 59 | |
62 | 60 | TbMsg actual = scriptEngine.executeUpdate(msg); |
63 | 61 | assertEquals("70", actual.getMetaData().getValue("temp")); |
62 | + scriptEngine.destroy(); | |
64 | 63 | } |
65 | 64 | |
66 | 65 | @Test |
67 | 66 | public void newAttributesCanBeAddedInMsg() throws ScriptException { |
68 | 67 | String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};"; |
69 | - scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); | |
68 | + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); | |
70 | 69 | TbMsgMetaData metaData = new TbMsgMetaData(); |
71 | 70 | metaData.putValue("temp", "7"); |
72 | 71 | metaData.putValue("humidity", "99"); |
... | ... | @@ -76,12 +75,13 @@ public class JsScriptEngineTest { |
76 | 75 | |
77 | 76 | TbMsg actual = scriptEngine.executeUpdate(msg); |
78 | 77 | assertEquals("94", actual.getMetaData().getValue("newAttr")); |
78 | + scriptEngine.destroy(); | |
79 | 79 | } |
80 | 80 | |
81 | 81 | @Test |
82 | 82 | public void payloadCanBeUpdated() throws ScriptException { |
83 | 83 | String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};"; |
84 | - scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); | |
84 | + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); | |
85 | 85 | TbMsgMetaData metaData = new TbMsgMetaData(); |
86 | 86 | metaData.putValue("temp", "7"); |
87 | 87 | metaData.putValue("humidity", "99"); |
... | ... | @@ -93,12 +93,13 @@ public class JsScriptEngineTest { |
93 | 93 | |
94 | 94 | String expectedJson = "{\"name\":\"Vit\",\"passed\":35,\"bigObj\":{\"prop\":42,\"newProp\":\"Ukraine\"}}"; |
95 | 95 | assertEquals(expectedJson, actual.getData()); |
96 | + scriptEngine.destroy(); | |
96 | 97 | } |
97 | 98 | |
98 | 99 | @Test |
99 | 100 | public void metadataAccessibleForFilter() throws ScriptException { |
100 | 101 | String function = "return metadata.humidity < 15;"; |
101 | - scriptEngine = new JsScriptEngine(jsSandboxService, function, "Filter"); | |
102 | + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); | |
102 | 103 | TbMsgMetaData metaData = new TbMsgMetaData(); |
103 | 104 | metaData.putValue("temp", "7"); |
104 | 105 | metaData.putValue("humidity", "99"); |
... | ... | @@ -106,12 +107,13 @@ public class JsScriptEngineTest { |
106 | 107 | |
107 | 108 | TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L); |
108 | 109 | assertFalse(scriptEngine.executeFilter(msg)); |
110 | + scriptEngine.destroy(); | |
109 | 111 | } |
110 | 112 | |
111 | 113 | @Test |
112 | 114 | public void dataAccessibleForFilter() throws ScriptException { |
113 | 115 | String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;"; |
114 | - scriptEngine = new JsScriptEngine(jsSandboxService, function, "Filter"); | |
116 | + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, function); | |
115 | 117 | TbMsgMetaData metaData = new TbMsgMetaData(); |
116 | 118 | metaData.putValue("temp", "7"); |
117 | 119 | metaData.putValue("humidity", "99"); |
... | ... | @@ -119,6 +121,7 @@ public class JsScriptEngineTest { |
119 | 121 | |
120 | 122 | TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L); |
121 | 123 | assertTrue(scriptEngine.executeFilter(msg)); |
124 | + scriptEngine.destroy(); | |
122 | 125 | } |
123 | 126 | |
124 | 127 | @Test |
... | ... | @@ -131,7 +134,7 @@ public class JsScriptEngineTest { |
131 | 134 | "};\n" + |
132 | 135 | "\n" + |
133 | 136 | "return nextRelation(metadata, msg);"; |
134 | - scriptEngine = new JsScriptEngine(jsSandboxService, jsCode, "Switch"); | |
137 | + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode); | |
135 | 138 | TbMsgMetaData metaData = new TbMsgMetaData(); |
136 | 139 | metaData.putValue("temp", "10"); |
137 | 140 | metaData.putValue("humidity", "99"); |
... | ... | @@ -140,6 +143,7 @@ public class JsScriptEngineTest { |
140 | 143 | TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L); |
141 | 144 | Set<String> actual = scriptEngine.executeSwitch(msg); |
142 | 145 | assertEquals(Sets.newHashSet("one"), actual); |
146 | + scriptEngine.destroy(); | |
143 | 147 | } |
144 | 148 | |
145 | 149 | @Test |
... | ... | @@ -152,7 +156,7 @@ public class JsScriptEngineTest { |
152 | 156 | "};\n" + |
153 | 157 | "\n" + |
154 | 158 | "return nextRelation(metadata, msg);"; |
155 | - scriptEngine = new JsScriptEngine(jsSandboxService, jsCode, "Switch"); | |
159 | + scriptEngine = new RuleNodeJsScriptEngine(jsSandboxService, jsCode); | |
156 | 160 | TbMsgMetaData metaData = new TbMsgMetaData(); |
157 | 161 | metaData.putValue("temp", "10"); |
158 | 162 | metaData.putValue("humidity", "99"); |
... | ... | @@ -161,6 +165,7 @@ public class JsScriptEngineTest { |
161 | 165 | TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", null, metaData, rawJson, null, null, 0L); |
162 | 166 | Set<String> actual = scriptEngine.executeSwitch(msg); |
163 | 167 | assertEquals(Sets.newHashSet("one", "three"), actual); |
168 | + scriptEngine.destroy(); | |
164 | 169 | } |
165 | 170 | |
166 | 171 | } |
\ No newline at end of file | ... | ... |
... | ... | @@ -16,39 +16,40 @@ |
16 | 16 | |
17 | 17 | package org.thingsboard.server.service.script; |
18 | 18 | |
19 | +import com.google.common.util.concurrent.ListenableFuture; | |
19 | 20 | import delight.nashornsandbox.NashornSandbox; |
20 | 21 | import delight.nashornsandbox.NashornSandboxes; |
21 | 22 | |
22 | 23 | import javax.script.ScriptException; |
24 | +import java.util.UUID; | |
23 | 25 | import java.util.concurrent.ExecutorService; |
24 | 26 | import java.util.concurrent.Executors; |
25 | 27 | |
26 | -public class TestNashornJsSandboxService implements JsSandboxService { | |
28 | +public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService { | |
27 | 29 | |
28 | - private NashornSandbox sandbox = NashornSandboxes.create(); | |
29 | - private ExecutorService monitorExecutorService; | |
30 | + private final int monitorThreadPoolSize; | |
31 | + private final long maxCpuTime; | |
32 | + private final int maxErrors; | |
30 | 33 | |
31 | - public TestNashornJsSandboxService(int monitorThreadPoolSize, long maxCpuTime) { | |
32 | - monitorExecutorService = Executors.newFixedThreadPool(monitorThreadPoolSize); | |
33 | - sandbox.setExecutor(monitorExecutorService); | |
34 | - sandbox.setMaxCPUTime(maxCpuTime); | |
35 | - sandbox.allowNoBraces(false); | |
36 | - sandbox.setMaxPreparedStatements(30); | |
34 | + public TestNashornJsSandboxService(int monitorThreadPoolSize, long maxCpuTime, int maxErrors) { | |
35 | + this.monitorThreadPoolSize = monitorThreadPoolSize; | |
36 | + this.maxCpuTime = maxCpuTime; | |
37 | + this.maxErrors = maxErrors; | |
38 | + init(); | |
37 | 39 | } |
38 | 40 | |
39 | 41 | @Override |
40 | - public Object eval(String js) throws ScriptException { | |
41 | - return sandbox.eval(js); | |
42 | + protected int getMonitorThreadPoolSize() { | |
43 | + return monitorThreadPoolSize; | |
42 | 44 | } |
43 | 45 | |
44 | 46 | @Override |
45 | - public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { | |
46 | - return sandbox.getSandboxedInvocable().invokeFunction(name, args); | |
47 | + protected long getMaxCpuTime() { | |
48 | + return maxCpuTime; | |
47 | 49 | } |
48 | 50 | |
49 | - public void destroy() { | |
50 | - if (monitorExecutorService != null) { | |
51 | - monitorExecutorService.shutdownNow(); | |
52 | - } | |
51 | + @Override | |
52 | + protected int getMaxErrors() { | |
53 | + return maxErrors; | |
53 | 54 | } |
54 | 55 | } | ... | ... |
... | ... | @@ -43,7 +43,7 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura |
43 | 43 | @Override |
44 | 44 | public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
45 | 45 | this.config = loadAlarmNodeConfig(configuration); |
46 | - this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details"); | |
46 | + this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs()); | |
47 | 47 | } |
48 | 48 | |
49 | 49 | protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException; | ... | ... |
... | ... | @@ -46,7 +46,7 @@ public class TbLogNode implements TbNode { |
46 | 46 | @Override |
47 | 47 | public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
48 | 48 | this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class); |
49 | - this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "ToString"); | |
49 | + this.jsEngine = ctx.createJsScriptEngine(config.getJsScript()); | |
50 | 50 | } |
51 | 51 | |
52 | 52 | @Override | ... | ... |
... | ... | @@ -65,7 +65,7 @@ public class TbMsgGeneratorNode implements TbNode { |
65 | 65 | } else { |
66 | 66 | originatorId = ctx.getSelfId(); |
67 | 67 | } |
68 | - this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Generate", "prevMsg", "prevMetadata", "prevMsgType"); | |
68 | + this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType"); | |
69 | 69 | sentTickMsg(ctx); |
70 | 70 | } |
71 | 71 | ... | ... |
... | ... | @@ -45,7 +45,7 @@ public class TbJsFilterNode implements TbNode { |
45 | 45 | @Override |
46 | 46 | public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
47 | 47 | this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); |
48 | - this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Filter"); | |
48 | + this.jsEngine = ctx.createJsScriptEngine(config.getJsScript()); | |
49 | 49 | } |
50 | 50 | |
51 | 51 | @Override | ... | ... |
... | ... | @@ -47,7 +47,7 @@ public class TbJsSwitchNode implements TbNode { |
47 | 47 | @Override |
48 | 48 | public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
49 | 49 | this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); |
50 | - this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Switch"); | |
50 | + this.jsEngine = ctx.createJsScriptEngine(config.getJsScript()); | |
51 | 51 | } |
52 | 52 | |
53 | 53 | @Override | ... | ... |
... | ... | @@ -43,7 +43,7 @@ public class TbTransformMsgNode extends TbAbstractTransformNode { |
43 | 43 | @Override |
44 | 44 | public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
45 | 45 | this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class); |
46 | - this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "Transform"); | |
46 | + this.jsEngine = ctx.createJsScriptEngine(config.getJsScript()); | |
47 | 47 | setConfig(config); |
48 | 48 | } |
49 | 49 | ... | ... |
... | ... | @@ -152,7 +152,7 @@ public class TbAlarmNodeTest { |
152 | 152 | |
153 | 153 | verifyError(msg, "message", NotImplementedException.class); |
154 | 154 | |
155 | - verify(ctx).createJsScriptEngine("DETAILS", "Details"); | |
155 | + verify(ctx).createJsScriptEngine("DETAILS"); | |
156 | 156 | verify(ctx, times(1)).getJsExecutor(); |
157 | 157 | verify(ctx).getAlarmService(); |
158 | 158 | verify(ctx, times(2)).getDbCallbackExecutor(); |
... | ... | @@ -314,7 +314,7 @@ public class TbAlarmNodeTest { |
314 | 314 | ObjectMapper mapper = new ObjectMapper(); |
315 | 315 | TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); |
316 | 316 | |
317 | - when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs); | |
317 | + when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); | |
318 | 318 | |
319 | 319 | when(ctx.getTenantId()).thenReturn(tenantId); |
320 | 320 | when(ctx.getJsExecutor()).thenReturn(executor); |
... | ... | @@ -338,7 +338,7 @@ public class TbAlarmNodeTest { |
338 | 338 | ObjectMapper mapper = new ObjectMapper(); |
339 | 339 | TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); |
340 | 340 | |
341 | - when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs); | |
341 | + when(ctx.createJsScriptEngine("DETAILS")).thenReturn(detailsJs); | |
342 | 342 | |
343 | 343 | when(ctx.getTenantId()).thenReturn(tenantId); |
344 | 344 | when(ctx.getJsExecutor()).thenReturn(executor); | ... | ... |
... | ... | @@ -97,7 +97,7 @@ public class TbJsFilterNodeTest { |
97 | 97 | ObjectMapper mapper = new ObjectMapper(); |
98 | 98 | TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); |
99 | 99 | |
100 | - when(ctx.createJsScriptEngine("scr", "Filter")).thenReturn(scriptEngine); | |
100 | + when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine); | |
101 | 101 | |
102 | 102 | node = new TbJsFilterNode(); |
103 | 103 | node.init(ctx, nodeConfiguration); | ... | ... |
... | ... | @@ -79,7 +79,7 @@ public class TbJsSwitchNodeTest { |
79 | 79 | ObjectMapper mapper = new ObjectMapper(); |
80 | 80 | TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); |
81 | 81 | |
82 | - when(ctx.createJsScriptEngine("scr", "Switch")).thenReturn(scriptEngine); | |
82 | + when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine); | |
83 | 83 | |
84 | 84 | node = new TbJsSwitchNode(); |
85 | 85 | node.init(ctx, nodeConfiguration); | ... | ... |
... | ... | @@ -97,7 +97,7 @@ public class TbTransformMsgNodeTest { |
97 | 97 | ObjectMapper mapper = new ObjectMapper(); |
98 | 98 | TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); |
99 | 99 | |
100 | - when(ctx.createJsScriptEngine("scr", "Transform")).thenReturn(scriptEngine); | |
100 | + when(ctx.createJsScriptEngine("scr")).thenReturn(scriptEngine); | |
101 | 101 | |
102 | 102 | node = new TbTransformMsgNode(); |
103 | 103 | node.init(ctx, nodeConfiguration); | ... | ... |