| 
<?php/***
 * Copyright 2016 Igor Dyshlenko
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
 
 /**
 * Shell performs operations with the connected server (ssh2) - connect, execute
 * commands, obtain execution results, etc.
 * The possibility of working on other protocols (for example, telnet) is
 * provided for the implementation of the corresponding classes with the
 * ShellConnector interface.
 *
 * The concept is taken from the class Net_Telnet,
 * Copyright 2012 Jesse Norell <[email protected]>
 * Copyright 2012 Kentec Communications, Inc.
 *
 * @author Igor Dyshlenko
 * @category Console
 * @license https://opensource.org/licenses/MIT MIT
 */
 
 class Shell {
 protected
 $connector,        //
 $eol = PHP_EOL,    // End of line code
 $timeout = 5,    // Timeout for io operations (sec.)
 $useSleep = 0,    // Use usleep (timeout in ms)
 $prompt,        // Terminal command prompt
 $logger;        // Logger - object Log class
 
 protected
 $readBuffer,
 $writeBuffer;
 
 /**
 * Constructor
 * @param ShellConnector $connector
 * @param string $prompt - Command interpreter prompt
 * @param Log $logger - PEAR Log object or null
 * @throws LogicException
 */
 public function __construct(ShellConnector $connector, $prompt='$ ', $timeout=null, $logger=null) {
 $this->logger = new LogWrapper($logger);
 
 if (($tOut = intval($timeout)) > 0) {
 $this->timeout = $tOut;
 $this->logger->debug(__METHOD__ . ': Timeout setted to ' . $tOut . ' sec.');
 }
 unset($tOut);
 
 if (!$connector->isConnected()) {
 $this->logger->err(__METHOD__ . ': Fail: connector state "Disconnected"!');
 throw new LogicException('Fail: connector state "Disconnected"!');
 }
 $this->connector = $connector;
 $this->prompt = $prompt;
 }
 
 public function __destruct(){
 $this->logout();
 if ($this->connector->isLoggedIn()) {
 $this->connector->logout();
 }
 if ($this->connector->isConnected()) {
 $this->connector->disconnect();
 }
 }
 
 /**
 * Login function
 * @param string $userName
 * @param string $pass
 * @return bool true if success.
 * @throws LogicException if authentication error.
 */
 public function login($username, $pass) {
 return $this->connector->login($username, $pass);
 }
 
 /**
 * Logout function
 * @return bool true if success, false if fail.
 */
 public function logout() {
 return $this->connector->logout();
 }
 
 /**
 * Execute the command.
 * @param string $command
 * @return string result.
 */
 public function exec($command) {
 $this->write($command . $this->eol);
 $this->read($this->prompt);
 return $this->getResult();
 }
 
 /**
 * @todo Abort execution of command
 */
 /*    public function stop() {
 
 }
 */
 
 /**
 * Get the result (screen buffer).
 * @return string
 */
 public function getResult() {
 $result = $this->readBuffer;
 $this->readBuffer = '';
 return $result;
 }
 
 /**
 * Get Error Message.
 * @return string error message if error, empty string ('') otherwise
 */
 public function getError() {
 return $this->connector->getError();
 }
 
 /**
 * Get Error number.
 * @return mixed int error code if error, NULL otherwise
 */
 public function getErrno() {
 return $this->connector->getErrno();
 }
 
 /**
 * Get "is connected" state
 * @return bool
 */
 public function isConnested() {
 return $this->connector->isConnected();
 }
 
 /**
 * Get "is logged in" state
 * @return bool
 */
 public function isLoggedIn() {
 return $this->connector->isLoggedIn();
 }
 
 /**
 * Can execute the operation?
 * @return boolean
 */
 public function isOnLine() {
 return $this->isConnested() && $this->isLoggedIn();
 }
 
 /***
 * @todo Is the previous command completed normally?
 */
 /*    public function isOk() {
 return false;
 }
 */
 /**
 * Set / get end-of-line value
 * @param mixed $eol if NULL - do nothing, else - set new end-of-line value.
 * @return string current end-of-line value.
 */
 public function eol($eol=null) {
 if (!is_null($eol)){
 $this->eol = strval($eol);
 $this->logger->debug(__METHOD__ . ': End of line setted to "' .
 str_replace(array("\n", "\r", "\t"), array('\n', '\r', '\t'), $eol) . '"');
 }
 return $this->eol;
 }
 
 /**
 * Set / get the value of the command prompt.
 * @param mixed $prompt if NULL - do nothing, else - set new command line
 *                        prompt value.
 * @return string current prompt command line value.
 */
 public function prompt($prompt=null) {
 if (!is_null($prompt)){
 $this->prompt = strval($prompt);
 $this->logger->debug(__METHOD__ . ': Prompt setted to "' .
 str_replace(array("\n", "\r", "\t"), array('\n', '\r', '\t'), $prompt) . '"');
 }
 return $this->prompt;
 }
 
 /**
 * Skip all data before the command prompt.
 * @return mixed FALSE if error, (int) count readed data bytes otherwise
 */
 public function goAhead() {
 return $this->read($this->prompt);
 }
 
 /**
 * Skip data from output stream
 * @param string $searchFor - skip data to the desired value inclusive.
 * @param int $numChars - skip a maximum of $numChars characters.
 * @return mixed FALSE if error, (int) count readed data bytes otherwise
 */
 public function read($searchFor=null, $numChars=null) {
 $buffer = '';
 $nums = intval($numChars);
 $search = strval($searchFor);
 $found = false;
 $started = time();
 $timedOut = false;
 
 while (    !$found && !$timedOut &&
 (($nums == 0) || ($nums && (strlen($buffer) < $nums))) &&
 (($char= $this->readStream()) !== false)    ) {
 
 $buffer .= $char;
 
 if (($searchFor !== null) && (substr($buffer, 0 - strlen($search)) === $search)) {
 $this->lastmatch = $search;
 $found = true;
 continue;
 }
 
 $timedOut = ((time() - $started) > $this->timeout);
 $found = ($nums && (strlen($buffer) >= $nums));
 }
 
 $this->readBuffer .= $buffer;
 
 return ($found) ? strlen($buffer) : false;
 }
 
 /**
 * Get symbol from connector input stream
 * @return mixed string or FALSE if eof().
 */
 protected function readStream() {
 return $this->connector->read();
 }
 
 /**
 * Write data to stream
 * @param string $data - data for write to input stream
 * @return int number of written chars
 */
 public function write($data) {
 $this->writeBuffer .= $data;
 return $this->writeStream();
 }
 
 /**
 * Put data to connector output stream
 * @param type $data
 * @return int
 * @throws RuntimeException
 */
 protected function writeStream($data=null) {
 $written = 0;
 $n = 0;
 
 if (!$this->isOnLine()){
 return 0;
 }
 
 if (($data !== null) and (strlen($data) > 0)){
 $buf = $data;
 $total = strlen($data);
 } else {
 $buf = $this->writeBuffer;
 $total = strlen($this->writeBuffer);
 $this->writeBuffer = null;
 }
 
 while ($written < $total) {
 $buf = substr($buf, $n);
 if (($n = $this->connector->write($buf)) === false) {
 if (!$this->isConnested()){
 $this->logger->debug(__METHOD__ . ': Disconnected.');
 break;
 } else {
 $this->logger->err(__METHOD__ . ': Error writing to socket.');
 throw new RuntimeException('Error writing to socket.');
 }
 }
 $written += $n;
 }
 
 return $written;
 }
 }
 |