From d881b28d5842d366ab839be8c8f852467a78f546 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Fri, 8 Jul 2022 16:30:03 +0100 Subject: [PATCH] Add the bulk of the work for returning task data from events. Credit: kiip1 for suggestion and example. --- pom.xml | 2 +- .../org/byteskript/skript/runtime/Skript.java | 21 +++-- .../skript/runtime/internal/EventHandler.java | 35 ++++++-- .../runtime/threading/ScriptFinishFuture.java | 33 ++++--- .../skript/runtime/type/EventData.java | 87 +++++++++++++++++++ .../byteskript/skript/test/ThreadingTest.java | 71 +++++++++++++++ 6 files changed, 221 insertions(+), 28 deletions(-) create mode 100644 src/main/java/org/byteskript/skript/runtime/type/EventData.java create mode 100644 src/test/java/org/byteskript/skript/test/ThreadingTest.java diff --git a/pom.xml b/pom.xml index f9bc153..7048eba 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.byteskript byteskript - 1.0.34 + 1.0.35 ByteSkript A compiled JVM implementation of the Skript language. diff --git a/src/main/java/org/byteskript/skript/runtime/Skript.java b/src/main/java/org/byteskript/skript/runtime/Skript.java index 3d4afdf..c0ff3bc 100644 --- a/src/main/java/org/byteskript/skript/runtime/Skript.java +++ b/src/main/java/org/byteskript/skript/runtime/Skript.java @@ -24,6 +24,7 @@ import org.byteskript.skript.runtime.internal.*; import org.byteskript.skript.runtime.threading.*; import org.byteskript.skript.runtime.type.Converter; +import org.byteskript.skript.runtime.type.EventData; import org.byteskript.skript.runtime.type.OperatorFunction; import java.io.*; @@ -463,15 +464,15 @@ public Future runScript(final ScriptRunner runner, final Event event) { final Runnable runnable = () -> { final ScriptThread thread = (ScriptThread) Thread.currentThread(); future.thread = thread; - thread.variables.clear(); + thread.variables.clear(); // Some threads get regurgitated and will have shadow variables from their previous run. thread.initiator = runner.owner(); thread.event = event; try { runner.run(); - future.value(runner.result()); } catch (ThreadDeath ignore) { - // This is likely from an exit the current process effect, we don't want to make noise + // This is likely from an exit the current process effect, we don't want to make noise. } finally { + future.value(runner.result()); future.finish(); } }; @@ -485,15 +486,16 @@ public Future runScript(final ScriptRunner runner, final Event event) { This will trigger only the given script. """) @GenerateExample - public boolean runEvent(final Event event, final Script script) { + public EventData runEvent(final Event event, final Script script) { boolean run = false; + final List futures = new ArrayList<>(); for (Map.Entry, EventHandler> entry : events.entrySet()) { final Class key = entry.getKey(); if (!key.isAssignableFrom(event.getClass())) continue; run = true; - entry.getValue().run(this, event, script); + futures.addAll(Arrays.asList(entry.getValue().run(this, event, script))); } - return run; + return new EventData<>(run, event, futures.toArray(new ScriptFinishFuture[0])); } @Description(""" @@ -777,15 +779,16 @@ public void unloadScript(Script script) { Each handler will spawn its own process. """) @GenerateExample - public boolean runEvent(final Event event) { + public EventData runEvent(final Event event) { boolean run = false; + final List futures = new ArrayList<>(); for (Map.Entry, EventHandler> entry : events.entrySet()) { final Class key = entry.getKey(); if (!key.isAssignableFrom(event.getClass())) continue; run = true; - entry.getValue().run(this, event); + futures.addAll(Arrays.asList(entry.getValue().run(this, event))); } - return run; + return new EventData<>(run, event, futures.toArray(new ScriptFinishFuture[0])); } @Description(""" diff --git a/src/main/java/org/byteskript/skript/runtime/internal/EventHandler.java b/src/main/java/org/byteskript/skript/runtime/internal/EventHandler.java index d678cfd..0a0b1b3 100644 --- a/src/main/java/org/byteskript/skript/runtime/internal/EventHandler.java +++ b/src/main/java/org/byteskript/skript/runtime/internal/EventHandler.java @@ -10,10 +10,12 @@ import org.byteskript.skript.api.Event; import org.byteskript.skript.runtime.Script; import org.byteskript.skript.runtime.Skript; +import org.byteskript.skript.runtime.threading.ScriptFinishFuture; import org.byteskript.skript.runtime.threading.ScriptRunner; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Future; @Ignore public class EventHandler { @@ -24,21 +26,38 @@ public List getTriggers() { return triggers; } - public void run(final Skript skript, final Event event) { - for (final ScriptRunner trigger : triggers) { - skript.runScript(trigger, event); + public ScriptFinishFuture[] run(final Skript skript, final Event event) { + final ScriptFinishFuture[] futures = new ScriptFinishFuture[triggers.size()]; + int count = 0; + synchronized (triggers) { // Reduce the chance of comodification. + for (final ScriptRunner trigger : triggers) { + futures[count] = (ScriptFinishFuture) skript.runScript(trigger, event); + assert futures[count] != null: "Script trigger didn't produce a task."; + count++; + } } + return futures; } - public void run(final Skript skript, final Event event, final Script script) { - for (final ScriptRunner trigger : triggers) { - if (trigger.owner() == script.mainClass()) - skript.runScript(trigger, event); + public ScriptFinishFuture[] run(final Skript skript, final Event event, final Script script) { + int count = 0; + synchronized (triggers) { + final List triggers = new ArrayList<>(this.triggers); + triggers.removeIf(trigger -> trigger.owner() != script.mainClass()); + final ScriptFinishFuture[] futures = new ScriptFinishFuture[triggers.size()]; + for (final ScriptRunner trigger : triggers) { + futures[count] = (ScriptFinishFuture) skript.runScript(trigger, event); + assert futures[count] != null: "Script trigger didn't produce a task."; + count++; + } + return futures; } } public void add(final ScriptRunner runner) { - this.triggers.add(runner); + synchronized (triggers) { + this.triggers.add(runner); + } } } diff --git a/src/main/java/org/byteskript/skript/runtime/threading/ScriptFinishFuture.java b/src/main/java/org/byteskript/skript/runtime/threading/ScriptFinishFuture.java index 87714ac..5feaddd 100644 --- a/src/main/java/org/byteskript/skript/runtime/threading/ScriptFinishFuture.java +++ b/src/main/java/org/byteskript/skript/runtime/threading/ScriptFinishFuture.java @@ -13,11 +13,13 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; -public class ScriptFinishFuture implements Future { +public class ScriptFinishFuture implements Supplier, Future { private final Skript skript; private final Object lock = new Object(); + private boolean done; public ScriptThread thread; protected Object value; @@ -25,16 +27,17 @@ public ScriptFinishFuture(Skript skript) { this.skript = skript; } - public void value(Object object) { - synchronized (this) { - this.value = object; - } + public synchronized void value(Object object) { + this.value = object; } public void finish() { synchronized (lock) { lock.notify(); } + synchronized (this) { + this.done = true; + } } @Override @@ -47,18 +50,25 @@ public boolean cancel(boolean mayInterruptIfRunning) { @Override public boolean isCancelled() { - return thread != null && !thread.isAlive(); + return thread == null || thread.isAlive(); } @Override - public boolean isDone() { - return thread != null && !thread.isAlive(); + public synchronized boolean isDone() { + return value != null; } @Override - public Object get() throws InterruptedException, ExecutionException { + public Object get() { + synchronized (this) { + if (this.done) return value; + } synchronized (lock) { - lock.wait(); + try { + lock.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } synchronized (this) { return value; @@ -67,6 +77,9 @@ public Object get() throws InterruptedException, ExecutionException { @Override public Object get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + synchronized (this) { + if (value != null) return value; + } synchronized (lock) { lock.wait(unit.toMillis(timeout)); } diff --git a/src/main/java/org/byteskript/skript/runtime/type/EventData.java b/src/main/java/org/byteskript/skript/runtime/type/EventData.java new file mode 100644 index 0000000..70be356 --- /dev/null +++ b/src/main/java/org/byteskript/skript/runtime/type/EventData.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 ByteSkript org (Moderocky) + * View the full licence information and permissions: + * https://github.com/Moderocky/ByteSkript/blob/master/LICENSE + */ + +package org.byteskript.skript.runtime.type; + +import org.byteskript.skript.api.Event; +import org.byteskript.skript.runtime.threading.ScriptFinishFuture; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +public final class EventData { + private final boolean run; + private final Type event; + private final ScriptFinishFuture[] tasks; + private CompletableFuture all, any; + private CompletableFuture[] futures; + + public EventData(boolean run, Type event, ScriptFinishFuture[] tasks) { + this.run = run; + this.event = event; + this.tasks = tasks; + } + + private void prepareFutures() { + this.futures = new CompletableFuture[tasks.length]; + for (int i = 0; i < tasks.length; i++) futures[i] = CompletableFuture.supplyAsync(tasks[i]); + } + + public CompletableFuture all() { + if (all != null) return all; + if (this.futures == null) this.prepareFutures(); + return all = CompletableFuture.allOf(futures); + } + + public CompletableFuture any() { + if (any != null) return any; + if (this.futures == null) this.prepareFutures(); + return any = CompletableFuture.anyOf(futures); + } + + @SuppressWarnings("unchecked") + public Class getType() { + return (Class) event.getClass(); + } + + public boolean run() { + return run; + } + + public Type event() { + return event; + } + + public ScriptFinishFuture[] tasks() { + return tasks; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (EventData) obj; + return this.run == that.run && + Objects.equals(this.event, that.event) && + Objects.equals(this.tasks, that.tasks); + } + + @Override + public int hashCode() { + return Objects.hash(run, event, tasks); + } + + @Override + public String toString() { + return "EventData[" + + "run=" + run + ", " + + "event=" + event + ", " + + "tasks=" + tasks + ']'; + } + + +} diff --git a/src/test/java/org/byteskript/skript/test/ThreadingTest.java b/src/test/java/org/byteskript/skript/test/ThreadingTest.java new file mode 100644 index 0000000..c4107b0 --- /dev/null +++ b/src/test/java/org/byteskript/skript/test/ThreadingTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 ByteSkript org (Moderocky) + * View the full licence information and permissions: + * https://github.com/Moderocky/ByteSkript/blob/master/LICENSE + */ + +package org.byteskript.skript.test; + +import mx.kenzie.foundation.language.PostCompileClass; +import org.byteskript.skript.api.Event; +import org.byteskript.skript.api.ModifiableLibrary; +import org.byteskript.skript.api.note.EventValue; +import org.byteskript.skript.runtime.Skript; +import org.byteskript.skript.runtime.config.ConfigEntry; +import org.byteskript.skript.runtime.config.ConfigMap; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class ThreadingTest extends SkriptTest { + + private static final Skript skript + = new Skript(); + // = new Skript(new DebugSkriptCompiler(Stream.controller(System.out))); + + private final String test = """ + on test: + trigger: + print "Start" + wait 1 second + print "Finish" + + """; + private static PostCompileClass cls; + + @BeforeClass + public static void setup() { + skript.registerLibrary(new ModifiableLibrary("test") {{ // Credit: kiip1 for suggestion. + generateSyntaxFrom(TestEvent.class); + }}); + cls = skript.compileScript( + """ + on test: + trigger: + wait 1 millisecond + set event-thing to 6 + """, "skript.events"); + skript.loadScript(cls); + } + + @Test + public void eventSynchronized() throws Throwable { + final TestEvent event; + skript.runEvent(event = new TestEvent()) + .all() + .get(); + assert event.number == 6; + } + + @org.byteskript.skript.api.note.Event("on test") + public static final class TestEvent extends Event { + public int number = 1; + @EventValue("thing") + public void setThing(Number number) { + this.number = (int) number; + } + } +}