package org.tanukisoftware.wrapper;

/*
 * Copyright (c) 1999, 2022 Tanuki Software, Ltd.
 * http://www.tanukisoftware.com
 * All rights reserved.
 *
 * This software is the proprietary information of Tanuki Software.
 * You shall use it only in accordance with the terms of the
 * license agreement you entered into with Tanuki Software.
 * http://wrapper.tanukisoftware.com/doc/english/licenseOverview.html
 */

import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.ServerSocket;
import java.util.Hashtable;

import org.tanukisoftware.wrapper.WrapperManager;

/**
 * If an application instantiates an instance of this class, the JVM will
 *  listen on the specified port for connections.  When a connection is
 *  detected, the first byte of input will be read from the socket and
 *  then the connection will be immediately closed.  An action will then
 *  be performed based on the byte read from the stream.
 * <p>
 * The easiest way to invoke an action manually is to telnet to the specified
 *  port and then type the single command key.
 *  <code>telnet localhost 9999</code>, for example.
 * <p>
 * Valid commands include:
 * <ul>
 *   <li><b>S</b> : Shutdown cleanly.</li>
 *   <li><b>H</b> : Immediate forced shutdown.</li>
 *   <li><b>R</b> : Restart</li>
 *   <li><b>D</b> : Perform a Thread Dump</li>
 *   <li><b>U</b> : Unexpected shutdown. (Simulate a crash for testing)</li>
 *   <li><b>V</b> : Cause an access violation. (For testing)</li>
 *   <li><b>G</b> : Make the JVM appear to be hung. (For testing)</li>
 * </ul>
 * <p>
 * Additional user defined actions can be defined by calling the
 *  {@link #registerAction( byte command, Runnable action )} method.
 *  The Wrapper project reserves the right to define any upper case
 *  commands in the future.  To avoid future conflicts, please use lower
 *  case for user defined commands.
 * <p>
 * This application will work even in most deadlock situations because the
 *  thread is in issolation from the rest of the application.  If the JVM
 *  is truely hung, this class will fail to accept connections but the
 *  Wrapper itself will detect the hang and restart the JVM externally.
 * <p>
 * The following code can be used in your application to start up the
 *  WrapperActionServer with all default actions enabled:
 * <pre>
 *  int port = 9999;
 *  WrapperActionServer server = new WrapperActionServer( port );
 *  server.enableShutdownAction( true );
 *  server.enableHaltExpectedAction( true );
 *  server.enableRestartAction( true );
 *  server.enableThreadDumpAction( true );
 *  server.enableHaltUnexpectedAction( true );
 *  server.enableAccessViolationAction( true );
 *  server.enableAppearHungAction( true );
 *  server.start();
 * </pre>
 * Then remember to stop the server when your application shuts down:
 * <pre>
 *  server.stop();
 * </pre>
 *
 * @author Tanuki Software Development Team &lt;support@tanukisoftware.com&gt;
 */
public class WrapperActionServer
    implements Runnable
{
    /** Command to invoke a shutdown action. */
    public final static byte COMMAND_SHUTDOWN         = (byte)'S';
    /** Command to invoke an expected halt action. */
    public final static byte COMMAND_HALT_EXPECTED    = (byte)'H';
    /** Command to invoke a restart action. */
    public final static byte COMMAND_RESTART          = (byte)'R';
    /** Command to invoke a thread dump action. */
    public final static byte COMMAND_DUMP             = (byte)'D';
    /** Command to invoke an unexpected halt action. */
    public final static byte COMMAND_HALT_UNEXPECTED  = (byte)'U';
    /** Command to invoke an access violation. */
    public final static byte COMMAND_ACCESS_VIOLATION = (byte)'V';
    /** Command to invoke an appear hung action. */
    public final static byte COMMAND_APPEAR_HUNG      = (byte)'G';

    /** The address to bind the port server to.  Null for any address. */
    private InetAddress m_bindAddr;
    
    /** The port to listen on for connections. */
    private int m_port;
    
    /** Reference to the worker thread. */
    private Thread m_runner;
    
    /** Flag set when the m_runner thread has been asked to stop. */
    private boolean m_runnerStop = false;
    
    /** Reference to the ServerSocket. */
    private ServerSocket m_serverSocket;
    
    /** Table of all the registered actions. */
    private Hashtable m_actions = new Hashtable();
    
    /** Log channel */
    private static WrapperPrintStream m_out;
    
    /*---------------------------------------------------------------
     * Constructors
     *-------------------------------------------------------------*/
    /**
     * Creates and starts WrapperActionServer instance bound to the
     *  specified port and address.
     *
     * @param port Port on which to listen for connections.
     * @param bindAddress Address to bind to.
     */
    public WrapperActionServer( int port, InetAddress bindAddress )
    {
        m_port = port;
        m_bindAddr = bindAddress;
        
        boolean streamSet = false;
        if ( "true".equals( System.getProperty( "wrapper.use_sun_encoding" ) ) ) {
            String sunStdoutEncoding = System.getProperty( "sun.stdout.encoding" );
            if ( ( sunStdoutEncoding != null ) && !sunStdoutEncoding.equals( System.getProperty( "file.encoding" ) ) ) {
                /* We need to create the stream using the same encoding as the one used for stdout, else this will lead to encoding issues. */
                try
                {
                    m_out = new WrapperPrintStream( System.out, false, sunStdoutEncoding, "WrapperActionServer: " );
                    streamSet = true;
                }
                catch ( UnsupportedEncodingException e )
                {
                    /* This should not happen because we always make sure the encoding exists before launching a JVM. */
                    System.out.println( WrapperManager.getRes().getString( "Failed to set the encoding ''{0}'' when creating a WrapperPrintStream.\n Make sure the value of sun.stdout.encoding is correct.", sunStdoutEncoding ) );
                }
            }
        }
        if ( !streamSet )
        {
            m_out = new WrapperPrintStream( System.out, "WrapperActionServer: " );
        }
    }
    
    /**
     * Creates and starts WrapperActionServer instance bound to the
     *  specified port.  The socket will bind to all addresses and
     *  should be concidered a security risk.
     *
     * @param port Port on which to listen for connections.
     */
    public WrapperActionServer( int port )
    {
        this( port, null );
    }
    
    /*---------------------------------------------------------------
     * Runnable Methods
     *-------------------------------------------------------------*/
    /**
     * Thread which will listen for connections on the socket.
     */
    public void run()
    {
        if ( Thread.currentThread() != m_runner )
        {
            throw new IllegalStateException(WrapperManager.getRes().getString( "Private method." ) );
        }
        
        try
        {
            while ( !m_runnerStop )
            {
                try
                {
                    int command;
                    Socket socket = m_serverSocket.accept();
                    try
                    {
                        // Set a short timeout of 15 seconds,
                        //  so connections will be promptly closed if left idle.
                        socket.setSoTimeout( 15000 );
                        
                        // Read a single byte.
                        command = socket.getInputStream().read();
                    }
                    finally
                    {
                        socket.close();
                    }
                    
                    if ( command >= 0 )
                    {
                        Runnable action;
                        synchronized( m_actions )
                        {
                            action = (Runnable)m_actions.get( new Integer( command ) );
                        }
                        
                        if ( action != null )
                        {
                            try
                            {
                                action.run();
                            }
                            catch ( Throwable t )
                            {
                                m_out.println( WrapperManager.getRes().getString( "Error processing action." ) );
                                t.printStackTrace( m_out );
                            }
                        }
                    }
                }
                catch ( Throwable t )
                {
                    // Check for throwable type this way rather than with seperate catches
                    //  to work around a problem where InterruptedException can be thrown
                    //  when the compiler gives an error saying that it can't.
                    if ( m_runnerStop
                        && ( ( t instanceof InterruptedException )
                            || ( t instanceof SocketException )
                            || ( t instanceof InterruptedIOException )
                            || ( ( t instanceof IOException ) && ( t.getMessage() != null ) && ( t.getMessage().indexOf( "Bad file descriptor" ) >= 0 ) )  // Happens on shutdown on IA64 Linux.
                            )
                        )
                    {
                        // This is expected, the service is being stopped.
                    }
                    else
                    {
                        m_out.println( WrapperManager.getRes().getString( "Unexpected error." ) );
                        t.printStackTrace( m_out );
                        
                        // Avoid tight thrashing
                        try
                        {
                            Thread.sleep( 5000 );
                        }
                        catch ( InterruptedException e )
                        {
                            // Ignore
                        }
                    }
                }
            }
        }
        finally
        {
            synchronized( this )
            {
                m_runner = null;
                
                // Wake up the stop method if it is waiting for the runner to stop.
                this.notify();
            }
        }
    }
    
    /*---------------------------------------------------------------
     * Methods
     *-------------------------------------------------------------*/
    /**
     * Starts the runner thread.
     *
     * @throws IOException If the server socket is unable to bind to the
     *                     specified port or there are any other problems
     *                     opening a socket.
     */
    public void start()
        throws IOException
    {
        // Create the server socket.
        m_serverSocket = new ServerSocket( m_port, 5, m_bindAddr );
        
        m_runner = new Thread( this, "WrapperActionServer_runner" );
        m_runner.setDaemon( true );
        m_runner.start();
    }
    
    /**
     * Stops the runner thread, blocking until it has stopped.
     * @throws Exception If the thread is unable to interrupt.
     */
    public void stop()
        throws Exception
    {
        Thread runner = m_runner;
        m_runnerStop = true;
        runner.interrupt();

        // Close the server socket so it stops blocking for new connections.
        ServerSocket serverSocket = m_serverSocket;
        if ( serverSocket != null )
        {
            try
            {
                serverSocket.close();
            }
            catch ( IOException e )
            {
                // Ignore.
            }
        }
        
        synchronized( this )
        {
            while( m_runner != null )
            {
                try
                {
                    // Wait to be notified that the thread has exited.
                    this.wait();
                }
                catch ( InterruptedException e )
                {
                    // Ignore
                }
            }
        }
    }
    
    /**
     * Registers an action with the action server.  The server will not accept
     *  any new connections until an action has returned, so keep that in mind
     *  when writing them.  Also be aware than any uncaught exceptions will be
     *  dumped to the console if uncaught by the action.  To avoid this, wrap
     *  the code in a <code>try { ... } catch (Throwable t) { ... }</code>
     *  block.
     *
     * @param command Command to be registered.  Will override any exiting
     *                action already registered with the same command.
     * @param action Action to be registered.
     */
    public void registerAction( byte command, Runnable action )
    {
        synchronized( m_actions )
        {
            m_actions.put( new Integer( command ), action );
        }
    }
    
    /**
     * Unregisters an action with the given command.  If no action exists with
     *  the specified command, the method will quietly ignore the call.
     * 
     * @param command Command to be unregistered.
     */
    public void unregisterAction( byte command )
    {
        synchronized( m_actions )
        {
            m_actions.remove( new Integer( command ) );
        }
    }
    
    /**
     * Enable or disable the shutdown command.  Disabled by default.
     *
     * @param enable True to enable to action, false to disable it.
     */
    public void enableShutdownAction( boolean enable )
    {
        if ( enable )
        {
            registerAction( COMMAND_SHUTDOWN, new Runnable()
                {
                    public void run()
                    {
                        WrapperManager.stopAndReturn( 0 );
                    }
                } );
        }
        else
        {
            unregisterAction( COMMAND_SHUTDOWN );
        }
    }
    
    /**
     * Enable or disable the expected halt command.  Disabled by default.
     *  This will shutdown the JVM, but will do so immediately without going
     *  through the clean shutdown process.
     *
     * @param enable True to enable to action, false to disable it.
     */
    public void enableHaltExpectedAction( boolean enable )
    {
        if ( enable )
        {
            registerAction( COMMAND_HALT_EXPECTED, new Runnable()
                {
                    public void run()
                    {
                        WrapperManager.stopImmediate( 0 );
                    }
                } );
        }
        else
        {
            unregisterAction( COMMAND_HALT_EXPECTED );
        }
    }
    
    /**
     * Enable or disable the restart command.  Disabled by default.
     *
     * @param enable True to enable to action, false to disable it.
     */
    public void enableRestartAction( boolean enable )
    {
        if ( enable )
        {
            registerAction( COMMAND_RESTART, new Runnable()
                {
                    public void run()
                    {
                        WrapperManager.restartAndReturn();
                    }
                } );
        }
        else
        {
            unregisterAction( COMMAND_RESTART );
        }
    }
    
    /**
     * Enable or disable the thread dump command.  Disabled by default.
     *
     * @param enable True to enable to action, false to disable it.
     */
    public void enableThreadDumpAction( boolean enable )
    {
        if ( enable )
        {
            registerAction( COMMAND_DUMP, new Runnable()
                {
                    public void run()
                    {
                        WrapperManager.requestThreadDump();
                    }
                } );
        }
        else
        {
            unregisterAction( COMMAND_DUMP );
        }
    }
    
    /**
     * Enable or disable the unexpected halt command.  Disabled by default.
     *  If this command is executed, the Wrapper will think the JVM crashed
     *  and restart it.
     *
     * @param enable True to enable to action, false to disable it.
     */
    public void enableHaltUnexpectedAction( boolean enable )
    {
        if ( enable )
        {
            registerAction( COMMAND_HALT_UNEXPECTED, new Runnable()
                {
                    public void run()
                    {
                        Runtime.getRuntime().halt( 0 );
                    }
                } );
        }
        else
        {
            unregisterAction( COMMAND_HALT_UNEXPECTED );
        }
    }
    
    /**
     * Enable or disable the access violation command.  Disabled by default.
     *  This command is useful for testing how an application handles the worst
     *  case situation where the JVM suddenly crashed.  When this happens, the
     *  the JVM will simply die and there will be absolutely no chance for any
     *  shutdown or cleanup work to be done by the JVM.
     *
     * @param enable True to enable to action, false to disable it.
     */
    public void enableAccessViolationAction( boolean enable )
    {
        if ( enable )
        {
            registerAction( COMMAND_ACCESS_VIOLATION, new Runnable()
                {
                    public void run()
                    {
                        WrapperManager.accessViolationNative();
                    }
                } );
        }
        else
        {
            unregisterAction( COMMAND_ACCESS_VIOLATION );
        }
    }
    
    /**
     * Enable or disable the appear hung command.  Disabled by default.
     *  This command is useful for testing how an application handles the
     *  situation where the JVM stops responding to the Wrapper's ping
     *  requests.   This can happen if the JVM hangs or some piece of code
     *  deadlocks.  When this happens, the Wrapper will give up after the
     *  ping timeout has expired and kill the JVM process.  The JVM will
     *  not have a chance to clean up and shudown gracefully.
     *
     * @param enable True to enable to action, false to disable it.
     */
    public void enableAppearHungAction( boolean enable )
    {
        if ( enable )
        {
            registerAction( COMMAND_APPEAR_HUNG, new Runnable()
                {
                    public void run()
                    {
                        WrapperManager.appearHung();
                    }
                } );
        }
        else
        {
            unregisterAction( COMMAND_APPEAR_HUNG );
        }
    }
}

