From 67af7a698bff0881076e9d267dc276b94c1ddcc6 Mon Sep 17 00:00:00 2001 From: SquidDev Date: Tue, 26 Feb 2019 10:31:29 +0000 Subject: [PATCH] Migrate the computer tasks into a separate thread - Move state management (turnOn, shutdown, etc...) event handling and the command queue into a ComputerExecutor - This means the computer thread now just handles running "work" on computer executors, rather than managing a separate command queue + requeuing it. --- .../computercraft/core/computer/Computer.java | 497 +++----------- .../core/computer/ComputerExecutor.java | 609 ++++++++++++++++++ .../core/computer/ComputerThread.java | 126 +--- .../shared/computer/core/ServerComputer.java | 12 +- .../core/computer/ComputerBootstrap.java | 2 +- 5 files changed, 724 insertions(+), 522 deletions(-) create mode 100644 src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java diff --git a/src/main/java/dan200/computercraft/core/computer/Computer.java b/src/main/java/dan200/computercraft/core/computer/Computer.java index f84390d02..d05d77317 100644 --- a/src/main/java/dan200/computercraft/core/computer/Computer.java +++ b/src/main/java/dan200/computercraft/core/computer/Computer.java @@ -7,57 +7,46 @@ package dan200.computercraft.core.computer; import com.google.common.base.Objects; -import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.filesystem.IMount; -import dan200.computercraft.api.filesystem.IWritableMount; import dan200.computercraft.api.lua.ILuaAPI; -import dan200.computercraft.api.lua.ILuaAPIFactory; import dan200.computercraft.api.peripheral.IPeripheral; -import dan200.computercraft.core.apis.*; +import dan200.computercraft.core.apis.IAPIEnvironment; import dan200.computercraft.core.filesystem.FileSystem; -import dan200.computercraft.core.filesystem.FileSystemException; -import dan200.computercraft.core.lua.CobaltLuaMachine; -import dan200.computercraft.core.lua.ILuaMachine; import dan200.computercraft.core.terminal.Terminal; -import javax.annotation.Nonnull; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +/** + * Represents a computer which may exist in-world or elsewhere. + * + * Note, this class has several (read: far, far too many) responsibilities, so can get a little unwieldy at times. + * + * + */ public class Computer { - private enum State - { - Off, - Starting, - Running, - Stopping, - } - - private static IMount s_romMount = null; + private static final int START_DELAY = 50; + // Various properties of the computer private int m_id; private String m_label = null; + + // Read-only fields about the computer private final IComputerEnvironment m_environment; - - private int m_ticksSinceStart = -1; - private boolean m_startRequested = false; - private State m_state = State.Off; - private boolean m_blinking = false; - - private ILuaMachine m_machine = null; - private final List m_apis = new ArrayList<>(); - final TimeoutState timeout = new TimeoutState(); - private final Environment m_internalEnvironment = new Environment( this ); - private final Terminal m_terminal; - private FileSystem m_fileSystem = null; - private IWritableMount m_rootMount = null; + private final ComputerExecutor executor; - private final AtomicBoolean externalOutputChanged = new AtomicBoolean(); + // Additional state about the computer and its environment. + private boolean m_blinking = false; + private final Environment internalEnvironment = new Environment( this ); + private AtomicBoolean externalOutputChanged = new AtomicBoolean(); + + private boolean startRequested; + private int m_ticksSinceStart = -1; public Computer( IComputerEnvironment environment, Terminal terminal, int id ) { @@ -65,24 +54,7 @@ public class Computer m_environment = environment; m_terminal = terminal; - // Ensure the computer thread is running as required. - ComputerThread.start(); - - // Add all default APIs to the loaded list. - m_apis.add( new TermAPI( m_internalEnvironment ) ); - m_apis.add( new RedstoneAPI( m_internalEnvironment ) ); - m_apis.add( new FSAPI( m_internalEnvironment ) ); - m_apis.add( new PeripheralAPI( m_internalEnvironment ) ); - m_apis.add( new OSAPI( m_internalEnvironment ) ); - if( ComputerCraft.http_enable ) m_apis.add( new HTTPAPI( m_internalEnvironment ) ); - - // Load in the API registered APIs. - for( ILuaAPIFactory factory : ApiFactories.getAll() ) - { - ComputerSystem system = new ComputerSystem( m_internalEnvironment ); - ILuaAPI api = factory.create( system ); - if( api != null ) m_apis.add( new ApiWrapper( api, system ) ); - } + executor = new ComputerExecutor( this ); } IComputerEnvironment getComputerEnvironment() @@ -92,7 +64,7 @@ public class Computer FileSystem getFileSystem() { - return m_fileSystem; + return executor.getFileSystem(); } Terminal getTerminal() @@ -102,40 +74,42 @@ public class Computer public Environment getEnvironment() { - return m_internalEnvironment; + return internalEnvironment; } public IAPIEnvironment getAPIEnvironment() { - return m_internalEnvironment; - } - - public void turnOn() - { - if( m_state == State.Off ) m_startRequested = true; - } - - public void shutdown() - { - stopComputer( false ); - } - - public void reboot() - { - stopComputer( true ); + return internalEnvironment; } public boolean isOn() { - synchronized( this ) - { - return m_state == State.Running; - } + 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() { - stopComputer( false ); + executor.queueStop( false, true ); + } + + public void queueEvent( String event, Object[] args ) + { + executor.queueEvent( event, args ); } public int getID() @@ -173,38 +147,28 @@ public class Computer public void tick() { - synchronized( this ) + // We keep track of the number of ticks since the last start, only + if( m_ticksSinceStart >= 0 && m_ticksSinceStart <= START_DELAY ) m_ticksSinceStart++; + + if( startRequested && !executor.isOn() && (m_ticksSinceStart < 0 || m_ticksSinceStart > START_DELAY) ) { - // Start after a number of ticks - if( m_ticksSinceStart >= 0 ) - { - m_ticksSinceStart++; - } - if( m_startRequested && (m_ticksSinceStart < 0 || m_ticksSinceStart > 50) ) - { - startComputer(); - m_startRequested = false; - } - - if( m_state == State.Running ) - { - // Update the environment's internal state. - m_internalEnvironment.update(); - - // Advance our APIs - for( ILuaAPI api : m_apis ) api.update(); - } + m_ticksSinceStart = 0; + startRequested = false; + executor.queueStart(); } - // Prepare to propagate the environment's output to the world. - if( m_internalEnvironment.updateOutput() ) externalOutputChanged.set( true ); + executor.tick(); + + // Update the environment's internal state. + internalEnvironment.update(); + + // 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 - boolean blinking = - m_terminal.getCursorBlink() && - m_terminal.getCursorX() >= 0 && m_terminal.getCursorX() < m_terminal.getWidth() && - m_terminal.getCursorY() >= 0 && m_terminal.getCursorY() < m_terminal.getHeight(); - + boolean blinking = m_terminal.getCursorBlink() && + m_terminal.getCursorX() >= 0 && m_terminal.getCursorX() < m_terminal.getWidth() && + m_terminal.getCursorY() >= 0 && m_terminal.getCursorY() < m_terminal.getHeight(); if( blinking != m_blinking ) { m_blinking = blinking; @@ -212,6 +176,11 @@ public class Computer } } + void markChanged() + { + externalOutputChanged.set( true ); + } + public boolean pollAndResetChanged() { return externalOutputChanged.getAndSet( false ); @@ -222,336 +191,27 @@ public class Computer return isOn() && m_blinking; } - public IWritableMount getRootMount() + public void addApi( ILuaAPI api ) { - if( m_rootMount == null ) - { - m_rootMount = m_environment.createSaveDirMount( "computer/" + assignID(), m_environment.getComputerSpaceLimit() ); - } - return m_rootMount; - } - - // FileSystem - - private boolean initFileSystem() - { - // Create the file system - assignID(); - try - { - m_fileSystem = new FileSystem( "hdd", getRootMount() ); - if( s_romMount == null ) s_romMount = m_environment.createResourceMount( "computercraft", "lua/rom" ); - if( s_romMount != null ) - { - m_fileSystem.mount( "rom", "rom", s_romMount ); - return true; - } - return false; - } - catch( FileSystemException e ) - { - ComputerCraft.log.error( "Cannot mount rom", e ); - return false; - } - } - - // Peripherals - - public void addAPI( ILuaAPI api ) - { - m_apis.add( api ); - } - - // Lua - - private void initLua() - { - // Create the lua machine - ILuaMachine machine = new CobaltLuaMachine( this, timeout ); - - // Add the APIs - for( ILuaAPI api : m_apis ) - { - machine.addAPI( api ); - api.startup(); - } - - // Load the bios resource - InputStream biosStream; - try - { - biosStream = m_environment.createResourceFile( "computercraft", "lua/bios.lua" ); - } - catch( Exception e ) - { - biosStream = null; - } - - // Start the machine running the bios resource - if( biosStream != null ) - { - machine.loadBios( biosStream ); - try - { - biosStream.close(); - } - catch( IOException e ) - { - // meh - } - - if( machine.isFinished() ) - { - m_terminal.reset(); - m_terminal.write( "Error starting bios.lua" ); - m_terminal.setCursorPos( 0, 1 ); - m_terminal.write( "ComputerCraft may be installed incorrectly" ); - - machine.close(); - m_machine = null; - } - else - { - m_machine = machine; - } - } - else - { - m_terminal.reset(); - m_terminal.write( "Error loading bios.lua" ); - m_terminal.setCursorPos( 0, 1 ); - m_terminal.write( "ComputerCraft may be installed incorrectly" ); - - machine.close(); - m_machine = null; - } - } - - private void startComputer() - { - synchronized( this ) - { - if( m_state != State.Off ) - { - return; - } - m_state = State.Starting; - externalOutputChanged.set( true ); - m_ticksSinceStart = 0; - } - - // Turn the computer on - ComputerThread.queueTask( new ITask() - { - @Nonnull - @Override - public Computer getOwner() - { - return Computer.this; - } - - @Override - public void execute() - { - synchronized( this ) - { - if( m_state != State.Starting ) - { - return; - } - - // Init terminal - m_terminal.reset(); - - // Init filesystem - if( !initFileSystem() ) - { - // Init failed, so shutdown - m_terminal.reset(); - m_terminal.write( "Error mounting lua/rom" ); - m_terminal.setCursorPos( 0, 1 ); - m_terminal.write( "ComputerCraft may be installed incorrectly" ); - - m_state = State.Running; - stopComputer( false ); - return; - } - - // Init lua - initLua(); - if( m_machine == null ) - { - m_terminal.reset(); - m_terminal.write( "Error loading bios.lua" ); - m_terminal.setCursorPos( 0, 1 ); - m_terminal.write( "ComputerCraft may be installed incorrectly" ); - - // Init failed, so shutdown - m_state = State.Running; - stopComputer( false ); - return; - } - - // Start a new state - m_state = State.Running; - externalOutputChanged.set( true ); - synchronized( m_machine ) - { - m_machine.handleEvent( null, null ); - } - } - } - } ); - } - - /** - * Abort this whole computer due to a timeout. This will immediately destroy the Lua machine, - * and then schedule a shutdown. - */ - void abort() - { - // TODO: We need to test this much more thoroughly. - ILuaMachine machine = m_machine; - if( machine != null ) machine.close(); - shutdown(); - } - - private void stopComputer( final boolean reboot ) - { - synchronized( this ) - { - if( m_state != State.Running ) - { - return; - } - m_state = State.Stopping; - externalOutputChanged.set( true ); - } - - // Turn the computercraft off - ComputerThread.queueTask( new ITask() - { - @Nonnull - @Override - public Computer getOwner() - { - return Computer.this; - } - - @Override - public void execute() - { - synchronized( this ) - { - if( m_state != State.Stopping ) - { - return; - } - - // Shutdown our APIs - synchronized( m_apis ) - { - for( ILuaAPI api : m_apis ) - { - api.shutdown(); - } - } - - // Shutdown terminal and filesystem - if( m_fileSystem != null ) - { - m_fileSystem.close(); - m_fileSystem = null; - } - - if( m_machine != null ) - { - m_terminal.reset(); - - synchronized( m_machine ) - { - m_machine.close(); - m_machine = null; - } - } - - // Reset redstone output - m_internalEnvironment.resetOutput(); - - m_state = State.Off; - externalOutputChanged.set( true ); - if( reboot ) - { - m_startRequested = true; - } - } - } - } ); - } - - public void queueEvent( final String event, final Object[] arguments ) - { - synchronized( this ) - { - if( m_state != State.Running ) - { - return; - } - } - - ComputerThread.queueTask( new ITask() - { - @Nonnull - @Override - public Computer getOwner() - { - return Computer.this; - } - - @Override - public void execute() - { - synchronized( this ) - { - if( m_state != State.Running ) - { - return; - } - } - - synchronized( m_machine ) - { - m_machine.handleEvent( event, arguments ); - if( m_machine.isFinished() ) - { - m_terminal.reset(); - m_terminal.write( "Error resuming bios.lua" ); - m_terminal.setCursorPos( 0, 1 ); - m_terminal.write( "ComputerCraft may be installed incorrectly" ); - - stopComputer( false ); - } - } - } - } ); + executor.addApi( api ); } @Deprecated public IPeripheral getPeripheral( int side ) { - return m_internalEnvironment.getPeripheral( side ); + return internalEnvironment.getPeripheral( side ); } @Deprecated public void setPeripheral( int side, IPeripheral peripheral ) { - m_internalEnvironment.setPeripheral( side, peripheral ); + internalEnvironment.setPeripheral( side, peripheral ); } @Deprecated public void addAPI( dan200.computercraft.core.apis.ILuaAPI api ) { - addAPI( (ILuaAPI) api ); + addApi( api ); } @Deprecated @@ -561,5 +221,6 @@ public class Computer tick(); } + @Deprecated public static final String[] s_sideNames = IAPIEnvironment.SIDE_NAMES; } diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java new file mode 100644 index 000000000..935a985cd --- /dev/null +++ b/src/main/java/dan200/computercraft/core/computer/ComputerExecutor.java @@ -0,0 +1,609 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.core.computer; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.ILuaAPI; +import dan200.computercraft.api.lua.ILuaAPIFactory; +import dan200.computercraft.core.apis.*; +import dan200.computercraft.core.filesystem.FileSystem; +import dan200.computercraft.core.filesystem.FileSystemException; +import dan200.computercraft.core.lua.CobaltLuaMachine; +import dan200.computercraft.core.lua.ILuaMachine; +import dan200.computercraft.core.terminal.Terminal; +import dan200.computercraft.core.tracking.Tracking; +import dan200.computercraft.shared.util.IoUtil; + +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.AtomicBoolean; +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. + * + * 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. + * + * 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)}. + * + * When a computer is on, we simply push any events onto to the {@link #eventQueue}. + * + * 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. + * + * 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 int QUEUE_LIMIT = 256; + + private static IMount romMount; + private static final Object romMountLock = new Object(); + + private final Computer computer; + private final List apis = new ArrayList<>(); + final TimeoutState timeout = new TimeoutState(); + + 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. + * + * 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 executer is present within {@link ComputerThread}. + */ + volatile boolean onComputerQueue = false; + + /** + * The command that {@link #work()} should execute on the computer thread. + * + * 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. + * + * 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. + * + * Note, this should be empty if this computer is off - it is cleared on shutdown and when turning on again. + */ + private final Queue eventQueue = new ArrayDeque<>(); + + /** + * 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; + + /** + * {@code true} when inside {@link #work()}. We use this to ensure we're only doing one bit of work at one time. + */ + private final AtomicBoolean isExecuting = new AtomicBoolean( false ); + + ComputerExecutor( Computer computer ) + { + // Ensure the computer thread is running as required. + ComputerThread.start(); + + this.computer = computer; + + Environment 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( ComputerCraft.http_enable ) apis.add( new HTTPAPI( environment ) ); + + // Load in the externally registered APIs. + for( ILuaAPIFactory factory : ApiFactories.getAll() ) + { + ComputerSystem system = new ComputerSystem( environment ); + ILuaAPI 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 || this.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; + this.closed = close; + + StateCommand 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() + { + // TODO: We need to test this much more thoroughly. + ILuaMachine machine = this.machine; + if( machine != null ) machine.close(); + + synchronized( queueLock ) + { + if( closed ) return; + command = StateCommand.ABORT; + 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 ) return; + onComputerQueue = true; + ComputerThread.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( ILuaAPI api : apis ) api.update(); + } + } + finally + { + isOnLock.unlock(); + } + } + } + + private IMount getRomMount() + { + if( romMount != null ) return romMount; + + synchronized( romMountLock ) + { + if( romMount != null ) return romMount; + return romMount = computer.getComputerEnvironment().createResourceMount( "computercraft", "lua/rom" ); + } + } + + private FileSystem createFileSystem() + { + if( rootMount == null ) + { + rootMount = computer.getComputerEnvironment().createSaveDirMount( + "computer/" + computer.assignID(), + computer.getComputerEnvironment().getComputerSpaceLimit() + ); + } + + FileSystem filesystem = null; + try + { + filesystem = new FileSystem( "hdd", rootMount ); + + IMount romMount = getRomMount(); + if( romMount == null ) + { + displayFailure( "Cannot mount rom" ); + return null; + } + + filesystem.mount( "rom", "rom", romMount ); + return filesystem; + } + catch( FileSystemException e ) + { + if( filesystem != null ) filesystem.close(); + ComputerCraft.log.error( "Cannot mount computer filesystem", e ); + + displayFailure( "Cannot mount computer system" ); + return null; + } + } + + private ILuaMachine createLuaMachine() + { + // Load the bios resource + InputStream biosStream = null; + try + { + biosStream = computer.getComputerEnvironment().createResourceFile( "computercraft", "lua/bios.lua" ); + } + catch( Exception ignored ) + { + } + + if( biosStream == null ) + { + displayFailure( "Error loading bios.lua" ); + return null; + } + + // Create the lua machine + ILuaMachine machine = new CobaltLuaMachine( computer, timeout ); + + // Add the APIs + for( ILuaAPI api : apis ) machine.addAPI( api ); + + // Start the machine running the bios resource + machine.loadBios( biosStream ); + IoUtil.closeQuietly( biosStream ); + + if( machine.isFinished() ) + { + machine.close(); + displayFailure( "Error starting bios.lua" ); + return null; + } + + return machine; + } + + private void turnOn() throws InterruptedException + { + isOnLock.lockInterruptibly(); + try + { + // Reset the terminal and event queue + computer.getTerminal().reset(); + synchronized( queueLock ) + { + eventQueue.clear(); + } + + // Init filesystem + if( (this.fileSystem = createFileSystem()) == null ) + { + shutdown(); + return; + } + + // Init APIs + for( ILuaAPI api : apis ) api.startup(); + + // Init lua + if( (this.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. + machine.handleEvent( null, null ); + } + + private void shutdown() throws InterruptedException + { + isOnLock.lockInterruptibly(); + try + { + isOn = false; + synchronized( queueLock ) + { + eventQueue.clear(); + } + + // Shutdown Lua machine + if( machine != null ) + { + machine.close(); + machine = null; + } + + // Shutdown our APIs + for( ILuaAPI api : apis ) api.shutdown(); + + // 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() + { + timeout.reset(); + } + + /** + * Called after executing {@link #work()}. Adds this back to the {@link ComputerThread} if we have more work, + * otherwise remove it. + */ + void afterWork() + { + Tracking.addTaskTiming( getComputer(), timeout.nanoSinceStart() ); + + synchronized( queueLock ) + { + if( eventQueue.isEmpty() && command == null ) + { + onComputerQueue = false; + } + else + { + ComputerThread.queue( this ); + } + } + } + + /** + * The main worker function, called by {@link ComputerThread}. + * + * 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( isExecuting.getAndSet( true ) ) + { + throw new IllegalStateException( "Multiple threads running on computer the same time" ); + } + + try + { + 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(); + break; + + case SHUTDOWN: + + if( !isOn ) return; + computer.getTerminal().reset(); + shutdown(); + break; + + case REBOOT: + if( !isOn ) return; + computer.getTerminal().reset(); + shutdown(); + + computer.turnOn(); + break; + + case ABORT: + if( !isOn ) return; + displayFailure( TimeoutState.ABORT_MESSAGE ); + shutdown(); + break; + } + } + else + { + machine.handleEvent( event.name, event.args ); + if( machine.isFinished() ) + { + displayFailure( "Error resuming bios.lua" ); + shutdown(); + } + } + } + finally + { + isExecuting.set( false ); + } + } + + private void displayFailure( String message ) + { + Terminal terminal = computer.getTerminal(); + terminal.reset(); + terminal.write( message ); + terminal.setCursorPos( 0, 1 ); + terminal.write( "ComputerCraft may be installed incorrectly" ); + } + + private enum StateCommand + { + TURN_ON, + SHUTDOWN, + REBOOT, + ABORT, + } + + private static class Event + { + final String name; + final Object[] args; + + private Event( String name, Object[] args ) + { + this.name = name; + this.args = args; + } + } +} diff --git a/src/main/java/dan200/computercraft/core/computer/ComputerThread.java b/src/main/java/dan200/computercraft/core/computer/ComputerThread.java index e0ae5c80b..0596e2a45 100644 --- a/src/main/java/dan200/computercraft/core/computer/ComputerThread.java +++ b/src/main/java/dan200/computercraft/core/computer/ComputerThread.java @@ -7,15 +7,13 @@ package dan200.computercraft.core.computer; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.core.tracking.Tracking; import dan200.computercraft.shared.util.ThreadUtils; -import java.util.HashSet; -import java.util.Set; -import java.util.WeakHashMap; +import javax.annotation.Nonnull; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; import static dan200.computercraft.core.computer.TimeoutState.ABORT_TIMEOUT; @@ -24,8 +22,10 @@ import static dan200.computercraft.core.computer.TimeoutState.TIMEOUT; /** * Responsible for running all tasks from a {@link Computer}. * - * This is split into two components: the {@link TaskRunner}s, which pull a task from the queue and execute it, and + * This is split into two components: the {@link TaskRunner}s, which pull an executor from the queue and execute it, and * a single {@link Monitor} which observes all runners and kills them if they are behaving badly. + * + * TODO: Flesh out the documentation here. */ public class ComputerThread { @@ -36,31 +36,15 @@ public class ComputerThread */ private static final int MONITOR_WAKEUP = 100; - /** - * The maximum number of entries in the event queue - */ - private static final int QUEUE_LIMIT = 256; - /** * Lock used for modifications to the array of current threads. */ private static final Object threadLock = new Object(); /** - * Lock for various task operations + * Active executors to run */ - private static final Object taskLock = new Object(); - - /** - * Map of objects to task list - */ - private static final WeakHashMap> computerTaskQueues = new WeakHashMap<>(); - - /** - * Active queues to execute - */ - private static final BlockingQueue> computerTasksActive = new LinkedBlockingQueue<>(); - private static final Set> computerTasksActiveSet = new HashSet<>(); + private static final BlockingQueue computersActive = new LinkedBlockingQueue<>(); /** * Whether the computer thread system is currently running @@ -130,40 +114,18 @@ public class ComputerThread } } - synchronized( taskLock ) - { - computerTaskQueues.clear(); - computerTasksActive.clear(); - computerTasksActiveSet.clear(); - } + computersActive.clear(); } /** - * Queue a task to execute on the thread + * Mark a computer as having work, enqueuing it on the thread. * - * @param task The task to execute + * @param computer The computer to execute work on. */ - static void queueTask( ITask task ) + static void queue( @Nonnull ComputerExecutor computer ) { - Computer computer = task.getOwner(); - BlockingQueue queue; - synchronized( computerTaskQueues ) - { - queue = computerTaskQueues.get( computer ); - if( queue == null ) - { - computerTaskQueues.put( computer, queue = new LinkedBlockingQueue<>( QUEUE_LIMIT ) ); - } - } - - synchronized( taskLock ) - { - if( queue.offer( task ) && !computerTasksActiveSet.contains( queue ) ) - { - computerTasksActive.add( queue ); - computerTasksActiveSet.add( queue ); - } - } + if( !computer.onComputerQueue ) throw new IllegalStateException( "Computer must be on queue" ); + computersActive.add( computer ); } /** @@ -193,18 +155,18 @@ public class ComputerThread if( runner == null ) continue; // If the runner has no work, skip - Computer computer = runner.currentComputer; - if( computer == null ) continue; + ComputerExecutor executor = runner.currentExecutor.get(); + if( executor == null ) continue; // If we're still within normal execution times (TIMEOUT) or soft abort (ABORT_TIMEOUT), // then we can let the Lua machine do its work. - long afterStart = computer.timeout.milliSinceStart(); + long afterStart = executor.timeout.milliSinceStart(); long afterHardAbort = afterStart - TIMEOUT - ABORT_TIMEOUT; if( afterHardAbort < 0 ) continue; // Set the hard abort flag. - computer.timeout.hardAbort(); - computer.abort(); + executor.timeout.hardAbort(); + executor.abort(); if( afterHardAbort >= ABORT_TIMEOUT + ABORT_TIMEOUT ) { @@ -212,7 +174,9 @@ public class ComputerThread // as dead, finish off the task, and spawn a new runner. // Note, we'll do the actual interruption of the thread in the next block. runner.running = false; - finishTask( computer, runner.currentQueue ); + + ComputerExecutor thisExecutor = runner.currentExecutor.getAndSet( null ); + if( thisExecutor != null ) executor.afterWork(); synchronized( threadLock ) { @@ -227,7 +191,7 @@ public class ComputerThread { // If we've hard aborted but we're still not dead, dump the stack trace and interrupt // the task. - timeoutTask( computer, runner.owner, afterStart ); + timeoutTask( executor, runner.owner, afterStart ); runner.owner.interrupt(); } } @@ -241,15 +205,14 @@ public class ComputerThread } /** - * Pulls tasks from the {@link #computerTasksActive} queue and runs them. + * Pulls tasks from the {@link #computersActive} queue and runs them. */ private static final class TaskRunner implements Runnable { Thread owner; volatile boolean running = true; - BlockingQueue currentQueue; - Computer currentComputer; + final AtomicReference currentExecutor = new AtomicReference<>(); @Override public void run() @@ -259,10 +222,10 @@ public class ComputerThread while( running && ComputerThread.running ) { // Wait for an active queue to execute - BlockingQueue queue; + ComputerExecutor executor; try { - queue = computerTasksActive.take(); + executor = computersActive.take(); } catch( InterruptedException ignored ) { @@ -272,36 +235,33 @@ public class ComputerThread } // Pull a task from this queue, and set what we're currently executing. - ITask task = queue.remove(); - Computer computer = this.currentComputer = task.getOwner(); - this.currentQueue = queue; + currentExecutor.set( executor ); // Execute the task - computer.timeout.reset(); + executor.beforeWork(); try { - task.execute(); + executor.work(); } catch( Exception e ) { - ComputerCraft.log.error( "Error running task on computer #" + computer.getID(), e ); + ComputerCraft.log.error( "Error running task on computer #" + executor.getComputer().getID(), e ); } finally { - if( running ) finishTask( computer, queue ); - this.currentQueue = null; - this.currentComputer = null; + ComputerExecutor thisExecutor = currentExecutor.getAndSet( null ); + if( thisExecutor != null ) executor.afterWork(); } } } } - private static void timeoutTask( Computer computer, Thread thread, long nanotime ) + private static void timeoutTask( ComputerExecutor executor, Thread thread, long nanotime ) { if( !ComputerCraft.logPeripheralErrors ) return; StringBuilder builder = new StringBuilder() - .append( "Terminating computer #" ).append( computer.getID() ) + .append( "Terminating computer #" ).append( executor.getComputer().getID() ) .append( " due to timeout (running for " ).append( nanotime / 1e9 ) .append( " seconds). This is NOT a bug, but may mean a computer is misbehaving. " ) .append( thread.getName() ) @@ -317,22 +277,4 @@ public class ComputerThread ComputerCraft.log.warn( builder.toString() ); } - - private static void finishTask( Computer computer, BlockingQueue queue ) - { - Tracking.addTaskTiming( computer, computer.timeout.nanoSinceStart() ); - - // Re-add it back onto the queue or remove it - synchronized( taskLock ) - { - if( queue.isEmpty() ) - { - computerTasksActiveSet.remove( queue ); - } - else - { - computerTasksActive.add( queue ); - } - } - } } diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java index 63195c540..8c4efbefd 100644 --- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java +++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java @@ -204,16 +204,6 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput NetworkHandler.sendToAllPlayers( new ComputerDeletedClientMessage( getInstanceID() ) ); } - public IWritableMount getRootMount() - { - return m_computer.getRootMount(); - } - - public int assignID() - { - return m_computer.assignID(); - } - public void setID( int id ) { m_computer.setID( id ); @@ -303,7 +293,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput public void addAPI( ILuaAPI api ) { - m_computer.addAPI( api ); + m_computer.addApi( api ); } @Deprecated diff --git a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java index 1e0ec1c8b..7e21aa102 100644 --- a/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java +++ b/src/test/java/dan200/computercraft/core/computer/ComputerBootstrap.java @@ -45,7 +45,7 @@ public class ComputerBootstrap final Computer computer = new Computer( new BasicEnvironment( mount ), term, 0 ); AssertApi api = new AssertApi(); - computer.addAPI( api ); + computer.addApi( api ); try {