mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-06-26 07:03:22 +00:00
![SquidDev](/assets/img/avatar_default.png)
- 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).
651 lines
25 KiB
Java
651 lines
25 KiB
Java
/*
|
|
* 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.lua;
|
|
|
|
import dan200.computercraft.ComputerCraft;
|
|
import dan200.computercraft.api.lua.*;
|
|
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;
|
|
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.function.LibFunction;
|
|
import org.squiddev.cobalt.function.LuaFunction;
|
|
import org.squiddev.cobalt.function.VarArgFunction;
|
|
import org.squiddev.cobalt.lib.*;
|
|
import org.squiddev.cobalt.lib.platform.VoidResourceManipulator;
|
|
|
|
import javax.annotation.Nonnull;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.IdentityHashMap;
|
|
import java.util.Map;
|
|
import java.util.concurrent.SynchronousQueue;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import static org.squiddev.cobalt.Constants.NONE;
|
|
import static org.squiddev.cobalt.ValueFactory.valueOf;
|
|
import static org.squiddev.cobalt.ValueFactory.varargsOf;
|
|
|
|
public class CobaltLuaMachine implements ILuaMachine
|
|
{
|
|
private static final ThreadPoolExecutor COROUTINES = new ThreadPoolExecutor(
|
|
0, Integer.MAX_VALUE,
|
|
5L, TimeUnit.MINUTES,
|
|
new SynchronousQueue<>(),
|
|
ThreadUtils.factory( "Coroutine" )
|
|
);
|
|
|
|
private final Computer m_computer;
|
|
private final TimeoutState timeout;
|
|
|
|
private LuaState m_state;
|
|
private LuaTable m_globals;
|
|
|
|
private LuaThread m_mainRoutine = null;
|
|
private String m_eventFilter = null;
|
|
|
|
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()
|
|
.resourceManipulator( new VoidResourceManipulator() )
|
|
.debug( new DebugHandler()
|
|
{
|
|
private int count = 0;
|
|
private boolean hasSoftAbort;
|
|
|
|
@Override
|
|
public void onInstruction( DebugState ds, DebugFrame di, int pc ) throws LuaError, UnwindThrowable
|
|
{
|
|
// 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
|
|
{
|
|
handleAbort();
|
|
}
|
|
|
|
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.
|
|
if( !timeout.isSoftAborted() )
|
|
{
|
|
hasSoftAbort = false;
|
|
return;
|
|
}
|
|
|
|
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( TimeoutState.ABORT_MESSAGE );
|
|
}
|
|
} )
|
|
.coroutineExecutor( command -> {
|
|
Tracking.addValue( m_computer, TrackingField.COROUTINES_CREATED, 1 );
|
|
COROUTINES.execute( () -> {
|
|
try
|
|
{
|
|
command.run();
|
|
}
|
|
finally
|
|
{
|
|
Tracking.addValue( m_computer, TrackingField.COROUTINES_DISPOSED, 1 );
|
|
}
|
|
} );
|
|
} )
|
|
.build();
|
|
|
|
m_globals = new LuaTable();
|
|
state.setupThread( m_globals );
|
|
|
|
// Add basic libraries
|
|
m_globals.load( state, new BaseLib() );
|
|
m_globals.load( state, new TableLib() );
|
|
m_globals.load( state, new StringLib() );
|
|
m_globals.load( state, new MathLib() );
|
|
m_globals.load( state, new CoroutineLib() );
|
|
m_globals.load( state, new Bit32Lib() );
|
|
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( "loadstring", new PrefixWrapperFunction( m_globals.rawget( "loadstring" ), 1 ) );
|
|
|
|
// Remove globals we don't want to expose
|
|
m_globals.rawset( "collectgarbage", Constants.NIL );
|
|
m_globals.rawset( "dofile", Constants.NIL );
|
|
m_globals.rawset( "loadfile", Constants.NIL );
|
|
m_globals.rawset( "print", Constants.NIL );
|
|
|
|
// Add version globals
|
|
m_globals.rawset( "_VERSION", valueOf( "Lua 5.1" ) );
|
|
m_globals.rawset( "_HOST", valueOf( computer.getAPIEnvironment().getComputerEnvironment().getHostString() ) );
|
|
m_globals.rawset( "_CC_DEFAULT_SETTINGS", valueOf( ComputerCraft.default_computer_settings ) );
|
|
if( ComputerCraft.disable_lua51_features )
|
|
{
|
|
m_globals.rawset( "_CC_DISABLE_LUA51_FEATURES", Constants.TRUE );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addAPI( ILuaAPI api )
|
|
{
|
|
// Add the methods of an API to the global table
|
|
LuaTable table = wrapLuaObject( api );
|
|
String[] names = api.getNames();
|
|
for( String name : names )
|
|
{
|
|
m_globals.rawset( name, table );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void loadBios( InputStream bios )
|
|
{
|
|
// Begin executing a file (ie, the bios)
|
|
if( m_mainRoutine != null ) return;
|
|
|
|
try
|
|
{
|
|
LuaFunction value = LoadState.load( m_state, bios, "@bios.lua", m_globals );
|
|
m_mainRoutine = new LuaThread( m_state, value, m_globals );
|
|
}
|
|
catch( CompileException e )
|
|
{
|
|
close();
|
|
}
|
|
catch( IOException e )
|
|
{
|
|
ComputerCraft.log.warn( "Could not load bios.lua ", e );
|
|
close();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void handleEvent( String eventName, Object[] arguments )
|
|
{
|
|
if( m_mainRoutine == null ) return;
|
|
|
|
if( m_eventFilter != null && eventName != null && !eventName.equals( m_eventFilter ) && !eventName.equals( "terminate" ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Varargs resumeArgs = Constants.NONE;
|
|
if( eventName != null )
|
|
{
|
|
resumeArgs = varargsOf( valueOf( eventName ), toValues( arguments ) );
|
|
}
|
|
|
|
Varargs results = LuaThread.run( m_mainRoutine, resumeArgs );
|
|
if( timeout.isHardAborted() ) throw new LuaError( TimeoutState.ABORT_MESSAGE );
|
|
|
|
LuaValue filter = results.first();
|
|
m_eventFilter = filter.isString() ? filter.toString() : null;
|
|
|
|
if( m_mainRoutine.getStatus().equals( "dead" ) ) close();
|
|
}
|
|
catch( LuaError | HardAbortError | InterruptedException e )
|
|
{
|
|
close();
|
|
ComputerCraft.log.warn( "Top level coroutine errored", e );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean isFinished()
|
|
{
|
|
return m_mainRoutine == null;
|
|
}
|
|
|
|
@Override
|
|
public void close()
|
|
{
|
|
LuaState state = m_state;
|
|
if( state == null ) return;
|
|
|
|
state.abandon();
|
|
m_mainRoutine = null;
|
|
m_state = null;
|
|
m_globals = null;
|
|
}
|
|
|
|
private LuaTable wrapLuaObject( ILuaObject object )
|
|
{
|
|
LuaTable table = new LuaTable();
|
|
String[] methods = object.getMethodNames();
|
|
for( int i = 0; i < methods.length; i++ )
|
|
{
|
|
if( methods[i] != null )
|
|
{
|
|
final int method = i;
|
|
final ILuaObject apiObject = object;
|
|
final String methodName = methods[i];
|
|
table.rawset( methodName, new VarArgFunction()
|
|
{
|
|
@Override
|
|
public Varargs invoke( final LuaState state, Varargs _args ) throws LuaError
|
|
{
|
|
Object[] arguments = toObjects( _args, 1 );
|
|
Object[] results;
|
|
try
|
|
{
|
|
results = apiObject.callMethod( new ILuaContext()
|
|
{
|
|
@Nonnull
|
|
@Override
|
|
public Object[] pullEvent( String filter ) throws LuaException, InterruptedException
|
|
{
|
|
Object[] results = pullEventRaw( filter );
|
|
if( results.length >= 1 && results[0].equals( "terminate" ) )
|
|
{
|
|
throw new LuaException( "Terminated", 0 );
|
|
}
|
|
return results;
|
|
}
|
|
|
|
@Nonnull
|
|
@Override
|
|
public Object[] pullEventRaw( String filter ) throws InterruptedException
|
|
{
|
|
return yield( new Object[] { filter } );
|
|
}
|
|
|
|
@Nonnull
|
|
@Override
|
|
public Object[] yield( Object[] yieldArgs ) throws InterruptedException
|
|
{
|
|
try
|
|
{
|
|
Varargs results = LuaThread.yieldBlocking( state, toValues( yieldArgs ) );
|
|
return toObjects( results, 1 );
|
|
}
|
|
catch( LuaError e )
|
|
{
|
|
throw new IllegalStateException( e );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long issueMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException
|
|
{
|
|
// Issue command
|
|
final long taskID = MainThread.getUniqueTaskID();
|
|
final ITask iTask = new ITask()
|
|
{
|
|
@Nonnull
|
|
@Override
|
|
public Computer getOwner()
|
|
{
|
|
return m_computer;
|
|
}
|
|
|
|
@Override
|
|
public void execute()
|
|
{
|
|
try
|
|
{
|
|
Object[] results = task.execute();
|
|
if( results != null )
|
|
{
|
|
Object[] eventArguments = new Object[results.length + 2];
|
|
eventArguments[0] = taskID;
|
|
eventArguments[1] = true;
|
|
System.arraycopy( results, 0, eventArguments, 2, results.length );
|
|
m_computer.queueEvent( "task_complete", eventArguments );
|
|
}
|
|
else
|
|
{
|
|
m_computer.queueEvent( "task_complete", new Object[] { taskID, true } );
|
|
}
|
|
}
|
|
catch( LuaException e )
|
|
{
|
|
m_computer.queueEvent( "task_complete", new Object[] {
|
|
taskID, false, e.getMessage()
|
|
} );
|
|
}
|
|
catch( Throwable t )
|
|
{
|
|
if( ComputerCraft.logPeripheralErrors )
|
|
{
|
|
ComputerCraft.log.error( "Error running task", t );
|
|
}
|
|
m_computer.queueEvent( "task_complete", new Object[] {
|
|
taskID, false, "Java Exception Thrown: " + t.toString()
|
|
} );
|
|
}
|
|
}
|
|
};
|
|
if( MainThread.queueTask( iTask ) )
|
|
{
|
|
return taskID;
|
|
}
|
|
else
|
|
{
|
|
throw new LuaException( "Task limit exceeded" );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Object[] executeMainThreadTask( @Nonnull final ILuaTask task ) throws LuaException, InterruptedException
|
|
{
|
|
// Issue task
|
|
final long taskID = issueMainThreadTask( task );
|
|
|
|
// Wait for response
|
|
while( true )
|
|
{
|
|
Object[] response = pullEvent( "task_complete" );
|
|
if( response.length >= 3 && response[1] instanceof Number && response[2] instanceof Boolean )
|
|
{
|
|
if( ((Number) response[1]).intValue() == taskID )
|
|
{
|
|
Object[] returnValues = new Object[response.length - 3];
|
|
if( (Boolean) response[2] )
|
|
{
|
|
// Extract the return values from the event and return them
|
|
System.arraycopy( response, 3, returnValues, 0, returnValues.length );
|
|
return returnValues;
|
|
}
|
|
else
|
|
{
|
|
// Extract the error message from the event and raise it
|
|
if( response.length >= 4 && response[3] instanceof String )
|
|
{
|
|
throw new LuaException( (String) response[3] );
|
|
}
|
|
else
|
|
{
|
|
throw new LuaException();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}, method, arguments );
|
|
}
|
|
catch( InterruptedException e )
|
|
{
|
|
throw new OrphanedThread();
|
|
}
|
|
catch( LuaException e )
|
|
{
|
|
throw new LuaError( e.getMessage(), e.getLevel() );
|
|
}
|
|
catch( Throwable t )
|
|
{
|
|
if( ComputerCraft.logPeripheralErrors )
|
|
{
|
|
ComputerCraft.log.error( "Error calling " + methodName + " on " + apiObject, t );
|
|
}
|
|
throw new LuaError( "Java Exception Thrown: " + t.toString(), 0 );
|
|
}
|
|
return toValues( results );
|
|
}
|
|
} );
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
|
|
private LuaValue toValue( Object object, Map<Object, LuaValue> values )
|
|
{
|
|
if( object == null )
|
|
{
|
|
return Constants.NIL;
|
|
}
|
|
else if( object instanceof Number )
|
|
{
|
|
double d = ((Number) object).doubleValue();
|
|
return valueOf( d );
|
|
}
|
|
else if( object instanceof Boolean )
|
|
{
|
|
return valueOf( (Boolean) object );
|
|
}
|
|
else if( object instanceof String )
|
|
{
|
|
String s = object.toString();
|
|
return valueOf( s );
|
|
}
|
|
else if( object instanceof byte[] )
|
|
{
|
|
byte[] b = (byte[]) object;
|
|
return valueOf( Arrays.copyOf( b, b.length ) );
|
|
}
|
|
else if( object instanceof Map )
|
|
{
|
|
// Table:
|
|
// Start remembering stuff
|
|
if( values == null )
|
|
{
|
|
values = new IdentityHashMap<>();
|
|
}
|
|
else if( values.containsKey( object ) )
|
|
{
|
|
return values.get( object );
|
|
}
|
|
LuaTable table = new LuaTable();
|
|
values.put( object, table );
|
|
|
|
// Convert all keys
|
|
for( Map.Entry<?, ?> pair : ((Map<?, ?>) object).entrySet() )
|
|
{
|
|
LuaValue key = toValue( pair.getKey(), values );
|
|
LuaValue value = toValue( pair.getValue(), values );
|
|
if( !key.isNil() && !value.isNil() )
|
|
{
|
|
table.rawset( key, value );
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
else if( object instanceof ILuaObject )
|
|
{
|
|
return wrapLuaObject( (ILuaObject) object );
|
|
}
|
|
else
|
|
{
|
|
return Constants.NIL;
|
|
}
|
|
}
|
|
|
|
private Varargs toValues( Object[] objects )
|
|
{
|
|
if( objects == null || objects.length == 0 )
|
|
{
|
|
return Constants.NONE;
|
|
}
|
|
|
|
LuaValue[] values = new LuaValue[objects.length];
|
|
for( int i = 0; i < values.length; i++ )
|
|
{
|
|
Object object = objects[i];
|
|
values[i] = toValue( object, null );
|
|
}
|
|
return varargsOf( values );
|
|
}
|
|
|
|
private 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<>();
|
|
}
|
|
else if( objects.containsKey( value ) )
|
|
{
|
|
return objects.get( value );
|
|
}
|
|
Map<Object, Object> table = new HashMap<>();
|
|
objects.put( value, table );
|
|
|
|
LuaTable luaTable = (LuaTable) value;
|
|
|
|
// Convert all keys
|
|
LuaValue k = Constants.NIL;
|
|
while( true )
|
|
{
|
|
Varargs keyValue;
|
|
try
|
|
{
|
|
keyValue = luaTable.next( k );
|
|
}
|
|
catch( LuaError luaError )
|
|
{
|
|
break;
|
|
}
|
|
k = keyValue.first();
|
|
if( k.isNil() )
|
|
{
|
|
break;
|
|
}
|
|
|
|
LuaValue v = keyValue.arg( 2 );
|
|
Object keyObject = toObject( k, objects );
|
|
Object valueObject = toObject( v, objects );
|
|
if( keyObject != null && valueObject != null )
|
|
{
|
|
table.put( keyObject, valueObject );
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
default:
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Object[] toObjects( Varargs values, int startIdx )
|
|
{
|
|
int count = values.count();
|
|
Object[] objects = new Object[count - startIdx + 1];
|
|
for( int n = startIdx; n <= count; n++ )
|
|
{
|
|
int i = n - startIdx;
|
|
LuaValue value = values.arg( n );
|
|
objects[i] = toObject( value, null );
|
|
}
|
|
return objects;
|
|
}
|
|
|
|
private static class PrefixWrapperFunction extends VarArgFunction
|
|
{
|
|
private static final LuaString FUNCTION_STR = valueOf( "function" );
|
|
private static final LuaString EQ_STR = valueOf( "=" );
|
|
|
|
private final LibFunction underlying;
|
|
|
|
public PrefixWrapperFunction( LuaValue wrap, int opcode )
|
|
{
|
|
LibFunction underlying = (LibFunction) wrap;
|
|
|
|
this.underlying = underlying;
|
|
this.opcode = opcode;
|
|
this.name = underlying.debugName();
|
|
this.env = underlying.getfenv();
|
|
}
|
|
|
|
@Override
|
|
public Varargs invoke( LuaState state, Varargs args ) throws LuaError, UnwindThrowable
|
|
{
|
|
switch( opcode )
|
|
{
|
|
case 0: // "load", // ( func [,chunkname] ) -> chunk | nil, msg
|
|
{
|
|
LuaValue func = args.arg( 1 ).checkFunction();
|
|
LuaString chunkname = args.arg( 2 ).optLuaString( FUNCTION_STR );
|
|
if( !chunkname.startsWith( '@' ) && !chunkname.startsWith( '=' ) )
|
|
{
|
|
chunkname = OperationHelper.concat( EQ_STR, chunkname );
|
|
}
|
|
return underlying.invoke( state, varargsOf( func, chunkname ) );
|
|
}
|
|
case 1: // "loadstring", // ( string [,chunkname] ) -> chunk | nil, msg
|
|
{
|
|
LuaString script = args.arg( 1 ).checkLuaString();
|
|
LuaString chunkname = args.arg( 2 ).optLuaString( script );
|
|
if( !chunkname.startsWith( '@' ) && !chunkname.startsWith( '=' ) )
|
|
{
|
|
chunkname = OperationHelper.concat( EQ_STR, chunkname );
|
|
}
|
|
return underlying.invoke( state, varargsOf( script, chunkname ) );
|
|
}
|
|
}
|
|
|
|
return NONE;
|
|
}
|
|
}
|
|
|
|
private static class HardAbortError extends Error
|
|
{
|
|
private static final long serialVersionUID = 7954092008586367501L;
|
|
|
|
public static final HardAbortError INSTANCE = new HardAbortError();
|
|
|
|
private HardAbortError()
|
|
{
|
|
super( "Hard Abort", null, true, false );
|
|
}
|
|
}
|
|
}
|