1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-25 22:53:22 +00:00

Rewrite ComputerThread and the timeout system

- Instead of setting soft/hard timeouts on the ILuaMachine, we instead
   provide it with a TimeoutState instance. This holds the current abort
   flags, which can then be polled within debug hooks.

   This means the Lua machine has to do less state management, but also
   allows a more flexible implementation of aborts.

 - Soft aborts are now handled by the TimeoutState - we track when the
   task was started, and now only need to check we're more than 7s since
   then.

   Note, these timers work with millisecond granularity, rather than
   nano, as this invokes substantially less overhead.

 - Instead of having n runners being observed with n managers, we now
   have n runners and 1 manager (or Monitor).

   The runners are now responsible for pulling work from the queue. When
   the start to execute a task, they set the time execution commenced.
   The monitor then just checks each runner every 0.1s and handles hard
   aborts (or killing the thread if need be).
This commit is contained in:
SquidDev 2019-02-26 10:04:21 +00:00
parent 6d383d005c
commit 06e76f9b15
5 changed files with 321 additions and 305 deletions

View File

@ -50,6 +50,7 @@ private enum State
private ILuaMachine m_machine = null;
private final List<ILuaAPI> m_apis = new ArrayList<>();
final TimeoutState timeout = new TimeoutState();
private final Environment m_internalEnvironment = new Environment( this );
private final Terminal m_terminal;
@ -132,24 +133,6 @@ public boolean isOn()
}
}
public void abort( boolean hard )
{
synchronized( this )
{
if( m_state != State.Off && m_machine != null )
{
if( hard )
{
m_machine.hardAbort( "Too long without yielding" );
}
else
{
m_machine.softAbort( "Too long without yielding" );
}
}
}
}
public void unload()
{
stopComputer( false );
@ -284,7 +267,7 @@ public void addAPI( ILuaAPI api )
private void initLua()
{
// Create the lua machine
ILuaMachine machine = new CobaltLuaMachine( this );
ILuaMachine machine = new CobaltLuaMachine( this, timeout );
// Add the APIs
for( ILuaAPI api : m_apis )
@ -421,6 +404,18 @@ public void execute()
} );
}
/**
* 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 )

View File

@ -18,63 +18,93 @@
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.locks.LockSupport;
import static dan200.computercraft.core.computer.TimeoutState.ABORT_TIMEOUT;
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
* a single {@link Monitor} which observes all runners and kills them if they are behaving badly.
*/
public class ComputerThread
{
/**
* How often the computer thread monitor should run, in milliseconds
*
* @see Monitor
*/
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 object
* Lock used for modifications to the array of current threads.
*/
private static final Object s_stateLock = new Object();
private static final Object threadLock = new Object();
/**
* Lock for various task operations
*/
private static final Object s_taskLock = new Object();
private static final Object taskLock = new Object();
/**
* Map of objects to task list
*/
private static final WeakHashMap<Computer, BlockingQueue<ITask>> s_computerTaskQueues = new WeakHashMap<>();
private static final WeakHashMap<Computer, BlockingQueue<ITask>> computerTaskQueues = new WeakHashMap<>();
/**
* Active queues to execute
*/
private static final BlockingQueue<BlockingQueue<ITask>> s_computerTasksActive = new LinkedBlockingQueue<>();
private static final Set<BlockingQueue<ITask>> s_computerTasksActiveSet = new HashSet<>();
private static final BlockingQueue<BlockingQueue<ITask>> computerTasksActive = new LinkedBlockingQueue<>();
private static final Set<BlockingQueue<ITask>> computerTasksActiveSet = new HashSet<>();
/**
* Whether the thread is stopped or should be stopped
* Whether the computer thread system is currently running
*/
private static boolean s_stopped = false;
private static volatile boolean running = false;
/**
* The thread tasks execute on
* The current task manager.
*/
private static Thread[] s_threads = null;
private static Thread monitor;
private static final ThreadFactory s_ManagerFactory = ThreadUtils.factory( "Computer-Manager" );
private static final ThreadFactory s_RunnerFactory = ThreadUtils.factory( "Computer-Runner" );
/**
* The array of current runners, and their owning threads.
*/
private static TaskRunner[] runners;
private static final ThreadFactory monitorFactory = ThreadUtils.factory( "Computer-Monitor" );
private static final ThreadFactory runnerFactory = ThreadUtils.factory( "Computer-Runner" );
/**
* Start the computer thread
*/
static void start()
{
synchronized( s_stateLock )
synchronized( threadLock )
{
s_stopped = false;
if( s_threads == null || s_threads.length != ComputerCraft.computer_threads )
running = true;
if( monitor == null || !monitor.isAlive() ) (monitor = monitorFactory.newThread( new Monitor() )).start();
if( runners == null || runners.length != ComputerCraft.computer_threads )
{
s_threads = new Thread[ComputerCraft.computer_threads];
// TODO: Resize this + kill old runners and start new ones.
runners = new TaskRunner[ComputerCraft.computer_threads];
}
for( int i = 0; i < s_threads.length; i++ )
for( int i = 0; i < runners.length; i++ )
{
Thread thread = s_threads[i];
if( thread == null || !thread.isAlive() )
TaskRunner runner = runners[i];
if( runner == null || runner.owner == null || !runner.owner.isAlive() )
{
(s_threads[i] = s_ManagerFactory.newThread( new TaskExecutor() )).start();
// Mark the old runner as dead, just in case.
if( runner != null ) runner.running = false;
// And start a new runner
runnerFactory.newThread( runners[i] = new TaskRunner() ).start();
}
}
}
@ -85,26 +115,26 @@ static void start()
*/
public static void stop()
{
synchronized( s_stateLock )
synchronized( threadLock )
{
if( s_threads != null )
running = false;
if( runners != null )
{
s_stopped = true;
for( Thread thread : s_threads )
for( TaskRunner runner : runners )
{
if( thread != null && thread.isAlive() )
{
thread.interrupt();
}
if( runner == null ) continue;
runner.running = false;
if( runner.owner != null ) runner.owner.interrupt();
}
}
}
synchronized( s_taskLock )
synchronized( taskLock )
{
s_computerTaskQueues.clear();
s_computerTasksActive.clear();
s_computerTasksActiveSet.clear();
computerTaskQueues.clear();
computerTasksActive.clear();
computerTasksActiveSet.clear();
}
}
@ -117,36 +147,33 @@ static void queueTask( ITask task )
{
Computer computer = task.getOwner();
BlockingQueue<ITask> queue;
synchronized( s_computerTaskQueues )
synchronized( computerTaskQueues )
{
queue = s_computerTaskQueues.get( computer );
queue = computerTaskQueues.get( computer );
if( queue == null )
{
s_computerTaskQueues.put( computer, queue = new LinkedBlockingQueue<>( QUEUE_LIMIT ) );
computerTaskQueues.put( computer, queue = new LinkedBlockingQueue<>( QUEUE_LIMIT ) );
}
}
synchronized( s_taskLock )
synchronized( taskLock )
{
if( queue.offer( task ) && !s_computerTasksActiveSet.contains( queue ) )
if( queue.offer( task ) && !computerTasksActiveSet.contains( queue ) )
{
s_computerTasksActive.add( queue );
s_computerTasksActiveSet.add( queue );
computerTasksActive.add( queue );
computerTasksActiveSet.add( queue );
}
}
}
/**
* Responsible for pulling and managing computer tasks. This pulls a task from {@link #s_computerTasksActive},
* creates a new thread using {@link TaskRunner} or reuses a previous one and uses that to execute the task.
* Observes all currently active {@link TaskRunner}s and terminates their tasks once they have exceeded the hard
* abort limit.
*
* If the task times out, then it will attempt to interrupt the {@link TaskRunner} instance.
* @see TimeoutState
*/
private static final class TaskExecutor implements Runnable
private static final class Monitor implements Runnable
{
private TaskRunner runner;
private Thread thread;
@Override
public void run()
{
@ -154,201 +181,158 @@ public void run()
{
while( true )
{
// Wait for an active queue to execute
BlockingQueue<ITask> queue = s_computerTasksActive.take();
Thread.sleep( MONITOR_WAKEUP );
// If threads should be stopped then return
synchronized( s_stateLock )
TaskRunner[] currentRunners = ComputerThread.runners;
if( currentRunners != null )
{
if( s_stopped ) return;
}
for( int i = 0; i < currentRunners.length; i++ )
{
TaskRunner runner = currentRunners[i];
// If we've no runner, skip.
if( runner == null ) continue;
execute( queue );
// If the runner has no work, skip
Computer computer = runner.currentComputer;
if( computer == 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 afterHardAbort = afterStart - TIMEOUT - ABORT_TIMEOUT;
if( afterHardAbort < 0 ) continue;
// Set the hard abort flag.
computer.timeout.hardAbort();
computer.abort();
if( afterHardAbort >= ABORT_TIMEOUT + ABORT_TIMEOUT )
{
// If we've hard aborted and interrupted, and we're still not dead, then mark the runner
// 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 );
synchronized( threadLock )
{
if( running && runners.length > i && runners[i] == runner )
{
runnerFactory.newThread( currentRunners[i] = new TaskRunner() ).start();
}
}
}
if( afterHardAbort >= ABORT_TIMEOUT )
{
// If we've hard aborted but we're still not dead, dump the stack trace and interrupt
// the task.
timeoutTask( computer, runner.owner, afterStart );
runner.owner.interrupt();
}
}
}
}
}
catch( InterruptedException ignored )
{
Thread.currentThread().interrupt();
}
}
private void execute( BlockingQueue<ITask> queue ) throws InterruptedException
{
ITask task = queue.remove();
if( thread == null || !thread.isAlive() )
{
runner = new TaskRunner();
(thread = s_RunnerFactory.newThread( runner )).start();
}
long start = System.nanoTime();
// Execute the task
runner.submit( task );
try
{
// If we timed out rather than exiting:
boolean done = runner.await( 7000 );
if( !done )
{
// Attempt to soft then hard abort
Computer computer = task.getOwner();
if( computer != null )
{
computer.abort( false );
done = runner.await( 1500 );
if( !done )
{
computer.abort( true );
done = runner.await( 1500 );
}
}
// Interrupt the thread
if( !done )
{
if( ComputerCraft.logPeripheralErrors )
{
long time = System.nanoTime() - start;
StringBuilder builder = new StringBuilder( "Terminating " );
if( computer != null )
{
builder.append( "computer #" ).append( computer.getID() );
}
else
{
builder.append( "unknown computer" );
}
{
builder.append( " due to timeout (running for " )
.append( time / 1e9 )
.append( " seconds). This is NOT a bug, but may mean a computer is misbehaving. " )
.append( thread.getName() )
.append( " is currently " )
.append( thread.getState() );
Object blocking = LockSupport.getBlocker( thread );
if( blocking != null ) builder.append( "\n on " ).append( blocking );
for( StackTraceElement element : thread.getStackTrace() )
{
builder.append( "\n at " ).append( element );
}
}
ComputerCraft.log.warn( builder.toString() );
}
thread.interrupt();
thread = null;
runner = null;
}
}
}
finally
{
long stop = System.nanoTime();
Computer computer = task.getOwner();
if( computer != null ) Tracking.addTaskTiming( computer, stop - start );
// Re-add it back onto the queue or remove it
synchronized( s_taskLock )
{
if( queue.isEmpty() )
{
s_computerTasksActiveSet.remove( queue );
}
else
{
s_computerTasksActive.add( queue );
}
}
}
}
}
/**
* Responsible for the actual running of tasks. It waitin for the {@link TaskRunner#input} semaphore to be
* triggered, consumes a task and then triggers {@link TaskRunner#finished}.
* Pulls tasks from the {@link #computerTasksActive} queue and runs them.
*/
private static final class TaskRunner implements Runnable
{
private final Semaphore input = new Semaphore();
private final Semaphore finished = new Semaphore();
private ITask task;
Thread owner;
volatile boolean running = true;
BlockingQueue<ITask> currentQueue;
Computer currentComputer;
@Override
public void run()
{
try
owner = Thread.currentThread();
while( running && ComputerThread.running )
{
while( true )
// Wait for an active queue to execute
BlockingQueue<ITask> queue;
try
{
input.await();
try
{
task.execute();
}
catch( RuntimeException e )
{
ComputerCraft.log.error( "Error running task.", e );
}
task = null;
finished.signal();
queue = computerTasksActive.take();
}
catch( InterruptedException ignored )
{
// If we've been interrupted, our running flag has probably been reset, so we'll
// just jump into the next iteration.
continue;
}
// 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;
// Execute the task
computer.timeout.reset();
try
{
task.execute();
}
catch( Exception e )
{
ComputerCraft.log.error( "Error running task on computer #" + computer.getID(), e );
}
finally
{
if( running ) finishTask( computer, queue );
this.currentQueue = null;
this.currentComputer = null;
}
}
catch( InterruptedException e )
{
ComputerCraft.log.error( "Error running task.", e );
Thread.currentThread().interrupt();
}
}
void submit( ITask task )
{
this.task = task;
input.signal();
}
boolean await( long timeout ) throws InterruptedException
{
return finished.await( timeout );
}
}
/**
* A simple method to allow awaiting/providing a signal.
*
* Java does provide similar classes, but I only needed something simple.
*/
private static final class Semaphore
private static void timeoutTask( Computer computer, Thread thread, long nanotime )
{
private volatile boolean state = false;
if( !ComputerCraft.logPeripheralErrors ) return;
synchronized void signal()
StringBuilder builder = new StringBuilder()
.append( "Terminating computer #" ).append( computer.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() )
.append( " is currently " )
.append( thread.getState() );
Object blocking = LockSupport.getBlocker( thread );
if( blocking != null ) builder.append( "\n on " ).append( blocking );
for( StackTraceElement element : thread.getStackTrace() )
{
state = true;
notify();
builder.append( "\n at " ).append( element );
}
synchronized void await() throws InterruptedException
{
while( !state ) wait();
state = false;
}
ComputerCraft.log.warn( builder.toString() );
}
synchronized boolean await( long timeout ) throws InterruptedException
private static void finishTask( Computer computer, BlockingQueue<ITask> queue )
{
Tracking.addTaskTiming( computer, computer.timeout.nanoSinceStart() );
// Re-add it back onto the queue or remove it
synchronized( taskLock )
{
if( !state )
if( queue.isEmpty() )
{
wait( timeout );
if( !state ) return false;
computerTasksActiveSet.remove( queue );
}
else
{
computerTasksActive.add( queue );
}
state = false;
return true;
}
}
}

View File

@ -0,0 +1,89 @@
/*
* 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;
/**
* Used to measure how long a computer has executed for, and thus the relevant timeout states.
*
* 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.
*
* 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).
*
* 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.
*
* @see ComputerThread
* @see dan200.computercraft.core.lua.ILuaMachine
*/
public final class TimeoutState
{
/**
* The total time a task is allowed to run before aborting in milliseconds
*/
static final long TIMEOUT = 7000;
/**
* The time the task is allowed to run after each abort in milliseconds
*/
static final long ABORT_TIMEOUT = 1500;
public static final String ABORT_MESSAGE = "Too long without yielding";
private volatile boolean softAbort;
private volatile boolean hardAbort;
private long milliTime;
private long nanoTime;
long milliSinceStart()
{
return System.currentTimeMillis() - milliTime;
}
long nanoSinceStart()
{
return System.nanoTime() - nanoTime;
}
/**
* If the machine should be passively aborted.
*/
public boolean isSoftAborted()
{
return softAbort || (softAbort = (System.currentTimeMillis() - milliTime) >= TIMEOUT);
}
/**
* If the machine should be forcibly aborted.
*/
public boolean isHardAborted()
{
return hardAbort;
}
/**
* If the machine should be forcibly aborted.
*/
void hardAbort()
{
softAbort = hardAbort = true;
}
/**
* Reset all abort flags and start the abort timer.
*/
void reset()
{
softAbort = hardAbort = false;
milliTime = System.currentTimeMillis();
nanoTime = System.nanoTime();
}
}

View File

@ -11,6 +11,7 @@
import dan200.computercraft.core.computer.Computer;
import dan200.computercraft.core.computer.ITask;
import dan200.computercraft.core.computer.MainThread;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.tracking.Tracking;
import dan200.computercraft.core.tracking.TrackingField;
import dan200.computercraft.shared.util.ThreadUtils;
@ -29,7 +30,6 @@
import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.IdentityHashMap;
@ -44,7 +44,7 @@
public class CobaltLuaMachine implements ILuaMachine
{
private static final ThreadPoolExecutor coroutines = new ThreadPoolExecutor(
private static final ThreadPoolExecutor COROUTINES = new ThreadPoolExecutor(
0, Integer.MAX_VALUE,
5L, TimeUnit.MINUTES,
new SynchronousQueue<>(),
@ -52,18 +52,18 @@ public class CobaltLuaMachine implements ILuaMachine
);
private final Computer m_computer;
private final TimeoutState timeout;
private LuaState m_state;
private LuaTable m_globals;
private LuaThread m_mainRoutine;
private String m_eventFilter;
private String m_softAbortMessage;
private String m_hardAbortMessage;
private LuaThread m_mainRoutine = null;
private String m_eventFilter = null;
public CobaltLuaMachine( Computer computer )
public CobaltLuaMachine( Computer computer, TimeoutState timeout )
{
m_computer = computer;
this.timeout = timeout;
// Create an environment to run in
LuaState state = this.m_state = LuaState.builder()
@ -76,50 +76,42 @@ public CobaltLuaMachine( Computer computer )
@Override
public void onInstruction( DebugState ds, DebugFrame di, int pc ) throws LuaError, UnwindThrowable
{
int count = ++this.count;
if( count > 100000 )
{
if( m_hardAbortMessage != null ) throw HardAbortError.INSTANCE;
this.count = 0;
}
else
{
handleSoftAbort();
}
// We check our current abort state every so 128 instructions.
if( (count = (count + 1) & 127) == 0 ) handleAbort();
super.onInstruction( ds, di, pc );
}
@Override
public void poll() throws LuaError
{
if( m_hardAbortMessage != null ) throw HardAbortError.INSTANCE;
handleSoftAbort();
handleAbort();
}
private void handleSoftAbort() throws LuaError
private void handleAbort() throws LuaError
{
// If we've been hard aborted or closed then abort.
if( timeout.isHardAborted() || m_state == null ) throw HardAbortError.INSTANCE;
// If the soft abort has been cleared then we can reset our flags and continue.
String message = m_softAbortMessage;
if( message == null )
if( !timeout.isSoftAborted() )
{
hasSoftAbort = false;
return;
}
if( hasSoftAbort && m_hardAbortMessage == null )
if( hasSoftAbort && !timeout.isHardAborted() )
{
// If we have fired our soft abort, but we haven't been hard aborted then everything is OK.
return;
}
hasSoftAbort = true;
throw new LuaError( message );
throw new LuaError( TimeoutState.ABORT_MESSAGE );
}
} )
.coroutineExecutor( command -> {
Tracking.addValue( m_computer, TrackingField.COROUTINES_CREATED, 1 );
coroutines.execute( () -> {
COROUTINES.execute( () -> {
try
{
command.run();
@ -145,7 +137,7 @@ private void handleSoftAbort() throws LuaError
if( ComputerCraft.debug_enable ) m_globals.load( state, new DebugLib() );
// Register custom load/loadstring provider which automatically adds prefixes.
m_globals.rawset( "load", new PrefixWrapperFunction( m_globals.rawget( "load" ), 0 )) ;
m_globals.rawset( "load", new PrefixWrapperFunction( m_globals.rawget( "load" ), 0 ) );
m_globals.rawset( "loadstring", new PrefixWrapperFunction( m_globals.rawget( "loadstring" ), 1 ) );
// Remove globals we don't want to expose
@ -162,13 +154,6 @@ private void handleSoftAbort() throws LuaError
{
m_globals.rawset( "_CC_DISABLE_LUA51_FEATURES", Constants.TRUE );
}
// Our main function will go here
m_mainRoutine = null;
m_eventFilter = null;
m_softAbortMessage = null;
m_hardAbortMessage = null;
}
@Override
@ -224,7 +209,7 @@ public void handleEvent( String eventName, Object[] arguments )
}
Varargs results = LuaThread.run( m_mainRoutine, resumeArgs );
if( m_hardAbortMessage != null ) throw new LuaError( m_hardAbortMessage );
if( timeout.isHardAborted() ) throw new LuaError( TimeoutState.ABORT_MESSAGE );
LuaValue filter = results.first();
m_eventFilter = filter.isString() ? filter.toString() : null;
@ -236,36 +221,6 @@ public void handleEvent( String eventName, Object[] arguments )
close();
ComputerCraft.log.warn( "Top level coroutine errored", e );
}
finally
{
m_softAbortMessage = null;
m_hardAbortMessage = null;
}
}
@Override
public void softAbort( String abortMessage )
{
m_softAbortMessage = abortMessage;
}
@Override
public void hardAbort( String abortMessage )
{
m_softAbortMessage = abortMessage;
m_hardAbortMessage = abortMessage;
}
@Override
public boolean saveState( OutputStream output )
{
return false;
}
@Override
public boolean restoreState( InputStream input )
{
return false;
}
@Override
@ -277,9 +232,10 @@ public boolean isFinished()
@Override
public void close()
{
if( m_state == null ) return;
LuaState state = m_state;
if( state == null ) return;
m_state.abandon();
state.abandon();
m_mainRoutine = null;
m_state = null;
m_globals = null;
@ -639,8 +595,9 @@ private static class PrefixWrapperFunction extends VarArgFunction
private final LibFunction underlying;
public PrefixWrapperFunction(LuaValue wrap, int opcode) {
LibFunction underlying = (LibFunction)wrap;
public PrefixWrapperFunction( LuaValue wrap, int opcode )
{
LibFunction underlying = (LibFunction) wrap;
this.underlying = underlying;
this.opcode = opcode;
@ -661,7 +618,7 @@ public Varargs invoke( LuaState state, Varargs args ) throws LuaError, UnwindThr
{
chunkname = OperationHelper.concat( EQ_STR, chunkname );
}
return underlying.invoke(state, varargsOf(func, chunkname));
return underlying.invoke( state, varargsOf( func, chunkname ) );
}
case 1: // "loadstring", // ( string [,chunkname] ) -> chunk | nil, msg
{
@ -671,7 +628,7 @@ public Varargs invoke( LuaState state, Varargs args ) throws LuaError, UnwindThr
{
chunkname = OperationHelper.concat( EQ_STR, chunkname );
}
return underlying.invoke(state, varargsOf(script, chunkname));
return underlying.invoke( state, varargsOf( script, chunkname ) );
}
}

View File

@ -9,7 +9,6 @@
import dan200.computercraft.api.lua.ILuaAPI;
import java.io.InputStream;
import java.io.OutputStream;
public interface ILuaMachine
{
@ -19,14 +18,6 @@ public interface ILuaMachine
void handleEvent( String eventName, Object[] arguments );
void softAbort( String abortMessage );
void hardAbort( String abortMessage );
boolean saveState( OutputStream output );
boolean restoreState( InputStream input );
boolean isFinished();
void close();