diff --git a/pom.xml b/pom.xml index 960e521..3326057 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.openautonomousconnection LuaScript - 1.0.0-BETA.1.1 + 0.0.0-STABLE.1.3 Open Autonomous Connection @@ -65,6 +65,10 @@ + + unlegitdqrk + https://repo.unlegitdqrk.dev/api/packages/UnlegitDqrk/maven + oac https://repo.open-autonomous-connection.org/api/packages/open-autonomous-connection/maven @@ -78,7 +82,7 @@ dev.unlegitdqrk unlegitlibrary - 1.8.1 + 1.8.3 org.luaj diff --git a/src/main/java/org/openautonomousconnection/luascript/fx/FxEventHost.java b/src/main/java/org/openautonomousconnection/luascript/fx/FxEventHost.java index 5dbf39f..a9cc08b 100644 --- a/src/main/java/org/openautonomousconnection/luascript/fx/FxEventHost.java +++ b/src/main/java/org/openautonomousconnection/luascript/fx/FxEventHost.java @@ -11,6 +11,8 @@ import org.w3c.dom.events.EventTarget; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * EventHost implementation for JavaFX WebView using W3C DOM EventTarget listeners. @@ -22,19 +24,31 @@ public final class FxEventHost implements EventHost { private static final String GLOBAL_TARGET_ID = "__global__"; private final FxDomHost dom; - private final LuaEventRouter router; + private volatile LuaEventRouter router; private final ConcurrentHashMap elementListeners = new ConcurrentHashMap<>(); private final ConcurrentHashMap globalListeners = new ConcurrentHashMap<>(); + private final ExecutorService dispatchExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "oac-fx-event-dispatch"); + t.setDaemon(true); + return t; + }); /** * Creates a new FxEventHost. * * @param dom fx dom host + */ + public FxEventHost(FxDomHost dom) { + this.dom = Objects.requireNonNull(dom, "dom"); + } + + /** + * Sets the active Lua router for this host. + * * @param router lua router */ - public FxEventHost(FxDomHost dom, LuaEventRouter router) { - this.dom = Objects.requireNonNull(dom, "dom"); + public void setRouter(LuaEventRouter router) { this.router = Objects.requireNonNull(router, "router"); } @@ -49,7 +63,7 @@ public final class FxEventHost implements EventHost { elementListeners.computeIfAbsent(key, k -> { EventListener listener = ev -> { Map payload = FxEventPayloadExtractor.extract(ev); - router.emit(elementId, evt, payload); + dispatchExecutor.execute(() -> emitSafely(elementId, evt, payload)); }; FxThreadBridge.runAndWait(() -> { @@ -87,7 +101,7 @@ public final class FxEventHost implements EventHost { globalListeners.computeIfAbsent(evt, k -> { EventListener listener = ev -> { Map payload = FxEventPayloadExtractor.extract(ev); - router.emit(GLOBAL_TARGET_ID, evt, payload); + dispatchExecutor.execute(() -> emitSafely(GLOBAL_TARGET_ID, evt, payload)); }; FxThreadBridge.runAndWait(() -> { @@ -113,4 +127,18 @@ public final class FxEventHost implements EventHost { ((EventTarget) doc).removeEventListener(evt, listener, false); }); } + + private void emitSafely(String elementId, String eventName, Map payload) { + try { + LuaEventRouter activeRouter = router; + if (activeRouter == null) { + System.err.println("[oac.event] Dropping '" + eventName + "' for '" + elementId + "': router not initialized"); + return; + } + activeRouter.emit(elementId, eventName, payload); + } catch (RuntimeException ex) { + System.err.println("[oac.event] Failed to dispatch '" + eventName + "' for '" + elementId + "': " + ex.getMessage()); + ex.printStackTrace(System.err); + } + } } diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/AudioHost.java b/src/main/java/org/openautonomousconnection/luascript/hosts/AudioHost.java new file mode 100644 index 0000000..d40f52a --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/AudioHost.java @@ -0,0 +1,9 @@ +package org.openautonomousconnection.luascript.hosts; + +import java.io.File; + +public interface AudioHost { + + void play(File audioFile); + +} diff --git a/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java b/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java index c34137f..5e595c7 100644 --- a/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java +++ b/src/main/java/org/openautonomousconnection/luascript/hosts/HostServices.java @@ -45,6 +45,8 @@ public interface HostServices { */ Optional console(); + Optional audio(); + /** * Simple immutable implementation. */ @@ -54,6 +56,7 @@ public interface HostServices { private final EventHost events; private final ResourceHost resources; private final ConsoleHost console; + private final AudioHost audioHost; /** * Creates a HostServices container. @@ -64,12 +67,13 @@ public interface HostServices { * @param resources resource host * @param console console host */ - public Default(UiHost ui, DomHost dom, EventHost events, ResourceHost resources, ConsoleHost console) { + public Default(UiHost ui, DomHost dom, EventHost events, ResourceHost resources, ConsoleHost console, AudioHost audioHost) { this.ui = ui; this.dom = dom; this.events = events; this.resources = resources; this.console = console; + this.audioHost = audioHost; } @Override @@ -96,6 +100,11 @@ public interface HostServices { public Optional console() { return Optional.ofNullable(console); } + + @Override + public Optional audio() { + return Optional.ofNullable(audioHost); + } } /** diff --git a/src/main/java/org/openautonomousconnection/luascript/runtime/FxLuaScriptEngine.java b/src/main/java/org/openautonomousconnection/luascript/runtime/FxLuaScriptEngine.java index 29d1d69..b5439e8 100644 --- a/src/main/java/org/openautonomousconnection/luascript/runtime/FxLuaScriptEngine.java +++ b/src/main/java/org/openautonomousconnection/luascript/runtime/FxLuaScriptEngine.java @@ -7,6 +7,7 @@ import org.openautonomousconnection.luascript.fx.FxDomHost; import org.openautonomousconnection.luascript.fx.FxEventHost; import org.openautonomousconnection.luascript.fx.FxUiHost; import org.openautonomousconnection.luascript.fx.FxWebViewResourceHost; +import org.openautonomousconnection.luascript.hosts.AudioHost; import org.openautonomousconnection.luascript.hosts.HostServices; import org.openautonomousconnection.luascript.security.LuaExecutionPolicy; import org.openautonomousconnection.luascript.utils.LuaGlobalsFactory; @@ -82,23 +83,14 @@ public final class FxLuaScriptEngine implements AutoCloseable { .sandbox(true) ); - // Create runtime first (router lives inside it). HostServices.StdoutConsole console = new HostServices.StdoutConsole("[lua] "); FxUiHost uiHost = new FxUiHost(engine, dom); FxWebViewResourceHost resourceHost = new FxWebViewResourceHost(engine); - - // runtime depends on services; events depends on runtime router. - // We'll create eventHost after runtime, then build HostServices with it. - LuaRuntime rt = new LuaRuntime(globals, new HostServices.Default(uiHost, dom, null, resourceHost, console), policy); - - FxEventHost eventHost = new FxEventHost(dom, rt.eventRouter()); - - // Rebuild services including eventHost and reinstall tables. - HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console); - - // Replace runtime with correct services (clean and deterministic). - rt.close(); - rt = new LuaRuntime(globals, services, policy); + FxEventHost eventHost = new FxEventHost(dom); + // TODO: Default implementation or parameter for "audioHost" + HostServices services = new HostServices.Default(uiHost, dom, eventHost, resourceHost, console, audioHost); + LuaRuntime rt = new LuaRuntime(globals, services, policy); + eventHost.setRouter(rt.eventRouter()); rt.installStdTables(true); rt.bootstrapFromDom(); @@ -131,4 +123,4 @@ public final class FxLuaScriptEngine implements AutoCloseable { } } } -} \ No newline at end of file +} diff --git a/src/main/java/org/openautonomousconnection/luascript/security/LuaExecutionPolicy.java b/src/main/java/org/openautonomousconnection/luascript/security/LuaExecutionPolicy.java index 7df2b46..90ab59d 100644 --- a/src/main/java/org/openautonomousconnection/luascript/security/LuaExecutionPolicy.java +++ b/src/main/java/org/openautonomousconnection/luascript/security/LuaExecutionPolicy.java @@ -24,6 +24,6 @@ public record LuaExecutionPolicy(Duration timeout, long instructionLimit, int ho * @return policy */ public static LuaExecutionPolicy uiDefault() { - return new LuaExecutionPolicy(Duration.ofMillis(50), 200_000, 5_000); + return new LuaExecutionPolicy(Duration.ofSeconds(5), 200_000, 5_000); } } diff --git a/src/main/java/org/openautonomousconnection/luascript/security/LuaSecurityManager.java b/src/main/java/org/openautonomousconnection/luascript/security/LuaSecurityManager.java index fa3abfe..fbb5c7e 100644 --- a/src/main/java/org/openautonomousconnection/luascript/security/LuaSecurityManager.java +++ b/src/main/java/org/openautonomousconnection/luascript/security/LuaSecurityManager.java @@ -1,8 +1,8 @@ package org.openautonomousconnection.luascript.security; import org.luaj.vm2.*; -import org.luaj.vm2.lib.DebugLib; import org.luaj.vm2.lib.VarArgFunction; +import org.openautonomousconnection.luascript.utils.LuaGlobalsFactory; import java.util.Objects; import java.util.concurrent.*; @@ -16,6 +16,7 @@ import java.util.concurrent.atomic.AtomicLong; public final class LuaSecurityManager implements AutoCloseable { private final ExecutorService executor; + private final ConcurrentHashMap debugSetHooks = new ConcurrentHashMap<>(); /** * Creates a new LuaSecurityManager that runs all Lua code on a single dedicated daemon thread. @@ -28,8 +29,8 @@ public final class LuaSecurityManager implements AutoCloseable { }); } - private static Varargs callWithHook(Globals globals, LuaFunction function, Varargs args, LuaExecutionPolicy policy) { - final LuaValue sethook = resolveAndHideDebugSetHook(globals); + private Varargs callWithHook(Globals globals, LuaFunction function, Varargs args, LuaExecutionPolicy policy) { + final LuaValue sethook = resolveDebugSetHook(globals); final long deadlineMillis = System.currentTimeMillis() + policy.timeout().toMillis(); final AtomicLong ticks = new AtomicLong(0); @@ -84,13 +85,19 @@ public final class LuaSecurityManager implements AutoCloseable { return resumed; } - private static LuaValue resolveAndHideDebugSetHook(Globals globals) { - // Ensure DebugLib exists for hooks. - globals.load(new DebugLib()); + private LuaValue resolveDebugSetHook(Globals globals) { + LuaValue cached = debugSetHooks.get(globals); + if (cached != null) return cached; + + LuaValue internal = globals.get(LuaGlobalsFactory.INTERNAL_DEBUG_SETHOOK_KEY); + if (!internal.isnil() && internal.isfunction()) { + debugSetHooks.put(globals, internal); + return internal; + } LuaValue debugTable = globals.get("debug"); if (debugTable.isnil() || !debugTable.istable()) { - throw new IllegalStateException("Debug library not available (debug table missing)"); + throw new IllegalStateException("Debug library not available (debug table missing and no internal sethook cached)"); } LuaValue sethook = debugTable.get("sethook"); @@ -100,6 +107,7 @@ public final class LuaSecurityManager implements AutoCloseable { // Hide debug from scripts. globals.set("debug", LuaValue.NIL); + debugSetHooks.put(globals, sethook); return sethook; } @@ -140,4 +148,4 @@ public final class LuaSecurityManager implements AutoCloseable { public void close() { executor.shutdownNow(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/openautonomousconnection/luascript/tables/AudioTable.java b/src/main/java/org/openautonomousconnection/luascript/tables/AudioTable.java new file mode 100644 index 0000000..c0de21b --- /dev/null +++ b/src/main/java/org/openautonomousconnection/luascript/tables/AudioTable.java @@ -0,0 +1,29 @@ +package org.openautonomousconnection.luascript.tables; + +import org.luaj.vm2.LuaValue; +import org.luaj.vm2.lib.OneArgFunction; +import org.openautonomousconnection.luascript.hosts.AudioHost; +import org.openautonomousconnection.luascript.hosts.HostServices; +import org.openautonomousconnection.luascript.utils.ScriptTable; + +import java.io.File; + +public class AudioTable extends ScriptTable { + protected AudioTable() { + super("audio"); + } + + @Override + protected void define(HostServices services) { + AudioHost audioHost = services.audio().orElseThrow(() -> new IllegalStateException("AudioHost not provided")); + + table().set("play", new OneArgFunction() { + @Override + public LuaValue call(LuaValue arg) { + String fileName = arg.checkjstring(); + audioHost.play(new File(fileName)); + return LuaValue.NIL; + } + }); + } +} diff --git a/src/main/java/org/openautonomousconnection/luascript/utils/LuaGlobalsFactory.java b/src/main/java/org/openautonomousconnection/luascript/utils/LuaGlobalsFactory.java index cfcca18..2c44049 100644 --- a/src/main/java/org/openautonomousconnection/luascript/utils/LuaGlobalsFactory.java +++ b/src/main/java/org/openautonomousconnection/luascript/utils/LuaGlobalsFactory.java @@ -22,6 +22,8 @@ import java.util.function.Consumer; */ public final class LuaGlobalsFactory { + public static final String INTERNAL_DEBUG_SETHOOK_KEY = "__oac_internal_debug_sethook"; + private LuaGlobalsFactory() { } @@ -35,12 +37,13 @@ public final class LuaGlobalsFactory { Objects.requireNonNull(options, "options"); Globals g = JsePlatform.standardGlobals(); + g.load(new DebugLib()); + cacheInternalDebugSethook(g); if (options.enableDebug) { - // Ensure debug functions are available (depending on defaults). - g.load(new DebugLib()); + // Keep debug table visible to scripts. } else { - // If sandbox is not enabled but you still want no debug, explicitly remove it. + // Keep hook support internally, but do not expose debug to scripts. g.set("debug", LuaValue.NIL); } @@ -55,6 +58,16 @@ public final class LuaGlobalsFactory { return g; } + private static void cacheInternalDebugSethook(Globals g) { + LuaValue debugTable = g.get("debug"); + if (debugTable.isnil() || !debugTable.istable()) return; + + LuaValue sethook = debugTable.get("sethook"); + if (!sethook.isnil() && sethook.isfunction()) { + g.set(INTERNAL_DEBUG_SETHOOK_KEY, sethook); + } + } + /** * Applies basic sandbox hardening to the given globals. *