22 changed files with 302 additions and 136 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue