<?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 <jesse@kci.net>
* Copyright 2012 Kentec Communications, Inc.
*
* @author Igor Dyshlenko
* @category Console
* @license https://opensource.org/licenses/MIT MIT
*/
namespace din70\Tools\ShellAccess;
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(
ShellConnectorInterface $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 isConnected()
{
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->isConnected() && $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 mixed - (int) count of written chars or FALSE if error
* @throws RuntimeException
*/
public function write($data)
{
if ($this->isOnLine()) {
$this->writeBuffer .= $data;
return $this->writeStream();
}
return false;
}
/**
* Put data to connector output stream
* @param string $data
* @return mixed - (int) count of written chars or FALSE if error
* @throws RuntimeException
*/
protected function writeStream($data = null)
{
$written = 0;
$n = 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->isConnected()) {
$this->logger->debug(__METHOD__ . ': Disconnected.');
return false;
} else {
$this->logger->err(__METHOD__ . ': Error writing to socket.');
throw new RuntimeException('Error writing to socket.');
}
}
$written += $n;
}
return $written;
}
}
|