diff --git a/docs/scarpet/language/SystemFunctions.md b/docs/scarpet/language/SystemFunctions.md index 1ddcc8e9..49690f57 100644 --- a/docs/scarpet/language/SystemFunctions.md +++ b/docs/scarpet/language/SystemFunctions.md @@ -89,7 +89,7 @@ are not thread safe and require the execution to park on the server thread, wher the off-tick time in between ticks that didn't take all 50ms. There are however benefits of running things in parallel, like fine time control not relying on the tick clock, or running things independent on each other. You can still run your actions on tick-by-tick basis, either taking control of the execution using `game_tick()` API function -(nasty solution), or scheduling tick using `schedule()` function (preffered solution), but threading gives much more control +(nasty solution), or scheduling tick using `schedule()` function (preferred solution), but threading gives much more control on the timings without impacting the main game and is the only solution to solve problems in parallel (see [scarpet camera](/src/main/resources/assets/carpet/scripts/camera.sc)). @@ -103,7 +103,10 @@ effectively parallelized. If the app is shutting down, creating new tasks via `task` will not succeed. Instead the new task will do nothing and return `null`, so most threaded application should handle closing apps naturally. Keep in mind in case you rely on task return values, -that they will return `null` regardless of anything in these situations. +that they will return `null` regardless of anything in these situations. When app handles `__on_close()` event, new tasks cannot +be submitted at this point, but current tasks are not terminated. Apps can use that opportunity to gracefully shutdown their tasks. +Regardless if the app handles `__on_close()` event, or does anything with their tasks in it, all tasks will be terminated exceptionally +within the next 1.5 seconds. ### `task(function, ... args)`, `task_thread(executor, function, ... args)` diff --git a/src/main/java/carpet/script/Expression.java b/src/main/java/carpet/script/Expression.java index 770b64d2..3da49f79 100644 --- a/src/main/java/carpet/script/Expression.java +++ b/src/main/java/carpet/script/Expression.java @@ -148,11 +148,6 @@ public class Expression { try { - if (v2 != null) - { - throw new ExpressionException(c, e, token, "Did not expect a second parameter for unary operator"); - } - Value.assertNotNull(v); return lazyfun.apply(c, t, v); } catch (RuntimeException exc) @@ -174,7 +169,6 @@ public class Expression { try { - Value.assertNotNull(v1, v2); return lazyfun.apply(c, type, e, t, v1, v2); } catch (RuntimeException exc) @@ -193,6 +187,7 @@ public class Expression @Override public LazyValue lazyEval(Context c, Integer type, Expression e, Tokenizer.Token t, List lv) { + ILazyFunction.checkInterrupts(); try { return lazyfun.apply(c, type, e, t, lv); @@ -234,9 +229,9 @@ public class Expression @Override public LazyValue lazyEval(Context c, Integer t, Expression e, Tokenizer.Token token, LazyValue v1, LazyValue v2) { + ILazyFunction.checkInterrupts(); try { - Value.assertNotNull(v1, v2); return lazyfun.apply(c, t, v1, v2); } catch (RuntimeException exc) @@ -257,7 +252,6 @@ public class Expression { try { - Value.assertNotNull(v1, v2); // does anybody needs those Value ret = fun.apply(c, t, v1.evalValue(c, Context.NONE), v2.evalValue(c, Context.NONE)); return (cc, tt) -> ret; } @@ -291,7 +285,7 @@ public class Expression @Override public Value evalUnary(Value v1) { - return fun.apply(Value.assertNotNull(v1)); + return fun.apply(v1); } }); } @@ -301,11 +295,7 @@ public class Expression operators.put(surface, new AbstractOperator(precedence, leftAssoc) { @Override - public Value eval(Value v1, Value v2) - { - Value.assertNotNull(v1, v2); - return fun.apply(v1, v2); - } + public Value eval(Value v1, Value v2) { return fun.apply(v1, v2); } }); } @@ -315,10 +305,7 @@ public class Expression functions.put(name, new AbstractFunction(1, name) { @Override - public Value eval(List parameters) - { - return fun.apply(Value.assertNotNull(parameters.get(0))); - } + public Value eval(List parameters) { return fun.apply(parameters.get(0)); } }); } @@ -327,13 +314,7 @@ public class Expression functions.put(name, new AbstractFunction(2, name) { @Override - public Value eval(List parameters) - { - Value v1 = parameters.get(0); - Value v2 = parameters.get(1); - Value.assertNotNull(v1, v2); - return fun.apply(v1, v2); - } + public Value eval(List parameters) { return fun.apply(parameters.get(0), parameters.get(1)); } }); } @@ -342,12 +323,7 @@ public class Expression functions.put(name, new AbstractFunction(-1, name) { @Override - public Value eval(List parameters) - { - for (Value v: parameters) - Value.assertNotNull(v); - return fun.apply(parameters); - } + public Value eval(List parameters) { return fun.apply(parameters); } }); } @@ -375,6 +351,7 @@ public class Expression @Override public LazyValue lazyEval(Context c, Integer i, Expression e, Tokenizer.Token t, List lazyParams) { + ILazyFunction.checkInterrupts(); try { return fun.apply(c, i, lazyParams); @@ -394,6 +371,7 @@ public class Expression @Override public LazyValue lazyEval(Context c, Integer i, Expression e, Tokenizer.Token t, List lazyParams) { + ILazyFunction.checkInterrupts(); try { Value ret = fun.apply(c, i, unpackArgs(lazyParams, c, Context.NONE)); diff --git a/src/main/java/carpet/script/Fluff.java b/src/main/java/carpet/script/Fluff.java index 122d75c5..4cb78357 100644 --- a/src/main/java/carpet/script/Fluff.java +++ b/src/main/java/carpet/script/Fluff.java @@ -37,6 +37,12 @@ public abstract class Fluff boolean numParamsVaries(); LazyValue lazyEval(Context c, Integer type, Expression expr, Tokenizer.Token token, List lazyParams); + + static void checkInterrupts() + { + if (ScriptHost.mainThread != Thread.currentThread() && Thread.currentThread().isInterrupted()) + throw new InternalExpressionException("Thread interrupted"); + } // lazy function has a chance to change execution based on contxt } @@ -130,6 +136,7 @@ public abstract class Fluff private List params; public Value evalValue(Context c, Integer type) { + ILazyFunction.checkInterrupts(); return AbstractFunction.this.eval(getParams(c)); } @@ -210,7 +217,7 @@ public abstract class Fluff { throw new ExpressionException(cc, e, t, "Did not expect a second parameter for unary operator"); } - return (c, ignored_type) -> AbstractUnaryOperator.this.evalUnary(v1.evalValue(c)); + return (c, ignored_type) -> AbstractUnaryOperator.this.evalUnary(v1.evalValue(c, Context.NONE)); } catch (RuntimeException exc) { diff --git a/src/main/java/carpet/script/ScriptHost.java b/src/main/java/carpet/script/ScriptHost.java index f57ae663..4d838c28 100644 --- a/src/main/java/carpet/script/ScriptHost.java +++ b/src/main/java/carpet/script/ScriptHost.java @@ -19,8 +19,10 @@ import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Stream; @@ -29,6 +31,7 @@ public abstract class ScriptHost public static Map systemGlobals = new ConcurrentHashMap<>(); private static final Map randomizers = new Long2ObjectOpenHashMap<>(); + public static Thread mainThread = null; private final Map executorServices = new HashMap<>(); private final Map locks = new ConcurrentHashMap<>(); protected boolean inTermination = false; @@ -113,6 +116,7 @@ public abstract class ScriptHost initializeModuleGlobals(moduleData); this.moduleData.put(code, moduleData); this.modules.put(code==null?null:code.getName(), code); + mainThread = Thread.currentThread(); } void initializeModuleGlobals(ModuleData md) @@ -375,8 +379,32 @@ public abstract class ScriptHost public void onClose() { inTermination = true; + executorServices.values().forEach(ThreadPoolExecutor::shutdown); for (ScriptHost uh : userHosts.values()) uh.onClose(); - executorServices.values().forEach(e -> {e.shutdown(); e.shutdownNow();}); + if (taskCount() > 0) + { + executorServices.values().forEach(e -> { + ExecutorService stopper = Executors.newSingleThreadExecutor(); + stopper.submit( () -> { + try { + // Wait a while for existing tasks to terminate + if (!e.awaitTermination(1500, TimeUnit.MILLISECONDS)) { + e.shutdownNow(); // Cancel currently executing tasks + // Wait a while for tasks to respond to being cancelled + if (!e.awaitTermination(1500, TimeUnit.MILLISECONDS)) + CarpetScriptServer.LOG.error("Failed to stop app's thread"); + } + } catch (InterruptedException ie) { + // (Re-)Cancel if current thread also interrupted + e.shutdownNow(); + // Preserve interrupt status + Thread.currentThread().interrupt(); + } + stopper.shutdown(); + stopper.shutdownNow(); + }); + }); + } } public void setPerPlayer(boolean isPerUser) diff --git a/src/main/java/carpet/script/value/Value.java b/src/main/java/carpet/script/value/Value.java index 389ede83..341af7c4 100644 --- a/src/main/java/carpet/script/value/Value.java +++ b/src/main/java/carpet/script/value/Value.java @@ -24,21 +24,6 @@ public abstract class Value implements Comparable, Cloneable public String boundVariable; - public static T assertNotNull(T t) - { - if (t == null) - throw new InternalExpressionException("Operand may not be null"); - return t; - } - - public static void assertNotNull(T t1, T t2) - { - if (t1 == null) - throw new InternalExpressionException("First operand may not be null"); - if (t2 == null) - throw new InternalExpressionException("Second operand may not be null"); - } - public boolean isBound() { return boundVariable != null;