/**
* Copyright (c) 2002-2011 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.RemoteObject;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
public abstract class SubProcess<T, P> implements Serializable
{
private interface NoInterface
{
}
private final Class<T> t;
@SuppressWarnings( "unchecked" )
public SubProcess()
{
if ( getClass().getSuperclass() != SubProcess.class )
{
throw new ClassCastException( SubProcess.class.getName()
+ " may only be extended one level " );
}
Class<?> me = getClass();
while ( me.getSuperclass() != SubProcess.class )
{
me = me.getSuperclass();
}
Type type = ( (ParameterizedType) me.getGenericSuperclass() ).getActualTypeArguments()[0];
@SuppressWarnings( { "hiding" } ) Class<T> t;
if ( type instanceof Class<?> )
{
t = (Class<T>) type;
}
else if ( type instanceof ParameterizedType )
{
t = (Class<T>) ( (ParameterizedType) type ).getRawType();
}
else
{
throw new ClassCastException( "Illegal type parameter " + type );
}
if ( t == Object.class ) t = (Class<T>) (Class) NoInterface.class;
if ( !t.isInterface() )
{
throw new ClassCastException( t + " is not an interface" );
}
if ( t.isAssignableFrom( getClass() ) || t == NoInterface.class )
{
this.t = t;
}
else
{
throw new ClassCastException( getClass().getName()
+ " must implement declared interface " + t );
}
}
public T start( P parameter )
{
DispatcherTrapImpl callback;
try
{
callback = new DispatcherTrapImpl( this, parameter );
}
catch ( RemoteException e )
{
throw new RuntimeException( "Failed to create local RMI endpoint.", e );
}
ProcessBuilder builder = new ProcessBuilder( "java", "-cp",
System.getProperty( "java.class.path" ), SubProcess.class.getName(),
serialize( callback ) );
Process process;
try
{
process = builder.start();
}
catch ( IOException e )
{
throw new RuntimeException( "Failed to start sub process", e );
}
String pid = getPid( process );
pipe( "[" + toString() + ":" + pid + "] ", process.getErrorStream(), System.err );
pipe( "[" + toString() + ":" + pid + "] ", process.getInputStream(), System.out );
Dispatcher dispatcher = callback.get( process );
if ( dispatcher == null ) throw new IllegalStateException( "failed to start sub process" );
return t.cast( Proxy.newProxyInstance( t.getClassLoader(), new Class[] { t },//
live( new Handler( t, dispatcher, process, "<" + toString() + ":" + pid + ">" ) ) ) );
}
protected abstract void startup( P parameter ) throws Throwable;
protected void shutdown()
{
System.exit( 0 );
}
public static void stop( Object subprocess )
{
( (Handler) Proxy.getInvocationHandler( subprocess ) ).stop();
}
public static void kill( Object subprocess )
{
( (Handler) Proxy.getInvocationHandler( subprocess ) ).kill( true );
}
@Override
public String toString()
{
return getClass().getSimpleName();
}
public static void main( String[] args ) throws Throwable
{
if ( args.length != 1 )
{
throw new IllegalArgumentException( "Needs to be started from "
+ SubProcess.class.getName() );
}
DispatcherTrap trap = deserialize( args[0] );
SubProcess<?, Object> subProcess = trap.getSubProcess();
subProcess.startup( trap.trap( new DispatcherImpl( subProcess ) ) );
}
private static final Field PID;
static
{
Field pid;
try
{
pid = ( (Class<?>) Class.forName( "java.lang.UNIXProcess" ) ).getDeclaredField( "pid" );
pid.setAccessible( true );
}
catch ( Throwable ex )
{
pid = null;
}
PID = pid;
}
private int lastPid = 0;
private String getPid( Process process )
{
if ( PID != null )
{
try
{
return PID.get( process ).toString();
}
catch ( Exception ok )
{
}
}
return Integer.toString( lastPid++ );
}
private static class PipeTask
{
private final String prefix;
private final InputStream source;
private final PrintStream target;
private StringBuilder line;
PipeTask( String prefix, InputStream source, PrintStream target )
{
this.prefix = prefix;
this.source = source;
this.target = target;
line = new StringBuilder();
}
boolean pipe()
{
try
{
int available = source.available();
if ( available != 0 )
{
byte[] data = new byte[available /*- ( available % 2 )*/];
source.read( data );
ByteBuffer chars = ByteBuffer.wrap( data );
while ( chars.hasRemaining() )
{
char c = (char) chars.get();
line.append( c );
if ( c == '\n' )
{
print();
}
}
}
}
catch ( IOException e )
{
if ( line.length() > 0 )
{
line.append( '\n' );
print();
}
return false;
}
return true;
}
private void print()
{
target.print( prefix + line.toString() );
line = new StringBuilder();
}
}
private static class PipeThread extends Thread
{
final CopyOnWriteArrayList<PipeTask> tasks = new CopyOnWriteArrayList<PipeTask>();
@Override
public void run()
{
while ( true )
{
List<PipeTask> done = new ArrayList<PipeTask>();
for ( PipeTask task : tasks )
{
if ( !task.pipe() )
{
done.add( task );
}
}
if ( !done.isEmpty() ) tasks.removeAll( done );
if ( tasks.isEmpty() )
{
synchronized ( PipeThread.class )
{
if ( tasks.isEmpty() )
{
piper = null;
return;
}
}
}
try
{
Thread.sleep( 10 );
}
catch ( InterruptedException e )
{
Thread.interrupted();
}
}
}
}
private static PipeThread piper;
private static void pipe( final String prefix, final InputStream source,
final PrintStream target )
{
synchronized ( PipeThread.class )
{
if ( piper == null )
{
piper = new PipeThread();
piper.start();
}
piper.tasks.add( new PipeTask( prefix, source, target ) );
}
}
private interface DispatcherTrap extends Remote
{
Object trap( Dispatcher dispatcher ) throws RemoteException;
SubProcess<?, Object> getSubProcess() throws RemoteException;
}
private static class DispatcherTrapImpl extends UnicastRemoteObject implements DispatcherTrap
{
private final Object parameter;
private volatile Dispatcher dispatcher;
private final SubProcess<?, ?> process;
DispatcherTrapImpl( SubProcess<?, ?> process, Object parameter ) throws RemoteException
{
super();
this.process = process;
this.parameter = parameter;
}
Dispatcher get( Process process )
{
while ( dispatcher == null )
{
try
{
Thread.sleep( 10 );
}
catch ( InterruptedException e )
{
Thread.currentThread().interrupt();
}
try
{
process.exitValue();
}
catch ( IllegalThreadStateException e )
{
continue;
}
return null;
}
return dispatcher;
}
public synchronized Object trap( Dispatcher dispatcher )
{
if ( this.dispatcher != null )
throw new IllegalStateException( "Dispatcher already trapped!" );
this.dispatcher = dispatcher;
return parameter;
}
@SuppressWarnings( "unchecked" )
public SubProcess<?, Object> getSubProcess()
{
return (SubProcess<?, Object>) process;
}
}
@SuppressWarnings( "restriction" )
private static String serialize( DispatcherTrapImpl obj )
{
ByteArrayOutputStream os = new ByteArrayOutputStream();
try
{
ObjectOutputStream oos = new ObjectOutputStream( os );
oos.writeObject( RemoteObject.toStub( obj ) );
oos.close();
}
catch ( IOException e )
{
throw new RuntimeException( "Broken implementation!", e );
}
return new sun.misc.BASE64Encoder().encode( os.toByteArray() );
}
@SuppressWarnings( "restriction" )
private static DispatcherTrap deserialize( String data )
{
try
{
return (DispatcherTrap) new ObjectInputStream( new ByteArrayInputStream(
new sun.misc.BASE64Decoder().decodeBuffer( data ) ) ).readObject();
}
catch ( Exception e )
{
return null;
}
}
private interface Dispatcher extends Remote
{
void stop() throws RemoteException;
Object dispatch( String name, String[] types, Object[] args ) throws RemoteException,
Throwable;
}
private static InvocationHandler live( Handler handler )
{
try
{
synchronized ( Handler.class )
{
if ( live == null )
{
final Set<Handler> handlers = live = new HashSet<Handler>();
Runtime.getRuntime().addShutdownHook( new Thread()
{
@Override
public void run()
{
killAll( handlers );
}
} );
}
live.add( handler );
}
}
catch ( UnsupportedOperationException e )
{
handler.kill( false );
throw new IllegalStateException( "JVM is shutting down!" );
}
return handler;
}
private static void dead( Handler handler )
{
synchronized ( Handler.class )
{
try
{
if ( live != null ) live.remove( handler );
}
catch ( UnsupportedOperationException ok )
{
}
}
}
private static void killAll( Set<Handler> handlers )
{
synchronized ( Handler.class )
{
if ( !handlers.isEmpty() )
{
for ( Handler handler : handlers )
{
try
{
handler.process.exitValue();
}
catch ( IllegalThreadStateException e )
{
handler.kill( false );
}
}
}
live = Collections.emptySet();
}
}
private static Set<Handler> live;
private static class Handler implements InvocationHandler
{
private final Dispatcher dispatcher;
private final Process process;
private final Class<?> type;
private final String repr;
Handler( Class<?> type, Dispatcher dispatcher, Process process, String repr )
{
this.type = type;
this.dispatcher = dispatcher;
this.process = process;
this.repr = repr;
}
@Override
public String toString()
{
return repr;
}
void kill( boolean wait )
{
process.destroy();
if ( wait )
{
dead( this );
await( process );
}
}
int stop()
{
try
{
dispatcher.stop();
}
catch ( RemoteException e )
{
process.destroy();
}
dead( this );
return await( process );
}
private static int await( Process process )
{
try
{
return process.waitFor();
}
catch ( InterruptedException e )
{
Thread.interrupted();
return 0;
}
}
public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable
{
try
{
if ( method.getDeclaringClass() == type )
{
return dispatch( method, args );
}
else if ( method.getDeclaringClass() == Object.class )
{
return method.invoke( this, args );
}
else
{
throw new UnsupportedOperationException( method.toString() );
}
}
catch ( RemoteException ex )
{
throw new IllegalStateException( "Subprocess connection disrupted", ex );
}
}
private Object dispatch( Method method, Object[] args ) throws Throwable
{
Class<?>[] params = method.getParameterTypes();
String[] types = new String[params.length];
for ( int i = 0; i < types.length; i++ )
{
types[i] = params[i].getName();
}
return dispatcher.dispatch( method.getName(), types, args );
}
}
private static class DispatcherImpl extends UnicastRemoteObject implements Dispatcher
{
private transient final SubProcess<?, ?> subprocess;
protected DispatcherImpl( SubProcess<?, ?> subprocess ) throws RemoteException
{
super();
this.subprocess = subprocess;
}
public Object dispatch( String name, String[] types, Object[] args )
throws RemoteException, Throwable
{
Class<?>[] params = new Class<?>[types.length];
for ( int i = 0; i < params.length; i++ )
{
params[i] = Class.forName( types[i] );
}
try
{
return subprocess.t.getMethod( name, params ).invoke( subprocess, args );
}
catch ( IllegalAccessException e )
{
throw new IllegalStateException( e );
}
catch ( InvocationTargetException e )
{
throw e.getTargetException();
}
}
public void stop() throws RemoteException
{
subprocess.shutdown();
}
}
}