/*
* This file or a portion of this file is licensed under the terms of
* the Globus Toolkit Public License, found in file GTPL, or at
* http://www.globus.org/toolkit/download/license.html. This notice must
* appear in redistributions of this file, with or without modification.
*
* Redistributions of this Software, with or without modification, must
* reproduce the GTPL in: (1) the Software, or (2) the Documentation or
* some other similar material which is provided with the Software (if
* any).
*
* Copyright 1999-2004 University of Chicago and The University of
* Southern California. All rights reserved.
*/
package edu.isi.pegasus.planner.invocation;
import java.net.*;
import java.io.*;
import java.sql.SQLException;
import java.util.Iterator;
import edu.isi.pegasus.planner.parser.InvocationParser;
import org.griphyn.vdl.toolkit.*;
import org.griphyn.vdl.dbschema.*;
import org.griphyn.vdl.util.Logging;
import org.griphyn.vdl.util.ChimeraProperties;
import org.griphyn.vdl.directive.*;
import org.apache.log4j.Logger;
import org.apache.log4j.Level;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.PatternLayout;
public class SimpleServer extends Toolkit
{
private static final int port = 65533;
public static boolean c_terminate = false;
public static Logger c_logger = null;
private boolean m_emptyFail = true;
private boolean m_noDBase;
DatabaseSchema m_dbschema;
InvocationParser m_parser;
ServerSocket m_server;
public static void setTerminate( boolean b )
{
c_terminate = b;
}
public static boolean getTerminate()
{
return c_terminate;
}
public void showUsage()
{
// empty for now
}
public SimpleServer( int port )
throws Exception
{
super("SimpleServer");
// stand up the connection to the PTC
this.m_noDBase = false;
ChimeraProperties props = ChimeraProperties.instance();
String ptcSchemaName = props.getPTCSchemaName();
if ( ptcSchemaName == null ) m_noDBase = true;
if ( ! m_noDBase ) {
// ignore -d option for now - grumbl, why?!
Connect connect = new Connect();
this.m_dbschema = connect.connectDatabase(ptcSchemaName);
// check for invocation record support
if ( ! (m_dbschema instanceof PTC) ) {
c_logger.warn( "Your database cannot store invocation records" +
", assuming no-database-mode" );
m_noDBase = true;
}
}
// create one XML parser -- once
m_parser = new InvocationParser( props.getPTCSchemaLocation() );
// setup socket
this.m_server = null;
try {
byte[] loopback = { 127, 0, 0, 1 };
this.m_server =
new ServerSocket( port, 5, InetAddress.getByAddress(loopback) );
// new ServerSocket( port, 5, InetAddress..getLocalHost() );
// new ServerSocket( port, 5 );
} catch ( UnknownHostException e ) {
c_logger.fatal( "Unable to determine own hostname: " +
e.getMessage() );
System.exit(1);
} catch ( IOException e ) {
c_logger.fatal( "Could not listen on port " + port + ": " +
e.getMessage() );
System.exit(1);
}
}
/**
* Copy the content of the file into memory. The copy operation also
* weeds out anything that may have been added by the remote batch
* scheduler. For instance, PBS is prone to add headers and footers.
*
* @param input is the file instance from which to read contents.
* @return the result code from reading the file
*/
private String extractToMemory( java.io.File input )
throws FriendlyNudge
{
StringWriter out = null;
// open the files
int p1, p2, state = 0;
try {
BufferedReader in = new BufferedReader( new FileReader(input) );
out = new StringWriter(4096);
String line = null;
while ( (line = in.readLine()) != null ) {
if ( (state & 1) == 0 ) {
// try to copy the XML line in any case
if ( (p1 = line.indexOf( "<?xml" )) > -1 )
if ( (p2 = line.indexOf( "?>", p1 )) > -1 )
out.write( line, p1, p2+2 );
// start state with the correct root element
if ( (p1 = line.indexOf( "<invocation")) > -1 ) {
if ( p1 > 0 ) line = line.substring( p1 );
++state;
}
}
if ( (state & 1) == 1 ) {
out.write( line );
if ( (p1 = line.indexOf("</invocation>")) > -1 ) ++state;
}
}
in.close();
out.flush();
out.close();
} catch ( IOException ioe ) {
throw new FriendlyNudge( "While copying " + input.getPath() +
" into temp. file: " + ioe.getMessage(), 5 );
}
// some sanity checks
if ( state == 0 )
throw new FriendlyNudge( "File " + input.getPath() +
" does not contain invocation records, assuming failure", 5 );
if ( (state & 1) == 1 )
throw new FriendlyNudge( "File " + input.getPath() +
" contains an incomplete invocation record, assuming failure", 5 );
// done
return out.toString();
}
/**
* Determines the exit code of an invocation record. Currently,
* we will determine the exit code from the main job only.
*
* @param ivr is the invocation record to put into the database
* @return the status code as exit code to signal failure etc.
* <pre>
* 0 regular exit with exit code 0
* 1 regular exit with exit code > 0
* 2 failure to run program from kickstart
* 3 application had died on signal
* 4 application was suspended (should not happen)
* 5 failure in exit code parsing
* 6 impossible case
* </pre>
*/
private int determineExitStatus( InvocationRecord ivr )
{
boolean seen = false;
for ( Iterator i=ivr.iterateJob(); i.hasNext(); ) {
Job job = (Job) i.next();
// clean-up jobs don't count in failure modes
if ( job.getTag().equals("cleanup") ) continue;
// obtains status from job
Status status = job.getStatus();
if ( status == null ) return 6;
JobStatus js = status.getJobStatus();
if ( js == null ) {
// should not happen
return 6;
} else if ( js instanceof JobStatusRegular ) {
// regular exit code - success or failure?
int exitcode = ((JobStatusRegular) js).getExitCode();
if ( exitcode != 0 ) return 1;
else seen = true;
// continue, if exitcode of 0 to implement chaining !!!!
} else if ( js instanceof JobStatusFailure ) {
// kickstart failure
return 2;
} else if ( js instanceof JobStatusSignal ) {
// died on signal
return 3;
} else if ( js instanceof JobStatusSuspend ) {
// suspended???
return 4;
} else {
// impossible/unknown case
return 6;
}
}
// success, or no [matching] jobs
return seen ? 0 : 5;
}
/**
* Reads the contents of the specified file, and returns with the
* remote exit code contained in the job chain.
*
* @param filename is the name of the file with the kickstart record.
* @return the exit code derived from the remote exit code.
*/
public int checkFile( String filename )
{
int result = 0;
try {
// check input file
java.io.File check = new java.io.File(filename);
// test 1: file exists
if ( ! check.exists() )
throw new FriendlyNudge( "file does not exist " + filename +
", assuming failure", 5 );
// test 2: file is readable
if ( ! check.canRead() )
throw new FriendlyNudge( "unable to read file " + filename +
", assuming failure", 5 );
// test 3: file has nonzero size
if ( check.length() == 0 ) {
if ( m_emptyFail ) {
throw new FriendlyNudge( "file " + filename + " has zero length" +
", assuming failure", 5 );
} else {
throw new FriendlyNudge( "file " + filename + " has zero length" +
", assuming success", 0 );
}
}
// test 4: extract XML into tmp file
String temp = extractToMemory(check);
// test 5: try to parse XML -- but there is only one parser
InvocationRecord invocation = null;
synchronized ( m_parser ) {
c_logger.info( "starting to parse invocation" );
invocation = m_parser.parse( new StringReader(temp) );
};
if ( invocation == null )
throw new FriendlyNudge( "invalid XML invocation record in " +
filename + ", assuming failure", 5 );
else
c_logger.info( "invocation was parsed successfully" );
// insert into database. This trickery works, because we already
// checked previously that the dbschema does support invocations.
// However, there is only one database connection at a time.
if ( ! m_noDBase ) {
PTC ptc = (PTC) m_dbschema;
synchronized ( ptc ) {
// FIXME: (start,host,pid) may not be a sufficient secondary key
if ( ptc.getInvocationID( invocation.getStart(),
invocation.getHostAddress(),
invocation.getPID() ) == -1 ) {
c_logger.info( "adding invocation to database" );
// may throw SQLException
ptc.saveInvocation( invocation );
} else {
c_logger.info( "invocation already exists, skipping!" );
}
}
}
// determine result code, just look at the main job for now
c_logger.info( "determining exit status of main job" );
result = determineExitStatus( invocation );
c_logger.info( "exit status = " + result );
} catch ( FriendlyNudge fn ) {
c_logger.warn( fn.getMessage() );
result = fn.getResult();
} catch ( Exception e ) {
c_logger.warn( e.getMessage() );
result = 5;
}
// done
return result;
}
public static void main( String args[] )
throws IOException
{
// setup logging
System.setProperty( "log4j.defaultInitOverride", "true" );
Logger root = Logger.getRootLogger();
root.addAppender(
new ConsoleAppender(
new PatternLayout("%d{yyyy-MM-dd HH:mm:ss.SSS} %-5p [%c{1}] %m%n") ) );
root.setLevel( Level.INFO );
c_logger = Logger.getLogger( SimpleServer.class );
c_logger.info( "starting" );
SimpleServer me = null;
try {
me = new SimpleServer(port);
} catch ( Exception e ) {
c_logger.fatal( "Unable to instantiate a server: " + e.getMessage() );
System.exit(1);
}
// run forever
try {
while ( ! c_terminate ) {
new SimpleServerThread( me, me.m_server.accept() ).start();
}
} catch ( SocketException se ) {
// ignore -- closing the server socket in a thread during shutdown
// will have accept fail with a socket exception in main()
}
// done
c_logger.info( "received shutdown" );
// count your threads, and the last one locks the
// door and switches off the light... Grrr.
synchronized ( me ) {
while ( SimpleServerThread.c_count > SimpleServerThread.c_cdone ) {
try {
me.wait(5000);
} catch ( InterruptedException e ) {
// ignore
}
}
}
try {
me.m_dbschema.close();
} catch ( Exception e ) {
c_logger.warn( "During database disconnect: " + e.getMessage() );
}
c_logger.warn( "finished shutdown" );
}
}