From de08c8b8c73e96a91c49f7599a58ef4eb2a61f8f Mon Sep 17 00:00:00 2001 From: wizjany Date: Wed, 6 Mar 2019 19:58:32 -0500 Subject: [PATCH] Add better control over expression timeouts. (#451) Add better control over expression timeouts. * //timeout command can be used to change player's current timeout. * Config now also has a max timeout, can be bypassed with permission * Timeout of < 0 will let expressions run indefinitely. * Said expressions won't run on a separate thread, slightly reducing the overhead from context switching. For large //gen commands, for example, this can actually increase speed. --- .../java/com/sk89q/worldedit/EditSession.java | 94 ++++++++++++++++--- .../sk89q/worldedit/LocalConfiguration.java | 1 + .../com/sk89q/worldedit/LocalSession.java | 19 ++++ .../worldedit/command/GeneralCommands.java | 40 +++++++- .../worldedit/command/GenerationCommands.java | 6 +- .../worldedit/command/RegionCommands.java | 2 +- .../worldedit/command/UtilityCommands.java | 12 ++- .../command/composition/SelectionCommand.java | 1 + .../composition/ShapedBrushCommand.java | 2 +- .../tool/brush/OperationFactoryBrush.java | 8 ++ .../parser/mask/ExpressionMaskParser.java | 8 ++ .../sk89q/worldedit/function/EditContext.java | 10 ++ .../worldedit/function/factory/Deform.java | 12 ++- .../function/mask/ExpressionMask.java | 20 +++- .../function/mask/ExpressionMask2D.java | 19 +++- .../internal/expression/Expression.java | 50 ++++++---- .../runtime/ExpressionTimeoutException.java | 29 ++++++ .../worldedit/session/SessionManager.java | 34 +++---- .../util/PropertiesConfiguration.java | 1 + .../worldedit/util/YAMLConfiguration.java | 1 + .../internal/expression/ExpressionTest.java | 8 +- 21 files changed, 301 insertions(+), 76 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionTimeoutException.java diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java index 38766ba6b..966479e98 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/EditSession.java @@ -81,6 +81,7 @@ import com.sk89q.worldedit.history.changeset.BlockOptimizedHistory; import com.sk89q.worldedit.history.changeset.ChangeSet; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; +import com.sk89q.worldedit.internal.expression.runtime.ExpressionTimeoutException; import com.sk89q.worldedit.internal.expression.runtime.RValue; import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; @@ -1879,7 +1880,42 @@ public class EditSession implements Extent, AutoCloseable { return count.getDistribution(); } - public int makeShape(final Region region, final Vector3 zero, final Vector3 unit, final Pattern pattern, final String expressionString, final boolean hollow) throws ExpressionException, MaxChangedBlocksException { + /** + * Generate a shape for the given expression. + * + * @param region the region to generate the shape in + * @param zero the coordinate origin for x/y/z variables + * @param unit the scale of the x/y/z/ variables + * @param pattern the default material to make the shape from + * @param expressionString the expression defining the shape + * @param hollow whether the shape should be hollow + * @return number of blocks changed + * @throws ExpressionException + * @throws MaxChangedBlocksException + */ + public int makeShape(final Region region, final Vector3 zero, final Vector3 unit, + final Pattern pattern, final String expressionString, final boolean hollow) + throws ExpressionException, MaxChangedBlocksException { + return makeShape(region, zero, unit, pattern, expressionString, hollow, WorldEdit.getInstance().getConfiguration().calculationTimeout); + } + + /** + * Generate a shape for the given expression. + * + * @param region the region to generate the shape in + * @param zero the coordinate origin for x/y/z variables + * @param unit the scale of the x/y/z/ variables + * @param pattern the default material to make the shape from + * @param expressionString the expression defining the shape + * @param hollow whether the shape should be hollow + * @param timeout the time, in milliseconds, to wait for each expression evaluation before halting it. -1 to disable + * @return number of blocks changed + * @throws ExpressionException + * @throws MaxChangedBlocksException + */ + public int makeShape(final Region region, final Vector3 zero, final Vector3 unit, + final Pattern pattern, final String expressionString, final boolean hollow, final int timeout) + throws ExpressionException, MaxChangedBlocksException { final Expression expression = Expression.compile(expressionString, "x", "y", "z", "type", "data"); expression.optimize(); @@ -1889,6 +1925,7 @@ public class EditSession implements Extent, AutoCloseable { final WorldEditExpressionEnvironment environment = new WorldEditExpressionEnvironment(this, unit, zero); expression.setEnvironment(environment); + final int[] timedOut = {0}; final ArbitraryShape shape = new ArbitraryShape(region) { @Override protected BaseBlock getMaterial(int x, int y, int z, BaseBlock defaultMaterial) { @@ -1906,27 +1943,42 @@ public class EditSession implements Extent, AutoCloseable { dataVar = legacy[1]; } } - if (expression.evaluate(scaled.getX(), scaled.getY(), scaled.getZ(), typeVar, dataVar) <= 0) { + if (expression.evaluate(new double[]{scaled.getX(), scaled.getY(), scaled.getZ(), typeVar, dataVar}, timeout) <= 0) { return null; } int newType = (int) typeVariable.getValue(); int newData = (int) dataVariable.getValue(); if (newType != typeVar || newData != dataVar) { - return LegacyMapper.getInstance().getBlockFromLegacy((int) typeVariable.getValue(), (int) dataVariable.getValue()).toBaseBlock(); + BlockState state = LegacyMapper.getInstance().getBlockFromLegacy(newType, newData); + return state == null ? defaultMaterial : state.toBaseBlock(); } else { return defaultMaterial; } + } catch (ExpressionTimeoutException e) { + timedOut[0] = timedOut[0] + 1; + return null; } catch (Exception e) { log.log(Level.WARNING, "Failed to create shape", e); return null; } } }; - - return shape.generate(this, pattern, hollow); + int changed = shape.generate(this, pattern, hollow); + if (timedOut[0] > 0) { + throw new ExpressionTimeoutException( + String.format("%d blocks changed. %d blocks took too long to evaluate (increase with //timeout).", + changed, timedOut[0])); + } + return changed; } - public int deformRegion(final Region region, final Vector3 zero, final Vector3 unit, final String expressionString) throws ExpressionException, MaxChangedBlocksException { + public int deformRegion(final Region region, final Vector3 zero, final Vector3 unit, final String expressionString) + throws ExpressionException, MaxChangedBlocksException { + return deformRegion(region, zero, unit, expressionString, WorldEdit.getInstance().getConfiguration().calculationTimeout); + } + + public int deformRegion(final Region region, final Vector3 zero, final Vector3 unit, final String expressionString, + final int timeout) throws ExpressionException, MaxChangedBlocksException { final Expression expression = Expression.compile(expressionString, "x", "y", "z"); expression.optimize(); @@ -1944,7 +1996,7 @@ public class EditSession implements Extent, AutoCloseable { final Vector3 scaled = position.toVector3().subtract(zero).divide(unit); // transform - expression.evaluate(scaled.getX(), scaled.getY(), scaled.getZ()); + expression.evaluate(new double[]{scaled.getX(), scaled.getY(), scaled.getZ()}, timeout); final BlockVector3 sourcePosition = environment.toWorld(x.getValue(), y.getValue(), z.getValue()); @@ -2131,7 +2183,8 @@ public class EditSession implements Extent, AutoCloseable { * @return number of blocks affected * @throws MaxChangedBlocksException thrown if too many blocks are changed */ - public int drawSpline(Pattern pattern, List nodevectors, double tension, double bias, double continuity, double quality, double radius, boolean filled) + public int drawSpline(Pattern pattern, List nodevectors, double tension, double bias, + double continuity, double quality, double radius, boolean filled) throws MaxChangedBlocksException { Set vset = new HashSet<>(); @@ -2231,7 +2284,15 @@ public class EditSession implements Extent, AutoCloseable { } } - public int makeBiomeShape(final Region region, final Vector3 zero, final Vector3 unit, final BiomeType biomeType, final String expressionString, final boolean hollow) throws ExpressionException, MaxChangedBlocksException { + public int makeBiomeShape(final Region region, final Vector3 zero, final Vector3 unit, final BiomeType biomeType, + final String expressionString, final boolean hollow) + throws ExpressionException, MaxChangedBlocksException { + return makeBiomeShape(region, zero, unit, biomeType, expressionString, hollow, WorldEdit.getInstance().getConfiguration().calculationTimeout); + } + + public int makeBiomeShape(final Region region, final Vector3 zero, final Vector3 unit, final BiomeType biomeType, + final String expressionString, final boolean hollow, final int timeout) + throws ExpressionException, MaxChangedBlocksException { final Vector2 zero2D = zero.toVector2(); final Vector2 unit2D = unit.toVector2(); @@ -2242,6 +2303,7 @@ public class EditSession implements Extent, AutoCloseable { final WorldEditExpressionEnvironment environment = new WorldEditExpressionEnvironment(editSession, unit, zero); expression.setEnvironment(environment); + final int[] timedOut = {0}; final ArbitraryBiomeShape shape = new ArbitraryBiomeShape(region) { @Override protected BiomeType getBiome(int x, int z, BiomeType defaultBiomeType) { @@ -2250,20 +2312,28 @@ public class EditSession implements Extent, AutoCloseable { final Vector2 scaled = current.subtract(zero2D).divide(unit2D); try { - if (expression.evaluate(scaled.getX(), scaled.getZ()) <= 0) { + if (expression.evaluate(new double[]{scaled.getX(), scaled.getZ()}, timeout) <= 0) { return null; } // TODO: Allow biome setting via a script variable (needs BiomeType<->int mapping) return defaultBiomeType; + } catch (ExpressionTimeoutException e) { + timedOut[0] = timedOut[0] + 1; + return null; } catch (Exception e) { log.log(Level.WARNING, "Failed to create shape", e); return null; } } }; - - return shape.generate(this, biomeType, hollow); + int changed = shape.generate(this, biomeType, hollow); + if (timedOut[0] > 0) { + throw new ExpressionTimeoutException( + String.format("%d blocks changed. %d blocks took too long to evaluate (increase time with //timeout)", + changed, timedOut[0])); + } + return changed; } private static final BlockVector3[] recurseDirections = { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java index 1e9d19660..51ace98a8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalConfiguration.java @@ -67,6 +67,7 @@ public abstract class LocalConfiguration { public int navigationWandMaxDistance = 50; public int scriptTimeout = 3000; public int calculationTimeout = 100; + public int maxCalculationTimeout = 300; public Set allowedDataCycleBlocks = new HashSet<>(); public String saveDir = "schematics"; public String scriptsDir = "craftscripts"; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java index 5d1c50806..1d0968aa6 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java @@ -85,6 +85,7 @@ public class LocalSession { private transient BlockTool pickaxeMode = new SinglePickaxe(); private transient Map tools = new HashMap<>(); private transient int maxBlocksChanged = -1; + private transient int maxTimeoutTime; private transient boolean useInventory; private transient Snapshot snapshot; private transient boolean hasCUISupport = false; @@ -415,6 +416,24 @@ public class LocalSession { this.maxBlocksChanged = maxBlocksChanged; } + /** + * Get the maximum time allowed for certain executions to run before cancelling them, such as expressions. + * + * @return timeout time, in milliseconds + */ + public int getTimeout() { + return maxTimeoutTime; + } + + /** + * Set the maximum number of blocks that can be changed. + * + * @param timeout the time, in milliseconds, to limit certain executions to, or -1 to disable + */ + public void setTimeout(int timeout) { + this.maxTimeoutTime = timeout; + } + /** * Checks whether the super pick axe is enabled. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java index 5e4f608ed..b16aece4e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GeneralCommands.java @@ -57,9 +57,9 @@ public class GeneralCommands { @Command( aliases = { "/limit" }, - usage = "", + usage = "[limit]", desc = "Modify block change limit", - min = 1, + min = 0, max = 1 ) @CommandPermissions("worldedit.limit") @@ -68,7 +68,7 @@ public class GeneralCommands { LocalConfiguration config = worldEdit.getConfiguration(); boolean mayDisable = player.hasPermission("worldedit.limit.unrestricted"); - int limit = Math.max(-1, args.getInteger(0)); + int limit = args.argsLength() == 0 ? config.defaultChangeLimit : Math.max(-1, args.getInteger(0)); if (!mayDisable && config.maxChangeLimit > -1) { if (limit > config.maxChangeLimit) { player.printError("Your maximum allowable limit is " + config.maxChangeLimit + "."); @@ -78,13 +78,43 @@ public class GeneralCommands { session.setBlockChangeLimit(limit); - if (limit != -1) { - player.print("Block change limit set to " + limit + ". (Use //limit -1 to go back to the default.)"); + if (limit != config.defaultChangeLimit) { + player.print("Block change limit set to " + limit + ". (Use //limit to go back to the default.)"); } else { player.print("Block change limit set to " + limit + "."); } } + @Command( + aliases = { "/timeout" }, + usage = "[time]", + desc = "Modify evaluation timeout time.", + min = 0, + max = 1 + ) + @CommandPermissions("worldedit.timeout") + public void timeout(Player player, LocalSession session, EditSession editSession, CommandContext args) throws WorldEditException { + + LocalConfiguration config = worldEdit.getConfiguration(); + boolean mayDisable = player.hasPermission("worldedit.timeout.unrestricted"); + + int limit = args.argsLength() == 0 ? config.calculationTimeout : Math.max(-1, args.getInteger(0)); + if (!mayDisable && config.maxCalculationTimeout > -1) { + if (limit > config.maxCalculationTimeout) { + player.printError("Your maximum allowable timeout is " + config.maxCalculationTimeout + " ms."); + return; + } + } + + session.setTimeout(limit); + + if (limit != config.calculationTimeout) { + player.print("Timeout time set to " + limit + " ms. (Use //timeout to go back to the default.)"); + } else { + player.print("Timeout time set to " + limit + " ms."); + } + } + @Command( aliases = { "/fast" }, usage = "[on|off]", diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java index 5d48ac3d9..d16edd976 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/GenerationCommands.java @@ -306,7 +306,7 @@ public class GenerationCommands { } try { - final int affected = editSession.makeShape(region, zero, unit, pattern, expression, hollow); + final int affected = editSession.makeShape(region, zero, unit, pattern, expression, hollow, session.getTimeout()); player.findFreePosition(); player.print(affected + " block(s) have been created."); } catch (ExpressionException e) { @@ -333,7 +333,7 @@ public class GenerationCommands { min = 2, max = -1 ) - @CommandPermissions({"worldedit.generation.shape", "worldedit.biome.set"}) + @CommandPermissions("worldedit.generation.shape.biome") @Logging(ALL) public void generateBiome(Player player, LocalSession session, EditSession editSession, @Selection Region region, @@ -371,7 +371,7 @@ public class GenerationCommands { } try { - final int affected = editSession.makeBiomeShape(region, zero, unit, target, expression, hollow); + final int affected = editSession.makeBiomeShape(region, zero, unit, target, expression, hollow, session.getTimeout()); player.findFreePosition(); player.print("" + affected + " columns affected."); } catch (ExpressionException e) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java index 00c70d5c6..b02b2ba42 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/RegionCommands.java @@ -404,7 +404,7 @@ public class RegionCommands { } try { - final int affected = editSession.deformRegion(region, zero, unit, expression); + final int affected = editSession.deformRegion(region, zero, unit, expression, session.getTimeout()); player.findFreePosition(); player.print(affected + " block(s) have been deformed."); } catch (ExpressionException e) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java index bd8ae4da9..8768be6c8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/UtilityCommands.java @@ -52,6 +52,7 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.CylinderRegion; import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.session.SessionOwner; import com.sk89q.worldedit.util.command.CommandCallable; import com.sk89q.worldedit.util.command.CommandMapping; import com.sk89q.worldedit.util.command.Dispatcher; @@ -541,13 +542,18 @@ public class UtilityCommands { public void calc(Actor actor, @Text String input) throws CommandException { try { Expression expression = Expression.compile(input); - actor.print("= " + expression.evaluate()); + if (actor instanceof SessionOwner) { + actor.print("= " + expression.evaluate( + new double[]{}, WorldEdit.getInstance().getSessionManager().get((SessionOwner) actor).getTimeout())); + } else { + actor.print("= " + expression.evaluate()); + } } catch (EvaluationException e) { actor.printError(String.format( - "'%s' could not be parsed as a valid expression", input)); + "'%s' could not be evaluated (error: %s)", input, e.getMessage())); } catch (ExpressionException e) { actor.printError(String.format( - "'%s' could not be evaluated (error: %s)", input, e.getMessage())); + "'%s' could not be parsed as a valid expression", input)); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/composition/SelectionCommand.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/composition/SelectionCommand.java index ffb06db2e..a8e89e364 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/composition/SelectionCommand.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/composition/SelectionCommand.java @@ -79,6 +79,7 @@ public class SelectionCommand extends SimpleCommand { EditContext editContext = new EditContext(); editContext.setDestination(locals.get(EditSession.class)); editContext.setRegion(selection); + editContext.setSession(session); Operation operation = operationFactory.createFromContext(editContext); Operations.completeBlindly(operation); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/composition/ShapedBrushCommand.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/composition/ShapedBrushCommand.java index 2fa613db4..98daedde2 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/composition/ShapedBrushCommand.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/composition/ShapedBrushCommand.java @@ -75,7 +75,7 @@ public class ShapedBrushCommand extends SimpleCommand { BrushTool tool = session.getBrushTool(player.getItemInHand(HandSide.MAIN_HAND).getType()); tool.setSize(radius); tool.setFill(null); - tool.setBrush(new OperationFactoryBrush(factory, regionFactory), permission); + tool.setBrush(new OperationFactoryBrush(factory, regionFactory, session), permission); } catch (MaxBrushRadiusException | InvalidToolBindException e) { WorldEdit.getInstance().getPlatformManager().getCommandManager().getExceptionConverter().convert(e); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/brush/OperationFactoryBrush.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/brush/OperationFactoryBrush.java index 6a323f14b..1d7c5e7ce 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/brush/OperationFactoryBrush.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/brush/OperationFactoryBrush.java @@ -20,6 +20,7 @@ package com.sk89q.worldedit.command.tool.brush; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.function.Contextual; import com.sk89q.worldedit.function.EditContext; @@ -33,10 +34,16 @@ public class OperationFactoryBrush implements Brush { private final Contextual operationFactory; private final RegionFactory regionFactory; + private final LocalSession session; public OperationFactoryBrush(Contextual operationFactory, RegionFactory regionFactory) { + this(operationFactory, regionFactory, null); + } + + public OperationFactoryBrush(Contextual operationFactory, RegionFactory regionFactory, LocalSession session) { this.operationFactory = operationFactory; this.regionFactory = regionFactory; + this.session = session; } @Override @@ -45,6 +52,7 @@ public class OperationFactoryBrush implements Brush { context.setDestination(editSession); context.setRegion(regionFactory.createCenteredAt(position, size)); context.setFill(pattern); + context.setSession(session); Operation operation = operationFactory.createFromContext(context); Operations.completeLegacy(operation); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/mask/ExpressionMaskParser.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/mask/ExpressionMaskParser.java index ba5fac5f1..9267af44a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/mask/ExpressionMaskParser.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/factory/parser/mask/ExpressionMaskParser.java @@ -29,8 +29,11 @@ import com.sk89q.worldedit.internal.expression.ExpressionException; import com.sk89q.worldedit.internal.registry.InputParser; import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.regions.shape.WorldEditExpressionEnvironment; +import com.sk89q.worldedit.session.SessionOwner; import com.sk89q.worldedit.session.request.Request; +import java.util.function.IntSupplier; + public class ExpressionMaskParser extends InputParser { public ExpressionMaskParser(WorldEdit worldEdit) { @@ -48,6 +51,11 @@ public class ExpressionMaskParser extends InputParser { WorldEditExpressionEnvironment env = new WorldEditExpressionEnvironment( Request.request().getEditSession(), Vector3.ONE, Vector3.ZERO); exp.setEnvironment(env); + if (context.getActor() instanceof SessionOwner) { + SessionOwner owner = (SessionOwner) context.getActor(); + IntSupplier timeout = () -> WorldEdit.getInstance().getSessionManager().get(owner).getTimeout(); + return new ExpressionMask(exp, timeout); + } return new ExpressionMask(exp); } catch (ExpressionException e) { throw new InputParseException("Invalid expression: " + e.getMessage()); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/EditContext.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/EditContext.java index 07c1515ba..b26f8d74f 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/EditContext.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/EditContext.java @@ -21,6 +21,7 @@ package com.sk89q.worldedit.function; import static com.google.common.base.Preconditions.checkNotNull; +import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.function.pattern.Pattern; import com.sk89q.worldedit.regions.Region; @@ -32,6 +33,7 @@ public class EditContext { private Extent destination; @Nullable private Region region; @Nullable private Pattern fill; + @Nullable private LocalSession session; public Extent getDestination() { return destination; @@ -60,4 +62,12 @@ public class EditContext { this.fill = fill; } + @Nullable + public LocalSession getSession() { + return session; + } + + public void setSession(@Nullable LocalSession session) { + this.session = session; + } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/Deform.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/Deform.java index ac52933be..0098d455a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/Deform.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/factory/Deform.java @@ -23,6 +23,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.sk89q.worldedit.util.GuavaUtil.firstNonNull; import com.sk89q.worldedit.EditSession; +import com.sk89q.worldedit.LocalSession; +import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.extent.NullExtent; @@ -147,7 +149,9 @@ public class Deform implements Contextual { unit = Vector3.ONE; } - return new DeformOperation(context.getDestination(), region, zero, unit, expression); + LocalSession session = context.getSession(); + return new DeformOperation(context.getDestination(), region, zero, unit, expression, + session == null ? WorldEdit.getInstance().getConfiguration().calculationTimeout : session.getTimeout()); } private static final class DeformOperation implements Operation { @@ -156,20 +160,22 @@ public class Deform implements Contextual { private final Vector3 zero; private final Vector3 unit; private final String expression; + private final int timeout; - private DeformOperation(Extent destination, Region region, Vector3 zero, Vector3 unit, String expression) { + private DeformOperation(Extent destination, Region region, Vector3 zero, Vector3 unit, String expression, int timeout) { this.destination = destination; this.region = region; this.zero = zero; this.unit = unit; this.expression = expression; + this.timeout = timeout; } @Override public Operation resume(RunContext run) throws WorldEditException { try { // TODO: Move deformation code - ((EditSession) destination).deformRegion(region, zero, unit, expression); + ((EditSession) destination).deformRegion(region, zero, unit, expression, timeout); return null; } catch (ExpressionException e) { throw new RuntimeException("Failed to execute expression", e); // TODO: Better exception to throw here? diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask.java index 9f597e267..d8ddcc704 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask.java @@ -21,6 +21,7 @@ package com.sk89q.worldedit.function.mask; import static com.google.common.base.Preconditions.checkNotNull; +import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; @@ -28,6 +29,7 @@ import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.shape.WorldEditExpressionEnvironment; import javax.annotation.Nullable; +import java.util.function.IntSupplier; /** * A mask that evaluates an expression. @@ -38,6 +40,7 @@ import javax.annotation.Nullable; public class ExpressionMask extends AbstractMask { private final Expression expression; + private final IntSupplier timeout; /** * Create a new instance. @@ -46,8 +49,7 @@ public class ExpressionMask extends AbstractMask { * @throws ExpressionException thrown if there is an error with the expression */ public ExpressionMask(String expression) throws ExpressionException { - checkNotNull(expression); - this.expression = Expression.compile(expression, "x", "y", "z"); + this(Expression.compile(checkNotNull(expression), "x", "y", "z")); } /** @@ -56,8 +58,13 @@ public class ExpressionMask extends AbstractMask { * @param expression the expression */ public ExpressionMask(Expression expression) { + this(expression, null); + } + + public ExpressionMask(Expression expression, @Nullable IntSupplier timeout) { checkNotNull(expression); this.expression = expression; + this.timeout = timeout; } @Override @@ -66,7 +73,12 @@ public class ExpressionMask extends AbstractMask { if (expression.getEnvironment() instanceof WorldEditExpressionEnvironment) { ((WorldEditExpressionEnvironment) expression.getEnvironment()).setCurrentBlock(vector.toVector3()); } - return expression.evaluate(vector.getX(), vector.getY(), vector.getZ()) > 0; + if (timeout == null) { + return expression.evaluate(vector.getX(), vector.getY(), vector.getZ()) > 0; + } else { + return expression.evaluate(new double[]{vector.getX(), vector.getY(), vector.getZ()}, + timeout.getAsInt()) > 0; + } } catch (EvaluationException e) { return false; } @@ -75,7 +87,7 @@ public class ExpressionMask extends AbstractMask { @Nullable @Override public Mask2D toMask2D() { - return new ExpressionMask2D(expression); + return new ExpressionMask2D(expression, timeout); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask2D.java b/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask2D.java index ffc6c9a94..0f4d9b198 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask2D.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/function/mask/ExpressionMask2D.java @@ -21,14 +21,19 @@ package com.sk89q.worldedit.function.mask; import static com.google.common.base.Preconditions.checkNotNull; +import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.ExpressionException; import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; import com.sk89q.worldedit.math.BlockVector2; +import javax.annotation.Nullable; +import java.util.function.IntSupplier; + public class ExpressionMask2D extends AbstractMask2D { private final Expression expression; + private final IntSupplier timeout; /** * Create a new instance. @@ -37,8 +42,7 @@ public class ExpressionMask2D extends AbstractMask2D { * @throws ExpressionException thrown if there is an error with the expression */ public ExpressionMask2D(String expression) throws ExpressionException { - checkNotNull(expression); - this.expression = Expression.compile(expression, "x", "z"); + this(Expression.compile(checkNotNull(expression), "x", "z")); } /** @@ -47,14 +51,23 @@ public class ExpressionMask2D extends AbstractMask2D { * @param expression the expression */ public ExpressionMask2D(Expression expression) { + this(expression, null); + } + + public ExpressionMask2D(Expression expression, @Nullable IntSupplier timeout) { checkNotNull(expression); this.expression = expression; + this.timeout = timeout; } @Override public boolean test(BlockVector2 vector) { try { - return expression.evaluate(vector.getX(), 0, vector.getZ()) > 0; + if (timeout != null) { + return expression.evaluate(vector.getX(), 0, vector.getZ()) > 0; + } else { + return expression.evaluate(new double[]{vector.getX(), 0, vector.getZ()}, timeout.getAsInt()) > 0; + } } catch (EvaluationException e) { return false; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java index ec774f088..944fd97d8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java @@ -27,6 +27,7 @@ import com.sk89q.worldedit.internal.expression.parser.Parser; import com.sk89q.worldedit.internal.expression.runtime.Constant; import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; import com.sk89q.worldedit.internal.expression.runtime.ExpressionEnvironment; +import com.sk89q.worldedit.internal.expression.runtime.ExpressionTimeoutException; import com.sk89q.worldedit.internal.expression.runtime.Functions; import com.sk89q.worldedit.internal.expression.runtime.RValue; import com.sk89q.worldedit.internal.expression.runtime.ReturnException; @@ -36,7 +37,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -117,6 +117,10 @@ public class Expression { } public double evaluate(double... values) throws EvaluationException { + return evaluate(values, WorldEdit.getInstance().getConfiguration().calculationTimeout); + } + + public double evaluate(double[] values, int timeout) throws EvaluationException { for (int i = 0; i < values.length; ++i) { final String variableName = variableNames[i]; final RValue invokable = variables.get(variableName); @@ -127,34 +131,44 @@ public class Expression { ((Variable) invokable).value = values[i]; } - Future result = evalThread.submit(new Callable() { - @Override - public Double call() throws Exception { - pushInstance(); - try { - return root.getValue(); - } finally { - popInstance(); - } - } - }); try { - return result.get(WorldEdit.getInstance().getConfiguration().calculationTimeout, TimeUnit.MILLISECONDS); + if (timeout < 0) { + return evaluateRoot(); + } + return evaluateRootTimed(timeout); + } catch (ReturnException e) { + return e.getValue(); + } // other evaluation exceptions are thrown out of this method + } + + private double evaluateRootTimed(int timeout) throws EvaluationException { + Future result = evalThread.submit(this::evaluateRoot); + try { + return result.get(timeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); + } catch (TimeoutException e) { + result.cancel(true); + throw new ExpressionTimeoutException("Calculations exceeded time limit."); } catch (ExecutionException e) { Throwable cause = e.getCause(); - if (cause instanceof ReturnException) { - return ((ReturnException) cause).getValue(); + if (cause instanceof EvaluationException) { + throw (EvaluationException) cause; } if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } throw new RuntimeException(cause); - } catch (TimeoutException e) { - result.cancel(true); - throw new EvaluationException(-1, "Calculations exceeded time limit."); + } + } + + private Double evaluateRoot() throws EvaluationException { + pushInstance(); + try { + return root.getValue(); + } finally { + popInstance(); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionTimeoutException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionTimeoutException.java new file mode 100644 index 000000000..ce7d55140 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/runtime/ExpressionTimeoutException.java @@ -0,0 +1,29 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression.runtime; + +/** + * Thrown when an evaluation exceeds the timeout time. + */ +public class ExpressionTimeoutException extends EvaluationException { + public ExpressionTimeoutException(String message) { + super(-1, message); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/session/SessionManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/session/SessionManager.java index af4fe5e3e..1da0c58cc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/session/SessionManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/session/SessionManager.java @@ -155,31 +155,20 @@ public class SessionManager { session.setConfiguration(config); session.setBlockChangeLimit(config.defaultChangeLimit); + session.setTimeout(config.calculationTimeout); // Remember the session regardless of if it's currently active or not. // And have the SessionTracker FLUSH inactive sessions. sessions.put(getKey(owner), new SessionHolder(sessionKey, session)); } - // Set the limit on the number of blocks that an operation can - // change at once, or don't if the owner has an override or there - // is no limit. There is also a default limit - int currentChangeLimit = session.getBlockChangeLimit(); - - if (!owner.hasPermission("worldedit.limit.unrestricted") && config.maxChangeLimit > -1) { - // If the default limit is infinite but there is a maximum - // limit, make sure to not have it be overridden - if (config.defaultChangeLimit < 0) { - if (currentChangeLimit < 0 || currentChangeLimit > config.maxChangeLimit) { - session.setBlockChangeLimit(config.maxChangeLimit); - } - } else { - // Bound the change limit - int maxChangeLimit = config.maxChangeLimit; - if (currentChangeLimit == -1 || currentChangeLimit > maxChangeLimit) { - session.setBlockChangeLimit(maxChangeLimit); - } - } + if (shouldBoundLimit(owner.hasPermission("worldedit.limit.unrestricted"), + session.getBlockChangeLimit(), config.maxChangeLimit)) { + session.setBlockChangeLimit(config.maxChangeLimit); + } + if (shouldBoundLimit(owner.hasPermission("worldedit.timeout.unrestricted"), + session.getTimeout(), config.maxCalculationTimeout)) { + session.setTimeout(config.maxCalculationTimeout); } // Have the session use inventory if it's enabled and the owner @@ -192,6 +181,13 @@ public class SessionManager { return session; } + private boolean shouldBoundLimit(boolean mayBypass, int currentLimit, int maxLimit) { + if (!mayBypass && maxLimit > -1) { // if player can't bypass and max is finite + return currentLimit < 0 || currentLimit > maxLimit; // make sure current is finite and less than max + } + return false; + } + /** * Save a map of sessions to disk. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java index b9dc1cece..acc845914 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/PropertiesConfiguration.java @@ -112,6 +112,7 @@ public class PropertiesConfiguration extends LocalConfiguration { navigationUseGlass = getBool("nav-use-glass", navigationUseGlass); scriptTimeout = getInt("scripting-timeout", scriptTimeout); calculationTimeout = getInt("calculation-timeout", calculationTimeout); + maxCalculationTimeout = getInt("max-calculation-timeout", maxCalculationTimeout); saveDir = getString("schematic-save-dir", saveDir); scriptsDir = getString("craftscript-dir", scriptsDir); butcherDefaultRadius = getInt("butcher-default-radius", butcherDefaultRadius); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java index fcfa35fd6..10940bbd9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/YAMLConfiguration.java @@ -108,6 +108,7 @@ public class YAMLConfiguration extends LocalConfiguration { scriptsDir = config.getString("scripting.dir", scriptsDir); calculationTimeout = config.getInt("calculation.timeout", calculationTimeout); + maxCalculationTimeout = config.getInt("calculation.max-timeout", maxCalculationTimeout); saveDir = config.getString("saving.dir", saveDir); diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java index bdb91abc2..28ad67b37 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java @@ -64,7 +64,7 @@ public class ExpressionTest { assertEquals(atan2(3, 4), simpleEval("atan2(3, 4)"), 0); // check variables - assertEquals(8, compile("foo+bar", "foo", "bar").evaluate(5, 3), 0); + assertEquals(8, compile("foo+bar", "foo", "bar").evaluate(5D, 3D), 0); } @Test @@ -123,7 +123,7 @@ public class ExpressionTest { @Test public void testAssign() throws ExpressionException { Expression foo = compile("{a=x} b=y; c=z", "x", "y", "z", "a", "b", "c"); - foo.evaluate(2, 3, 5); + foo.evaluate(2D, 3D, 5D); assertEquals(2, foo.getVariable("a", false).getValue(), 0); assertEquals(3, foo.getVariable("b", false).getValue(), 0); assertEquals(5, foo.getVariable("c", false).getValue(), 0); @@ -136,13 +136,13 @@ public class ExpressionTest { // test 'dangling else' final Expression expression1 = compile("if (1) if (0) x=4; else y=5;", "x", "y"); - expression1.evaluate(1, 2); + expression1.evaluate(1D, 2D); assertEquals(1, expression1.getVariable("x", false).getValue(), 0); assertEquals(5, expression1.getVariable("y", false).getValue(), 0); // test if the if construct is correctly recognized as a statement final Expression expression2 = compile("if (0) if (1) x=5; y=4;", "x", "y"); - expression2.evaluate(1, 2); + expression2.evaluate(1D, 2D); assertEquals(4, expression2.getVariable("y", false).getValue(), 0); }