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.
This commit is contained in:
wizjany 2019-03-06 19:58:32 -05:00 committed by GitHub
parent f84f3c6f85
commit de08c8b8c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 301 additions and 76 deletions

View File

@ -81,6 +81,7 @@ import com.sk89q.worldedit.history.changeset.BlockOptimizedHistory;
import com.sk89q.worldedit.history.changeset.ChangeSet; import com.sk89q.worldedit.history.changeset.ChangeSet;
import com.sk89q.worldedit.internal.expression.Expression; import com.sk89q.worldedit.internal.expression.Expression;
import com.sk89q.worldedit.internal.expression.ExpressionException; 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.internal.expression.runtime.RValue;
import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector2;
import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.BlockVector3;
@ -1879,7 +1880,42 @@ public class EditSession implements Extent, AutoCloseable {
return count.getDistribution(); 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"); final Expression expression = Expression.compile(expressionString, "x", "y", "z", "type", "data");
expression.optimize(); expression.optimize();
@ -1889,6 +1925,7 @@ public class EditSession implements Extent, AutoCloseable {
final WorldEditExpressionEnvironment environment = new WorldEditExpressionEnvironment(this, unit, zero); final WorldEditExpressionEnvironment environment = new WorldEditExpressionEnvironment(this, unit, zero);
expression.setEnvironment(environment); expression.setEnvironment(environment);
final int[] timedOut = {0};
final ArbitraryShape shape = new ArbitraryShape(region) { final ArbitraryShape shape = new ArbitraryShape(region) {
@Override @Override
protected BaseBlock getMaterial(int x, int y, int z, BaseBlock defaultMaterial) { protected BaseBlock getMaterial(int x, int y, int z, BaseBlock defaultMaterial) {
@ -1906,27 +1943,42 @@ public class EditSession implements Extent, AutoCloseable {
dataVar = legacy[1]; 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; return null;
} }
int newType = (int) typeVariable.getValue(); int newType = (int) typeVariable.getValue();
int newData = (int) dataVariable.getValue(); int newData = (int) dataVariable.getValue();
if (newType != typeVar || newData != dataVar) { 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 { } else {
return defaultMaterial; return defaultMaterial;
} }
} catch (ExpressionTimeoutException e) {
timedOut[0] = timedOut[0] + 1;
return null;
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING, "Failed to create shape", e); log.log(Level.WARNING, "Failed to create shape", e);
return null; return null;
} }
} }
}; };
int changed = shape.generate(this, pattern, hollow);
return 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"); final Expression expression = Expression.compile(expressionString, "x", "y", "z");
expression.optimize(); expression.optimize();
@ -1944,7 +1996,7 @@ public class EditSession implements Extent, AutoCloseable {
final Vector3 scaled = position.toVector3().subtract(zero).divide(unit); final Vector3 scaled = position.toVector3().subtract(zero).divide(unit);
// transform // 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()); 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 * @return number of blocks affected
* @throws MaxChangedBlocksException thrown if too many blocks are changed * @throws MaxChangedBlocksException thrown if too many blocks are changed
*/ */
public int drawSpline(Pattern pattern, List<BlockVector3> nodevectors, double tension, double bias, double continuity, double quality, double radius, boolean filled) public int drawSpline(Pattern pattern, List<BlockVector3> nodevectors, double tension, double bias,
double continuity, double quality, double radius, boolean filled)
throws MaxChangedBlocksException { throws MaxChangedBlocksException {
Set<BlockVector3> vset = new HashSet<>(); Set<BlockVector3> 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 zero2D = zero.toVector2();
final Vector2 unit2D = unit.toVector2(); final Vector2 unit2D = unit.toVector2();
@ -2242,6 +2303,7 @@ public class EditSession implements Extent, AutoCloseable {
final WorldEditExpressionEnvironment environment = new WorldEditExpressionEnvironment(editSession, unit, zero); final WorldEditExpressionEnvironment environment = new WorldEditExpressionEnvironment(editSession, unit, zero);
expression.setEnvironment(environment); expression.setEnvironment(environment);
final int[] timedOut = {0};
final ArbitraryBiomeShape shape = new ArbitraryBiomeShape(region) { final ArbitraryBiomeShape shape = new ArbitraryBiomeShape(region) {
@Override @Override
protected BiomeType getBiome(int x, int z, BiomeType defaultBiomeType) { 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); final Vector2 scaled = current.subtract(zero2D).divide(unit2D);
try { try {
if (expression.evaluate(scaled.getX(), scaled.getZ()) <= 0) { if (expression.evaluate(new double[]{scaled.getX(), scaled.getZ()}, timeout) <= 0) {
return null; return null;
} }
// TODO: Allow biome setting via a script variable (needs BiomeType<->int mapping) // TODO: Allow biome setting via a script variable (needs BiomeType<->int mapping)
return defaultBiomeType; return defaultBiomeType;
} catch (ExpressionTimeoutException e) {
timedOut[0] = timedOut[0] + 1;
return null;
} catch (Exception e) { } catch (Exception e) {
log.log(Level.WARNING, "Failed to create shape", e); log.log(Level.WARNING, "Failed to create shape", e);
return null; return null;
} }
} }
}; };
int changed = shape.generate(this, biomeType, hollow);
return 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 = { private static final BlockVector3[] recurseDirections = {

View File

@ -67,6 +67,7 @@ public abstract class LocalConfiguration {
public int navigationWandMaxDistance = 50; public int navigationWandMaxDistance = 50;
public int scriptTimeout = 3000; public int scriptTimeout = 3000;
public int calculationTimeout = 100; public int calculationTimeout = 100;
public int maxCalculationTimeout = 300;
public Set<String> allowedDataCycleBlocks = new HashSet<>(); public Set<String> allowedDataCycleBlocks = new HashSet<>();
public String saveDir = "schematics"; public String saveDir = "schematics";
public String scriptsDir = "craftscripts"; public String scriptsDir = "craftscripts";

View File

@ -85,6 +85,7 @@ public class LocalSession {
private transient BlockTool pickaxeMode = new SinglePickaxe(); private transient BlockTool pickaxeMode = new SinglePickaxe();
private transient Map<ItemType, Tool> tools = new HashMap<>(); private transient Map<ItemType, Tool> tools = new HashMap<>();
private transient int maxBlocksChanged = -1; private transient int maxBlocksChanged = -1;
private transient int maxTimeoutTime;
private transient boolean useInventory; private transient boolean useInventory;
private transient Snapshot snapshot; private transient Snapshot snapshot;
private transient boolean hasCUISupport = false; private transient boolean hasCUISupport = false;
@ -415,6 +416,24 @@ public class LocalSession {
this.maxBlocksChanged = maxBlocksChanged; 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. * Checks whether the super pick axe is enabled.
* *

View File

@ -57,9 +57,9 @@ public class GeneralCommands {
@Command( @Command(
aliases = { "/limit" }, aliases = { "/limit" },
usage = "<limit>", usage = "[limit]",
desc = "Modify block change limit", desc = "Modify block change limit",
min = 1, min = 0,
max = 1 max = 1
) )
@CommandPermissions("worldedit.limit") @CommandPermissions("worldedit.limit")
@ -68,7 +68,7 @@ public class GeneralCommands {
LocalConfiguration config = worldEdit.getConfiguration(); LocalConfiguration config = worldEdit.getConfiguration();
boolean mayDisable = player.hasPermission("worldedit.limit.unrestricted"); 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 (!mayDisable && config.maxChangeLimit > -1) {
if (limit > config.maxChangeLimit) { if (limit > config.maxChangeLimit) {
player.printError("Your maximum allowable limit is " + config.maxChangeLimit + "."); player.printError("Your maximum allowable limit is " + config.maxChangeLimit + ".");
@ -78,13 +78,43 @@ public class GeneralCommands {
session.setBlockChangeLimit(limit); session.setBlockChangeLimit(limit);
if (limit != -1) { if (limit != config.defaultChangeLimit) {
player.print("Block change limit set to " + limit + ". (Use //limit -1 to go back to the default.)"); player.print("Block change limit set to " + limit + ". (Use //limit to go back to the default.)");
} else { } else {
player.print("Block change limit set to " + limit + "."); 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( @Command(
aliases = { "/fast" }, aliases = { "/fast" },
usage = "[on|off]", usage = "[on|off]",

View File

@ -306,7 +306,7 @@ public class GenerationCommands {
} }
try { 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.findFreePosition();
player.print(affected + " block(s) have been created."); player.print(affected + " block(s) have been created.");
} catch (ExpressionException e) { } catch (ExpressionException e) {
@ -333,7 +333,7 @@ public class GenerationCommands {
min = 2, min = 2,
max = -1 max = -1
) )
@CommandPermissions({"worldedit.generation.shape", "worldedit.biome.set"}) @CommandPermissions("worldedit.generation.shape.biome")
@Logging(ALL) @Logging(ALL)
public void generateBiome(Player player, LocalSession session, EditSession editSession, public void generateBiome(Player player, LocalSession session, EditSession editSession,
@Selection Region region, @Selection Region region,
@ -371,7 +371,7 @@ public class GenerationCommands {
} }
try { 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.findFreePosition();
player.print("" + affected + " columns affected."); player.print("" + affected + " columns affected.");
} catch (ExpressionException e) { } catch (ExpressionException e) {

View File

@ -404,7 +404,7 @@ public class RegionCommands {
} }
try { try {
final int affected = editSession.deformRegion(region, zero, unit, expression); final int affected = editSession.deformRegion(region, zero, unit, expression, session.getTimeout());
player.findFreePosition(); player.findFreePosition();
player.print(affected + " block(s) have been deformed."); player.print(affected + " block(s) have been deformed.");
} catch (ExpressionException e) { } catch (ExpressionException e) {

View File

@ -52,6 +52,7 @@ import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.regions.CuboidRegion; import com.sk89q.worldedit.regions.CuboidRegion;
import com.sk89q.worldedit.regions.CylinderRegion; import com.sk89q.worldedit.regions.CylinderRegion;
import com.sk89q.worldedit.regions.Region; 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.CommandCallable;
import com.sk89q.worldedit.util.command.CommandMapping; import com.sk89q.worldedit.util.command.CommandMapping;
import com.sk89q.worldedit.util.command.Dispatcher; import com.sk89q.worldedit.util.command.Dispatcher;
@ -541,13 +542,18 @@ public class UtilityCommands {
public void calc(Actor actor, @Text String input) throws CommandException { public void calc(Actor actor, @Text String input) throws CommandException {
try { try {
Expression expression = Expression.compile(input); Expression expression = Expression.compile(input);
if (actor instanceof SessionOwner) {
actor.print("= " + expression.evaluate(
new double[]{}, WorldEdit.getInstance().getSessionManager().get((SessionOwner) actor).getTimeout()));
} else {
actor.print("= " + expression.evaluate()); actor.print("= " + expression.evaluate());
}
} catch (EvaluationException e) { } catch (EvaluationException e) {
actor.printError(String.format( 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) { } catch (ExpressionException e) {
actor.printError(String.format( actor.printError(String.format(
"'%s' could not be evaluated (error: %s)", input, e.getMessage())); "'%s' could not be parsed as a valid expression", input));
} }
} }

View File

@ -79,6 +79,7 @@ public class SelectionCommand extends SimpleCommand<Operation> {
EditContext editContext = new EditContext(); EditContext editContext = new EditContext();
editContext.setDestination(locals.get(EditSession.class)); editContext.setDestination(locals.get(EditSession.class));
editContext.setRegion(selection); editContext.setRegion(selection);
editContext.setSession(session);
Operation operation = operationFactory.createFromContext(editContext); Operation operation = operationFactory.createFromContext(editContext);
Operations.completeBlindly(operation); Operations.completeBlindly(operation);

View File

@ -75,7 +75,7 @@ public class ShapedBrushCommand extends SimpleCommand<Object> {
BrushTool tool = session.getBrushTool(player.getItemInHand(HandSide.MAIN_HAND).getType()); BrushTool tool = session.getBrushTool(player.getItemInHand(HandSide.MAIN_HAND).getType());
tool.setSize(radius); tool.setSize(radius);
tool.setFill(null); tool.setFill(null);
tool.setBrush(new OperationFactoryBrush(factory, regionFactory), permission); tool.setBrush(new OperationFactoryBrush(factory, regionFactory, session), permission);
} catch (MaxBrushRadiusException | InvalidToolBindException e) { } catch (MaxBrushRadiusException | InvalidToolBindException e) {
WorldEdit.getInstance().getPlatformManager().getCommandManager().getExceptionConverter().convert(e); WorldEdit.getInstance().getPlatformManager().getCommandManager().getExceptionConverter().convert(e);
} }

View File

@ -20,6 +20,7 @@
package com.sk89q.worldedit.command.tool.brush; package com.sk89q.worldedit.command.tool.brush;
import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.LocalSession;
import com.sk89q.worldedit.MaxChangedBlocksException; import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.function.Contextual; import com.sk89q.worldedit.function.Contextual;
import com.sk89q.worldedit.function.EditContext; import com.sk89q.worldedit.function.EditContext;
@ -33,10 +34,16 @@ public class OperationFactoryBrush implements Brush {
private final Contextual<? extends Operation> operationFactory; private final Contextual<? extends Operation> operationFactory;
private final RegionFactory regionFactory; private final RegionFactory regionFactory;
private final LocalSession session;
public OperationFactoryBrush(Contextual<? extends Operation> operationFactory, RegionFactory regionFactory) { public OperationFactoryBrush(Contextual<? extends Operation> operationFactory, RegionFactory regionFactory) {
this(operationFactory, regionFactory, null);
}
public OperationFactoryBrush(Contextual<? extends Operation> operationFactory, RegionFactory regionFactory, LocalSession session) {
this.operationFactory = operationFactory; this.operationFactory = operationFactory;
this.regionFactory = regionFactory; this.regionFactory = regionFactory;
this.session = session;
} }
@Override @Override
@ -45,6 +52,7 @@ public class OperationFactoryBrush implements Brush {
context.setDestination(editSession); context.setDestination(editSession);
context.setRegion(regionFactory.createCenteredAt(position, size)); context.setRegion(regionFactory.createCenteredAt(position, size));
context.setFill(pattern); context.setFill(pattern);
context.setSession(session);
Operation operation = operationFactory.createFromContext(context); Operation operation = operationFactory.createFromContext(context);
Operations.completeLegacy(operation); Operations.completeLegacy(operation);
} }

View File

@ -29,8 +29,11 @@ import com.sk89q.worldedit.internal.expression.ExpressionException;
import com.sk89q.worldedit.internal.registry.InputParser; import com.sk89q.worldedit.internal.registry.InputParser;
import com.sk89q.worldedit.math.Vector3; import com.sk89q.worldedit.math.Vector3;
import com.sk89q.worldedit.regions.shape.WorldEditExpressionEnvironment; import com.sk89q.worldedit.regions.shape.WorldEditExpressionEnvironment;
import com.sk89q.worldedit.session.SessionOwner;
import com.sk89q.worldedit.session.request.Request; import com.sk89q.worldedit.session.request.Request;
import java.util.function.IntSupplier;
public class ExpressionMaskParser extends InputParser<Mask> { public class ExpressionMaskParser extends InputParser<Mask> {
public ExpressionMaskParser(WorldEdit worldEdit) { public ExpressionMaskParser(WorldEdit worldEdit) {
@ -48,6 +51,11 @@ public class ExpressionMaskParser extends InputParser<Mask> {
WorldEditExpressionEnvironment env = new WorldEditExpressionEnvironment( WorldEditExpressionEnvironment env = new WorldEditExpressionEnvironment(
Request.request().getEditSession(), Vector3.ONE, Vector3.ZERO); Request.request().getEditSession(), Vector3.ONE, Vector3.ZERO);
exp.setEnvironment(env); 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); return new ExpressionMask(exp);
} catch (ExpressionException e) { } catch (ExpressionException e) {
throw new InputParseException("Invalid expression: " + e.getMessage()); throw new InputParseException("Invalid expression: " + e.getMessage());

View File

@ -21,6 +21,7 @@ package com.sk89q.worldedit.function;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import com.sk89q.worldedit.LocalSession;
import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.extent.Extent;
import com.sk89q.worldedit.function.pattern.Pattern; import com.sk89q.worldedit.function.pattern.Pattern;
import com.sk89q.worldedit.regions.Region; import com.sk89q.worldedit.regions.Region;
@ -32,6 +33,7 @@ public class EditContext {
private Extent destination; private Extent destination;
@Nullable private Region region; @Nullable private Region region;
@Nullable private Pattern fill; @Nullable private Pattern fill;
@Nullable private LocalSession session;
public Extent getDestination() { public Extent getDestination() {
return destination; return destination;
@ -60,4 +62,12 @@ public class EditContext {
this.fill = fill; this.fill = fill;
} }
@Nullable
public LocalSession getSession() {
return session;
}
public void setSession(@Nullable LocalSession session) {
this.session = session;
}
} }

View File

@ -23,6 +23,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static com.sk89q.worldedit.util.GuavaUtil.firstNonNull; import static com.sk89q.worldedit.util.GuavaUtil.firstNonNull;
import com.sk89q.worldedit.EditSession; import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.LocalSession;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.extent.Extent; import com.sk89q.worldedit.extent.Extent;
import com.sk89q.worldedit.extent.NullExtent; import com.sk89q.worldedit.extent.NullExtent;
@ -147,7 +149,9 @@ public class Deform implements Contextual<Operation> {
unit = Vector3.ONE; 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 { private static final class DeformOperation implements Operation {
@ -156,20 +160,22 @@ public class Deform implements Contextual<Operation> {
private final Vector3 zero; private final Vector3 zero;
private final Vector3 unit; private final Vector3 unit;
private final String expression; 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.destination = destination;
this.region = region; this.region = region;
this.zero = zero; this.zero = zero;
this.unit = unit; this.unit = unit;
this.expression = expression; this.expression = expression;
this.timeout = timeout;
} }
@Override @Override
public Operation resume(RunContext run) throws WorldEditException { public Operation resume(RunContext run) throws WorldEditException {
try { try {
// TODO: Move deformation code // TODO: Move deformation code
((EditSession) destination).deformRegion(region, zero, unit, expression); ((EditSession) destination).deformRegion(region, zero, unit, expression, timeout);
return null; return null;
} catch (ExpressionException e) { } catch (ExpressionException e) {
throw new RuntimeException("Failed to execute expression", e); // TODO: Better exception to throw here? throw new RuntimeException("Failed to execute expression", e); // TODO: Better exception to throw here?

View File

@ -21,6 +21,7 @@ package com.sk89q.worldedit.function.mask;
import static com.google.common.base.Preconditions.checkNotNull; 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.Expression;
import com.sk89q.worldedit.internal.expression.ExpressionException; import com.sk89q.worldedit.internal.expression.ExpressionException;
import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; 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 com.sk89q.worldedit.regions.shape.WorldEditExpressionEnvironment;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.function.IntSupplier;
/** /**
* A mask that evaluates an expression. * A mask that evaluates an expression.
@ -38,6 +40,7 @@ import javax.annotation.Nullable;
public class ExpressionMask extends AbstractMask { public class ExpressionMask extends AbstractMask {
private final Expression expression; private final Expression expression;
private final IntSupplier timeout;
/** /**
* Create a new instance. * Create a new instance.
@ -46,8 +49,7 @@ public class ExpressionMask extends AbstractMask {
* @throws ExpressionException thrown if there is an error with the expression * @throws ExpressionException thrown if there is an error with the expression
*/ */
public ExpressionMask(String expression) throws ExpressionException { public ExpressionMask(String expression) throws ExpressionException {
checkNotNull(expression); this(Expression.compile(checkNotNull(expression), "x", "y", "z"));
this.expression = Expression.compile(expression, "x", "y", "z");
} }
/** /**
@ -56,8 +58,13 @@ public class ExpressionMask extends AbstractMask {
* @param expression the expression * @param expression the expression
*/ */
public ExpressionMask(Expression expression) { public ExpressionMask(Expression expression) {
this(expression, null);
}
public ExpressionMask(Expression expression, @Nullable IntSupplier timeout) {
checkNotNull(expression); checkNotNull(expression);
this.expression = expression; this.expression = expression;
this.timeout = timeout;
} }
@Override @Override
@ -66,7 +73,12 @@ public class ExpressionMask extends AbstractMask {
if (expression.getEnvironment() instanceof WorldEditExpressionEnvironment) { if (expression.getEnvironment() instanceof WorldEditExpressionEnvironment) {
((WorldEditExpressionEnvironment) expression.getEnvironment()).setCurrentBlock(vector.toVector3()); ((WorldEditExpressionEnvironment) expression.getEnvironment()).setCurrentBlock(vector.toVector3());
} }
if (timeout == null) {
return expression.evaluate(vector.getX(), vector.getY(), vector.getZ()) > 0; 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) { } catch (EvaluationException e) {
return false; return false;
} }
@ -75,7 +87,7 @@ public class ExpressionMask extends AbstractMask {
@Nullable @Nullable
@Override @Override
public Mask2D toMask2D() { public Mask2D toMask2D() {
return new ExpressionMask2D(expression); return new ExpressionMask2D(expression, timeout);
} }
} }

View File

@ -21,14 +21,19 @@ package com.sk89q.worldedit.function.mask;
import static com.google.common.base.Preconditions.checkNotNull; 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.Expression;
import com.sk89q.worldedit.internal.expression.ExpressionException; import com.sk89q.worldedit.internal.expression.ExpressionException;
import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; import com.sk89q.worldedit.internal.expression.runtime.EvaluationException;
import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector2;
import javax.annotation.Nullable;
import java.util.function.IntSupplier;
public class ExpressionMask2D extends AbstractMask2D { public class ExpressionMask2D extends AbstractMask2D {
private final Expression expression; private final Expression expression;
private final IntSupplier timeout;
/** /**
* Create a new instance. * Create a new instance.
@ -37,8 +42,7 @@ public class ExpressionMask2D extends AbstractMask2D {
* @throws ExpressionException thrown if there is an error with the expression * @throws ExpressionException thrown if there is an error with the expression
*/ */
public ExpressionMask2D(String expression) throws ExpressionException { public ExpressionMask2D(String expression) throws ExpressionException {
checkNotNull(expression); this(Expression.compile(checkNotNull(expression), "x", "z"));
this.expression = Expression.compile(expression, "x", "z");
} }
/** /**
@ -47,14 +51,23 @@ public class ExpressionMask2D extends AbstractMask2D {
* @param expression the expression * @param expression the expression
*/ */
public ExpressionMask2D(Expression expression) { public ExpressionMask2D(Expression expression) {
this(expression, null);
}
public ExpressionMask2D(Expression expression, @Nullable IntSupplier timeout) {
checkNotNull(expression); checkNotNull(expression);
this.expression = expression; this.expression = expression;
this.timeout = timeout;
} }
@Override @Override
public boolean test(BlockVector2 vector) { public boolean test(BlockVector2 vector) {
try { try {
if (timeout != null) {
return expression.evaluate(vector.getX(), 0, vector.getZ()) > 0; 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) { } catch (EvaluationException e) {
return false; return false;
} }

View File

@ -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.Constant;
import com.sk89q.worldedit.internal.expression.runtime.EvaluationException; import com.sk89q.worldedit.internal.expression.runtime.EvaluationException;
import com.sk89q.worldedit.internal.expression.runtime.ExpressionEnvironment; 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.Functions;
import com.sk89q.worldedit.internal.expression.runtime.RValue; import com.sk89q.worldedit.internal.expression.runtime.RValue;
import com.sk89q.worldedit.internal.expression.runtime.ReturnException; import com.sk89q.worldedit.internal.expression.runtime.ReturnException;
@ -36,7 +37,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Stack; import java.util.Stack;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -117,6 +117,10 @@ public class Expression {
} }
public double evaluate(double... values) throws EvaluationException { 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) { for (int i = 0; i < values.length; ++i) {
final String variableName = variableNames[i]; final String variableName = variableNames[i];
final RValue invokable = variables.get(variableName); final RValue invokable = variables.get(variableName);
@ -127,34 +131,44 @@ public class Expression {
((Variable) invokable).value = values[i]; ((Variable) invokable).value = values[i];
} }
Future<Double> result = evalThread.submit(new Callable<Double>() {
@Override
public Double call() throws Exception {
pushInstance();
try { try {
return root.getValue(); if (timeout < 0) {
} finally { return evaluateRoot();
popInstance();
} }
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<Double> result = evalThread.submit(this::evaluateRoot);
try { try {
return result.get(WorldEdit.getInstance().getConfiguration().calculationTimeout, TimeUnit.MILLISECONDS); return result.get(timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (TimeoutException e) {
result.cancel(true);
throw new ExpressionTimeoutException("Calculations exceeded time limit.");
} catch (ExecutionException e) { } catch (ExecutionException e) {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof ReturnException) { if (cause instanceof EvaluationException) {
return ((ReturnException) cause).getValue(); throw (EvaluationException) cause;
} }
if (cause instanceof RuntimeException) { if (cause instanceof RuntimeException) {
throw (RuntimeException) cause; throw (RuntimeException) cause;
} }
throw new 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();
} }
} }

View File

@ -0,0 +1,29 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -155,31 +155,20 @@ public class SessionManager {
session.setConfiguration(config); session.setConfiguration(config);
session.setBlockChangeLimit(config.defaultChangeLimit); session.setBlockChangeLimit(config.defaultChangeLimit);
session.setTimeout(config.calculationTimeout);
// Remember the session regardless of if it's currently active or not. // Remember the session regardless of if it's currently active or not.
// And have the SessionTracker FLUSH inactive sessions. // And have the SessionTracker FLUSH inactive sessions.
sessions.put(getKey(owner), new SessionHolder(sessionKey, session)); sessions.put(getKey(owner), new SessionHolder(sessionKey, session));
} }
// Set the limit on the number of blocks that an operation can if (shouldBoundLimit(owner.hasPermission("worldedit.limit.unrestricted"),
// change at once, or don't if the owner has an override or there session.getBlockChangeLimit(), config.maxChangeLimit)) {
// 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); session.setBlockChangeLimit(config.maxChangeLimit);
} }
} else { if (shouldBoundLimit(owner.hasPermission("worldedit.timeout.unrestricted"),
// Bound the change limit session.getTimeout(), config.maxCalculationTimeout)) {
int maxChangeLimit = config.maxChangeLimit; session.setTimeout(config.maxCalculationTimeout);
if (currentChangeLimit == -1 || currentChangeLimit > maxChangeLimit) {
session.setBlockChangeLimit(maxChangeLimit);
}
}
} }
// Have the session use inventory if it's enabled and the owner // Have the session use inventory if it's enabled and the owner
@ -192,6 +181,13 @@ public class SessionManager {
return session; 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. * Save a map of sessions to disk.
* *

View File

@ -112,6 +112,7 @@ public class PropertiesConfiguration extends LocalConfiguration {
navigationUseGlass = getBool("nav-use-glass", navigationUseGlass); navigationUseGlass = getBool("nav-use-glass", navigationUseGlass);
scriptTimeout = getInt("scripting-timeout", scriptTimeout); scriptTimeout = getInt("scripting-timeout", scriptTimeout);
calculationTimeout = getInt("calculation-timeout", calculationTimeout); calculationTimeout = getInt("calculation-timeout", calculationTimeout);
maxCalculationTimeout = getInt("max-calculation-timeout", maxCalculationTimeout);
saveDir = getString("schematic-save-dir", saveDir); saveDir = getString("schematic-save-dir", saveDir);
scriptsDir = getString("craftscript-dir", scriptsDir); scriptsDir = getString("craftscript-dir", scriptsDir);
butcherDefaultRadius = getInt("butcher-default-radius", butcherDefaultRadius); butcherDefaultRadius = getInt("butcher-default-radius", butcherDefaultRadius);

View File

@ -108,6 +108,7 @@ public class YAMLConfiguration extends LocalConfiguration {
scriptsDir = config.getString("scripting.dir", scriptsDir); scriptsDir = config.getString("scripting.dir", scriptsDir);
calculationTimeout = config.getInt("calculation.timeout", calculationTimeout); calculationTimeout = config.getInt("calculation.timeout", calculationTimeout);
maxCalculationTimeout = config.getInt("calculation.max-timeout", maxCalculationTimeout);
saveDir = config.getString("saving.dir", saveDir); saveDir = config.getString("saving.dir", saveDir);

View File

@ -64,7 +64,7 @@ public class ExpressionTest {
assertEquals(atan2(3, 4), simpleEval("atan2(3, 4)"), 0); assertEquals(atan2(3, 4), simpleEval("atan2(3, 4)"), 0);
// check variables // 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 @Test
@ -123,7 +123,7 @@ public class ExpressionTest {
@Test @Test
public void testAssign() throws ExpressionException { public void testAssign() throws ExpressionException {
Expression foo = compile("{a=x} b=y; c=z", "x", "y", "z", "a", "b", "c"); 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(2, foo.getVariable("a", false).getValue(), 0);
assertEquals(3, foo.getVariable("b", false).getValue(), 0); assertEquals(3, foo.getVariable("b", false).getValue(), 0);
assertEquals(5, foo.getVariable("c", false).getValue(), 0); assertEquals(5, foo.getVariable("c", false).getValue(), 0);
@ -136,13 +136,13 @@ public class ExpressionTest {
// test 'dangling else' // test 'dangling else'
final Expression expression1 = compile("if (1) if (0) x=4; else y=5;", "x", "y"); 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(1, expression1.getVariable("x", false).getValue(), 0);
assertEquals(5, expression1.getVariable("y", false).getValue(), 0); assertEquals(5, expression1.getVariable("y", false).getValue(), 0);
// test if the if construct is correctly recognized as a statement // 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"); 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); assertEquals(4, expression2.getVariable("y", false).getValue(), 0);
} }