28
28
import java .time .Instant ;
29
29
import java .time .ZonedDateTime ;
30
30
import java .util .ArrayList ;
31
+ import java .util .Arrays ;
31
32
import java .util .Collection ;
32
33
import java .util .HashSet ;
33
34
import java .util .List ;
39
40
import java .util .function .BiFunction ;
40
41
import java .util .function .Consumer ;
41
42
import java .util .function .Function ;
43
+ import java .util .stream .Collectors ;
42
44
43
45
import javax .script .ScriptContext ;
44
- import javax .script .ScriptEngine ;
46
+ import javax .script .ScriptException ;
45
47
46
48
import org .eclipse .jdt .annotation .Nullable ;
47
49
import org .graalvm .polyglot .Context ;
@@ -90,6 +92,10 @@ public class PythonScriptEngine
90
92
.get (OpenHAB .getUserDataFolder (), "cache" , PythonScriptEngine .class .getPackageName (), "cachedir" )
91
93
.toString ();
92
94
95
+ private static final int STACK_TRACE_LENGTH = 5 ;
96
+
97
+ public static final String LOGGER_INIT_NAME = "__logger_init__" ;
98
+
93
99
/** Shared Polyglot {@link Engine} across all instances of {@link PythonScriptEngine} */
94
100
private static final Engine ENGINE = Engine .newBuilder ().allowExperimentalOptions (true )
95
101
.option ("engine.WarnInterpreterOnly" , "false" ).build ();
@@ -243,26 +249,17 @@ public void checkAccess(Path path, Set<? extends AccessMode> modes,
243
249
244
250
@ Override
245
251
protected void beforeInvocation () {
246
- super .beforeInvocation ();
247
252
248
253
lock .lock ();
249
254
logger .debug ("Lock acquired before invocation." );
250
255
251
- // must be initialized every time, because of dynamic used attributes like ruleUID
252
- Logger scriptLogger = initScriptLogger (delegate );
253
- scriptOutputStream .setLogger (scriptLogger );
254
- scriptErrorStream .setLogger (scriptLogger );
255
-
256
256
if (initialized ) {
257
257
return ;
258
258
}
259
259
260
260
logger .debug ("Initializing GraalPython script engine..." );
261
261
262
- ScriptContext ctx = delegate .getContext ();
263
- if (ctx == null ) {
264
- throw new IllegalStateException ("Failed to retrieve script context" );
265
- }
262
+ ScriptContext ctx = getScriptContext ();
266
263
267
264
// these are added post-construction, so we need to fetch them late
268
265
String engineIdentifier = (String ) ctx .getAttribute (CONTEXT_KEY_ENGINE_IDENTIFIER );
@@ -298,6 +295,7 @@ protected void beforeInvocation() {
298
295
delegate .getPolyglotContext ().eval (Source .newBuilder (GraalPythonScriptEngine .LANGUAGE_ID ,
299
296
wrapperContent , PythonScriptEngineFactory .PYTHON_WRAPPER_FILE_PATH .toString ()).build ());
300
297
298
+ // inject scope, Registry and logger
301
299
if (injectionEnabled != PythonScriptEngineFactory .INJECTION_DISABLED
302
300
&& (ctx .getAttribute ("javax.script.filename" ) == null
303
301
|| injectionEnabled == PythonScriptEngineFactory .INJECTION_ENABLED_FOR_ALL_SCRIPTS )) {
@@ -311,9 +309,30 @@ protected void beforeInvocation() {
311
309
}
312
310
}
313
311
312
+ // logger initialization, for non file based scripts, has to be delayed, because ruleUID is not available yet
313
+ if (ctx .getAttribute ("javax.script.filename" ) == null ) {
314
+ Runnable wrapperLoggerFn = () -> setScriptLogger ();
315
+ delegate .getBindings (ScriptContext .ENGINE_SCOPE ).put (LOGGER_INIT_NAME , wrapperLoggerFn );
316
+ } else {
317
+ setScriptLogger ();
318
+ }
319
+
314
320
initialized = true ;
315
321
}
316
322
323
+ @ Override
324
+ protected String beforeInvocation (String source ) {
325
+ String _source = super .beforeInvocation (source );
326
+
327
+ // Happens for Transform and UI based rules (eval and compile)
328
+ // and has to be evaluate every time, because of changing and late injected ruleUID
329
+ if (delegate .getBindings (ScriptContext .ENGINE_SCOPE ).get (LOGGER_INIT_NAME ) != null ) {
330
+ return LOGGER_INIT_NAME + "()\n " + _source ;
331
+ }
332
+
333
+ return _source ;
334
+ }
335
+
317
336
@ Override
318
337
protected Object afterInvocation (Object obj ) {
319
338
lock .unlock ();
@@ -323,20 +342,35 @@ protected Object afterInvocation(Object obj) {
323
342
324
343
@ Override
325
344
protected Exception afterThrowsInvocation (Exception e ) {
345
+ // OPS4J Pax Logging holds a reference to the exception, which causes the OpenhabGraalJSScriptEngine to not be
346
+ // removed from heap by garbage collection and causing a memory leak.
347
+ // Therefore, don't pass the exceptions itself to the logger, but only their message!
348
+ if (e instanceof ScriptException ) {
349
+ // PolyglotException will always be wrapped into ScriptException and they will be visualized in
350
+ // org.openhab.core.automation.module.script.internal.ScriptEngineManagerImpl
351
+ if (scriptErrorStream .logger .isDebugEnabled ()) {
352
+ scriptErrorStream .logger .debug ("Failed to execute script (PolyglotException): {}" ,
353
+ stringifyThrowable (e .getCause ()));
354
+ }
355
+ } else if (e .getCause () instanceof IllegalArgumentException ) {
356
+ scriptErrorStream .logger .error ("Failed to execute script (IllegalArgumentException): {}" ,
357
+ stringifyThrowable (e .getCause ()));
358
+ }
359
+
326
360
lock .unlock ();
361
+
327
362
return super .afterThrowsInvocation (e );
328
363
}
329
364
330
365
@ Override
331
366
// collect JSR223 (scope) variables separately, because they are delivered via 'import scope'
332
367
public void put (String key , Object value ) {
333
- if (key .equals ("javax.script.filename" ) || key . equals ( "ruleUID" ) ) {
368
+ if (key .equals ("javax.script.filename" )) {
334
369
// super.put("__file__", value);
335
370
super .put (key , value );
336
371
} else {
337
- // use a custom lifecycleTracker to handle dispose hook before polyglot context is closed {@link
338
- // #close()}
339
- // original openHAB {@link LifecycleScriptExtensionProvider}
372
+ // use a custom lifecycleTracker to handle dispose hook before polyglot context is closed
373
+ // original lifecycleTracker is handling it when polyglot context is already closed
340
374
if (key .equals ("lifecycleTracker" )) {
341
375
value = lifecycleTracker ;
342
376
}
@@ -361,12 +395,24 @@ public void lockInterruptibly() throws InterruptedException {
361
395
362
396
@ Override
363
397
public boolean tryLock () {
364
- return lock .tryLock ();
398
+ boolean acquired = lock .tryLock ();
399
+ if (acquired ) {
400
+ logger .debug ("Lock acquired." );
401
+ } else {
402
+ logger .debug ("Lock not acquired." );
403
+ }
404
+ return acquired ;
365
405
}
366
406
367
407
@ Override
368
408
public boolean tryLock (long l , TimeUnit timeUnit ) throws InterruptedException {
369
- return lock .tryLock (l , timeUnit );
409
+ boolean acquired = lock .tryLock (l , timeUnit );
410
+ if (acquired ) {
411
+ logger .debug ("Lock acquired." );
412
+ } else {
413
+ logger .debug ("Lock not acquired." );
414
+ }
415
+ return acquired ;
370
416
}
371
417
372
418
@ Override
@@ -386,23 +432,36 @@ public Condition newCondition() {
386
432
return lock .newCondition ();
387
433
}
388
434
389
- private static Set <String > transformGraalWrapperSet (Value value ) {
390
- // Value raw_value = value.invokeMember("getWrappedSetValues");
391
- Set <String > set = new HashSet <String >();
392
- for (int i = 0 ; i < value .getArraySize (); ++i ) {
393
- Value element = value .getArrayElement (i );
394
- set .add (element .asString ());
435
+ private void setScriptLogger () {
436
+ Logger scriptLogger = initScriptLogger ();
437
+ scriptOutputStream .setLogger (scriptLogger );
438
+ scriptErrorStream .setLogger (scriptLogger );
439
+ }
440
+
441
+ private ScriptContext getScriptContext () {
442
+ ScriptContext ctx = delegate .getContext ();
443
+ if (ctx == null ) {
444
+ throw new IllegalStateException ("Failed to retrieve script context" );
395
445
}
396
- return set ;
446
+ return ctx ;
447
+ }
448
+
449
+ private String stringifyThrowable (Throwable throwable ) {
450
+ String message = throwable .getMessage ();
451
+ StackTraceElement [] stackTraceElements = throwable .getStackTrace ();
452
+ String stackTrace = Arrays .stream (stackTraceElements ).limit (STACK_TRACE_LENGTH )
453
+ .map (t -> " at " + t .toString ()).collect (Collectors .joining (System .lineSeparator ()))
454
+ + System .lineSeparator () + " ... " + stackTraceElements .length + " more" ;
455
+ return (message != null ) ? message + System .lineSeparator () + stackTrace : stackTrace ;
397
456
}
398
457
399
458
/**
400
459
* Initializes the logger.
401
460
* This cannot be done on script engine creation because the context variables are not yet initialized.
402
461
* Therefore, the logger needs to be initialized on the first use after script engine creation.
403
462
*/
404
- public static Logger initScriptLogger (ScriptEngine scriptEngine ) {
405
- ScriptContext ctx = scriptEngine . getContext ();
463
+ private Logger initScriptLogger () {
464
+ ScriptContext ctx = getScriptContext ();
406
465
Object fileName = ctx .getAttribute ("javax.script.filename" );
407
466
Object ruleUID = ctx .getAttribute ("ruleUID" );
408
467
Object ohEngineIdentifier = ctx .getAttribute ("oh.engine-identifier" );
@@ -421,6 +480,16 @@ public static Logger initScriptLogger(ScriptEngine scriptEngine) {
421
480
return LoggerFactory .getLogger ("org.openhab.automation.pythonscripting." + identifier );
422
481
}
423
482
483
+ private static Set <String > transformGraalWrapperSet (Value value ) {
484
+ // Value raw_value = value.invokeMember("getWrappedSetValues");
485
+ Set <String > set = new HashSet <String >();
486
+ for (int i = 0 ; i < value .getArraySize (); ++i ) {
487
+ Value element = value .getArrayElement (i );
488
+ set .add (element .asString ());
489
+ }
490
+ return set ;
491
+ }
492
+
424
493
private static class LogOutputStream extends OutputStream {
425
494
private static final int DEFAULT_BUFFER_LENGTH = 2048 ;
426
495
private static final String LINE_SEPERATOR = System .getProperty ("line.separator" );
0 commit comments