mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-27 03:47:38 +00:00
Move dan200.computercraft.core into a separate module
This is a very big diff in changed files, but very small in actual changes.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core;
|
||||
|
||||
import dan200.computercraft.core.computer.ComputerThread;
|
||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
||||
import dan200.computercraft.core.lua.CobaltLuaMachine;
|
||||
import dan200.computercraft.core.lua.ILuaMachine;
|
||||
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* The global context under which computers run.
|
||||
*/
|
||||
public final class ComputerContext {
|
||||
private final GlobalEnvironment globalEnvironment;
|
||||
private final ComputerThread computerScheduler;
|
||||
private final MainThreadScheduler mainThreadScheduler;
|
||||
private final ILuaMachine.Factory factory;
|
||||
|
||||
public ComputerContext(
|
||||
GlobalEnvironment globalEnvironment, ComputerThread computerScheduler,
|
||||
MainThreadScheduler mainThreadScheduler, ILuaMachine.Factory factory
|
||||
) {
|
||||
this.globalEnvironment = globalEnvironment;
|
||||
this.computerScheduler = computerScheduler;
|
||||
this.mainThreadScheduler = mainThreadScheduler;
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a default {@link ComputerContext} with the given global environment.
|
||||
*
|
||||
* @param environment The current global environment.
|
||||
* @param threads The number of threads to use for the {@link #computerScheduler()}
|
||||
* @param mainThreadScheduler The main thread scheduler to use.
|
||||
*/
|
||||
public ComputerContext(GlobalEnvironment environment, int threads, MainThreadScheduler mainThreadScheduler) {
|
||||
this(environment, new ComputerThread(threads), mainThreadScheduler, CobaltLuaMachine::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* The global environment.
|
||||
*
|
||||
* @return The current global environment.
|
||||
*/
|
||||
public GlobalEnvironment globalEnvironment() {
|
||||
return globalEnvironment;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ComputerThread} instance under which computers are run. This is closed when the context is closed, and
|
||||
* so should be unique per-context.
|
||||
*
|
||||
* @return The current computer thread manager.
|
||||
*/
|
||||
public ComputerThread computerScheduler() {
|
||||
return computerScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link MainThreadScheduler} instance used to run main-thread tasks.
|
||||
*
|
||||
* @return The current main thread scheduler.
|
||||
*/
|
||||
public MainThreadScheduler mainThreadScheduler() {
|
||||
return mainThreadScheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* The factory to create new Lua machines.
|
||||
*
|
||||
* @return The current Lua machine factory.
|
||||
*/
|
||||
public ILuaMachine.Factory luaFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current {@link ComputerContext}, disposing of any resources inside.
|
||||
*
|
||||
* @param timeout The maximum time to wait.
|
||||
* @param unit The unit {@code timeout} is in.
|
||||
* @return Whether the context was successfully shut down.
|
||||
* @throws InterruptedException If interrupted while waiting.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public boolean close(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
return computerScheduler().stop(timeout, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current {@link ComputerContext}, disposing of any resources inside.
|
||||
*
|
||||
* @param timeout The maximum time to wait.
|
||||
* @param unit The unit {@code timeout} is in.
|
||||
* @throws IllegalStateException If the computer thread was not shut down in time.
|
||||
* @throws InterruptedException If interrupted while waiting.
|
||||
*/
|
||||
public void ensureClosed(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
if (!computerScheduler().stop(timeout, unit)) {
|
||||
throw new IllegalStateException("Failed to shutdown ComputerContext in time.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core;
|
||||
|
||||
import dan200.computercraft.core.apis.http.options.Action;
|
||||
import dan200.computercraft.core.apis.http.options.AddressRule;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Config options for ComputerCraft's Lua runtime.
|
||||
*/
|
||||
public final class CoreConfig {
|
||||
// TODO: Ideally this would be an instance in {@link ComputerContext}, but sharing this everywhere it needs to be is
|
||||
// tricky.
|
||||
|
||||
private CoreConfig() {
|
||||
}
|
||||
|
||||
public static int maximumFilesOpen = 128;
|
||||
public static boolean disableLua51Features = false;
|
||||
public static String defaultComputerSettings = "";
|
||||
|
||||
public static long maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos(10);
|
||||
public static long maxMainComputerTime = TimeUnit.MILLISECONDS.toNanos(5);
|
||||
|
||||
public static boolean httpEnabled = true;
|
||||
public static boolean httpWebsocketEnabled = true;
|
||||
public static List<AddressRule> httpRules = List.of(
|
||||
AddressRule.parse("$private", OptionalInt.empty(), Action.DENY.toPartial()),
|
||||
AddressRule.parse("*", OptionalInt.empty(), Action.ALLOW.toPartial())
|
||||
);
|
||||
public static int httpMaxRequests = 16;
|
||||
public static int httpMaxWebsockets = 4;
|
||||
public static int httpDownloadBandwidth = 32 * 1024 * 1024;
|
||||
public static int httpUploadBandwidth = 32 * 1024 * 1024;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core;
|
||||
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.MarkerFactory;
|
||||
|
||||
/**
|
||||
* Shared log markers for ComputerCraft.
|
||||
*/
|
||||
public final class Logging {
|
||||
public static final Marker COMPUTER_ERROR = MarkerFactory.getMarker("COMPUTER_ERROR");
|
||||
public static final Marker HTTP_ERROR = MarkerFactory.getMarker("COMPUTER_ERROR.HTTP");
|
||||
public static final Marker JAVA_ERROR = MarkerFactory.getMarker("COMPUTER_ERROR.JAVA");
|
||||
|
||||
static {
|
||||
HTTP_ERROR.add(COMPUTER_ERROR);
|
||||
JAVA_ERROR.add(JAVA_ERROR);
|
||||
}
|
||||
|
||||
private Logging() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ApiFactories {
|
||||
private ApiFactories() {
|
||||
}
|
||||
|
||||
private static final Collection<ILuaAPIFactory> factories = new LinkedHashSet<>();
|
||||
private static final Collection<ILuaAPIFactory> factoriesView = Collections.unmodifiableCollection(factories);
|
||||
|
||||
public static synchronized void register(@Nonnull ILuaAPIFactory factory) {
|
||||
Objects.requireNonNull(factory, "provider cannot be null");
|
||||
factories.add(factory);
|
||||
}
|
||||
|
||||
public static Iterable<ILuaAPIFactory> getAll() {
|
||||
return factoriesView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.api.peripheral.IWorkMonitor;
|
||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class ComputerAccess implements IComputerAccess {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ComputerAccess.class);
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final Set<String> mounts = new HashSet<>();
|
||||
|
||||
protected ComputerAccess(IAPIEnvironment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
public void unmountAll() {
|
||||
var fileSystem = environment.getFileSystem();
|
||||
if (!mounts.isEmpty()) {
|
||||
LOG.warn("Peripheral or API called mount but did not call unmount for {}", mounts);
|
||||
}
|
||||
|
||||
for (var mount : mounts) {
|
||||
fileSystem.unmount(mount);
|
||||
}
|
||||
mounts.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String mount(@Nonnull String desiredLoc, @Nonnull IMount mount, @Nonnull String driveName) {
|
||||
Objects.requireNonNull(desiredLoc, "desiredLocation cannot be null");
|
||||
Objects.requireNonNull(mount, "mount cannot be null");
|
||||
Objects.requireNonNull(driveName, "driveName cannot be null");
|
||||
|
||||
// Mount the location
|
||||
String location;
|
||||
var fileSystem = environment.getFileSystem();
|
||||
if (fileSystem == null) throw new IllegalStateException("File system has not been created");
|
||||
|
||||
synchronized (fileSystem) {
|
||||
location = findFreeLocation(desiredLoc);
|
||||
if (location != null) {
|
||||
try {
|
||||
fileSystem.mount(driveName, location, mount);
|
||||
} catch (FileSystemException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (location != null) mounts.add(location);
|
||||
return location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String mountWritable(@Nonnull String desiredLoc, @Nonnull IWritableMount mount, @Nonnull String driveName) {
|
||||
Objects.requireNonNull(desiredLoc, "desiredLocation cannot be null");
|
||||
Objects.requireNonNull(mount, "mount cannot be null");
|
||||
Objects.requireNonNull(driveName, "driveName cannot be null");
|
||||
|
||||
// Mount the location
|
||||
String location;
|
||||
var fileSystem = environment.getFileSystem();
|
||||
if (fileSystem == null) throw new IllegalStateException("File system has not been created");
|
||||
|
||||
synchronized (fileSystem) {
|
||||
location = findFreeLocation(desiredLoc);
|
||||
if (location != null) {
|
||||
try {
|
||||
fileSystem.mountWritable(driveName, location, mount);
|
||||
} catch (FileSystemException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (location != null) mounts.add(location);
|
||||
return location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount(String location) {
|
||||
if (location == null) return;
|
||||
if (!mounts.contains(location)) throw new IllegalStateException("You didn't mount this location");
|
||||
|
||||
environment.getFileSystem().unmount(location);
|
||||
mounts.remove(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() {
|
||||
return environment.getComputerID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(@Nonnull String event, Object... arguments) {
|
||||
Objects.requireNonNull(event, "event cannot be null");
|
||||
environment.queueEvent(event, arguments);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public IWorkMonitor getMainThreadMonitor() {
|
||||
return environment.getMainThreadMonitor();
|
||||
}
|
||||
|
||||
private String findFreeLocation(String desiredLoc) {
|
||||
try {
|
||||
var fileSystem = environment.getFileSystem();
|
||||
if (!fileSystem.exists(desiredLoc)) return desiredLoc;
|
||||
|
||||
// We used to check foo2, foo3, foo4, etc here but the disk drive does this itself now
|
||||
return null;
|
||||
} catch (FileSystemException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.BinaryWritableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedWritableHandle;
|
||||
import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Interact with the computer's files and filesystem, allowing you to manipulate files, directories and paths. This
|
||||
* includes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>**Reading and writing files:** Call {@link #open} to obtain a file "handle", which can be used to read from or
|
||||
* write to a file.</li>
|
||||
* <li>**Path manipulation:** {@link #combine}, {@link #getName} and {@link #getDir} allow you to manipulate file
|
||||
* paths, joining them together or extracting components.</li>
|
||||
* <li>**Querying paths:** For instance, checking if a file exists, or whether it's a directory. See {@link #getSize},
|
||||
* {@link #exists}, {@link #isDir}, {@link #isReadOnly} and {@link #attributes}.</li>
|
||||
* <li>**File and directory manipulation:** For instance, moving or copying files. See {@link #makeDir}, {@link #move},
|
||||
* {@link #copy} and {@link #delete}.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* :::note
|
||||
* All functions in the API work on absolute paths, and do not take the @{shell.dir|current directory} into account.
|
||||
* You can use @{shell.resolve} to convert a relative path into an absolute one.
|
||||
* :::
|
||||
* <p>
|
||||
* ## Mounts
|
||||
* While a computer can only have one hard drive and filesystem, other filesystems may be "mounted" inside it. For
|
||||
* instance, the {@link dan200.computercraft.shared.peripheral.diskdrive.DiskDrivePeripheral drive peripheral} mounts
|
||||
* its disk's contents at {@code "disk/"}, {@code "disk1/"}, etc...
|
||||
* <p>
|
||||
* You can see which mount a path belongs to with the {@link #getDrive} function. This returns {@code "hdd"} for the
|
||||
* computer's main filesystem ({@code "/"}), {@code "rom"} for the rom ({@code "rom/"}).
|
||||
* <p>
|
||||
* Most filesystems have a limited capacity, operations which would cause that capacity to be reached (such as writing
|
||||
* an incredibly large file) will fail. You can see a mount's capacity with {@link #getCapacity} and the remaining
|
||||
* space with {@link #getFreeSpace}.
|
||||
*
|
||||
* @cc.module fs
|
||||
*/
|
||||
public class FSAPI implements ILuaAPI {
|
||||
private final IAPIEnvironment environment;
|
||||
private FileSystem fileSystem = null;
|
||||
|
||||
public FSAPI(IAPIEnvironment env) {
|
||||
environment = env;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[]{ "fs" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startup() {
|
||||
fileSystem = environment.getFileSystem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
fileSystem = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of files in a directory.
|
||||
*
|
||||
* @param path The path to list.
|
||||
* @return A table with a list of files in the directory.
|
||||
* @throws LuaException If the path doesn't exist.
|
||||
* @cc.usage List all files under {@code /rom/}
|
||||
* <pre>{@code
|
||||
* local files = fs.list("/rom/")
|
||||
* for i = 1, #files do
|
||||
* print(files[i])
|
||||
* end
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final String[] list(String path) throws LuaException {
|
||||
environment.observe(Metrics.FS_OPS);
|
||||
try {
|
||||
return fileSystem.list(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines several parts of a path into one full path, adding separators as
|
||||
* needed.
|
||||
*
|
||||
* @param arguments The paths to combine.
|
||||
* @return The new path, with separators added between parts as needed.
|
||||
* @throws LuaException On argument errors.
|
||||
* @cc.tparam string path The first part of the path. For example, a parent directory path.
|
||||
* @cc.tparam string ... Additional parts of the path to combine.
|
||||
* @cc.changed 1.95.0 Now supports multiple arguments.
|
||||
* @cc.usage Combine several file paths together
|
||||
* <pre>{@code
|
||||
* fs.combine("/rom/programs", "../apis", "parallel.lua")
|
||||
* -- => rom/apis/parallel.lua
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final String combine(IArguments arguments) throws LuaException {
|
||||
var result = new StringBuilder();
|
||||
result.append(FileSystem.sanitizePath(arguments.getString(0), true));
|
||||
|
||||
for (int i = 1, n = arguments.count(); i < n; i++) {
|
||||
var part = FileSystem.sanitizePath(arguments.getString(i), true);
|
||||
if (result.length() != 0 && !part.isEmpty()) result.append('/');
|
||||
result.append(part);
|
||||
}
|
||||
|
||||
return FileSystem.sanitizePath(result.toString(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file name portion of a path.
|
||||
*
|
||||
* @param path The path to get the name from.
|
||||
* @return The final part of the path (the file name).
|
||||
* @cc.since 1.2
|
||||
* @cc.usage Get the file name of {@code rom/startup.lua}
|
||||
* <pre>{@code
|
||||
* fs.getName("rom/startup.lua")
|
||||
* -- => startup.lua
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final String getName(String path) {
|
||||
return FileSystem.getName(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent directory portion of a path.
|
||||
*
|
||||
* @param path The path to get the directory from.
|
||||
* @return The path with the final part removed (the parent directory).
|
||||
* @cc.since 1.63
|
||||
* @cc.usage Get the directory name of {@code rom/startup.lua}
|
||||
* <pre>{@code
|
||||
* fs.getDir("rom/startup.lua")
|
||||
* -- => rom
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final String getDir(String path) {
|
||||
return FileSystem.getDirectory(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the specified file.
|
||||
*
|
||||
* @param path The file to get the file size of.
|
||||
* @return The size of the file, in bytes.
|
||||
* @throws LuaException If the path doesn't exist.
|
||||
* @cc.since 1.3
|
||||
*/
|
||||
@LuaFunction
|
||||
public final long getSize(String path) throws LuaException {
|
||||
try {
|
||||
return fileSystem.getSize(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified path exists.
|
||||
*
|
||||
* @param path The path to check the existence of.
|
||||
* @return Whether the path exists.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean exists(String path) {
|
||||
try {
|
||||
return fileSystem.exists(path);
|
||||
} catch (FileSystemException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified path is a directory.
|
||||
*
|
||||
* @param path The path to check.
|
||||
* @return Whether the path is a directory.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean isDir(String path) {
|
||||
try {
|
||||
return fileSystem.isDir(path);
|
||||
} catch (FileSystemException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a path is read-only.
|
||||
*
|
||||
* @param path The path to check.
|
||||
* @return Whether the path cannot be written to.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean isReadOnly(String path) {
|
||||
try {
|
||||
return fileSystem.isReadOnly(path);
|
||||
} catch (FileSystemException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a directory, and any missing parents, at the specified path.
|
||||
*
|
||||
* @param path The path to the directory to create.
|
||||
* @throws LuaException If the directory couldn't be created.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void makeDir(String path) throws LuaException {
|
||||
try {
|
||||
environment.observe(Metrics.FS_OPS);
|
||||
fileSystem.makeDir(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a file or directory from one path to another.
|
||||
* <p>
|
||||
* Any parent directories are created as needed.
|
||||
*
|
||||
* @param path The current file or directory to move from.
|
||||
* @param dest The destination path for the file or directory.
|
||||
* @throws LuaException If the file or directory couldn't be moved.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void move(String path, String dest) throws LuaException {
|
||||
try {
|
||||
environment.observe(Metrics.FS_OPS);
|
||||
fileSystem.move(path, dest);
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file or directory to a new path.
|
||||
* <p>
|
||||
* Any parent directories are created as needed.
|
||||
*
|
||||
* @param path The file or directory to copy.
|
||||
* @param dest The path to the destination file or directory.
|
||||
* @throws LuaException If the file or directory couldn't be copied.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void copy(String path, String dest) throws LuaException {
|
||||
try {
|
||||
environment.observe(Metrics.FS_OPS);
|
||||
fileSystem.copy(path, dest);
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file or directory.
|
||||
* <p>
|
||||
* If the path points to a directory, all of the enclosed files and
|
||||
* subdirectories are also deleted.
|
||||
*
|
||||
* @param path The path to the file or directory to delete.
|
||||
* @throws LuaException If the file or directory couldn't be deleted.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void delete(String path) throws LuaException {
|
||||
try {
|
||||
environment.observe(Metrics.FS_OPS);
|
||||
fileSystem.delete(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Add individual handle type documentation
|
||||
|
||||
/**
|
||||
* Opens a file for reading or writing at a path.
|
||||
* <p>
|
||||
* The {@code mode} string can be any of the following:
|
||||
* <ul>
|
||||
* <li><strong>"r"</strong>: Read mode</li>
|
||||
* <li><strong>"w"</strong>: Write mode</li>
|
||||
* <li><strong>"a"</strong>: Append mode</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The mode may also have a "b" at the end, which opens the file in "binary
|
||||
* mode". This allows you to read binary files, as well as seek within a file.
|
||||
*
|
||||
* @param path The path to the file to open.
|
||||
* @param mode The mode to open the file with.
|
||||
* @return A file handle object for the file, or {@code nil} + an error message on error.
|
||||
* @throws LuaException If an invalid mode was specified.
|
||||
* @cc.treturn [1] table A file handle object for the file.
|
||||
* @cc.treturn [2] nil If the file does not exist, or cannot be opened.
|
||||
* @cc.treturn string|nil A message explaining why the file cannot be opened.
|
||||
* @cc.usage Read the contents of a file.
|
||||
* <pre>{@code
|
||||
* local file = fs.open("/rom/help/intro.txt", "r")
|
||||
* local contents = file.readAll()
|
||||
* file.close()
|
||||
*
|
||||
* print(contents)
|
||||
* }</pre>
|
||||
* @cc.usage Open a file and read all lines into a table. @{io.lines} offers an alternative way to do this.
|
||||
* <pre>{@code
|
||||
* local file = fs.open("/rom/motd.txt", "r")
|
||||
* local lines = {}
|
||||
* while true do
|
||||
* local line = file.readLine()
|
||||
*
|
||||
* -- If line is nil then we've reached the end of the file and should stop
|
||||
* if not line then break end
|
||||
*
|
||||
* lines[#lines + 1] = line
|
||||
* end
|
||||
*
|
||||
* file.close()
|
||||
*
|
||||
* print(lines[math.random(#lines)]) -- Pick a random line and print it.
|
||||
* }</pre>
|
||||
* @cc.usage Open a file and write some text to it. You can run {@code edit out.txt} to see the written text.
|
||||
* <pre>{@code
|
||||
* local file = fs.open("out.txt", "w")
|
||||
* file.write("Just testing some code")
|
||||
* file.close() -- Remember to call close, otherwise changes may not be written!
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] open(String path, String mode) throws LuaException {
|
||||
environment.observe(Metrics.FS_OPS);
|
||||
try {
|
||||
switch (mode) {
|
||||
case "r" -> {
|
||||
// Open the file for reading, then create a wrapper around the reader
|
||||
var reader = fileSystem.openForRead(path, EncodedReadableHandle::openUtf8);
|
||||
return new Object[]{ new EncodedReadableHandle(reader.get(), reader) };
|
||||
}
|
||||
case "w" -> {
|
||||
// Open the file for writing, then create a wrapper around the writer
|
||||
var writer = fileSystem.openForWrite(path, false, EncodedWritableHandle::openUtf8);
|
||||
return new Object[]{ new EncodedWritableHandle(writer.get(), writer) };
|
||||
}
|
||||
case "a" -> {
|
||||
// Open the file for appending, then create a wrapper around the writer
|
||||
var writer = fileSystem.openForWrite(path, true, EncodedWritableHandle::openUtf8);
|
||||
return new Object[]{ new EncodedWritableHandle(writer.get(), writer) };
|
||||
}
|
||||
case "rb" -> {
|
||||
// Open the file for binary reading, then create a wrapper around the reader
|
||||
var reader = fileSystem.openForRead(path, Function.identity());
|
||||
return new Object[]{ BinaryReadableHandle.of(reader.get(), reader) };
|
||||
}
|
||||
case "wb" -> {
|
||||
// Open the file for binary writing, then create a wrapper around the writer
|
||||
var writer = fileSystem.openForWrite(path, false, Function.identity());
|
||||
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer) };
|
||||
}
|
||||
case "ab" -> {
|
||||
// Open the file for binary appending, then create a wrapper around the reader
|
||||
var writer = fileSystem.openForWrite(path, true, Function.identity());
|
||||
return new Object[]{ BinaryWritableHandle.of(writer.get(), writer) };
|
||||
}
|
||||
default -> throw new LuaException("Unsupported mode");
|
||||
}
|
||||
} catch (FileSystemException e) {
|
||||
return new Object[]{ null, e.getMessage() };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the mount that the specified path is located on.
|
||||
*
|
||||
* @param path The path to get the drive of.
|
||||
* @return The name of the drive that the file is on; e.g. {@code hdd} for local files, or {@code rom} for ROM files.
|
||||
* @throws LuaException If the path doesn't exist.
|
||||
* @cc.treturn string The name of the drive that the file is on; e.g. {@code hdd} for local files, or {@code rom} for ROM files.
|
||||
* @cc.usage Print the drives of a couple of mounts:
|
||||
*
|
||||
* <pre>{@code
|
||||
* print("/: " .. fs.getDrive("/"))
|
||||
* print("/rom/: " .. fs.getDrive("rom"))
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] getDrive(String path) throws LuaException {
|
||||
try {
|
||||
return fileSystem.exists(path) ? new Object[]{ fileSystem.getMountLabel(path) } : null;
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of free space available on the drive the path is
|
||||
* located on.
|
||||
*
|
||||
* @param path The path to check the free space for.
|
||||
* @return The amount of free space available, in bytes.
|
||||
* @throws LuaException If the path doesn't exist.
|
||||
* @cc.treturn number|"unlimited" The amount of free space available, in bytes, or "unlimited".
|
||||
* @cc.since 1.4
|
||||
* @see #getCapacity To get the capacity of this drive.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object getFreeSpace(String path) throws LuaException {
|
||||
try {
|
||||
var freeSpace = fileSystem.getFreeSpace(path);
|
||||
return freeSpace >= 0 ? freeSpace : "unlimited";
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for files matching a string with wildcards.
|
||||
* <p>
|
||||
* This string is formatted like a normal path string, but can include any
|
||||
* number of wildcards ({@code *}) to look for files matching anything.
|
||||
* For example, <code>rom/*/command*</code> will look for any path starting with
|
||||
* {@code command} inside any subdirectory of {@code /rom}.
|
||||
*
|
||||
* @param path The wildcard-qualified path to search for.
|
||||
* @return A list of paths that match the search string.
|
||||
* @throws LuaException If the path doesn't exist.
|
||||
* @cc.since 1.6
|
||||
*/
|
||||
@LuaFunction
|
||||
public final String[] find(String path) throws LuaException {
|
||||
try {
|
||||
environment.observe(Metrics.FS_OPS);
|
||||
return fileSystem.find(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the capacity of the drive the path is located on.
|
||||
*
|
||||
* @param path The path of the drive to get.
|
||||
* @return The drive's capacity.
|
||||
* @throws LuaException If the capacity cannot be determined.
|
||||
* @cc.treturn number|nil This drive's capacity. This will be nil for "read-only" drives, such as the ROM or
|
||||
* treasure disks.
|
||||
* @cc.since 1.87.0
|
||||
* @see #getFreeSpace To get the free space available on this drive.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object getCapacity(String path) throws LuaException {
|
||||
try {
|
||||
var capacity = fileSystem.getCapacity(path);
|
||||
return capacity.isPresent() ? capacity.getAsLong() : null;
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes about a specific file or folder.
|
||||
* <p>
|
||||
* The returned attributes table contains information about the size of the file, whether it is a directory,
|
||||
* when it was created and last modified, and whether it is read only.
|
||||
* <p>
|
||||
* The creation and modification times are given as the number of milliseconds since the UNIX epoch. This may be
|
||||
* given to {@link OSAPI#date} in order to convert it to more usable form.
|
||||
*
|
||||
* @param path The path to get attributes for.
|
||||
* @return The resulting attributes.
|
||||
* @throws LuaException If the path does not exist.
|
||||
* @cc.treturn { size = number, isDir = boolean, isReadOnly = boolean, created = number, modified = number } The resulting attributes.
|
||||
* @cc.since 1.87.0
|
||||
* @cc.changed 1.91.0 Renamed `modification` field to `modified`.
|
||||
* @cc.changed 1.95.2 Added `isReadOnly` to attributes.
|
||||
* @see #getSize If you only care about the file's size.
|
||||
* @see #isDir If you only care whether a path is a directory or not.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Map<String, Object> attributes(String path) throws LuaException {
|
||||
try {
|
||||
var attributes = fileSystem.getAttributes(path);
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("modification", getFileTime(attributes.lastModifiedTime()));
|
||||
result.put("modified", getFileTime(attributes.lastModifiedTime()));
|
||||
result.put("created", getFileTime(attributes.creationTime()));
|
||||
result.put("size", attributes.isDirectory() ? 0 : attributes.size());
|
||||
result.put("isDir", attributes.isDirectory());
|
||||
result.put("isReadOnly", fileSystem.isReadOnly(path));
|
||||
return result;
|
||||
} catch (FileSystemException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static long getFileTime(FileTime time) {
|
||||
return time == null ? 0 : time.toMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* A Lua exception which does not contain its stack trace.
|
||||
*/
|
||||
public class FastLuaException extends LuaException {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 5957864899303561143L;
|
||||
|
||||
public FastLuaException(@Nullable String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FastLuaException(@Nullable String message, int level) {
|
||||
super(message, level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Throwable fillInStackTrace() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.apis.http.*;
|
||||
import dan200.computercraft.core.apis.http.request.HttpRequest;
|
||||
import dan200.computercraft.core.apis.http.websocket.Websocket;
|
||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static dan200.computercraft.core.apis.TableHelper.*;
|
||||
|
||||
/**
|
||||
* Placeholder description, please ignore.
|
||||
*
|
||||
* @cc.module http
|
||||
* @hidden
|
||||
*/
|
||||
public class HTTPAPI implements ILuaAPI {
|
||||
private final IAPIEnvironment apiEnvironment;
|
||||
|
||||
private final ResourceGroup<CheckUrl> checkUrls = new ResourceGroup<>(ResourceGroup.DEFAULT);
|
||||
private final ResourceGroup<HttpRequest> requests = new ResourceQueue<>(() -> CoreConfig.httpMaxRequests);
|
||||
private final ResourceGroup<Websocket> websockets = new ResourceGroup<>(() -> CoreConfig.httpMaxWebsockets);
|
||||
|
||||
public HTTPAPI(IAPIEnvironment environment) {
|
||||
apiEnvironment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[]{ "http" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startup() {
|
||||
checkUrls.startup();
|
||||
requests.startup();
|
||||
websockets.startup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
checkUrls.shutdown();
|
||||
requests.shutdown();
|
||||
websockets.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
// It's rather ugly to run this here, but we need to clean up
|
||||
// resources as often as possible to reduce blocking.
|
||||
Resource.cleanup();
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] request(IArguments args) throws LuaException {
|
||||
String address, postString, requestMethod;
|
||||
Map<?, ?> headerTable;
|
||||
boolean binary, redirect;
|
||||
|
||||
if (args.get(0) instanceof Map) {
|
||||
var options = args.getTable(0);
|
||||
address = getStringField(options, "url");
|
||||
postString = optStringField(options, "body", null);
|
||||
headerTable = optTableField(options, "headers", Collections.emptyMap());
|
||||
binary = optBooleanField(options, "binary", false);
|
||||
requestMethod = optStringField(options, "method", null);
|
||||
redirect = optBooleanField(options, "redirect", true);
|
||||
|
||||
} else {
|
||||
// Get URL and post information
|
||||
address = args.getString(0);
|
||||
postString = args.optString(1, null);
|
||||
headerTable = args.optTable(2, Collections.emptyMap());
|
||||
binary = args.optBoolean(3, false);
|
||||
requestMethod = null;
|
||||
redirect = true;
|
||||
}
|
||||
|
||||
var headers = getHeaders(headerTable);
|
||||
|
||||
HttpMethod httpMethod;
|
||||
if (requestMethod == null) {
|
||||
httpMethod = postString == null ? HttpMethod.GET : HttpMethod.POST;
|
||||
} else {
|
||||
httpMethod = HttpMethod.valueOf(requestMethod.toUpperCase(Locale.ROOT));
|
||||
if (httpMethod == null || requestMethod.equalsIgnoreCase("CONNECT")) {
|
||||
throw new LuaException("Unsupported HTTP method");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
var uri = HttpRequest.checkUri(address);
|
||||
var request = new HttpRequest(requests, apiEnvironment, address, postString, headers, binary, redirect);
|
||||
|
||||
// Make the request
|
||||
if (!request.queue(r -> r.request(uri, httpMethod))) {
|
||||
throw new LuaException("Too many ongoing HTTP requests");
|
||||
}
|
||||
|
||||
return new Object[]{ true };
|
||||
} catch (HTTPRequestException e) {
|
||||
return new Object[]{ false, e.getMessage() };
|
||||
}
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] checkURL(String address) throws LuaException {
|
||||
try {
|
||||
var uri = HttpRequest.checkUri(address);
|
||||
if (!new CheckUrl(checkUrls, apiEnvironment, address, uri).queue(CheckUrl::run)) {
|
||||
throw new LuaException("Too many ongoing checkUrl calls");
|
||||
}
|
||||
|
||||
return new Object[]{ true };
|
||||
} catch (HTTPRequestException e) {
|
||||
return new Object[]{ false, e.getMessage() };
|
||||
}
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] websocket(String address, Optional<Map<?, ?>> headerTbl) throws LuaException {
|
||||
if (!CoreConfig.httpWebsocketEnabled) {
|
||||
throw new LuaException("Websocket connections are disabled");
|
||||
}
|
||||
|
||||
var headers = getHeaders(headerTbl.orElse(Collections.emptyMap()));
|
||||
|
||||
try {
|
||||
var uri = Websocket.checkUri(address);
|
||||
if (!new Websocket(websockets, apiEnvironment, uri, address, headers).queue(Websocket::connect)) {
|
||||
throw new LuaException("Too many websockets already open");
|
||||
}
|
||||
|
||||
return new Object[]{ true };
|
||||
} catch (HTTPRequestException e) {
|
||||
return new Object[]{ false, e.getMessage() };
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private HttpHeaders getHeaders(@Nonnull Map<?, ?> headerTable) throws LuaException {
|
||||
HttpHeaders headers = new DefaultHttpHeaders();
|
||||
for (Map.Entry<?, ?> entry : headerTable.entrySet()) {
|
||||
var value = entry.getValue();
|
||||
if (entry.getKey() instanceof String && value instanceof String) {
|
||||
try {
|
||||
headers.add((String) entry.getKey(), value);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!headers.contains(HttpHeaderNames.USER_AGENT)) {
|
||||
headers.set(HttpHeaderNames.USER_AGENT, apiEnvironment.getGlobalEnvironment().getUserAgent());
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.peripheral.IWorkMonitor;
|
||||
import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||
import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.metrics.Metric;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public interface IAPIEnvironment {
|
||||
String TIMER_EVENT = "timer";
|
||||
|
||||
@FunctionalInterface
|
||||
interface IPeripheralChangeListener {
|
||||
void onPeripheralChanged(ComputerSide side, @Nullable IPeripheral newPeripheral);
|
||||
}
|
||||
|
||||
int getComputerID();
|
||||
|
||||
@Nonnull
|
||||
ComputerEnvironment getComputerEnvironment();
|
||||
|
||||
@Nonnull
|
||||
GlobalEnvironment getGlobalEnvironment();
|
||||
|
||||
@Nonnull
|
||||
IWorkMonitor getMainThreadMonitor();
|
||||
|
||||
@Nonnull
|
||||
Terminal getTerminal();
|
||||
|
||||
FileSystem getFileSystem();
|
||||
|
||||
void shutdown();
|
||||
|
||||
void reboot();
|
||||
|
||||
void queueEvent(String event, Object... args);
|
||||
|
||||
void setOutput(ComputerSide side, int output);
|
||||
|
||||
int getOutput(ComputerSide side);
|
||||
|
||||
int getInput(ComputerSide side);
|
||||
|
||||
void setBundledOutput(ComputerSide side, int output);
|
||||
|
||||
int getBundledOutput(ComputerSide side);
|
||||
|
||||
int getBundledInput(ComputerSide side);
|
||||
|
||||
void setPeripheralChangeListener(@Nullable IPeripheralChangeListener listener);
|
||||
|
||||
@Nullable
|
||||
IPeripheral getPeripheral(ComputerSide side);
|
||||
|
||||
@Nullable
|
||||
String getLabel();
|
||||
|
||||
void setLabel(@Nullable String label);
|
||||
|
||||
int startTimer(long ticks);
|
||||
|
||||
void cancelTimer(int id);
|
||||
|
||||
void observe(@Nonnull Metric.Event event, long change);
|
||||
|
||||
void observe(@Nonnull Metric.Counter counter);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.format.TextStyle;
|
||||
import java.time.temporal.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.LongUnaryOperator;
|
||||
|
||||
final class LuaDateTime {
|
||||
private LuaDateTime() {
|
||||
}
|
||||
|
||||
static void format(DateTimeFormatterBuilder formatter, String format) throws LuaException {
|
||||
for (var i = 0; i < format.length(); ) {
|
||||
char c;
|
||||
switch (c = format.charAt(i++)) {
|
||||
case '\n' -> formatter.appendLiteral('\n');
|
||||
default -> formatter.appendLiteral(c);
|
||||
case '%' -> {
|
||||
if (i >= format.length()) break;
|
||||
switch (c = format.charAt(i++)) {
|
||||
default -> throw new LuaException("bad argument #1: invalid conversion specifier '%" + c + "'");
|
||||
case '%' -> formatter.appendLiteral('%');
|
||||
case 'a' -> formatter.appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT);
|
||||
case 'A' -> formatter.appendText(ChronoField.DAY_OF_WEEK, TextStyle.FULL);
|
||||
case 'b', 'h' -> formatter.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.SHORT);
|
||||
case 'B' -> formatter.appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL);
|
||||
case 'c' -> format(formatter, "%a %b %e %H:%M:%S %Y");
|
||||
case 'C' -> formatter.appendValueReduced(CENTURY, 2, 2, 0);
|
||||
case 'd' -> formatter.appendValue(ChronoField.DAY_OF_MONTH, 2);
|
||||
case 'D', 'x' -> format(formatter, "%m/%d/%y");
|
||||
case 'e' -> formatter.padNext(2).appendValue(ChronoField.DAY_OF_MONTH);
|
||||
case 'F' -> format(formatter, "%Y-%m-%d");
|
||||
case 'g' -> formatter.appendValueReduced(IsoFields.WEEK_BASED_YEAR, 2, 2, 0);
|
||||
case 'G' -> formatter.appendValue(IsoFields.WEEK_BASED_YEAR);
|
||||
case 'H' -> formatter.appendValue(ChronoField.HOUR_OF_DAY, 2);
|
||||
case 'I' -> formatter.appendValue(ChronoField.HOUR_OF_AMPM, 2);
|
||||
case 'j' -> formatter.appendValue(ChronoField.DAY_OF_YEAR, 3);
|
||||
case 'm' -> formatter.appendValue(ChronoField.MONTH_OF_YEAR, 2);
|
||||
case 'M' -> formatter.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
|
||||
case 'n' -> formatter.appendLiteral('\n');
|
||||
case 'p' -> formatter.appendText(ChronoField.AMPM_OF_DAY);
|
||||
case 'r' -> format(formatter, "%I:%M:%S %p");
|
||||
case 'R' -> format(formatter, "%H:%M");
|
||||
case 'S' -> formatter.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
|
||||
case 't' -> formatter.appendLiteral('\t');
|
||||
case 'T', 'X' -> format(formatter, "%H:%M:%S");
|
||||
case 'u' -> formatter.appendValue(ChronoField.DAY_OF_WEEK);
|
||||
case 'U' -> formatter.appendValue(ChronoField.ALIGNED_WEEK_OF_YEAR, 2);
|
||||
case 'V' -> formatter.appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2);
|
||||
case 'w' -> formatter.appendValue(ZERO_WEEK);
|
||||
case 'W' -> formatter.appendValue(WeekFields.ISO.weekOfYear(), 2);
|
||||
case 'y' -> formatter.appendValueReduced(ChronoField.YEAR, 2, 2, 0);
|
||||
case 'Y' -> formatter.appendValue(ChronoField.YEAR);
|
||||
case 'z' -> formatter.appendOffset("+HHMM", "+0000");
|
||||
case 'Z' -> formatter.appendChronologyId();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static long fromTable(Map<?, ?> table) throws LuaException {
|
||||
var year = getField(table, "year", -1);
|
||||
var month = getField(table, "month", -1);
|
||||
var day = getField(table, "day", -1);
|
||||
var hour = getField(table, "hour", 12);
|
||||
var minute = getField(table, "min", 12);
|
||||
var second = getField(table, "sec", 12);
|
||||
var time = LocalDateTime.of(year, month, day, hour, minute, second);
|
||||
|
||||
var isDst = getBoolField(table, "isdst");
|
||||
if (isDst != null) {
|
||||
boolean requireDst = isDst;
|
||||
for (var possibleOffset : ZoneOffset.systemDefault().getRules().getValidOffsets(time)) {
|
||||
var instant = time.toInstant(possibleOffset);
|
||||
if (possibleOffset.getRules().getDaylightSavings(instant).isZero() == requireDst) {
|
||||
return instant.getEpochSecond();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var offset = ZoneOffset.systemDefault().getRules().getOffset(time);
|
||||
return time.toInstant(offset).getEpochSecond();
|
||||
}
|
||||
|
||||
static Map<String, ?> toTable(TemporalAccessor date, ZoneId offset, Instant instant) {
|
||||
var table = new HashMap<String, Object>(9);
|
||||
table.put("year", date.getLong(ChronoField.YEAR));
|
||||
table.put("month", date.getLong(ChronoField.MONTH_OF_YEAR));
|
||||
table.put("day", date.getLong(ChronoField.DAY_OF_MONTH));
|
||||
table.put("hour", date.getLong(ChronoField.HOUR_OF_DAY));
|
||||
table.put("min", date.getLong(ChronoField.MINUTE_OF_HOUR));
|
||||
table.put("sec", date.getLong(ChronoField.SECOND_OF_MINUTE));
|
||||
table.put("wday", date.getLong(WeekFields.SUNDAY_START.dayOfWeek()));
|
||||
table.put("yday", date.getLong(ChronoField.DAY_OF_YEAR));
|
||||
table.put("isdst", offset.getRules().isDaylightSavings(instant));
|
||||
return table;
|
||||
}
|
||||
|
||||
private static int getField(Map<?, ?> table, String field, int def) throws LuaException {
|
||||
var value = table.get(field);
|
||||
if (value instanceof Number) return ((Number) value).intValue();
|
||||
if (def < 0) throw new LuaException("field \"" + field + "\" missing in date table");
|
||||
return def;
|
||||
}
|
||||
|
||||
private static Boolean getBoolField(Map<?, ?> table, String field) throws LuaException {
|
||||
var value = table.get(field);
|
||||
if (value instanceof Boolean || value == null) return (Boolean) value;
|
||||
throw new LuaException("field \"" + field + "\" missing in date table");
|
||||
}
|
||||
|
||||
private static final TemporalField CENTURY = map(ChronoField.YEAR, ValueRange.of(0, 99), x -> (x / 100) % 100);
|
||||
private static final TemporalField ZERO_WEEK = map(WeekFields.SUNDAY_START.dayOfWeek(), ValueRange.of(0, 6), x -> x - 1);
|
||||
|
||||
private static TemporalField map(TemporalField field, ValueRange range, LongUnaryOperator convert) {
|
||||
return new TemporalField() {
|
||||
@Override
|
||||
public TemporalUnit getBaseUnit() {
|
||||
return field.getBaseUnit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TemporalUnit getRangeUnit() {
|
||||
return field.getRangeUnit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueRange range() {
|
||||
return range;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDateBased() {
|
||||
return field.isDateBased();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTimeBased() {
|
||||
return field.isTimeBased();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupportedBy(TemporalAccessor temporal) {
|
||||
return field.isSupportedBy(temporal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValueRange rangeRefinedBy(TemporalAccessor temporal) {
|
||||
return range;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrom(TemporalAccessor temporal) {
|
||||
return convert.applyAsLong(temporal.getLong(field));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <R extends Temporal> R adjustInto(R temporal, long newValue) {
|
||||
return (R) temporal.with(field, newValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.util.*;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||
|
||||
/**
|
||||
* The {@link OSAPI} API allows interacting with the current computer.
|
||||
*
|
||||
* @cc.module os
|
||||
*/
|
||||
public class OSAPI implements ILuaAPI {
|
||||
private final IAPIEnvironment apiEnvironment;
|
||||
|
||||
private final Int2ObjectMap<Alarm> alarms = new Int2ObjectOpenHashMap<>();
|
||||
private int clock;
|
||||
private double time;
|
||||
private int day;
|
||||
|
||||
private int nextAlarmToken = 0;
|
||||
|
||||
private record Alarm(double time, int day) implements Comparable<Alarm> {
|
||||
@Override
|
||||
public int compareTo(@Nonnull Alarm o) {
|
||||
var t = day * 24.0 + time;
|
||||
var ot = day * 24.0 + time;
|
||||
return Double.compare(t, ot);
|
||||
}
|
||||
}
|
||||
|
||||
public OSAPI(IAPIEnvironment environment) {
|
||||
apiEnvironment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[]{ "os" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startup() {
|
||||
time = apiEnvironment.getComputerEnvironment().getTimeOfDay();
|
||||
day = apiEnvironment.getComputerEnvironment().getDay();
|
||||
clock = 0;
|
||||
|
||||
synchronized (alarms) {
|
||||
alarms.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
clock++;
|
||||
|
||||
// Wait for all of our alarms
|
||||
synchronized (alarms) {
|
||||
var previousTime = time;
|
||||
var previousDay = day;
|
||||
var time = apiEnvironment.getComputerEnvironment().getTimeOfDay();
|
||||
var day = apiEnvironment.getComputerEnvironment().getDay();
|
||||
|
||||
if (time > previousTime || day > previousDay) {
|
||||
var now = this.day * 24.0 + this.time;
|
||||
Iterator<Int2ObjectMap.Entry<Alarm>> it = alarms.int2ObjectEntrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
var entry = it.next();
|
||||
var alarm = entry.getValue();
|
||||
var t = alarm.day * 24.0 + alarm.time;
|
||||
if (now >= t) {
|
||||
apiEnvironment.queueEvent("alarm", entry.getIntKey());
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.time = time;
|
||||
this.day = day;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
synchronized (alarms) {
|
||||
alarms.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static float getTimeForCalendar(Calendar c) {
|
||||
float time = c.get(Calendar.HOUR_OF_DAY);
|
||||
time += c.get(Calendar.MINUTE) / 60.0f;
|
||||
time += c.get(Calendar.SECOND) / (60.0f * 60.0f);
|
||||
return time;
|
||||
}
|
||||
|
||||
private static int getDayForCalendar(Calendar c) {
|
||||
var g = c instanceof GregorianCalendar ? (GregorianCalendar) c : new GregorianCalendar();
|
||||
var year = c.get(Calendar.YEAR);
|
||||
var day = 0;
|
||||
for (var y = 1970; y < year; y++) {
|
||||
day += g.isLeapYear(y) ? 366 : 365;
|
||||
}
|
||||
day += c.get(Calendar.DAY_OF_YEAR);
|
||||
return day;
|
||||
}
|
||||
|
||||
private static long getEpochForCalendar(Calendar c) {
|
||||
return c.getTime().getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event to the event queue. This event can later be pulled with
|
||||
* os.pullEvent.
|
||||
*
|
||||
* @param name The name of the event to queue.
|
||||
* @param args The parameters of the event.
|
||||
* @cc.tparam string name The name of the event to queue.
|
||||
* @cc.param ... The parameters of the event.
|
||||
* @cc.see os.pullEvent To pull the event queued
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void queueEvent(String name, IArguments args) {
|
||||
apiEnvironment.queueEvent(name, args.drop(1).getAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a timer that will run for the specified number of seconds. Once
|
||||
* the timer fires, a {@code timer} event will be added to the queue with
|
||||
* the ID returned from this function as the first parameter.
|
||||
* <p>
|
||||
* As with @{os.sleep|sleep}, {@code timer} will automatically be rounded up
|
||||
* to the nearest multiple of 0.05 seconds, as it waits for a fixed amount
|
||||
* of world ticks.
|
||||
*
|
||||
* @param timer The number of seconds until the timer fires.
|
||||
* @return The ID of the new timer. This can be used to filter the
|
||||
* {@code timer} event, or {@link #cancelTimer cancel the timer}.
|
||||
* @throws LuaException If the time is below zero.
|
||||
* @see #cancelTimer To cancel a timer.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final int startTimer(double timer) throws LuaException {
|
||||
return apiEnvironment.startTimer(Math.round(checkFinite(0, timer) / 0.05));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a timer previously started with startTimer. This will stop the
|
||||
* timer from firing.
|
||||
*
|
||||
* @param token The ID of the timer to cancel.
|
||||
* @see #startTimer To start a timer.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void cancelTimer(int token) {
|
||||
apiEnvironment.cancelTimer(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an alarm that will fire at the specified in-game time. When it
|
||||
* fires, * an {@code alarm} event will be added to the event queue with the
|
||||
* ID * returned from this function as the first parameter.
|
||||
*
|
||||
* @param time The time at which to fire the alarm, in the range [0.0, 24.0).
|
||||
* @return The ID of the new alarm. This can be used to filter the
|
||||
* {@code alarm} event, or {@link #cancelAlarm cancel the alarm}.
|
||||
* @throws LuaException If the time is out of range.
|
||||
* @cc.since 1.2
|
||||
* @see #cancelAlarm To cancel an alarm.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final int setAlarm(double time) throws LuaException {
|
||||
checkFinite(0, time);
|
||||
if (time < 0.0 || time >= 24.0) throw new LuaException("Number out of range");
|
||||
synchronized (alarms) {
|
||||
var day = time > this.time ? this.day : this.day + 1;
|
||||
alarms.put(nextAlarmToken, new Alarm(time, day));
|
||||
return nextAlarmToken++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an alarm previously started with setAlarm. This will stop the
|
||||
* alarm from firing.
|
||||
*
|
||||
* @param token The ID of the alarm to cancel.
|
||||
* @cc.since 1.2
|
||||
* @see #setAlarm To set an alarm.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void cancelAlarm(int token) {
|
||||
synchronized (alarms) {
|
||||
alarms.remove(token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the computer immediately.
|
||||
*/
|
||||
@LuaFunction("shutdown")
|
||||
public final void doShutdown() {
|
||||
apiEnvironment.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reboots the computer immediately.
|
||||
*/
|
||||
@LuaFunction("reboot")
|
||||
public final void doReboot() {
|
||||
apiEnvironment.reboot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the computer.
|
||||
*
|
||||
* @return The ID of the computer.
|
||||
*/
|
||||
@LuaFunction({ "getComputerID", "computerID" })
|
||||
public final int getComputerID() {
|
||||
return apiEnvironment.getComputerID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label of the computer, or {@code nil} if none is set.
|
||||
*
|
||||
* @return The label of the computer.
|
||||
* @cc.treturn string The label of the computer.
|
||||
* @cc.since 1.3
|
||||
*/
|
||||
@LuaFunction({ "getComputerLabel", "computerLabel" })
|
||||
public final Object[] getComputerLabel() {
|
||||
var label = apiEnvironment.getLabel();
|
||||
return label == null ? null : new Object[]{ label };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the label of this computer.
|
||||
*
|
||||
* @param label The new label. May be {@code nil} in order to clear it.
|
||||
* @cc.since 1.3
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void setComputerLabel(Optional<String> label) {
|
||||
apiEnvironment.setLabel(StringUtil.normaliseLabel(label.orElse(null)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of seconds that the computer has been running.
|
||||
*
|
||||
* @return The computer's uptime.
|
||||
* @cc.since 1.2
|
||||
*/
|
||||
@LuaFunction
|
||||
public final double clock() {
|
||||
return clock * 0.05;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current time depending on the string passed in. This will
|
||||
* always be in the range [0.0, 24.0).
|
||||
* <p>
|
||||
* * If called with {@code ingame}, the current world time will be returned.
|
||||
* This is the default if nothing is passed.
|
||||
* * If called with {@code utc}, returns the hour of the day in UTC time.
|
||||
* * If called with {@code local}, returns the hour of the day in the
|
||||
* timezone the server is located in.
|
||||
* <p>
|
||||
* This function can also be called with a table returned from {@link #date},
|
||||
* which will convert the date fields into a UNIX timestamp (number of
|
||||
* seconds since 1 January 1970).
|
||||
*
|
||||
* @param args The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified.
|
||||
* @return The hour of the selected locale, or a UNIX timestamp from the table, depending on the argument passed in.
|
||||
* @throws LuaException If an invalid locale is passed.
|
||||
* @cc.tparam [opt] string|table locale The locale of the time, or a table filled by {@code os.date("*t")} to decode. Defaults to {@code ingame} locale if not specified.
|
||||
* @cc.see textutils.formatTime To convert times into a user-readable string.
|
||||
* @cc.usage Print the current in-game time.
|
||||
* <pre>{@code
|
||||
* textutils.formatTime(os.time())
|
||||
* }</pre>
|
||||
* @cc.since 1.2
|
||||
* @cc.changed 1.80pr1 Add support for getting the local local and UTC time.
|
||||
* @cc.changed 1.82.0 Arguments are now case insensitive.
|
||||
* @cc.changed 1.83.0 {@link #time(IArguments)} now accepts table arguments and converts them to UNIX timestamps.
|
||||
* @see #date To get a date table that can be converted with this function.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object time(IArguments args) throws LuaException {
|
||||
var value = args.get(0);
|
||||
if (value instanceof Map) return LuaDateTime.fromTable((Map<?, ?>) value);
|
||||
|
||||
var param = args.optString(0, "ingame");
|
||||
return switch (param.toLowerCase(Locale.ROOT)) {
|
||||
case "utc" -> getTimeForCalendar(Calendar.getInstance(TimeZone.getTimeZone("UTC")));
|
||||
case "local" -> getTimeForCalendar(Calendar.getInstance());
|
||||
case "ingame" -> time;
|
||||
default -> throw new LuaException("Unsupported operation");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the day depending on the locale specified.
|
||||
* <p>
|
||||
* * If called with {@code ingame}, returns the number of days since the
|
||||
* world was created. This is the default.
|
||||
* * If called with {@code utc}, returns the number of days since 1 January
|
||||
* 1970 in the UTC timezone.
|
||||
* * If called with {@code local}, returns the number of days since 1
|
||||
* January 1970 in the server's local timezone.
|
||||
*
|
||||
* @param args The locale to get the day for. Defaults to {@code ingame} if not set.
|
||||
* @return The day depending on the selected locale.
|
||||
* @throws LuaException If an invalid locale is passed.
|
||||
* @cc.since 1.48
|
||||
* @cc.changed 1.82.0 Arguments are now case insensitive.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final int day(Optional<String> args) throws LuaException {
|
||||
return switch (args.orElse("ingame").toLowerCase(Locale.ROOT)) {
|
||||
case "utc" -> getDayForCalendar(Calendar.getInstance(TimeZone.getTimeZone("UTC")));
|
||||
case "local" -> getDayForCalendar(Calendar.getInstance());
|
||||
case "ingame" -> day;
|
||||
default -> throw new LuaException("Unsupported operation");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of milliseconds since an epoch depending on the locale.
|
||||
* <p>
|
||||
* * If called with {@code ingame}, returns the number of milliseconds since the
|
||||
* world was created. This is the default.
|
||||
* * If called with {@code utc}, returns the number of milliseconds since 1
|
||||
* January 1970 in the UTC timezone.
|
||||
* * If called with {@code local}, returns the number of milliseconds since 1
|
||||
* January 1970 in the server's local timezone.
|
||||
*
|
||||
* @param args The locale to get the milliseconds for. Defaults to {@code ingame} if not set.
|
||||
* @return The milliseconds since the epoch depending on the selected locale.
|
||||
* @throws LuaException If an invalid locale is passed.
|
||||
* @cc.since 1.80pr1
|
||||
* @cc.usage Get the current time and use {@link #date} to convert it to a table.
|
||||
* <pre>{@code
|
||||
* -- Dividing by 1000 converts it from milliseconds to seconds.
|
||||
* local time = os.epoch("local") / 1000
|
||||
* local time_table = os.date("*t", time)
|
||||
* print(textutils.serialize(time_table))
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final long epoch(Optional<String> args) throws LuaException {
|
||||
switch (args.orElse("ingame").toLowerCase(Locale.ROOT)) {
|
||||
case "utc": {
|
||||
// Get utc epoch
|
||||
var c = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
return getEpochForCalendar(c);
|
||||
}
|
||||
case "local": {
|
||||
// Get local epoch
|
||||
var c = Calendar.getInstance();
|
||||
return getEpochForCalendar(c);
|
||||
}
|
||||
case "ingame":
|
||||
// Get in-game epoch
|
||||
synchronized (alarms) {
|
||||
return day * 86400000L + (long) (time * 3600000.0);
|
||||
}
|
||||
default:
|
||||
throw new LuaException("Unsupported operation");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a date string (or table) using a specified format string and
|
||||
* optional time to format.
|
||||
* <p>
|
||||
* The format string takes the same formats as C's {@code strftime} function
|
||||
* (http://www.cplusplus.com/reference/ctime/strftime/). In extension, it
|
||||
* can be prefixed with an exclamation mark ({@code !}) to use UTC time
|
||||
* instead of the server's local timezone.
|
||||
* <p>
|
||||
* If the format is exactly {@code *t} (optionally prefixed with {@code !}), a
|
||||
* table will be returned instead. This table has fields for the year, month,
|
||||
* day, hour, minute, second, day of the week, day of the year, and whether
|
||||
* Daylight Savings Time is in effect. This table can be converted to a UNIX
|
||||
* timestamp (days since 1 January 1970) with {@link #date}.
|
||||
*
|
||||
* @param formatA The format of the string to return. This defaults to {@code %c}, which expands to a string similar to "Sat Dec 24 16:58:00 2011".
|
||||
* @param timeA The time to convert to a string. This defaults to the current time.
|
||||
* @return The resulting format string.
|
||||
* @throws LuaException If an invalid format is passed.
|
||||
* @cc.since 1.83.0
|
||||
* @cc.usage Print the current date in a user-friendly string.
|
||||
* <pre>{@code
|
||||
* os.date("%A %d %B %Y") -- See the reference above!
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object date(Optional<String> formatA, Optional<Long> timeA) throws LuaException {
|
||||
var format = formatA.orElse("%c");
|
||||
long time = timeA.orElseGet(() -> Instant.now().getEpochSecond());
|
||||
|
||||
var instant = Instant.ofEpochSecond(time);
|
||||
ZonedDateTime date;
|
||||
ZoneOffset offset;
|
||||
if (format.startsWith("!")) {
|
||||
offset = ZoneOffset.UTC;
|
||||
date = ZonedDateTime.ofInstant(instant, offset);
|
||||
format = format.substring(1);
|
||||
} else {
|
||||
var id = ZoneId.systemDefault();
|
||||
offset = id.getRules().getOffset(instant);
|
||||
date = ZonedDateTime.ofInstant(instant, id);
|
||||
}
|
||||
|
||||
if (format.equals("*t")) return LuaDateTime.toTable(date, offset, instant);
|
||||
|
||||
var formatter = new DateTimeFormatterBuilder();
|
||||
LuaDateTime.format(formatter, format);
|
||||
// ROOT would be more sensible, but US appears more consistent with the default C locale
|
||||
// on Linux.
|
||||
return formatter.toFormatter(Locale.US).format(date);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.peripheral.IWorkMonitor;
|
||||
import dan200.computercraft.api.peripheral.NotAttachedException;
|
||||
import dan200.computercraft.core.asm.LuaMethod;
|
||||
import dan200.computercraft.core.asm.PeripheralMethod;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.core.util.LuaUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* CC's "native" peripheral API. This is wrapped within CraftOS to provide a version which works with modems.
|
||||
*
|
||||
* @cc.module peripheral
|
||||
* @hidden
|
||||
*/
|
||||
public class PeripheralAPI implements ILuaAPI, IAPIEnvironment.IPeripheralChangeListener {
|
||||
private class PeripheralWrapper extends ComputerAccess {
|
||||
private final String side;
|
||||
private final IPeripheral peripheral;
|
||||
|
||||
private final String type;
|
||||
private final Set<String> additionalTypes;
|
||||
private final Map<String, PeripheralMethod> methodMap;
|
||||
private boolean attached = false;
|
||||
|
||||
PeripheralWrapper(IPeripheral peripheral, String side) {
|
||||
super(environment);
|
||||
this.side = side;
|
||||
this.peripheral = peripheral;
|
||||
|
||||
type = Objects.requireNonNull(peripheral.getType(), "Peripheral type cannot be null");
|
||||
additionalTypes = peripheral.getAdditionalTypes();
|
||||
|
||||
methodMap = PeripheralAPI.getMethods(peripheral);
|
||||
}
|
||||
|
||||
public IPeripheral getPeripheral() {
|
||||
return peripheral;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Set<String> getAdditionalTypes() {
|
||||
return additionalTypes;
|
||||
}
|
||||
|
||||
public Collection<String> getMethods() {
|
||||
return methodMap.keySet();
|
||||
}
|
||||
|
||||
public synchronized boolean isAttached() {
|
||||
return attached;
|
||||
}
|
||||
|
||||
public synchronized void attach() {
|
||||
attached = true;
|
||||
peripheral.attach(this);
|
||||
}
|
||||
|
||||
public void detach() {
|
||||
// Call detach
|
||||
peripheral.detach(this);
|
||||
|
||||
synchronized (this) {
|
||||
// Unmount everything the detach function forgot to do
|
||||
unmountAll();
|
||||
}
|
||||
|
||||
attached = false;
|
||||
}
|
||||
|
||||
public MethodResult call(ILuaContext context, String methodName, IArguments arguments) throws LuaException {
|
||||
PeripheralMethod method;
|
||||
synchronized (this) {
|
||||
method = methodMap.get(methodName);
|
||||
}
|
||||
|
||||
if (method == null) throw new LuaException("No such method " + methodName);
|
||||
|
||||
environment.observe(Metrics.PERIPHERAL_OPS);
|
||||
return method.apply(peripheral, context, this, arguments);
|
||||
}
|
||||
|
||||
// IComputerAccess implementation
|
||||
@Override
|
||||
public synchronized String mount(@Nonnull String desiredLoc, @Nonnull IMount mount, @Nonnull String driveName) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
return super.mount(desiredLoc, mount, driveName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized String mountWritable(@Nonnull String desiredLoc, @Nonnull IWritableMount mount, @Nonnull String driveName) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
return super.mountWritable(desiredLoc, mount, driveName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount(String location) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
super.unmount(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getID() {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
return super.getID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(@Nonnull String event, Object... arguments) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
super.queueEvent(event, arguments);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getAttachmentName() {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
return side;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Map<String, IPeripheral> getAvailablePeripherals() {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
|
||||
Map<String, IPeripheral> peripherals = new HashMap<>();
|
||||
for (var wrapper : PeripheralAPI.this.peripherals) {
|
||||
if (wrapper != null && wrapper.isAttached()) {
|
||||
peripherals.put(wrapper.getAttachmentName(), wrapper.getPeripheral());
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(peripherals);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IPeripheral getAvailablePeripheral(@Nonnull String name) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
|
||||
for (var wrapper : peripherals) {
|
||||
if (wrapper != null && wrapper.isAttached() && wrapper.getAttachmentName().equals(name)) {
|
||||
return wrapper.getPeripheral();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public IWorkMonitor getMainThreadMonitor() {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
return super.getMainThreadMonitor();
|
||||
}
|
||||
}
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final PeripheralWrapper[] peripherals = new PeripheralWrapper[6];
|
||||
private boolean running;
|
||||
|
||||
public PeripheralAPI(IAPIEnvironment environment) {
|
||||
this.environment = environment;
|
||||
this.environment.setPeripheralChangeListener(this);
|
||||
running = false;
|
||||
}
|
||||
|
||||
// IPeripheralChangeListener
|
||||
|
||||
@Override
|
||||
public void onPeripheralChanged(ComputerSide side, IPeripheral newPeripheral) {
|
||||
synchronized (peripherals) {
|
||||
var index = side.ordinal();
|
||||
if (peripherals[index] != null) {
|
||||
// Queue a detachment
|
||||
final var wrapper = peripherals[index];
|
||||
if (wrapper.isAttached()) wrapper.detach();
|
||||
|
||||
// Queue a detachment event
|
||||
environment.queueEvent("peripheral_detach", side.getName());
|
||||
}
|
||||
|
||||
// Assign the new peripheral
|
||||
peripherals[index] = newPeripheral == null ? null
|
||||
: new PeripheralWrapper(newPeripheral, side.getName());
|
||||
|
||||
if (peripherals[index] != null) {
|
||||
// Queue an attachment
|
||||
final var wrapper = peripherals[index];
|
||||
if (running && !wrapper.isAttached()) wrapper.attach();
|
||||
|
||||
// Queue an attachment event
|
||||
environment.queueEvent("peripheral", side.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[]{ "peripheral" };
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startup() {
|
||||
synchronized (peripherals) {
|
||||
running = true;
|
||||
for (var i = 0; i < 6; i++) {
|
||||
var wrapper = peripherals[i];
|
||||
if (wrapper != null && !wrapper.isAttached()) wrapper.attach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
synchronized (peripherals) {
|
||||
running = false;
|
||||
for (var i = 0; i < 6; i++) {
|
||||
var wrapper = peripherals[i];
|
||||
if (wrapper != null && wrapper.isAttached()) {
|
||||
wrapper.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final boolean isPresent(String sideName) {
|
||||
var side = ComputerSide.valueOfInsensitive(sideName);
|
||||
if (side != null) {
|
||||
synchronized (peripherals) {
|
||||
var p = peripherals[side.ordinal()];
|
||||
if (p != null) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] getType(String sideName) {
|
||||
var side = ComputerSide.valueOfInsensitive(sideName);
|
||||
if (side == null) return null;
|
||||
|
||||
synchronized (peripherals) {
|
||||
var p = peripherals[side.ordinal()];
|
||||
return p == null ? null : LuaUtil.consArray(p.getType(), p.getAdditionalTypes());
|
||||
}
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] hasType(String sideName, String type) {
|
||||
var side = ComputerSide.valueOfInsensitive(sideName);
|
||||
if (side == null) return null;
|
||||
|
||||
synchronized (peripherals) {
|
||||
var p = peripherals[side.ordinal()];
|
||||
if (p != null) {
|
||||
return new Object[]{ p.getType().equals(type) || p.getAdditionalTypes().contains(type) };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final Object[] getMethods(String sideName) {
|
||||
var side = ComputerSide.valueOfInsensitive(sideName);
|
||||
if (side == null) return null;
|
||||
|
||||
synchronized (peripherals) {
|
||||
var p = peripherals[side.ordinal()];
|
||||
if (p != null) return new Object[]{ p.getMethods() };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@LuaFunction
|
||||
public final MethodResult call(ILuaContext context, IArguments args) throws LuaException {
|
||||
var side = ComputerSide.valueOfInsensitive(args.getString(0));
|
||||
var methodName = args.getString(1);
|
||||
var methodArgs = args.drop(2);
|
||||
|
||||
if (side == null) throw new LuaException("No peripheral attached");
|
||||
|
||||
PeripheralWrapper p;
|
||||
synchronized (peripherals) {
|
||||
p = peripherals[side.ordinal()];
|
||||
}
|
||||
if (p == null) throw new LuaException("No peripheral attached");
|
||||
|
||||
try {
|
||||
return p.call(context, methodName, methodArgs).adjustError(1);
|
||||
} catch (LuaException e) {
|
||||
// We increase the error level by one in order to shift the error level to where peripheral.call was
|
||||
// invoked. It would be possible to do it in Lua code, but would add significantly more overhead.
|
||||
if (e.getLevel() > 0) throw new FastLuaException(e.getMessage(), e.getLevel() + 1);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, PeripheralMethod> getMethods(IPeripheral peripheral) {
|
||||
var dynamicMethods = peripheral instanceof IDynamicPeripheral
|
||||
? Objects.requireNonNull(((IDynamicPeripheral) peripheral).getMethodNames(), "Peripheral methods cannot be null")
|
||||
: LuaMethod.EMPTY_METHODS;
|
||||
|
||||
var methods = PeripheralMethod.GENERATOR.getMethods(peripheral.getClass());
|
||||
|
||||
Map<String, PeripheralMethod> methodMap = new HashMap<>(methods.size() + dynamicMethods.length);
|
||||
for (var i = 0; i < dynamicMethods.length; i++) {
|
||||
methodMap.put(dynamicMethods[i], PeripheralMethod.DYNAMIC.get(i));
|
||||
}
|
||||
for (var method : methods) {
|
||||
methodMap.put(method.getName(), method.getMethod());
|
||||
}
|
||||
return methodMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.computer.ComputerSide;
|
||||
|
||||
/**
|
||||
* Get and set redstone signals adjacent to this computer.
|
||||
* <p>
|
||||
* The {@link RedstoneAPI} library exposes three "types" of redstone control:
|
||||
* - Binary input/output ({@link #setOutput}/{@link #getInput}): These simply check if a redstone wire has any input or
|
||||
* output. A signal strength of 1 and 15 are treated the same.
|
||||
* - Analogue input/output ({@link #setAnalogOutput}/{@link #getAnalogInput}): These work with the actual signal
|
||||
* strength of the redstone wired, from 0 to 15.
|
||||
* - Bundled cables ({@link #setBundledOutput}/{@link #getBundledInput}): These interact with "bundled" cables, such
|
||||
* as those from Project:Red. These allow you to send 16 separate on/off signals. Each channel corresponds to a
|
||||
* colour, with the first being @{colors.white} and the last @{colors.black}.
|
||||
* <p>
|
||||
* Whenever a redstone input changes, a @{event!redstone} event will be fired. This may be used instead of repeativly
|
||||
* polling.
|
||||
* <p>
|
||||
* This module may also be referred to as {@code rs}. For example, one may call {@code rs.getSides()} instead of
|
||||
* {@link #getSides}.
|
||||
*
|
||||
* @cc.usage Toggle the redstone signal above the computer every 0.5 seconds.
|
||||
*
|
||||
* <pre>{@code
|
||||
* while true do
|
||||
* redstone.setOutput("top", not redstone.getOutput("top"))
|
||||
* sleep(0.5)
|
||||
* end
|
||||
* }</pre>
|
||||
* @cc.usage Mimic a redstone comparator in [subtraction mode][comparator].
|
||||
*
|
||||
* <pre>{@code
|
||||
* while true do
|
||||
* local rear = rs.getAnalogueInput("back")
|
||||
* local sides = math.max(rs.getAnalogueInput("left"), rs.getAnalogueInput("right"))
|
||||
* rs.setAnalogueOutput("front", math.max(rear - sides, 0))
|
||||
*
|
||||
* os.pullEvent("redstone") -- Wait for a change to inputs.
|
||||
* end
|
||||
* }</pre>
|
||||
* <p>
|
||||
* [comparator]: https://minecraft.gamepedia.com/Redstone_Comparator#Subtract_signal_strength "Redstone Comparator on
|
||||
* the Minecraft wiki."
|
||||
* @cc.module redstone
|
||||
*/
|
||||
public class RedstoneAPI implements ILuaAPI {
|
||||
private final IAPIEnvironment environment;
|
||||
|
||||
public RedstoneAPI(IAPIEnvironment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[]{ "rs", "redstone" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a table containing the six sides of the computer. Namely, "top", "bottom", "left", "right", "front" and
|
||||
* "back".
|
||||
*
|
||||
* @return A table of valid sides.
|
||||
* @cc.since 1.2
|
||||
*/
|
||||
@LuaFunction
|
||||
public final String[] getSides() {
|
||||
return ComputerSide.NAMES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn the redstone signal of a specific side on or off.
|
||||
*
|
||||
* @param side The side to set.
|
||||
* @param on Whether the redstone signal should be on or off. When on, a signal strength of 15 is emitted.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void setOutput(ComputerSide side, boolean on) {
|
||||
environment.setOutput(side, on ? 15 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current redstone output of a specific side.
|
||||
*
|
||||
* @param side The side to get.
|
||||
* @return Whether the redstone output is on or off.
|
||||
* @see #setOutput
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean getOutput(ComputerSide side) {
|
||||
return environment.getOutput(side) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current redstone input of a specific side.
|
||||
*
|
||||
* @param side The side to get.
|
||||
* @return Whether the redstone input is on or off.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean getInput(ComputerSide side) {
|
||||
return environment.getInput(side) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the redstone signal strength for a specific side.
|
||||
*
|
||||
* @param side The side to set.
|
||||
* @param value The signal strength between 0 and 15.
|
||||
* @throws LuaException If {@code value} is not betwene 0 and 15.
|
||||
* @cc.since 1.51
|
||||
*/
|
||||
@LuaFunction({ "setAnalogOutput", "setAnalogueOutput" })
|
||||
public final void setAnalogOutput(ComputerSide side, int value) throws LuaException {
|
||||
if (value < 0 || value > 15) throw new LuaException("Expected number in range 0-15");
|
||||
environment.setOutput(side, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the redstone output signal strength for a specific side.
|
||||
*
|
||||
* @param side The side to get.
|
||||
* @return The output signal strength, between 0 and 15.
|
||||
* @cc.since 1.51
|
||||
* @see #setAnalogOutput
|
||||
*/
|
||||
@LuaFunction({ "getAnalogOutput", "getAnalogueOutput" })
|
||||
public final int getAnalogOutput(ComputerSide side) {
|
||||
return environment.getOutput(side);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the redstone input signal strength for a specific side.
|
||||
*
|
||||
* @param side The side to get.
|
||||
* @return The input signal strength, between 0 and 15.
|
||||
* @cc.since 1.51
|
||||
*/
|
||||
@LuaFunction({ "getAnalogInput", "getAnalogueInput" })
|
||||
public final int getAnalogInput(ComputerSide side) {
|
||||
return environment.getInput(side);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the bundled cable output for a specific side.
|
||||
*
|
||||
* @param side The side to set.
|
||||
* @param output The colour bitmask to set.
|
||||
* @cc.see colors.subtract For removing a colour from the bitmask.
|
||||
* @cc.see colors.combine For adding a color to the bitmask.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void setBundledOutput(ComputerSide side, int output) {
|
||||
environment.setBundledOutput(side, output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bundled cable output for a specific side.
|
||||
*
|
||||
* @param side The side to get.
|
||||
* @return The bundle cable's output.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final int getBundledOutput(ComputerSide side) {
|
||||
return environment.getBundledOutput(side);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bundled cable input for a specific side.
|
||||
*
|
||||
* @param side The side to get.
|
||||
* @return The bundle cable's input.
|
||||
* @see #testBundledInput To determine if a specific colour is set.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final int getBundledInput(ComputerSide side) {
|
||||
return environment.getBundledInput(side);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a specific combination of colours are on for the given side.
|
||||
*
|
||||
* @param side The side to test.
|
||||
* @param mask The mask to test.
|
||||
* @return If the colours are on.
|
||||
* @cc.usage Check if @{colors.white} and @{colors.black} are on above the computer.
|
||||
* <pre>{@code
|
||||
* print(redstone.testBundledInput("top", colors.combine(colors.white, colors.black)))
|
||||
* }</pre>
|
||||
* @see #getBundledInput
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean testBundledInput(ComputerSide side, int mask) {
|
||||
var input = environment.getBundledInput(side);
|
||||
return (input & mask) == mask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.getNumericType;
|
||||
|
||||
/**
|
||||
* Various helpers for tables.
|
||||
*/
|
||||
public final class TableHelper {
|
||||
private TableHelper() {
|
||||
throw new IllegalStateException("Cannot instantiate singleton " + getClass().getName());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static LuaException badKey(@Nonnull String key, @Nonnull String expected, @Nullable Object actual) {
|
||||
return badKey(key, expected, LuaValues.getType(actual));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static LuaException badKey(@Nonnull String key, @Nonnull String expected, @Nonnull String actual) {
|
||||
return new LuaException("bad field '" + key + "' (" + expected + " expected, got " + actual + ")");
|
||||
}
|
||||
|
||||
public static double getNumberField(@Nonnull Map<?, ?> table, @Nonnull String key) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
} else {
|
||||
throw badKey(key, "number", value);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getIntField(@Nonnull Map<?, ?> table, @Nonnull String key) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value instanceof Number) {
|
||||
return (int) ((Number) value).longValue();
|
||||
} else {
|
||||
throw badKey(key, "number", value);
|
||||
}
|
||||
}
|
||||
|
||||
public static double getRealField(@Nonnull Map<?, ?> table, @Nonnull String key) throws LuaException {
|
||||
return checkReal(key, getNumberField(table, key));
|
||||
}
|
||||
|
||||
public static boolean getBooleanField(@Nonnull Map<?, ?> table, @Nonnull String key) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
} else {
|
||||
throw badKey(key, "boolean", value);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String getStringField(@Nonnull Map<?, ?> table, @Nonnull String key) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
} else {
|
||||
throw badKey(key, "string", value);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nonnull
|
||||
public static Map<Object, Object> getTableField(@Nonnull Map<?, ?> table, @Nonnull String key) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value instanceof Map) {
|
||||
return (Map<Object, Object>) value;
|
||||
} else {
|
||||
throw badKey(key, "table", value);
|
||||
}
|
||||
}
|
||||
|
||||
public static double optNumberField(@Nonnull Map<?, ?> table, @Nonnull String key, double def) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value == null) {
|
||||
return def;
|
||||
} else if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
} else {
|
||||
throw badKey(key, "number", value);
|
||||
}
|
||||
}
|
||||
|
||||
public static int optIntField(@Nonnull Map<?, ?> table, @Nonnull String key, int def) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value == null) {
|
||||
return def;
|
||||
} else if (value instanceof Number) {
|
||||
return (int) ((Number) value).longValue();
|
||||
} else {
|
||||
throw badKey(key, "number", value);
|
||||
}
|
||||
}
|
||||
|
||||
public static double optRealField(@Nonnull Map<?, ?> table, @Nonnull String key, double def) throws LuaException {
|
||||
return checkReal(key, optNumberField(table, key, def));
|
||||
}
|
||||
|
||||
public static boolean optBooleanField(@Nonnull Map<?, ?> table, @Nonnull String key, boolean def) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value == null) {
|
||||
return def;
|
||||
} else if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
} else {
|
||||
throw badKey(key, "boolean", value);
|
||||
}
|
||||
}
|
||||
|
||||
public static String optStringField(@Nonnull Map<?, ?> table, @Nonnull String key, String def) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value == null) {
|
||||
return def;
|
||||
} else if (value instanceof String) {
|
||||
return (String) value;
|
||||
} else {
|
||||
throw badKey(key, "string", value);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<Object, Object> optTableField(@Nonnull Map<?, ?> table, @Nonnull String key, Map<Object, Object> def) throws LuaException {
|
||||
var value = table.get(key);
|
||||
if (value == null) {
|
||||
return def;
|
||||
} else if (value instanceof Map) {
|
||||
return (Map<Object, Object>) value;
|
||||
} else {
|
||||
throw badKey(key, "table", value);
|
||||
}
|
||||
}
|
||||
|
||||
private static double checkReal(@Nonnull String key, double value) throws LuaException {
|
||||
if (!Double.isFinite(value)) throw badKey(key, "number", getNumericType(value));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Interact with a computer's terminal or monitors, writing text and drawing
|
||||
* ASCII graphics.
|
||||
*
|
||||
* @cc.module term
|
||||
*/
|
||||
public class TermAPI extends TermMethods implements ILuaAPI {
|
||||
private final Terminal terminal;
|
||||
|
||||
public TermAPI(IAPIEnvironment environment) {
|
||||
terminal = environment.getTerminal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return new String[]{ "term" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default palette value for a colour.
|
||||
*
|
||||
* @param colour The colour whose palette should be fetched.
|
||||
* @return The RGB values.
|
||||
* @throws LuaException When given an invalid colour.
|
||||
* @cc.treturn number The red channel, will be between 0 and 1.
|
||||
* @cc.treturn number The green channel, will be between 0 and 1.
|
||||
* @cc.treturn number The blue channel, will be between 0 and 1.
|
||||
* @cc.since 1.81.0
|
||||
* @see TermMethods#setPaletteColour(IArguments) To change the palette colour.
|
||||
*/
|
||||
@LuaFunction({ "nativePaletteColour", "nativePaletteColor" })
|
||||
public final Object[] nativePaletteColour(int colour) throws LuaException {
|
||||
var actualColour = 15 - parseColour(colour);
|
||||
var c = Colour.fromInt(actualColour);
|
||||
return new Object[]{ c.getR(), c.getG(), c.getB() };
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Terminal getTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.terminal.Palette;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A base class for all objects which interact with a terminal. Namely the {@link TermAPI} and monitors.
|
||||
*
|
||||
* @cc.module term.Redirect
|
||||
*/
|
||||
public abstract class TermMethods {
|
||||
private static int getHighestBit(int group) {
|
||||
var bit = 0;
|
||||
while (group > 0) {
|
||||
group >>= 1;
|
||||
bit++;
|
||||
}
|
||||
return bit;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public abstract Terminal getTerminal() throws LuaException;
|
||||
|
||||
/**
|
||||
* Write {@code text} at the current cursor position, moving the cursor to the end of the text.
|
||||
* <p>
|
||||
* Unlike functions like {@code write} and {@code print}, this does not wrap the text - it simply copies the
|
||||
* text to the current terminal line.
|
||||
*
|
||||
* @param arguments The text to write.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.param text The text to write.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void write(IArguments arguments) throws LuaException {
|
||||
var text = StringUtil.toString(arguments.get(0));
|
||||
var terminal = getTerminal();
|
||||
synchronized (terminal) {
|
||||
terminal.write(text);
|
||||
terminal.setCursorPos(terminal.getCursorX() + text.length(), terminal.getCursorY());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move all positions up (or down) by {@code y} pixels.
|
||||
* <p>
|
||||
* Every pixel in the terminal will be replaced by the line {@code y} pixels below it. If {@code y} is negative, it
|
||||
* will copy pixels from above instead.
|
||||
*
|
||||
* @param y The number of lines to move up by. This may be a negative number.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void scroll(int y) throws LuaException {
|
||||
getTerminal().scroll(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the position of the cursor.
|
||||
*
|
||||
* @return The cursor's position.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.treturn number The x position of the cursor.
|
||||
* @cc.treturn number The y position of the cursor.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] getCursorPos() throws LuaException {
|
||||
var terminal = getTerminal();
|
||||
return new Object[]{ terminal.getCursorX() + 1, terminal.getCursorY() + 1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the position of the cursor. {@link #write(IArguments) terminal writes} will begin from this position.
|
||||
*
|
||||
* @param x The new x position of the cursor.
|
||||
* @param y The new y position of the cursor.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void setCursorPos(int x, int y) throws LuaException {
|
||||
var terminal = getTerminal();
|
||||
synchronized (terminal) {
|
||||
terminal.setCursorPos(x - 1, y - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cursor is currently blinking.
|
||||
*
|
||||
* @return If the cursor is blinking.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.since 1.80pr1.9
|
||||
*/
|
||||
@LuaFunction
|
||||
public final boolean getCursorBlink() throws LuaException {
|
||||
return getTerminal().getCursorBlink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the cursor should be visible (and blinking) at the current {@link #getCursorPos() cursor position}.
|
||||
*
|
||||
* @param blink Whether the cursor should blink.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void setCursorBlink(boolean blink) throws LuaException {
|
||||
var terminal = getTerminal();
|
||||
synchronized (terminal) {
|
||||
terminal.setCursorBlink(blink);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the terminal.
|
||||
*
|
||||
* @return The terminal's size.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.treturn number The terminal's width.
|
||||
* @cc.treturn number The terminal's height.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] getSize() throws LuaException {
|
||||
var terminal = getTerminal();
|
||||
return new Object[]{ terminal.getWidth(), terminal.getHeight() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the terminal, filling it with the {@link #getBackgroundColour() current background colour}.
|
||||
*
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void clear() throws LuaException {
|
||||
getTerminal().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the line the cursor is currently on, filling it with the {@link #getBackgroundColour() current background
|
||||
* colour}.
|
||||
*
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void clearLine() throws LuaException {
|
||||
getTerminal().clearLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the colour that new text will be written as.
|
||||
*
|
||||
* @return The current text colour.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.see colors For a list of colour constants, returned by this function.
|
||||
* @cc.since 1.74
|
||||
*/
|
||||
@LuaFunction({ "getTextColour", "getTextColor" })
|
||||
public final int getTextColour() throws LuaException {
|
||||
return encodeColour(getTerminal().getTextColour());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the colour that new text will be written as.
|
||||
*
|
||||
* @param colourArg The new text colour.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.see colors For a list of colour constants.
|
||||
* @cc.since 1.45
|
||||
* @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen.
|
||||
*/
|
||||
@LuaFunction({ "setTextColour", "setTextColor" })
|
||||
public final void setTextColour(int colourArg) throws LuaException {
|
||||
var colour = parseColour(colourArg);
|
||||
var terminal = getTerminal();
|
||||
synchronized (terminal) {
|
||||
terminal.setTextColour(colour);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current background colour. This is used when {@link #write writing text} and {@link #clear clearing}
|
||||
* the terminal.
|
||||
*
|
||||
* @return The current background colour.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.see colors For a list of colour constants, returned by this function.
|
||||
* @cc.since 1.74
|
||||
*/
|
||||
@LuaFunction({ "getBackgroundColour", "getBackgroundColor" })
|
||||
public final int getBackgroundColour() throws LuaException {
|
||||
return encodeColour(getTerminal().getBackgroundColour());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current background colour. This is used when {@link #write writing text} and {@link #clear clearing} the
|
||||
* terminal.
|
||||
*
|
||||
* @param colourArg The new background colour.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.see colors For a list of colour constants.
|
||||
* @cc.since 1.45
|
||||
* @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen.
|
||||
*/
|
||||
@LuaFunction({ "setBackgroundColour", "setBackgroundColor" })
|
||||
public final void setBackgroundColour(int colourArg) throws LuaException {
|
||||
var colour = parseColour(colourArg);
|
||||
var terminal = getTerminal();
|
||||
synchronized (terminal) {
|
||||
terminal.setBackgroundColour(colour);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this terminal supports colour.
|
||||
* <p>
|
||||
* Terminals which do not support colour will still allow writing coloured text/backgrounds, but it will be
|
||||
* displayed in greyscale.
|
||||
*
|
||||
* @return Whether this terminal supports colour.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.since 1.45
|
||||
*/
|
||||
@LuaFunction({ "isColour", "isColor" })
|
||||
public final boolean getIsColour() throws LuaException {
|
||||
return getTerminal().isColour();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes {@code text} to the terminal with the specific foreground and background characters.
|
||||
* <p>
|
||||
* As with {@link #write(IArguments)}, the text will be written at the current cursor location, with the cursor
|
||||
* moving to the end of the text.
|
||||
* <p>
|
||||
* {@code textColour} and {@code backgroundColour} must both be strings the same length as {@code text}. All
|
||||
* characters represent a single hexadecimal digit, which is converted to one of CC's colours. For instance,
|
||||
* {@code "a"} corresponds to purple.
|
||||
*
|
||||
* @param text The text to write.
|
||||
* @param textColour The corresponding text colours.
|
||||
* @param backgroundColour The corresponding background colours.
|
||||
* @throws LuaException If the three inputs are not the same length.
|
||||
* @cc.see colors For a list of colour constants, and their hexadecimal values.
|
||||
* @cc.since 1.74
|
||||
* @cc.changed 1.80pr1 Standard computers can now use all 16 colors, being changed to grayscale on screen.
|
||||
* @cc.usage Prints "Hello, world!" in rainbow text.
|
||||
* <pre>{@code
|
||||
* term.blit("Hello, world!","01234456789ab","0000000000000")
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void blit(ByteBuffer text, ByteBuffer textColour, ByteBuffer backgroundColour) throws LuaException {
|
||||
if (textColour.remaining() != text.remaining() || backgroundColour.remaining() != text.remaining()) {
|
||||
throw new LuaException("Arguments must be the same length");
|
||||
}
|
||||
|
||||
var terminal = getTerminal();
|
||||
synchronized (terminal) {
|
||||
terminal.blit(text, textColour, backgroundColour);
|
||||
terminal.setCursorPos(terminal.getCursorX() + text.remaining(), terminal.getCursorY());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the palette for a specific colour.
|
||||
* <p>
|
||||
* ComputerCraft's palette system allows you to change how a specific colour should be displayed. For instance, you
|
||||
* can make @{colors.red} <em>more red</em> by setting its palette to #FF0000. This does now allow you to draw more
|
||||
* colours - you are still limited to 16 on the screen at one time - but you can change <em>which</em> colours are
|
||||
* used.
|
||||
*
|
||||
* @param args The new palette values.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.tparam [1] number index The colour whose palette should be changed.
|
||||
* @cc.tparam number colour A 24-bit integer representing the RGB value of the colour. For instance the integer
|
||||
* `0xFF0000` corresponds to the colour #FF0000.
|
||||
* @cc.tparam [2] number index The colour whose palette should be changed.
|
||||
* @cc.tparam number r The intensity of the red channel, between 0 and 1.
|
||||
* @cc.tparam number g The intensity of the green channel, between 0 and 1.
|
||||
* @cc.tparam number b The intensity of the blue channel, between 0 and 1.
|
||||
* @cc.usage Change the @{colors.red|red colour} from the default #CC4C4C to #FF0000.
|
||||
* <pre>{@code
|
||||
* term.setPaletteColour(colors.red, 0xFF0000)
|
||||
* term.setTextColour(colors.red)
|
||||
* print("Hello, world!")
|
||||
* }</pre>
|
||||
* @cc.usage As above, but specifying each colour channel separately.
|
||||
* <pre>{@code
|
||||
* term.setPaletteColour(colors.red, 1, 0, 0)
|
||||
* term.setTextColour(colors.red)
|
||||
* print("Hello, world!")
|
||||
* }</pre>
|
||||
* @cc.see colors.unpackRGB To convert from the 24-bit format to three separate channels.
|
||||
* @cc.see colors.packRGB To convert from three separate channels to the 24-bit format.
|
||||
* @cc.since 1.80pr1
|
||||
*/
|
||||
@LuaFunction({ "setPaletteColour", "setPaletteColor" })
|
||||
public final void setPaletteColour(IArguments args) throws LuaException {
|
||||
var colour = 15 - parseColour(args.getInt(0));
|
||||
if (args.count() == 2) {
|
||||
var hex = args.getInt(1);
|
||||
var rgb = Palette.decodeRGB8(hex);
|
||||
setColour(getTerminal(), colour, rgb[0], rgb[1], rgb[2]);
|
||||
} else {
|
||||
var r = args.getFiniteDouble(1);
|
||||
var g = args.getFiniteDouble(2);
|
||||
var b = args.getFiniteDouble(3);
|
||||
setColour(getTerminal(), colour, r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current palette for a specific colour.
|
||||
*
|
||||
* @param colourArg The colour whose palette should be fetched.
|
||||
* @return The resulting colour.
|
||||
* @throws LuaException (hidden) If the terminal cannot be found.
|
||||
* @cc.treturn number The red channel, will be between 0 and 1.
|
||||
* @cc.treturn number The green channel, will be between 0 and 1.
|
||||
* @cc.treturn number The blue channel, will be between 0 and 1.
|
||||
* @cc.since 1.80pr1
|
||||
*/
|
||||
@LuaFunction({ "getPaletteColour", "getPaletteColor" })
|
||||
public final Object[] getPaletteColour(int colourArg) throws LuaException {
|
||||
var colour = 15 - parseColour(colourArg);
|
||||
var terminal = getTerminal();
|
||||
synchronized (terminal) {
|
||||
var colourValues = terminal.getPalette().getColour(colour);
|
||||
return new Object[]{ colourValues[0], colourValues[1], colourValues[2] };
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseColour(int colour) throws LuaException {
|
||||
if (colour <= 0) throw new LuaException("Colour out of range");
|
||||
colour = getHighestBit(colour) - 1;
|
||||
if (colour < 0 || colour > 15) throw new LuaException("Colour out of range");
|
||||
return colour;
|
||||
}
|
||||
|
||||
|
||||
public static int encodeColour(int colour) {
|
||||
return 1 << colour;
|
||||
}
|
||||
|
||||
public static void setColour(Terminal terminal, int colour, double r, double g, double b) {
|
||||
terminal.getPalette().setColour(colour, r, g, b);
|
||||
terminal.setChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.NonWritableChannelException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A seekable, readable byte channel which is backed by a simple byte array.
|
||||
*/
|
||||
public class ArrayByteChannel implements SeekableByteChannel {
|
||||
private boolean closed = false;
|
||||
private int position = 0;
|
||||
|
||||
private final byte[] backing;
|
||||
|
||||
public ArrayByteChannel(byte[] backing) {
|
||||
this.backing = backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer destination) throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
Objects.requireNonNull(destination, "destination");
|
||||
|
||||
if (position >= backing.length) return -1;
|
||||
|
||||
var remaining = Math.min(backing.length - position, destination.remaining());
|
||||
destination.put(backing, position, remaining);
|
||||
position += remaining;
|
||||
return remaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src) throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
if (newPosition < 0 || newPosition > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Position out of bounds");
|
||||
}
|
||||
position = (int) newPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
return backing.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return !closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "rb"}
|
||||
* mode.
|
||||
*
|
||||
* @cc.module fs.BinaryReadHandle
|
||||
*/
|
||||
public class BinaryReadableHandle extends HandleGeneric {
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
private final ReadableByteChannel reader;
|
||||
final SeekableByteChannel seekable;
|
||||
private final ByteBuffer single = ByteBuffer.allocate(1);
|
||||
|
||||
BinaryReadableHandle(ReadableByteChannel reader, SeekableByteChannel seekable, TrackingCloseable closeable) {
|
||||
super(closeable);
|
||||
this.reader = reader;
|
||||
this.seekable = seekable;
|
||||
}
|
||||
|
||||
public static BinaryReadableHandle of(ReadableByteChannel channel, TrackingCloseable closeable) {
|
||||
var seekable = asSeekable(channel);
|
||||
return seekable == null ? new BinaryReadableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
|
||||
}
|
||||
|
||||
public static BinaryReadableHandle of(ReadableByteChannel channel) {
|
||||
return of(channel, new TrackingCloseable.Impl(channel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a number of bytes from this file.
|
||||
*
|
||||
* @param countArg The number of bytes to read. When absent, a single byte will be read <em>as a number</em>. This
|
||||
* may be 0 to determine we are at the end of the file.
|
||||
* @return The read bytes.
|
||||
* @throws LuaException When trying to read a negative number of bytes.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn [1] nil If we are at the end of the file.
|
||||
* @cc.treturn [2] number The value of the byte read. This is returned when the {@code count} is absent.
|
||||
* @cc.treturn [3] string The bytes read as a string. This is returned when the {@code count} is given.
|
||||
* @cc.changed 1.80pr1 Now accepts an integer argument to read multiple bytes, returning a string instead of a number.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] read(Optional<Integer> countArg) throws LuaException {
|
||||
checkOpen();
|
||||
try {
|
||||
if (countArg.isPresent()) {
|
||||
int count = countArg.get();
|
||||
if (count < 0) throw new LuaException("Cannot read a negative number of bytes");
|
||||
if (count == 0 && seekable != null) {
|
||||
return seekable.position() >= seekable.size() ? null : new Object[]{ "" };
|
||||
}
|
||||
|
||||
if (count <= BUFFER_SIZE) {
|
||||
var buffer = ByteBuffer.allocate(count);
|
||||
|
||||
var read = reader.read(buffer);
|
||||
if (read < 0) return null;
|
||||
buffer.flip();
|
||||
return new Object[]{ buffer };
|
||||
} else {
|
||||
// Read the initial set of characters, failing if none are read.
|
||||
var buffer = ByteBuffer.allocate(BUFFER_SIZE);
|
||||
var read = reader.read(buffer);
|
||||
if (read < 0) return null;
|
||||
|
||||
// If we failed to read "enough" here, let's just abort
|
||||
if (read >= count || read < BUFFER_SIZE) {
|
||||
buffer.flip();
|
||||
return new Object[]{ buffer };
|
||||
}
|
||||
|
||||
// Build up an array of ByteBuffers. Hopefully this means we can perform less allocation
|
||||
// than doubling up the buffer each time.
|
||||
var totalRead = read;
|
||||
List<ByteBuffer> parts = new ArrayList<>(4);
|
||||
parts.add(buffer);
|
||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
||||
buffer = ByteBuffer.allocate(Math.min(BUFFER_SIZE, count - totalRead));
|
||||
read = reader.read(buffer);
|
||||
if (read < 0) break;
|
||||
|
||||
totalRead += read;
|
||||
parts.add(buffer);
|
||||
}
|
||||
|
||||
// Now just copy all the bytes across!
|
||||
var bytes = new byte[totalRead];
|
||||
var pos = 0;
|
||||
for (var part : parts) {
|
||||
System.arraycopy(part.array(), 0, bytes, pos, part.position());
|
||||
pos += part.position();
|
||||
}
|
||||
return new Object[]{ bytes };
|
||||
}
|
||||
} else {
|
||||
single.clear();
|
||||
var b = reader.read(single);
|
||||
return b == -1 ? null : new Object[]{ single.get(0) & 0xFF };
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the remainder of the file.
|
||||
*
|
||||
* @return The file, or {@code null} if at the end of it.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn string|nil The remaining contents of the file, or {@code nil} if we are at the end.
|
||||
* @cc.since 1.80pr1
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readAll() throws LuaException {
|
||||
checkOpen();
|
||||
try {
|
||||
var expected = 32;
|
||||
if (seekable != null) expected = Math.max(expected, (int) (seekable.size() - seekable.position()));
|
||||
var stream = new ByteArrayOutputStream(expected);
|
||||
|
||||
var buf = ByteBuffer.allocate(8192);
|
||||
var readAnything = false;
|
||||
while (true) {
|
||||
buf.clear();
|
||||
var r = reader.read(buf);
|
||||
if (r == -1) break;
|
||||
|
||||
readAnything = true;
|
||||
stream.write(buf.array(), 0, r);
|
||||
}
|
||||
return readAnything ? new Object[]{ stream.toByteArray() } : null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a line from the file.
|
||||
*
|
||||
* @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}.
|
||||
* @return The read string.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
|
||||
* @cc.since 1.80pr1.9
|
||||
* @cc.changed 1.81.0 `\r` is now stripped.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||
checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse(false);
|
||||
try {
|
||||
var stream = new ByteArrayOutputStream();
|
||||
|
||||
boolean readAnything = false, readRc = false;
|
||||
while (true) {
|
||||
single.clear();
|
||||
var read = reader.read(single);
|
||||
if (read <= 0) {
|
||||
// Nothing else to read, and we saw no \n. Return the array. If we saw a \r, then add it
|
||||
// back.
|
||||
if (readRc) stream.write('\r');
|
||||
return readAnything ? new Object[]{ stream.toByteArray() } : null;
|
||||
}
|
||||
|
||||
readAnything = true;
|
||||
|
||||
var chr = single.get(0);
|
||||
if (chr == '\n') {
|
||||
if (withTrailing) {
|
||||
if (readRc) stream.write('\r');
|
||||
stream.write(chr);
|
||||
}
|
||||
return new Object[]{ stream.toByteArray() };
|
||||
} else {
|
||||
// We want to skip \r\n, but obviously need to include cases where \r is not followed by \n.
|
||||
// Note, this behaviour is non-standard compliant (strictly speaking we should have no
|
||||
// special logic for \r), but we preserve compatibility with EncodedReadableHandle and
|
||||
// previous behaviour of the io library.
|
||||
if (readRc) stream.write('\r');
|
||||
readRc = chr == '\r';
|
||||
if (!readRc) stream.write(chr);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Seekable extends BinaryReadableHandle {
|
||||
Seekable(SeekableByteChannel seekable, TrackingCloseable closeable) {
|
||||
super(seekable, seekable, closeable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
|
||||
* given by {@code offset}, relative to a start position determined by {@code whence}:
|
||||
* <p>
|
||||
* - {@code "set"}: {@code offset} is relative to the beginning of the file.
|
||||
* - {@code "cur"}: Relative to the current position. This is the default.
|
||||
* - {@code "end"}: Relative to the end of the file.
|
||||
* <p>
|
||||
* In case of success, {@code seek} returns the new file position from the beginning of the file.
|
||||
*
|
||||
* @param whence Where the offset is relative to.
|
||||
* @param offset The offset to seek to.
|
||||
* @return The new position.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn [1] number The new position.
|
||||
* @cc.treturn [2] nil If seeking failed.
|
||||
* @cc.treturn string The reason seeking failed.
|
||||
* @cc.since 1.80pr1.9
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
|
||||
checkOpen();
|
||||
return handleSeek(seekable, whence, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "wb"} or {@code "ab"}
|
||||
* modes.
|
||||
*
|
||||
* @cc.module fs.BinaryWriteHandle
|
||||
*/
|
||||
public class BinaryWritableHandle extends HandleGeneric {
|
||||
private final WritableByteChannel writer;
|
||||
final SeekableByteChannel seekable;
|
||||
private final ByteBuffer single = ByteBuffer.allocate(1);
|
||||
|
||||
protected BinaryWritableHandle(WritableByteChannel writer, SeekableByteChannel seekable, TrackingCloseable closeable) {
|
||||
super(closeable);
|
||||
this.writer = writer;
|
||||
this.seekable = seekable;
|
||||
}
|
||||
|
||||
public static BinaryWritableHandle of(WritableByteChannel channel, TrackingCloseable closeable) {
|
||||
var seekable = asSeekable(channel);
|
||||
return seekable == null ? new BinaryWritableHandle(channel, null, closeable) : new Seekable(seekable, closeable);
|
||||
}
|
||||
|
||||
public static BinaryWritableHandle of(WritableByteChannel channel) {
|
||||
return of(channel, new TrackingCloseable.Impl(channel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a string or byte to the file.
|
||||
*
|
||||
* @param arguments The value to write.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.tparam [1] number The byte to write.
|
||||
* @cc.tparam [2] string The string to write.
|
||||
* @cc.changed 1.80pr1 Now accepts a string to write multiple bytes.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void write(IArguments arguments) throws LuaException {
|
||||
checkOpen();
|
||||
try {
|
||||
var arg = arguments.get(0);
|
||||
if (arg instanceof Number) {
|
||||
var number = ((Number) arg).intValue();
|
||||
single.clear();
|
||||
single.put((byte) number);
|
||||
single.flip();
|
||||
|
||||
writer.write(single);
|
||||
} else if (arg instanceof String) {
|
||||
writer.write(arguments.getBytes(0));
|
||||
} else {
|
||||
throw LuaValues.badArgumentOf(0, "string or number", arg);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current file without closing it.
|
||||
*
|
||||
* @throws LuaException If the file has been closed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void flush() throws LuaException {
|
||||
checkOpen();
|
||||
try {
|
||||
// Technically this is not needed
|
||||
if (writer instanceof FileChannel channel) channel.force(false);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Seekable extends BinaryWritableHandle {
|
||||
public Seekable(SeekableByteChannel seekable, TrackingCloseable closeable) {
|
||||
super(seekable, seekable, closeable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to a new position within the file, changing where bytes are written to. The new position is an offset
|
||||
* given by {@code offset}, relative to a start position determined by {@code whence}:
|
||||
* <p>
|
||||
* - {@code "set"}: {@code offset} is relative to the beginning of the file.
|
||||
* - {@code "cur"}: Relative to the current position. This is the default.
|
||||
* - {@code "end"}: Relative to the end of the file.
|
||||
* <p>
|
||||
* In case of success, {@code seek} returns the new file position from the beginning of the file.
|
||||
*
|
||||
* @param whence Where the offset is relative to.
|
||||
* @param offset The offset to seek to.
|
||||
* @return The new position.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn [1] number The new position.
|
||||
* @cc.treturn [2] nil If seeking failed.
|
||||
* @cc.treturn string The reason seeking failed.
|
||||
* @cc.since 1.80pr1.9
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] seek(Optional<String> whence, Optional<Long> offset) throws LuaException {
|
||||
checkOpen();
|
||||
return handleSeek(seekable, whence, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.NonWritableChannelException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A seekable, readable byte channel which is backed by a {@link ByteBuffer}.
|
||||
*/
|
||||
public class ByteBufferChannel implements SeekableByteChannel {
|
||||
private boolean closed = false;
|
||||
private int position = 0;
|
||||
|
||||
private final ByteBuffer backing;
|
||||
|
||||
public ByteBufferChannel(ByteBuffer backing) {
|
||||
this.backing = backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer destination) throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
Objects.requireNonNull(destination, "destination");
|
||||
|
||||
if (position >= backing.limit()) return -1;
|
||||
|
||||
var remaining = Math.min(backing.limit() - position, destination.remaining());
|
||||
|
||||
// TODO: Switch to Java 17 methods on 1.18.x
|
||||
var slice = backing.slice();
|
||||
slice.position(position);
|
||||
slice.limit(position + remaining);
|
||||
destination.put(slice);
|
||||
position += remaining;
|
||||
return remaining;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(ByteBuffer src) throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
if (newPosition < 0 || newPosition > Integer.MAX_VALUE) {
|
||||
throw new IllegalArgumentException("Position out of bounds");
|
||||
}
|
||||
position = (int) newPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
return backing.limit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) throws ClosedChannelException {
|
||||
if (closed) throw new ClosedChannelException();
|
||||
throw new NonWritableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return !closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A file handle opened with {@link dan200.computercraft.core.apis.FSAPI#open(String, String)} with the {@code "r"}
|
||||
* mode.
|
||||
*
|
||||
* @cc.module fs.ReadHandle
|
||||
*/
|
||||
public class EncodedReadableHandle extends HandleGeneric {
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
|
||||
private final BufferedReader reader;
|
||||
|
||||
public EncodedReadableHandle(@Nonnull BufferedReader reader, @Nonnull TrackingCloseable closable) {
|
||||
super(closable);
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public EncodedReadableHandle(@Nonnull BufferedReader reader) {
|
||||
this(reader, new TrackingCloseable.Impl(reader));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a line from the file.
|
||||
*
|
||||
* @param withTrailingArg Whether to include the newline characters with the returned string. Defaults to {@code false}.
|
||||
* @return The read string.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn string|nil The read line or {@code nil} if at the end of the file.
|
||||
* @cc.changed 1.81.0 Added option to return trailing newline.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readLine(Optional<Boolean> withTrailingArg) throws LuaException {
|
||||
checkOpen();
|
||||
boolean withTrailing = withTrailingArg.orElse(false);
|
||||
try {
|
||||
var line = reader.readLine();
|
||||
if (line != null) {
|
||||
// While this is technically inaccurate, it's better than nothing
|
||||
if (withTrailing) line += "\n";
|
||||
return new Object[]{ line };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the remainder of the file.
|
||||
*
|
||||
* @return The file, or {@code null} if at the end of it.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn nil|string The remaining contents of the file, or {@code nil} if we are at the end.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] readAll() throws LuaException {
|
||||
checkOpen();
|
||||
try {
|
||||
var result = new StringBuilder();
|
||||
var line = reader.readLine();
|
||||
while (line != null) {
|
||||
result.append(line);
|
||||
line = reader.readLine();
|
||||
if (line != null) {
|
||||
result.append("\n");
|
||||
}
|
||||
}
|
||||
return new Object[]{ result.toString() };
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a number of characters from this file.
|
||||
*
|
||||
* @param countA The number of characters to read, defaulting to 1.
|
||||
* @return The read characters.
|
||||
* @throws LuaException When trying to read a negative number of characters.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.treturn string|nil The read characters, or {@code nil} if at the of the file.
|
||||
* @cc.since 1.80pr1.4
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] read(Optional<Integer> countA) throws LuaException {
|
||||
checkOpen();
|
||||
try {
|
||||
int count = countA.orElse(1);
|
||||
if (count < 0) {
|
||||
// Whilst this may seem absurd to allow reading 0 characters, PUC Lua it so
|
||||
// it seems best to remain somewhat consistent.
|
||||
throw new LuaException("Cannot read a negative number of characters");
|
||||
} else if (count <= BUFFER_SIZE) {
|
||||
// If we've got a small count, then allocate that and read it.
|
||||
var chars = new char[count];
|
||||
var read = reader.read(chars);
|
||||
|
||||
return read < 0 ? null : new Object[]{ new String(chars, 0, read) };
|
||||
} else {
|
||||
// If we've got a large count, read in bunches of 8192.
|
||||
var buffer = new char[BUFFER_SIZE];
|
||||
|
||||
// Read the initial set of characters, failing if none are read.
|
||||
var read = reader.read(buffer, 0, Math.min(buffer.length, count));
|
||||
if (read < 0) return null;
|
||||
|
||||
var out = new StringBuilder(read);
|
||||
var totalRead = read;
|
||||
out.append(buffer, 0, read);
|
||||
|
||||
// Otherwise read until we either reach the limit or we no longer consume
|
||||
// the full buffer.
|
||||
while (read >= BUFFER_SIZE && totalRead < count) {
|
||||
read = reader.read(buffer, 0, Math.min(BUFFER_SIZE, count - totalRead));
|
||||
if (read < 0) break;
|
||||
|
||||
totalRead += read;
|
||||
out.append(buffer, 0, read);
|
||||
}
|
||||
|
||||
return new Object[]{ out.toString() };
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static BufferedReader openUtf8(ReadableByteChannel channel) {
|
||||
return open(channel, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static BufferedReader open(ReadableByteChannel channel, Charset charset) {
|
||||
// Create a charset decoder with the same properties as StreamDecoder does for
|
||||
// InputStreams: namely, replace everything instead of erroring.
|
||||
var decoder = charset.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
return new BufferedReader(Channels.newReader(channel, decoder, -1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* A file handle opened by {@link dan200.computercraft.core.apis.FSAPI#open} using the {@code "w"} or {@code "a"} modes.
|
||||
*
|
||||
* @cc.module fs.WriteHandle
|
||||
*/
|
||||
public class EncodedWritableHandle extends HandleGeneric {
|
||||
private final BufferedWriter writer;
|
||||
|
||||
public EncodedWritableHandle(@Nonnull BufferedWriter writer, @Nonnull TrackingCloseable closable) {
|
||||
super(closable);
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a string of characters to the file.
|
||||
*
|
||||
* @param args The value to write.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.param value The value to write to the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void write(IArguments args) throws LuaException {
|
||||
checkOpen();
|
||||
var text = StringUtil.toString(args.get(0));
|
||||
try {
|
||||
writer.write(text, 0, text.length());
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a string of characters to the file, follwing them with a new line character.
|
||||
*
|
||||
* @param args The value to write.
|
||||
* @throws LuaException If the file has been closed.
|
||||
* @cc.param value The value to write to the file.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void writeLine(IArguments args) throws LuaException {
|
||||
checkOpen();
|
||||
var text = StringUtil.toString(args.get(0));
|
||||
try {
|
||||
writer.write(text, 0, text.length());
|
||||
writer.newLine();
|
||||
} catch (IOException e) {
|
||||
throw new LuaException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current file without closing it.
|
||||
*
|
||||
* @throws LuaException If the file has been closed.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void flush() throws LuaException {
|
||||
checkOpen();
|
||||
try {
|
||||
writer.flush();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static BufferedWriter openUtf8(WritableByteChannel channel) {
|
||||
return open(channel, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static BufferedWriter open(WritableByteChannel channel, Charset charset) {
|
||||
// Create a charset encoder with the same properties as StreamEncoder does for
|
||||
// OutputStreams: namely, replace everything instead of erroring.
|
||||
var encoder = charset.newEncoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
return new BufferedWriter(Channels.newWriter(channel, encoder, -1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.handles;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.filesystem.TrackingCloseable;
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class HandleGeneric {
|
||||
private TrackingCloseable closeable;
|
||||
|
||||
protected HandleGeneric(@Nonnull TrackingCloseable closeable) {
|
||||
this.closeable = closeable;
|
||||
}
|
||||
|
||||
protected void checkOpen() throws LuaException {
|
||||
var closeable = this.closeable;
|
||||
if (closeable == null || !closeable.isOpen()) throw new LuaException("attempt to use a closed file");
|
||||
}
|
||||
|
||||
protected final void close() {
|
||||
IoUtil.closeQuietly(closeable);
|
||||
closeable = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this file, freeing any resources it uses.
|
||||
* <p>
|
||||
* Once a file is closed it may no longer be read or written to.
|
||||
*
|
||||
* @throws LuaException If the file has already been closed.
|
||||
*/
|
||||
@LuaFunction("close")
|
||||
public final void doClose() throws LuaException {
|
||||
checkOpen();
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shared implementation for various file handle types.
|
||||
*
|
||||
* @param channel The channel to seek in
|
||||
* @param whence The seeking mode.
|
||||
* @param offset The offset to seek to.
|
||||
* @return The new position of the file, or null if some error occurred.
|
||||
* @throws LuaException If the arguments were invalid
|
||||
* @see <a href="https://www.lua.org/manual/5.1/manual.html#pdf-file:seek">{@code file:seek} in the Lua manual.</a>
|
||||
*/
|
||||
protected static Object[] handleSeek(SeekableByteChannel channel, Optional<String> whence, Optional<Long> offset) throws LuaException {
|
||||
long actualOffset = offset.orElse(0L);
|
||||
try {
|
||||
switch (whence.orElse("cur")) {
|
||||
case "set" -> channel.position(actualOffset);
|
||||
case "cur" -> channel.position(channel.position() + actualOffset);
|
||||
case "end" -> channel.position(channel.size() + actualOffset);
|
||||
default -> throw new LuaException("bad argument #1 to 'seek' (invalid option '" + whence + "'");
|
||||
}
|
||||
|
||||
return new Object[]{ channel.position() };
|
||||
} catch (IllegalArgumentException e) {
|
||||
return new Object[]{ null, "Position is negative" };
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected static SeekableByteChannel asSeekable(Channel channel) {
|
||||
if (!(channel instanceof SeekableByteChannel seekable)) return null;
|
||||
|
||||
try {
|
||||
seekable.position(seekable.position());
|
||||
return seekable;
|
||||
} catch (IOException | UnsupportedOperationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Checks a URL using {@link NetworkUtils#getAddress(String, int, boolean)}}
|
||||
* <p>
|
||||
* This requires a DNS lookup, and so needs to occur off-thread.
|
||||
*/
|
||||
public class CheckUrl extends Resource<CheckUrl> {
|
||||
private static final String EVENT = "http_check";
|
||||
|
||||
private Future<?> future;
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final String address;
|
||||
private final URI uri;
|
||||
|
||||
public CheckUrl(ResourceGroup<CheckUrl> limiter, IAPIEnvironment environment, String address, URI uri) {
|
||||
super(limiter);
|
||||
this.environment = environment;
|
||||
this.address = address;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
if (isClosed()) return;
|
||||
future = NetworkUtils.EXECUTOR.submit(this::doRun);
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
private void doRun() {
|
||||
if (isClosed()) return;
|
||||
|
||||
try {
|
||||
var ssl = uri.getScheme().equalsIgnoreCase("https");
|
||||
var netAddress = NetworkUtils.getAddress(uri, ssl);
|
||||
NetworkUtils.getOptions(uri.getHost(), netAddress);
|
||||
|
||||
if (tryClose()) environment.queueEvent(EVENT, address, true);
|
||||
} catch (HTTPRequestException e) {
|
||||
if (tryClose()) environment.queueEvent(EVENT, address, false, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
super.dispose();
|
||||
future = closeFuture(future);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
public class HTTPRequestException extends Exception {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 7591208619422744652L;
|
||||
|
||||
public HTTPRequestException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable fillInStackTrace() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.apis.http.options.Action;
|
||||
import dan200.computercraft.core.apis.http.options.AddressRule;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.util.ThreadUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ConnectTimeoutException;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.TooLongFrameException;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.timeout.ReadTimeoutException;
|
||||
import io.netty.handler.traffic.AbstractTrafficShapingHandler;
|
||||
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
import java.security.KeyStore;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Just a shared object for executing simple HTTP related tasks.
|
||||
*/
|
||||
public final class NetworkUtils {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NetworkUtils.class);
|
||||
|
||||
public static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(
|
||||
4,
|
||||
ThreadUtils.builder("Network")
|
||||
.setPriority(Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2)
|
||||
.build()
|
||||
);
|
||||
|
||||
public static final EventLoopGroup LOOP_GROUP = new NioEventLoopGroup(4, ThreadUtils.builder("Netty")
|
||||
.setPriority(Thread.MIN_PRIORITY + (Thread.NORM_PRIORITY - Thread.MIN_PRIORITY) / 2)
|
||||
.build()
|
||||
);
|
||||
|
||||
public static final AbstractTrafficShapingHandler SHAPING_HANDLER = new GlobalTrafficShapingHandler(
|
||||
EXECUTOR, CoreConfig.httpUploadBandwidth, CoreConfig.httpDownloadBandwidth
|
||||
);
|
||||
|
||||
static {
|
||||
EXECUTOR.setKeepAliveTime(60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private NetworkUtils() {
|
||||
}
|
||||
|
||||
private static final Object sslLock = new Object();
|
||||
private static TrustManagerFactory trustManager;
|
||||
private static SslContext sslContext;
|
||||
private static boolean triedSslContext = false;
|
||||
|
||||
private static TrustManagerFactory getTrustManager() {
|
||||
if (trustManager != null) return trustManager;
|
||||
synchronized (sslLock) {
|
||||
if (trustManager != null) return trustManager;
|
||||
|
||||
TrustManagerFactory tmf = null;
|
||||
try {
|
||||
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Cannot setup trust manager", e);
|
||||
}
|
||||
|
||||
return trustManager = tmf;
|
||||
}
|
||||
}
|
||||
|
||||
public static SslContext getSslContext() throws HTTPRequestException {
|
||||
if (sslContext != null || triedSslContext) return sslContext;
|
||||
synchronized (sslLock) {
|
||||
if (sslContext != null || triedSslContext) return sslContext;
|
||||
try {
|
||||
return sslContext = SslContextBuilder
|
||||
.forClient()
|
||||
.trustManager(getTrustManager())
|
||||
.build();
|
||||
} catch (SSLException e) {
|
||||
LOG.error("Cannot construct SSL context", e);
|
||||
triedSslContext = true;
|
||||
sslContext = null;
|
||||
|
||||
throw new HTTPRequestException("Cannot create a secure connection");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void reloadConfig() {
|
||||
SHAPING_HANDLER.configure(CoreConfig.httpUploadBandwidth, CoreConfig.httpDownloadBandwidth);
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
SHAPING_HANDLER.trafficCounter().resetCumulativeTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link InetSocketAddress} from a {@link java.net.URI}.
|
||||
* <p>
|
||||
* Note, this may require a DNS lookup, and so should not be executed on the main CC thread.
|
||||
*
|
||||
* @param uri The URI to fetch.
|
||||
* @param ssl Whether to connect with SSL. This is used to find the default port if not otherwise specified.
|
||||
* @return The resolved address.
|
||||
* @throws HTTPRequestException If the host is not malformed.
|
||||
*/
|
||||
public static InetSocketAddress getAddress(URI uri, boolean ssl) throws HTTPRequestException {
|
||||
return getAddress(uri.getHost(), uri.getPort(), ssl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link InetSocketAddress} from the resolved {@code host} and port.
|
||||
* <p>
|
||||
* Note, this may require a DNS lookup, and so should not be executed on the main CC thread.
|
||||
*
|
||||
* @param host The host to resolve.
|
||||
* @param port The port, or -1 if not defined.
|
||||
* @param ssl Whether to connect with SSL. This is used to find the default port if not otherwise specified.
|
||||
* @return The resolved address.
|
||||
* @throws HTTPRequestException If the host is not malformed.
|
||||
*/
|
||||
public static InetSocketAddress getAddress(String host, int port, boolean ssl) throws HTTPRequestException {
|
||||
if (port < 0) port = ssl ? 443 : 80;
|
||||
var socketAddress = new InetSocketAddress(host, port);
|
||||
if (socketAddress.isUnresolved()) throw new HTTPRequestException("Unknown host");
|
||||
return socketAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options for a specific domain.
|
||||
*
|
||||
* @param host The host to resolve.
|
||||
* @param address The address, resolved by {@link #getAddress(String, int, boolean)}.
|
||||
* @return The options for this host.
|
||||
* @throws HTTPRequestException If the host is not permitted
|
||||
*/
|
||||
public static Options getOptions(String host, InetSocketAddress address) throws HTTPRequestException {
|
||||
var options = AddressRule.apply(CoreConfig.httpRules, host, address);
|
||||
if (options.action == Action.DENY) throw new HTTPRequestException("Domain not permitted");
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a {@link ByteBuf} into a byte array.
|
||||
*
|
||||
* @param buffer The buffer to read.
|
||||
* @return The resulting bytes.
|
||||
*/
|
||||
public static byte[] toBytes(ByteBuf buffer) {
|
||||
var bytes = new byte[buffer.readableBytes()];
|
||||
buffer.readBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String toFriendlyError(@Nonnull Throwable cause) {
|
||||
if (cause instanceof WebSocketHandshakeException || cause instanceof HTTPRequestException) {
|
||||
return cause.getMessage();
|
||||
} else if (cause instanceof TooLongFrameException) {
|
||||
return "Message is too large";
|
||||
} else if (cause instanceof ReadTimeoutException || cause instanceof ConnectTimeoutException) {
|
||||
return "Timed out";
|
||||
} else if (cause instanceof SSLHandshakeException || (cause instanceof DecoderException && cause.getCause() instanceof SSLHandshakeException)) {
|
||||
return "Could not create a secure connection";
|
||||
} else {
|
||||
return "Could not connect";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A holder for one or more resources, with a lifetime.
|
||||
*
|
||||
* @param <T> The type of this resource. Should be the class extending from {@link Resource}.
|
||||
*/
|
||||
public abstract class Resource<T extends Resource<T>> implements Closeable {
|
||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||
private final ResourceGroup<T> limiter;
|
||||
|
||||
protected Resource(ResourceGroup<T> limiter) {
|
||||
this.limiter = limiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this resource is closed.
|
||||
*
|
||||
* @return Whether this resource is closed.
|
||||
*/
|
||||
public final boolean isClosed() {
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this has been cancelled. If so, it'll clean up any existing resources and cancel any pending futures.
|
||||
*
|
||||
* @return Whether this resource has been closed.
|
||||
*/
|
||||
public final boolean checkClosed() {
|
||||
if (!closed.get()) return false;
|
||||
dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to close the current resource.
|
||||
*
|
||||
* @return Whether this was successfully closed, or {@code false} if it has already been closed.
|
||||
*/
|
||||
protected final boolean tryClose() {
|
||||
if (closed.getAndSet(true)) return false;
|
||||
dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up any pending resources
|
||||
* <p>
|
||||
* Note, this may be called multiple times, and so should be thread-safe and
|
||||
* avoid any major side effects.
|
||||
*/
|
||||
protected void dispose() {
|
||||
@SuppressWarnings("unchecked")
|
||||
var thisT = (T) this;
|
||||
limiter.release(thisT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link WeakReference} which will close {@code this} when collected.
|
||||
*
|
||||
* @param <R> The object we are wrapping in a reference.
|
||||
* @param object The object to reference to
|
||||
* @return The weak reference.
|
||||
*/
|
||||
protected <R> WeakReference<R> createOwnerReference(R object) {
|
||||
return new CloseReference<>(this, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void close() {
|
||||
tryClose();
|
||||
}
|
||||
|
||||
public final boolean queue(Consumer<T> task) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var thisT = (T) this;
|
||||
return limiter.queue(thisT, () -> task.accept(thisT));
|
||||
}
|
||||
|
||||
protected static <T extends Closeable> T closeCloseable(T closeable) {
|
||||
IoUtil.closeQuietly(closeable);
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static ChannelFuture closeChannel(ChannelFuture future) {
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
var channel = future.channel();
|
||||
if (channel != null && channel.isOpen()) channel.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static <T extends Future<?>> T closeFuture(T future) {
|
||||
if (future != null) future.cancel(true);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue<>();
|
||||
|
||||
private static class CloseReference<T> extends WeakReference<T> {
|
||||
final Resource<?> resource;
|
||||
|
||||
CloseReference(Resource<?> resource, T referent) {
|
||||
super(referent, QUEUE);
|
||||
this.resource = resource;
|
||||
}
|
||||
}
|
||||
|
||||
public static void cleanup() {
|
||||
Reference<?> reference;
|
||||
while ((reference = QUEUE.poll()) != null) ((CloseReference<?>) reference).resource.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A collection of {@link Resource}s, with an upper bound on capacity.
|
||||
*
|
||||
* @param <T> The type of the resource this group manages.
|
||||
*/
|
||||
public class ResourceGroup<T extends Resource<T>> {
|
||||
public static final int DEFAULT_LIMIT = 512;
|
||||
public static final IntSupplier DEFAULT = () -> DEFAULT_LIMIT;
|
||||
|
||||
private static final IntSupplier ZERO = () -> 0;
|
||||
|
||||
final IntSupplier limit;
|
||||
|
||||
boolean active = false;
|
||||
|
||||
final Set<T> resources = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
public ResourceGroup(IntSupplier limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public ResourceGroup() {
|
||||
limit = ZERO;
|
||||
}
|
||||
|
||||
public void startup() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
public synchronized void shutdown() {
|
||||
active = false;
|
||||
|
||||
for (var resource : resources) resource.close();
|
||||
resources.clear();
|
||||
|
||||
Resource.cleanup();
|
||||
}
|
||||
|
||||
|
||||
public final boolean queue(T resource, Runnable setup) {
|
||||
return queue(() -> {
|
||||
setup.run();
|
||||
return resource;
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized boolean queue(Supplier<T> resource) {
|
||||
Resource.cleanup();
|
||||
if (!active) return false;
|
||||
|
||||
var limit = this.limit.getAsInt();
|
||||
if (limit <= 0 || resources.size() < limit) {
|
||||
resources.add(resource.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public synchronized void release(T resource) {
|
||||
resources.remove(resource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A {@link ResourceGroup} which will queue items when the group at capacity.
|
||||
*
|
||||
* @param <T> The type of the resource this queue manages.
|
||||
*/
|
||||
public class ResourceQueue<T extends Resource<T>> extends ResourceGroup<T> {
|
||||
private final ArrayDeque<Supplier<T>> pending = new ArrayDeque<>();
|
||||
|
||||
public ResourceQueue(IntSupplier limit) {
|
||||
super(limit);
|
||||
}
|
||||
|
||||
public ResourceQueue() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void shutdown() {
|
||||
super.shutdown();
|
||||
pending.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean queue(Supplier<T> resource) {
|
||||
if (!active) return false;
|
||||
if (super.queue(resource)) return true;
|
||||
if (pending.size() > DEFAULT_LIMIT) return false;
|
||||
|
||||
pending.add(resource);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void release(T resource) {
|
||||
super.release(resource);
|
||||
|
||||
if (!active) return;
|
||||
|
||||
var limit = this.limit.getAsInt();
|
||||
if (limit <= 0 || resources.size() < limit) {
|
||||
var next = pending.poll();
|
||||
if (next != null) resources.add(next.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
public enum Action {
|
||||
ALLOW,
|
||||
DENY;
|
||||
|
||||
private final PartialOptions partial = new PartialOptions(
|
||||
this, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), OptionalInt.empty()
|
||||
);
|
||||
|
||||
@Nonnull
|
||||
public PartialOptions toPartial() {
|
||||
return partial;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import com.google.common.net.InetAddresses;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A predicate on an address. Matches against a domain and an ip address.
|
||||
*
|
||||
* @see AddressRule#apply(Iterable, String, InetSocketAddress) for the actual handling of this rule.
|
||||
*/
|
||||
interface AddressPredicate {
|
||||
Logger LOG = LoggerFactory.getLogger(AddressPredicate.class);
|
||||
|
||||
default boolean matches(String domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean matches(InetAddress socketAddress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final class HostRange implements AddressPredicate {
|
||||
private final byte[] min;
|
||||
private final byte[] max;
|
||||
|
||||
HostRange(byte[] min, byte[] max) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(InetAddress address) {
|
||||
var entry = address.getAddress();
|
||||
if (entry.length != min.length) return false;
|
||||
|
||||
for (var i = 0; i < entry.length; i++) {
|
||||
var value = 0xFF & entry[i];
|
||||
if (value < (0xFF & min[i]) || value > (0xFF & max[i])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static HostRange parse(String addressStr, String prefixSizeStr) {
|
||||
int prefixSize;
|
||||
try {
|
||||
prefixSize = Integer.parseInt(prefixSizeStr);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.error(
|
||||
"Malformed http whitelist/blacklist entry '{}': Cannot extract size of CIDR mask from '{}'.",
|
||||
addressStr + '/' + prefixSizeStr, prefixSizeStr
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
InetAddress address;
|
||||
try {
|
||||
address = InetAddresses.forString(addressStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.error(
|
||||
"Malformed http whitelist/blacklist entry '{}': Cannot extract IP address from '{}'.",
|
||||
addressStr + '/' + prefixSizeStr, prefixSizeStr
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mask the bytes of the IP address.
|
||||
byte[] minBytes = address.getAddress(), maxBytes = address.getAddress();
|
||||
var size = prefixSize;
|
||||
for (var i = 0; i < minBytes.length; i++) {
|
||||
if (size <= 0) {
|
||||
minBytes[i] &= 0;
|
||||
maxBytes[i] |= 0xFF;
|
||||
} else if (size < 8) {
|
||||
minBytes[i] &= 0xFF << (8 - size);
|
||||
maxBytes[i] |= ~(0xFF << (8 - size));
|
||||
}
|
||||
|
||||
size -= 8;
|
||||
}
|
||||
|
||||
return new HostRange(minBytes, maxBytes);
|
||||
}
|
||||
}
|
||||
|
||||
final class DomainPattern implements AddressPredicate {
|
||||
private final Pattern pattern;
|
||||
|
||||
DomainPattern(Pattern pattern) {
|
||||
this.pattern = pattern;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String domain) {
|
||||
return pattern.matcher(domain).matches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(InetAddress socketAddress) {
|
||||
return pattern.matcher(socketAddress.getHostAddress()).matches();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class PrivatePattern implements AddressPredicate {
|
||||
static final PrivatePattern INSTANCE = new PrivatePattern();
|
||||
|
||||
@Override
|
||||
public boolean matches(InetAddress socketAddress) {
|
||||
return socketAddress.isAnyLocalAddress()
|
||||
|| socketAddress.isLoopbackAddress()
|
||||
|| socketAddress.isLinkLocalAddress()
|
||||
|| socketAddress.isSiteLocalAddress();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import com.google.common.net.InetAddresses;
|
||||
import dan200.computercraft.core.apis.http.options.AddressPredicate.DomainPattern;
|
||||
import dan200.computercraft.core.apis.http.options.AddressPredicate.HostRange;
|
||||
import dan200.computercraft.core.apis.http.options.AddressPredicate.PrivatePattern;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A pattern which matches an address, and controls whether it is accessible or not.
|
||||
*/
|
||||
public final class AddressRule {
|
||||
public static final long MAX_DOWNLOAD = 16 * 1024 * 1024;
|
||||
public static final long MAX_UPLOAD = 4 * 1024 * 1024;
|
||||
public static final int TIMEOUT = 30_000;
|
||||
public static final int WEBSOCKET_MESSAGE = 128 * 1024;
|
||||
|
||||
private final AddressPredicate predicate;
|
||||
private final OptionalInt port;
|
||||
private final PartialOptions partial;
|
||||
|
||||
private AddressRule(@Nonnull AddressPredicate predicate, OptionalInt port, @Nonnull PartialOptions partial) {
|
||||
this.predicate = predicate;
|
||||
this.partial = partial;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AddressRule parse(String filter, OptionalInt port, @Nonnull PartialOptions partial) {
|
||||
var cidr = filter.indexOf('/');
|
||||
if (cidr >= 0) {
|
||||
var addressStr = filter.substring(0, cidr);
|
||||
var prefixSizeStr = filter.substring(cidr + 1);
|
||||
var range = HostRange.parse(addressStr, prefixSizeStr);
|
||||
return range == null ? null : new AddressRule(range, port, partial);
|
||||
} else if (filter.equalsIgnoreCase("$private")) {
|
||||
return new AddressRule(PrivatePattern.INSTANCE, port, partial);
|
||||
} else {
|
||||
var pattern = Pattern.compile("^\\Q" + filter.replaceAll("\\*", "\\\\E.*\\\\Q") + "\\E$", Pattern.CASE_INSENSITIVE);
|
||||
return new AddressRule(new DomainPattern(pattern), port, partial);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given address matches a series of patterns.
|
||||
*
|
||||
* @param domain The domain to match
|
||||
* @param port The port of the address.
|
||||
* @param address The address to check.
|
||||
* @param ipv4Address An ipv4 version of the address, if the original was an ipv6 address.
|
||||
* @return Whether it matches any of these patterns.
|
||||
*/
|
||||
private boolean matches(String domain, int port, InetAddress address, Inet4Address ipv4Address) {
|
||||
if (this.port.isPresent() && this.port.getAsInt() != port) return false;
|
||||
return predicate.matches(domain)
|
||||
|| predicate.matches(address)
|
||||
|| (ipv4Address != null && predicate.matches(ipv4Address));
|
||||
}
|
||||
|
||||
public static Options apply(Iterable<? extends AddressRule> rules, String domain, InetSocketAddress socketAddress) {
|
||||
var options = PartialOptions.DEFAULT;
|
||||
|
||||
var port = socketAddress.getPort();
|
||||
var address = socketAddress.getAddress();
|
||||
var ipv4Address = address instanceof Inet6Address inet6 && InetAddresses.is6to4Address(inet6)
|
||||
? InetAddresses.get6to4IPv4Address(inet6) : null;
|
||||
|
||||
for (AddressRule rule : rules) {
|
||||
if (!rule.matches(domain, port, address, ipv4Address)) continue;
|
||||
options = options.merge(rule.partial);
|
||||
}
|
||||
|
||||
return options.toOptions();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Options about a specific domain.
|
||||
*/
|
||||
public final class Options {
|
||||
@Nonnull
|
||||
public final Action action;
|
||||
public final long maxUpload;
|
||||
public final long maxDownload;
|
||||
public final int timeout;
|
||||
public final int websocketMessage;
|
||||
|
||||
Options(@Nonnull Action action, long maxUpload, long maxDownload, int timeout, int websocketMessage) {
|
||||
this.action = action;
|
||||
this.maxUpload = maxUpload;
|
||||
this.maxDownload = maxDownload;
|
||||
this.timeout = timeout;
|
||||
this.websocketMessage = websocketMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.options;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
public final class PartialOptions {
|
||||
public static final PartialOptions DEFAULT = new PartialOptions(
|
||||
null, OptionalLong.empty(), OptionalLong.empty(), OptionalInt.empty(), OptionalInt.empty()
|
||||
);
|
||||
|
||||
private final @Nullable Action action;
|
||||
private final OptionalLong maxUpload;
|
||||
private final OptionalLong maxDownload;
|
||||
private final OptionalInt timeout;
|
||||
private final OptionalInt websocketMessage;
|
||||
|
||||
private @Nullable Options options;
|
||||
|
||||
public PartialOptions(@Nullable Action action, OptionalLong maxUpload, OptionalLong maxDownload, OptionalInt timeout, OptionalInt websocketMessage) {
|
||||
this.action = action;
|
||||
this.maxUpload = maxUpload;
|
||||
this.maxDownload = maxDownload;
|
||||
this.timeout = timeout;
|
||||
this.websocketMessage = websocketMessage;
|
||||
}
|
||||
|
||||
Options toOptions() {
|
||||
if (options != null) return options;
|
||||
|
||||
return options = new Options(
|
||||
action == null ? Action.DENY : action,
|
||||
maxUpload.orElse(AddressRule.MAX_UPLOAD),
|
||||
maxDownload.orElse(AddressRule.MAX_DOWNLOAD),
|
||||
timeout.orElse(AddressRule.TIMEOUT),
|
||||
websocketMessage.orElse(AddressRule.WEBSOCKET_MESSAGE)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a left-biased union of two {@link PartialOptions}.
|
||||
*
|
||||
* @param other The other partial options to combine with.
|
||||
* @return The merged options map.
|
||||
*/
|
||||
PartialOptions merge(PartialOptions other) {
|
||||
// Short circuit for DEFAULT. This has no effect on the outcome, but avoids an allocation.
|
||||
if (this == DEFAULT) return other;
|
||||
|
||||
return new PartialOptions(
|
||||
action == null && other.action != null ? other.action : action,
|
||||
maxUpload.isPresent() ? maxUpload : other.maxUpload,
|
||||
maxDownload.isPresent() ? maxDownload : other.maxDownload,
|
||||
timeout.isPresent() ? timeout : other.timeout,
|
||||
websocketMessage.isPresent() ? websocketMessage : other.websocketMessage
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Represents an in-progress HTTP request.
|
||||
*/
|
||||
public class HttpRequest extends Resource<HttpRequest> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpRequest.class);
|
||||
private static final String SUCCESS_EVENT = "http_success";
|
||||
private static final String FAILURE_EVENT = "http_failure";
|
||||
|
||||
private static final int MAX_REDIRECTS = 16;
|
||||
|
||||
private Future<?> executorFuture;
|
||||
private ChannelFuture connectFuture;
|
||||
private HttpRequestHandler currentRequest;
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
|
||||
private final String address;
|
||||
private final ByteBuf postBuffer;
|
||||
private final HttpHeaders headers;
|
||||
private final boolean binary;
|
||||
|
||||
final AtomicInteger redirects;
|
||||
|
||||
public HttpRequest(ResourceGroup<HttpRequest> limiter, IAPIEnvironment environment, String address, String postText, HttpHeaders headers, boolean binary, boolean followRedirects) {
|
||||
super(limiter);
|
||||
this.environment = environment;
|
||||
this.address = address;
|
||||
postBuffer = postText != null
|
||||
? Unpooled.wrappedBuffer(postText.getBytes(StandardCharsets.UTF_8))
|
||||
: Unpooled.buffer(0);
|
||||
this.headers = headers;
|
||||
this.binary = binary;
|
||||
redirects = new AtomicInteger(followRedirects ? MAX_REDIRECTS : 0);
|
||||
|
||||
if (postText != null) {
|
||||
if (!headers.contains(HttpHeaderNames.CONTENT_TYPE)) {
|
||||
headers.set(HttpHeaderNames.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=utf-8");
|
||||
}
|
||||
|
||||
if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH)) {
|
||||
headers.set(HttpHeaderNames.CONTENT_LENGTH, postBuffer.readableBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IAPIEnvironment environment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
public static URI checkUri(String address) throws HTTPRequestException {
|
||||
URI url;
|
||||
try {
|
||||
url = new URI(address);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new HTTPRequestException("URL malformed");
|
||||
}
|
||||
|
||||
checkUri(url);
|
||||
return url;
|
||||
}
|
||||
|
||||
public static void checkUri(URI url) throws HTTPRequestException {
|
||||
// Validate the URL
|
||||
if (url.getScheme() == null) throw new HTTPRequestException("Must specify http or https");
|
||||
if (url.getHost() == null) throw new HTTPRequestException("URL malformed");
|
||||
|
||||
var scheme = url.getScheme().toLowerCase(Locale.ROOT);
|
||||
if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
|
||||
throw new HTTPRequestException("Invalid protocol '" + scheme + "'");
|
||||
}
|
||||
}
|
||||
|
||||
public void request(URI uri, HttpMethod method) {
|
||||
if (isClosed()) return;
|
||||
executorFuture = NetworkUtils.EXECUTOR.submit(() -> doRequest(uri, method));
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
private void doRequest(URI uri, HttpMethod method) {
|
||||
// If we're cancelled, abort.
|
||||
if (isClosed()) return;
|
||||
|
||||
try {
|
||||
var ssl = uri.getScheme().equalsIgnoreCase("https");
|
||||
var socketAddress = NetworkUtils.getAddress(uri, ssl);
|
||||
var options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
|
||||
var sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
||||
|
||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||
if (isClosed()) return;
|
||||
|
||||
var requestBody = getHeaderSize(headers) + postBuffer.capacity();
|
||||
if (options.maxUpload != 0 && requestBody > options.maxUpload) {
|
||||
failure("Request body is too large");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add request size to the tracker before opening the connection
|
||||
environment.observe(Metrics.HTTP_REQUESTS);
|
||||
environment.observe(Metrics.HTTP_UPLOAD, requestBody);
|
||||
|
||||
var handler = currentRequest = new HttpRequestHandler(this, uri, method, options);
|
||||
connectFuture = new Bootstrap()
|
||||
.group(NetworkUtils.LOOP_GROUP)
|
||||
.channelFactory(NioSocketChannel::new)
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
|
||||
if (options.timeout > 0) {
|
||||
ch.config().setConnectTimeoutMillis(options.timeout);
|
||||
}
|
||||
|
||||
var p = ch.pipeline();
|
||||
p.addLast(NetworkUtils.SHAPING_HANDLER);
|
||||
if (sslContext != null) {
|
||||
p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort()));
|
||||
}
|
||||
|
||||
if (options.timeout > 0) {
|
||||
p.addLast(new ReadTimeoutHandler(options.timeout, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
p.addLast(
|
||||
new HttpClientCodec(),
|
||||
new HttpContentDecompressor(),
|
||||
handler
|
||||
);
|
||||
}
|
||||
})
|
||||
.remoteAddress(socketAddress)
|
||||
.connect()
|
||||
.addListener(c -> {
|
||||
if (!c.isSuccess()) failure(NetworkUtils.toFriendlyError(c.cause()));
|
||||
});
|
||||
|
||||
// Do an additional check for cancellation
|
||||
checkClosed();
|
||||
} catch (HTTPRequestException e) {
|
||||
failure(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
failure(NetworkUtils.toFriendlyError(e));
|
||||
LOG.error(Logging.HTTP_ERROR, "Error in HTTP request", e);
|
||||
}
|
||||
}
|
||||
|
||||
void failure(String message) {
|
||||
if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message);
|
||||
}
|
||||
|
||||
void failure(String message, HttpResponseHandle object) {
|
||||
if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message, object);
|
||||
}
|
||||
|
||||
void success(HttpResponseHandle object) {
|
||||
if (tryClose()) environment.queueEvent(SUCCESS_EVENT, address, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
super.dispose();
|
||||
|
||||
executorFuture = closeFuture(executorFuture);
|
||||
connectFuture = closeChannel(connectFuture);
|
||||
currentRequest = closeCloseable(currentRequest);
|
||||
}
|
||||
|
||||
public static long getHeaderSize(HttpHeaders headers) {
|
||||
long size = 0;
|
||||
for (var header : headers) {
|
||||
size += header.getKey() == null ? 0 : header.getKey().length();
|
||||
size += header.getValue() == null ? 0 : header.getValue().length() + 1;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
public ByteBuf body() {
|
||||
return postBuffer;
|
||||
}
|
||||
|
||||
public HttpHeaders headers() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public boolean isBinary() {
|
||||
return binary;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import io.netty.buffer.CompositeByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.http.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static dan200.computercraft.core.apis.http.request.HttpRequest.getHeaderSize;
|
||||
|
||||
public final class HttpRequestHandler extends SimpleChannelInboundHandler<HttpObject> implements Closeable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpRequestHandler.class);
|
||||
|
||||
/**
|
||||
* Same as {@link io.netty.handler.codec.MessageAggregator}.
|
||||
*/
|
||||
private static final int DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS = 1024;
|
||||
|
||||
private static final byte[] EMPTY_BYTES = new byte[0];
|
||||
|
||||
private final HttpRequest request;
|
||||
private boolean closed = false;
|
||||
|
||||
private final URI uri;
|
||||
private final HttpMethod method;
|
||||
private final Options options;
|
||||
|
||||
private Charset responseCharset;
|
||||
private final HttpHeaders responseHeaders = new DefaultHttpHeaders();
|
||||
private HttpResponseStatus responseStatus;
|
||||
private CompositeByteBuf responseBody;
|
||||
|
||||
HttpRequestHandler(HttpRequest request, URI uri, HttpMethod method, Options options) {
|
||||
this.request = request;
|
||||
|
||||
this.uri = uri;
|
||||
this.method = method;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
if (request.checkClosed()) return;
|
||||
|
||||
var body = request.body();
|
||||
body.resetReaderIndex().retain();
|
||||
|
||||
var requestUri = uri.getRawPath();
|
||||
if (uri.getRawQuery() != null) requestUri += "?" + uri.getRawQuery();
|
||||
|
||||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, requestUri, body);
|
||||
request.setMethod(method);
|
||||
request.headers().set(this.request.headers());
|
||||
|
||||
// We force some headers to be always applied
|
||||
if (!request.headers().contains(HttpHeaderNames.ACCEPT_CHARSET)) {
|
||||
request.headers().set(HttpHeaderNames.ACCEPT_CHARSET, "UTF-8");
|
||||
}
|
||||
request.headers().set(HttpHeaderNames.HOST, uri.getPort() < 0 ? uri.getHost() : uri.getHost() + ":" + uri.getPort());
|
||||
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
|
||||
|
||||
ctx.channel().writeAndFlush(request);
|
||||
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
if (!closed) request.failure("Could not connect");
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead0(ChannelHandlerContext ctx, HttpObject message) {
|
||||
if (closed || request.checkClosed()) return;
|
||||
|
||||
if (message instanceof HttpResponse response) {
|
||||
|
||||
if (request.redirects.get() > 0) {
|
||||
var redirect = getRedirect(response.status(), response.headers());
|
||||
if (redirect != null && !uri.equals(redirect) && request.redirects.getAndDecrement() > 0) {
|
||||
// If we have a redirect, and don't end up at the same place, then follow it.
|
||||
|
||||
// We mark ourselves as disposed first though, to avoid firing events when the channel
|
||||
// becomes inactive or disposed.
|
||||
closed = true;
|
||||
ctx.close();
|
||||
|
||||
try {
|
||||
HttpRequest.checkUri(redirect);
|
||||
} catch (HTTPRequestException e) {
|
||||
// If we cannot visit this uri, then fail.
|
||||
request.failure(e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
request.request(redirect, response.status().code() == 303 ? HttpMethod.GET : method);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
responseCharset = HttpUtil.getCharset(response, StandardCharsets.UTF_8);
|
||||
responseStatus = response.status();
|
||||
responseHeaders.add(response.headers());
|
||||
}
|
||||
|
||||
if (message instanceof HttpContent content) {
|
||||
|
||||
if (responseBody == null) {
|
||||
responseBody = ctx.alloc().compositeBuffer(DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS);
|
||||
}
|
||||
|
||||
var partial = content.content();
|
||||
if (partial.isReadable()) {
|
||||
// If we've read more than we're allowed to handle, abort as soon as possible.
|
||||
if (options.maxDownload != 0 && responseBody.readableBytes() + partial.readableBytes() > options.maxDownload) {
|
||||
closed = true;
|
||||
ctx.close();
|
||||
|
||||
request.failure("Response is too large");
|
||||
return;
|
||||
}
|
||||
|
||||
responseBody.addComponent(true, partial.retain());
|
||||
}
|
||||
|
||||
if (message instanceof LastHttpContent last) {
|
||||
responseHeaders.add(last.trailingHeaders());
|
||||
|
||||
// Set the content length, if not already given.
|
||||
if (responseHeaders.contains(HttpHeaderNames.CONTENT_LENGTH)) {
|
||||
responseHeaders.set(HttpHeaderNames.CONTENT_LENGTH, responseBody.readableBytes());
|
||||
}
|
||||
|
||||
ctx.close();
|
||||
sendResponse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
LOG.error(Logging.HTTP_ERROR, "Error handling HTTP response", cause);
|
||||
request.failure(NetworkUtils.toFriendlyError(cause));
|
||||
}
|
||||
|
||||
private void sendResponse() {
|
||||
// Read the ByteBuf into a channel.
|
||||
var body = responseBody;
|
||||
var bytes = body == null ? EMPTY_BYTES : NetworkUtils.toBytes(body);
|
||||
|
||||
// Decode the headers
|
||||
var status = responseStatus;
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
for (var header : responseHeaders) {
|
||||
var existing = headers.get(header.getKey());
|
||||
headers.put(header.getKey(), existing == null ? header.getValue() : existing + "," + header.getValue());
|
||||
}
|
||||
|
||||
// Fire off a stats event
|
||||
request.environment().observe(Metrics.HTTP_DOWNLOAD, getHeaderSize(responseHeaders) + bytes.length);
|
||||
|
||||
// Prepare to queue an event
|
||||
var contents = new ArrayByteChannel(bytes);
|
||||
var reader = request.isBinary()
|
||||
? BinaryReadableHandle.of(contents)
|
||||
: new EncodedReadableHandle(EncodedReadableHandle.open(contents, responseCharset));
|
||||
var stream = new HttpResponseHandle(reader, status.code(), status.reasonPhrase(), headers);
|
||||
|
||||
if (status.code() >= 200 && status.code() < 400) {
|
||||
request.success(stream);
|
||||
} else {
|
||||
request.failure(status.reasonPhrase(), stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the redirect from this response.
|
||||
*
|
||||
* @param status The status of the HTTP response.
|
||||
* @param headers The headers of the HTTP response.
|
||||
* @return The URI to redirect to, or {@code null} if no redirect should occur.
|
||||
*/
|
||||
private URI getRedirect(HttpResponseStatus status, HttpHeaders headers) {
|
||||
var code = status.code();
|
||||
if (code < 300 || code > 307 || code == 304 || code == 306) return null;
|
||||
|
||||
var location = headers.get(HttpHeaderNames.LOCATION);
|
||||
if (location == null) return null;
|
||||
|
||||
try {
|
||||
return uri.resolve(new URI(location));
|
||||
} catch (IllegalArgumentException | URISyntaxException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
if (responseBody != null) {
|
||||
responseBody.release();
|
||||
responseBody = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.request;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.apis.HTTPAPI;
|
||||
import dan200.computercraft.core.apis.handles.BinaryReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.EncodedReadableHandle;
|
||||
import dan200.computercraft.core.apis.handles.HandleGeneric;
|
||||
import dan200.computercraft.core.asm.ObjectSource;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A http response. This provides the same methods as a {@link EncodedReadableHandle file} (or
|
||||
* {@link BinaryReadableHandle binary file} if the request used binary mode), though provides several request specific
|
||||
* methods.
|
||||
*
|
||||
* @cc.module http.Response
|
||||
* @see HTTPAPI#request(IArguments) On how to make a http request.
|
||||
*/
|
||||
public class HttpResponseHandle implements ObjectSource {
|
||||
private final Object reader;
|
||||
private final int responseCode;
|
||||
private final String responseStatus;
|
||||
private final Map<String, String> responseHeaders;
|
||||
|
||||
public HttpResponseHandle(@Nonnull HandleGeneric reader, int responseCode, String responseStatus, @Nonnull Map<String, String> responseHeaders) {
|
||||
this.reader = reader;
|
||||
this.responseCode = responseCode;
|
||||
this.responseStatus = responseStatus;
|
||||
this.responseHeaders = responseHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response code and response message returned by the server.
|
||||
*
|
||||
* @return The response code and message.
|
||||
* @cc.treturn number The response code (i.e. 200)
|
||||
* @cc.treturn string The response message (i.e. "OK")
|
||||
* @cc.changed 1.80pr1.13 Added response message return value.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Object[] getResponseCode() {
|
||||
return new Object[]{ responseCode, responseStatus };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a table containing the response's headers, in a format similar to that required by {@link HTTPAPI#request}.
|
||||
* If multiple headers are sent with the same name, they will be combined with a comma.
|
||||
*
|
||||
* @return The response's headers.
|
||||
* @cc.usage Make a request to [example.tweaked.cc](https://example.tweaked.cc), and print the
|
||||
* returned headers.
|
||||
* <pre>{@code
|
||||
* local request = http.get("https://example.tweaked.cc")
|
||||
* print(textutils.serialize(request.getResponseHeaders()))
|
||||
* -- => {
|
||||
* -- [ "Content-Type" ] = "text/plain; charset=utf8",
|
||||
* -- [ "content-length" ] = 17,
|
||||
* -- ...
|
||||
* -- }
|
||||
* request.close()
|
||||
* }</pre>
|
||||
*/
|
||||
@LuaFunction
|
||||
public final Map<String, String> getResponseHeaders() {
|
||||
return responseHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Object> getExtra() {
|
||||
return Collections.singletonList(reader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker13;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* A version of {@link WebSocketClientHandshaker13} which doesn't add the {@link HttpHeaderNames#ORIGIN} header to the
|
||||
* original HTTP request.
|
||||
*/
|
||||
public class NoOriginWebSocketHanshakder extends WebSocketClientHandshaker13 {
|
||||
public NoOriginWebSocketHanshakder(URI webSocketURL, WebSocketVersion version, String subprotocol, boolean allowExtensions, HttpHeaders customHeaders, int maxFramePayloadLength) {
|
||||
super(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FullHttpRequest newHandshakeRequest() {
|
||||
var request = super.newHandshakeRequest();
|
||||
var headers = request.headers();
|
||||
if (!customHeaders.contains(HttpHeaderNames.ORIGIN)) headers.remove(HttpHeaderNames.ORIGIN);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.http.HTTPRequestException;
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.Resource;
|
||||
import dan200.computercraft.core.apis.http.ResourceGroup;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Provides functionality to verify and connect to a remote websocket.
|
||||
*/
|
||||
public class Websocket extends Resource<Websocket> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Websocket.class);
|
||||
|
||||
/**
|
||||
* We declare the maximum size to be 2^30 bytes. While messages can be much longer, we set an arbitrary limit as
|
||||
* working with larger messages (especially within a Lua VM) is absurd.
|
||||
*/
|
||||
public static final int MAX_MESSAGE_SIZE = 1 << 30;
|
||||
|
||||
static final String SUCCESS_EVENT = "websocket_success";
|
||||
static final String FAILURE_EVENT = "websocket_failure";
|
||||
static final String CLOSE_EVENT = "websocket_closed";
|
||||
static final String MESSAGE_EVENT = "websocket_message";
|
||||
|
||||
private Future<?> executorFuture;
|
||||
private ChannelFuture connectFuture;
|
||||
private WeakReference<WebsocketHandle> websocketHandle;
|
||||
|
||||
private final IAPIEnvironment environment;
|
||||
private final URI uri;
|
||||
private final String address;
|
||||
private final HttpHeaders headers;
|
||||
|
||||
public Websocket(ResourceGroup<Websocket> limiter, IAPIEnvironment environment, URI uri, String address, HttpHeaders headers) {
|
||||
super(limiter);
|
||||
this.environment = environment;
|
||||
this.uri = uri;
|
||||
this.address = address;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
public static URI checkUri(String address) throws HTTPRequestException {
|
||||
URI uri = null;
|
||||
try {
|
||||
uri = new URI(address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + address);
|
||||
} catch (URISyntaxException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (uri == null || uri.getHost() == null) throw new HTTPRequestException("URL malformed");
|
||||
|
||||
var scheme = uri.getScheme();
|
||||
if (scheme == null) {
|
||||
try {
|
||||
uri = new URI("ws://" + uri);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new HTTPRequestException("URL malformed");
|
||||
}
|
||||
} else if (!scheme.equalsIgnoreCase("wss") && !scheme.equalsIgnoreCase("ws")) {
|
||||
throw new HTTPRequestException("Invalid scheme '" + scheme + "'");
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
|
||||
public void connect() {
|
||||
if (isClosed()) return;
|
||||
executorFuture = NetworkUtils.EXECUTOR.submit(this::doConnect);
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
private void doConnect() {
|
||||
// If we're cancelled, abort.
|
||||
if (isClosed()) return;
|
||||
|
||||
try {
|
||||
var ssl = uri.getScheme().equalsIgnoreCase("wss");
|
||||
var socketAddress = NetworkUtils.getAddress(uri, ssl);
|
||||
var options = NetworkUtils.getOptions(uri.getHost(), socketAddress);
|
||||
var sslContext = ssl ? NetworkUtils.getSslContext() : null;
|
||||
|
||||
// getAddress may have a slight delay, so let's perform another cancellation check.
|
||||
if (isClosed()) return;
|
||||
|
||||
connectFuture = new Bootstrap()
|
||||
.group(NetworkUtils.LOOP_GROUP)
|
||||
.channel(NioSocketChannel.class)
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
var p = ch.pipeline();
|
||||
p.addLast(NetworkUtils.SHAPING_HANDLER);
|
||||
if (sslContext != null) {
|
||||
p.addLast(sslContext.newHandler(ch.alloc(), uri.getHost(), socketAddress.getPort()));
|
||||
}
|
||||
|
||||
var subprotocol = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
|
||||
WebSocketClientHandshaker handshaker = new NoOriginWebSocketHanshakder(
|
||||
uri, WebSocketVersion.V13, subprotocol, true, headers,
|
||||
options.websocketMessage <= 0 ? MAX_MESSAGE_SIZE : options.websocketMessage
|
||||
);
|
||||
|
||||
p.addLast(
|
||||
new HttpClientCodec(),
|
||||
new HttpObjectAggregator(8192),
|
||||
WebsocketCompressionHandler.INSTANCE,
|
||||
new WebsocketHandler(Websocket.this, handshaker, options)
|
||||
);
|
||||
}
|
||||
})
|
||||
.remoteAddress(socketAddress)
|
||||
.connect()
|
||||
.addListener(c -> {
|
||||
if (!c.isSuccess()) failure(NetworkUtils.toFriendlyError(c.cause()));
|
||||
});
|
||||
|
||||
// Do an additional check for cancellation
|
||||
checkClosed();
|
||||
} catch (HTTPRequestException e) {
|
||||
failure(e.getMessage());
|
||||
} catch (Exception e) {
|
||||
failure(NetworkUtils.toFriendlyError(e));
|
||||
LOG.error(Logging.HTTP_ERROR, "Error in websocket", e);
|
||||
}
|
||||
}
|
||||
|
||||
void success(Channel channel, Options options) {
|
||||
if (isClosed()) return;
|
||||
|
||||
var handle = new WebsocketHandle(this, options, channel);
|
||||
environment().queueEvent(SUCCESS_EVENT, address, handle);
|
||||
websocketHandle = createOwnerReference(handle);
|
||||
|
||||
checkClosed();
|
||||
}
|
||||
|
||||
void failure(String message) {
|
||||
if (tryClose()) environment.queueEvent(FAILURE_EVENT, address, message);
|
||||
}
|
||||
|
||||
void close(int status, String reason) {
|
||||
if (tryClose()) {
|
||||
environment.queueEvent(CLOSE_EVENT, address,
|
||||
Strings.isNullOrEmpty(reason) ? null : reason,
|
||||
status < 0 ? null : status);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dispose() {
|
||||
super.dispose();
|
||||
|
||||
executorFuture = closeFuture(executorFuture);
|
||||
connectFuture = closeChannel(connectFuture);
|
||||
|
||||
var websocketHandleRef = websocketHandle;
|
||||
var websocketHandle = websocketHandleRef == null ? null : websocketHandleRef.get();
|
||||
IoUtil.closeQuietly(websocketHandle);
|
||||
this.websocketHandle = null;
|
||||
}
|
||||
|
||||
public IAPIEnvironment environment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
public String address() {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.handler.codec.compression.ZlibCodecFactory;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketClientExtensionHandler;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameClientExtensionHandshaker;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateClientExtensionHandshaker;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
|
||||
|
||||
import static io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE;
|
||||
|
||||
/**
|
||||
* An alternative to {@link WebSocketClientCompressionHandler} which supports the {@code client_no_context_takeover}
|
||||
* extension. Makes CC <em>slightly</em> more flexible.
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
final class WebsocketCompressionHandler extends WebSocketClientExtensionHandler {
|
||||
public static final WebsocketCompressionHandler INSTANCE = new WebsocketCompressionHandler();
|
||||
|
||||
private WebsocketCompressionHandler() {
|
||||
super(
|
||||
new PerMessageDeflateClientExtensionHandshaker(
|
||||
6, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(), MAX_WINDOW_SIZE,
|
||||
true, false
|
||||
),
|
||||
new DeflateFrameClientExtensionHandshaker(false),
|
||||
new DeflateFrameClientExtensionHandshaker(true)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.lua.*;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.checkFinite;
|
||||
import static dan200.computercraft.core.apis.IAPIEnvironment.TIMER_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.CLOSE_EVENT;
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
||||
|
||||
/**
|
||||
* A websocket, which can be used to send an receive messages with a web server.
|
||||
*
|
||||
* @cc.module http.Websocket
|
||||
* @see dan200.computercraft.core.apis.HTTPAPI#websocket On how to open a websocket.
|
||||
*/
|
||||
public class WebsocketHandle implements Closeable {
|
||||
private final Websocket websocket;
|
||||
private final Options options;
|
||||
private boolean closed = false;
|
||||
|
||||
private Channel channel;
|
||||
|
||||
public WebsocketHandle(Websocket websocket, Options options, Channel channel) {
|
||||
this.websocket = websocket;
|
||||
this.options = options;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a message from the server.
|
||||
*
|
||||
* @param timeout The number of seconds to wait if no message is received.
|
||||
* @return The result of receiving.
|
||||
* @throws LuaException If the websocket has been closed.
|
||||
* @cc.treturn [1] string The received message.
|
||||
* @cc.treturn boolean If this was a binary message.
|
||||
* @cc.treturn [2] nil If the websocket was closed while waiting, or if we timed out.
|
||||
* @cc.changed 1.80pr1.13 Added return value indicating whether the message was binary.
|
||||
* @cc.changed 1.87.0 Added timeout argument.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final MethodResult receive(Optional<Double> timeout) throws LuaException {
|
||||
checkOpen();
|
||||
var timeoutId = timeout.isPresent()
|
||||
? websocket.environment().startTimer(Math.round(checkFinite(0, timeout.get()) / 0.05))
|
||||
: -1;
|
||||
|
||||
return new ReceiveCallback(timeoutId).pull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a websocket message to the connected server.
|
||||
*
|
||||
* @param message The message to send.
|
||||
* @param binary Whether this message should be treated as a
|
||||
* @throws LuaException If the message is too large.
|
||||
* @throws LuaException If the websocket has been closed.
|
||||
* @cc.changed 1.81.0 Added argument for binary mode.
|
||||
*/
|
||||
@LuaFunction
|
||||
public final void send(Object message, Optional<Boolean> binary) throws LuaException {
|
||||
checkOpen();
|
||||
|
||||
var text = StringUtil.toString(message);
|
||||
if (options.websocketMessage != 0 && text.length() > options.websocketMessage) {
|
||||
throw new LuaException("Message is too large");
|
||||
}
|
||||
|
||||
websocket.environment().observe(Metrics.WEBSOCKET_OUTGOING, text.length());
|
||||
|
||||
var channel = this.channel;
|
||||
if (channel != null) {
|
||||
channel.writeAndFlush(binary.orElse(false)
|
||||
? new BinaryWebSocketFrame(Unpooled.wrappedBuffer(LuaValues.encode(text)))
|
||||
: new TextWebSocketFrame(text));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this websocket. This will terminate the connection, meaning messages can no longer be sent or received
|
||||
* along it.
|
||||
*/
|
||||
@LuaFunction("close")
|
||||
public final void doClose() {
|
||||
close();
|
||||
websocket.close();
|
||||
}
|
||||
|
||||
private void checkOpen() throws LuaException {
|
||||
if (closed) throw new LuaException("attempt to use a closed file");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
|
||||
var channel = this.channel;
|
||||
if (channel != null) {
|
||||
channel.close();
|
||||
this.channel = null;
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReceiveCallback implements ILuaCallback {
|
||||
final MethodResult pull = MethodResult.pullEvent(null, this);
|
||||
private final int timeoutId;
|
||||
|
||||
ReceiveCallback(int timeoutId) {
|
||||
this.timeoutId = timeoutId;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MethodResult resume(Object[] event) {
|
||||
if (event.length >= 3 && Objects.equal(event[0], MESSAGE_EVENT) && Objects.equal(event[1], websocket.address())) {
|
||||
return MethodResult.of(Arrays.copyOfRange(event, 2, event.length));
|
||||
} else if (event.length >= 2 && Objects.equal(event[0], CLOSE_EVENT) && Objects.equal(event[1], websocket.address()) && closed) {
|
||||
// If the socket is closed abort.
|
||||
return MethodResult.of();
|
||||
} else if (event.length >= 2 && timeoutId != -1 && Objects.equal(event[0], TIMER_EVENT)
|
||||
&& event[1] instanceof Number id && id.intValue() == timeoutId) {
|
||||
// If we received a matching timer event then abort.
|
||||
return MethodResult.of();
|
||||
}
|
||||
|
||||
return pull;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.apis.http.websocket;
|
||||
|
||||
import dan200.computercraft.core.apis.http.NetworkUtils;
|
||||
import dan200.computercraft.core.apis.http.options.Options;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.websocketx.*;
|
||||
import io.netty.util.CharsetUtil;
|
||||
|
||||
import static dan200.computercraft.core.apis.http.websocket.Websocket.MESSAGE_EVENT;
|
||||
|
||||
public class WebsocketHandler extends SimpleChannelInboundHandler<Object> {
|
||||
private final Websocket websocket;
|
||||
private final WebSocketClientHandshaker handshaker;
|
||||
private final Options options;
|
||||
|
||||
public WebsocketHandler(Websocket websocket, WebSocketClientHandshaker handshaker, Options options) {
|
||||
this.handshaker = handshaker;
|
||||
this.websocket = websocket;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
handshaker.handshake(ctx.channel());
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
websocket.close(-1, "Websocket is inactive");
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead0(ChannelHandlerContext ctx, Object msg) {
|
||||
if (websocket.isClosed()) return;
|
||||
|
||||
if (!handshaker.isHandshakeComplete()) {
|
||||
handshaker.finishHandshake(ctx.channel(), (FullHttpResponse) msg);
|
||||
websocket.success(ctx.channel(), options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg instanceof FullHttpResponse response) {
|
||||
throw new IllegalStateException("Unexpected FullHttpResponse (getStatus=" + response.status() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ')');
|
||||
}
|
||||
|
||||
var frame = (WebSocketFrame) msg;
|
||||
if (frame instanceof TextWebSocketFrame textFrame) {
|
||||
var data = textFrame.text();
|
||||
|
||||
websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, data.length());
|
||||
websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), data, false);
|
||||
} else if (frame instanceof BinaryWebSocketFrame) {
|
||||
var converted = NetworkUtils.toBytes(frame.content());
|
||||
|
||||
websocket.environment().observe(Metrics.WEBSOCKET_INCOMING, converted.length);
|
||||
websocket.environment().queueEvent(MESSAGE_EVENT, websocket.address(), converted, true);
|
||||
} else if (frame instanceof CloseWebSocketFrame closeFrame) {
|
||||
websocket.close(closeFrame.statusCode(), closeFrame.reasonText());
|
||||
} else if (frame instanceof PingWebSocketFrame) {
|
||||
frame.content().retain();
|
||||
ctx.channel().writeAndFlush(new PongWebSocketFrame(frame.content()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
ctx.close();
|
||||
|
||||
var message = NetworkUtils.toFriendlyError(cause);
|
||||
if (handshaker.isHandshakeComplete()) {
|
||||
websocket.close(-1, message);
|
||||
} else {
|
||||
websocket.failure(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import java.security.ProtectionDomain;
|
||||
|
||||
final class DeclaringClassLoader extends ClassLoader {
|
||||
static final DeclaringClassLoader INSTANCE = new DeclaringClassLoader();
|
||||
|
||||
private DeclaringClassLoader() {
|
||||
super(DeclaringClassLoader.class.getClassLoader());
|
||||
}
|
||||
|
||||
Class<?> define(String name, byte[] bytes, ProtectionDomain protectionDomain) throws ClassFormatError {
|
||||
return defineClass(name, bytes, 0, bytes.length, protectionDomain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.primitives.Primitives;
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.api.peripheral.PeripheralType;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.*;
|
||||
|
||||
public final class Generator<T> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Generator.class);
|
||||
|
||||
private static final AtomicInteger METHOD_ID = new AtomicInteger();
|
||||
|
||||
private static final String METHOD_NAME = "apply";
|
||||
private static final String[] EXCEPTIONS = new String[]{ Type.getInternalName(LuaException.class) };
|
||||
|
||||
private static final String INTERNAL_METHOD_RESULT = Type.getInternalName(MethodResult.class);
|
||||
private static final String DESC_METHOD_RESULT = Type.getDescriptor(MethodResult.class);
|
||||
|
||||
private static final String INTERNAL_ARGUMENTS = Type.getInternalName(IArguments.class);
|
||||
private static final String DESC_ARGUMENTS = Type.getDescriptor(IArguments.class);
|
||||
|
||||
private final Class<T> base;
|
||||
private final List<Class<?>> context;
|
||||
|
||||
private final String[] interfaces;
|
||||
private final String methodDesc;
|
||||
|
||||
private final Function<T, T> wrap;
|
||||
|
||||
private final LoadingCache<Class<?>, List<NamedMethod<T>>> classCache = CacheBuilder
|
||||
.newBuilder()
|
||||
.build(CacheLoader.from(catching(this::build, Collections.emptyList())));
|
||||
|
||||
private final LoadingCache<Method, Optional<T>> methodCache = CacheBuilder
|
||||
.newBuilder()
|
||||
.build(CacheLoader.from(catching(this::build, Optional.empty())));
|
||||
|
||||
Generator(Class<T> base, List<Class<?>> context, Function<T, T> wrap) {
|
||||
this.base = base;
|
||||
this.context = context;
|
||||
interfaces = new String[]{ Type.getInternalName(base) };
|
||||
this.wrap = wrap;
|
||||
|
||||
var methodDesc = new StringBuilder().append("(Ljava/lang/Object;");
|
||||
for (var klass : context) methodDesc.append(Type.getDescriptor(klass));
|
||||
methodDesc.append(DESC_ARGUMENTS).append(")").append(DESC_METHOD_RESULT);
|
||||
this.methodDesc = methodDesc.toString();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<NamedMethod<T>> getMethods(@Nonnull Class<?> klass) {
|
||||
try {
|
||||
return classCache.get(klass);
|
||||
} catch (ExecutionException e) {
|
||||
LOG.error("Error getting methods for {}.", klass.getName(), e.getCause());
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<NamedMethod<T>> build(Class<?> klass) {
|
||||
ArrayList<NamedMethod<T>> methods = null;
|
||||
for (var method : klass.getMethods()) {
|
||||
var annotation = method.getAnnotation(LuaFunction.class);
|
||||
if (annotation == null) continue;
|
||||
|
||||
if (Modifier.isStatic(method.getModifiers())) {
|
||||
LOG.warn("LuaFunction method {}.{} should be an instance method.", method.getDeclaringClass(), method.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
var instance = methodCache.getUnchecked(method).orElse(null);
|
||||
if (instance == null) continue;
|
||||
|
||||
if (methods == null) methods = new ArrayList<>();
|
||||
addMethod(methods, method, annotation, null, instance);
|
||||
}
|
||||
|
||||
for (var method : GenericMethod.all()) {
|
||||
if (!method.target.isAssignableFrom(klass)) continue;
|
||||
|
||||
var instance = methodCache.getUnchecked(method.method).orElse(null);
|
||||
if (instance == null) continue;
|
||||
|
||||
if (methods == null) methods = new ArrayList<>();
|
||||
addMethod(methods, method.method, method.annotation, method.peripheralType, instance);
|
||||
}
|
||||
|
||||
if (methods == null) return Collections.emptyList();
|
||||
methods.trimToSize();
|
||||
return Collections.unmodifiableList(methods);
|
||||
}
|
||||
|
||||
private void addMethod(List<NamedMethod<T>> methods, Method method, LuaFunction annotation, PeripheralType genericType, T instance) {
|
||||
var names = annotation.value();
|
||||
var isSimple = method.getReturnType() != MethodResult.class && !annotation.mainThread();
|
||||
if (names.length == 0) {
|
||||
methods.add(new NamedMethod<>(method.getName(), instance, isSimple, genericType));
|
||||
} else {
|
||||
for (var name : names) {
|
||||
methods.add(new NamedMethod<>(name, instance, isSimple, genericType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Optional<T> build(Method method) {
|
||||
var name = method.getDeclaringClass().getName() + "." + method.getName();
|
||||
var modifiers = method.getModifiers();
|
||||
|
||||
// Instance methods must be final - this prevents them being overridden and potentially exposed twice.
|
||||
if (!Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)) {
|
||||
LOG.warn("Lua Method {} should be final.", name);
|
||||
}
|
||||
|
||||
if (!Modifier.isPublic(modifiers)) {
|
||||
LOG.error("Lua Method {} should be a public method.", name);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
|
||||
LOG.error("Lua Method {} should be on a public class.", name);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
LOG.debug("Generating method wrapper for {}.", name);
|
||||
|
||||
var exceptions = method.getExceptionTypes();
|
||||
for (var exception : exceptions) {
|
||||
if (exception != LuaException.class) {
|
||||
LOG.error("Lua Method {} cannot throw {}.", name, exception.getName());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
var annotation = method.getAnnotation(LuaFunction.class);
|
||||
if (annotation.unsafe() && annotation.mainThread()) {
|
||||
LOG.error("Lua Method {} cannot use unsafe and mainThread", name);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// We have some rather ugly handling of static methods in both here and the main generate function. Static methods
|
||||
// only come from generic sources, so this should be safe.
|
||||
var target = Modifier.isStatic(modifiers) ? method.getParameterTypes()[0] : method.getDeclaringClass();
|
||||
|
||||
try {
|
||||
var className = method.getDeclaringClass().getName() + "$cc$" + method.getName() + METHOD_ID.getAndIncrement();
|
||||
var bytes = generate(className, target, method, annotation.unsafe());
|
||||
if (bytes == null) return Optional.empty();
|
||||
|
||||
var klass = DeclaringClassLoader.INSTANCE.define(className, bytes, method.getDeclaringClass().getProtectionDomain());
|
||||
|
||||
var instance = klass.asSubclass(base).getDeclaredConstructor().newInstance();
|
||||
return Optional.of(annotation.mainThread() ? wrap.apply(instance) : instance);
|
||||
} catch (ReflectiveOperationException | ClassFormatError | RuntimeException e) {
|
||||
LOG.error("Error generating wrapper for {}.", name, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private byte[] generate(String className, Class<?> target, Method method, boolean unsafe) {
|
||||
var internalName = className.replace(".", "/");
|
||||
|
||||
// Construct a public final class which extends Object and implements MethodInstance.Delegate
|
||||
var cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
cw.visit(V1_8, ACC_PUBLIC | ACC_FINAL, internalName, null, "java/lang/Object", interfaces);
|
||||
cw.visitSource("CC generated method", null);
|
||||
|
||||
{ // Constructor just invokes super.
|
||||
var mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
mw.visitCode();
|
||||
mw.visitVarInsn(ALOAD, 0);
|
||||
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
|
||||
mw.visitInsn(RETURN);
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
}
|
||||
|
||||
{
|
||||
var mw = cw.visitMethod(ACC_PUBLIC, METHOD_NAME, methodDesc, null, EXCEPTIONS);
|
||||
mw.visitCode();
|
||||
|
||||
// If we're an instance method, load the this parameter.
|
||||
if (!Modifier.isStatic(method.getModifiers())) {
|
||||
mw.visitVarInsn(ALOAD, 1);
|
||||
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(target));
|
||||
}
|
||||
|
||||
var argIndex = 0;
|
||||
for (var genericArg : method.getGenericParameterTypes()) {
|
||||
var loadedArg = loadArg(mw, target, method, unsafe, genericArg, argIndex);
|
||||
if (loadedArg == null) return null;
|
||||
if (loadedArg) argIndex++;
|
||||
}
|
||||
|
||||
mw.visitMethodInsn(
|
||||
Modifier.isStatic(method.getModifiers()) ? INVOKESTATIC : INVOKEVIRTUAL,
|
||||
Type.getInternalName(method.getDeclaringClass()), method.getName(),
|
||||
Type.getMethodDescriptor(method), false
|
||||
);
|
||||
|
||||
// We allow a reasonable amount of flexibility on the return value's type. Alongside the obvious MethodResult,
|
||||
// we convert basic types into an immediate result.
|
||||
var ret = method.getReturnType();
|
||||
if (ret != MethodResult.class) {
|
||||
if (ret == void.class) {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "()" + DESC_METHOD_RESULT, false);
|
||||
} else if (ret.isPrimitive()) {
|
||||
var boxed = Primitives.wrap(ret);
|
||||
mw.visitMethodInsn(INVOKESTATIC, Type.getInternalName(boxed), "valueOf", "(" + Type.getDescriptor(ret) + ")" + Type.getDescriptor(boxed), false);
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false);
|
||||
} else if (ret == Object[].class) {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "([Ljava/lang/Object;)" + DESC_METHOD_RESULT, false);
|
||||
} else {
|
||||
mw.visitMethodInsn(INVOKESTATIC, INTERNAL_METHOD_RESULT, "of", "(Ljava/lang/Object;)" + DESC_METHOD_RESULT, false);
|
||||
}
|
||||
}
|
||||
|
||||
mw.visitInsn(ARETURN);
|
||||
|
||||
mw.visitMaxs(0, 0);
|
||||
mw.visitEnd();
|
||||
}
|
||||
|
||||
cw.visitEnd();
|
||||
|
||||
return cw.toByteArray();
|
||||
}
|
||||
|
||||
private Boolean loadArg(MethodVisitor mw, Class<?> target, Method method, boolean unsafe, java.lang.reflect.Type genericArg, int argIndex) {
|
||||
if (genericArg == target) {
|
||||
mw.visitVarInsn(ALOAD, 1);
|
||||
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(target));
|
||||
return false;
|
||||
}
|
||||
|
||||
var arg = Reflect.getRawType(method, genericArg, true);
|
||||
if (arg == null) return null;
|
||||
|
||||
if (arg == IArguments.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
var idx = context.indexOf(arg);
|
||||
if (idx >= 0) {
|
||||
mw.visitVarInsn(ALOAD, 2 + idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arg == Optional.class) {
|
||||
var klass = Reflect.getRawType(method, TypeToken.of(genericArg).resolveType(Reflect.OPTIONAL_IN).getType(), false);
|
||||
if (klass == null) return null;
|
||||
|
||||
if (Enum.class.isAssignableFrom(klass) && klass != Enum.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitLdcInsn(Type.getType(klass));
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "optEnum", "(ILjava/lang/Class;)Ljava/util/Optional;", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
var name = Reflect.getLuaName(Primitives.unwrap(klass), unsafe);
|
||||
if (name != null) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "opt" + name, "(I)Ljava/util/Optional;", true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Enum.class.isAssignableFrom(arg) && arg != Enum.class) {
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitLdcInsn(Type.getType(arg));
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "getEnum", "(ILjava/lang/Class;)Ljava/lang/Enum;", true);
|
||||
mw.visitTypeInsn(CHECKCAST, Type.getInternalName(arg));
|
||||
return true;
|
||||
}
|
||||
|
||||
var name = arg == Object.class ? "" : Reflect.getLuaName(arg, unsafe);
|
||||
if (name != null) {
|
||||
if (Reflect.getRawType(method, genericArg, false) == null) return null;
|
||||
|
||||
mw.visitVarInsn(ALOAD, 2 + context.size());
|
||||
Reflect.loadInt(mw, argIndex);
|
||||
mw.visitMethodInsn(INVOKEINTERFACE, INTERNAL_ARGUMENTS, "get" + name, "(I)" + Type.getDescriptor(arg), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG.error("Unknown parameter type {} for method {}.{}.",
|
||||
arg.getName(), method.getDeclaringClass().getName(), method.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("Guava")
|
||||
private static <T, U> com.google.common.base.Function<T, U> catching(Function<T, U> function, U def) {
|
||||
return x -> {
|
||||
try {
|
||||
return function.apply(x);
|
||||
} catch (Exception | LinkageError e) {
|
||||
// LinkageError due to possible codegen bugs and NoClassDefFoundError. The latter occurs when fetching
|
||||
// methods on a class which references non-existent (i.e. client-only) types.
|
||||
LOG.error("Error generating @LuaFunctions", e);
|
||||
return def;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.GenericSource;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.api.peripheral.GenericPeripheral;
|
||||
import dan200.computercraft.api.peripheral.PeripheralType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A generic method is a method belonging to a {@link GenericSource} with a known target.
|
||||
*/
|
||||
public class GenericMethod {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GenericMethod.class);
|
||||
|
||||
final Method method;
|
||||
final LuaFunction annotation;
|
||||
final Class<?> target;
|
||||
final PeripheralType peripheralType;
|
||||
|
||||
private static final List<GenericSource> sources = new ArrayList<>();
|
||||
private static List<GenericMethod> cache;
|
||||
|
||||
GenericMethod(Method method, LuaFunction annotation, Class<?> target, PeripheralType peripheralType) {
|
||||
this.method = method;
|
||||
this.annotation = annotation;
|
||||
this.target = target;
|
||||
this.peripheralType = peripheralType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all public static methods annotated with {@link LuaFunction} which belong to a {@link GenericSource}.
|
||||
*
|
||||
* @return All available generic methods.
|
||||
*/
|
||||
static List<GenericMethod> all() {
|
||||
if (cache != null) return cache;
|
||||
return cache = sources.stream().flatMap(GenericMethod::getMethods).toList();
|
||||
}
|
||||
|
||||
public static synchronized void register(@Nonnull GenericSource source) {
|
||||
Objects.requireNonNull(source, "Source cannot be null");
|
||||
|
||||
if (cache != null) {
|
||||
LOG.warn("Registering a generic source {} after cache has been built. This source will be ignored.", cache);
|
||||
}
|
||||
|
||||
sources.add(source);
|
||||
}
|
||||
|
||||
private static Stream<GenericMethod> getMethods(GenericSource source) {
|
||||
Class<?> klass = source.getClass();
|
||||
var type = source instanceof GenericPeripheral generic ? generic.getType() : null;
|
||||
|
||||
return Arrays.stream(klass.getDeclaredMethods())
|
||||
.map(method -> {
|
||||
var annotation = method.getAnnotation(LuaFunction.class);
|
||||
if (annotation == null) return null;
|
||||
|
||||
if (!Modifier.isStatic(method.getModifiers())) {
|
||||
LOG.error("GenericSource method {}.{} should be static.", method.getDeclaringClass(), method.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
var types = method.getGenericParameterTypes();
|
||||
if (types.length == 0) {
|
||||
LOG.error("GenericSource method {}.{} has no parameters.", method.getDeclaringClass(), method.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
var target = Reflect.getRawType(method, types[0], false);
|
||||
if (target == null) return null;
|
||||
|
||||
return new GenericMethod(method, annotation, target, type);
|
||||
})
|
||||
.filter(Objects::nonNull);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
public final class IntCache<T> {
|
||||
private final IntFunction<T> factory;
|
||||
private volatile Object[] cache = new Object[16];
|
||||
|
||||
IntCache(IntFunction<T> factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public T get(int index) {
|
||||
if (index < 0) throw new IllegalArgumentException("index < 0");
|
||||
|
||||
if (index < cache.length) {
|
||||
var current = (T) cache[index];
|
||||
if (current != null) return current;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (index >= cache.length) cache = Arrays.copyOf(cache, Math.max(cache.length * 2, index + 1));
|
||||
var current = (T) cache[index];
|
||||
if (current == null) cache[index] = current = factory.apply(index);
|
||||
return current;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.*;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Collections;
|
||||
|
||||
public interface LuaMethod {
|
||||
Generator<LuaMethod> GENERATOR = new Generator<>(LuaMethod.class, Collections.singletonList(ILuaContext.class),
|
||||
m -> (target, context, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, args)))
|
||||
);
|
||||
|
||||
IntCache<LuaMethod> DYNAMIC = new IntCache<>(
|
||||
method -> (instance, context, args) -> ((IDynamicLuaObject) instance).callMethod(context, method, args)
|
||||
);
|
||||
|
||||
String[] EMPTY_METHODS = new String[0];
|
||||
|
||||
@Nonnull
|
||||
MethodResult apply(@Nonnull Object target, @Nonnull ILuaContext context, @Nonnull IArguments args) throws LuaException;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.peripheral.PeripheralType;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class NamedMethod<T> {
|
||||
private final String name;
|
||||
private final T method;
|
||||
private final boolean nonYielding;
|
||||
|
||||
private final PeripheralType genericType;
|
||||
|
||||
NamedMethod(String name, T method, boolean nonYielding, PeripheralType genericType) {
|
||||
this.name = name;
|
||||
this.method = method;
|
||||
this.nonYielding = nonYielding;
|
||||
this.genericType = genericType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public T getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public boolean nonYielding() {
|
||||
return nonYielding;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PeripheralType getGenericType() {
|
||||
return genericType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* A Lua object which exposes additional methods.
|
||||
* <p>
|
||||
* This can be used to merge multiple objects together into one. Ideally this'd be part of the API, but I'm not entirely
|
||||
* happy with the interface - something I'd like to think about first.
|
||||
*/
|
||||
public interface ObjectSource {
|
||||
Iterable<Object> getExtra();
|
||||
|
||||
static <T> void allMethods(Generator<T> generator, Object object, BiConsumer<Object, NamedMethod<T>> accept) {
|
||||
for (var method : generator.getMethods(object.getClass())) accept.accept(object, method);
|
||||
|
||||
if (object instanceof ObjectSource source) {
|
||||
for (var extra : source.getExtra()) {
|
||||
for (var method : generator.getMethods(extra.getClass())) accept.accept(extra, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.api.peripheral.IDynamicPeripheral;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Arrays;
|
||||
|
||||
public interface PeripheralMethod {
|
||||
Generator<PeripheralMethod> GENERATOR = new Generator<>(PeripheralMethod.class, Arrays.asList(ILuaContext.class, IComputerAccess.class),
|
||||
m -> (target, context, computer, args) -> context.executeMainThreadTask(() -> ResultHelpers.checkNormalResult(m.apply(target, context, computer, args)))
|
||||
);
|
||||
|
||||
IntCache<PeripheralMethod> DYNAMIC = new IntCache<>(
|
||||
method -> (instance, context, computer, args) -> ((IDynamicPeripheral) instance).callMethod(computer, context, method, args)
|
||||
);
|
||||
|
||||
@Nonnull
|
||||
MethodResult apply(@Nonnull Object target, @Nonnull ILuaContext context, @Nonnull IComputerAccess computer, @Nonnull IArguments args) throws LuaException;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaTable;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.ICONST_0;
|
||||
|
||||
final class Reflect {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Reflect.class);
|
||||
static final java.lang.reflect.Type OPTIONAL_IN = Optional.class.getTypeParameters()[0];
|
||||
|
||||
private Reflect() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static String getLuaName(Class<?> klass, boolean unsafe) {
|
||||
if (klass.isPrimitive()) {
|
||||
if (klass == int.class) return "Int";
|
||||
if (klass == boolean.class) return "Boolean";
|
||||
if (klass == double.class) return "Double";
|
||||
if (klass == long.class) return "Long";
|
||||
} else {
|
||||
if (klass == Map.class) return "Table";
|
||||
if (klass == String.class) return "String";
|
||||
if (klass == ByteBuffer.class) return "Bytes";
|
||||
if (klass == LuaTable.class && unsafe) return "TableUnsafe";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static Class<?> getRawType(Method method, Type root, boolean allowParameter) {
|
||||
var underlying = root;
|
||||
while (true) {
|
||||
if (underlying instanceof Class<?> klass) return klass;
|
||||
|
||||
if (underlying instanceof ParameterizedType type) {
|
||||
if (!allowParameter) {
|
||||
for (var arg : type.getActualTypeArguments()) {
|
||||
if (arg instanceof WildcardType) continue;
|
||||
if (arg instanceof TypeVariable<?> var && var.getName().startsWith("capture#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG.error("Method {}.{} has generic type {} with non-wildcard argument {}.", method.getDeclaringClass(), method.getName(), root, arg);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Continue to extract from this child
|
||||
underlying = type.getRawType();
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG.error("Method {}.{} has unknown generic type {}.", method.getDeclaringClass(), method.getName(), root);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void loadInt(MethodVisitor visitor, int value) {
|
||||
if (value >= -1 && value <= 5) {
|
||||
visitor.visitInsn(ICONST_0 + value);
|
||||
} else {
|
||||
visitor.visitLdcInsn(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.asm;
|
||||
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
|
||||
final class ResultHelpers {
|
||||
private ResultHelpers() {
|
||||
}
|
||||
|
||||
static Object[] checkNormalResult(MethodResult result) {
|
||||
if (result.getCallback() != null) {
|
||||
// Due to how tasks are implemented, we can't currently return a MethodResult. This is an
|
||||
// entirely artificial limitation - we can remove it if it ever becomes an issue.
|
||||
throw new IllegalStateException("Must return MethodResult.of from mainThread function.");
|
||||
}
|
||||
|
||||
return result.getResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
|
||||
/**
|
||||
* A wrapper for {@link ILuaAPI}s which cleans up after a {@link ComputerSystem} when the computer is shutdown.
|
||||
*/
|
||||
final class ApiWrapper implements ILuaAPI {
|
||||
private final ILuaAPI delegate;
|
||||
private final ComputerSystem system;
|
||||
|
||||
ApiWrapper(ILuaAPI delegate, ComputerSystem system) {
|
||||
this.delegate = delegate;
|
||||
this.system = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getNames() {
|
||||
return delegate.getNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startup() {
|
||||
delegate.startup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
delegate.update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
delegate.shutdown();
|
||||
system.unmountAll();
|
||||
}
|
||||
|
||||
public ILuaAPI getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.ILuaTask;
|
||||
import dan200.computercraft.api.peripheral.IWorkMonitor;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
||||
import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Represents a computer which may exist in-world or elsewhere.
|
||||
* <p>
|
||||
* Note, this class has several (read: far, far too many) responsibilities, so can get a little unwieldy at times.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Updates the {@link Environment}.</li>
|
||||
* <li>Keeps track of whether the computer is on and blinking.</li>
|
||||
* <li>Monitors whether the computer's visible state (redstone, on/off/blinking) has changed.</li>
|
||||
* <li>Passes commands and events to the {@link ComputerExecutor}.</li>
|
||||
* <li>Passes main thread tasks to the {@link MainThreadScheduler.Executor}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class Computer {
|
||||
private static final int START_DELAY = 50;
|
||||
|
||||
// Various properties of the computer
|
||||
private final int id;
|
||||
private String label = null;
|
||||
|
||||
// Read-only fields about the computer
|
||||
private final GlobalEnvironment globalEnvironment;
|
||||
private final Terminal terminal;
|
||||
private final ComputerExecutor executor;
|
||||
private final MainThreadScheduler.Executor serverExecutor;
|
||||
|
||||
/**
|
||||
* An internal counter for {@link ILuaTask} ids.
|
||||
*
|
||||
* @see ILuaContext#issueMainThreadTask(ILuaTask)
|
||||
* @see #getUniqueTaskId()
|
||||
*/
|
||||
private final AtomicLong lastTaskId = new AtomicLong();
|
||||
|
||||
// Additional state about the computer and its environment.
|
||||
private boolean blinking = false;
|
||||
private final Environment internalEnvironment;
|
||||
private final AtomicBoolean externalOutputChanged = new AtomicBoolean();
|
||||
|
||||
private boolean startRequested;
|
||||
private int ticksSinceStart = -1;
|
||||
|
||||
public Computer(ComputerContext context, ComputerEnvironment environment, Terminal terminal, int id) {
|
||||
if (id < 0) throw new IllegalStateException("Id has not been assigned");
|
||||
this.id = id;
|
||||
globalEnvironment = context.globalEnvironment();
|
||||
this.terminal = terminal;
|
||||
|
||||
internalEnvironment = new Environment(this, environment);
|
||||
executor = new ComputerExecutor(this, environment, context);
|
||||
serverExecutor = context.mainThreadScheduler().createExecutor(environment.getMetrics());
|
||||
}
|
||||
|
||||
GlobalEnvironment getGlobalEnvironment() {
|
||||
return globalEnvironment;
|
||||
}
|
||||
|
||||
FileSystem getFileSystem() {
|
||||
return executor.getFileSystem();
|
||||
}
|
||||
|
||||
Terminal getTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
public Environment getEnvironment() {
|
||||
return internalEnvironment;
|
||||
}
|
||||
|
||||
public IAPIEnvironment getAPIEnvironment() {
|
||||
return internalEnvironment;
|
||||
}
|
||||
|
||||
public boolean isOn() {
|
||||
return executor.isOn();
|
||||
}
|
||||
|
||||
public void turnOn() {
|
||||
startRequested = true;
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
executor.queueStop(false, false);
|
||||
}
|
||||
|
||||
public void reboot() {
|
||||
executor.queueStop(true, false);
|
||||
}
|
||||
|
||||
public void unload() {
|
||||
executor.queueStop(false, true);
|
||||
}
|
||||
|
||||
public void queueEvent(String event, Object[] args) {
|
||||
executor.queueEvent(event, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a task to be run on the main thread, using {@link MainThreadScheduler}.
|
||||
*
|
||||
* @param runnable The task to run
|
||||
* @return If the task was successfully queued (namely, whether there is space on it).
|
||||
*/
|
||||
public boolean queueMainThread(Runnable runnable) {
|
||||
return serverExecutor.enqueue(runnable);
|
||||
}
|
||||
|
||||
public IWorkMonitor getMainThreadMonitor() {
|
||||
return serverExecutor;
|
||||
}
|
||||
|
||||
public int getID() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
if (!Objects.equal(label, this.label)) {
|
||||
this.label = label;
|
||||
externalOutputChanged.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
// We keep track of the number of ticks since the last start, only
|
||||
if (ticksSinceStart >= 0 && ticksSinceStart <= START_DELAY) ticksSinceStart++;
|
||||
|
||||
if (startRequested && (ticksSinceStart < 0 || ticksSinceStart > START_DELAY)) {
|
||||
startRequested = false;
|
||||
if (!executor.isOn()) {
|
||||
ticksSinceStart = 0;
|
||||
executor.queueStart();
|
||||
}
|
||||
}
|
||||
|
||||
executor.tick();
|
||||
|
||||
// Update the environment's internal state.
|
||||
internalEnvironment.tick();
|
||||
|
||||
// Propagate the environment's output to the world.
|
||||
if (internalEnvironment.updateOutput()) externalOutputChanged.set(true);
|
||||
|
||||
// Set output changed if the terminal has changed from blinking to not
|
||||
var blinking = terminal.getCursorBlink() &&
|
||||
terminal.getCursorX() >= 0 && terminal.getCursorX() < terminal.getWidth() &&
|
||||
terminal.getCursorY() >= 0 && terminal.getCursorY() < terminal.getHeight();
|
||||
if (blinking != this.blinking) {
|
||||
this.blinking = blinking;
|
||||
externalOutputChanged.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
void markChanged() {
|
||||
externalOutputChanged.set(true);
|
||||
}
|
||||
|
||||
public boolean pollAndResetChanged() {
|
||||
return externalOutputChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
public boolean isBlinking() {
|
||||
return isOn() && blinking;
|
||||
}
|
||||
|
||||
public void addApi(ILuaAPI api) {
|
||||
executor.addApi(api);
|
||||
}
|
||||
|
||||
long getUniqueTaskId() {
|
||||
return lastTaskId.incrementAndGet();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
import dan200.computercraft.core.filesystem.FileMount;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public interface ComputerEnvironment {
|
||||
/**
|
||||
* Get the current in-game day.
|
||||
*
|
||||
* @return The current day.
|
||||
*/
|
||||
int getDay();
|
||||
|
||||
/**
|
||||
* Get the current in-game time of day.
|
||||
*
|
||||
* @return The current time.
|
||||
*/
|
||||
double getTimeOfDay();
|
||||
|
||||
/**
|
||||
* Get the {@link MetricsObserver} for this computer. This should be constant for the duration of this
|
||||
* {@link ComputerEnvironment}.
|
||||
*
|
||||
* @return This computer's {@link MetricsObserver}.
|
||||
*/
|
||||
MetricsObserver getMetrics();
|
||||
|
||||
/**
|
||||
* Construct the mount for this computer's user-writable data.
|
||||
*
|
||||
* @return The constructed mount or {@code null} if the mount could not be created.
|
||||
* @see FileMount
|
||||
*/
|
||||
@Nullable
|
||||
IWritableMount createRootMount();
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.apis.*;
|
||||
import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||
import dan200.computercraft.core.lua.ILuaMachine;
|
||||
import dan200.computercraft.core.lua.MachineEnvironment;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* The main task queue and executor for a single computer. This handles turning on and off a computer, as well as
|
||||
* running events.
|
||||
* <p>
|
||||
* When the computer is instructed to turn on or off, or handle an event, we queue a task and register this to be
|
||||
* executed on the {@link ComputerThread}. Note, as we may be starting many events in a single tick, the external
|
||||
* cannot lock on anything which may be held for a long time.
|
||||
* <p>
|
||||
* The executor is effectively composed of two separate queues. Firstly, we have a "single element" queue
|
||||
* {@link #command} which determines which state the computer should transition too. This is set by
|
||||
* {@link #queueStart()} and {@link #queueStop(boolean, boolean)}.
|
||||
* <p>
|
||||
* When a computer is on, we simply push any events onto to the {@link #eventQueue}.
|
||||
* <p>
|
||||
* Both queues are run from the {@link #work()} method, which tries to execute a command if one exists, or resumes the
|
||||
* machine with an event otherwise.
|
||||
* <p>
|
||||
* One final responsibility for the executor is calling {@link ILuaAPI#update()} every tick, via the {@link #tick()}
|
||||
* method. This should only be called when the computer is actually on ({@link #isOn}).
|
||||
*/
|
||||
final class ComputerExecutor {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ComputerExecutor.class);
|
||||
private static final int QUEUE_LIMIT = 256;
|
||||
|
||||
private final Computer computer;
|
||||
private final ComputerEnvironment computerEnvironment;
|
||||
private final MetricsObserver metrics;
|
||||
private final List<ILuaAPI> apis = new ArrayList<>();
|
||||
private final ComputerThread scheduler;
|
||||
final TimeoutState timeout;
|
||||
|
||||
private FileSystem fileSystem;
|
||||
|
||||
private ILuaMachine machine;
|
||||
|
||||
/**
|
||||
* Whether the computer is currently on. This is set to false when a shutdown starts, or when turning on completes
|
||||
* (but just before the Lua machine is started).
|
||||
*
|
||||
* @see #isOnLock
|
||||
*/
|
||||
private volatile boolean isOn = false;
|
||||
|
||||
/**
|
||||
* The lock to acquire when you need to modify the "on state" of a computer.
|
||||
* <p>
|
||||
* We hold this lock when running any command, and attempt to hold it when updating APIs. This ensures you don't
|
||||
* update APIs while also starting/stopping them.
|
||||
*
|
||||
* @see #isOn
|
||||
* @see #tick()
|
||||
* @see #turnOn()
|
||||
* @see #shutdown()
|
||||
*/
|
||||
private final ReentrantLock isOnLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* A lock used for any changes to {@link #eventQueue}, {@link #command} or {@link #onComputerQueue}. This will be
|
||||
* used on the main thread, so locks should be kept as brief as possible.
|
||||
*/
|
||||
private final Object queueLock = new Object();
|
||||
|
||||
/**
|
||||
* Determines if this executor is present within {@link ComputerThread}.
|
||||
*
|
||||
* @see #queueLock
|
||||
* @see #enqueue()
|
||||
* @see #afterWork()
|
||||
*/
|
||||
volatile boolean onComputerQueue = false;
|
||||
|
||||
/**
|
||||
* The amount of time this computer has used on a theoretical machine which shares work evenly amongst computers.
|
||||
*
|
||||
* @see ComputerThread
|
||||
*/
|
||||
long virtualRuntime = 0;
|
||||
|
||||
/**
|
||||
* The last time at which we updated {@link #virtualRuntime}.
|
||||
*
|
||||
* @see ComputerThread
|
||||
*/
|
||||
long vRuntimeStart;
|
||||
|
||||
/**
|
||||
* The command that {@link #work()} should execute on the computer thread.
|
||||
* <p>
|
||||
* One sets the command with {@link #queueStart()} and {@link #queueStop(boolean, boolean)}. Neither of these will
|
||||
* queue a new event if there is an existing one in the queue.
|
||||
* <p>
|
||||
* Note, if command is not {@code null}, then some command is scheduled to be executed. Otherwise it is not
|
||||
* currently in the queue (or is currently being executed).
|
||||
*/
|
||||
private volatile StateCommand command;
|
||||
|
||||
/**
|
||||
* The queue of events which should be executed when this computer is on.
|
||||
* <p>
|
||||
* Note, this should be empty if this computer is off - it is cleared on shutdown and when turning on again.
|
||||
*/
|
||||
private final Queue<Event> eventQueue = new ArrayDeque<>(4);
|
||||
|
||||
/**
|
||||
* Whether we interrupted an event and so should resume it instead of executing another task.
|
||||
*
|
||||
* @see #work()
|
||||
* @see #resumeMachine(String, Object[])
|
||||
*/
|
||||
private boolean interruptedEvent = false;
|
||||
|
||||
/**
|
||||
* Whether this executor has been closed, and will no longer accept any incoming commands or events.
|
||||
*
|
||||
* @see #queueStop(boolean, boolean)
|
||||
*/
|
||||
private boolean closed;
|
||||
|
||||
private IWritableMount rootMount;
|
||||
|
||||
/**
|
||||
* The thread the executor is running on. This is non-null when performing work. We use this to ensure we're only
|
||||
* doing one bit of work at one time.
|
||||
*
|
||||
* @see ComputerThread
|
||||
*/
|
||||
final AtomicReference<Thread> executingThread = new AtomicReference<>();
|
||||
|
||||
private final ILuaMachine.Factory luaFactory;
|
||||
|
||||
ComputerExecutor(Computer computer, ComputerEnvironment computerEnvironment, ComputerContext context) {
|
||||
this.computer = computer;
|
||||
this.computerEnvironment = computerEnvironment;
|
||||
metrics = computerEnvironment.getMetrics();
|
||||
luaFactory = context.luaFactory();
|
||||
scheduler = context.computerScheduler();
|
||||
timeout = new TimeoutState(scheduler);
|
||||
|
||||
var environment = computer.getEnvironment();
|
||||
|
||||
// Add all default APIs to the loaded list.
|
||||
apis.add(new TermAPI(environment));
|
||||
apis.add(new RedstoneAPI(environment));
|
||||
apis.add(new FSAPI(environment));
|
||||
apis.add(new PeripheralAPI(environment));
|
||||
apis.add(new OSAPI(environment));
|
||||
if (CoreConfig.httpEnabled) apis.add(new HTTPAPI(environment));
|
||||
|
||||
// Load in the externally registered APIs.
|
||||
for (var factory : ApiFactories.getAll()) {
|
||||
var system = new ComputerSystem(environment);
|
||||
var api = factory.create(system);
|
||||
if (api != null) apis.add(new ApiWrapper(api, system));
|
||||
}
|
||||
}
|
||||
|
||||
boolean isOn() {
|
||||
return isOn;
|
||||
}
|
||||
|
||||
FileSystem getFileSystem() {
|
||||
return fileSystem;
|
||||
}
|
||||
|
||||
Computer getComputer() {
|
||||
return computer;
|
||||
}
|
||||
|
||||
void addApi(ILuaAPI api) {
|
||||
apis.add(api);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule this computer to be started if not already on.
|
||||
*/
|
||||
void queueStart() {
|
||||
synchronized (queueLock) {
|
||||
// We should only schedule a start if we're not currently on and there's turn on.
|
||||
if (closed || isOn || command != null) return;
|
||||
|
||||
command = StateCommand.TURN_ON;
|
||||
enqueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule this computer to be stopped if not already on.
|
||||
*
|
||||
* @param reboot Reboot the computer after stopping
|
||||
* @param close Close the computer after stopping.
|
||||
* @see #closed
|
||||
*/
|
||||
void queueStop(boolean reboot, boolean close) {
|
||||
synchronized (queueLock) {
|
||||
if (closed) return;
|
||||
closed = close;
|
||||
|
||||
var newCommand = reboot ? StateCommand.REBOOT : StateCommand.SHUTDOWN;
|
||||
|
||||
// We should only schedule a stop if we're currently on and there's no shutdown pending.
|
||||
if (!isOn || command != null) {
|
||||
// If we're closing, set the command just in case.
|
||||
if (close) command = newCommand;
|
||||
return;
|
||||
}
|
||||
|
||||
command = newCommand;
|
||||
enqueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort this whole computer due to a timeout. This will immediately destroy the Lua machine,
|
||||
* and then schedule a shutdown.
|
||||
*/
|
||||
void abort() {
|
||||
immediateFail(StateCommand.ABORT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort this whole computer due to an internal error. This will immediately destroy the Lua machine,
|
||||
* and then schedule a shutdown.
|
||||
*/
|
||||
void fastFail() {
|
||||
immediateFail(StateCommand.ERROR);
|
||||
}
|
||||
|
||||
private void immediateFail(StateCommand command) {
|
||||
var machine = this.machine;
|
||||
if (machine != null) machine.close();
|
||||
|
||||
synchronized (queueLock) {
|
||||
if (closed) return;
|
||||
this.command = command;
|
||||
if (isOn) enqueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue an event if the computer is on.
|
||||
*
|
||||
* @param event The event's name
|
||||
* @param args The event's arguments
|
||||
*/
|
||||
void queueEvent(@Nonnull String event, @Nullable Object[] args) {
|
||||
// Events should be skipped if we're not on.
|
||||
if (!isOn) return;
|
||||
|
||||
synchronized (queueLock) {
|
||||
// And if we've got some command in the pipeline, then don't queue events - they'll
|
||||
// probably be disposed of anyway.
|
||||
// We also limit the number of events which can be queued.
|
||||
if (closed || command != null || eventQueue.size() >= QUEUE_LIMIT) return;
|
||||
|
||||
eventQueue.offer(new Event(event, args));
|
||||
enqueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add this executor to the {@link ComputerThread} if not already there.
|
||||
*/
|
||||
private void enqueue() {
|
||||
synchronized (queueLock) {
|
||||
if (!onComputerQueue) scheduler.queue(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the internals of the executor.
|
||||
*/
|
||||
void tick() {
|
||||
if (isOn && isOnLock.tryLock()) {
|
||||
// This horrific structure means we don't try to update APIs while the state is being changed
|
||||
// (and so they may be running startup/shutdown).
|
||||
// We use tryLock here, as it has minimal delay, and it doesn't matter if we miss an advance at the
|
||||
// beginning or end of a computer's lifetime.
|
||||
try {
|
||||
if (isOn) {
|
||||
// Advance our APIs.
|
||||
for (var api : apis) api.update();
|
||||
}
|
||||
} finally {
|
||||
isOnLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IMount getRomMount() {
|
||||
return computer.getGlobalEnvironment().createResourceMount("computercraft", "lua/rom");
|
||||
}
|
||||
|
||||
private IWritableMount getRootMount() {
|
||||
if (rootMount == null) rootMount = computerEnvironment.createRootMount();
|
||||
return rootMount;
|
||||
}
|
||||
|
||||
private FileSystem createFileSystem() {
|
||||
FileSystem filesystem = null;
|
||||
try {
|
||||
filesystem = new FileSystem("hdd", getRootMount());
|
||||
|
||||
var romMount = getRomMount();
|
||||
if (romMount == null) {
|
||||
displayFailure("Cannot mount ROM", null);
|
||||
return null;
|
||||
}
|
||||
|
||||
filesystem.mount("rom", "rom", romMount);
|
||||
return filesystem;
|
||||
} catch (FileSystemException e) {
|
||||
if (filesystem != null) filesystem.close();
|
||||
LOG.error("Cannot mount computer filesystem", e);
|
||||
|
||||
displayFailure("Cannot mount computer system", null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ILuaMachine createLuaMachine() {
|
||||
// Load the bios resource
|
||||
InputStream biosStream = null;
|
||||
try {
|
||||
biosStream = computer.getGlobalEnvironment().createResourceFile("computercraft", "lua/bios.lua");
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (biosStream == null) {
|
||||
displayFailure("Error loading bios.lua", null);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the lua machine
|
||||
var machine = luaFactory.create(new MachineEnvironment(
|
||||
new LuaContext(computer), metrics, timeout, computer.getGlobalEnvironment().getHostString()
|
||||
));
|
||||
|
||||
// Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object.
|
||||
for (var api : apis) machine.addAPI(api instanceof ApiWrapper wrapper ? wrapper.getDelegate() : api);
|
||||
|
||||
// Start the machine running the bios resource
|
||||
var result = machine.loadBios(biosStream);
|
||||
IoUtil.closeQuietly(biosStream);
|
||||
|
||||
if (result.isError()) {
|
||||
machine.close();
|
||||
displayFailure("Error loading bios.lua", result.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
return machine;
|
||||
}
|
||||
|
||||
private void turnOn() throws InterruptedException {
|
||||
isOnLock.lockInterruptibly();
|
||||
try {
|
||||
// Reset the terminal and event queue
|
||||
computer.getTerminal().reset();
|
||||
interruptedEvent = false;
|
||||
synchronized (queueLock) {
|
||||
eventQueue.clear();
|
||||
}
|
||||
|
||||
// Init filesystem
|
||||
if ((fileSystem = createFileSystem()) == null) {
|
||||
shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// Init APIs
|
||||
computer.getEnvironment().reset();
|
||||
for (var api : apis) api.startup();
|
||||
|
||||
// Init lua
|
||||
if ((machine = createLuaMachine()) == null) {
|
||||
shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialisation has finished, so let's mark ourselves as on.
|
||||
isOn = true;
|
||||
computer.markChanged();
|
||||
} finally {
|
||||
isOnLock.unlock();
|
||||
}
|
||||
|
||||
// Now actually start the computer, now that everything is set up.
|
||||
resumeMachine(null, null);
|
||||
}
|
||||
|
||||
private void shutdown() throws InterruptedException {
|
||||
isOnLock.lockInterruptibly();
|
||||
try {
|
||||
isOn = false;
|
||||
interruptedEvent = false;
|
||||
synchronized (queueLock) {
|
||||
eventQueue.clear();
|
||||
}
|
||||
|
||||
// Shutdown Lua machine
|
||||
if (machine != null) {
|
||||
machine.close();
|
||||
machine = null;
|
||||
}
|
||||
|
||||
// Shutdown our APIs
|
||||
for (var api : apis) api.shutdown();
|
||||
computer.getEnvironment().reset();
|
||||
|
||||
// Unload filesystem
|
||||
if (fileSystem != null) {
|
||||
fileSystem.close();
|
||||
fileSystem = null;
|
||||
}
|
||||
|
||||
computer.getEnvironment().resetOutput();
|
||||
computer.markChanged();
|
||||
} finally {
|
||||
isOnLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before calling {@link #work()}, setting up any important state.
|
||||
*/
|
||||
void beforeWork() {
|
||||
vRuntimeStart = System.nanoTime();
|
||||
timeout.startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after executing {@link #work()}.
|
||||
*
|
||||
* @return If we have more work to do.
|
||||
*/
|
||||
boolean afterWork() {
|
||||
if (interruptedEvent) {
|
||||
timeout.pauseTimer();
|
||||
} else {
|
||||
timeout.stopTimer();
|
||||
}
|
||||
|
||||
metrics.observe(Metrics.COMPUTER_TASKS, timeout.nanoCurrent());
|
||||
|
||||
if (interruptedEvent) return true;
|
||||
|
||||
synchronized (queueLock) {
|
||||
if (eventQueue.isEmpty() && command == null) return onComputerQueue = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main worker function, called by {@link ComputerThread}.
|
||||
* <p>
|
||||
* This either executes a {@link StateCommand} or attempts to run an event
|
||||
*
|
||||
* @throws InterruptedException If various locks could not be acquired.
|
||||
* @see #command
|
||||
* @see #eventQueue
|
||||
*/
|
||||
void work() throws InterruptedException {
|
||||
if (interruptedEvent && !closed) {
|
||||
interruptedEvent = false;
|
||||
if (machine != null) {
|
||||
resumeMachine(null, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
StateCommand command;
|
||||
Event event = null;
|
||||
synchronized (queueLock) {
|
||||
command = this.command;
|
||||
this.command = null;
|
||||
|
||||
// If we've no command, pull something from the event queue instead.
|
||||
if (command == null) {
|
||||
if (!isOn) {
|
||||
// We're not on and had no command, but we had work queued. This should never happen, so clear
|
||||
// the event queue just in case.
|
||||
eventQueue.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
event = eventQueue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
if (command != null) {
|
||||
switch (command) {
|
||||
case TURN_ON -> {
|
||||
if (isOn) return;
|
||||
turnOn();
|
||||
}
|
||||
case SHUTDOWN -> {
|
||||
if (!isOn) return;
|
||||
computer.getTerminal().reset();
|
||||
shutdown();
|
||||
}
|
||||
case REBOOT -> {
|
||||
if (!isOn) return;
|
||||
computer.getTerminal().reset();
|
||||
shutdown();
|
||||
computer.turnOn();
|
||||
}
|
||||
case ABORT -> {
|
||||
if (!isOn) return;
|
||||
displayFailure("Error running computer", TimeoutState.ABORT_MESSAGE);
|
||||
shutdown();
|
||||
}
|
||||
case ERROR -> {
|
||||
if (!isOn) return;
|
||||
displayFailure("Error running computer", "An internal error occurred, see logs.");
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
} else if (event != null) {
|
||||
resumeMachine(event.name, event.args);
|
||||
}
|
||||
}
|
||||
|
||||
void printState(StringBuilder out) {
|
||||
out.append("Enqueued command: ").append(command).append('\n');
|
||||
out.append("Enqueued events: ").append(eventQueue.size()).append('\n');
|
||||
|
||||
var machine = this.machine;
|
||||
if (machine != null) machine.printExecutionState(out);
|
||||
}
|
||||
|
||||
private void displayFailure(String message, String extra) {
|
||||
var terminal = computer.getTerminal();
|
||||
terminal.reset();
|
||||
|
||||
// Display our primary error message
|
||||
if (terminal.isColour()) terminal.setTextColour(15 - Colour.RED.ordinal());
|
||||
terminal.write(message);
|
||||
|
||||
if (extra != null) {
|
||||
// Display any additional information. This generally comes from the Lua Machine, such as compilation or
|
||||
// runtime errors.
|
||||
terminal.setCursorPos(0, terminal.getCursorY() + 1);
|
||||
terminal.write(extra);
|
||||
}
|
||||
|
||||
// And display our generic "CC may be installed incorrectly" message.
|
||||
terminal.setCursorPos(0, terminal.getCursorY() + 1);
|
||||
if (terminal.isColour()) terminal.setTextColour(15 - Colour.WHITE.ordinal());
|
||||
terminal.write("ComputerCraft may be installed incorrectly");
|
||||
}
|
||||
|
||||
private void resumeMachine(String event, Object[] args) throws InterruptedException {
|
||||
var result = machine.handleEvent(event, args);
|
||||
interruptedEvent = result.isPause();
|
||||
if (!result.isError()) return;
|
||||
|
||||
displayFailure("Error running computer", result.getMessage());
|
||||
shutdown();
|
||||
}
|
||||
|
||||
private enum StateCommand {
|
||||
TURN_ON,
|
||||
SHUTDOWN,
|
||||
REBOOT,
|
||||
ABORT,
|
||||
ERROR,
|
||||
}
|
||||
|
||||
private record Event(String name, Object[] args) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A side on a computer. This is relative to the direction the computer is facing.
|
||||
*/
|
||||
public enum ComputerSide {
|
||||
BOTTOM("bottom"),
|
||||
TOP("top"),
|
||||
BACK("back"),
|
||||
FRONT("front"),
|
||||
RIGHT("right"),
|
||||
LEFT("left");
|
||||
|
||||
public static final String[] NAMES = new String[]{ "bottom", "top", "back", "front", "right", "left" };
|
||||
|
||||
public static final int COUNT = 6;
|
||||
|
||||
private static final ComputerSide[] VALUES = values();
|
||||
|
||||
private final String name;
|
||||
|
||||
ComputerSide(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static ComputerSide valueOf(int side) {
|
||||
return VALUES[side];
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ComputerSide valueOfInsensitive(@Nonnull String name) {
|
||||
for (var side : VALUES) {
|
||||
if (side.name.equalsIgnoreCase(name)) return side;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.api.filesystem.IFileSystem;
|
||||
import dan200.computercraft.api.lua.IComputerSystem;
|
||||
import dan200.computercraft.api.lua.ILuaAPIFactory;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.core.apis.ComputerAccess;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Implementation of {@link IComputerAccess}/{@link IComputerSystem} for usage by externally registered APIs.
|
||||
*
|
||||
* @see dan200.computercraft.api.ComputerCraftAPI#registerAPIFactory(ILuaAPIFactory)
|
||||
* @see ILuaAPIFactory
|
||||
* @see ApiWrapper
|
||||
*/
|
||||
public class ComputerSystem extends ComputerAccess implements IComputerSystem {
|
||||
private final IAPIEnvironment environment;
|
||||
|
||||
ComputerSystem(IAPIEnvironment environment) {
|
||||
super(environment);
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String getAttachmentName() {
|
||||
return "computer";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IFileSystem getFileSystem() {
|
||||
var fs = environment.getFileSystem();
|
||||
return fs == null ? null : fs.getMountWrapper();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return environment.getLabel();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Map<String, IPeripheral> getAvailablePeripherals() {
|
||||
// TODO: Should this return peripherals on the current computer?
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IPeripheral getAvailablePeripheral(@Nonnull String name) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import dan200.computercraft.core.ComputerContext;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.util.ThreadUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Runs all scheduled tasks for computers in a {@link ComputerContext}.
|
||||
* <p>
|
||||
* This acts as an over-complicated {@link ThreadPoolExecutor}: It creates several {@link Worker} threads which pull
|
||||
* tasks from a shared queue, executing them. It also creates a single {@link Monitor} thread, which updates computer
|
||||
* timeouts, killing workers if they have not been terminated by {@link TimeoutState#isSoftAborted()}.
|
||||
* <p>
|
||||
* Computers are executed using a priority system, with those who have spent less time executing having a higher
|
||||
* priority than those hogging the thread. This, combined with {@link TimeoutState#isPaused()} means we can reduce the
|
||||
* risk of badly behaved computers stalling execution for everyone else.
|
||||
* <p>
|
||||
* This is done using an implementation of Linux's Completely Fair Scheduler. When a computer executes, we compute what
|
||||
* share of execution time it has used (time executed/number of tasks). We then pick the computer who has the least
|
||||
* "virtual execution time" (aka {@link ComputerExecutor#virtualRuntime}).
|
||||
* <p>
|
||||
* When adding a computer to the queue, we make sure its "virtual runtime" is at least as big as the smallest runtime.
|
||||
* This means that adding computers which have slept a lot do not then have massive priority over everyone else. See
|
||||
* {@link #queue(ComputerExecutor)} for how this is implemented.
|
||||
* <p>
|
||||
* In reality, it's unlikely that more than a few computers are waiting to execute at once, so this will not have much
|
||||
* effect unless you have a computer hogging execution time. However, it is pretty effective in those situations.
|
||||
*
|
||||
* @see TimeoutState For how hard timeouts are handled.
|
||||
* @see ComputerExecutor For how computers actually do execution.
|
||||
*/
|
||||
public final class ComputerThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ComputerThread.class);
|
||||
private static final ThreadFactory monitorFactory = ThreadUtils.factory("Computer-Monitor");
|
||||
private static final ThreadFactory workerFactory = ThreadUtils.factory("Computer-Worker");
|
||||
|
||||
/**
|
||||
* How often the computer thread monitor should run.
|
||||
*
|
||||
* @see Monitor
|
||||
*/
|
||||
private static final long MONITOR_WAKEUP = TimeUnit.MILLISECONDS.toNanos(100);
|
||||
|
||||
/**
|
||||
* The target latency between executing two tasks on a single machine.
|
||||
* <p>
|
||||
* An average tick takes 50ms, and so we ideally need to have handled a couple of events within that window in order
|
||||
* to have a perceived low latency.
|
||||
*/
|
||||
private static final long DEFAULT_LATENCY = TimeUnit.MILLISECONDS.toNanos(50);
|
||||
|
||||
/**
|
||||
* The minimum value that {@link #DEFAULT_LATENCY} can have when scaled.
|
||||
* <p>
|
||||
* From statistics gathered on SwitchCraft, almost all machines will execute under 15ms, 75% under 1.5ms, with the
|
||||
* mean being about 3ms. Most computers shouldn't be too impacted with having such a short period to execute in.
|
||||
*/
|
||||
private static final long DEFAULT_MIN_PERIOD = TimeUnit.MILLISECONDS.toNanos(5);
|
||||
|
||||
/**
|
||||
* The maximum number of tasks before we have to start scaling latency linearly.
|
||||
*/
|
||||
private static final long LATENCY_MAX_TASKS = DEFAULT_LATENCY / DEFAULT_MIN_PERIOD;
|
||||
|
||||
/**
|
||||
* Time difference between reporting crashed threads.
|
||||
*
|
||||
* @see Worker#reportTimeout(ComputerExecutor, long)
|
||||
*/
|
||||
private static final long REPORT_DEBOUNCE = TimeUnit.SECONDS.toNanos(1);
|
||||
|
||||
/**
|
||||
* Lock used for modifications to the array of current threads.
|
||||
*/
|
||||
private final ReentrantLock threadLock = new ReentrantLock();
|
||||
|
||||
private static final int RUNNING = 0;
|
||||
private static final int STOPPING = 1;
|
||||
private static final int CLOSED = 2;
|
||||
|
||||
/**
|
||||
* Whether the computer thread system is currently running.
|
||||
*/
|
||||
private final AtomicInteger state = new AtomicInteger(RUNNING);
|
||||
|
||||
/**
|
||||
* The current task manager.
|
||||
*/
|
||||
private @Nullable Thread monitor;
|
||||
|
||||
/**
|
||||
* The array of current workers, and their owning threads.
|
||||
*/
|
||||
@GuardedBy("threadLock")
|
||||
private final Worker[] workers;
|
||||
|
||||
/**
|
||||
* The number of workers in {@link #workers}.
|
||||
*/
|
||||
@GuardedBy("threadLock")
|
||||
private int workerCount = 0;
|
||||
|
||||
private final Condition shutdown = threadLock.newCondition();
|
||||
|
||||
private final long latency;
|
||||
private final long minPeriod;
|
||||
|
||||
private final ReentrantLock computerLock = new ReentrantLock();
|
||||
private final Condition workerWakeup = computerLock.newCondition();
|
||||
private final Condition monitorWakeup = computerLock.newCondition();
|
||||
|
||||
private final AtomicInteger idleWorkers = new AtomicInteger(0);
|
||||
|
||||
/**
|
||||
* Active queues to execute.
|
||||
*/
|
||||
private final TreeSet<ComputerExecutor> computerQueue = new TreeSet<>((a, b) -> {
|
||||
if (a == b) return 0; // Should never happen, but let's be consistent here
|
||||
|
||||
long at = a.virtualRuntime, bt = b.virtualRuntime;
|
||||
if (at == bt) return Integer.compare(a.hashCode(), b.hashCode());
|
||||
return at < bt ? -1 : 1;
|
||||
});
|
||||
|
||||
/**
|
||||
* The minimum {@link ComputerExecutor#virtualRuntime} time on the tree.
|
||||
*/
|
||||
private long minimumVirtualRuntime = 0;
|
||||
|
||||
public ComputerThread(int threadCount) {
|
||||
workers = new Worker[threadCount];
|
||||
|
||||
// latency and minPeriod are scaled by 1 + floor(log2(threads)). We can afford to execute tasks for
|
||||
// longer when executing on more than one thread.
|
||||
var factor = 64 - Long.numberOfLeadingZeros(workers.length);
|
||||
latency = DEFAULT_LATENCY * factor;
|
||||
minPeriod = DEFAULT_MIN_PERIOD * factor;
|
||||
}
|
||||
|
||||
@GuardedBy("threadLock")
|
||||
private void addWorker(int index) {
|
||||
LOG.trace("Spawning new worker {}.", index);
|
||||
(workers[index] = new Worker(index)).owner.start();
|
||||
workerCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure sufficient workers are running.
|
||||
*/
|
||||
@GuardedBy("computerLock")
|
||||
private void ensureRunning() {
|
||||
// Don't even enter the lock if we've a monitor and don't need to/can't spawn an additional worker.
|
||||
// We'll be holding the computer lock at this point, so there's no problems with idleWorkers being wrong.
|
||||
if (monitor != null && (idleWorkers.get() > 0 || workerCount == workers.length)) return;
|
||||
|
||||
threadLock.lock();
|
||||
try {
|
||||
LOG.trace("Possibly spawning a worker or monitor.");
|
||||
|
||||
if (monitor == null || !monitor.isAlive()) (monitor = monitorFactory.newThread(new Monitor())).start();
|
||||
if (idleWorkers.get() == 0 || workerCount < workers.length) {
|
||||
for (var i = 0; i < workers.length; i++) {
|
||||
if (workers[i] == null) {
|
||||
addWorker(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
threadLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void advanceState(int newState) {
|
||||
while (true) {
|
||||
var current = state.get();
|
||||
if (current >= newState || state.compareAndSet(current, newState)) break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to stop the computer thread. This interrupts each worker, and clears the task queue.
|
||||
*
|
||||
* @param timeout The maximum time to wait.
|
||||
* @param unit The unit {@code timeout} is in.
|
||||
* @return Whether the thread was successfully shut down.
|
||||
* @throws InterruptedException If interrupted while waiting.
|
||||
*/
|
||||
public boolean stop(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
advanceState(STOPPING);
|
||||
|
||||
// Encourage any currently running runners to terminate.
|
||||
threadLock.lock();
|
||||
try {
|
||||
for (@Nullable var worker : workers) {
|
||||
if (worker == null) continue;
|
||||
|
||||
var executor = worker.currentExecutor.get();
|
||||
if (executor != null) executor.timeout.hardAbort();
|
||||
}
|
||||
} finally {
|
||||
threadLock.unlock();
|
||||
}
|
||||
|
||||
// Wake all workers
|
||||
computerLock.lock();
|
||||
try {
|
||||
workerWakeup.signalAll();
|
||||
} finally {
|
||||
computerLock.unlock();
|
||||
}
|
||||
|
||||
// Wait for all workers to signal they have finished.
|
||||
var timeoutNs = unit.toNanos(timeout);
|
||||
threadLock.lock();
|
||||
try {
|
||||
while (workerCount > 0) {
|
||||
if (timeoutNs <= 0) return false;
|
||||
timeoutNs = shutdown.awaitNanos(timeoutNs);
|
||||
}
|
||||
} finally {
|
||||
threadLock.unlock();
|
||||
}
|
||||
|
||||
advanceState(CLOSED);
|
||||
|
||||
// Signal the monitor to finish, but don't wait for it to stop.
|
||||
computerLock.lock();
|
||||
try {
|
||||
monitorWakeup.signal();
|
||||
} finally {
|
||||
computerLock.unlock();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a computer as having work, enqueuing it on the thread.
|
||||
* <p>
|
||||
* You must be holding {@link ComputerExecutor}'s {@code queueLock} when calling this method - it should only
|
||||
* be called from {@code enqueue}.
|
||||
*
|
||||
* @param executor The computer to execute work on.
|
||||
*/
|
||||
void queue(ComputerExecutor executor) {
|
||||
computerLock.lock();
|
||||
try {
|
||||
if (state.get() != RUNNING) throw new IllegalStateException("ComputerThread is no longer running");
|
||||
|
||||
// Ensure we've got a worker running.
|
||||
ensureRunning();
|
||||
|
||||
if (executor.onComputerQueue) throw new IllegalStateException("Cannot queue already queued executor");
|
||||
executor.onComputerQueue = true;
|
||||
|
||||
updateRuntimes(null);
|
||||
|
||||
// We're not currently on the queue, so update its current execution time to
|
||||
// ensure its at least as high as the minimum.
|
||||
var newRuntime = minimumVirtualRuntime;
|
||||
|
||||
if (executor.virtualRuntime == 0) {
|
||||
// Slow down new computers a little bit.
|
||||
newRuntime += scaledPeriod();
|
||||
} else {
|
||||
// Give a small boost to computers which have slept a little.
|
||||
newRuntime -= latency / 2;
|
||||
}
|
||||
|
||||
executor.virtualRuntime = Math.max(newRuntime, executor.virtualRuntime);
|
||||
|
||||
var wasBusy = isBusy();
|
||||
// Add to the queue, and signal the workers.
|
||||
computerQueue.add(executor);
|
||||
workerWakeup.signal();
|
||||
|
||||
// If we've transitioned into a busy state, notify the monitor. This will cause it to sleep for scaledPeriod
|
||||
// instead of the longer wakeup duration.
|
||||
if (!wasBusy && isBusy()) monitorWakeup.signal();
|
||||
} finally {
|
||||
computerLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the {@link ComputerExecutor#virtualRuntime}s of all running tasks, and then update the
|
||||
* {@link #minimumVirtualRuntime} based on the current tasks.
|
||||
* <p>
|
||||
* This is called before queueing tasks, to ensure that {@link #minimumVirtualRuntime} is up-to-date.
|
||||
*
|
||||
* @param current The machine which we updating runtimes from.
|
||||
*/
|
||||
private void updateRuntimes(@Nullable ComputerExecutor current) {
|
||||
var minRuntime = Long.MAX_VALUE;
|
||||
|
||||
// If we've a task on the queue, use that as our base time.
|
||||
if (!computerQueue.isEmpty()) minRuntime = computerQueue.first().virtualRuntime;
|
||||
|
||||
// Update all the currently executing tasks
|
||||
var now = System.nanoTime();
|
||||
var tasks = 1 + computerQueue.size();
|
||||
for (@Nullable var runner : workers) {
|
||||
if (runner == null) continue;
|
||||
var executor = runner.currentExecutor.get();
|
||||
if (executor == null) continue;
|
||||
|
||||
// We do two things here: first we update the task's virtual runtime based on when we
|
||||
// last checked, and then we check the minimum.
|
||||
minRuntime = Math.min(minRuntime, executor.virtualRuntime += (now - executor.vRuntimeStart) / tasks);
|
||||
executor.vRuntimeStart = now;
|
||||
}
|
||||
|
||||
// And update the most recently executed one (if set).
|
||||
if (current != null) {
|
||||
minRuntime = Math.min(minRuntime, current.virtualRuntime += (now - current.vRuntimeStart) / tasks);
|
||||
}
|
||||
|
||||
if (minRuntime > minimumVirtualRuntime && minRuntime < Long.MAX_VALUE) {
|
||||
minimumVirtualRuntime = minRuntime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the "currently working" state of the executor is reset, the timings are updated, and then requeue the
|
||||
* executor if needed.
|
||||
*
|
||||
* @param runner The runner this task was on.
|
||||
* @param executor The executor to requeue
|
||||
*/
|
||||
private void afterWork(Worker runner, ComputerExecutor executor) {
|
||||
// Clear the executor's thread.
|
||||
var currentThread = executor.executingThread.getAndSet(null);
|
||||
if (currentThread != runner.owner) {
|
||||
|
||||
LOG.error(
|
||||
"Expected computer #{} to be running on {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
|
||||
executor.getComputer().getID(),
|
||||
runner.owner.getName(),
|
||||
currentThread == null ? "nothing" : currentThread.getName()
|
||||
);
|
||||
}
|
||||
|
||||
computerLock.lock();
|
||||
try {
|
||||
updateRuntimes(executor);
|
||||
|
||||
// If we've no more tasks, just return.
|
||||
if (!executor.afterWork() || state.get() != RUNNING) return;
|
||||
|
||||
// Otherwise, add to the queue, and signal any waiting workers.
|
||||
computerQueue.add(executor);
|
||||
workerWakeup.signal();
|
||||
} finally {
|
||||
computerLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The scaled period for a single task.
|
||||
*
|
||||
* @return The scaled period for the task
|
||||
* @see #DEFAULT_LATENCY
|
||||
* @see #DEFAULT_MIN_PERIOD
|
||||
* @see #LATENCY_MAX_TASKS
|
||||
*/
|
||||
long scaledPeriod() {
|
||||
// FIXME: We access this on other threads (in TimeoutState), so their reads won't be consistent. This isn't
|
||||
// "criticial" behaviour, so not clear if it matters too much.
|
||||
|
||||
// +1 to include the current task
|
||||
var count = 1 + computerQueue.size();
|
||||
return count < LATENCY_MAX_TASKS ? latency / count : minPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the thread has computers queued up.
|
||||
*
|
||||
* @return If we have work queued up.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public boolean hasPendingWork() {
|
||||
// FIXME: See comment in scaledPeriod. Again, we access this in multiple threads but not clear if it matters!
|
||||
return !computerQueue.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have more work queued than we have capacity for. Effectively a more fine-grained version of
|
||||
* {@link #hasPendingWork()}.
|
||||
*
|
||||
* @return If the computer threads are busy.
|
||||
*/
|
||||
@GuardedBy("computerLock")
|
||||
private boolean isBusy() {
|
||||
return computerQueue.size() > idleWorkers.get();
|
||||
}
|
||||
|
||||
private void workerFinished(Worker worker) {
|
||||
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
|
||||
// worker finishes normally.
|
||||
if (!worker.running.getAndSet(false)) return;
|
||||
|
||||
LOG.trace("Worker {} finished.", worker.index);
|
||||
|
||||
var executor = worker.currentExecutor.getAndSet(null);
|
||||
if (executor != null) executor.afterWork();
|
||||
|
||||
threadLock.lock();
|
||||
try {
|
||||
workerCount--;
|
||||
|
||||
if (workers[worker.index] != worker) {
|
||||
LOG.error("Worker {} closed, but new runner has been spawned.", worker.index);
|
||||
} else if (state.get() == RUNNING || (state.get() == STOPPING && hasPendingWork())) {
|
||||
addWorker(worker.index);
|
||||
workerCount++;
|
||||
} else {
|
||||
workers[worker.index] = null;
|
||||
}
|
||||
} finally {
|
||||
threadLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes all currently active {@link Worker}s and terminates their tasks once they have exceeded the hard
|
||||
* abort limit.
|
||||
*
|
||||
* @see TimeoutState
|
||||
*/
|
||||
private final class Monitor implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.trace("Monitor starting.");
|
||||
try {
|
||||
runImpl();
|
||||
} finally {
|
||||
LOG.trace("Monitor shutting down. Current state is {}.", state.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void runImpl() {
|
||||
while (state.get() < CLOSED) {
|
||||
computerLock.lock();
|
||||
try {
|
||||
// If we've got more work than we have capacity for it, then we'll need to pause a task soon, so
|
||||
// sleep for a single pause duration. Otherwise we only need to wake up to set the soft/hard abort
|
||||
// flags, which are far less granular.
|
||||
monitorWakeup.awaitNanos(isBusy() ? scaledPeriod() : MONITOR_WAKEUP);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.error("Monitor thread interrupted. Computers may behave very badly!", e);
|
||||
break;
|
||||
} finally {
|
||||
computerLock.unlock();
|
||||
}
|
||||
|
||||
checkRunners();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRunners() {
|
||||
for (@Nullable var runner : workers) {
|
||||
if (runner == null) continue;
|
||||
|
||||
// If the worker has no work, skip
|
||||
var executor = runner.currentExecutor.get();
|
||||
if (executor == null) continue;
|
||||
|
||||
// Refresh the timeout state. Will set the pause/soft timeout flags as appropriate.
|
||||
executor.timeout.refresh();
|
||||
|
||||
// If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT),
|
||||
// then we can let the Lua machine do its work.
|
||||
var afterStart = executor.timeout.nanoCumulative();
|
||||
var afterHardAbort = afterStart - TimeoutState.TIMEOUT - TimeoutState.ABORT_TIMEOUT;
|
||||
if (afterHardAbort < 0) continue;
|
||||
|
||||
// Set the hard abort flag.
|
||||
executor.timeout.hardAbort();
|
||||
executor.abort();
|
||||
|
||||
if (afterHardAbort >= TimeoutState.ABORT_TIMEOUT * 2) {
|
||||
// If we've hard aborted and interrupted, and we're still not dead, then mark the worker
|
||||
// as dead, finish off the task, and spawn a new runner.
|
||||
runner.reportTimeout(executor, afterStart);
|
||||
runner.owner.interrupt();
|
||||
|
||||
workerFinished(runner);
|
||||
} else if (afterHardAbort >= TimeoutState.ABORT_TIMEOUT) {
|
||||
// If we've hard aborted but we're still not dead, dump the stack trace and interrupt
|
||||
// the task.
|
||||
runner.reportTimeout(executor, afterStart);
|
||||
runner.owner.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls tasks from the {@link #computerQueue} queue and runs them.
|
||||
* <p>
|
||||
* This is responsible for running the {@link ComputerExecutor#work()}, {@link ComputerExecutor#beforeWork()} and
|
||||
* {@link ComputerExecutor#afterWork()} functions. Everything else is either handled by the executor, timeout
|
||||
* state or monitor.
|
||||
*/
|
||||
private final class Worker implements Runnable {
|
||||
/**
|
||||
* The index into the {@link #workers} array.
|
||||
*/
|
||||
final int index;
|
||||
|
||||
/**
|
||||
* The thread this runner runs on.
|
||||
*/
|
||||
final @Nonnull Thread owner;
|
||||
|
||||
/**
|
||||
* Whether this runner is currently executing. This may be set to false when this worker terminates, or when
|
||||
* we try to abandon a worker in the monitor
|
||||
*
|
||||
* @see #workerFinished(Worker)
|
||||
*/
|
||||
final AtomicBoolean running = new AtomicBoolean(true);
|
||||
|
||||
/**
|
||||
* The computer we're currently running.
|
||||
*/
|
||||
final AtomicReference<ComputerExecutor> currentExecutor = new AtomicReference<>(null);
|
||||
|
||||
/**
|
||||
* The last time we reported a stack trace, used to avoid spamming the logs.
|
||||
*/
|
||||
AtomicLong lastReport = new AtomicLong(Long.MIN_VALUE);
|
||||
|
||||
Worker(int index) {
|
||||
this.index = index;
|
||||
owner = workerFactory.newThread(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
runImpl();
|
||||
} finally {
|
||||
workerFinished(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void runImpl() {
|
||||
tasks:
|
||||
while (running.get()) {
|
||||
// Wait for an active queue to execute
|
||||
ComputerExecutor executor;
|
||||
computerLock.lock();
|
||||
try {
|
||||
idleWorkers.getAndIncrement();
|
||||
while ((executor = computerQueue.pollFirst()) == null) {
|
||||
if (state.get() >= STOPPING) return;
|
||||
|
||||
// We should never interrupt() the worker, so this should be fine.
|
||||
workerWakeup.awaitUninterruptibly();
|
||||
}
|
||||
} finally {
|
||||
idleWorkers.getAndDecrement();
|
||||
computerLock.unlock();
|
||||
}
|
||||
|
||||
// If we're trying to executing some task on this computer while someone else is doing work, something
|
||||
// is seriously wrong.
|
||||
while (!executor.executingThread.compareAndSet(null, owner)) {
|
||||
var existing = executor.executingThread.get();
|
||||
if (existing != null) {
|
||||
LOG.error(
|
||||
"Trying to run computer #{} on thread {}, but already running on {}. This is a SERIOUS bug, please report with your debug.log.",
|
||||
executor.getComputer().getID(), owner.getName(), existing.getName()
|
||||
);
|
||||
continue tasks;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're stopping, the only thing this executor should be doing is shutting down.
|
||||
if (state.get() >= STOPPING) executor.queueStop(false, true);
|
||||
|
||||
// Reset the timers
|
||||
executor.beforeWork();
|
||||
|
||||
// And then set the current executor. It's important to do it afterwards, as otherwise we introduce
|
||||
// race conditions with the monitor.
|
||||
currentExecutor.set(executor);
|
||||
|
||||
// Execute the task
|
||||
try {
|
||||
executor.work();
|
||||
} catch (Exception | LinkageError | VirtualMachineError e) {
|
||||
LOG.error("Error running task on computer #" + executor.getComputer().getID(), e);
|
||||
// Tear down the computer immediately. There's no guarantee it's well-behaved from now on.
|
||||
executor.fastFail();
|
||||
} finally {
|
||||
var thisExecutor = currentExecutor.getAndSet(null);
|
||||
if (thisExecutor != null) afterWork(this, executor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reportTimeout(ComputerExecutor executor, long time) {
|
||||
if (!LOG.isErrorEnabled(Logging.COMPUTER_ERROR)) return;
|
||||
|
||||
// Attempt to debounce stack trace reporting, limiting ourselves to one every second. There's no need to be
|
||||
// ultra-precise in our atomics, as long as one of them wins!
|
||||
var now = System.nanoTime();
|
||||
var then = lastReport.get();
|
||||
if (then != Long.MIN_VALUE && now - then - REPORT_DEBOUNCE <= 0) return;
|
||||
if (!lastReport.compareAndSet(then, now)) return;
|
||||
|
||||
var owner = Objects.requireNonNull(this.owner);
|
||||
|
||||
var builder = new StringBuilder()
|
||||
.append("Terminating computer #").append(executor.getComputer().getID())
|
||||
.append(" due to timeout (running for ").append(time * 1e-9)
|
||||
.append(" seconds). This is NOT a bug, but may mean a computer is misbehaving.\n")
|
||||
.append("Thread ")
|
||||
.append(owner.getName())
|
||||
.append(" is currently ")
|
||||
.append(owner.getState())
|
||||
.append('\n');
|
||||
var blocking = LockSupport.getBlocker(owner);
|
||||
if (blocking != null) builder.append(" on ").append(blocking).append('\n');
|
||||
|
||||
for (var element : owner.getStackTrace()) {
|
||||
builder.append(" at ").append(element).append('\n');
|
||||
}
|
||||
|
||||
executor.printState(builder);
|
||||
|
||||
LOG.warn(builder.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.peripheral.IWorkMonitor;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.filesystem.FileSystem;
|
||||
import dan200.computercraft.core.metrics.Metric;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
import dan200.computercraft.core.terminal.Terminal;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Represents the "environment" that a {@link Computer} exists in.
|
||||
* <p>
|
||||
* This handles storing and updating of peripherals and redstone.
|
||||
*
|
||||
* <h1>Redstone</h1>
|
||||
* We holds three kinds of arrays for redstone, in normal and bundled versions:
|
||||
* <ul>
|
||||
* <li>{@link #internalOutput} is the redstone output which the computer has currently set. This is read on both
|
||||
* threads, and written on the computer thread.</li>
|
||||
* <li>{@link #externalOutput} is the redstone output currently propagated to the world. This is only read and written
|
||||
* on the main thread.</li>
|
||||
* <li>{@link #input} is the redstone input from external sources. This is read on both threads, and written on the main
|
||||
* thread.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h1>Peripheral</h1>
|
||||
* We also keep track of peripherals. These are read on both threads, and only written on the main thread.
|
||||
*/
|
||||
public final class Environment implements IAPIEnvironment {
|
||||
private final Computer computer;
|
||||
private final ComputerEnvironment environment;
|
||||
private final MetricsObserver metrics;
|
||||
|
||||
private boolean internalOutputChanged = false;
|
||||
private final int[] internalOutput = new int[ComputerSide.COUNT];
|
||||
private final int[] internalBundledOutput = new int[ComputerSide.COUNT];
|
||||
|
||||
private final int[] externalOutput = new int[ComputerSide.COUNT];
|
||||
private final int[] externalBundledOutput = new int[ComputerSide.COUNT];
|
||||
|
||||
private boolean inputChanged = false;
|
||||
private final int[] input = new int[ComputerSide.COUNT];
|
||||
private final int[] bundledInput = new int[ComputerSide.COUNT];
|
||||
|
||||
private final IPeripheral[] peripherals = new IPeripheral[ComputerSide.COUNT];
|
||||
private IPeripheralChangeListener peripheralListener = null;
|
||||
|
||||
private final Int2ObjectMap<Timer> timers = new Int2ObjectOpenHashMap<>();
|
||||
private int nextTimerToken = 0;
|
||||
|
||||
Environment(Computer computer, ComputerEnvironment environment) {
|
||||
this.computer = computer;
|
||||
this.environment = environment;
|
||||
metrics = environment.getMetrics();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getComputerID() {
|
||||
return computer.getID();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ComputerEnvironment getComputerEnvironment() {
|
||||
return environment;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public GlobalEnvironment getGlobalEnvironment() {
|
||||
return computer.getGlobalEnvironment();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public IWorkMonitor getMainThreadMonitor() {
|
||||
return computer.getMainThreadMonitor();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Terminal getTerminal() {
|
||||
return computer.getTerminal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileSystem getFileSystem() {
|
||||
return computer.getFileSystem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
computer.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reboot() {
|
||||
computer.reboot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEvent(String event, Object... args) {
|
||||
computer.queueEvent(event, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInput(ComputerSide side) {
|
||||
return input[side.ordinal()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBundledInput(ComputerSide side) {
|
||||
return bundledInput[side.ordinal()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutput(ComputerSide side, int output) {
|
||||
var index = side.ordinal();
|
||||
synchronized (internalOutput) {
|
||||
if (internalOutput[index] != output) {
|
||||
internalOutput[index] = output;
|
||||
internalOutputChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutput(ComputerSide side) {
|
||||
synchronized (internalOutput) {
|
||||
return computer.isOn() ? internalOutput[side.ordinal()] : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBundledOutput(ComputerSide side, int output) {
|
||||
var index = side.ordinal();
|
||||
synchronized (internalOutput) {
|
||||
if (internalBundledOutput[index] != output) {
|
||||
internalBundledOutput[index] = output;
|
||||
internalOutputChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBundledOutput(ComputerSide side) {
|
||||
synchronized (internalOutput) {
|
||||
return computer.isOn() ? internalBundledOutput[side.ordinal()] : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int getExternalRedstoneOutput(ComputerSide side) {
|
||||
return computer.isOn() ? externalOutput[side.ordinal()] : 0;
|
||||
}
|
||||
|
||||
public int getExternalBundledRedstoneOutput(ComputerSide side) {
|
||||
return computer.isOn() ? externalBundledOutput[side.ordinal()] : 0;
|
||||
}
|
||||
|
||||
public void setRedstoneInput(ComputerSide side, int level) {
|
||||
var index = side.ordinal();
|
||||
if (input[index] != level) {
|
||||
input[index] = level;
|
||||
inputChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void setBundledRedstoneInput(ComputerSide side, int combination) {
|
||||
var index = side.ordinal();
|
||||
if (bundledInput[index] != combination) {
|
||||
bundledInput[index] = combination;
|
||||
inputChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the computer starts up or shuts down, to reset any internal state.
|
||||
*
|
||||
* @see ILuaAPI#startup()
|
||||
* @see ILuaAPI#shutdown()
|
||||
*/
|
||||
void reset() {
|
||||
synchronized (timers) {
|
||||
timers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on the main thread to update the internal state of the computer.
|
||||
*/
|
||||
void tick() {
|
||||
if (inputChanged) {
|
||||
inputChanged = false;
|
||||
queueEvent("redstone");
|
||||
}
|
||||
|
||||
synchronized (timers) {
|
||||
// Countdown all of our active timers
|
||||
Iterator<Int2ObjectMap.Entry<Timer>> it = timers.int2ObjectEntrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
var entry = it.next();
|
||||
var timer = entry.getValue();
|
||||
timer.ticksLeft--;
|
||||
if (timer.ticksLeft <= 0) {
|
||||
// Queue the "timer" event
|
||||
queueEvent(TIMER_EVENT, entry.getIntKey());
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on the main thread to propagate the internal outputs to the external ones.
|
||||
*
|
||||
* @return If the outputs have changed.
|
||||
*/
|
||||
boolean updateOutput() {
|
||||
// Mark output as changed if the internal redstone has changed
|
||||
synchronized (internalOutput) {
|
||||
if (!internalOutputChanged) return false;
|
||||
|
||||
var changed = false;
|
||||
|
||||
for (var i = 0; i < ComputerSide.COUNT; i++) {
|
||||
if (externalOutput[i] != internalOutput[i]) {
|
||||
externalOutput[i] = internalOutput[i];
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (externalBundledOutput[i] != internalBundledOutput[i]) {
|
||||
externalBundledOutput[i] = internalBundledOutput[i];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
internalOutputChanged = false;
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
void resetOutput() {
|
||||
// Reset redstone output
|
||||
synchronized (internalOutput) {
|
||||
Arrays.fill(internalOutput, 0);
|
||||
Arrays.fill(internalBundledOutput, 0);
|
||||
internalOutputChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPeripheral getPeripheral(ComputerSide side) {
|
||||
synchronized (peripherals) {
|
||||
return peripherals[side.ordinal()];
|
||||
}
|
||||
}
|
||||
|
||||
public void setPeripheral(ComputerSide side, IPeripheral peripheral) {
|
||||
synchronized (peripherals) {
|
||||
var index = side.ordinal();
|
||||
var existing = peripherals[index];
|
||||
if ((existing == null && peripheral != null) ||
|
||||
(existing != null && peripheral == null) ||
|
||||
(existing != null && !existing.equals(peripheral))) {
|
||||
peripherals[index] = peripheral;
|
||||
if (peripheralListener != null) peripheralListener.onPeripheralChanged(side, peripheral);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPeripheralChangeListener(IPeripheralChangeListener listener) {
|
||||
synchronized (peripherals) {
|
||||
peripheralListener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return computer.getLabel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLabel(String label) {
|
||||
computer.setLabel(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int startTimer(long ticks) {
|
||||
synchronized (timers) {
|
||||
timers.put(nextTimerToken, new Timer(ticks));
|
||||
return nextTimerToken++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelTimer(int id) {
|
||||
synchronized (timers) {
|
||||
timers.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observe(@Nonnull Metric.Event event, long change) {
|
||||
metrics.observe(event, change);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void observe(@Nonnull Metric.Counter counter) {
|
||||
metrics.observe(counter);
|
||||
}
|
||||
|
||||
private static class Timer {
|
||||
long ticksLeft;
|
||||
|
||||
Timer(long ticksLeft) {
|
||||
this.ticksLeft = ticksLeft;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* The global environment in which computers reside.
|
||||
*/
|
||||
public interface GlobalEnvironment {
|
||||
/**
|
||||
* Get a "host" string describing the program hosting CC. It should be of the form {@literal ComputerCraft
|
||||
* $CC_VERSION ($HOST)}, where {@literal $HOST} is a user-defined string such as {@literal Minecraft 1.19}.
|
||||
*
|
||||
* @return The host string.
|
||||
*/
|
||||
String getHostString();
|
||||
|
||||
/**
|
||||
* Get the HTTP user-agent to use for requests. This should be similar to {@link #getHostString()} , but in the form
|
||||
* of a HTTP User-Agent.
|
||||
*
|
||||
* @return The HTTP
|
||||
*/
|
||||
String getUserAgent();
|
||||
|
||||
/**
|
||||
* Create a mount from mod-provided resources.
|
||||
*
|
||||
* @param domain The domain (i.e. mod id) providing resources.
|
||||
* @param subPath The path to these resources under the domain.
|
||||
* @return The created mount or {@code null} if it could not be created.
|
||||
*/
|
||||
@Nullable
|
||||
IMount createResourceMount(String domain, String subPath);
|
||||
|
||||
/**
|
||||
* Open a single mod-provided file.
|
||||
*
|
||||
* @param domain The domain (i.e. mod id) providing resources.
|
||||
* @param subPath The path to these files under the domain.
|
||||
* @return The opened file or {@code null} if it could not be opened.
|
||||
*/
|
||||
@Nullable
|
||||
InputStream createResourceFile(String domain, String subPath);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.ILuaTask;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
class LuaContext implements ILuaContext {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LuaContext.class);
|
||||
private final Computer computer;
|
||||
|
||||
LuaContext(Computer computer) {
|
||||
this.computer = computer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long issueMainThreadTask(@Nonnull final ILuaTask task) throws LuaException {
|
||||
// Issue command
|
||||
final var taskID = computer.getUniqueTaskId();
|
||||
final Runnable iTask = () -> {
|
||||
try {
|
||||
var results = task.execute();
|
||||
if (results != null) {
|
||||
var eventArguments = new Object[results.length + 2];
|
||||
eventArguments[0] = taskID;
|
||||
eventArguments[1] = true;
|
||||
System.arraycopy(results, 0, eventArguments, 2, results.length);
|
||||
computer.queueEvent("task_complete", eventArguments);
|
||||
} else {
|
||||
computer.queueEvent("task_complete", new Object[]{ taskID, true });
|
||||
}
|
||||
} catch (LuaException e) {
|
||||
computer.queueEvent("task_complete", new Object[]{ taskID, false, e.getMessage() });
|
||||
} catch (Exception t) {
|
||||
LOG.error(Logging.JAVA_ERROR, "Error running task", t);
|
||||
computer.queueEvent("task_complete", new Object[]{
|
||||
taskID, false, "Java Exception Thrown: " + t,
|
||||
});
|
||||
}
|
||||
};
|
||||
if (computer.queueMainThread(iTask)) {
|
||||
return taskID;
|
||||
} else {
|
||||
throw new LuaException("Task limit exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer;
|
||||
|
||||
import dan200.computercraft.core.lua.ILuaMachine;
|
||||
import dan200.computercraft.core.lua.MachineResult;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Used to measure how long a computer has executed for, and thus the relevant timeout states.
|
||||
* <p>
|
||||
* Timeouts are mostly used for execution of Lua code: we should ideally never have a state where constructing the APIs
|
||||
* or machines themselves takes more than a fraction of a second.
|
||||
* <p>
|
||||
* When a computer runs, it is allowed to run for 7 seconds ({@link #TIMEOUT}). After that point, the "soft abort" flag
|
||||
* is set ({@link #isSoftAborted()}). Here, the Lua machine will attempt to abort the program in some safe manner
|
||||
* (namely, throwing a "Too long without yielding" error).
|
||||
* <p>
|
||||
* Now, if a computer still does not stop after that period, they're behaving really badly. 1.5 seconds after a soft
|
||||
* abort ({@link #ABORT_TIMEOUT}), we trigger a hard abort (note, this is done from the computer thread manager). This
|
||||
* will destroy the entire Lua runtime and shut the computer down.
|
||||
* <p>
|
||||
* The Lua runtime is also allowed to pause execution if there are other computers contesting for work. All computers
|
||||
* are allowed to run for {@link ComputerThread#scaledPeriod()} nanoseconds (see {@link #currentDeadline}). After that
|
||||
* period, if any computers are waiting to be executed then we'll set the paused flag to true ({@link #isPaused()}.
|
||||
*
|
||||
* @see ComputerThread
|
||||
* @see ILuaMachine
|
||||
* @see MachineResult#isPause()
|
||||
*/
|
||||
public final class TimeoutState {
|
||||
/**
|
||||
* The total time a task is allowed to run before aborting in nanoseconds.
|
||||
*/
|
||||
static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos(7000);
|
||||
|
||||
/**
|
||||
* The time the task is allowed to run after each abort in nanoseconds.
|
||||
*/
|
||||
static final long ABORT_TIMEOUT = TimeUnit.MILLISECONDS.toNanos(1500);
|
||||
|
||||
/**
|
||||
* The error message to display when we trigger an abort.
|
||||
*/
|
||||
public static final String ABORT_MESSAGE = "Too long without yielding";
|
||||
|
||||
private final ComputerThread scheduler;
|
||||
|
||||
private boolean paused;
|
||||
private boolean softAbort;
|
||||
private volatile boolean hardAbort;
|
||||
|
||||
/**
|
||||
* When the cumulative time would have started had the whole event been processed in one go.
|
||||
*/
|
||||
private long cumulativeStart;
|
||||
|
||||
/**
|
||||
* How much cumulative time has elapsed. This is effectively {@code cumulativeStart - currentStart}.
|
||||
*/
|
||||
private long cumulativeElapsed;
|
||||
|
||||
/**
|
||||
* When this execution round started.
|
||||
*/
|
||||
private long currentStart;
|
||||
|
||||
/**
|
||||
* When this execution round should look potentially be paused.
|
||||
*/
|
||||
private long currentDeadline;
|
||||
|
||||
public TimeoutState(ComputerThread scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
long nanoCumulative() {
|
||||
return System.nanoTime() - cumulativeStart;
|
||||
}
|
||||
|
||||
long nanoCurrent() {
|
||||
return System.nanoTime() - currentStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute the {@link #isSoftAborted()} and {@link #isPaused()} flags.
|
||||
*/
|
||||
public synchronized void refresh() {
|
||||
// Important: The weird arithmetic here is important, as nanoTime may return negative values, and so we
|
||||
// need to handle overflow.
|
||||
var now = System.nanoTime();
|
||||
if (!paused) paused = currentDeadline - now <= 0 && scheduler.hasPendingWork(); // now >= currentDeadline
|
||||
if (!softAbort) softAbort = now - cumulativeStart - TIMEOUT >= 0; // now - cumulativeStart >= TIMEOUT
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we should pause execution of this machine.
|
||||
* <p>
|
||||
* This is determined by whether we've consumed our time slice, and if there are other computers waiting to perform
|
||||
* work.
|
||||
*
|
||||
* @return Whether we should pause execution.
|
||||
*/
|
||||
public boolean isPaused() {
|
||||
return paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the machine should be passively aborted.
|
||||
*
|
||||
* @return {@code true} if we should throw a timeout error.
|
||||
*/
|
||||
public boolean isSoftAborted() {
|
||||
return softAbort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the machine should be forcibly aborted.
|
||||
*
|
||||
* @return {@code true} if the machine should be forcibly shut down.
|
||||
*/
|
||||
public boolean isHardAborted() {
|
||||
return hardAbort;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the machine should be forcibly aborted.
|
||||
*/
|
||||
void hardAbort() {
|
||||
softAbort = hardAbort = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the current and cumulative timers again.
|
||||
*/
|
||||
void startTimer() {
|
||||
var now = System.nanoTime();
|
||||
currentStart = now;
|
||||
currentDeadline = now + scheduler.scaledPeriod();
|
||||
// Compute the "nominal start time".
|
||||
cumulativeStart = now - cumulativeElapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the cumulative time, to be resumed by {@link #startTimer()}.
|
||||
*
|
||||
* @see #nanoCumulative()
|
||||
*/
|
||||
synchronized void pauseTimer() {
|
||||
// We set the cumulative time to difference between current time and "nominal start time".
|
||||
cumulativeElapsed = System.nanoTime() - cumulativeStart;
|
||||
paused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cumulative time and resets the abort flags.
|
||||
*/
|
||||
synchronized void stopTimer() {
|
||||
cumulativeElapsed = 0;
|
||||
paused = softAbort = hardAbort = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer.mainthread;
|
||||
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Runs tasks on the main (server) thread, ticks {@link MainThreadExecutor}s, and limits how much time is used this
|
||||
* tick.
|
||||
* <p>
|
||||
* Similar to {@link MainThreadExecutor}, the {@link MainThread} can be in one of three states: cool, hot and cooling.
|
||||
* However, the implementation here is a little different:
|
||||
* <p>
|
||||
* {@link MainThread} starts cool, and runs as many tasks as it can in the current {@link #budget}ns. Any external tasks
|
||||
* (those run by tile entities, etc...) will also consume the budget
|
||||
* <p>
|
||||
* Next tick, we add {@link CoreConfig#maxMainGlobalTime} to our budget (clamp it to that value too). If we're still
|
||||
* over budget, then we should not execute <em>any</em> work (either as part of {@link MainThread} or externally).
|
||||
*/
|
||||
public final class MainThread implements MainThreadScheduler {
|
||||
/**
|
||||
* The queue of {@link MainThreadExecutor}s with tasks to perform.
|
||||
*/
|
||||
private final TreeSet<MainThreadExecutor> executors = new TreeSet<>((a, b) -> {
|
||||
if (a == b) return 0; // Should never happen, but let's be consistent here
|
||||
|
||||
long at = a.virtualTime, bt = b.virtualTime;
|
||||
if (at == bt) return Integer.compare(a.hashCode(), b.hashCode());
|
||||
return at < bt ? -1 : 1;
|
||||
});
|
||||
|
||||
/**
|
||||
* The set of executors which went over budget in a previous tick, and are waiting for their time to run down.
|
||||
*
|
||||
* @see MainThreadExecutor#tickCooling()
|
||||
* @see #cooling(MainThreadExecutor)
|
||||
*/
|
||||
private final HashSet<MainThreadExecutor> cooling = new HashSet<>();
|
||||
|
||||
/**
|
||||
* The current tick number. This is used by {@link MainThreadExecutor} to determine when to reset its own time
|
||||
* counter.
|
||||
*
|
||||
* @see #currentTick()
|
||||
*/
|
||||
private int currentTick;
|
||||
|
||||
/**
|
||||
* The remaining budgeted time for this tick. This may be negative, in the case that we've gone over budget.
|
||||
*/
|
||||
private long budget;
|
||||
|
||||
/**
|
||||
* Whether we should be executing any work this tick.
|
||||
* <p>
|
||||
* This is true iff {@code MAX_TICK_TIME - currentTime} was true <em>at the beginning of the tick</em>.
|
||||
*/
|
||||
private boolean canExecute = true;
|
||||
|
||||
private long minimumTime = 0;
|
||||
|
||||
public MainThread() {
|
||||
}
|
||||
|
||||
void queue(MainThreadExecutor executor) {
|
||||
synchronized (executors) {
|
||||
if (executor.onQueue) throw new IllegalStateException("Cannot queue already queued executor");
|
||||
executor.onQueue = true;
|
||||
executor.updateTime();
|
||||
|
||||
// We're not currently on the queue, so update its current execution time to
|
||||
// ensure it's at least as high as the minimum.
|
||||
var newRuntime = minimumTime;
|
||||
|
||||
// Slow down new computers a little bit.
|
||||
if (executor.virtualTime == 0) newRuntime += CoreConfig.maxMainComputerTime;
|
||||
|
||||
executor.virtualTime = Math.max(newRuntime, executor.virtualTime);
|
||||
|
||||
executors.add(executor);
|
||||
}
|
||||
}
|
||||
|
||||
void cooling(MainThreadExecutor executor) {
|
||||
cooling.add(executor);
|
||||
}
|
||||
|
||||
void consumeTime(long time) {
|
||||
budget -= time;
|
||||
}
|
||||
|
||||
boolean canExecute() {
|
||||
return canExecute;
|
||||
}
|
||||
|
||||
int currentTick() {
|
||||
return currentTick;
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
// Move onto the next tick and cool down the global executor. We're allowed to execute if we have _any_ time
|
||||
// allocated for this tick. This means we'll stick much closer to doing MAX_TICK_TIME work every tick.
|
||||
//
|
||||
// Of course, we'll go over the MAX_TICK_TIME most of the time, but eventually that overrun will accumulate
|
||||
// and we'll skip a whole tick - bringing the average back down again.
|
||||
currentTick++;
|
||||
budget = Math.min(budget + CoreConfig.maxMainGlobalTime, CoreConfig.maxMainGlobalTime);
|
||||
canExecute = budget > 0;
|
||||
|
||||
// Cool down any warm computers.
|
||||
cooling.removeIf(MainThreadExecutor::tickCooling);
|
||||
|
||||
if (!canExecute) return;
|
||||
|
||||
// Run until we meet the deadline.
|
||||
var start = System.nanoTime();
|
||||
var deadline = start + budget;
|
||||
while (true) {
|
||||
MainThreadExecutor executor;
|
||||
synchronized (executors) {
|
||||
executor = executors.pollFirst();
|
||||
}
|
||||
if (executor == null) break;
|
||||
|
||||
var taskStart = System.nanoTime();
|
||||
executor.execute();
|
||||
|
||||
var taskStop = System.nanoTime();
|
||||
synchronized (executors) {
|
||||
if (executor.afterExecute(taskStop - taskStart)) executors.add(executor);
|
||||
|
||||
// Compute the new minimum time (including the next task on the queue too). Note that this may also include
|
||||
// time spent in external tasks.
|
||||
var newMinimum = executor.virtualTime;
|
||||
if (!executors.isEmpty()) {
|
||||
var next = executors.first();
|
||||
if (next.virtualTime < newMinimum) newMinimum = next.virtualTime;
|
||||
}
|
||||
minimumTime = Math.max(minimumTime, newMinimum);
|
||||
}
|
||||
|
||||
if (taskStop >= deadline) break;
|
||||
}
|
||||
|
||||
consumeTime(System.nanoTime() - start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Executor createExecutor(MetricsObserver metrics) {
|
||||
return new MainThreadExecutor(metrics, this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer.mainthread;
|
||||
|
||||
import dan200.computercraft.api.peripheral.IWorkMonitor;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.computer.Computer;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Keeps track of tasks that a {@link Computer} should run on the main thread and how long that has been spent executing
|
||||
* them.
|
||||
* <p>
|
||||
* This provides rate-limiting mechanism for tasks enqueued with {@link Computer#queueMainThread(Runnable)}, but also
|
||||
* those run elsewhere (such as during the turtle's tick). In order to handle this, the executor goes through three
|
||||
* stages:
|
||||
* <p>
|
||||
* When {@link State#COOL}, the computer is allocated {@link CoreConfig#maxMainComputerTime}ns to execute any work
|
||||
* this tick. At the beginning of the tick, we execute as many {@link MainThread} tasks as possible, until our
|
||||
* time-frame or the global time frame has expired.
|
||||
* <p>
|
||||
* Then, when other objects (such as block entities or entities) are ticked, we update how much time we've used via
|
||||
* {@link IWorkMonitor#trackWork(long, TimeUnit)}.
|
||||
* <p>
|
||||
* Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as
|
||||
* {@link State#HOT}. This means it will no longer be able to execute {@link MainThread} tasks (though will still
|
||||
* execute tile entity tasks, in order to prevent the main thread from exhausting work every tick).
|
||||
* <p>
|
||||
* At the beginning of the next tick, we increment the budget e by {@link CoreConfig#maxMainComputerTime} and any
|
||||
* {@link State#HOT} executors are marked as {@link State#COOLING}. They will remain cooling until their budget is fully
|
||||
* replenished (is equal to {@link CoreConfig#maxMainComputerTime}). Note, this is different to {@link MainThread},
|
||||
* which allows running when it has any budget left. When cooling, <em>no</em> tasks are executed - be they on the tile
|
||||
* entity or main thread.
|
||||
* <p>
|
||||
* This mechanism means that, on average, computers will use at most {@link CoreConfig#maxMainComputerTime}ns per
|
||||
* second, but one task source will not prevent others from executing.
|
||||
*
|
||||
* @see MainThread
|
||||
* @see IWorkMonitor
|
||||
* @see Computer#getMainThreadMonitor()
|
||||
* @see Computer#queueMainThread(Runnable)
|
||||
*/
|
||||
final class MainThreadExecutor implements MainThreadScheduler.Executor {
|
||||
/**
|
||||
* The maximum number of {@link MainThread} tasks allowed on the queue.
|
||||
*/
|
||||
private static final int MAX_TASKS = 5000;
|
||||
|
||||
private final MetricsObserver metrics;
|
||||
|
||||
/**
|
||||
* A lock used for any changes to {@link #tasks}, or {@link #onQueue}. This will be
|
||||
* used on the main thread, so locks should be kept as brief as possible.
|
||||
*/
|
||||
private final Object queueLock = new Object();
|
||||
|
||||
/**
|
||||
* The queue of tasks which should be executed.
|
||||
*
|
||||
* @see #queueLock
|
||||
*/
|
||||
private final Queue<Runnable> tasks = new ArrayDeque<>(4);
|
||||
|
||||
/**
|
||||
* Determines if this executor is currently present on the queue.
|
||||
* <p>
|
||||
* This should be true iff {@link #tasks} is non-empty.
|
||||
*
|
||||
* @see #queueLock
|
||||
* @see #enqueue(Runnable)
|
||||
* @see #afterExecute(long)
|
||||
*/
|
||||
volatile boolean onQueue;
|
||||
|
||||
/**
|
||||
* The remaining budgeted time for this tick. This may be negative, in the case that we've gone over budget.
|
||||
*
|
||||
* @see #tickCooling()
|
||||
* @see #consumeTime(long)
|
||||
*/
|
||||
private long budget = 0;
|
||||
|
||||
/**
|
||||
* The last tick that {@link #budget} was updated.
|
||||
*
|
||||
* @see #tickCooling()
|
||||
* @see #consumeTime(long)
|
||||
*/
|
||||
private int currentTick = -1;
|
||||
|
||||
/**
|
||||
* The current state of this executor.
|
||||
*
|
||||
* @see #canWork()
|
||||
*/
|
||||
private State state = State.COOL;
|
||||
|
||||
private long pendingTime;
|
||||
|
||||
long virtualTime;
|
||||
|
||||
private final MainThread scheduler;
|
||||
|
||||
MainThreadExecutor(MetricsObserver metrics, MainThread scheduler) {
|
||||
this.metrics = metrics;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a task onto this executor's queue, pushing it onto the {@link MainThread} if needed.
|
||||
*
|
||||
* @param runnable The task to run on the main thread.
|
||||
* @return Whether this task was enqueued (namely, was there space).
|
||||
*/
|
||||
@Override
|
||||
public boolean enqueue(Runnable runnable) {
|
||||
synchronized (queueLock) {
|
||||
if (tasks.size() >= MAX_TASKS || !tasks.offer(runnable)) return false;
|
||||
if (!onQueue && state == State.COOL) scheduler.queue(this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void execute() {
|
||||
if (state != State.COOL) return;
|
||||
|
||||
Runnable task;
|
||||
synchronized (queueLock) {
|
||||
task = tasks.poll();
|
||||
}
|
||||
|
||||
if (task != null) task.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the time taken to run an {@link #enqueue(Runnable)} task.
|
||||
*
|
||||
* @param time The time some task took to run.
|
||||
* @return Whether this should be added back to the queue.
|
||||
*/
|
||||
boolean afterExecute(long time) {
|
||||
consumeTime(time);
|
||||
|
||||
synchronized (queueLock) {
|
||||
virtualTime += time;
|
||||
updateTime();
|
||||
if (state != State.COOL || tasks.isEmpty()) return onQueue = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether we should execute "external" tasks (ones not part of {@link #tasks}).
|
||||
*
|
||||
* @return Whether we can execute external tasks.
|
||||
*/
|
||||
@Override
|
||||
public boolean canWork() {
|
||||
return state != State.COOLING && scheduler.canExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
return state == State.COOL && scheduler.canExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trackWork(long time, TimeUnit unit) {
|
||||
var nanoTime = unit.toNanos(time);
|
||||
synchronized (queueLock) {
|
||||
pendingTime += nanoTime;
|
||||
}
|
||||
|
||||
consumeTime(nanoTime);
|
||||
scheduler.consumeTime(nanoTime);
|
||||
}
|
||||
|
||||
private void consumeTime(long time) {
|
||||
metrics.observe(Metrics.SERVER_TASKS, time);
|
||||
|
||||
// Reset the budget if moving onto a new tick. We know this is safe, as this will only have happened if
|
||||
// #tickCooling() isn't called, and so we didn't overrun the previous tick.
|
||||
if (currentTick != scheduler.currentTick()) {
|
||||
currentTick = scheduler.currentTick();
|
||||
budget = CoreConfig.maxMainComputerTime;
|
||||
}
|
||||
|
||||
budget -= time;
|
||||
|
||||
// If we've gone over our limit, mark us as having to cool down.
|
||||
if (budget < 0 && state == State.COOL) {
|
||||
state = State.HOT;
|
||||
scheduler.cooling(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move this executor forward one tick, replenishing the budget by {@link CoreConfig#maxMainComputerTime}.
|
||||
*
|
||||
* @return Whether this executor has cooled down, and so is safe to run again.
|
||||
*/
|
||||
boolean tickCooling() {
|
||||
state = State.COOLING;
|
||||
currentTick = scheduler.currentTick();
|
||||
budget = Math.min(budget + CoreConfig.maxMainComputerTime, CoreConfig.maxMainComputerTime);
|
||||
if (budget < CoreConfig.maxMainComputerTime) return false;
|
||||
|
||||
state = State.COOL;
|
||||
synchronized (queueLock) {
|
||||
if (!tasks.isEmpty() && !onQueue) scheduler.queue(this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void updateTime() {
|
||||
virtualTime += pendingTime;
|
||||
pendingTime = 0;
|
||||
}
|
||||
|
||||
private enum State {
|
||||
COOL,
|
||||
HOT,
|
||||
COOLING,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer.mainthread;
|
||||
|
||||
import dan200.computercraft.api.peripheral.IWorkMonitor;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
|
||||
import java.util.OptionalLong;
|
||||
|
||||
/**
|
||||
* A {@link MainThreadScheduler} is responsible for running work on the main thread, for instance the server thread in
|
||||
* Minecraft.
|
||||
*
|
||||
* @see MainThread is the default implementation
|
||||
*/
|
||||
public interface MainThreadScheduler {
|
||||
/**
|
||||
* Create an executor for a computer. This should only be called once for a single computer.
|
||||
*
|
||||
* @param observer A sink for metrics, used to monitor task timings.
|
||||
* @return The executor for this computer.
|
||||
*/
|
||||
Executor createExecutor(MetricsObserver observer);
|
||||
|
||||
/**
|
||||
* An {@link Executor} is responsible for managing scheduled tasks for a single computer.
|
||||
*/
|
||||
interface Executor extends IWorkMonitor {
|
||||
/**
|
||||
* Schedule a task to be run on the main thread. This can be called from any thread.
|
||||
*
|
||||
* @param task The task to schedule.
|
||||
* @return The task ID if the task could be scheduled, or {@link OptionalLong#empty()} if the task failed to
|
||||
* be scheduled.
|
||||
*/
|
||||
boolean enqueue(Runnable task);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.computer.mainthread;
|
||||
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A {@link MainThreadScheduler} which fails when a computer tries to enqueue work.
|
||||
* <p>
|
||||
* This is useful for emulators, where we'll never make any main thread calls.
|
||||
*/
|
||||
public class NoWorkMainThreadScheduler implements MainThreadScheduler {
|
||||
@Override
|
||||
public Executor createExecutor(MetricsObserver observer) {
|
||||
return new ExecutorImpl();
|
||||
}
|
||||
|
||||
private static class ExecutorImpl implements Executor {
|
||||
@Override
|
||||
public boolean enqueue(Runnable task) {
|
||||
throw new IllegalStateException("Cannot schedule tasks");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWork() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void trackWork(long time, @Nonnull TimeUnit unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.Channel;
|
||||
|
||||
/**
|
||||
* Wraps some closeable object such as a buffered writer, and the underlying stream.
|
||||
* <p>
|
||||
* When flushing a buffer before closing, some implementations will not close the buffer if an exception is thrown
|
||||
* this causes us to release the channel, but not actually close it. This wrapper will attempt to close the wrapper (and
|
||||
* so hopefully flush the channel), and then close the underlying channel.
|
||||
*
|
||||
* @param <T> The type of the closeable object to write.
|
||||
*/
|
||||
class ChannelWrapper<T extends Closeable> implements Closeable {
|
||||
private final T wrapper;
|
||||
private final Channel channel;
|
||||
|
||||
ChannelWrapper(T wrapper, Channel channel) {
|
||||
this.wrapper = wrapper;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
wrapper.close();
|
||||
} finally {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
T get() {
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class ComboMount implements IMount {
|
||||
private final IMount[] parts;
|
||||
|
||||
public ComboMount(IMount[] parts) {
|
||||
this.parts = parts;
|
||||
}
|
||||
|
||||
// IMount implementation
|
||||
|
||||
@Override
|
||||
public boolean exists(@Nonnull String path) throws IOException {
|
||||
for (var i = parts.length - 1; i >= 0; --i) {
|
||||
var part = parts[i];
|
||||
if (part.exists(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory(@Nonnull String path) throws IOException {
|
||||
for (var i = parts.length - 1; i >= 0; --i) {
|
||||
var part = parts[i];
|
||||
if (part.isDirectory(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void list(@Nonnull String path, @Nonnull List<String> contents) throws IOException {
|
||||
// Combine the lists from all the mounts
|
||||
List<String> foundFiles = null;
|
||||
var foundDirs = 0;
|
||||
for (var i = parts.length - 1; i >= 0; --i) {
|
||||
var part = parts[i];
|
||||
if (part.exists(path) && part.isDirectory(path)) {
|
||||
if (foundFiles == null) {
|
||||
foundFiles = new ArrayList<>();
|
||||
}
|
||||
part.list(path, foundFiles);
|
||||
foundDirs++;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundDirs == 1) {
|
||||
// We found one directory, so we know it already doesn't contain duplicates
|
||||
contents.addAll(foundFiles);
|
||||
} else if (foundDirs > 1) {
|
||||
// We found multiple directories, so filter for duplicates
|
||||
Set<String> seen = new HashSet<>();
|
||||
for (var file : foundFiles) {
|
||||
if (seen.add(file)) {
|
||||
contents.add(file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new FileOperationException(path, "Not a directory");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(@Nonnull String path) throws IOException {
|
||||
for (var i = parts.length - 1; i >= 0; --i) {
|
||||
var part = parts[i];
|
||||
if (part.exists(path)) {
|
||||
return part.getSize(path);
|
||||
}
|
||||
}
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ReadableByteChannel openForRead(@Nonnull String path) throws IOException {
|
||||
for (var i = parts.length - 1; i >= 0; --i) {
|
||||
var part = parts[i];
|
||||
if (part.exists(path) && !part.isDirectory(path)) {
|
||||
return part.openForRead(path);
|
||||
}
|
||||
}
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public BasicFileAttributes getAttributes(@Nonnull String path) throws IOException {
|
||||
for (var i = parts.length - 1; i >= 0; --i) {
|
||||
var part = parts[i];
|
||||
if (part.exists(path) && !part.isDirectory(path)) {
|
||||
return part.getAttributes(path);
|
||||
}
|
||||
}
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.util.List;
|
||||
|
||||
public class EmptyMount implements IMount {
|
||||
@Override
|
||||
public boolean exists(@Nonnull String path) {
|
||||
return path.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory(@Nonnull String path) {
|
||||
return path.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void list(@Nonnull String path, @Nonnull List<String> contents) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(@Nonnull String path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ReadableByteChannel openForRead(@Nonnull String path) throws IOException {
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.*;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
|
||||
public class FileMount implements IWritableMount {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileMount.class);
|
||||
private static final int MINIMUM_FILE_SIZE = 500;
|
||||
private static final Set<OpenOption> READ_OPTIONS = Collections.singleton(StandardOpenOption.READ);
|
||||
private static final Set<OpenOption> WRITE_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
private static final Set<OpenOption> APPEND_OPTIONS = Sets.newHashSet(StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
|
||||
private class WritableCountingChannel implements WritableByteChannel {
|
||||
|
||||
private final WritableByteChannel inner;
|
||||
long ignoredBytesLeft;
|
||||
|
||||
WritableCountingChannel(WritableByteChannel inner, long bytesToIgnore) {
|
||||
this.inner = inner;
|
||||
ignoredBytesLeft = bytesToIgnore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int write(@Nonnull ByteBuffer b) throws IOException {
|
||||
count(b.remaining());
|
||||
return inner.write(b);
|
||||
}
|
||||
|
||||
void count(long n) throws IOException {
|
||||
ignoredBytesLeft -= n;
|
||||
if (ignoredBytesLeft < 0) {
|
||||
var newBytes = -ignoredBytesLeft;
|
||||
ignoredBytesLeft = 0;
|
||||
|
||||
var bytesLeft = capacity - usedSpace;
|
||||
if (newBytes > bytesLeft) throw new IOException("Out of space");
|
||||
usedSpace += newBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return inner.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
inner.close();
|
||||
}
|
||||
}
|
||||
|
||||
private class SeekableCountingChannel extends WritableCountingChannel implements SeekableByteChannel {
|
||||
private final SeekableByteChannel inner;
|
||||
|
||||
SeekableCountingChannel(SeekableByteChannel inner, long bytesToIgnore) {
|
||||
super(inner, bytesToIgnore);
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel position(long newPosition) throws IOException {
|
||||
if (!isOpen()) throw new ClosedChannelException();
|
||||
if (newPosition < 0) {
|
||||
throw new IllegalArgumentException("Cannot seek before the beginning of the stream");
|
||||
}
|
||||
|
||||
var delta = newPosition - inner.position();
|
||||
if (delta < 0) {
|
||||
ignoredBytesLeft -= delta;
|
||||
} else {
|
||||
count(delta);
|
||||
}
|
||||
|
||||
return inner.position(newPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeekableByteChannel truncate(long size) throws IOException {
|
||||
throw new IOException("Not yet implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(ByteBuffer dst) throws ClosedChannelException {
|
||||
if (!inner.isOpen()) throw new ClosedChannelException();
|
||||
throw new NonReadableChannelException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long position() throws IOException {
|
||||
return inner.position();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() throws IOException {
|
||||
return inner.size();
|
||||
}
|
||||
}
|
||||
|
||||
private final File rootPath;
|
||||
private final long capacity;
|
||||
private long usedSpace;
|
||||
|
||||
public FileMount(File rootPath, long capacity) {
|
||||
this.rootPath = rootPath;
|
||||
this.capacity = capacity + MINIMUM_FILE_SIZE;
|
||||
usedSpace = created() ? measureUsedSpace(this.rootPath) : MINIMUM_FILE_SIZE;
|
||||
}
|
||||
|
||||
// IMount implementation
|
||||
|
||||
@Override
|
||||
public boolean exists(@Nonnull String path) {
|
||||
if (!created()) return path.isEmpty();
|
||||
|
||||
var file = getRealPath(path);
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory(@Nonnull String path) {
|
||||
if (!created()) return path.isEmpty();
|
||||
|
||||
var file = getRealPath(path);
|
||||
return file.exists() && file.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void list(@Nonnull String path, @Nonnull List<String> contents) throws IOException {
|
||||
if (!created()) {
|
||||
if (!path.isEmpty()) throw new FileOperationException(path, "Not a directory");
|
||||
return;
|
||||
}
|
||||
|
||||
var file = getRealPath(path);
|
||||
if (!file.exists() || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
|
||||
|
||||
var paths = file.list();
|
||||
for (var subPath : paths) {
|
||||
if (new File(file, subPath).exists()) contents.add(subPath);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(@Nonnull String path) throws IOException {
|
||||
if (!created()) {
|
||||
if (path.isEmpty()) return 0;
|
||||
} else {
|
||||
var file = getRealPath(path);
|
||||
if (file.exists()) return file.isDirectory() ? 0 : file.length();
|
||||
}
|
||||
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ReadableByteChannel openForRead(@Nonnull String path) throws IOException {
|
||||
if (created()) {
|
||||
var file = getRealPath(path);
|
||||
if (file.exists() && !file.isDirectory()) return FileChannel.open(file.toPath(), READ_OPTIONS);
|
||||
}
|
||||
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public BasicFileAttributes getAttributes(@Nonnull String path) throws IOException {
|
||||
if (created()) {
|
||||
var file = getRealPath(path);
|
||||
if (file.exists()) return Files.readAttributes(file.toPath(), BasicFileAttributes.class);
|
||||
}
|
||||
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
// IWritableMount implementation
|
||||
|
||||
@Override
|
||||
public void makeDirectory(@Nonnull String path) throws IOException {
|
||||
create();
|
||||
var file = getRealPath(path);
|
||||
if (file.exists()) {
|
||||
if (!file.isDirectory()) throw new FileOperationException(path, "File exists");
|
||||
return;
|
||||
}
|
||||
|
||||
var dirsToCreate = 1;
|
||||
var parent = file.getParentFile();
|
||||
while (!parent.exists()) {
|
||||
++dirsToCreate;
|
||||
parent = parent.getParentFile();
|
||||
}
|
||||
|
||||
if (getRemainingSpace() < dirsToCreate * MINIMUM_FILE_SIZE) {
|
||||
throw new FileOperationException(path, "Out of space");
|
||||
}
|
||||
|
||||
if (file.mkdirs()) {
|
||||
usedSpace += dirsToCreate * MINIMUM_FILE_SIZE;
|
||||
} else {
|
||||
throw new FileOperationException(path, "Access denied");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(@Nonnull String path) throws IOException {
|
||||
if (path.isEmpty()) throw new FileOperationException(path, "Access denied");
|
||||
|
||||
if (created()) {
|
||||
var file = getRealPath(path);
|
||||
if (file.exists()) deleteRecursively(file);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteRecursively(File file) throws IOException {
|
||||
// Empty directories first
|
||||
if (file.isDirectory()) {
|
||||
var children = file.list();
|
||||
for (var aChildren : children) {
|
||||
deleteRecursively(new File(file, aChildren));
|
||||
}
|
||||
}
|
||||
|
||||
// Then delete
|
||||
var fileSize = file.isDirectory() ? 0 : file.length();
|
||||
var success = file.delete();
|
||||
if (success) {
|
||||
usedSpace -= Math.max(MINIMUM_FILE_SIZE, fileSize);
|
||||
} else {
|
||||
throw new IOException("Access denied");
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public WritableByteChannel openForWrite(@Nonnull String path) throws IOException {
|
||||
create();
|
||||
var file = getRealPath(path);
|
||||
if (file.exists() && file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
|
||||
|
||||
if (file.exists()) {
|
||||
usedSpace -= Math.max(file.length(), MINIMUM_FILE_SIZE);
|
||||
} else if (getRemainingSpace() < MINIMUM_FILE_SIZE) {
|
||||
throw new FileOperationException(path, "Out of space");
|
||||
}
|
||||
usedSpace += MINIMUM_FILE_SIZE;
|
||||
|
||||
return new SeekableCountingChannel(Files.newByteChannel(file.toPath(), WRITE_OPTIONS), MINIMUM_FILE_SIZE);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public WritableByteChannel openForAppend(@Nonnull String path) throws IOException {
|
||||
if (!created()) {
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
var file = getRealPath(path);
|
||||
if (!file.exists()) throw new FileOperationException(path, "No such file");
|
||||
if (file.isDirectory()) throw new FileOperationException(path, "Cannot write to directory");
|
||||
|
||||
// Allowing seeking when appending is not recommended, so we use a separate channel.
|
||||
return new WritableCountingChannel(
|
||||
Files.newByteChannel(file.toPath(), APPEND_OPTIONS),
|
||||
Math.max(MINIMUM_FILE_SIZE - file.length(), 0)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRemainingSpace() {
|
||||
return Math.max(capacity - usedSpace, 0);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public OptionalLong getCapacity() {
|
||||
return OptionalLong.of(capacity - MINIMUM_FILE_SIZE);
|
||||
}
|
||||
|
||||
private File getRealPath(String path) {
|
||||
return new File(rootPath, path);
|
||||
}
|
||||
|
||||
private boolean created() {
|
||||
return rootPath.exists();
|
||||
}
|
||||
|
||||
private void create() throws IOException {
|
||||
if (!rootPath.exists()) {
|
||||
var success = rootPath.mkdirs();
|
||||
if (!success) {
|
||||
throw new IOException("Access denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Visitor extends SimpleFileVisitor<Path> {
|
||||
long size;
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
|
||||
size += MINIMUM_FILE_SIZE;
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
size += Math.max(attrs.size(), MINIMUM_FILE_SIZE);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) {
|
||||
LOG.error("Error computing file size for {}", file, exc);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
}
|
||||
|
||||
private static long measureUsedSpace(File file) {
|
||||
if (!file.exists()) return 0;
|
||||
|
||||
try {
|
||||
var visitor = new Visitor();
|
||||
Files.walkFileTree(file.toPath(), visitor);
|
||||
return visitor.size;
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error computing file size for {}", file, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import dan200.computercraft.api.filesystem.IFileSystem;
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.channels.Channel;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FileSystem {
|
||||
/**
|
||||
* Maximum depth that {@link #copyRecursive(String, MountWrapper, String, MountWrapper, int)} will descend into.
|
||||
* <p>
|
||||
* This is a pretty arbitrary value, though hopefully it is large enough that it'll never be normally hit. This
|
||||
* exists to prevent it overflowing if it ever gets into an infinite loop.
|
||||
*/
|
||||
private static final int MAX_COPY_DEPTH = 128;
|
||||
|
||||
private final FileSystemWrapperMount wrapper = new FileSystemWrapperMount(this);
|
||||
private final Map<String, MountWrapper> mounts = new HashMap<>();
|
||||
|
||||
private final HashMap<WeakReference<FileSystemWrapper<?>>, ChannelWrapper<?>> openFiles = new HashMap<>();
|
||||
private final ReferenceQueue<FileSystemWrapper<?>> openFileQueue = new ReferenceQueue<>();
|
||||
|
||||
public FileSystem(String rootLabel, IMount rootMount) throws FileSystemException {
|
||||
mount(rootLabel, "", rootMount);
|
||||
}
|
||||
|
||||
public FileSystem(String rootLabel, IWritableMount rootMount) throws FileSystemException {
|
||||
mountWritable(rootLabel, "", rootMount);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
// Close all dangling open files
|
||||
synchronized (openFiles) {
|
||||
for (Closeable file : openFiles.values()) IoUtil.closeQuietly(file);
|
||||
openFiles.clear();
|
||||
while (openFileQueue.poll() != null) ;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void mount(String label, String location, IMount mount) throws FileSystemException {
|
||||
if (mount == null) throw new NullPointerException();
|
||||
location = sanitizePath(location);
|
||||
if (location.contains("..")) throw new FileSystemException("Cannot mount below the root");
|
||||
mount(new MountWrapper(label, location, mount));
|
||||
}
|
||||
|
||||
public synchronized void mountWritable(String label, String location, IWritableMount mount) throws FileSystemException {
|
||||
if (mount == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
location = sanitizePath(location);
|
||||
if (location.contains("..")) {
|
||||
throw new FileSystemException("Cannot mount below the root");
|
||||
}
|
||||
mount(new MountWrapper(label, location, mount));
|
||||
}
|
||||
|
||||
private synchronized void mount(MountWrapper wrapper) {
|
||||
var location = wrapper.getLocation();
|
||||
mounts.remove(location);
|
||||
mounts.put(location, wrapper);
|
||||
}
|
||||
|
||||
public synchronized void unmount(String path) {
|
||||
var mount = mounts.remove(sanitizePath(path));
|
||||
if (mount == null) return;
|
||||
|
||||
cleanup();
|
||||
|
||||
// Close any files which belong to this mount - don't want people writing to a disk after it's been ejected!
|
||||
// There's no point storing a Mount -> Wrapper[] map, as openFiles is small and unmount isn't called very
|
||||
// often.
|
||||
synchronized (openFiles) {
|
||||
for (var iterator = openFiles.keySet().iterator(); iterator.hasNext(); ) {
|
||||
var reference = iterator.next();
|
||||
var wrapper = reference.get();
|
||||
if (wrapper == null) continue;
|
||||
|
||||
if (wrapper.mount == mount) {
|
||||
wrapper.closeExternally();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String combine(String path, String childPath) {
|
||||
path = sanitizePath(path, true);
|
||||
childPath = sanitizePath(childPath, true);
|
||||
|
||||
if (path.isEmpty()) {
|
||||
return childPath;
|
||||
} else if (childPath.isEmpty()) {
|
||||
return path;
|
||||
} else {
|
||||
return sanitizePath(path + '/' + childPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getDirectory(String path) {
|
||||
path = sanitizePath(path, true);
|
||||
if (path.isEmpty()) {
|
||||
return "..";
|
||||
}
|
||||
|
||||
var lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash >= 0) {
|
||||
return path.substring(0, lastSlash);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getName(String path) {
|
||||
path = sanitizePath(path, true);
|
||||
if (path.isEmpty()) return "root";
|
||||
|
||||
var lastSlash = path.lastIndexOf('/');
|
||||
return lastSlash >= 0 ? path.substring(lastSlash + 1) : path;
|
||||
}
|
||||
|
||||
public synchronized long getSize(String path) throws FileSystemException {
|
||||
return getMount(sanitizePath(path)).getSize(sanitizePath(path));
|
||||
}
|
||||
|
||||
public synchronized BasicFileAttributes getAttributes(String path) throws FileSystemException {
|
||||
return getMount(sanitizePath(path)).getAttributes(sanitizePath(path));
|
||||
}
|
||||
|
||||
public synchronized String[] list(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
|
||||
// Gets a list of the files in the mount
|
||||
List<String> list = new ArrayList<>();
|
||||
mount.list(path, list);
|
||||
|
||||
// Add any mounts that are mounted at this location
|
||||
for (var otherMount : mounts.values()) {
|
||||
if (getDirectory(otherMount.getLocation()).equals(path)) {
|
||||
list.add(getName(otherMount.getLocation()));
|
||||
}
|
||||
}
|
||||
|
||||
// Return list
|
||||
var array = new String[list.size()];
|
||||
list.toArray(array);
|
||||
Arrays.sort(array);
|
||||
return array;
|
||||
}
|
||||
|
||||
private void findIn(String dir, List<String> matches, Pattern wildPattern) throws FileSystemException {
|
||||
var list = list(dir);
|
||||
for (var entry : list) {
|
||||
var entryPath = dir.isEmpty() ? entry : dir + "/" + entry;
|
||||
if (wildPattern.matcher(entryPath).matches()) {
|
||||
matches.add(entryPath);
|
||||
}
|
||||
if (isDir(entryPath)) {
|
||||
findIn(entryPath, matches, wildPattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized String[] find(String wildPath) throws FileSystemException {
|
||||
// Match all the files on the system
|
||||
wildPath = sanitizePath(wildPath, true);
|
||||
|
||||
// If we don't have a wildcard at all just check the file exists
|
||||
var starIndex = wildPath.indexOf('*');
|
||||
if (starIndex == -1) {
|
||||
return exists(wildPath) ? new String[]{ wildPath } : new String[0];
|
||||
}
|
||||
|
||||
// Find the all non-wildcarded directories. For instance foo/bar/baz* -> foo/bar
|
||||
var prevDir = wildPath.substring(0, starIndex).lastIndexOf('/');
|
||||
var startDir = prevDir == -1 ? "" : wildPath.substring(0, prevDir);
|
||||
|
||||
// If this isn't a directory then just abort
|
||||
if (!isDir(startDir)) return new String[0];
|
||||
|
||||
// Scan as normal, starting from this directory
|
||||
var wildPattern = Pattern.compile("^\\Q" + wildPath.replaceAll("\\*", "\\\\E[^\\\\/]*\\\\Q") + "\\E$");
|
||||
List<String> matches = new ArrayList<>();
|
||||
findIn(startDir, matches, wildPattern);
|
||||
|
||||
// Return matches
|
||||
var array = new String[matches.size()];
|
||||
matches.toArray(array);
|
||||
return array;
|
||||
}
|
||||
|
||||
public synchronized boolean exists(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
return mount.exists(path);
|
||||
}
|
||||
|
||||
public synchronized boolean isDir(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
return mount.isDirectory(path);
|
||||
}
|
||||
|
||||
public synchronized boolean isReadOnly(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
return mount.isReadOnly(path);
|
||||
}
|
||||
|
||||
public synchronized String getMountLabel(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
return mount.getLabel();
|
||||
}
|
||||
|
||||
public synchronized void makeDir(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
mount.makeDirectory(path);
|
||||
}
|
||||
|
||||
public synchronized void delete(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
mount.delete(path);
|
||||
}
|
||||
|
||||
public synchronized void move(String sourcePath, String destPath) throws FileSystemException {
|
||||
sourcePath = sanitizePath(sourcePath);
|
||||
destPath = sanitizePath(destPath);
|
||||
if (isReadOnly(sourcePath) || isReadOnly(destPath)) {
|
||||
throw new FileSystemException("Access denied");
|
||||
}
|
||||
if (!exists(sourcePath)) {
|
||||
throw new FileSystemException("No such file");
|
||||
}
|
||||
if (exists(destPath)) {
|
||||
throw new FileSystemException("File exists");
|
||||
}
|
||||
if (contains(sourcePath, destPath)) {
|
||||
throw new FileSystemException("Can't move a directory inside itself");
|
||||
}
|
||||
copy(sourcePath, destPath);
|
||||
delete(sourcePath);
|
||||
}
|
||||
|
||||
public synchronized void copy(String sourcePath, String destPath) throws FileSystemException {
|
||||
sourcePath = sanitizePath(sourcePath);
|
||||
destPath = sanitizePath(destPath);
|
||||
if (isReadOnly(destPath)) {
|
||||
throw new FileSystemException("/" + destPath + ": Access denied");
|
||||
}
|
||||
if (!exists(sourcePath)) {
|
||||
throw new FileSystemException("/" + sourcePath + ": No such file");
|
||||
}
|
||||
if (exists(destPath)) {
|
||||
throw new FileSystemException("/" + destPath + ": File exists");
|
||||
}
|
||||
if (contains(sourcePath, destPath)) {
|
||||
throw new FileSystemException("/" + sourcePath + ": Can't copy a directory inside itself");
|
||||
}
|
||||
copyRecursive(sourcePath, getMount(sourcePath), destPath, getMount(destPath), 0);
|
||||
}
|
||||
|
||||
private synchronized void copyRecursive(String sourcePath, MountWrapper sourceMount, String destinationPath, MountWrapper destinationMount, int depth) throws FileSystemException {
|
||||
if (!sourceMount.exists(sourcePath)) return;
|
||||
if (depth >= MAX_COPY_DEPTH) throw new FileSystemException("Too many directories to copy");
|
||||
|
||||
if (sourceMount.isDirectory(sourcePath)) {
|
||||
// Copy a directory:
|
||||
// Make the new directory
|
||||
destinationMount.makeDirectory(destinationPath);
|
||||
|
||||
// Copy the source contents into it
|
||||
List<String> sourceChildren = new ArrayList<>();
|
||||
sourceMount.list(sourcePath, sourceChildren);
|
||||
for (var child : sourceChildren) {
|
||||
copyRecursive(
|
||||
combine(sourcePath, child), sourceMount,
|
||||
combine(destinationPath, child), destinationMount,
|
||||
depth + 1
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Copy a file:
|
||||
try (var source = sourceMount.openForRead(sourcePath);
|
||||
var destination = destinationMount.openForWrite(destinationPath)) {
|
||||
// Copy bytes as fast as we can
|
||||
ByteStreams.copy(source, destination);
|
||||
} catch (AccessDeniedException e) {
|
||||
throw new FileSystemException("Access denied");
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
synchronized (openFiles) {
|
||||
Reference<?> ref;
|
||||
while ((ref = openFileQueue.poll()) != null) {
|
||||
IoUtil.closeQuietly(openFiles.remove(ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized <T extends Closeable> FileSystemWrapper<T> openFile(@Nonnull MountWrapper mount, @Nonnull Channel channel, @Nonnull T file) throws FileSystemException {
|
||||
synchronized (openFiles) {
|
||||
if (CoreConfig.maximumFilesOpen > 0 &&
|
||||
openFiles.size() >= CoreConfig.maximumFilesOpen) {
|
||||
IoUtil.closeQuietly(file);
|
||||
IoUtil.closeQuietly(channel);
|
||||
throw new FileSystemException("Too many files already open");
|
||||
}
|
||||
|
||||
var channelWrapper = new ChannelWrapper<T>(file, channel);
|
||||
var fsWrapper = new FileSystemWrapper<T>(this, mount, channelWrapper, openFileQueue);
|
||||
openFiles.put(fsWrapper.self, channelWrapper);
|
||||
return fsWrapper;
|
||||
}
|
||||
}
|
||||
|
||||
void removeFile(FileSystemWrapper<?> handle) {
|
||||
synchronized (openFiles) {
|
||||
openFiles.remove(handle.self);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized <T extends Closeable> FileSystemWrapper<T> openForRead(String path, Function<ReadableByteChannel, T> open) throws FileSystemException {
|
||||
cleanup();
|
||||
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
var channel = mount.openForRead(path);
|
||||
return channel != null ? openFile(mount, channel, open.apply(channel)) : null;
|
||||
}
|
||||
|
||||
public synchronized <T extends Closeable> FileSystemWrapper<T> openForWrite(String path, boolean append, Function<WritableByteChannel, T> open) throws FileSystemException {
|
||||
cleanup();
|
||||
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
var channel = append ? mount.openForAppend(path) : mount.openForWrite(path);
|
||||
return channel != null ? openFile(mount, channel, open.apply(channel)) : null;
|
||||
}
|
||||
|
||||
public synchronized long getFreeSpace(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
return mount.getFreeSpace();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public synchronized OptionalLong getCapacity(String path) throws FileSystemException {
|
||||
path = sanitizePath(path);
|
||||
var mount = getMount(path);
|
||||
return mount.getCapacity();
|
||||
}
|
||||
|
||||
private synchronized MountWrapper getMount(String path) throws FileSystemException {
|
||||
// Return the deepest mount that contains a given path
|
||||
var it = mounts.values().iterator();
|
||||
MountWrapper match = null;
|
||||
var matchLength = 999;
|
||||
while (it.hasNext()) {
|
||||
var mount = it.next();
|
||||
if (contains(mount.getLocation(), path)) {
|
||||
var len = toLocal(path, mount.getLocation()).length();
|
||||
if (match == null || len < matchLength) {
|
||||
match = mount;
|
||||
matchLength = len;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (match == null) {
|
||||
throw new FileSystemException("/" + path + ": Invalid Path");
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
public IFileSystem getMountWrapper() {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private static String sanitizePath(String path) {
|
||||
return sanitizePath(path, false);
|
||||
}
|
||||
|
||||
private static final Pattern threeDotsPattern = Pattern.compile("^\\.{3,}$");
|
||||
|
||||
public static String sanitizePath(String path, boolean allowWildcards) {
|
||||
// Allow windowsy slashes
|
||||
path = path.replace('\\', '/');
|
||||
|
||||
// Clean the path or illegal characters.
|
||||
final var specialChars = new char[]{
|
||||
'"', ':', '<', '>', '?', '|', // Sorted by ascii value (important)
|
||||
};
|
||||
|
||||
var cleanName = new StringBuilder();
|
||||
for (var i = 0; i < path.length(); i++) {
|
||||
var c = path.charAt(i);
|
||||
if (c >= 32 && Arrays.binarySearch(specialChars, c) < 0 && (allowWildcards || c != '*')) {
|
||||
cleanName.append(c);
|
||||
}
|
||||
}
|
||||
path = cleanName.toString();
|
||||
|
||||
// Collapse the string into its component parts, removing ..'s
|
||||
var parts = path.split("/");
|
||||
var outputParts = new Stack<String>();
|
||||
for (var part : parts) {
|
||||
if (part.isEmpty() || part.equals(".") || threeDotsPattern.matcher(part).matches()) {
|
||||
// . is redundant
|
||||
// ... and more are treated as .
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part.equals("..")) {
|
||||
// .. can cancel out the last folder entered
|
||||
if (!outputParts.empty()) {
|
||||
var top = outputParts.peek();
|
||||
if (!top.equals("..")) {
|
||||
outputParts.pop();
|
||||
} else {
|
||||
outputParts.push("..");
|
||||
}
|
||||
} else {
|
||||
outputParts.push("..");
|
||||
}
|
||||
} else if (part.length() >= 255) {
|
||||
// If part length > 255 and it is the last part
|
||||
outputParts.push(part.substring(0, 255));
|
||||
} else {
|
||||
// Anything else we add to the stack
|
||||
outputParts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
// Recombine the output parts into a new string
|
||||
var result = new StringBuilder();
|
||||
var it = outputParts.iterator();
|
||||
while (it.hasNext()) {
|
||||
var part = it.next();
|
||||
result.append(part);
|
||||
if (it.hasNext()) {
|
||||
result.append('/');
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static boolean contains(String pathA, String pathB) {
|
||||
pathA = sanitizePath(pathA).toLowerCase(Locale.ROOT);
|
||||
pathB = sanitizePath(pathB).toLowerCase(Locale.ROOT);
|
||||
|
||||
if (pathB.equals("..")) {
|
||||
return false;
|
||||
} else if (pathB.startsWith("../")) {
|
||||
return false;
|
||||
} else if (pathB.equals(pathA)) {
|
||||
return true;
|
||||
} else if (pathA.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
return pathB.startsWith(pathA + "/");
|
||||
}
|
||||
}
|
||||
|
||||
public static String toLocal(String path, String location) {
|
||||
path = sanitizePath(path);
|
||||
location = sanitizePath(location);
|
||||
|
||||
assert contains(location, path);
|
||||
var local = path.substring(location.length());
|
||||
if (local.startsWith("/")) {
|
||||
return local.substring(1);
|
||||
} else {
|
||||
return local;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
public class FileSystemException extends Exception {
|
||||
@Serial
|
||||
private static final long serialVersionUID = -2500631644868104029L;
|
||||
|
||||
FileSystemException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* An alternative closeable implementation that will free up resources in the filesystem.
|
||||
* <p>
|
||||
* The {@link FileSystem} maps weak references of this to its underlying object. If the wrapper has been disposed of
|
||||
* (say, the Lua object referencing it has gone), then the wrapped object will be closed by the filesystem.
|
||||
* <p>
|
||||
* Closing this will stop the filesystem tracking it, reducing the current descriptor count.
|
||||
* <p>
|
||||
* In an ideal world, we'd just wrap the closeable. However, as we do some {@code instanceof} checks
|
||||
* on the stream, it's not really possible as it'd require numerous instances.
|
||||
*
|
||||
* @param <T> The type of writer or channel to wrap.
|
||||
*/
|
||||
public class FileSystemWrapper<T extends Closeable> implements TrackingCloseable {
|
||||
private final FileSystem fileSystem;
|
||||
final MountWrapper mount;
|
||||
private final ChannelWrapper<T> closeable;
|
||||
final WeakReference<FileSystemWrapper<?>> self;
|
||||
private boolean isOpen = true;
|
||||
|
||||
FileSystemWrapper(FileSystem fileSystem, MountWrapper mount, ChannelWrapper<T> closeable, ReferenceQueue<FileSystemWrapper<?>> queue) {
|
||||
this.fileSystem = fileSystem;
|
||||
this.mount = mount;
|
||||
this.closeable = closeable;
|
||||
self = new WeakReference<>(this, queue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
isOpen = false;
|
||||
fileSystem.removeFile(this);
|
||||
closeable.close();
|
||||
}
|
||||
|
||||
void closeExternally() {
|
||||
isOpen = false;
|
||||
IoUtil.closeQuietly(closeable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public T get() {
|
||||
return closeable.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.IFileSystem;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class FileSystemWrapperMount implements IFileSystem {
|
||||
private final FileSystem filesystem;
|
||||
|
||||
public FileSystemWrapperMount(FileSystem filesystem) {
|
||||
this.filesystem = filesystem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeDirectory(@Nonnull String path) throws IOException {
|
||||
try {
|
||||
filesystem.makeDir(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(@Nonnull String path) throws IOException {
|
||||
try {
|
||||
filesystem.delete(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ReadableByteChannel openForRead(@Nonnull String path) throws IOException {
|
||||
try {
|
||||
// FIXME: Think of a better way of implementing this, so closing this will close on the computer.
|
||||
return filesystem.openForRead(path, Function.identity()).get();
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public WritableByteChannel openForWrite(@Nonnull String path) throws IOException {
|
||||
try {
|
||||
return filesystem.openForWrite(path, false, Function.identity()).get();
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public WritableByteChannel openForAppend(@Nonnull String path) throws IOException {
|
||||
try {
|
||||
return filesystem.openForWrite(path, true, Function.identity()).get();
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRemainingSpace() throws IOException {
|
||||
try {
|
||||
return filesystem.getFreeSpace("/");
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(@Nonnull String path) throws IOException {
|
||||
try {
|
||||
return filesystem.exists(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory(@Nonnull String path) throws IOException {
|
||||
try {
|
||||
return filesystem.isDir(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void list(@Nonnull String path, @Nonnull List<String> contents) throws IOException {
|
||||
try {
|
||||
Collections.addAll(contents, filesystem.list(path));
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(@Nonnull String path) throws IOException {
|
||||
try {
|
||||
return filesystem.getSize(path);
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String combine(String path, String child) {
|
||||
return filesystem.combine(path, child);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(String from, String to) throws IOException {
|
||||
try {
|
||||
filesystem.copy(from, to);
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(String from, String to) throws IOException {
|
||||
try {
|
||||
filesystem.move(from, to);
|
||||
} catch (FileSystemException e) {
|
||||
throw new IOException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
import dan200.computercraft.core.util.IoUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class JarMount implements IMount {
|
||||
/**
|
||||
* Only cache files smaller than 1MiB.
|
||||
*/
|
||||
private static final int MAX_CACHED_SIZE = 1 << 20;
|
||||
|
||||
/**
|
||||
* Limit the entire cache to 64MiB.
|
||||
*/
|
||||
private static final int MAX_CACHE_SIZE = 64 << 20;
|
||||
|
||||
/**
|
||||
* We maintain a cache of the contents of all files in the mount. This allows us to allow
|
||||
* seeking within ROM files, and reduces the amount we need to access disk for computer startup.
|
||||
*/
|
||||
private static final Cache<FileEntry, byte[]> CONTENTS_CACHE = CacheBuilder.newBuilder()
|
||||
.concurrencyLevel(4)
|
||||
.expireAfterAccess(60, TimeUnit.SECONDS)
|
||||
.maximumWeight(MAX_CACHE_SIZE)
|
||||
.weakKeys()
|
||||
.<FileEntry, byte[]>weigher((k, v) -> v.length)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* We have a {@link ReferenceQueue} of all mounts, a long with their corresponding {@link ZipFile}. If
|
||||
* the mount has been destroyed, we clean up after it.
|
||||
*/
|
||||
private static final ReferenceQueue<JarMount> MOUNT_QUEUE = new ReferenceQueue<>();
|
||||
|
||||
private final ZipFile zip;
|
||||
private final FileEntry root;
|
||||
|
||||
public JarMount(File jarFile, String subPath) throws IOException {
|
||||
// Cleanup any old mounts. It's unlikely that there will be any, but it's best to be safe.
|
||||
cleanup();
|
||||
|
||||
if (!jarFile.exists() || jarFile.isDirectory()) throw new FileNotFoundException("Cannot find " + jarFile);
|
||||
|
||||
// Open the zip file
|
||||
try {
|
||||
zip = new ZipFile(jarFile);
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Error loading zip file", e);
|
||||
}
|
||||
|
||||
// Ensure the root entry exists.
|
||||
if (zip.getEntry(subPath) == null) {
|
||||
zip.close();
|
||||
throw new FileNotFoundException("Zip does not contain path");
|
||||
}
|
||||
|
||||
// We now create a weak reference to this mount. This is automatically added to the appropriate queue.
|
||||
new MountReference(this);
|
||||
|
||||
// Read in all the entries
|
||||
root = new FileEntry();
|
||||
var zipEntries = zip.entries();
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
var entry = zipEntries.nextElement();
|
||||
|
||||
var entryPath = entry.getName();
|
||||
if (!entryPath.startsWith(subPath)) continue;
|
||||
|
||||
var localPath = FileSystem.toLocal(entryPath, subPath);
|
||||
create(entry, localPath);
|
||||
}
|
||||
}
|
||||
|
||||
private FileEntry get(String path) {
|
||||
var lastEntry = root;
|
||||
var lastIndex = 0;
|
||||
|
||||
while (lastEntry != null && lastIndex < path.length()) {
|
||||
var nextIndex = path.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = path.length();
|
||||
|
||||
lastEntry = lastEntry.children == null ? null : lastEntry.children.get(path.substring(lastIndex, nextIndex));
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
|
||||
return lastEntry;
|
||||
}
|
||||
|
||||
private void create(ZipEntry entry, String localPath) {
|
||||
var lastEntry = root;
|
||||
|
||||
var lastIndex = 0;
|
||||
while (lastIndex < localPath.length()) {
|
||||
var nextIndex = localPath.indexOf('/', lastIndex);
|
||||
if (nextIndex < 0) nextIndex = localPath.length();
|
||||
|
||||
var part = localPath.substring(lastIndex, nextIndex);
|
||||
if (lastEntry.children == null) lastEntry.children = new HashMap<>(0);
|
||||
|
||||
var nextEntry = lastEntry.children.get(part);
|
||||
if (nextEntry == null || !nextEntry.isDirectory()) {
|
||||
lastEntry.children.put(part, nextEntry = new FileEntry());
|
||||
}
|
||||
|
||||
lastEntry = nextEntry;
|
||||
lastIndex = nextIndex + 1;
|
||||
}
|
||||
|
||||
lastEntry.setup(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(@Nonnull String path) {
|
||||
return get(path) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory(@Nonnull String path) {
|
||||
var file = get(path);
|
||||
return file != null && file.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void list(@Nonnull String path, @Nonnull List<String> contents) throws IOException {
|
||||
var file = get(path);
|
||||
if (file == null || !file.isDirectory()) throw new FileOperationException(path, "Not a directory");
|
||||
|
||||
file.list(contents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(@Nonnull String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file != null) return file.size;
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ReadableByteChannel openForRead(@Nonnull String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file != null && !file.isDirectory()) {
|
||||
var contents = CONTENTS_CACHE.getIfPresent(file);
|
||||
if (contents != null) return new ArrayByteChannel(contents);
|
||||
|
||||
try {
|
||||
var entry = zip.getEntry(file.path);
|
||||
if (entry != null) {
|
||||
try (var stream = zip.getInputStream(entry)) {
|
||||
if (stream.available() > MAX_CACHED_SIZE) return Channels.newChannel(stream);
|
||||
|
||||
contents = ByteStreams.toByteArray(stream);
|
||||
CONTENTS_CACHE.put(file, contents);
|
||||
return new ArrayByteChannel(contents);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Treat errors as non-existence of file
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public BasicFileAttributes getAttributes(@Nonnull String path) throws IOException {
|
||||
var file = get(path);
|
||||
if (file != null) {
|
||||
var entry = zip.getEntry(file.path);
|
||||
if (entry != null) return new ZipEntryAttributes(entry);
|
||||
}
|
||||
|
||||
throw new FileOperationException(path, "No such file");
|
||||
}
|
||||
|
||||
private static class FileEntry {
|
||||
String path;
|
||||
long size;
|
||||
Map<String, FileEntry> children;
|
||||
|
||||
void setup(ZipEntry entry) {
|
||||
path = entry.getName();
|
||||
size = entry.getSize();
|
||||
if (children == null && entry.isDirectory()) children = new HashMap<>(0);
|
||||
}
|
||||
|
||||
boolean isDirectory() {
|
||||
return children != null;
|
||||
}
|
||||
|
||||
void list(List<String> contents) {
|
||||
if (children != null) contents.addAll(children.keySet());
|
||||
}
|
||||
}
|
||||
|
||||
private static class MountReference extends WeakReference<JarMount> {
|
||||
final ZipFile file;
|
||||
|
||||
MountReference(JarMount file) {
|
||||
super(file, MOUNT_QUEUE);
|
||||
this.file = file.zip;
|
||||
}
|
||||
}
|
||||
|
||||
private static void cleanup() {
|
||||
Reference<? extends JarMount> next;
|
||||
while ((next = MOUNT_QUEUE.poll()) != null) IoUtil.closeQuietly(((MountReference) next).file);
|
||||
}
|
||||
|
||||
private static class ZipEntryAttributes implements BasicFileAttributes {
|
||||
private final ZipEntry entry;
|
||||
|
||||
ZipEntryAttributes(ZipEntry entry) {
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime lastModifiedTime() {
|
||||
return orEpoch(entry.getLastModifiedTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime lastAccessTime() {
|
||||
return orEpoch(entry.getLastAccessTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTime creationTime() {
|
||||
var time = entry.getCreationTime();
|
||||
return time == null ? lastModifiedTime() : time;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRegularFile() {
|
||||
return !entry.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return entry.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSymbolicLink() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOther() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long size() {
|
||||
return entry.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fileKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final FileTime EPOCH = FileTime.from(Instant.EPOCH);
|
||||
|
||||
private static FileTime orEpoch(FileTime time) {
|
||||
return time == null ? EPOCH : time;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.FileOperationException;
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
class MountWrapper {
|
||||
private final String label;
|
||||
private final String location;
|
||||
|
||||
private final IMount mount;
|
||||
private final IWritableMount writableMount;
|
||||
|
||||
MountWrapper(String label, String location, IMount mount) {
|
||||
this.label = label;
|
||||
this.location = location;
|
||||
this.mount = mount;
|
||||
writableMount = null;
|
||||
}
|
||||
|
||||
MountWrapper(String label, String location, IWritableMount mount) {
|
||||
this.label = label;
|
||||
this.location = location;
|
||||
this.mount = mount;
|
||||
writableMount = mount;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public long getFreeSpace() {
|
||||
if (writableMount == null) return 0;
|
||||
|
||||
try {
|
||||
return writableMount.getRemainingSpace();
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public OptionalLong getCapacity() {
|
||||
return writableMount == null ? OptionalLong.empty() : writableMount.getCapacity();
|
||||
}
|
||||
|
||||
public boolean isReadOnly(String path) {
|
||||
return writableMount == null;
|
||||
}
|
||||
|
||||
public boolean exists(String path) throws FileSystemException {
|
||||
path = toLocal(path);
|
||||
try {
|
||||
return mount.exists(path);
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDirectory(String path) throws FileSystemException {
|
||||
path = toLocal(path);
|
||||
try {
|
||||
return mount.exists(path) && mount.isDirectory(path);
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void list(String path, List<String> contents) throws FileSystemException {
|
||||
path = toLocal(path);
|
||||
try {
|
||||
if (!mount.exists(path) || !mount.isDirectory(path)) {
|
||||
throw localExceptionOf(path, "Not a directory");
|
||||
}
|
||||
|
||||
mount.list(path, contents);
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public long getSize(String path) throws FileSystemException {
|
||||
path = toLocal(path);
|
||||
try {
|
||||
if (!mount.exists(path)) throw localExceptionOf(path, "No such file");
|
||||
return mount.isDirectory(path) ? 0 : mount.getSize(path);
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public BasicFileAttributes getAttributes(String path) throws FileSystemException {
|
||||
path = toLocal(path);
|
||||
try {
|
||||
if (!mount.exists(path)) throw localExceptionOf(path, "No such file");
|
||||
return mount.getAttributes(path);
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public ReadableByteChannel openForRead(String path) throws FileSystemException {
|
||||
path = toLocal(path);
|
||||
try {
|
||||
if (mount.exists(path) && !mount.isDirectory(path)) {
|
||||
return mount.openForRead(path);
|
||||
} else {
|
||||
throw localExceptionOf(path, "No such file");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void makeDirectory(String path) throws FileSystemException {
|
||||
if (writableMount == null) throw exceptionOf(path, "Access denied");
|
||||
|
||||
path = toLocal(path);
|
||||
try {
|
||||
if (mount.exists(path)) {
|
||||
if (!mount.isDirectory(path)) throw localExceptionOf(path, "File exists");
|
||||
} else {
|
||||
writableMount.makeDirectory(path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(String path) throws FileSystemException {
|
||||
if (writableMount == null) throw exceptionOf(path, "Access denied");
|
||||
|
||||
path = toLocal(path);
|
||||
try {
|
||||
if (mount.exists(path)) {
|
||||
writableMount.delete(path);
|
||||
}
|
||||
} catch (AccessDeniedException e) {
|
||||
throw new FileSystemException("Access denied");
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public WritableByteChannel openForWrite(String path) throws FileSystemException {
|
||||
if (writableMount == null) throw exceptionOf(path, "Access denied");
|
||||
|
||||
path = toLocal(path);
|
||||
try {
|
||||
if (mount.exists(path) && mount.isDirectory(path)) {
|
||||
throw localExceptionOf(path, "Cannot write to directory");
|
||||
} else {
|
||||
if (!path.isEmpty()) {
|
||||
var dir = FileSystem.getDirectory(path);
|
||||
if (!dir.isEmpty() && !mount.exists(path)) {
|
||||
writableMount.makeDirectory(dir);
|
||||
}
|
||||
}
|
||||
return writableMount.openForWrite(path);
|
||||
}
|
||||
} catch (AccessDeniedException e) {
|
||||
throw new FileSystemException("Access denied");
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
public WritableByteChannel openForAppend(String path) throws FileSystemException {
|
||||
if (writableMount == null) throw exceptionOf(path, "Access denied");
|
||||
|
||||
path = toLocal(path);
|
||||
try {
|
||||
if (!mount.exists(path)) {
|
||||
if (!path.isEmpty()) {
|
||||
var dir = FileSystem.getDirectory(path);
|
||||
if (!dir.isEmpty() && !mount.exists(path)) {
|
||||
writableMount.makeDirectory(dir);
|
||||
}
|
||||
}
|
||||
return writableMount.openForWrite(path);
|
||||
} else if (mount.isDirectory(path)) {
|
||||
throw localExceptionOf(path, "Cannot write to directory");
|
||||
} else {
|
||||
return writableMount.openForAppend(path);
|
||||
}
|
||||
} catch (AccessDeniedException e) {
|
||||
throw new FileSystemException("Access denied");
|
||||
} catch (IOException e) {
|
||||
throw localExceptionOf(path, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String toLocal(String path) {
|
||||
return FileSystem.toLocal(path, location);
|
||||
}
|
||||
|
||||
private FileSystemException localExceptionOf(@Nullable String localPath, @Nonnull IOException e) {
|
||||
if (!location.isEmpty() && e instanceof FileOperationException ex) {
|
||||
if (ex.getFilename() != null) return localExceptionOf(ex.getFilename(), ex.getMessage());
|
||||
}
|
||||
|
||||
if (e instanceof java.nio.file.FileSystemException ex) {
|
||||
// This error will contain the absolute path, leaking information about where MC is installed. We drop that,
|
||||
// just taking the reason. We assume that the error refers to the input path.
|
||||
var message = ex.getReason().trim();
|
||||
return localPath == null ? new FileSystemException(message) : localExceptionOf(localPath, message);
|
||||
}
|
||||
|
||||
return new FileSystemException(e.getMessage());
|
||||
}
|
||||
|
||||
private FileSystemException localExceptionOf(String path, String message) {
|
||||
if (!location.isEmpty()) path = path.isEmpty() ? location : location + "/" + path;
|
||||
return exceptionOf(path, message);
|
||||
}
|
||||
|
||||
private static FileSystemException exceptionOf(String path, String message) {
|
||||
return new FileSystemException("/" + path + ": " + message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import dan200.computercraft.api.filesystem.IMount;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.List;
|
||||
|
||||
public class SubMount implements IMount {
|
||||
private final IMount parent;
|
||||
private final String subPath;
|
||||
|
||||
public SubMount(IMount parent, String subPath) {
|
||||
this.parent = parent;
|
||||
this.subPath = subPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(@Nonnull String path) throws IOException {
|
||||
return parent.exists(getFullPath(path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory(@Nonnull String path) throws IOException {
|
||||
return parent.isDirectory(getFullPath(path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void list(@Nonnull String path, @Nonnull List<String> contents) throws IOException {
|
||||
parent.list(getFullPath(path), contents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize(@Nonnull String path) throws IOException {
|
||||
return parent.getSize(getFullPath(path));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ReadableByteChannel openForRead(@Nonnull String path) throws IOException {
|
||||
return parent.openForRead(getFullPath(path));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public BasicFileAttributes getAttributes(@Nonnull String path) throws IOException {
|
||||
return parent.getAttributes(getFullPath(path));
|
||||
}
|
||||
|
||||
private String getFullPath(String path) {
|
||||
return path.isEmpty() ? subPath : subPath + "/" + path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.filesystem;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A {@link Closeable} which knows when it has been closed.
|
||||
* <p>
|
||||
* This is a quick (though racey) way of providing more friendly (and more similar to Lua)
|
||||
* error messages to the user.
|
||||
*/
|
||||
public interface TrackingCloseable extends Closeable {
|
||||
boolean isOpen();
|
||||
|
||||
class Impl implements TrackingCloseable {
|
||||
private final Closeable object;
|
||||
private boolean isOpen = true;
|
||||
|
||||
public Impl(Closeable object) {
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOpen() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
isOpen = false;
|
||||
object.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.lua;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.asm.LuaMethod;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.squiddev.cobalt.LuaError;
|
||||
import org.squiddev.cobalt.LuaState;
|
||||
import org.squiddev.cobalt.Varargs;
|
||||
import org.squiddev.cobalt.function.VarArgFunction;
|
||||
|
||||
/**
|
||||
* An "optimised" version of {@link ResultInterpreterFunction} which is guaranteed to never yield.
|
||||
* <p>
|
||||
* As we never yield, we do not need to push a function to the stack, which removes a small amount of overhead.
|
||||
*/
|
||||
class BasicFunction extends VarArgFunction {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BasicFunction.class);
|
||||
private final CobaltLuaMachine machine;
|
||||
private final LuaMethod method;
|
||||
private final Object instance;
|
||||
private final ILuaContext context;
|
||||
private final String name;
|
||||
|
||||
BasicFunction(CobaltLuaMachine machine, LuaMethod method, Object instance, ILuaContext context, String name) {
|
||||
this.machine = machine;
|
||||
this.method = method;
|
||||
this.instance = instance;
|
||||
this.context = context;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Varargs invoke(LuaState luaState, Varargs args) throws LuaError {
|
||||
var arguments = VarargArguments.of(args);
|
||||
MethodResult results;
|
||||
try {
|
||||
results = method.apply(instance, context, arguments);
|
||||
} catch (LuaException e) {
|
||||
throw wrap(e);
|
||||
} catch (Throwable t) {
|
||||
LOG.error(Logging.JAVA_ERROR, "Error calling {} on {}", name, instance, t);
|
||||
throw new LuaError("Java Exception Thrown: " + t, 0);
|
||||
} finally {
|
||||
arguments.close();
|
||||
}
|
||||
|
||||
if (results.getCallback() != null) {
|
||||
throw new IllegalStateException("Cannot have a yielding non-yielding function");
|
||||
}
|
||||
return machine.toValues(results.getResult());
|
||||
}
|
||||
|
||||
public static LuaError wrap(LuaException exception) {
|
||||
return exception.hasLevel() ? new LuaError(exception.getMessage()) : new LuaError(exception.getMessage(), exception.getLevel());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.lua;
|
||||
|
||||
import dan200.computercraft.api.lua.IDynamicLuaObject;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.ILuaFunction;
|
||||
import dan200.computercraft.core.CoreConfig;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.asm.LuaMethod;
|
||||
import dan200.computercraft.core.asm.ObjectSource;
|
||||
import dan200.computercraft.core.computer.TimeoutState;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.core.util.ThreadUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.squiddev.cobalt.*;
|
||||
import org.squiddev.cobalt.compiler.CompileException;
|
||||
import org.squiddev.cobalt.compiler.LoadState;
|
||||
import org.squiddev.cobalt.debug.DebugFrame;
|
||||
import org.squiddev.cobalt.debug.DebugHandler;
|
||||
import org.squiddev.cobalt.debug.DebugState;
|
||||
import org.squiddev.cobalt.lib.*;
|
||||
import org.squiddev.cobalt.lib.platform.VoidResourceManipulator;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serial;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.squiddev.cobalt.ValueFactory.valueOf;
|
||||
import static org.squiddev.cobalt.ValueFactory.varargsOf;
|
||||
import static org.squiddev.cobalt.debug.DebugFrame.FLAG_HOOKED;
|
||||
import static org.squiddev.cobalt.debug.DebugFrame.FLAG_HOOKYIELD;
|
||||
|
||||
public class CobaltLuaMachine implements ILuaMachine {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CobaltLuaMachine.class);
|
||||
|
||||
private static final ThreadPoolExecutor COROUTINES = new ThreadPoolExecutor(
|
||||
0, Integer.MAX_VALUE,
|
||||
5L, TimeUnit.MINUTES,
|
||||
new SynchronousQueue<>(),
|
||||
ThreadUtils.factory("Coroutine")
|
||||
);
|
||||
|
||||
private static final LuaMethod FUNCTION_METHOD = (target, context, args) -> ((ILuaFunction) target).call(args);
|
||||
|
||||
private final TimeoutState timeout;
|
||||
private final TimeoutDebugHandler debug;
|
||||
private final ILuaContext context;
|
||||
|
||||
private LuaState state;
|
||||
private LuaTable globals;
|
||||
|
||||
private LuaThread mainRoutine = null;
|
||||
private String eventFilter = null;
|
||||
|
||||
public CobaltLuaMachine(MachineEnvironment environment) {
|
||||
timeout = environment.timeout();
|
||||
context = environment.context();
|
||||
debug = new TimeoutDebugHandler();
|
||||
|
||||
// Create an environment to run in
|
||||
var metrics = environment.metrics();
|
||||
var state = this.state = LuaState.builder()
|
||||
.resourceManipulator(new VoidResourceManipulator())
|
||||
.debug(debug)
|
||||
.coroutineExecutor(command -> {
|
||||
metrics.observe(Metrics.COROUTINES_CREATED);
|
||||
COROUTINES.execute(() -> {
|
||||
try {
|
||||
command.run();
|
||||
} finally {
|
||||
metrics.observe(Metrics.COROUTINES_DISPOSED);
|
||||
}
|
||||
});
|
||||
})
|
||||
.build();
|
||||
|
||||
globals = new LuaTable();
|
||||
state.setupThread(globals);
|
||||
|
||||
// Add basic libraries
|
||||
globals.load(state, new BaseLib());
|
||||
globals.load(state, new TableLib());
|
||||
globals.load(state, new StringLib());
|
||||
globals.load(state, new MathLib());
|
||||
globals.load(state, new CoroutineLib());
|
||||
globals.load(state, new Bit32Lib());
|
||||
globals.load(state, new Utf8Lib());
|
||||
globals.load(state, new DebugLib());
|
||||
|
||||
// Remove globals we don't want to expose
|
||||
globals.rawset("collectgarbage", Constants.NIL);
|
||||
globals.rawset("dofile", Constants.NIL);
|
||||
globals.rawset("loadfile", Constants.NIL);
|
||||
globals.rawset("print", Constants.NIL);
|
||||
|
||||
// Add version globals
|
||||
globals.rawset("_VERSION", valueOf("Lua 5.1"));
|
||||
globals.rawset("_HOST", valueOf(environment.hostString()));
|
||||
globals.rawset("_CC_DEFAULT_SETTINGS", valueOf(CoreConfig.defaultComputerSettings));
|
||||
if (CoreConfig.disableLua51Features) {
|
||||
globals.rawset("_CC_DISABLE_LUA51_FEATURES", Constants.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAPI(@Nonnull ILuaAPI api) {
|
||||
// Add the methods of an API to the global table
|
||||
var table = wrapLuaObject(api);
|
||||
if (table == null) {
|
||||
LOG.warn("API {} does not provide any methods", api);
|
||||
table = new LuaTable();
|
||||
}
|
||||
|
||||
var names = api.getNames();
|
||||
for (var name : names) globals.rawset(name, table);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MachineResult loadBios(@Nonnull InputStream bios) {
|
||||
// Begin executing a file (ie, the bios)
|
||||
if (mainRoutine != null) return MachineResult.OK;
|
||||
|
||||
try {
|
||||
var value = LoadState.load(state, bios, "@bios.lua", globals);
|
||||
mainRoutine = new LuaThread(state, value, globals);
|
||||
return MachineResult.OK;
|
||||
} catch (CompileException e) {
|
||||
close();
|
||||
return MachineResult.error(e);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Could not load bios.lua", e);
|
||||
close();
|
||||
return MachineResult.GENERIC_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MachineResult handleEvent(String eventName, Object[] arguments) {
|
||||
if (mainRoutine == null) return MachineResult.OK;
|
||||
|
||||
if (eventFilter != null && eventName != null && !eventName.equals(eventFilter) && !eventName.equals("terminate")) {
|
||||
return MachineResult.OK;
|
||||
}
|
||||
|
||||
// If the soft abort has been cleared then we can reset our flag.
|
||||
timeout.refresh();
|
||||
if (!timeout.isSoftAborted()) debug.thrownSoftAbort = false;
|
||||
|
||||
try {
|
||||
Varargs resumeArgs = Constants.NONE;
|
||||
if (eventName != null) {
|
||||
resumeArgs = varargsOf(valueOf(eventName), toValues(arguments));
|
||||
}
|
||||
|
||||
// Resume the current thread, or the main one when first starting off.
|
||||
var thread = state.getCurrentThread();
|
||||
if (thread == null || thread == state.getMainThread()) thread = mainRoutine;
|
||||
|
||||
var results = LuaThread.run(thread, resumeArgs);
|
||||
if (timeout.isHardAborted()) throw HardAbortError.INSTANCE;
|
||||
if (results == null) return MachineResult.PAUSE;
|
||||
|
||||
var filter = results.first();
|
||||
eventFilter = filter.isString() ? filter.toString() : null;
|
||||
|
||||
if (mainRoutine.getStatus().equals("dead")) {
|
||||
close();
|
||||
return MachineResult.GENERIC_ERROR;
|
||||
} else {
|
||||
return MachineResult.OK;
|
||||
}
|
||||
} catch (HardAbortError | InterruptedException e) {
|
||||
close();
|
||||
return MachineResult.TIMEOUT;
|
||||
} catch (LuaError e) {
|
||||
close();
|
||||
LOG.warn("Top level coroutine errored", e);
|
||||
return MachineResult.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printExecutionState(StringBuilder out) {
|
||||
var state = this.state;
|
||||
if (state == null) {
|
||||
out.append("CobaltLuaMachine is terminated\n");
|
||||
} else {
|
||||
state.printExecutionState(out);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
var state = this.state;
|
||||
if (state == null) return;
|
||||
|
||||
state.abandon();
|
||||
mainRoutine = null;
|
||||
this.state = null;
|
||||
globals = null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LuaTable wrapLuaObject(Object object) {
|
||||
var dynamicMethods = object instanceof IDynamicLuaObject dynamic
|
||||
? Objects.requireNonNull(dynamic.getMethodNames(), "Methods cannot be null")
|
||||
: LuaMethod.EMPTY_METHODS;
|
||||
|
||||
var table = new LuaTable();
|
||||
for (var i = 0; i < dynamicMethods.length; i++) {
|
||||
var method = dynamicMethods[i];
|
||||
table.rawset(method, new ResultInterpreterFunction(this, LuaMethod.DYNAMIC.get(i), object, context, method));
|
||||
}
|
||||
|
||||
ObjectSource.allMethods(LuaMethod.GENERATOR, object, (instance, method) ->
|
||||
table.rawset(method.getName(), method.nonYielding()
|
||||
? new BasicFunction(this, method.getMethod(), instance, context, method.getName())
|
||||
: new ResultInterpreterFunction(this, method.getMethod(), instance, context, method.getName())));
|
||||
|
||||
try {
|
||||
if (table.keyCount() == 0) return null;
|
||||
} catch (LuaError ignored) {
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private LuaValue toValue(@Nullable Object object, @Nullable Map<Object, LuaValue> values) {
|
||||
if (object == null) return Constants.NIL;
|
||||
if (object instanceof Number num) return valueOf(num.doubleValue());
|
||||
if (object instanceof Boolean bool) return valueOf(bool);
|
||||
if (object instanceof String str) return valueOf(str);
|
||||
if (object instanceof byte[] b) {
|
||||
return valueOf(Arrays.copyOf(b, b.length));
|
||||
}
|
||||
if (object instanceof ByteBuffer b) {
|
||||
var bytes = new byte[b.remaining()];
|
||||
b.get(bytes);
|
||||
return valueOf(bytes);
|
||||
}
|
||||
|
||||
if (values == null) values = new IdentityHashMap<>(1);
|
||||
var result = values.get(object);
|
||||
if (result != null) return result;
|
||||
|
||||
if (object instanceof ILuaFunction) {
|
||||
return new ResultInterpreterFunction(this, FUNCTION_METHOD, object, context, object.toString());
|
||||
}
|
||||
|
||||
if (object instanceof IDynamicLuaObject) {
|
||||
LuaValue wrapped = wrapLuaObject(object);
|
||||
if (wrapped == null) wrapped = new LuaTable();
|
||||
values.put(object, wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
if (object instanceof Map<?, ?> map) {
|
||||
var table = new LuaTable();
|
||||
values.put(object, table);
|
||||
|
||||
for (Map.Entry<?, ?> pair : map.entrySet()) {
|
||||
var key = toValue(pair.getKey(), values);
|
||||
var value = toValue(pair.getValue(), values);
|
||||
if (!key.isNil() && !value.isNil()) table.rawset(key, value);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
if (object instanceof Collection<?> objects) {
|
||||
var table = new LuaTable(objects.size(), 0);
|
||||
values.put(object, table);
|
||||
var i = 0;
|
||||
for (Object child : objects) table.rawset(++i, toValue(child, values));
|
||||
return table;
|
||||
}
|
||||
|
||||
if (object instanceof Object[] objects) {
|
||||
var table = new LuaTable(objects.length, 0);
|
||||
values.put(object, table);
|
||||
for (var i = 0; i < objects.length; i++) table.rawset(i + 1, toValue(objects[i], values));
|
||||
return table;
|
||||
}
|
||||
|
||||
var wrapped = wrapLuaObject(object);
|
||||
if (wrapped != null) {
|
||||
values.put(object, wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
LOG.warn(Logging.JAVA_ERROR, "Received unknown type '{}', returning nil.", object.getClass().getName());
|
||||
return Constants.NIL;
|
||||
}
|
||||
|
||||
Varargs toValues(Object[] objects) {
|
||||
if (objects == null || objects.length == 0) return Constants.NONE;
|
||||
if (objects.length == 1) return toValue(objects[0], null);
|
||||
|
||||
Map<Object, LuaValue> result = new IdentityHashMap<>(0);
|
||||
var values = new LuaValue[objects.length];
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var object = objects[i];
|
||||
values[i] = toValue(object, result);
|
||||
}
|
||||
return varargsOf(values);
|
||||
}
|
||||
|
||||
static Object toObject(LuaValue value, Map<LuaValue, Object> objects) {
|
||||
switch (value.type()) {
|
||||
case Constants.TNIL:
|
||||
case Constants.TNONE:
|
||||
return null;
|
||||
case Constants.TINT:
|
||||
case Constants.TNUMBER:
|
||||
return value.toDouble();
|
||||
case Constants.TBOOLEAN:
|
||||
return value.toBoolean();
|
||||
case Constants.TSTRING:
|
||||
return value.toString();
|
||||
case Constants.TTABLE: {
|
||||
// Table:
|
||||
// Start remembering stuff
|
||||
if (objects == null) {
|
||||
objects = new IdentityHashMap<>(1);
|
||||
} else {
|
||||
var existing = objects.get(value);
|
||||
if (existing != null) return existing;
|
||||
}
|
||||
Map<Object, Object> table = new HashMap<>();
|
||||
objects.put(value, table);
|
||||
|
||||
var luaTable = (LuaTable) value;
|
||||
|
||||
// Convert all keys
|
||||
var k = Constants.NIL;
|
||||
while (true) {
|
||||
Varargs keyValue;
|
||||
try {
|
||||
keyValue = luaTable.next(k);
|
||||
} catch (LuaError luaError) {
|
||||
break;
|
||||
}
|
||||
k = keyValue.first();
|
||||
if (k.isNil()) break;
|
||||
|
||||
var v = keyValue.arg(2);
|
||||
var keyObject = toObject(k, objects);
|
||||
var valueObject = toObject(v, objects);
|
||||
if (keyObject != null && valueObject != null) {
|
||||
table.put(keyObject, valueObject);
|
||||
}
|
||||
}
|
||||
return table;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Object[] toObjects(Varargs values) {
|
||||
var count = values.count();
|
||||
var objects = new Object[count];
|
||||
for (var i = 0; i < count; i++) objects[i] = toObject(values.arg(i + 1), null);
|
||||
return objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link DebugHandler} which observes the {@link TimeoutState} and responds accordingly.
|
||||
*/
|
||||
private class TimeoutDebugHandler extends DebugHandler {
|
||||
private final TimeoutState timeout;
|
||||
private int count = 0;
|
||||
boolean thrownSoftAbort;
|
||||
|
||||
private boolean isPaused;
|
||||
private int oldFlags;
|
||||
private boolean oldInHook;
|
||||
|
||||
TimeoutDebugHandler() {
|
||||
timeout = CobaltLuaMachine.this.timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstruction(DebugState ds, DebugFrame di, int pc) throws LuaError, UnwindThrowable {
|
||||
di.pc = pc;
|
||||
|
||||
if (isPaused) resetPaused(ds, di);
|
||||
|
||||
// We check our current pause/abort state every 128 instructions.
|
||||
if ((count = (count + 1) & 127) == 0) {
|
||||
if (timeout.isHardAborted() || state == null) throw HardAbortError.INSTANCE;
|
||||
if (timeout.isPaused()) handlePause(ds, di);
|
||||
if (timeout.isSoftAborted()) handleSoftAbort();
|
||||
}
|
||||
|
||||
super.onInstruction(ds, di, pc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void poll() throws LuaError {
|
||||
var state = CobaltLuaMachine.this.state;
|
||||
if (timeout.isHardAborted() || state == null) throw HardAbortError.INSTANCE;
|
||||
if (timeout.isPaused()) LuaThread.suspendBlocking(state);
|
||||
if (timeout.isSoftAborted()) handleSoftAbort();
|
||||
}
|
||||
|
||||
private void resetPaused(DebugState ds, DebugFrame di) {
|
||||
// Restore the previous paused state
|
||||
isPaused = false;
|
||||
ds.inhook = oldInHook;
|
||||
di.flags = oldFlags;
|
||||
}
|
||||
|
||||
private void handleSoftAbort() throws LuaError {
|
||||
// If we already thrown our soft abort error then don't do it again.
|
||||
if (thrownSoftAbort) return;
|
||||
|
||||
thrownSoftAbort = true;
|
||||
throw new LuaError(TimeoutState.ABORT_MESSAGE);
|
||||
}
|
||||
|
||||
private void handlePause(DebugState ds, DebugFrame di) throws LuaError, UnwindThrowable {
|
||||
// Preserve the current state
|
||||
isPaused = true;
|
||||
oldInHook = ds.inhook;
|
||||
oldFlags = di.flags;
|
||||
|
||||
// Suspend the state. This will probably throw, but we need to handle the case where it won't.
|
||||
di.flags |= FLAG_HOOKYIELD | FLAG_HOOKED;
|
||||
LuaThread.suspend(ds.getLuaState());
|
||||
resetPaused(ds, di);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class HardAbortError extends Error {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 7954092008586367501L;
|
||||
|
||||
static final HardAbortError INSTANCE = new HardAbortError();
|
||||
|
||||
private HardAbortError() {
|
||||
super("Hard Abort", null, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.lua;
|
||||
|
||||
import dan200.computercraft.api.lua.IDynamicLuaObject;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Represents a machine which will execute Lua code. Technically this API is flexible enough to support many languages,
|
||||
* but you'd need a way to provide alternative ROMs, BIOSes, etc...
|
||||
* <p>
|
||||
* There should only be one concrete implementation at any one time, which is currently {@link CobaltLuaMachine}. If
|
||||
* external mod authors are interested in registering their own machines, we can look into how we can provide some
|
||||
* mechanism for registering these.
|
||||
* <p>
|
||||
* This should provide implementations of {@link dan200.computercraft.api.lua.ILuaContext}, and the ability to convert
|
||||
* {@link IDynamicLuaObject}s into something the VM understands, as well as handling method calls.
|
||||
*/
|
||||
public interface ILuaMachine {
|
||||
/**
|
||||
* Inject an API into the global environment of this machine. This should construct an object, as it would for any
|
||||
* {@link IDynamicLuaObject} and set it to all names in {@link ILuaAPI#getNames()}.
|
||||
* <p>
|
||||
* Called before {@link #loadBios(InputStream)}.
|
||||
*
|
||||
* @param api The API to register.
|
||||
*/
|
||||
void addAPI(@Nonnull ILuaAPI api);
|
||||
|
||||
/**
|
||||
* Create a function from the provided program, and set it up to run when {@link #handleEvent(String, Object[])} is
|
||||
* called.
|
||||
* <p>
|
||||
* This should destroy the machine if it failed to load the bios.
|
||||
*
|
||||
* @param bios The stream containing the boot program.
|
||||
* @return The result of loading this machine. Will either be OK, or the error message when loading the bios.
|
||||
*/
|
||||
MachineResult loadBios(@Nonnull InputStream bios);
|
||||
|
||||
/**
|
||||
* Resume the machine, either starting or resuming the coroutine.
|
||||
* <p>
|
||||
* This should destroy the machine if it failed to execute successfully.
|
||||
*
|
||||
* @param eventName The name of the event. This is {@code null} when first starting the machine. Note, this may
|
||||
* do nothing if it does not match the event filter.
|
||||
* @param arguments The arguments for this event.
|
||||
* @return The result of loading this machine. Will either be OK, or the error message that occurred when
|
||||
* executing.
|
||||
*/
|
||||
MachineResult handleEvent(@Nullable String eventName, @Nullable Object[] arguments);
|
||||
|
||||
/**
|
||||
* Print some information about the internal execution state.
|
||||
* <p>
|
||||
* This function is purely intended for debugging, its output should not be relied on in any way.
|
||||
*
|
||||
* @param out The buffer to write to.
|
||||
*/
|
||||
void printExecutionState(StringBuilder out);
|
||||
|
||||
/**
|
||||
* Close the Lua machine, aborting any running functions and deleting the internal state.
|
||||
*/
|
||||
void close();
|
||||
|
||||
interface Factory {
|
||||
ILuaMachine create(MachineEnvironment environment);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.lua;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||
import dan200.computercraft.core.computer.TimeoutState;
|
||||
import dan200.computercraft.core.metrics.Metrics;
|
||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||
|
||||
/**
|
||||
* Arguments used to construct a {@link ILuaMachine}.
|
||||
*
|
||||
* @param context The Lua context to execute main-thread tasks with.
|
||||
* @param metrics A sink to submit metrics to. You do not need to submit task timings here, it should only be for additional
|
||||
* metrics such as {@link Metrics#COROUTINES_CREATED}
|
||||
* @param timeout The current timeout state. This should be used by the machine to interrupt its execution.
|
||||
* @param hostString A {@linkplain GlobalEnvironment#getHostString() host string} to identify the current environment.
|
||||
* @see ILuaMachine.Factory
|
||||
*/
|
||||
public record MachineEnvironment(
|
||||
ILuaContext context,
|
||||
MetricsObserver metrics,
|
||||
TimeoutState timeout,
|
||||
String hostString
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.lua;
|
||||
|
||||
import dan200.computercraft.core.computer.TimeoutState;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* The result of executing an action on a machine.
|
||||
* <p>
|
||||
* Errors should halt the machine and display the error to the user.
|
||||
*
|
||||
* @see ILuaMachine#loadBios(InputStream)
|
||||
* @see ILuaMachine#handleEvent(String, Object[])
|
||||
*/
|
||||
public final class MachineResult {
|
||||
/**
|
||||
* A successful complete execution.
|
||||
*/
|
||||
public static final MachineResult OK = new MachineResult(false, false, null);
|
||||
|
||||
/**
|
||||
* A successful paused execution.
|
||||
*/
|
||||
public static final MachineResult PAUSE = new MachineResult(false, true, null);
|
||||
|
||||
/**
|
||||
* An execution which timed out.
|
||||
*/
|
||||
public static final MachineResult TIMEOUT = new MachineResult(true, false, TimeoutState.ABORT_MESSAGE);
|
||||
|
||||
/**
|
||||
* An error with no user-friendly error message.
|
||||
*/
|
||||
public static final MachineResult GENERIC_ERROR = new MachineResult(true, false, null);
|
||||
|
||||
private final boolean error;
|
||||
private final boolean pause;
|
||||
private final String message;
|
||||
|
||||
private MachineResult(boolean error, boolean pause, String message) {
|
||||
this.pause = pause;
|
||||
this.message = message;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public static MachineResult error(@Nonnull String error) {
|
||||
return new MachineResult(true, false, error);
|
||||
}
|
||||
|
||||
public static MachineResult error(@Nonnull Exception error) {
|
||||
return new MachineResult(true, false, error.getMessage());
|
||||
}
|
||||
|
||||
public boolean isError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public boolean isPause() {
|
||||
return pause;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.lua;
|
||||
|
||||
import dan200.computercraft.api.lua.ILuaCallback;
|
||||
import dan200.computercraft.api.lua.ILuaContext;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.asm.LuaMethod;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.squiddev.cobalt.*;
|
||||
import org.squiddev.cobalt.debug.DebugFrame;
|
||||
import org.squiddev.cobalt.function.ResumableVarArgFunction;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Calls a {@link LuaMethod}, and interprets the resulting {@link MethodResult}, either returning the result or yielding
|
||||
* and resuming the supplied continuation.
|
||||
*/
|
||||
class ResultInterpreterFunction extends ResumableVarArgFunction<ResultInterpreterFunction.Container> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResultInterpreterFunction.class);
|
||||
|
||||
@Nonnull
|
||||
static class Container {
|
||||
ILuaCallback callback;
|
||||
final int errorAdjust;
|
||||
|
||||
Container(ILuaCallback callback, int errorAdjust) {
|
||||
this.callback = callback;
|
||||
this.errorAdjust = errorAdjust;
|
||||
}
|
||||
}
|
||||
|
||||
private final CobaltLuaMachine machine;
|
||||
private final LuaMethod method;
|
||||
private final Object instance;
|
||||
private final ILuaContext context;
|
||||
private final String name;
|
||||
|
||||
ResultInterpreterFunction(CobaltLuaMachine machine, LuaMethod method, Object instance, ILuaContext context, String name) {
|
||||
this.machine = machine;
|
||||
this.method = method;
|
||||
this.instance = instance;
|
||||
this.context = context;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Varargs invoke(LuaState state, DebugFrame debugFrame, Varargs args) throws LuaError, UnwindThrowable {
|
||||
var arguments = VarargArguments.of(args);
|
||||
MethodResult results;
|
||||
try {
|
||||
results = method.apply(instance, context, arguments);
|
||||
} catch (LuaException e) {
|
||||
throw wrap(e, 0);
|
||||
} catch (Throwable t) {
|
||||
LOG.error(Logging.JAVA_ERROR, "Error calling {} on {}", name, instance, t);
|
||||
throw new LuaError("Java Exception Thrown: " + t, 0);
|
||||
} finally {
|
||||
arguments.close();
|
||||
}
|
||||
|
||||
var callback = results.getCallback();
|
||||
var ret = machine.toValues(results.getResult());
|
||||
|
||||
if (callback == null) return ret;
|
||||
|
||||
debugFrame.state = new Container(callback, results.getErrorAdjust());
|
||||
return LuaThread.yield(state, ret);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Varargs resumeThis(LuaState state, Container container, Varargs args) throws LuaError, UnwindThrowable {
|
||||
MethodResult results;
|
||||
var arguments = CobaltLuaMachine.toObjects(args);
|
||||
try {
|
||||
results = container.callback.resume(arguments);
|
||||
} catch (LuaException e) {
|
||||
throw wrap(e, container.errorAdjust);
|
||||
} catch (Throwable t) {
|
||||
LOG.error(Logging.JAVA_ERROR, "Error calling {} on {}", name, container.callback, t);
|
||||
throw new LuaError("Java Exception Thrown: " + t, 0);
|
||||
}
|
||||
|
||||
var ret = machine.toValues(results.getResult());
|
||||
|
||||
var callback = results.getCallback();
|
||||
if (callback == null) return ret;
|
||||
|
||||
container.callback = callback;
|
||||
return LuaThread.yield(state, ret);
|
||||
}
|
||||
|
||||
public static LuaError wrap(LuaException exception, int adjust) {
|
||||
if (!exception.hasLevel() && adjust == 0) return new LuaError(exception.getMessage());
|
||||
|
||||
var level = exception.getLevel();
|
||||
return new LuaError(exception.getMessage(), level <= 0 ? level : level + adjust + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.lua;
|
||||
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
import org.squiddev.cobalt.*;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.*;
|
||||
|
||||
import static dan200.computercraft.api.lua.LuaValues.badTableItem;
|
||||
import static dan200.computercraft.api.lua.LuaValues.getNumericType;
|
||||
|
||||
class TableImpl implements dan200.computercraft.api.lua.LuaTable<Object, Object> {
|
||||
private final VarargArguments arguments;
|
||||
private final LuaTable table;
|
||||
private Map<Object, Object> backingMap;
|
||||
|
||||
TableImpl(VarargArguments arguments, LuaTable table) {
|
||||
this.arguments = arguments;
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
checkValid();
|
||||
try {
|
||||
return table.keyCount();
|
||||
} catch (LuaError e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return table.length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(int index) throws LuaException {
|
||||
var value = table.rawget(index);
|
||||
if (!(value instanceof LuaNumber)) throw LuaValues.badTableItem(index, "number", value.typeName());
|
||||
if (value instanceof LuaInteger) return value.toInteger();
|
||||
|
||||
var number = value.toDouble();
|
||||
if (!Double.isFinite(number)) throw badTableItem(index, "number", getNumericType(number));
|
||||
return (long) number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
checkValid();
|
||||
try {
|
||||
return table.next(Constants.NIL).first().isNil();
|
||||
} catch (LuaError e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private LuaValue getImpl(Object o) {
|
||||
checkValid();
|
||||
if (o instanceof String) return table.rawget((String) o);
|
||||
if (o instanceof Integer) return table.rawget((Integer) o);
|
||||
return Constants.NIL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object o) {
|
||||
return !getImpl(o).isNil();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object o) {
|
||||
return CobaltLuaMachine.toObject(getImpl(o), null);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Map<Object, Object> getBackingMap() {
|
||||
checkValid();
|
||||
if (backingMap != null) return backingMap;
|
||||
return backingMap = Collections.unmodifiableMap(
|
||||
Objects.requireNonNull((Map<?, ?>) CobaltLuaMachine.toObject(table, null))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsValue(Object o) {
|
||||
return getBackingMap().containsKey(o);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Object> keySet() {
|
||||
return getBackingMap().keySet();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Collection<Object> values() {
|
||||
return getBackingMap().values();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Entry<Object, Object>> entrySet() {
|
||||
return getBackingMap().entrySet();
|
||||
}
|
||||
|
||||
private void checkValid() {
|
||||
if (arguments.closed) {
|
||||
throw new IllegalStateException("Cannot use LuaTable after IArguments has been released");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.lua;
|
||||
|
||||
import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaValues;
|
||||
import org.squiddev.cobalt.*;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
|
||||
final class VarargArguments implements IArguments {
|
||||
private static final VarargArguments EMPTY = new VarargArguments(Constants.NONE);
|
||||
|
||||
boolean closed;
|
||||
private final Varargs varargs;
|
||||
private Object[] cache;
|
||||
|
||||
private VarargArguments(Varargs varargs) {
|
||||
this.varargs = varargs;
|
||||
}
|
||||
|
||||
static VarargArguments of(Varargs values) {
|
||||
return values == Constants.NONE ? EMPTY : new VarargArguments(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count() {
|
||||
return varargs.count();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object get(int index) {
|
||||
if (index < 0 || index >= varargs.count()) return null;
|
||||
|
||||
var cache = this.cache;
|
||||
if (cache == null) {
|
||||
cache = this.cache = new Object[varargs.count()];
|
||||
} else {
|
||||
var existing = cache[index];
|
||||
if (existing != null) return existing;
|
||||
}
|
||||
|
||||
return cache[index] = CobaltLuaMachine.toObject(varargs.arg(index + 1), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IArguments drop(int count) {
|
||||
if (count < 0) throw new IllegalStateException("count cannot be negative");
|
||||
if (count == 0) return this;
|
||||
return new VarargArguments(varargs.subargs(count + 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getDouble(int index) throws LuaException {
|
||||
var value = varargs.arg(index + 1);
|
||||
if (!(value instanceof LuaNumber)) throw LuaValues.badArgument(index, "number", value.typeName());
|
||||
return value.toDouble();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(int index) throws LuaException {
|
||||
var value = varargs.arg(index + 1);
|
||||
if (!(value instanceof LuaNumber)) throw LuaValues.badArgument(index, "number", value.typeName());
|
||||
return value instanceof LuaInteger ? value.toInteger() : (long) LuaValues.checkFinite(index, value.toDouble());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ByteBuffer getBytes(int index) throws LuaException {
|
||||
var value = varargs.arg(index + 1);
|
||||
if (!(value instanceof LuaBaseString)) throw LuaValues.badArgument(index, "string", value.typeName());
|
||||
|
||||
var str = ((LuaBaseString) value).strvalue();
|
||||
return ByteBuffer.wrap(str.bytes, str.offset, str.length).asReadOnlyBuffer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ByteBuffer> optBytes(int index) throws LuaException {
|
||||
var value = varargs.arg(index + 1);
|
||||
if (value.isNil()) return Optional.empty();
|
||||
if (!(value instanceof LuaBaseString)) throw LuaValues.badArgument(index, "string", value.typeName());
|
||||
|
||||
var str = ((LuaBaseString) value).strvalue();
|
||||
return Optional.of(ByteBuffer.wrap(str.bytes, str.offset, str.length).asReadOnlyBuffer());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public dan200.computercraft.api.lua.LuaTable<?, ?> getTableUnsafe(int index) throws LuaException {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Cannot use getTableUnsafe after IArguments has been closed.");
|
||||
}
|
||||
|
||||
var value = varargs.arg(index + 1);
|
||||
if (!(value instanceof LuaTable)) throw LuaValues.badArgument(index, "table", value.typeName());
|
||||
return new TableImpl(this, (LuaTable) value);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Optional<dan200.computercraft.api.lua.LuaTable<?, ?>> optTableUnsafe(int index) throws LuaException {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Cannot use optTableUnsafe after IArguments has been closed.");
|
||||
}
|
||||
|
||||
var value = varargs.arg(index + 1);
|
||||
if (value.isNil()) return Optional.empty();
|
||||
if (!(value instanceof LuaTable)) throw LuaValues.badArgument(index, "table", value.typeName());
|
||||
return Optional.of(new TableImpl(this, (LuaTable) value));
|
||||
}
|
||||
|
||||
public void close() {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.metrics;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.LongFunction;
|
||||
|
||||
/**
|
||||
* A metric is some event which is emitted by a computer and observed by a {@link MetricsObserver}.
|
||||
* <p>
|
||||
* It comes in two forms: a simple {@link Metric.Counter} counts how many times an event has occurred (for instance,
|
||||
* how many HTTP requests have there been) while a {@link Metric.Event} has a discrete value for each event (for
|
||||
* instance, the number of bytes downloaded in this HTTP request). The values tied to a {@link Metric.Event}s can be
|
||||
* accumulated, and thus used to derive averages, rates and be sorted into histogram buckets.
|
||||
*/
|
||||
public abstract class Metric {
|
||||
private static final Map<String, Metric> allMetrics = new ConcurrentHashMap<>();
|
||||
private static final AtomicInteger nextId = new AtomicInteger();
|
||||
|
||||
private final int id;
|
||||
private final String name;
|
||||
private final String unit;
|
||||
private final LongFunction<String> format;
|
||||
|
||||
private Metric(String name, String unit, LongFunction<String> format) {
|
||||
if (allMetrics.containsKey(name)) throw new IllegalStateException("Duplicate key " + name);
|
||||
|
||||
id = nextId.getAndIncrement();
|
||||
this.name = name;
|
||||
this.unit = unit;
|
||||
this.format = format;
|
||||
allMetrics.put(name, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique ID for this metric.
|
||||
* <p>
|
||||
* Each metric is assigned a consecutive metric ID starting from 0. This allows you to store metrics in an array,
|
||||
* rather than a map.
|
||||
*
|
||||
* @return The metric's ID.
|
||||
*/
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique name for this metric.
|
||||
*
|
||||
* @return The metric's name.
|
||||
*/
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unit used for this metric. This should be a lowercase "identifier-like" such as {@literal ms}. If no unit is
|
||||
* relevant, it can be empty.
|
||||
*
|
||||
* @return The metric's unit.
|
||||
*/
|
||||
public String unit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a value according to the metric's formatting rules. Implementations may choose to append units to the
|
||||
* returned value where relevant.
|
||||
*
|
||||
* @param value The value to format.
|
||||
* @return The formatted value.
|
||||
*/
|
||||
public String format(long value) {
|
||||
return format.apply(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getClass().getName() + ":" + name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of all metrics.
|
||||
*
|
||||
* @return A map of all metrics.
|
||||
*/
|
||||
public static Map<String, Metric> metrics() {
|
||||
return Collections.unmodifiableMap(allMetrics);
|
||||
}
|
||||
|
||||
public static final class Counter extends Metric {
|
||||
public Counter(String id) {
|
||||
super(id, "", Metric::formatDefault);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Event extends Metric {
|
||||
public Event(String id, String unit, LongFunction<String> format) {
|
||||
super(id, unit, format);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatTime(long value) {
|
||||
return String.format("%.1fms", value * 1e-6);
|
||||
}
|
||||
|
||||
public static String formatDefault(long value) {
|
||||
return String.format("%d", value);
|
||||
}
|
||||
|
||||
private static final int KILOBYTE_SIZE = 1024;
|
||||
private static final String SI_PREFIXES = "KMGT";
|
||||
|
||||
public static String formatBytes(long bytes) {
|
||||
if (bytes < KILOBYTE_SIZE) return String.format("%d B", bytes);
|
||||
var exp = (int) (Math.log((double) bytes) / Math.log(KILOBYTE_SIZE));
|
||||
if (exp > SI_PREFIXES.length()) exp = SI_PREFIXES.length();
|
||||
return String.format("%.1f %siB", bytes / Math.pow(KILOBYTE_SIZE, exp), SI_PREFIXES.charAt(exp - 1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.metrics;
|
||||
|
||||
/**
|
||||
* Built-in metrics that CC produces.
|
||||
*/
|
||||
public final class Metrics {
|
||||
private Metrics() {
|
||||
}
|
||||
|
||||
public static final Metric.Event COMPUTER_TASKS = new Metric.Event("computer_tasks", "ms", Metric::formatTime);
|
||||
public static final Metric.Event SERVER_TASKS = new Metric.Event("server_tasks", "ms", Metric::formatTime);
|
||||
|
||||
public static final Metric.Counter PERIPHERAL_OPS = new Metric.Counter("peripheral");
|
||||
public static final Metric.Counter FS_OPS = new Metric.Counter("fs");
|
||||
|
||||
public static final Metric.Counter HTTP_REQUESTS = new Metric.Counter("http_requests");
|
||||
public static final Metric.Event HTTP_UPLOAD = new Metric.Event("http_upload", "bytes", Metric::formatBytes);
|
||||
public static final Metric.Event HTTP_DOWNLOAD = new Metric.Event("http_download", "bytes", Metric::formatBytes);
|
||||
|
||||
public static final Metric.Event WEBSOCKET_INCOMING = new Metric.Event("websocket_incoming", "bytes", Metric::formatBytes);
|
||||
public static final Metric.Event WEBSOCKET_OUTGOING = new Metric.Event("websocket_outgoing", "bytes", Metric::formatBytes);
|
||||
|
||||
public static final Metric.Counter COROUTINES_CREATED = new Metric.Counter("coroutines_created");
|
||||
public static final Metric.Counter COROUTINES_DISPOSED = new Metric.Counter("coroutines_dead");
|
||||
|
||||
public static final Metric.Counter TURTLE_OPS = new Metric.Counter("turtle_ops");
|
||||
|
||||
/**
|
||||
* Ensures metrics are registered.
|
||||
*/
|
||||
public static void init() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.metrics;
|
||||
|
||||
import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||
|
||||
/**
|
||||
* A metrics observer is used to report metrics for a single computer.
|
||||
* <p>
|
||||
* Various components (such as the computer scheduler or http API) will report statistics about their behaviour to the
|
||||
* observer. The observer may choose to consume these metrics, aggregating them and presenting them to the user in some
|
||||
* manner.
|
||||
*
|
||||
* @see ComputerEnvironment#getMetrics()
|
||||
* @see Metrics Built-in metrics which will be reported.
|
||||
*/
|
||||
public interface MetricsObserver {
|
||||
/**
|
||||
* Increment a counter by 1.
|
||||
*
|
||||
* @param counter The counter to observe.
|
||||
*/
|
||||
void observe(Metric.Counter counter);
|
||||
|
||||
/**
|
||||
* Observe a single instance of an event.
|
||||
*
|
||||
* @param event The event to observe.
|
||||
* @param value The value corresponding to this event.
|
||||
*/
|
||||
void observe(Metric.Event event, long value);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.terminal;
|
||||
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class Palette {
|
||||
public static final int PALETTE_SIZE = 16;
|
||||
|
||||
private final boolean colour;
|
||||
private final double[][] colours = new double[PALETTE_SIZE][3];
|
||||
private final byte[][] byteColours = new byte[PALETTE_SIZE][4];
|
||||
|
||||
public static final Palette DEFAULT = new Palette(true);
|
||||
|
||||
public Palette(boolean colour) {
|
||||
this.colour = colour;
|
||||
resetColours();
|
||||
|
||||
for (var i = 0; i < PALETTE_SIZE; i++) byteColours[i][3] = (byte) 255;
|
||||
}
|
||||
|
||||
public void setColour(int i, double r, double g, double b) {
|
||||
if (i < 0 || i >= PALETTE_SIZE) return;
|
||||
colours[i][0] = r;
|
||||
colours[i][1] = g;
|
||||
colours[i][2] = b;
|
||||
|
||||
if (colour) {
|
||||
byteColours[i][0] = (byte) (int) (r * 255);
|
||||
byteColours[i][1] = (byte) (int) (g * 255);
|
||||
byteColours[i][2] = (byte) (int) (b * 255);
|
||||
} else {
|
||||
var grey = (byte) (int) ((r + g + b) / 3 * 255);
|
||||
byteColours[i][0] = byteColours[i][1] = byteColours[i][2] = grey;
|
||||
}
|
||||
}
|
||||
|
||||
public void setColour(int i, Colour colour) {
|
||||
setColour(i, colour.getR(), colour.getG(), colour.getB());
|
||||
}
|
||||
|
||||
public double[] getColour(int i) {
|
||||
return i >= 0 && i < PALETTE_SIZE ? colours[i] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the colour as a set of RGB values suitable for rendering. Colours are automatically converted to greyscale
|
||||
* when using a black and white palette.
|
||||
* <p>
|
||||
* This returns a byte array, suitable for being used directly by our terminal vertex format.
|
||||
*
|
||||
* @param i The colour index.
|
||||
* @return The number as a tuple of bytes.
|
||||
*/
|
||||
@Nonnull
|
||||
public byte[] getRenderColours(int i) {
|
||||
return byteColours[i];
|
||||
}
|
||||
|
||||
public void resetColour(int i) {
|
||||
if (i >= 0 && i < PALETTE_SIZE) setColour(i, Colour.VALUES[i]);
|
||||
}
|
||||
|
||||
public void resetColours() {
|
||||
for (var i = 0; i < Colour.VALUES.length; i++) {
|
||||
resetColour(i);
|
||||
}
|
||||
}
|
||||
|
||||
public static int encodeRGB8(double[] rgb) {
|
||||
var r = (int) (rgb[0] * 255) & 0xFF;
|
||||
var g = (int) (rgb[1] * 255) & 0xFF;
|
||||
var b = (int) (rgb[2] * 255) & 0xFF;
|
||||
|
||||
return (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
public static double[] decodeRGB8(int rgb) {
|
||||
return new double[]{
|
||||
((rgb >> 16) & 0xFF) / 255.0f,
|
||||
((rgb >> 8) & 0xFF) / 255.0f,
|
||||
(rgb & 0xFF) / 255.0f,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.terminal;
|
||||
|
||||
import dan200.computercraft.core.util.Colour;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class Terminal {
|
||||
protected static final String BASE_16 = "0123456789abcdef";
|
||||
|
||||
protected int width;
|
||||
protected int height;
|
||||
protected final boolean colour;
|
||||
|
||||
protected int cursorX = 0;
|
||||
protected int cursorY = 0;
|
||||
protected boolean cursorBlink = false;
|
||||
protected int cursorColour = 0;
|
||||
protected int cursorBackgroundColour = 15;
|
||||
|
||||
protected TextBuffer[] text;
|
||||
protected TextBuffer[] textColour;
|
||||
protected TextBuffer[] backgroundColour;
|
||||
|
||||
protected final Palette palette;
|
||||
|
||||
private final @Nullable Runnable onChanged;
|
||||
|
||||
public Terminal(int width, int height, boolean colour) {
|
||||
this(width, height, colour, null);
|
||||
}
|
||||
|
||||
public Terminal(int width, int height, boolean colour, Runnable changedCallback) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.colour = colour;
|
||||
palette = new Palette(colour);
|
||||
onChanged = changedCallback;
|
||||
|
||||
text = new TextBuffer[height];
|
||||
textColour = new TextBuffer[height];
|
||||
backgroundColour = new TextBuffer[height];
|
||||
for (var i = 0; i < this.height; i++) {
|
||||
text[i] = new TextBuffer(' ', this.width);
|
||||
textColour[i] = new TextBuffer(BASE_16.charAt(cursorColour), this.width);
|
||||
backgroundColour[i] = new TextBuffer(BASE_16.charAt(cursorBackgroundColour), this.width);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void reset() {
|
||||
cursorColour = 0;
|
||||
cursorBackgroundColour = 15;
|
||||
cursorX = 0;
|
||||
cursorY = 0;
|
||||
cursorBlink = false;
|
||||
clear();
|
||||
setChanged();
|
||||
palette.resetColours();
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public boolean isColour() {
|
||||
return colour;
|
||||
}
|
||||
|
||||
public synchronized void resize(int width, int height) {
|
||||
if (width == this.width && height == this.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
var oldHeight = this.height;
|
||||
var oldWidth = this.width;
|
||||
var oldText = text;
|
||||
var oldTextColour = textColour;
|
||||
var oldBackgroundColour = backgroundColour;
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
text = new TextBuffer[height];
|
||||
textColour = new TextBuffer[height];
|
||||
backgroundColour = new TextBuffer[height];
|
||||
for (var i = 0; i < this.height; i++) {
|
||||
if (i >= oldHeight) {
|
||||
text[i] = new TextBuffer(' ', this.width);
|
||||
textColour[i] = new TextBuffer(BASE_16.charAt(cursorColour), this.width);
|
||||
backgroundColour[i] = new TextBuffer(BASE_16.charAt(cursorBackgroundColour), this.width);
|
||||
} else if (this.width == oldWidth) {
|
||||
text[i] = oldText[i];
|
||||
textColour[i] = oldTextColour[i];
|
||||
backgroundColour[i] = oldBackgroundColour[i];
|
||||
} else {
|
||||
text[i] = new TextBuffer(' ', this.width);
|
||||
textColour[i] = new TextBuffer(BASE_16.charAt(cursorColour), this.width);
|
||||
backgroundColour[i] = new TextBuffer(BASE_16.charAt(cursorBackgroundColour), this.width);
|
||||
text[i].write(oldText[i]);
|
||||
textColour[i].write(oldTextColour[i]);
|
||||
backgroundColour[i].write(oldBackgroundColour[i]);
|
||||
}
|
||||
}
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public void setCursorPos(int x, int y) {
|
||||
if (cursorX != x || cursorY != y) {
|
||||
cursorX = x;
|
||||
cursorY = y;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCursorBlink(boolean blink) {
|
||||
if (cursorBlink != blink) {
|
||||
cursorBlink = blink;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setTextColour(int colour) {
|
||||
if (cursorColour != colour) {
|
||||
cursorColour = colour;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setBackgroundColour(int colour) {
|
||||
if (cursorBackgroundColour != colour) {
|
||||
cursorBackgroundColour = colour;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int getCursorX() {
|
||||
return cursorX;
|
||||
}
|
||||
|
||||
public int getCursorY() {
|
||||
return cursorY;
|
||||
}
|
||||
|
||||
public boolean getCursorBlink() {
|
||||
return cursorBlink;
|
||||
}
|
||||
|
||||
public int getTextColour() {
|
||||
return cursorColour;
|
||||
}
|
||||
|
||||
public int getBackgroundColour() {
|
||||
return cursorBackgroundColour;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Palette getPalette() {
|
||||
return palette;
|
||||
}
|
||||
|
||||
public synchronized void blit(ByteBuffer text, ByteBuffer textColour, ByteBuffer backgroundColour) {
|
||||
var x = cursorX;
|
||||
var y = cursorY;
|
||||
if (y >= 0 && y < height) {
|
||||
this.text[y].write(text, x);
|
||||
this.textColour[y].write(textColour, x);
|
||||
this.backgroundColour[y].write(backgroundColour, x);
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void write(String text) {
|
||||
var x = cursorX;
|
||||
var y = cursorY;
|
||||
if (y >= 0 && y < height) {
|
||||
this.text[y].write(text, x);
|
||||
textColour[y].fill(BASE_16.charAt(cursorColour), x, x + text.length());
|
||||
backgroundColour[y].fill(BASE_16.charAt(cursorBackgroundColour), x, x + text.length());
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void scroll(int yDiff) {
|
||||
if (yDiff != 0) {
|
||||
var newText = new TextBuffer[height];
|
||||
var newTextColour = new TextBuffer[height];
|
||||
var newBackgroundColour = new TextBuffer[height];
|
||||
for (var y = 0; y < height; y++) {
|
||||
var oldY = y + yDiff;
|
||||
if (oldY >= 0 && oldY < height) {
|
||||
newText[y] = text[oldY];
|
||||
newTextColour[y] = textColour[oldY];
|
||||
newBackgroundColour[y] = backgroundColour[oldY];
|
||||
} else {
|
||||
newText[y] = new TextBuffer(' ', width);
|
||||
newTextColour[y] = new TextBuffer(BASE_16.charAt(cursorColour), width);
|
||||
newBackgroundColour[y] = new TextBuffer(BASE_16.charAt(cursorBackgroundColour), width);
|
||||
}
|
||||
}
|
||||
text = newText;
|
||||
textColour = newTextColour;
|
||||
backgroundColour = newBackgroundColour;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
for (var y = 0; y < height; y++) {
|
||||
text[y].fill(' ');
|
||||
textColour[y].fill(BASE_16.charAt(cursorColour));
|
||||
backgroundColour[y].fill(BASE_16.charAt(cursorBackgroundColour));
|
||||
}
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public synchronized void clearLine() {
|
||||
var y = cursorY;
|
||||
if (y >= 0 && y < height) {
|
||||
text[y].fill(' ');
|
||||
textColour[y].fill(BASE_16.charAt(cursorColour));
|
||||
backgroundColour[y].fill(BASE_16.charAt(cursorBackgroundColour));
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized TextBuffer getLine(int y) {
|
||||
if (y >= 0 && y < height) {
|
||||
return text[y];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized void setLine(int y, String text, String textColour, String backgroundColour) {
|
||||
this.text[y].write(text);
|
||||
this.textColour[y].write(textColour);
|
||||
this.backgroundColour[y].write(backgroundColour);
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public synchronized TextBuffer getTextColourLine(int y) {
|
||||
if (y >= 0 && y < height) {
|
||||
return textColour[y];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized TextBuffer getBackgroundColourLine(int y) {
|
||||
if (y >= 0 && y < height) {
|
||||
return backgroundColour[y];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public final void setChanged() {
|
||||
if (onChanged != null) onChanged.run();
|
||||
}
|
||||
|
||||
public static int getColour(char c, Colour def) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return 15 - def.ordinal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.terminal;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class TextBuffer {
|
||||
private final char[] text;
|
||||
|
||||
public TextBuffer(char c, int length) {
|
||||
text = new char[length];
|
||||
fill(c);
|
||||
}
|
||||
|
||||
public TextBuffer(String text) {
|
||||
this.text = text.toCharArray();
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return text.length;
|
||||
}
|
||||
|
||||
public void write(String text) {
|
||||
write(text, 0);
|
||||
}
|
||||
|
||||
public void write(String text, int start) {
|
||||
var pos = start;
|
||||
start = Math.max(start, 0);
|
||||
var end = Math.min(start + text.length(), pos + text.length());
|
||||
end = Math.min(end, this.text.length);
|
||||
for (var i = start; i < end; i++) {
|
||||
this.text[i] = text.charAt(i - pos);
|
||||
}
|
||||
}
|
||||
|
||||
public void write(ByteBuffer text, int start) {
|
||||
var pos = start;
|
||||
var bufferPos = text.position();
|
||||
|
||||
start = Math.max(start, 0);
|
||||
var length = text.remaining();
|
||||
var end = Math.min(start + length, pos + length);
|
||||
end = Math.min(end, this.text.length);
|
||||
for (var i = start; i < end; i++) {
|
||||
this.text[i] = (char) (text.get(bufferPos + i - pos) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
public void write(TextBuffer text) {
|
||||
var end = Math.min(text.length(), this.text.length);
|
||||
for (var i = 0; i < end; i++) {
|
||||
this.text[i] = text.charAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void fill(char c) {
|
||||
fill(c, 0, text.length);
|
||||
}
|
||||
|
||||
public void fill(char c, int start, int end) {
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, text.length);
|
||||
for (var i = start; i < end; i++) {
|
||||
text[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
public char charAt(int i) {
|
||||
return text[i];
|
||||
}
|
||||
|
||||
public void setChar(int i, char c) {
|
||||
if (i >= 0 && i < text.length) {
|
||||
text[i] = c;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new String(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.util;
|
||||
|
||||
public enum Colour {
|
||||
BLACK(0x111111),
|
||||
RED(0xcc4c4c),
|
||||
GREEN(0x57A64E),
|
||||
BROWN(0x7f664c),
|
||||
BLUE(0x3366cc),
|
||||
PURPLE(0xb266e5),
|
||||
CYAN(0x4c99b2),
|
||||
LIGHT_GREY(0x999999),
|
||||
GREY(0x4c4c4c),
|
||||
PINK(0xf2b2cc),
|
||||
LIME(0x7fcc19),
|
||||
YELLOW(0xdede6c),
|
||||
LIGHT_BLUE(0x99b2f2),
|
||||
MAGENTA(0xe57fd8),
|
||||
ORANGE(0xf2b233),
|
||||
WHITE(0xf0f0f0);
|
||||
|
||||
public static final Colour[] VALUES = values();
|
||||
|
||||
public static Colour fromInt(int colour) {
|
||||
return Colour.VALUES[colour];
|
||||
}
|
||||
|
||||
public static Colour fromHex(int colour) {
|
||||
for (var entry : VALUES) {
|
||||
if (entry.getHex() == colour) return entry;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private final int hex;
|
||||
private final float red, green, blue;
|
||||
|
||||
Colour(int hex) {
|
||||
this.hex = hex;
|
||||
red = ((hex >> 16) & 0xFF) / 255.0f;
|
||||
green = ((hex >> 8) & 0xFF) / 255.0f;
|
||||
blue = (hex & 0xFF) / 255.0f;
|
||||
}
|
||||
|
||||
public Colour getNext() {
|
||||
return VALUES[(ordinal() + 1) % 16];
|
||||
}
|
||||
|
||||
public Colour getPrevious() {
|
||||
return VALUES[(ordinal() + 15) % 16];
|
||||
}
|
||||
|
||||
public int getHex() {
|
||||
return hex;
|
||||
}
|
||||
|
||||
public float getR() {
|
||||
return red;
|
||||
}
|
||||
|
||||
public float getG() {
|
||||
return green;
|
||||
}
|
||||
|
||||
public float getB() {
|
||||
return blue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.util;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
public final class IoUtil {
|
||||
private IoUtil() {
|
||||
}
|
||||
|
||||
public static void closeQuietly(@Nullable Closeable closeable) {
|
||||
try {
|
||||
if (closeable != null) closeable.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.util;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class LuaUtil {
|
||||
public static Object[] consArray(Object value, Collection<?> rest) {
|
||||
if (rest.isEmpty()) return new Object[]{ value };
|
||||
|
||||
// I'm not proud of this code.
|
||||
var out = new Object[rest.size() + 1];
|
||||
out[0] = value;
|
||||
var i = 1;
|
||||
for (Object additionalType : rest) out[i++] = additionalType;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.util;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class StringUtil {
|
||||
private StringUtil() {
|
||||
}
|
||||
|
||||
public static String normaliseLabel(String label) {
|
||||
if (label == null) return null;
|
||||
|
||||
var length = Math.min(32, label.length());
|
||||
var builder = new StringBuilder(length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
var c = label.charAt(i);
|
||||
if ((c >= ' ' && c <= '~') || (c >= 161 && c <= 172) || (c >= 174 && c <= 255)) {
|
||||
builder.append(c);
|
||||
} else {
|
||||
builder.append('?');
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String toString(@Nullable Object value) {
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||
* Send enquiries to dratcliffe@gmail.com
|
||||
*/
|
||||
package dan200.computercraft.core.util;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
|
||||
/**
|
||||
* Provides some utilities to create thread groups.
|
||||
*/
|
||||
public final class ThreadUtils {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ThreadUtils.class);
|
||||
private static final ThreadGroup baseGroup = new ThreadGroup("ComputerCraft");
|
||||
|
||||
private ThreadUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base thread group, that all off-thread ComputerCraft activities are run on.
|
||||
*
|
||||
* @return The ComputerCraft group.
|
||||
*/
|
||||
public static ThreadGroup group() {
|
||||
return baseGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a group under ComputerCraft's shared group.
|
||||
*
|
||||
* @param name The group's name. This will be prefixed with "ComputerCraft-".
|
||||
* @return The constructed thread group.
|
||||
*/
|
||||
public static ThreadGroup group(String name) {
|
||||
return new ThreadGroup(baseGroup, baseGroup.getName() + "-" + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ThreadFactoryBuilder}, which constructs threads under a group of the given {@code name}.
|
||||
* <p>
|
||||
* Each thread will be of the format {@code ComputerCraft-<name>-<number>}, and belong to a group
|
||||
* called {@code ComputerCraft-<name>} (which in turn will be a child group of the main {@code ComputerCraft} group.
|
||||
*
|
||||
* @param name The name for the thread group and child threads.
|
||||
* @return The constructed thread factory builder, which may be extended with other properties.
|
||||
* @see #factory(String)
|
||||
*/
|
||||
public static ThreadFactoryBuilder builder(String name) {
|
||||
var group = group(name);
|
||||
return new ThreadFactoryBuilder()
|
||||
.setDaemon(true)
|
||||
.setNameFormat(group.getName().replace("%", "%%") + "-%d")
|
||||
.setUncaughtExceptionHandler((t, e) -> LOG.error("Exception in thread " + t.getName(), e))
|
||||
.setThreadFactory(x -> new Thread(group, x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ThreadFactory}, which constructs threads under a group of the given {@code name}.
|
||||
* <p>
|
||||
* Each thread will be of the format {@code ComputerCraft-<name>-<number>}, and belong to a group
|
||||
* called {@code ComputerCraft-<name>} (which in turn will be a child group of the main {@code ComputerCraft} group.
|
||||
*
|
||||
* @param name The name for the thread group and child threads.
|
||||
* @return The constructed thread factory.
|
||||
* @see #builder(String)
|
||||
*/
|
||||
public static ThreadFactory factory(String name) {
|
||||
return builder(name).build();
|
||||
}
|
||||
}
|
||||
1021
projects/core/src/main/resources/data/computercraft/lua/bios.lua
Normal file
1021
projects/core/src/main/resources/data/computercraft/lua/bios.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,361 @@
|
||||
--[[- Constants and functions for colour values, suitable for working with
|
||||
@{term} and @{redstone}.
|
||||
|
||||
This is useful in conjunction with @{redstone.setBundledOutput|Bundled Cables}
|
||||
from mods like Project Red, and @{term.setTextColour|colors on Advanced
|
||||
Computers and Advanced Monitors}.
|
||||
|
||||
For the non-American English version just replace @{colors} with @{colours}.
|
||||
This alternative API is exactly the same, except the colours use British English
|
||||
(e.g. @{colors.gray} is spelt @{colours.grey}).
|
||||
|
||||
On basic terminals (such as the Computer and Monitor), all the colors are
|
||||
converted to grayscale. This means you can still use all 16 colors on the
|
||||
screen, but they will appear as the nearest tint of gray. You can check if a
|
||||
terminal supports color by using the function @{term.isColor}.
|
||||
|
||||
Grayscale colors are calculated by taking the average of the three components,
|
||||
i.e. `(red + green + blue) / 3`.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th colspan="8" align="center">Default Colors</th></tr>
|
||||
<tr>
|
||||
<th rowspan="2" align="center">Color</th>
|
||||
<th colspan="3" align="center">Value</th>
|
||||
<th colspan="4" align="center">Default Palette Color</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Dec</th><th>Hex</th><th>Paint/Blit</th>
|
||||
<th>Preview</th><th>Hex</th><th>RGB</th><th>Grayscale</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>colors.white</code></td>
|
||||
<td align="right">1</td><td align="right">0x1</td><td align="right">0</td>
|
||||
<td style="background:#F0F0F0"></td><td>#F0F0F0</td><td>240, 240, 240</td>
|
||||
<td style="background:#F0F0F0"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.orange</code></td>
|
||||
<td align="right">2</td><td align="right">0x2</td><td align="right">1</td>
|
||||
<td style="background:#F2B233"></td><td>#F2B233</td><td>242, 178, 51</td>
|
||||
<td style="background:#9D9D9D"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.magenta</code></td>
|
||||
<td align="right">4</td><td align="right">0x4</td><td align="right">2</td>
|
||||
<td style="background:#E57FD8"></td><td>#E57FD8</td><td>229, 127, 216</td>
|
||||
<td style="background:#BEBEBE"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.lightBlue</code></td>
|
||||
<td align="right">8</td><td align="right">0x8</td><td align="right">3</td>
|
||||
<td style="background:#99B2F2"></td><td>#99B2F2</td><td>153, 178, 242</td>
|
||||
<td style="background:#BFBFBF"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.yellow</code></td>
|
||||
<td align="right">16</td><td align="right">0x10</td><td align="right">4</td>
|
||||
<td style="background:#DEDE6C"></td><td>#DEDE6C</td><td>222, 222, 108</td>
|
||||
<td style="background:#B8B8B8"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.lime</code></td>
|
||||
<td align="right">32</td><td align="right">0x20</td><td align="right">5</td>
|
||||
<td style="background:#7FCC19"></td><td>#7FCC19</td><td>127, 204, 25</td>
|
||||
<td style="background:#767676"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.pink</code></td>
|
||||
<td align="right">64</td><td align="right">0x40</td><td align="right">6</td>
|
||||
<td style="background:#F2B2CC"></td><td>#F2B2CC</td><td>242, 178, 204</td>
|
||||
<td style="background:#D0D0D0"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.gray</code></td>
|
||||
<td align="right">128</td><td align="right">0x80</td><td align="right">7</td>
|
||||
<td style="background:#4C4C4C"></td><td>#4C4C4C</td><td>76, 76, 76</td>
|
||||
<td style="background:#4C4C4C"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.lightGray</code></td>
|
||||
<td align="right">256</td><td align="right">0x100</td><td align="right">8</td>
|
||||
<td style="background:#999999"></td><td>#999999</td><td>153, 153, 153</td>
|
||||
<td style="background:#999999"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.cyan</code></td>
|
||||
<td align="right">512</td><td align="right">0x200</td><td align="right">9</td>
|
||||
<td style="background:#4C99B2"></td><td>#4C99B2</td><td>76, 153, 178</td>
|
||||
<td style="background:#878787"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.purple</code></td>
|
||||
<td align="right">1024</td><td align="right">0x400</td><td align="right">a</td>
|
||||
<td style="background:#B266E5"></td><td>#B266E5</td><td>178, 102, 229</td>
|
||||
<td style="background:#A9A9A9"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.blue</code></td>
|
||||
<td align="right">2048</td><td align="right">0x800</td><td align="right">b</td>
|
||||
<td style="background:#3366CC"></td><td>#3366CC</td><td>51, 102, 204</td>
|
||||
<td style="background:#777777"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.brown</code></td>
|
||||
<td align="right">4096</td><td align="right">0x1000</td><td align="right">c</td>
|
||||
<td style="background:#7F664C"></td><td>#7F664C</td><td>127, 102, 76</td>
|
||||
<td style="background:#656565"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.green</code></td>
|
||||
<td align="right">8192</td><td align="right">0x2000</td><td align="right">d</td>
|
||||
<td style="background:#57A64E"></td><td>#57A64E</td><td>87, 166, 78</td>
|
||||
<td style="background:#6E6E6E"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.red</code></td>
|
||||
<td align="right">16384</td><td align="right">0x4000</td><td align="right">e</td>
|
||||
<td style="background:#CC4C4C"></td><td>#CC4C4C</td><td>204, 76, 76</td>
|
||||
<td style="background:#767676"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>colors.black</code></td>
|
||||
<td align="right">32768</td><td align="right">0x8000</td><td align="right">f</td>
|
||||
<td style="background:#111111"></td><td>#111111</td><td>17, 17, 17</td>
|
||||
<td style="background:#111111"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@see colours
|
||||
@module colors
|
||||
]]
|
||||
|
||||
local expect = dofile("rom/modules/main/cc/expect.lua").expect
|
||||
|
||||
--- White: Written as `0` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #F0F0F0.
|
||||
white = 0x1
|
||||
|
||||
--- Orange: Written as `1` in paint files and @{term.blit}, has a
|
||||
-- default terminal colour of #F2B233.
|
||||
orange = 0x2
|
||||
|
||||
--- Magenta: Written as `2` in paint files and @{term.blit}, has a
|
||||
-- default terminal colour of #E57FD8.
|
||||
magenta = 0x4
|
||||
|
||||
--- Light blue: Written as `3` in paint files and @{term.blit}, has a
|
||||
-- default terminal colour of #99B2F2.
|
||||
lightBlue = 0x8
|
||||
|
||||
--- Yellow: Written as `4` in paint files and @{term.blit}, has a
|
||||
-- default terminal colour of #DEDE6C.
|
||||
yellow = 0x10
|
||||
|
||||
--- Lime: Written as `5` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #7FCC19.
|
||||
lime = 0x20
|
||||
|
||||
--- Pink: Written as `6` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #F2B2CC.
|
||||
pink = 0x40
|
||||
|
||||
--- Gray: Written as `7` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #4C4C4C.
|
||||
gray = 0x80
|
||||
|
||||
--- Light gray: Written as `8` in paint files and @{term.blit}, has a
|
||||
-- default terminal colour of #999999.
|
||||
lightGray = 0x100
|
||||
|
||||
--- Cyan: Written as `9` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #4C99B2.
|
||||
cyan = 0x200
|
||||
|
||||
--- Purple: Written as `a` in paint files and @{term.blit}, has a
|
||||
-- default terminal colour of #B266E5.
|
||||
purple = 0x400
|
||||
|
||||
--- Blue: Written as `b` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #3366CC.
|
||||
blue = 0x800
|
||||
|
||||
--- Brown: Written as `c` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #7F664C.
|
||||
brown = 0x1000
|
||||
|
||||
--- Green: Written as `d` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #57A64E.
|
||||
green = 0x2000
|
||||
|
||||
--- Red: Written as `e` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #CC4C4C.
|
||||
red = 0x4000
|
||||
|
||||
--- Black: Written as `f` in paint files and @{term.blit}, has a default
|
||||
-- terminal colour of #111111.
|
||||
black = 0x8000
|
||||
|
||||
--- Combines a set of colors (or sets of colors) into a larger set. Useful for
|
||||
-- Bundled Cables.
|
||||
--
|
||||
-- @tparam number ... The colors to combine.
|
||||
-- @treturn number The union of the color sets given in `...`
|
||||
-- @since 1.2
|
||||
-- @usage
|
||||
-- ```lua
|
||||
-- colors.combine(colors.white, colors.magenta, colours.lightBlue)
|
||||
-- -- => 13
|
||||
-- ```
|
||||
function combine(...)
|
||||
local r = 0
|
||||
for i = 1, select('#', ...) do
|
||||
local c = select(i, ...)
|
||||
expect(i, c, "number")
|
||||
r = bit32.bor(r, c)
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
--- Removes one or more colors (or sets of colors) from an initial set. Useful
|
||||
-- for Bundled Cables.
|
||||
--
|
||||
-- Each parameter beyond the first may be a single color or may be a set of
|
||||
-- colors (in the latter case, all colors in the set are removed from the
|
||||
-- original set).
|
||||
--
|
||||
-- @tparam number colors The color from which to subtract.
|
||||
-- @tparam number ... The colors to subtract.
|
||||
-- @treturn number The resulting color.
|
||||
-- @since 1.2
|
||||
-- @usage
|
||||
-- ```lua
|
||||
-- colours.subtract(colours.lime, colours.orange, colours.white)
|
||||
-- -- => 32
|
||||
-- ```
|
||||
function subtract(colors, ...)
|
||||
expect(1, colors, "number")
|
||||
local r = colors
|
||||
for i = 1, select('#', ...) do
|
||||
local c = select(i, ...)
|
||||
expect(i + 1, c, "number")
|
||||
r = bit32.band(r, bit32.bnot(c))
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
--- Tests whether `color` is contained within `colors`. Useful for Bundled
|
||||
-- Cables.
|
||||
--
|
||||
-- @tparam number colors A color, or color set
|
||||
-- @tparam number color A color or set of colors that `colors` should contain.
|
||||
-- @treturn boolean If `colors` contains all colors within `color`.
|
||||
-- @since 1.2
|
||||
-- @usage
|
||||
-- ```lua
|
||||
-- colors.test(colors.combine(colors.white, colors.magenta, colours.lightBlue), colors.lightBlue)
|
||||
-- -- => true
|
||||
-- ```
|
||||
function test(colors, color)
|
||||
expect(1, colors, "number")
|
||||
expect(2, color, "number")
|
||||
return bit32.band(colors, color) == color
|
||||
end
|
||||
|
||||
--- Combine a three-colour RGB value into one hexadecimal representation.
|
||||
--
|
||||
-- @tparam number r The red channel, should be between 0 and 1.
|
||||
-- @tparam number g The red channel, should be between 0 and 1.
|
||||
-- @tparam number b The blue channel, should be between 0 and 1.
|
||||
-- @treturn number The combined hexadecimal colour.
|
||||
-- @usage
|
||||
-- ```lua
|
||||
-- colors.packRGB(0.7, 0.2, 0.6)
|
||||
-- -- => 0xb23399
|
||||
-- ```
|
||||
-- @since 1.81.0
|
||||
function packRGB(r, g, b)
|
||||
expect(1, r, "number")
|
||||
expect(2, g, "number")
|
||||
expect(3, b, "number")
|
||||
return
|
||||
bit32.band(r * 255, 0xFF) * 2 ^ 16 +
|
||||
bit32.band(g * 255, 0xFF) * 2 ^ 8 +
|
||||
bit32.band(b * 255, 0xFF)
|
||||
end
|
||||
|
||||
--- Separate a hexadecimal RGB colour into its three constituent channels.
|
||||
--
|
||||
-- @tparam number rgb The combined hexadecimal colour.
|
||||
-- @treturn number The red channel, will be between 0 and 1.
|
||||
-- @treturn number The red channel, will be between 0 and 1.
|
||||
-- @treturn number The blue channel, will be between 0 and 1.
|
||||
-- @usage
|
||||
-- ```lua
|
||||
-- colors.unpackRGB(0xb23399)
|
||||
-- -- => 0.7, 0.2, 0.6
|
||||
-- ```
|
||||
-- @see colors.packRGB
|
||||
-- @since 1.81.0
|
||||
function unpackRGB(rgb)
|
||||
expect(1, rgb, "number")
|
||||
return
|
||||
bit32.band(bit32.rshift(rgb, 16), 0xFF) / 255,
|
||||
bit32.band(bit32.rshift(rgb, 8), 0xFF) / 255,
|
||||
bit32.band(rgb, 0xFF) / 255
|
||||
end
|
||||
|
||||
--- Either calls @{colors.packRGB} or @{colors.unpackRGB}, depending on how many
|
||||
-- arguments it receives.
|
||||
--
|
||||
-- @tparam[1] number r The red channel, as an argument to @{colors.packRGB}.
|
||||
-- @tparam[1] number g The green channel, as an argument to @{colors.packRGB}.
|
||||
-- @tparam[1] number b The blue channel, as an argument to @{colors.packRGB}.
|
||||
-- @tparam[2] number rgb The combined hexadecimal color, as an argument to @{colors.unpackRGB}.
|
||||
-- @treturn[1] number The combined hexadecimal colour, as returned by @{colors.packRGB}.
|
||||
-- @treturn[2] number The red channel, as returned by @{colors.unpackRGB}
|
||||
-- @treturn[2] number The green channel, as returned by @{colors.unpackRGB}
|
||||
-- @treturn[2] number The blue channel, as returned by @{colors.unpackRGB}
|
||||
-- @deprecated Use @{packRGB} or @{unpackRGB} directly.
|
||||
-- @usage
|
||||
-- ```lua
|
||||
-- colors.rgb8(0xb23399)
|
||||
-- -- => 0.7, 0.2, 0.6
|
||||
-- ```
|
||||
-- @usage
|
||||
-- ```lua
|
||||
-- colors.rgb8(0.7, 0.2, 0.6)
|
||||
-- -- => 0xb23399
|
||||
-- ```
|
||||
-- @since 1.80pr1
|
||||
-- @changed 1.81.0 Deprecated in favor of colors.(un)packRGB.
|
||||
function rgb8(r, g, b)
|
||||
if g == nil and b == nil then
|
||||
return unpackRGB(r)
|
||||
else
|
||||
return packRGB(r, g, b)
|
||||
end
|
||||
end
|
||||
|
||||
-- Colour to hex lookup table for toBlit
|
||||
local color_hex_lookup = {}
|
||||
for i = 0, 15 do
|
||||
color_hex_lookup[2 ^ i] = string.format("%x", i)
|
||||
end
|
||||
|
||||
--- Converts the given color to a paint/blit hex character (0-9a-f).
|
||||
--
|
||||
-- This is equivalent to converting floor(log_2(color)) to hexadecimal.
|
||||
--
|
||||
-- @tparam number color The color to convert.
|
||||
-- @treturn string The blit hex code of the color.
|
||||
-- @since 1.94.0
|
||||
function toBlit(color)
|
||||
expect(1, color, "number")
|
||||
return color_hex_lookup[color] or
|
||||
string.format("%x", math.floor(math.log(color) / math.log(2)))
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user