Skip to content

Commit 94295ca

Browse files
authored
[jsscripting] Implement debugger support (#20440)
* [jsscripting] Move ModuleLocator & ScriptExtensionModuleProvider into scope sub-package Signed-off-by: Florian Hotze <dev@florianhotze.com>
1 parent 41ef321 commit 94295ca

10 files changed

Lines changed: 237 additions & 27 deletions

File tree

bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77
<feature>openhab-runtime-base</feature>
88
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.js.js-language/25.0.1</bundle>
99
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.js.js-scriptengine/25.0.1</bundle>
10-
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.regex.regex/25.0.1</bundle>
1110
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.polyglot.polyglot/25.0.1</bundle>
11+
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.regex.regex/25.0.1</bundle>
1212
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.collections/25.0.1</bundle>
1313
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.jniutils/25.0.1</bundle>
1414
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.nativeimage/25.0.1</bundle>
1515
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.sdk.word/25.0.1</bundle>
1616
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.icu4j/25.0.1</bundle>
17+
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.json/25.0.1</bundle>
1718
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/25.0.1</bundle>
19+
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.tools.chromeinspector-tool/25.0.1</bundle>
20+
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.tools.profiler-tool/25.0.1</bundle>
1821
<bundle dependency="true" start-level="79">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-api/25.0.1</bundle>
1922
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/25.0.1</bundle>
2023
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/25.0.1</bundle>

bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineConfiguration.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,22 @@ public class GraalJSScriptEngineConfiguration {
3333
private static final String CFG_SCRIPT_CONDITION_WRAPPER_ENABLED = "scriptConditionWrapperEnabled";
3434
private static final String CFG_EVENT_CONVERSION_ENABLED = "eventConversionEnabled";
3535
private static final String CFG_DEPENDENCY_TRACKING_ENABLED = "dependencyTrackingEnabled";
36+
private static final String CFG_DEBUGGER_ENABLED = "debuggerEnabled";
37+
private static final String CFG_DEBUGGER_PORT = "debuggerPort";
3638

3739
private static final int INJECTION_ENABLED_FOR_SCRIPT_MODULES_ONLY = 1;
3840
private static final int INJECTION_ENABLED_FOR_SCRIPT_MODULES_AND_TRANSFORMATIONS = 2;
3941
private static final int INJECTION_ENABLED_FOR_ALL_SCRIPTS = 3;
4042

43+
private static final int DEBUGGER_PORT_DEFAULT = 9229;
44+
4145
private int injectionEnabled = INJECTION_ENABLED_FOR_ALL_SCRIPTS;
4246
private boolean injectionCachingEnabled = true;
4347
private boolean scriptConditionWrapperEnabled = false;
4448
private boolean eventConversionEnabled = true;
4549
private boolean dependencyTrackingEnabled = true;
50+
private boolean debuggerEnabled = false;
51+
private int debuggerPort = DEBUGGER_PORT_DEFAULT;
4652

4753
/**
4854
* Create a new configuration instance from the given parameters.
@@ -63,6 +69,8 @@ void modified(Map<String, ?> config) {
6369
boolean oldDependencyTrackingEnabled = dependencyTrackingEnabled;
6470
boolean oldScriptConditionWrapperEnabled = scriptConditionWrapperEnabled;
6571
boolean oldEventConversionEnabled = eventConversionEnabled;
72+
boolean oldDebuggerEnabled = debuggerEnabled;
73+
int oldDebuggerPort = debuggerPort;
6674

6775
this.update(config);
6876

@@ -91,6 +99,12 @@ void modified(Map<String, ?> config) {
9199
"Disabled event conversion for JavaScript Scripting. Please resave your scripts to apply this change.");
92100
}
93101
}
102+
if (oldDebuggerEnabled != debuggerEnabled) {
103+
logger.warn("{} debugger for JavaScript Scripting. Restart openHAB to apply this change.",
104+
debuggerEnabled ? "Enabled" : "Disabled");
105+
} else if (oldDebuggerPort != debuggerPort) {
106+
logger.warn("Reconfigured debugger for JavaScript Scripting. Restart openHAB to apply this change.");
107+
}
94108
}
95109

96110
/**
@@ -111,6 +125,8 @@ private void update(Map<String, ?> config) {
111125
true);
112126
dependencyTrackingEnabled = ConfigParser.valueAsOrElse(config.get(CFG_DEPENDENCY_TRACKING_ENABLED),
113127
Boolean.class, true);
128+
debuggerEnabled = ConfigParser.valueAsOrElse(config.get(CFG_DEBUGGER_ENABLED), Boolean.class, false);
129+
debuggerPort = ConfigParser.valueAsOrElse(config.get(CFG_DEBUGGER_PORT), Integer.class, DEBUGGER_PORT_DEFAULT);
114130
}
115131

116132
/**
@@ -163,4 +179,12 @@ public boolean isEventConversionEnabled() {
163179
public boolean isDependencyTrackingEnabled() {
164180
return dependencyTrackingEnabled;
165181
}
182+
183+
public boolean isDebuggerEnabled() {
184+
return debuggerEnabled;
185+
}
186+
187+
public int getDebuggerPort() {
188+
return debuggerPort;
189+
}
166190
}

bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,34 @@
2121

2222
import org.eclipse.jdt.annotation.NonNullByDefault;
2323
import org.eclipse.jdt.annotation.Nullable;
24+
import org.graalvm.polyglot.Engine;
25+
import org.graalvm.polyglot.Language;
2426
import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
27+
import org.openhab.automation.jsscripting.internal.util.ThreadLocalSlf4jOutputStream;
2528
import org.openhab.core.OpenHAB;
2629
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
2730
import org.openhab.core.automation.module.script.ScriptEngineFactory;
2831
import org.openhab.core.config.core.ConfigurableService;
2932
import org.osgi.framework.Constants;
3033
import org.osgi.service.component.annotations.Activate;
3134
import org.osgi.service.component.annotations.Component;
35+
import org.osgi.service.component.annotations.Deactivate;
3236
import org.osgi.service.component.annotations.Modified;
3337
import org.osgi.service.component.annotations.Reference;
3438
import org.slf4j.Logger;
3539
import org.slf4j.LoggerFactory;
40+
import org.slf4j.event.Level;
3641

3742
/**
3843
* An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
3944
*
4045
* @author Jonathan Gilbert - Initial contribution
4146
* @author Dan Cunningham - Script injections
47+
* @author Florian Hotze - Debugger support
4248
*/
4349
@Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.jsscripting", property = Constants.SERVICE_PID
4450
+ "=org.openhab.jsscripting")
45-
@ConfigurableService(category = "automation", label = "JS Scripting", description_uri = "automation:jsscripting")
51+
@ConfigurableService(category = "automation", label = "JavaScript Scripting", description_uri = "automation:jsscripting")
4652
@NonNullByDefault
4753
public class GraalJSScriptEngineFactory implements ScriptEngineFactory {
4854
public static final Path JS_DEFAULT_PATH = Paths.get(OpenHAB.getConfigFolder(), "automation", "js");
@@ -60,6 +66,10 @@ public class GraalJSScriptEngineFactory implements ScriptEngineFactory {
6066

6167
private final Logger logger = LoggerFactory.getLogger(GraalJSScriptEngineFactory.class);
6268
private final GraalJSScriptEngineConfiguration configuration;
69+
/**
70+
* Shared Polyglot {@link Engine} instance to be used by all instances of {@link OpenhabGraalJSScriptEngine}.
71+
*/
72+
private final Engine engine;
6373

6474
private final JSScriptServiceUtil jsScriptServiceUtil;
6575
private final JSDependencyTracker jsDependencyTracker;
@@ -73,11 +83,51 @@ public GraalJSScriptEngineFactory(final @Reference JSScriptServiceUtil jsScriptS
7383
this.jsScriptServiceUtil = jsScriptServiceUtil;
7484
this.configuration = new GraalJSScriptEngineConfiguration(config);
7585

76-
if (OpenhabGraalJSScriptEngine.getLanguage() == null) {
86+
if (configuration.isDebuggerEnabled()) {
87+
Engine.Builder engineBuilder = createEngineBuilder();
88+
engineBuilder //
89+
.option("inspect", "0.0.0.0:" + configuration.getDebuggerPort()) //
90+
.option("inspect.Suspend", "false") // Don't pause at startup waiting for debugger to attach
91+
.option("inspect.WaitAttached", "false") // Don't block code execution waiting for debugger to
92+
// attach
93+
.option("inspect.Secure", "false"); // Disable TLS
94+
Engine engine;
95+
try {
96+
engine = engineBuilder.build();
97+
} catch (RuntimeException e) {
98+
logger.error(
99+
"Failed to initialize Graal JavaScript engine with debugger support. Continuing without debugger support.",
100+
e);
101+
engine = createEngineBuilder().build();
102+
}
103+
logger.info("Debugger support is enabled for JavaScript Scripting.");
104+
this.engine = engine;
105+
} else {
106+
this.engine = createEngineBuilder().build();
107+
}
108+
109+
if (getLanguage() == null) {
77110
logger.error(LANG_NOT_INITIALIZED_MSG);
78111
}
79112
}
80113

114+
private Engine.Builder createEngineBuilder() {
115+
Logger engineLogger = LoggerFactory
116+
.getLogger(GraalJSScriptEngineFactory.class.getPackageName() + ".org.graalvm.polyglot.Engine");
117+
return Engine.newBuilder().allowExperimentalOptions(true) //
118+
.option("engine.WarnInterpreterOnly", "false") //
119+
.out(new ThreadLocalSlf4jOutputStream(engineLogger, Level.DEBUG)) //
120+
// Note: Due to a bug in GraalVM, info messages are logged to the err stream, so hide it until the fix
121+
// is available. FTR: https://github.com/oracle/graal/issues/13222
122+
// TODO: Increase level to WARN when upgrading GraalVM
123+
.err(new ThreadLocalSlf4jOutputStream(engineLogger, Level.DEBUG));
124+
}
125+
126+
@Deactivate
127+
public void dispose() {
128+
this.engine.close();
129+
}
130+
81131
@Modified
82132
protected void modified(Map<String, ?> config) {
83133
configuration.modified(config);
@@ -98,16 +148,25 @@ public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValu
98148
if (!SCRIPT_TYPES.contains(scriptType)) {
99149
return null;
100150
}
101-
if (OpenhabGraalJSScriptEngine.getLanguage() == null) {
151+
if (getLanguage() == null) {
102152
logger.error(LANG_NOT_INITIALIZED_MSG);
103153
return null;
104154
}
105155
return new DebuggingGraalScriptEngine<>(
106-
new OpenhabGraalJSScriptEngine(configuration, jsScriptServiceUtil, jsDependencyTracker));
156+
new OpenhabGraalJSScriptEngine(configuration, engine, jsScriptServiceUtil, jsDependencyTracker));
107157
}
108158

109159
@Override
110160
public @Nullable ScriptDependencyTracker getDependencyTracker() {
111161
return jsDependencyTracker;
112162
}
163+
164+
/**
165+
* Gets the Graal language of {@link OpenhabGraalJSScriptEngine}.
166+
*
167+
* @return the Graal language of {@link OpenhabGraalJSScriptEngine} or {@code null} if not available
168+
*/
169+
private @Nullable Language getLanguage() {
170+
return engine.getLanguages().get(OpenhabGraalJSScriptEngine.LANGUAGE_ID);
171+
}
113172
}

bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,17 @@
4949
import org.graalvm.polyglot.Context;
5050
import org.graalvm.polyglot.Engine;
5151
import org.graalvm.polyglot.HostAccess;
52-
import org.graalvm.polyglot.Language;
5352
import org.graalvm.polyglot.Source;
5453
import org.graalvm.polyglot.Value;
5554
import org.graalvm.polyglot.io.IOAccess;
5655
import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
5756
import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
5857
import org.openhab.automation.jsscripting.internal.fs.ReadOnlySeekableByteArrayChannel;
5958
import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
59+
import org.openhab.automation.jsscripting.internal.scope.ScriptExtensionModuleProvider;
6060
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndCompilableAndAutoCloseable;
6161
import org.openhab.automation.jsscripting.internal.scriptengine.helper.LifecycleTracker;
62+
import org.openhab.automation.jsscripting.internal.util.Slf4jOutputStream;
6263
import org.openhab.core.OpenHAB;
6364
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
6465
import org.openhab.core.automation.module.script.internal.handler.AbstractScriptModuleHandler;
@@ -68,6 +69,7 @@
6869
import org.openhab.core.library.types.QuantityType;
6970
import org.slf4j.Logger;
7071
import org.slf4j.LoggerFactory;
72+
import org.slf4j.event.Level;
7173

7274
import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
7375

@@ -85,7 +87,7 @@ public class OpenhabGraalJSScriptEngine
8587
implements Lock {
8688

8789
// see private constant GraalJSScriptEngine.ID
88-
private static final String LANGUAGE_ID = "js";
90+
static final String LANGUAGE_ID = "js";
8991

9092
private static final Source GLOBAL_SOURCE;
9193
static {
@@ -124,9 +126,6 @@ public class OpenhabGraalJSScriptEngine
124126
File cachePath = Path.of(OpenHAB.getUserDataFolder(), "cache", "org.graalvm.polyglot").toFile();
125127
System.setProperty("polyglot.engine.userResourceCache", cachePath.getAbsolutePath());
126128
}
127-
/** Shared Polyglot {@link Engine} across all instances of {@link OpenhabGraalJSScriptEngine} */
128-
private static final Engine ENGINE = Engine.newBuilder().allowExperimentalOptions(true)
129-
.option("engine.WarnInterpreterOnly", "false").build();
130129
/** Provides unlimited host access as well as custom translations from JS to Java Objects */
131130
private static final HostAccess HOST_ACCESS = HostAccess.newBuilder(HostAccess.ALL)
132131
// Translate JS-Joda ZonedDateTime to java.time.ZonedDateTime
@@ -175,13 +174,15 @@ public class OpenhabGraalJSScriptEngine
175174
* Creates an implementation of ScriptEngine {@code (& Invocable)}, wrapping the contained engine,
176175
* that tracks the script lifecycle and provides hooks for scripts to do so too.
177176
*/
178-
public OpenhabGraalJSScriptEngine(GraalJSScriptEngineConfiguration configuration,
177+
public OpenhabGraalJSScriptEngine(GraalJSScriptEngineConfiguration configuration, Engine engine,
179178
JSScriptServiceUtil jsScriptServiceUtil, JSDependencyTracker jsDependencyTracker) {
180179
super(null); // delegate depends on fields not yet initialized, so we cannot set it immediately
181180
this.configuration = configuration;
182181
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
183182

184-
delegate = GraalJSScriptEngine.create(ENGINE, Context.newBuilder(LANGUAGE_ID) //
183+
Logger contextLogger = LoggerFactory
184+
.getLogger(OpenhabGraalJSScriptEngine.class.getPackageName() + ".org.graalvm.polyglot.Context");
185+
delegate = GraalJSScriptEngine.create(engine, Context.newBuilder(LANGUAGE_ID) //
185186
.allowIO(IOAccess.newBuilder() //
186187
.fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
187188
@Override
@@ -256,6 +257,9 @@ public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException
256257
.hostClassLoader(getClass().getClassLoader()) //
257258
// allow experimental options
258259
.allowExperimentalOptions(true) //
260+
// redirect context's out/err streams to SLF4J
261+
.out(new Slf4jOutputStream(contextLogger, Level.DEBUG)) //
262+
.err(new Slf4jOutputStream(contextLogger, Level.WARN)) //
259263
// choose the path to look for CommonJS module (i.e. node_modules)
260264
.option("js.commonjs-require-cwd", jsDependencyTracker.getLibraryPath().toString()) //
261265
// enable Nashorn compat mode as openhab-js relies on accessors, see
@@ -602,13 +606,4 @@ public void unlock() {
602606
public Condition newCondition() {
603607
return lock.newCondition();
604608
}
605-
606-
/**
607-
* Gets the Graal language of {@link OpenhabGraalJSScriptEngine}.
608-
*
609-
* @return the Graal language of {@link OpenhabGraalJSScriptEngine} or {@code null} if not available
610-
*/
611-
public static @Nullable Language getLanguage() {
612-
return ENGINE.getLanguages().get(LANGUAGE_ID);
613-
}
614609
}

bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java renamed to bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ModuleLocator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*
1111
* SPDX-License-Identifier: EPL-2.0
1212
*/
13-
package org.openhab.automation.jsscripting.internal;
13+
package org.openhab.automation.jsscripting.internal.scope;
1414

1515
import java.util.Optional;
1616

bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java renamed to bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptExtensionModuleProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*
1111
* SPDX-License-Identifier: EPL-2.0
1212
*/
13-
package org.openhab.automation.jsscripting.internal;
13+
package org.openhab.automation.jsscripting.internal.scope;
1414

1515
import java.io.IOException;
1616
import java.util.HashMap;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.automation.jsscripting.internal.util;
14+
15+
import java.io.ByteArrayOutputStream;
16+
import java.io.IOException;
17+
import java.io.OutputStream;
18+
19+
import org.eclipse.jdt.annotation.NonNullByDefault;
20+
import org.slf4j.Logger;
21+
import org.slf4j.event.Level;
22+
23+
/**
24+
* A {@link OutputStream} implementation that redirects to SLF4J logging.
25+
*
26+
* @author Florian Hotze - Initial contribution
27+
*/
28+
@NonNullByDefault
29+
public class Slf4jOutputStream extends OutputStream {
30+
private final Logger logger;
31+
private final Level level;
32+
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
33+
34+
public Slf4jOutputStream(Logger logger, Level level) {
35+
this.logger = logger;
36+
this.level = level;
37+
}
38+
39+
protected ByteArrayOutputStream getBuffer() {
40+
return buffer;
41+
}
42+
43+
@Override
44+
public void write(int b) {
45+
if (b == '\n') {
46+
flush();
47+
} else {
48+
getBuffer().write(b);
49+
}
50+
}
51+
52+
@Override
53+
public void flush() {
54+
var buffer = getBuffer();
55+
if (buffer.size() > 0) {
56+
logger.atLevel(level).log(buffer.toString());
57+
buffer.reset();
58+
}
59+
}
60+
61+
@Override
62+
public void close() throws IOException {
63+
flush();
64+
getBuffer().close();
65+
super.close();
66+
}
67+
}

0 commit comments

Comments
 (0)