Files
fabric-carpet/src/main/java/carpet/script/ScriptHost.java
T

451 lines
17 KiB
Java

package carpet.script;
import carpet.script.bundled.Module;
import carpet.script.exception.ExpressionException;
import carpet.script.exception.IntegrityException;
import carpet.script.exception.InternalExpressionException;
import carpet.script.language.Arithmetic;
import carpet.script.value.FunctionValue;
import carpet.script.value.Value;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
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;
public abstract class ScriptHost
{
public static Map<Value, Value> systemGlobals = new ConcurrentHashMap<>();
private static final Map<Long, Random> randomizers = new Long2ObjectOpenHashMap<>();
public static Thread mainThread = null;
private final Map<Value, ThreadPoolExecutor> executorServices = new HashMap<>();
private final Map<Value, Object> locks = new ConcurrentHashMap<>();
protected boolean inTermination = false;
private final Set<String> deprecations = new HashSet<>();
public Random getRandom(long aLong)
{
if (randomizers.size() > 65536)
randomizers.clear();
return randomizers.computeIfAbsent(aLong, Random::new);
}
public boolean resetRandom(long aLong) {
return randomizers.remove(aLong) != null;
}
public static class ModuleData
{
Module parent;
public Map<String, FunctionValue> globalFunctions = new Object2ObjectOpenHashMap<>();
public Map<String, LazyValue> globalVariables = new Object2ObjectOpenHashMap<>();
public Map<String, ModuleData> functionImports = new Object2ObjectOpenHashMap<>(); // imported functions string to module
public Map<String, ModuleData> globalsImports = new Object2ObjectOpenHashMap<>(); // imported global variables string to module
public Map<String, ModuleData> futureImports = new Object2ObjectOpenHashMap<>(); // imports not known before used
public ModuleData(Module parent, ModuleData other)
{
// imports are just pointers, but they still point to the wrong modules (point to the parent)
this.parent = parent;
globalFunctions.putAll(other.globalFunctions);
other.globalVariables.forEach((key, value) ->
{
Value var = value.evalValue(null);
Value copy = var.deepcopy();
copy.boundVariable = var.boundVariable;
globalVariables.put(key, (c, t) -> copy);
});
}
public void setImportsBasedOn(ScriptHost host, ModuleData other)
{
// fixing imports
other.functionImports.forEach((name, targetData) -> {
functionImports.put(name, host.moduleData.get(targetData.parent));
});
other.globalsImports.forEach((name, targetData) -> {
globalsImports.put(name, host.moduleData.get(targetData.parent));
});
other.futureImports.forEach((name, targetData) -> {
futureImports.put(name, host.moduleData.get(targetData.parent));
});
}
public ModuleData(Module parent)
{
this.parent = parent;
}
}
protected final Map<String, ScriptHost> userHosts = new Object2ObjectOpenHashMap<>();
private Map<Module,ModuleData> moduleData = new HashMap<>(); // marking imports
private Map<String,Module> modules = new HashMap<>();
protected ScriptHost parent;
protected boolean perUser;
public String user;
public String getName() {return main ==null?null: main.getName();}
public final Module main;
@FunctionalInterface
public interface ErrorSnooper
{
List<String> apply(Expression expression, Tokenizer.Token token, String message);
}
public ErrorSnooper errorSnooper = null;
protected ScriptHost(Module code, boolean perUser, ScriptHost parent)
{
this.parent = parent;
this.main = code;
this.perUser = perUser;
this.user = null;
ModuleData moduleData = new ModuleData(code);
initializeModuleGlobals(moduleData);
this.moduleData.put(code, moduleData);
this.modules.put(code==null?null:code.getName(), code);
mainThread = Thread.currentThread();
}
void initializeModuleGlobals(ModuleData md)
{
}
public void importModule(Context c, String moduleName)
{
if (modules.containsKey(moduleName.toLowerCase(Locale.ROOT))) return; // aready imported
Module module = getModuleOrLibraryByName(moduleName);
if (modules.containsKey(module.getName())) return; // aready imported, once again, in case some discrepancies in names?
modules.put(module.getName(), module);
ModuleData data = new ModuleData(module);
initializeModuleGlobals(data);
moduleData.put(module, data);
runModuleCode(c, module);
//moduleData.remove(module); // we are pooped already, but doesn't hurt to clean that up.
//modules.remove(module.getName());
//throw new InternalExpressionException("Failed to import a module "+moduleName);
}
public void importNames(Context c, Module targetModule, String sourceModuleName, List<String> identifiers )
{
if (!moduleData.containsKey(targetModule))
{
throw new InternalExpressionException("Cannot import to module that doesn't exist");
}
Module source = modules.get(sourceModuleName);
ModuleData sourceData = moduleData.get(source);
ModuleData targetData = moduleData.get(targetModule);
if (sourceData == null || targetData == null)
{
throw new InternalExpressionException("Cannot import from module that is not imported");
}
for (String identifier: identifiers)
{
if (sourceData.globalFunctions.containsKey(identifier))
{
targetData.functionImports.put(identifier, sourceData);
}
else if (sourceData.globalVariables.containsKey(identifier))
{
targetData.globalsImports.put(identifier, sourceData);
}
else
{
targetData.futureImports.put(identifier, sourceData);
}
}
}
public Stream<String> availableImports(String moduleName)
{
Module source = modules.get(moduleName);
ModuleData sourceData = moduleData.get(source);
if (sourceData == null)
{
throw new InternalExpressionException("Cannot import from module that is not imported");
}
return Stream.concat(
globalVariableNames(source, s -> s.startsWith("global_")),
globalFunctionNames(source, s -> true)
).distinct().sorted();
}
protected abstract Module getModuleOrLibraryByName(String name); // this should be shell out in the executor
protected abstract void runModuleCode(Context c, Module module); // this should be shell out in the executor
public FunctionValue getFunction(String name) { return getFunction(main, name); }
public FunctionValue getAssertFunction(Module module, String name)
{
FunctionValue ret = getFunction(module, name);
if (ret == null)
{
if (module == main)
throw new InternalExpressionException("Function '"+name+"' is not defined yet");
else
throw new InternalExpressionException("Function '"+name+"' is not defined nor visible by its name in the imported module '"+module.getName()+"'");
}
return ret;
}
private FunctionValue getFunction(Module module, String name)
{
ModuleData local = getModuleData(module);
FunctionValue ret = local.globalFunctions.get(name); // most uses would be from local scope anyways
if (ret != null) return ret;
ModuleData target = local.functionImports.get(name);
if (target != null)
{
ret = target.globalFunctions.get(name);
if (ret != null) return ret;
}
// not in local scope - will need to travel over import links
target = local.futureImports.get(name);
if (target == null) return null;
target = findModuleDataFromFunctionImports(name, target, 0);
if (target == null) return null;
local.futureImports.remove(name);
local.functionImports.put(name, target);
return target.globalFunctions.get(name);
}
private ModuleData findModuleDataFromFunctionImports(String name, ModuleData source, int ttl)
{
if (ttl > 64) throw new InternalExpressionException("Cannot import "+name+", either your imports are too deep or too loopy");
if (source.globalFunctions.containsKey(name))
return source;
if (source.functionImports.containsKey(name))
return findModuleDataFromFunctionImports(name, source.functionImports.get(name), ttl+1);
if (source.futureImports.containsKey(name))
return findModuleDataFromFunctionImports(name, source.futureImports.get(name), ttl+1);
return null;
}
public LazyValue getGlobalVariable(String name) { return getGlobalVariable(main, name); }
public LazyValue getGlobalVariable(Module module, String name)
{
ModuleData local = getModuleData(module);
LazyValue ret = local.globalVariables.get(name); // most uses would be from local scope anyways
if (ret != null) return ret;
ModuleData target = local.globalsImports.get(name);
if (target != null)
{
ret = target.globalVariables.get(name);
if (ret != null) return ret;
}
// not in local scope - will need to travel over import links
target = local.futureImports.get(name);
if (target == null) return null;
target = findModuleDataFromGlobalImports(name, target, 0);
if (target == null) return null;
local.futureImports.remove(name);
local.globalsImports.put(name, target);
return target.globalVariables.get(name);
}
private ModuleData findModuleDataFromGlobalImports(String name, ModuleData source, int ttl)
{
if (ttl > 64) throw new InternalExpressionException("Cannot import "+name+", either your imports are too deep or too loopy");
if (source.globalVariables.containsKey(name))
return source;
if (source.globalsImports.containsKey(name))
return findModuleDataFromGlobalImports(name, source.globalsImports.get(name), ttl+1);
if (source.futureImports.containsKey(name))
return findModuleDataFromGlobalImports(name, source.futureImports.get(name), ttl+1);
return null;
}
public void delFunctionWithPrefix(Module module, String prefix)
{
ModuleData data = getModuleData(module);
data.globalFunctions.entrySet().removeIf(e -> e.getKey().startsWith(prefix));
data.functionImports.entrySet().removeIf(e -> e.getKey().startsWith(prefix));
}
public void delFunction(Module module, String funName)
{
ModuleData data = getModuleData(module);
data.globalFunctions.remove(funName);
data.functionImports.remove(funName);
}
public void delGlobalVariableWithPrefix(Module module, String prefix)
{
ModuleData data = getModuleData(module);
data.globalVariables.entrySet().removeIf(e -> e.getKey().startsWith(prefix));
data.globalsImports.entrySet().removeIf(e -> e.getKey().startsWith(prefix));
}
public void delGlobalVariable(Module module, String varName)
{
ModuleData data = getModuleData(module);
data.globalFunctions.remove(varName);
data.functionImports.remove(varName);
}
private ModuleData getModuleData(Module module)
{
ModuleData data = moduleData.get(module);
if (data == null) throw new IntegrityException("Module structure changed for the app. Did you reload the app with tasks running?");
return data;
}
protected void assertAppIntegrity(Module module)
{
getModuleData(module);
}
public void addUserDefinedFunction(Context ctx, Module module, String name, FunctionValue fun)
{
getModuleData(module).globalFunctions.put(name, fun);
}
public void setGlobalVariable(Module module, String name, LazyValue lv)
{
getModuleData(module).globalVariables.put(name, lv);
}
public Stream<String> globalVariableNames(Module module, Predicate<String> predicate)
{
return Stream.concat(Stream.concat(
getModuleData(module).globalVariables.keySet().stream(),
getModuleData(module).globalsImports.keySet().stream()
), getModuleData(module).futureImports.keySet().stream().filter(s -> s.startsWith("global_"))).filter(predicate);
}
public Stream<String> globalFunctionNames(Module module, Predicate<String> predicate)
{
return Stream.concat(Stream.concat(
getModuleData(module).globalFunctions.keySet().stream(),
getModuleData(module).functionImports.keySet().stream()
),getModuleData(module).futureImports.keySet().stream().filter(s -> !s.startsWith("global_"))).filter(predicate);
}
public ScriptHost retrieveForExecution(String /*Nullable*/ user)
{
if (!perUser) return this;
ScriptHost oldUserHost = userHosts.get(user);
if (oldUserHost != null) return oldUserHost;
ScriptHost userHost = this.duplicate();
userHost.user = user;
this.setupUserHost(userHost);
userHosts.put(user, userHost);
return userHost;
}
protected void setupUserHost(ScriptHost host)
{
// adding imports
host.modules.putAll(this.modules);
this.moduleData.forEach((key, value) -> host.moduleData.put(key, new ModuleData(key, value)));
// fixing imports
host.moduleData.forEach((module, data) -> data.setImportsBasedOn(host, this.moduleData.get(data.parent)));
}
synchronized public void handleExpressionException(String msg, ExpressionException exc)
{
System.out.println(msg+": "+exc);
}
protected abstract ScriptHost duplicate();
public Object getLock(Value name)
{
return locks.computeIfAbsent(name, (n) -> new Object());
}
public ThreadPoolExecutor getExecutor(Value pool)
{
if (inTermination) return null;
return executorServices.computeIfAbsent(pool, (v) -> (ThreadPoolExecutor) Executors.newCachedThreadPool());
}
public int taskCount()
{
return executorServices.values().stream().map(ThreadPoolExecutor::getActiveCount).reduce(0, Integer::sum);
}
public int taskCount(Value pool)
{
if (executorServices.containsKey(pool))
{
return executorServices.get(pool).getActiveCount();
}
return 0;
}
public void onClose()
{
inTermination = true;
executorServices.values().forEach(ThreadPoolExecutor::shutdown);
for (ScriptHost uh : userHosts.values()) uh.onClose();
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)
{
perUser = isPerUser;
}
public boolean isPerUser() {return perUser;}
public Set<String> getUserList()
{
return userHosts.keySet();
}
public void resetErrorSnooper()
{
errorSnooper=null;
}
public static final Logger DEPRECATION_LOG = LogManager.getLogger("Scarpet Deprecation Warnings");
public boolean issueDeprecation(String feature)
{
if (deprecations.contains(feature)) return false;
deprecations.add(feature);
DEPRECATION_LOG.warn("'"+feature+"' is deprecated and soon will be removed. Please consult the docs for their replacement");
return true;
}
}