mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-06-26 07:03:22 +00:00
d02575528b
When closing a BufferedWriter, we close the underlying writer. As we're using channels, this is an instance of sun.nio.cs.StreamEncoder. This will attempt to flush the pending character. However, if throwing an exception within .write errors, the flush will fail and so the underlying stream is not closed. This was causing us to leak file descriptors. We fix this by introducing ChannelWrappers - this holds the wrapper object (say, a BufferedWriter) and underlying channel. When closed, we dispose of the wrapper, and then the channel. You could think of this as doing a nested try-with-resources, rather than a single one. Note, this is not related to JDK-6378948 - this occurs in the underlying stream encoder instead.
652 lines
25 KiB
Java
652 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 we have fired our soft abort, but we haven't been hard aborted then everything is OK.
|
|
if( hasSoftAbort ) 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( @Nonnull 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( @Nonnull 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( HardAbortError | InterruptedException e )
|
|
{
|
|
close();
|
|
}
|
|
catch( LuaError 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 );
|
|
}
|
|
}
|
|
}
|