682 lines
20 KiB
Java
682 lines
20 KiB
Java
/*
|
|
* This file is part of ComputerCraft - http://www.computercraft.info
|
|
* Copyright Daniel Ratcliffe, 2011-2021. 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.lua.MachineResult;
|
|
import dan200.computercraft.core.terminal.Terminal;
|
|
import dan200.computercraft.core.tracking.Tracking;
|
|
import dan200.computercraft.shared.util.Colour;
|
|
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.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.
|
|
*
|
|
* 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 final Computer computer;
|
|
private final List<ILuaAPI> 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 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.
|
|
*
|
|
* 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<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<>();
|
|
|
|
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.httpEnabled ) 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 || 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;
|
|
|
|
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()
|
|
{
|
|
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 )
|
|
{
|
|
ILuaMachine 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 ) 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()
|
|
{
|
|
return computer.getComputerEnvironment().createResourceMount( "computercraft", "lua/rom" );
|
|
}
|
|
|
|
private IWritableMount getRootMount()
|
|
{
|
|
if( rootMount == null )
|
|
{
|
|
rootMount = computer.getComputerEnvironment().createSaveDirMount(
|
|
"computer/" + computer.assignID(),
|
|
computer.getComputerEnvironment().getComputerSpaceLimit()
|
|
);
|
|
}
|
|
return rootMount;
|
|
}
|
|
|
|
private FileSystem createFileSystem()
|
|
{
|
|
FileSystem filesystem = null;
|
|
try
|
|
{
|
|
filesystem = new FileSystem( "hdd", getRootMount() );
|
|
|
|
IMount 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();
|
|
ComputerCraft.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.getComputerEnvironment().createResourceFile( "computercraft", "lua/bios.lua" );
|
|
}
|
|
catch( Exception ignored )
|
|
{
|
|
}
|
|
|
|
if( biosStream == null )
|
|
{
|
|
displayFailure( "Error loading bios.lua", null );
|
|
return null;
|
|
}
|
|
|
|
// Create the lua machine
|
|
ILuaMachine machine = new CobaltLuaMachine( computer, timeout );
|
|
|
|
// Add the APIs. We unwrap them (yes, this is horrible) to get access to the underlying object.
|
|
for( ILuaAPI api : apis ) machine.addAPI( api instanceof ApiWrapper ? ((ApiWrapper) api).getDelegate() : api );
|
|
|
|
// Start the machine running the bios resource
|
|
MachineResult 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( ILuaAPI 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( ILuaAPI 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();
|
|
}
|
|
|
|
Tracking.addTaskTiming( getComputer(), 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}.
|
|
*
|
|
* 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 )
|
|
{
|
|
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();
|
|
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( "Error running computer", TimeoutState.ABORT_MESSAGE );
|
|
shutdown();
|
|
break;
|
|
|
|
case ERROR:
|
|
if( !isOn ) return;
|
|
displayFailure( "Error running computer", "An internal error occurred, see logs." );
|
|
shutdown();
|
|
break;
|
|
}
|
|
}
|
|
else if( event != null )
|
|
{
|
|
resumeMachine( event.name, event.args );
|
|
}
|
|
}
|
|
|
|
private void displayFailure( String message, String extra )
|
|
{
|
|
Terminal terminal = computer.getTerminal();
|
|
boolean colour = computer.getComputerEnvironment().isColour();
|
|
terminal.reset();
|
|
|
|
// Display our primary error message
|
|
if( colour ) 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( colour ) terminal.setTextColour( 15 - Colour.WHITE.ordinal() );
|
|
terminal.write( "ComputerCraft may be installed incorrectly" );
|
|
}
|
|
|
|
private void resumeMachine( String event, Object[] args ) throws InterruptedException
|
|
{
|
|
MachineResult 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 static final class Event
|
|
{
|
|
final String name;
|
|
final Object[] args;
|
|
|
|
private Event( String name, Object[] args )
|
|
{
|
|
this.name = name;
|
|
this.args = args;
|
|
}
|
|
}
|
|
}
|