/* Milenia Grafter Server Copyright (c) 2007-2008 by Milan Toth. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package com.milgra.server; /** Server class @mail milgra@milgra.com @author Milan Toth @version 20080316 Tasks of Server - process command line parameters - create a new instance if needed - init closing on an old instance if needed - initialize containers - load and initialize custom applications - register applications - register processes - register clients - register streams **/ import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.URL; import java.net.Socket; import java.net.URLClassLoader; import java.util.List; import java.util.Arrays; import java.util.HashMap; import java.util.ArrayList; import com.milgra.server.api.Client; import com.milgra.server.api.IApplication; public class Server { public static SocketConnector socketConnector; // players - stream players // routers - stream routers public static ArrayList < OStream > players; public static ArrayList < OStream > routers; // jars - container for existing jar files // states - container for application states // pools - container for process groups // customs - container for custom application instances // clients - container for clients public static HashMap < String , File > jars; public static HashMap < String , Boolean > states; public static HashMap < String , ProcessGroup > pools; public static HashMap < String , IApplication > customs; public static HashMap < String , ArrayList < Client > > clients; /** * Server constructor **/ public Server ( ) { // System.out.println( System.currentTimeMillis() + " Server.construct" ); // construct players = new ArrayList < OStream > ( ); routers = new ArrayList < OStream > ( ); jars = new HashMap < String , File > ( ); pools = new HashMap < String , ProcessGroup > ( ); states = new HashMap < String , Boolean > ( ); customs = new HashMap < String , IApplication > ( ); clients = new HashMap < String , ArrayList < Client > > ( ); // register socket connector socketConnector = new SocketConnector( ); registerProcess( socketConnector , "sockets" ); // start readApplications( ); loadApplications( ); } /** * Reads up application jars **/ public static void readApplications ( ) { // System.out.println( System.currentTimeMillis( ) + " Server.readApplications" ); File directory = new File( Library.CUSTOMDIR ); if ( directory.exists( ) ) { // reset jars jars.clear( ); // get list File [ ] files = directory.listFiles( ); for ( File file : files ) { String fileID = file.getName( ); if ( fileID.endsWith( ".jar" ) ) { // extract application name String customID = fileID.substring( 0 , fileID.length( ) - 4 ); // store file jars.put( customID , file ); } } } else System.out.println( Library.NOAPPS ); } /** * Loads all available applications on startup **/ public static void loadApplications ( ) { // System.out.println( System.currentTimeMillis() + " Server.loadApplications" ); for ( String id : jars.keySet() ) loadApplication( id , id ); } /** * Loads an application based on the file name of the class * @param idX custom application id **/ public static void loadApplication ( String jarX , String idX ) { // System.out.println( System.currentTimeMillis( ) + " Server.loadApplication " + idX ); // check if application is already loaded if ( !customs.containsKey( idX ) ) { try { // create classloader File file = jars.get( jarX ); URL [ ] urls = new URL[ ]{ file.toURL() }; ClassLoader classLoader = new URLClassLoader( urls ); // instantiate Class <?> customClass = classLoader.loadClass( "application.Application" ); IApplication customInstance = ( IApplication ) customClass.newInstance( ); customInstance.onStart( idX ); // refresh containers states.put( idX , true ); customs.put( idX , customInstance ); clients.put( idX , new ArrayList < Client > ( ) ); } catch ( Exception exception ) { exception.printStackTrace( ); } } } /** * Unloads an application, cleans up resources * @param idX custom application id **/ public static void unloadApplication ( String idX ) { // System.out.println( System.currentTimeMillis( ) + " Server.unloadApplication " + idX ); if ( states.containsKey( idX ) ) { boolean state = states.get( idX ); if ( state ) { IApplication customInstance; ArrayList < Client > clientList; synchronized ( clients ) { // remove client registry // get custom application instance clientList = clients.remove( idX ); customInstance = customs.get( idX ); } // detaching clients for ( Client client : clientList ) client.detach( ); // custom application cleanup trigger customInstance.onClose( ); states.put( idX , false ); // if custom app jar is deleted we cannot reload if ( !jars.containsKey( idX ) ) { // cleanup containers states.remove( idX ); customs.remove( idX ); } } } } /** * Returns application instance based on id * @param idX custom application id * @return IApplication custom application instance **/ public static IApplication getApplication ( String idX ) { // System.out.println( System.currentTimeMillis( ) + " Server.getInstance " + idX ); if ( customs.containsKey( idX ) ) { boolean state = states.get( idX ); if ( state ) return customs.get( idX ); } else { String [ ] parts = idX.split( "/" ); if ( jars.containsKey( parts[ 0 ] ) ) loadApplication( parts[ 0 ] , idX ); return customs.get( idX ); } return null; } /** * Adds process to process group * @param processX process * @param groupX process group identifier **/ public static void registerProcess ( OProcess processX , String groupX ) { // System.out.println( System.currentTimeMillis() + " Server.registerProcess " + processX + " " + groupX ); if ( !pools.containsKey( groupX ) ) pools.put( groupX , new ProcessGroup( groupX ) ); pools.get( groupX ).addProcess( processX ); } /** * Removes process from process group * @param processX process * @param groupX process group identifier **/ public static void unregisterProcess ( OProcess processX , String groupX ) { // System.out.println( System.currentTimeMillis() + " Server.unregisterProcess " + processX + " " + groupX ); if ( !pools.containsKey( groupX ) ) return; pools.get( groupX ).removeProcess( processX ); } /** * Pairs a client with an application * @param clientX ClientController instance * @param applicationIDX client's application **/ public static void registerClient ( IApplication customX , ClientController clientX ) { //System.out.println( System.currentTimeMillis( ) + " Server.registerClient " + clientX.id + " " + customX ); synchronized ( clients ) { // searching for application instance for ( String id : customs.keySet( ) ) { // if got it, store client if ( customs.get( id ) == customX ) { ArrayList < Client > list = clients.get( id ); // if custom application is not unloaded if ( list != null ) list.add( clientX.client ); return; } } } } /** * Unpairs client with application * @param clientX ClientController instance * @param applicationIDX client's application **/ public static void unregisterClient ( IApplication customX , ClientController clientX ) { // System.out.println( System.currentTimeMillis( ) + " Server.unregisterClient " + clientX.id + " " + customX ); synchronized ( clients ) { // searching for application instance for ( String id : customs.keySet( ) ) { // if got it, delete it if ( customs.get( id ) == customX ) { ArrayList < Client > list = clients.get( id ); // if custom application is not unloaded if ( list != null ) list.remove( clientX.client ); return; } } } } /** * Registers a stream router * @param nameX stream name * @param routerX router instance **/ public static void registerRouter ( OStream routerX ) { // System.out.println( System.currentTimeMillis() + " Server.registerRouter " + routerX ); synchronized ( routers ) { routers.add( routerX ); } } /** * Unregisters a stream router * @param nameX stream name * @param routerX router instance **/ public static void unregisterRouter ( OStream routerX ) { // System.out.println( System.currentTimeMillis( ) + " Server.unregisterRouter " + nameX + " " + routerX ); synchronized ( routers ) { routers.remove( routerX ); } } /** * Registers a stream player * @param playerX player instance **/ public static void registerPlayer ( OStream playerX ) { // System.out.println( System.currentTimeMillis( ) + " Server.registerPlayer " + playerX ); synchronized ( players ) { players.add( playerX ); } } /** * Unregisters a stream player * @param playerX player istance **/ public static void unregisterPlayer ( OStream playerX ) { // System.out.println( System.currentTimeMillis() + " Server.unregisterPlayer " + playerX ); synchronized ( players ) { players.remove( playerX ); } } /** * Subscribes player to available router * @param playerX player instance * @param nameX stream name **/ public static void connectPlayer ( OStream playerX , String nameX ) { // System.out.println( System.currentTimeMillis() + " Server.connectPlayer " + playerX + " " + nameX ); synchronized ( routers ) { // search for router for ( OStream router : routers ) if ( router.getName( ).equals( nameX ) ) router.subscribe( playerX ); } } /** * UNSubscribes player from available router * @param playerX player instance * @param nameX stream name **/ public static void disconnectPlayer ( OStream playerX , String nameX ) { // System.out.println( System.currentTimeMillis() + " Server.connectPlayer " + playerX + " " + nameX ); synchronized ( routers ) { // search for router for ( OStream router : routers ) if ( router.getName( ).equals( nameX ) ) router.unsubscribe( playerX ); } } /** * Subscribe players to router * @param playerX player instance * @param nameX stream name */ public static void connectRouter ( OStream routerX , String nameX ) { // System.out.println( System.currentTimeMillis() + " Server.connectRouter " + routerX + " " + nameX ); synchronized ( players ) { // search for players for ( OStream player : players ) if ( player.getName( ).equals( nameX ) ) routerX.subscribe( player ); } } /** * Subscribe players to router * @param playerX player instance * @param nameX stream name */ public static void disconnectRouter ( OStream routerX , String nameX ) { // System.out.println( System.currentTimeMillis() + " Server.connectRouter " + routerX + " " + nameX ); synchronized ( players ) { // search for players for ( OStream player : players ) if ( player.getName( ).equals( nameX ) ) routerX.unsubscribe( player ); } } /** * Returns stream router names * @return copy of actual stream router names **/ public static ArrayList < String > getStreamNames ( ) { // System.out.println( System.currentTimeMillis() + " Server.getStreamRouters" ); ArrayList < String > names = new ArrayList < String > ( ); for ( OStream router : routers ) names.add( router.getName( ) ); return names; } /** * Creates a new Server instance * @param args command line arguments **/ public static void main ( String [ ] argumentsX ) { // print console greetings System.out.println( Library.SALUTE ); int index; List < String > arguments = Arrays.asList( argumentsX ); if ( argumentsX.length > 0 ) { index = arguments.indexOf( "port" ); if ( index != -1 ) Library.PORT = new Integer( arguments.get( index + 1 ) ); index = arguments.indexOf( "iostep" ); if ( index != -1 ) Library.STEPTIME = new Integer( arguments.get( index + 1 ) ); index = arguments.indexOf( "iobuffer" ); if ( index != -1 ) Library.IOBUFFER = new Integer( arguments.get( index + 1 ) ); index = arguments.indexOf( "iothreads" ); if ( index != -1 ) Library.IOTHREAD = new Integer( arguments.get( index + 1 ) ); index = arguments.indexOf( "streams" ); if ( index != -1 ) Library.STREAMDIR = new String( arguments.get( index + 1 ) ); index = arguments.indexOf( "applications" ); if ( index != -1 ) Library.CUSTOMDIR = new String( arguments.get( index + 1 ) ); if ( arguments.get( 0 ).equals( "start" ) ) new Server( ); else closeRequest( ); } else System.out.println( Library.PARAMS ); } /** * Shuts down server instance **/ public static void shutdown ( ) { // System.out.println( System.currentTimeMillis( ) + " Server.shutdown" ); // stop listening to new connections unregisterProcess( socketConnector , "sockets" ); // clone all applications container to avoid concurrencies ArrayList < String > ids = new ArrayList < String > ( customs.keySet( ) ); // close all applications for ( String id : ids ) unloadApplication( id ); // close process groups for ( ProcessGroup pool : pools.values( ) ) pool.close( ); } /** * Attempts to close a Milenia instance attached to a specific port * Creates a temporary file, then sends a zero byte to the selected port. The decoder * recognizes the attempt, since an rtmp handshake should start with 0x03 **/ public static void closeRequest ( ) { // System.out.println( System.currentTimeMillis( ) + " Server.closeRequest" ); try { // create temporary file and socket File trigger = new File( Library.CLOSEFILE ); Socket socket = new Socket( "localhost" , Library.PORT ); OutputStream stream = socket.getOutputStream( ); // trigger trigger.createNewFile( ); stream.write( 0 ); // cleanup stream.close( ); socket.close( ); } catch ( IOException exception ) { System.out.println( Library.NOPORT ); } } /** * Checks if shutdown request is valid by checking the existence of the temporary file **/ public static void shutdownCheck ( ) { // System.out.println( System.currentTimeMillis( ) + " Server.shutCheck" ); File trigger = new File( Library.CLOSEFILE ); if ( trigger.exists( ) ) { trigger.delete( ); shutdown( ); } } }