/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * 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 Lesser General Public License for more details. * * Copyright 2016 Pentaho Corporation. All rights reserved. */ package org.pentaho.platform.osgi; import org.apache.commons.lang.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.net.ConnectException; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.FileLock; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This implementation resolves Karaf instance numbers based on a ServerSocket port strategy. * <p/> * It also handles assigning a cache folder appropriate for the client type (spoon, kitchen, carte, etc) guaranteed to * not be in use by another instance. * <p/> * Created by nbaker on 3/21/16. */ class ServerSocketBasedKarafInstanceResolver implements IKarafInstanceResolver { private static final int START_PORT_NUMBER = determineStartPort(); public static final String PENTAHO_KARAF_INSTANCE_START_PORT = "pentaho.karaf.instance.start.port"; static int determineStartPort() { String portStr = System.getProperty( PENTAHO_KARAF_INSTANCE_START_PORT, "11000" ); int portNo = NumberUtils.toInt( portStr ); return portNo != 0 ? portNo : 11000; } private static final int MAX_NUMBER_OF_KARAF_INSTANCES = 1000; public static final String DATA = "data"; private Logger logger = LoggerFactory.getLogger( getClass() ); @Override public void resolveInstance( KarafInstance instance ) throws KarafInstanceResolverException { // Obtaining a valid instance number in and of itself isn't sufficient. Since ports will be assigned based on // the instance number as an offset all ports must resolve as well otherwise the instance isn't valid and another // should be tried. int latestOffsetTried = 0; do { latestOffsetTried = resolveInstanceNumber( instance, latestOffsetTried ); } while ( !resolvePorts( instance ) ); // If no exception was thrown we're here now with all ports resolved assignAvailableCacheFolderForType( instance ); } private void assignAvailableCacheFolderForType( KarafInstance instance ) { // something like karaf/caches String cacheParentFolder = instance.getCacheParentFolder(); // We separate the caches by client type to avoid reuse of an inappropriate data folder File clientTypeCacheFolder = new File( cacheParentFolder + "/" + instance.getClientType() ); clientTypeCacheFolder.mkdirs(); File[] dataDirectories = clientTypeCacheFolder.listFiles( new FilenameFilter() { @Override public boolean accept( File dir, String name ) { return name.startsWith( DATA ); } } ); int maxInstanceNoFound = 0; Pattern pattern = Pattern.compile( DATA + "\\-([0-9]+)" ); // Go through existing data directories. If one is not in-use, use that. If not find the highest number and create // one greater for ( File dataDirectory : dataDirectories ) { boolean locked = true; Matcher matcher = pattern.matcher( dataDirectory.getName() ); if ( !matcher.matches() ) { // unexpected directory, not a data folder, skipping continue; } // extract the data folder number int instanceNo = Integer.parseInt( matcher.group( 1 ) ); maxInstanceNoFound = Math.max( maxInstanceNoFound, instanceNo ); File lockFile = new File( dataDirectory, ".lock" ); FileLock lock = null; if ( !lockFile.exists() ) { // Lock file was not present, we can use it locked = false; } else { // Lock file was there, see if another process actually holds a lock on it try { FileOutputStream fileOutputStream = new FileOutputStream( lockFile ); try { lock = fileOutputStream.getChannel().tryLock(); if ( lock != null ) { // not locked by another program instance.setCacheLock( lock ); locked = false; } } catch ( Exception e ) { // Lock active on another program } } catch ( FileNotFoundException e ) { logger.error( "Error locking file in data cache directory", e ); } } if ( !locked ) { instance.setCachePath( dataDirectory.getPath() ); // we're good to use this one, break out of existing directory loop break; } } if ( instance.getCachePath() == null ) { // Create a new cache folder File newCacheFolder = null; while ( newCacheFolder == null ) { maxInstanceNoFound++; File candidate = new File( clientTypeCacheFolder, DATA + "-" + maxInstanceNoFound ); if ( candidate.exists() ) { // Another process slipped in and created a folder, lets skip over them continue; } newCacheFolder = candidate; } FileOutputStream fileOutputStream = null; try { newCacheFolder.mkdir(); // create lock file and lock it for this process File lockFile = new File( newCacheFolder, ".lock" ); fileOutputStream = new FileOutputStream( lockFile ); FileLock lock = fileOutputStream.getChannel().lock(); instance.setCachePath( newCacheFolder.getPath() ); instance.setCacheLock( lock ); } catch ( IOException e ) { logger.error( "Error creating data cache folder", e ); } } } private boolean resolvePorts( KarafInstance instance ) { List<KarafInstancePort> ports = instance.getPorts(); int instanceNumber = instance.getInstanceNumber(); for ( KarafInstancePort port : ports ) { int portNo = port.getStartPort() + instanceNumber; if ( !isPortAvailable( portNo ) ) { return false; } port.setAssignedPort( portNo ); } return true; } private boolean isPortAvailable( int port ) { try ( Socket ignored = new Socket( "localhost", port ) ) { return false; } catch ( IOException e ) { return true; } } private int resolveInstanceNumber( KarafInstance instance, int latestOffsetTried ) throws KarafInstanceResolverException { logger.debug( "Attempting to resolve available Karaf instance number by way of Server Socket" ); int testInstance = latestOffsetTried + 1; Integer instanceNo = null; do { int candidate = START_PORT_NUMBER + testInstance; Socket socket = null; try { logger.debug( "Instance test. Trying port " + candidate ); socket = new Socket( "localhost", candidate ); socket.close(); } catch ( ConnectException e ) { // port not in-use try { ServerSocket ssocket = new ServerSocket( candidate ); instanceNo = testInstance; instance.setInstanceSocket( ssocket ); instance.setInstanceNumber( instanceNo ); logger.debug( "Karaf instance resolved to: " + instanceNo ); } catch ( IOException e1 ) { logger.error( "Error creating ServerSocket", e1 ); } } catch ( IOException ignored ) { // Some other error, move to next candidate } } while ( instanceNo == null && testInstance++ <= MAX_NUMBER_OF_KARAF_INSTANCES ); if ( instanceNo == null ) { throw new KarafInstanceResolverException( "Unable to resolve Karaf Instance number" ); } return instanceNo; } }