/* * Copyright 2008 Alin Dreghiciu. * Copyright 2009 Toni Menzel. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. * * See the License for the specific language governing permissions and * limitations under the License. */ package org.ops4j.pax.exam.container.def.internal; import static org.ops4j.pax.exam.Constants.START_LEVEL_SYSTEM_BUNDLES; import static org.ops4j.pax.exam.Constants.WAIT_FOREVER; import static org.ops4j.pax.exam.CoreOptions.bootDelegationPackage; import static org.ops4j.pax.exam.CoreOptions.mavenBundle; import static org.ops4j.pax.exam.CoreOptions.systemProperty; import static org.ops4j.pax.exam.OptionUtils.combine; import static org.ops4j.pax.exam.OptionUtils.expand; import static org.ops4j.pax.exam.OptionUtils.filter; import static org.ops4j.pax.exam.OptionUtils.remove; import static org.ops4j.pax.exam.container.def.PaxRunnerOptions.scanBundle; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.rmi.NoSuchObjectException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.ops4j.io.FileUtils; import org.ops4j.pax.exam.CompositeCustomizer; import org.ops4j.pax.exam.CoreOptions; import org.ops4j.pax.exam.Info; import org.ops4j.pax.exam.Option; import org.ops4j.pax.exam.container.def.options.BundleScannerProvisionOption; import org.ops4j.pax.exam.container.def.options.RBCLookupTimeoutOption; import org.ops4j.pax.exam.container.def.options.Scanner; import org.ops4j.pax.exam.options.ProvisionOption; import org.ops4j.pax.exam.options.TestContainerStartTimeoutOption; import org.ops4j.pax.exam.rbc.Constants; import org.ops4j.pax.exam.rbc.client.RemoteBundleContextClient; import org.ops4j.pax.exam.spi.container.TestContainer; import org.ops4j.pax.exam.spi.container.TestContainerException; import org.ops4j.pax.exam.spi.container.TimeoutException; import org.ops4j.pax.runner.Run; import org.ops4j.pax.runner.handler.internal.URLUtils; import org.ops4j.pax.runner.platform.DefaultJavaRunner; import org.ops4j.store.Handle; import org.ops4j.store.Store; import org.ops4j.store.StoreFactory; import org.osgi.framework.Bundle; /** * {@link TestContainer} implementation using Pax Runner. * * @author Alin Dreghiciu (adreghiciu@gmail.com) * @since 0.3.0, December 09, 2008 */ class PaxRunnerTestContainer implements TestContainer { /** * JCL logger. */ // private static final Log LOG = GrowlFactory.getLogger( LogFactory.getLog( PaxRunnerTestContainer.class ), // "Pax Exam", // GrowlLogger.GROWL_INFO | GrowlLogger.GROWL_ERROR // ); private static final Log LOG = LogFactory.getLog( PaxRunnerTestContainer.class ); /** * Number of ports to check for a free rmi communication port. */ private static final int AMOUNT_OF_PORTS_TO_CHECK = 100; /** * System bundle id. */ private static final int SYSTEM_BUNDLE = 0; /** * Remote bundle context client. */ private final RemoteBundleContextClient m_remoteBundleContextClient; /** * Java runner to be used to start up Pax Runner. */ private final DefaultJavaRunner m_javaRunner; /** * Pax Runner arguments, out of options. */ private final ArgumentsBuilder m_arguments; /** * test container start timeout. */ private final long m_startTimeout; private final Store<InputStream> m_store; private final Map<String, Handle> m_cache; private final CompositeCustomizer m_customizers; /** * */ private TestContainerSemaphore m_semaphore; private boolean m_started = false; /** * RMI registry. */ private Registry m_registry; /** * Constructor. * * @param javaRunner java runner to be used to start up Pax Runner * @param options user startup options */ PaxRunnerTestContainer( final DefaultJavaRunner javaRunner, final Option... options ) { m_javaRunner = javaRunner; m_startTimeout = getTestContainerStartTimeout( options ); int registryPort = createRegistry(); m_remoteBundleContextClient = new RemoteBundleContextClient( registryPort, getRMITimeout( options ) ); m_arguments = new ArgumentsBuilder( wrap( expand( combine( options, localOptions() ) ) ) ); m_customizers = new CompositeCustomizer( m_arguments.getCustomizers() ); m_store = StoreFactory.sharedLocalStore(); m_cache = new HashMap<String, Handle>(); } /** * {@inheritDoc} Delegates to {@link RemoteBundleContextClient}. */ public <T> T getService( final Class<T> serviceType ) { LOG.debug( "Lookup a [" + serviceType.getName() + "]" ); return m_remoteBundleContextClient.getService( serviceType ); } /** * {@inheritDoc} Delegates to {@link RemoteBundleContextClient}. */ public <T> T getService( final Class<T> serviceType, final long timeoutInMillis ) { LOG.debug( "Lookup a [" + serviceType.getName() + "]" ); return m_remoteBundleContextClient.getService( serviceType, timeoutInMillis ); } /** * {@inheritDoc} Delegates to {@link RemoteBundleContextClient}. */ public long installBundle( final String bundleUrl ) { LOG.debug( "Preparing and Installing bundle [" + bundleUrl + "] .." ); long id; try { id = m_remoteBundleContextClient.installBundle( m_store.getLocation( storeAndGetData( bundleUrl ) ).toASCIIString() ); } catch ( IOException e ) { throw new RuntimeException( e ); } LOG.debug( "Installed bundle " + bundleUrl + " as ID: " + id ); return id; } private Handle storeAndGetData( String bundleUrl ) { try { Handle handle = m_cache.get( bundleUrl ); if ( handle == null ) { // new, so build, customize and store URL url = new URL( bundleUrl ); InputStream in = url.openStream(); in = m_customizers.customizeTestProbe( in ); // store in and overwrite handle handle = m_store.store( in ); m_cache.put( bundleUrl, handle ); } return handle; } catch ( Exception e ) { LOG.error( "problem in preparing probe. ", e ); } return null; } /** * {@inheritDoc} Delegates to {@link RemoteBundleContextClient}. */ public long installBundle( final String bundleLocation, final byte[] bundle ) { LOG.debug( "Installing bundle [" + bundleLocation + "] .." ); final long id = m_remoteBundleContextClient.installBundle( bundleLocation, bundle ); LOG.debug( "Installed bundle " + bundleLocation + " as ID: " + id ); return id; } /** * {@inheritDoc} Delegates to {@link RemoteBundleContextClient}. */ public void startBundle( long bundleId ) throws TestContainerException { LOG.debug( "Starting test bundle with ID " + bundleId ); m_remoteBundleContextClient.startBundle( bundleId ); LOG.debug( "Started test bundle with ID " + bundleId ); } /** * {@inheritDoc} Delegates to {@link RemoteBundleContextClient}. */ public void setBundleStartLevel( final long bundleId, final int startLevel ) throws TestContainerException { m_remoteBundleContextClient.setBundleStartLevel( bundleId, startLevel ); } /** * {@inheritDoc} */ public void start() { LOG.info( "Starting up the test container (Pax Runner " + Info.getPaxRunnerVersion() + " )" ); /** */ m_semaphore = new TestContainerSemaphore( m_arguments.getWorkingFolder() ); // this makes sure the system is ready to launch a new instance. // this could fail, based on what acquire actually checks. // this also creates some persistent mark that will be removed by m_semaphore.release() if ( !m_semaphore.acquire() ) { // here we can react. // Prompt user with the fact that there might be another instance running. if ( !FileUtils.delete( m_arguments.getWorkingFolder() ) ) { throw new RuntimeException( "There might be another instance of Pax Exam running. Have a look at " + m_semaphore.getLockFile().getAbsolutePath() ); } } // customize environment m_customizers.customizeEnvironment( m_arguments.getWorkingFolder() ); long startedAt = System.currentTimeMillis(); URLUtils.resetURLStreamHandlerFactory(); Run.start( m_javaRunner, m_arguments.getArguments() ); LOG.info( "Test container (Pax Runner " + Info.getPaxRunnerVersion() + ") started in " + ( System.currentTimeMillis() - startedAt ) + " millis" ); LOG.info( "Wait for test container to finish its initialization " + ( m_startTimeout == WAIT_FOREVER ? "without timing out" : "for " + m_startTimeout + " millis" ) ); try { waitForState( SYSTEM_BUNDLE, Bundle.ACTIVE, m_startTimeout ); } catch ( TimeoutException e ) { throw new TimeoutException( "Test container did not initialize in the expected time of " + m_startTimeout + " millis" ); } m_started = true; } /** * {@inheritDoc} */ public void stop() { LOG.info( "Shutting down the test container (Pax Runner)" ); try { if ( m_started ) { if ( m_remoteBundleContextClient != null ) { m_remoteBundleContextClient.stop(); } if ( m_javaRunner != null ) { m_javaRunner.waitForExit(); } if ( m_registry != null ) { try { UnicastRemoteObject.unexportObject( m_registry, true ); } catch (NoSuchObjectException e) { LOG.error( "Problem in shutting down RMI registry. ", e ); } // this is necessary, unfortunately.. RMI wouldn' stop otherwise System.gc(); LOG.info( "RMI registry stopped" ); m_registry = null; } } } finally { m_semaphore.release(); m_started = false; } } /** * {@inheritDoc} */ public void waitForState( final long bundleId, final int state, final long timeoutInMillis ) throws TimeoutException { m_remoteBundleContextClient.waitForState( bundleId, state, timeoutInMillis ); } /** * Return the options required by this container implementation. * * @return local options */ private Option[] localOptions() { return new Option[] { // remote bundle context bundle mavenBundle().groupId( "org.ops4j.pax.exam" ).artifactId( "pax-exam-container-rbc" ).version( Info.getPaxExamVersion() ).update( Info.isPaxExamSnapshotVersion() ).startLevel( START_LEVEL_SYSTEM_BUNDLES ), // rmi communication port systemProperty( Constants.RMI_PORT_PROPERTY ).value( m_remoteBundleContextClient.getRmiPort().toString() ), // boot delegation for sun.*. This seems only necessary in Knopflerfish version > 2.0.0 bootDelegationPackage( "sun.*" ) }; } /** * Wrap provision options that are not already scanner provision bundles with a {@link BundleScannerProvisionOption} * in order to force update. * * @param options options to be wrapped (can be null or an empty array) * @return eventual wrapped bundles */ private Option[] wrap( final Option... options ) { if ( options != null && options.length > 0 ) { // get provison options out of options final ProvisionOption[] provisionOptions = filter( ProvisionOption.class, options ); if ( provisionOptions != null && provisionOptions.length > 0 ) { final List<Option> processed = new ArrayList<Option>(); for ( final ProvisionOption provisionOption : provisionOptions ) { if ( !( provisionOption instanceof Scanner ) ) { processed.add( scanBundle( provisionOption ).start( provisionOption.shouldStart() ).startLevel( provisionOption.getStartLevel() ).update( provisionOption.shouldUpdate() ) ); } else { processed.add( provisionOption ); } } // finally combine the processed provision options with the original options // (where provison options are removed) return combine( remove( ProvisionOption.class, options ), processed.toArray( new Option[processed.size()] ) ); } } // if there is nothing to process of there are no provision options just return the original options return options; } /** * Determine the rmi lookup timeout.<br/> * Timeout is dermined by first looking for a {@link RBCLookupTimeoutOption} in the user options. If not specified a * default is used. * * @param options user options * @return rmi lookup timeout */ private static long getRMITimeout( final Option... options ) { final RBCLookupTimeoutOption[] timeoutOptions = filter( RBCLookupTimeoutOption.class, options ); if ( timeoutOptions.length > 0 ) { return timeoutOptions[0].getTimeout(); } return getTestContainerStartTimeout( options ); } /** * Determine the timeout while starting the osgi framework.<br/> * Timeout is dermined by first looking for a {@link TestContainerStartTimeoutOption} in the user options. If not * specified a default is used. * * @param options user options * @return rmi lookup timeout */ private static long getTestContainerStartTimeout( final Option... options ) { final TestContainerStartTimeoutOption[] timeoutOptions = filter( TestContainerStartTimeoutOption.class, options ); if ( timeoutOptions.length > 0 ) { return timeoutOptions[0].getTimeout(); } return CoreOptions.waitForFrameworkStartup().getTimeout(); } @Override public String toString() { return "PaxRunnerTestContainer{}"; } /** * Creates an RMI registry on the first free port found. * * @return RMI registry */ protected int createRegistry() { for( int port = Registry.REGISTRY_PORT; port <= Registry.REGISTRY_PORT + AMOUNT_OF_PORTS_TO_CHECK; port++ ) { try { m_registry = LocateRegistry.createRegistry( port ); LOG.info( "RMI registry started on port [" + port + "]" ); return port; } catch (Exception e) { // ignore and try next port number } } throw new RuntimeException( "No free port in range " + Registry.REGISTRY_PORT + ":" + Registry.REGISTRY_PORT + AMOUNT_OF_PORTS_TO_CHECK ); } }