1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2024-06-26 07:03:22 +00:00
CC-Tweaked/src/main/java/dan200/computercraft/core/lua/CobaltLuaMachine.java
SquidDev d02575528b Fix leaking file descriptors when closing
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.
2019-02-28 11:24:12 +00:00

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 );
}
}
}