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 )
}
}
}