All Downloads are FREE. Search and download functionalities are using the official Maven repository.

expect.Expect.scala Maven / Gradle / Ivy

The newest version!
/*
 *  This file is part of Expect-for-Scala.
 *
 *  Copyright (C) 2015-2018 Franck Cassez.
 *
 *  Expect-for-Scala is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or (at
 *  your option) any later version.
 *
 *  Expect-for-Scala 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.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with Expect-for-Scala. (See files COPYING and COPYING.LESSER.)  If
 *  not, see  .
 */

package org.bitbucket.franck44.expect

/**
 * An interface to programmatically communicate with an interactive program.
 *
 * @param exec  The name of the interactive program to run
 * @param args  The command line arguments to the program
 *
 * @example     exec = "ls" and args = List("-al")
 *
 */
case class Expect( private val exec : String, private val args : List[ String ] = List() ) {

    import com.typesafe.scalalogging.Logger
    import org.slf4j.LoggerFactory
    import java.io.{ BufferedInputStream, BufferedOutputStream }
    import scala.util.{ Try, Success, Failure }

    /**
     *  Logger to log debug information.
     */
    private val logger = Logger( LoggerFactory.getLogger( this.getClass ) )

    /**
     *  Scanner for the input stream of the Expect (output of the program)
     */
    lazy private val inputScanner = new java.util.Scanner( in )

    /**
     *  The interactive process that runs the program to interact with.
     *
     *  @note   This is a lazy val so even if you instantiate an [[Expect]]
     *          object with an 'exec' you may not have a running process with
     *          the name 'exec' (as displayed with unix 'ps | grep exec' command)
     *
     */
    private lazy val process : Process = spawn( exec, args )

    /**
     *  The input and output streams of the Expect object
     */
    lazy private val ( in, out ) = try {

        //  Try to bind the streams to BufferedStreams
        val ( i, o ) =
            (
                new BufferedInputStream( process.getInputStream ),
                new BufferedOutputStream( process.getOutputStream )
            )

        logger.info( "Process buffered streams created" )

        ( i, o )

    } catch {

        //  If input or output streams cannot be created,
        //  we release acquired resources, streams and process
        case e : Throwable ⇒

            process.getInputStream.close()
            process.getOutputStream.close()
            process.destroy()

            logger.error( "Process buffered streams could not be created" )

            throw ( e )
    }

    /**
     *  Release resources of the Expect object
     *
     *  Try to close the streams and kill the process. If  this fails
     *  because the process is alredy killed, capture the throwm exception.
     *  There is no easy way to check whether a stream is closed apart
     *  trying to write on it (or using ready()) which may result in
     *  an exception. See link below for more detailed explanation.
     *  {@link http://stackoverflow.com/questions/981196/how-to-know-if-a-bufferedreader-stream-is-closed}
     *  @todo Log the failures of the commands
     *
     */
    def destroy() : Unit = {

        //  As the process may be killed every one of them may fail
        //  and we cannot do anything about nor diagnose the root
        //  cause so we ignore the results of the command
        Try { in.close() }
        Try { out.close() }
        Try { process.destroy() }

        logger.info( "process destroyed -- streams closed" )

    }

    /**
     *  Spawns an (interactive) process
     *
     *  @param exec    The name of the program to run (e.g. 'ls')
     *  @param args    The arguments (e.g. '-al')
     *
     */
    private def spawn( exec : String, args : List[ String ] ) : Process = {

        val pb = new ProcessBuilder(
            ( exec :: args ) : _*
        ).redirectErrorStream( true )

        logger.info( "starting process {}", ( exec :: args ).mkString( " " ) )

        pb.start()

    }

    /**
     *  Send a string to interactive process' input stream.
     *
     *  @param s The string to send
     *
     */
    def send( s : String ) : Try[ Unit ] = Try {

        logger.info( "Entering send" )
        logger.debug( "Sending {} to output stream", s )

        out.write( s.getBytes() )
        out.flush()

        logger.debug( "Sent" )
        logger.info( "Exiting send" )

    }

    import scala.concurrent.duration._
    import scala.util.matching.Regex

    import java.util.concurrent.{ ExecutorService, Executors, RejectedExecutionException }

    /**
     * ExecutorService provides a more complete asynchronous task
     * execution framework (compared to Executor). An ExecutorService
     * manages queuing and scheduling of tasks, and more importantly
     * allows **controlled shutdown**.
     *
     * public static ExecutorService newSingleThreadExecutor()
     * Creates an Executor that uses a single worker thread operating off an
     * unbounded queue. (Note however that if this single thread terminates
     * due to a failure during execution prior to shutdown, a new one will
     * take its place if needed to execute subsequent tasks.) Tasks are guaranteed
     * to execute sequentially, and no more than one task will be active at any
     * given time. Unlike the otherwise equivalent newFixedThreadPool(1) the
     * returned executor is guaranteed not to be reconfigurable to use
     * additional threads.
     * Returns: the newly created single-threaded Executor
     */
    lazy private val pool : ExecutorService = Executors.newSingleThreadExecutor()

    /**
     *  Expect a response on the output stream of the interactive process
     *
     *  @param prompt    A regex that defines the prompt
     *  @param timeout   A maximum duration to see a sequence of chars
     *                   on the input stream that ends with the prompt
     *
     *  @note            If the process' input stream is closed (for some reasons)
     *                   this method will return a Failure timeoutException.
     */
    def expect( prompt : Regex, timeout : Duration ) : Try[ String ] = {

        import java.util.concurrent.TimeUnit

        logger.info( "Entering expect" )

        try {

            //  Command to set the delimiter to the prompt and get the next token.
            //  Submits a runnable Future task that returns a String to the pool.
            val getResponse = pool.submit[ String ] { () ⇒
                val k = inputScanner.useDelimiter( prompt.pattern ).next()
                logger.info( s"Scanner returns $k" )
                k
            }
            logger.info( "Waiting with timeout" )

            //  We run getResponse with a timeout with Future.get (getResponse is a Future).
            //  Either Future.get will return a String or an Exception will be
            //  thrown (and the three possible cases are caught).
            val result = getResponse.get( timeout.toMillis, TimeUnit.MILLISECONDS )

            //  If we are here, Future.get succeeded and we have the string
            //  that precedes the prompt in result.
            logger.info( "Expect succeeded:" + result )

            //  The scanner position is on the first character of
            //  the prompt, so we advance it to the end of the
            //  prompt to get ready for the next expect request
            val k = inputScanner.skip( prompt.pattern )

            logger.info( s"Result of skip was: $k" )

            Success( result )

        } catch {

            //  After any exception we shutdown the thread.

            //  Exceptions due to pool.submit
            case ex : RejectedExecutionException ⇒
                pool.shutdownNow()

                logger.error( s"getResponse task could not be scheduled for execution -- $ex" )
                Failure( ex )

            case ex : NullPointerException ⇒
                pool.shutdownNow()

                logger.error( s"getResponse task is null -- $ex" )
                Failure( ex )

            //  Convert java timeoutException into scala counterpart.
            case to : java.util.concurrent.TimeoutException ⇒
                pool.shutdownNow()

                logger.error( s"getResponse task timed out -- $to" )
                Failure( new scala.concurrent.TimeoutException( s"Futures timed out after [$timeout]" ) )

            //  Other exeptions.
            //  Three types of exceptions can be thrown:
            //  InterruptedException: Waiting timeout thread was interrupted
            //  TimeoutException : No prompt before the timeout
            //  IllegalArgumentException : Timeout bound undefined
            case other : Throwable ⇒
                pool.shutdownNow()

                logger.error( s"Error occurred in Expect -- $other" )
                Failure( other )
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy