Skip to content

Commit 0124dc7

Browse files
authored
[pythonscripting] Implement debugger support (#20443)
* add python scripting debugger support Signed-off-by: Holger Hees <holger.hees@gmail.com>
1 parent aa474db commit 0124dc7

11 files changed

Lines changed: 200 additions & 100 deletions

File tree

bundles/org.openhab.automation.pythonscripting/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ If you use the marketplace version of this Add-on, it is necessary to use the co
117117
# For tips and instructions, please refer to <a href="https://www.graalvm.org/latest/reference-manual/python/Modern-Python-on-JVM">Jython Migration Guide</a>.
118118
#
119119
#org.openhab.automation.pythonscripting:jythonEmulation = false
120+
121+
# Enable debugger
122+
#
123+
# Enables Chrome Debugger support for Python Scripting. This allows you to attach any debugger compatible with the
124+
# <a href="https://chromedevtools.github.io/devtools-protocol/">Chrome DevTools Protocol</a>, such as Chrome's Developer Tools or Visual Studio Code.
125+
#
126+
#org.openhab.automation.pythonscripting:debuggerEnabled = false
127+
128+
# Debugger port
129+
#
130+
# The port to bind the debugger to.
131+
#
132+
#org.openhab.automation.pythonscripting:debuggerPort = 9230
120133
```
121134

122135
### Console

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.llvm.llvm-api/25.0.1</bundle>
1313
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.json/25.0.1</bundle>
1414
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.shadowed.xz/25.0.1</bundle>
15-
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.tools.profiler-tool/25.0.1</bundle>
1615
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.polyglot.polyglot/25.0.1</bundle>
1716
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-runtime/25.0.1</bundle>
1817
<bundle dependency="true">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-compiler/25.0.1</bundle>
1918
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.regex.regex/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>
2021
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.python.python-resources/25.0.1</bundle>
2122
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.python.python-language/25.0.1</bundle>
2223
<bundle dependency="true" start-level="78">mvn:org.openhab.osgiify/org.graalvm.truffle.truffle-nfi/25.0.1</bundle>

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/PythonScriptEngine.java

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,13 @@
4545
import org.graalvm.polyglot.Context;
4646
import org.graalvm.polyglot.Engine;
4747
import org.graalvm.polyglot.HostAccess;
48-
import org.graalvm.polyglot.Language;
4948
import org.graalvm.polyglot.PolyglotException;
5049
import org.graalvm.polyglot.Source;
5150
import org.graalvm.polyglot.Value;
5251
import org.graalvm.polyglot.io.IOAccess;
5352
import org.openhab.automation.pythonscripting.internal.context.ContextInput;
5453
import org.openhab.automation.pythonscripting.internal.context.ContextOutput;
55-
import org.openhab.automation.pythonscripting.internal.context.ContextOutputLogger;
54+
import org.openhab.automation.pythonscripting.internal.context.ThreadLocalContextOutputLogger;
5655
import org.openhab.automation.pythonscripting.internal.fs.DelegatingFileSystem;
5756
import org.openhab.automation.pythonscripting.internal.provider.LifecycleTracker;
5857
import org.openhab.automation.pythonscripting.internal.provider.ScriptExtensionModuleProvider;
@@ -83,10 +82,6 @@ public class PythonScriptEngine extends InvocationInterceptingPythonScriptEngine
8382
public static final String CONTEXT_KEY_ENGINE_LOGGER_INPUT = "ctx.engine-logger-input";
8483
private static final String CONTEXT_KEY_SCRIPT_FILENAME = "javax.script.filename";
8584

86-
private static final String PYTHON_OPTION_ENGINE_WARNINTERPRETERONLY = "engine.WarnInterpreterOnly";
87-
88-
private static final String SYSTEM_PROPERTY_ATTACH_LIBRARY_FAILURE_ACTION = "polyglotimpl.AttachLibraryFailureAction";
89-
9085
private static final String PYTHON_OPTION_PYTHONPATH = "python.PythonPath";
9186
private static final String PYTHON_OPTION_EMULATEJYTHON = "python.EmulateJython";
9287
private static final String PYTHON_OPTION_POSIXMODULEBACKEND = "python.PosixModuleBackend";
@@ -106,16 +101,6 @@ public class PythonScriptEngine extends InvocationInterceptingPythonScriptEngine
106101

107102
private static final String LOGGER_INIT_NAME = "__logger_init__";
108103

109-
/** Shared Polyglot {@link Engine} across all instances of {@link PythonScriptEngine} */
110-
private static Engine engine = Engine.newBuilder()
111-
// disable warning about fallback runtime (is only available in graalvm)
112-
.option(PYTHON_OPTION_ENGINE_WARNINTERPRETERONLY, Boolean.toString(false)).build();
113-
114-
static {
115-
// disable warning about missing TruffleAttach library (is only available in graalvm)
116-
System.getProperties().setProperty(SYSTEM_PROPERTY_ATTACH_LIBRARY_FAILURE_ACTION, "ignore");
117-
}
118-
119104
// private static final boolean isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
120105

121106
/** Provides unlimited host access as well as custom translations from Python to Java Objects */
@@ -166,12 +151,12 @@ public class PythonScriptEngine extends InvocationInterceptingPythonScriptEngine
166151
* Creates an implementation of ScriptEngine {@code (& Invocable)}, wrapping the contained engine,
167152
* that tracks the script lifecycle and provides hooks for scripts to do so too.
168153
*/
169-
public PythonScriptEngine(PythonScriptEngineConfiguration pythonScriptEngineConfiguration,
154+
public PythonScriptEngine(PythonScriptEngineConfiguration pythonScriptEngineConfiguration, Engine engine,
170155
PythonScriptEngineFactory pythonScriptEngineFactory) {
171156
this.pythonScriptEngineConfiguration = pythonScriptEngineConfiguration;
172157

173-
this.scriptOutputStream = new ContextOutput(new ContextOutputLogger(logger, Level.INFO));
174-
this.scriptErrorStream = new ContextOutput(new ContextOutputLogger(logger, Level.ERROR));
158+
this.scriptOutputStream = new ContextOutput(new ThreadLocalContextOutputLogger(logger, Level.INFO));
159+
this.scriptErrorStream = new ContextOutput(new ThreadLocalContextOutputLogger(logger, Level.ERROR));
175160
this.scriptInputStream = new ContextInput(null);
176161

177162
this.lifecycleTracker = new LifecycleTracker();
@@ -510,8 +495,8 @@ private void setScriptLogger() {
510495

511496
Logger scriptLogger = LoggerFactory.getLogger("org.openhab.automation.pythonscripting." + identifier);
512497

513-
scriptOutputStream.setOutputStream(new ContextOutputLogger(scriptLogger, Level.INFO));
514-
scriptErrorStream.setOutputStream(new ContextOutputLogger(scriptLogger, Level.ERROR));
498+
scriptOutputStream.setOutputStream(new ThreadLocalContextOutputLogger(scriptLogger, Level.INFO));
499+
scriptErrorStream.setOutputStream(new ThreadLocalContextOutputLogger(scriptLogger, Level.ERROR));
515500
}
516501

517502
private String stringifyThrowable(Throwable throwable) {
@@ -631,8 +616,4 @@ private static ZonedDateTime parseDatetime(Value value) {
631616
? OffsetDateTime.now().getOffset().getId()
632617
: ""));
633618
}
634-
635-
public static @Nullable Language getLanguage() {
636-
return engine.getLanguages().get(GraalPythonScriptEngine.LANGUAGE_ID);
637-
}
638619
}

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/PythonScriptEngineConfiguration.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public class PythonScriptEngineConfiguration {
4848
private final Logger logger = LoggerFactory.getLogger(PythonScriptEngineConfiguration.class);
4949

5050
private static final String SYSTEM_PROPERTY_POLYGLOT_ENGINE_USERRESOURCECACHE = "polyglot.engine.userResourceCache";
51-
51+
private static final String SYSTEM_PROPERTY_ATTACH_LIBRARY_FAILURE_ACTION = "polyglotimpl.AttachLibraryFailureAction";
5252
private static final String SYSTEM_PROPERTY_JAVA_IO_TMPDIR = "java.io.tmpdir";
5353

5454
public static final String PATH_SEPARATOR = FileSystems.getDefault().getSeparator();
@@ -67,12 +67,16 @@ public class PythonScriptEngineConfiguration {
6767
private static final int INJECTION_ENABLED_FOR_SCRIPT_MODULES_AND_TRANSFORMATIONS = 3;
6868
private static final int INJECTION_ENABLED_FOR_ALL_SCRIPTS = 4;
6969

70+
private static final int DEBUGGER_PORT_DEFAULT = 9230;
71+
7072
// The variable names must match the configuration keys in config.xml
7173
public static class PythonScriptingConfiguration {
7274
public int injectionEnabled = INJECTION_ENABLED_FOR_SCRIPT_MODULES_ONLY;
7375
public boolean dependencyTrackingEnabled = true;
7476
public boolean cachingEnabled = true;
7577
public boolean jythonEmulation = false;
78+
public boolean debuggerEnabled = false;
79+
public int debuggerPort = DEBUGGER_PORT_DEFAULT;
7680
public String pipModules = "";
7781
}
7882

@@ -94,6 +98,11 @@ public static Version parseHelperLibVersion(@Nullable String version) throws Ill
9498
return Version.parse(version != null && version.startsWith("v") ? version.substring(1) : version);
9599
}
96100

101+
static {
102+
// disable warning about missing TruffleAttach library (is only available in graalvm)
103+
System.getProperties().setProperty(SYSTEM_PROPERTY_ATTACH_LIBRARY_FAILURE_ACTION, "ignore");
104+
}
105+
97106
@Activate
98107
public PythonScriptEngineConfiguration(Map<String, Object> config) {
99108
Path userdataDir = Paths.get(OpenHAB.getUserDataFolder());
@@ -123,6 +132,7 @@ public PythonScriptEngineConfiguration(Map<String, Object> config) {
123132
Properties props = System.getProperties();
124133
props.setProperty(SYSTEM_PROPERTY_POLYGLOT_ENGINE_USERRESOURCECACHE,
125134
userdataDir.resolve("cache").resolve("org.graalvm.polyglot").toString());
135+
props.setProperty(SYSTEM_PROPERTY_ATTACH_LIBRARY_FAILURE_ACTION, "ignore");
126136

127137
String packageName = PythonScriptEngineConfiguration.class.getPackageName();
128138
packageName = packageName.substring(0, packageName.lastIndexOf("."));
@@ -153,22 +163,31 @@ public void init(PythonScriptEngineFactory factory) {
153163
public void modified(Map<String, Object> config, PythonScriptEngineFactory factory) {
154164
int oldInjectionEnabled = configuration.injectionEnabled;
155165
boolean oldDependencyTrackingEnabled = isDependencyTrackingEnabled();
156-
157166
String oldPipModules = configuration.pipModules;
167+
boolean oldDebuggerEnabled = configuration.debuggerEnabled;
168+
int oldDebuggerPort = configuration.debuggerPort;
169+
158170
configuration = new Configuration(config).as(PythonScriptingConfiguration.class);
171+
159172
if (!oldPipModules.equals(configuration.pipModules)) {
160173
PythonScriptEngineHelper.initPipModules(this, factory);
161174
}
162175

163176
if (oldInjectionEnabled != configuration.injectionEnabled) {
164-
logger.info(
177+
logger.warn(
165178
"Changed helper module setting for Python Scripting. Please resave your python scripts to apply this change.");
166179
}
167180
if (oldDependencyTrackingEnabled != isDependencyTrackingEnabled()) {
168-
logger.info(
181+
logger.warn(
169182
"{} dependency tracking for Python Scripting. Please resave your python scripts to apply this change.",
170183
isDependencyTrackingEnabled() ? "Enabled" : "Disabled");
171184
}
185+
if (oldDebuggerEnabled != configuration.debuggerEnabled) {
186+
logger.warn("{} debugger for Python Scripting. Restart openHAB to apply this change.",
187+
configuration.debuggerEnabled ? "Enabled" : "Disabled");
188+
} else if (oldDebuggerPort != configuration.debuggerPort) {
189+
logger.warn("Reconfigured debugger for Python Scripting. Restart openHAB to apply this change.");
190+
}
172191
}
173192

174193
public void setHelperLibVersion(Version version) {
@@ -203,6 +222,14 @@ public boolean isJythonEmulation() {
203222
return configuration.jythonEmulation;
204223
}
205224

225+
public boolean isDebuggerEnabled() {
226+
return configuration.debuggerEnabled;
227+
}
228+
229+
public int getDebuggerPort() {
230+
return configuration.debuggerPort;
231+
}
232+
206233
public String getPIPModules() {
207234
return configuration.pipModules;
208235
}

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/PythonScriptEngineFactory.java

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323

2424
import org.eclipse.jdt.annotation.NonNullByDefault;
2525
import org.eclipse.jdt.annotation.Nullable;
26+
import org.graalvm.polyglot.Engine;
2627
import org.graalvm.polyglot.Language;
2728
import org.openhab.automation.pythonscripting.internal.fs.PythonDependencyTracker;
29+
import org.openhab.automation.pythonscripting.internal.scriptengine.graal.GraalPythonScriptEngine;
2830
import org.openhab.automation.pythonscripting.internal.scriptengine.graal.GraalPythonScriptEngine.ScriptEngineProvider;
2931
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
3032
import org.openhab.core.automation.module.script.ScriptEngineFactory;
@@ -60,19 +62,18 @@ public class PythonScriptEngineFactory implements ScriptEngineFactory, ScriptEng
6062
private final PythonDependencyTracker pythonDependencyTracker;
6163
private final PythonScriptEngineConfiguration configuration;
6264

63-
private final @Nullable Language language;
65+
private static final String PYTHON_OPTION_ENGINE_WARNINTERPRETERONLY = "engine.WarnInterpreterOnly";
66+
67+
/**
68+
* Shared Polyglot {@link Engine} instance to be used by all instances of {@link PythonScriptEngine}.
69+
*/
70+
private final Engine engine;
6471

6572
@Activate
6673
public PythonScriptEngineFactory(final @Reference PythonDependencyTracker pythonDependencyTracker,
6774
final @Reference TimeZoneProvider timeZoneProvider, Map<String, Object> config) {
6875
logger.debug("Loading PythonScriptEngineFactory");
6976

70-
this.language = PythonScriptEngine.getLanguage();
71-
if (this.language == null) {
72-
logger.error(
73-
"Graal Python language not initialized. Restart openHAB to initialize available Graal languages properly.");
74-
}
75-
7677
String defaultTimezone = ZoneId.systemDefault().getId();
7778
String providerTimezone = timeZoneProvider.getTimeZone().getId();
7879
if (!defaultTimezone.equals(providerTimezone)) {
@@ -84,6 +85,41 @@ public PythonScriptEngineFactory(final @Reference PythonDependencyTracker python
8485
this.pythonDependencyTracker = pythonDependencyTracker;
8586
this.configuration = new PythonScriptEngineConfiguration(config);
8687
this.configuration.init(this);
88+
89+
Engine.Builder engineBuilder = createEngineBuilder();
90+
if (configuration.isDebuggerEnabled()) {
91+
engineBuilder //
92+
.option("inspect", "0.0.0.0:" + configuration.getDebuggerPort()) //
93+
.option("inspect.Suspend", "false") // Don't pause at startup waiting for debugger to attach
94+
.option("inspect.WaitAttached", "false") // Don't block code execution waiting for debugger to
95+
// attach
96+
.option("inspect.Secure", "false"); // Disable TLS
97+
98+
Engine engine;
99+
try {
100+
engine = engineBuilder.build();
101+
logger.info("Debugger support is enabled for Python Scripting on port {}",
102+
configuration.getDebuggerPort());
103+
} catch (RuntimeException e) {
104+
logger.error(
105+
"Failed to initialize Graal Python engine with debugger support. Continuing without debugger support.",
106+
e);
107+
engine = createEngineBuilder().build();
108+
}
109+
this.engine = engine;
110+
} else {
111+
this.engine = createEngineBuilder().build();
112+
}
113+
114+
if (getLanguage() == null) {
115+
logger.error(
116+
"Graal Python language not initialized. Restart openHAB to initialize available Graal languages properly.");
117+
}
118+
}
119+
120+
private Engine.Builder createEngineBuilder() {
121+
return Engine.newBuilder().allowExperimentalOptions(true) //
122+
.option(PYTHON_OPTION_ENGINE_WARNINTERPRETERONLY, Boolean.toString(false));
87123
}
88124

89125
@Deactivate
@@ -127,10 +163,10 @@ public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValu
127163

128164
@Override
129165
public @Nullable ScriptEngine createScriptEngine() {
130-
if (language == null) {
166+
if (getLanguage() == null) {
131167
return null;
132168
}
133-
return new PythonScriptEngine(configuration, this);
169+
return new PythonScriptEngine(configuration, engine, this);
134170
}
135171

136172
@Override
@@ -141,4 +177,13 @@ public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValu
141177
public PythonScriptEngineConfiguration getConfiguration() {
142178
return this.configuration;
143179
}
180+
181+
/**
182+
* Gets the Graal language of {@link PythonScriptEngine}.
183+
*
184+
* @return the Graal language of {@link PythonScriptEngine} or {@code null} if not available
185+
*/
186+
public @Nullable Language getLanguage() {
187+
return engine.getLanguages().get(GraalPythonScriptEngine.LANGUAGE_ID);
188+
}
144189
}

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/console/PythonConsoleCommandExtension.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ public void execute(String[] args, Console console) {
174174
}
175175

176176
private void info(Console console) {
177-
new InfoCmd(pythonScriptEngineConfiguration, console).show(configDescriptionRegistry);
177+
new InfoCmd(pythonScriptEngineConfiguration, console, this.pythonScriptEngineFactory.getLanguage())
178+
.show(configDescriptionRegistry);
178179
}
179180

180181
private void startConsole(Console console, String[] args) {

bundles/org.openhab.automation.pythonscripting/src/main/java/org/openhab/automation/pythonscripting/internal/console/handler/InfoCmd.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
import java.util.Map;
2121

2222
import org.eclipse.jdt.annotation.NonNullByDefault;
23+
import org.eclipse.jdt.annotation.Nullable;
2324
import org.graalvm.polyglot.Language;
24-
import org.openhab.automation.pythonscripting.internal.PythonScriptEngine;
2525
import org.openhab.automation.pythonscripting.internal.PythonScriptEngineConfiguration;
2626
import org.openhab.automation.pythonscripting.internal.PythonScriptEngineFactory;
2727
import org.openhab.core.config.core.ConfigDescription;
@@ -38,10 +38,12 @@
3838
public class InfoCmd {
3939
private final PythonScriptEngineConfiguration config;
4040
private final Console console;
41+
private final @Nullable Language language;
4142

42-
public InfoCmd(PythonScriptEngineConfiguration config, Console console) {
43+
public InfoCmd(PythonScriptEngineConfiguration config, Console console, @Nullable Language language) {
4344
this.config = config;
4445
this.console = console;
46+
this.language = language;
4547
}
4648

4749
public void show(ConfigDescriptionRegistry registry) {
@@ -50,7 +52,6 @@ public void show(ConfigDescriptionRegistry registry) {
5052
console.println(" Runtime:");
5153
console.println(" Bundle version: " + config.getBundleVersion());
5254
console.println(" GraalVM version: " + config.getGraalVersion());
53-
Language language = PythonScriptEngine.getLanguage();
5455
console.println(" Python version: " + (language != null ? language.getVersion() : "unavailable"));
5556
Version version = config.getInstalledHelperLibVersion();
5657
console.println(" Helper lib version: " + (version != null ? version.toString() : "disabled"));

0 commit comments

Comments
 (0)