nextcloud/build/acceptance/features/core/NextcloudTestServerDroneHel...

273 lines
9.1 KiB
PHP

<?php
/**
*
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace NextcloudServerControl {
class SocketException extends \Exception {
public function __construct($message) {
parent::__construct($message);
}
}
/**
* Common class for communication between client and server.
*
* Clients and server communicate through messages: a client sends a request and
* the server answers with a response. Requests and responses all have the same
* common structure composed by a mandatory header and optional data. The header
* contains a code that identifies the type of request or response followed by
* the length of the data (which can be 0). The data is a free form string that
* depends on each request and response type.
*
* The Messenger abstracts all that and provides two public methods: readMessage
* and writeMessage. For each connection a client first writes the request
* message and then reads the response message, while the server first reads the
* request message and then writes the response message. If the client needs to
* send another request it must connect again to the server.
*
* The Messenger class in the server must be kept in sync with the Messenger
* class in the client. Due to the size of the code and its current use it was
* more practical, at least for the time being, to keep two copies of the code
* than creating a library that had to be downloaded and included in the client
* and in the server.
*/
class Messenger {
/**
* Reset the Nextcloud server.
*
* -Request data: empty
* -OK response data: empty.
* -Failed response data: error information.
*/
const CODE_REQUEST_RESET = 0;
const CODE_RESPONSE_OK = 0;
const CODE_RESPONSE_FAILED = 1;
const HEADER_LENGTH = 5;
/**
* Reads a message from the given socket.
*
* The message is returned as an indexed array with keys "code" and "data".
*
* @param resource $socket the socket to read the message from.
* @return array the message read.
* @throws SocketException if an error occurs while reading the socket.
*/
public static function readMessage($socket) {
$header = self::readSocket($socket, self::HEADER_LENGTH);
$header = unpack("Ccode/VdataLength", $header);
$data = self::readSocket($socket, $header["dataLength"]);
return [ "code" => $header["code"], "data" => $data ];
}
/**
* Reads content from the given socket.
*
* It blocks until the specified number of bytes were read.
*
* @param resource $socket the socket to read the message from.
* @param int $length the number of bytes to read.
* @return string the content read.
* @throws SocketException if an error occurs while reading the socket.
*/
private static function readSocket($socket, $length) {
if ($socket == null) {
throw new SocketException("Null socket can not be read from");
}
$pendingLength = $length;
$content = "";
while ($pendingLength > 0) {
$readContent = socket_read($socket, $pendingLength);
if ($readContent === "") {
throw new SocketException("Socket could not be read: $pendingLength bytes are pending, but there is no more data to read");
} else if ($readContent == false) {
throw new SocketException("Socket could not be read: " . socket_strerror(socket_last_error()));
}
$pendingLength -= strlen($readContent);
$content = $content . $readContent;
}
return $content;
}
/**
* Writes a message to the given socket.
*
* @param resource $socket the socket to write the message to.
* @param int $code the message code.
* @param string $data the message data, if any.
* @throws SocketException if an error occurs while reading the socket.
*/
public static function writeMessage($socket, $code, $data = "") {
if ($socket == null) {
throw new SocketException("Null socket can not be written to");
}
$header = pack("CV", $code, strlen($data));
$message = $header . $data;
$pendingLength = strlen($message);
while ($pendingLength > 0) {
$sent = socket_write($socket, $message, $pendingLength);
if ($sent !== 0 && $sent == false) {
throw new SocketException("Message ($message) could not be written: " . socket_strerror(socket_last_error()));
}
$pendingLength -= $sent;
$message = substr($message, $sent);
}
}
}
}
namespace {
use NextcloudServerControl\Messenger;
use NextcloudServerControl\SocketException;
/**
* Helper to manage the Nextcloud test server running in a Drone service.
*
* The NextcloudTestServerDroneHelper controls a Nextcloud test server running
* in a Drone service. The "setUp" method resets the Nextcloud server to its
* initial state; nothing needs to be done in the "cleanUp" method. To be able
* to control the remote Nextcloud server the Drone service must provide the
* Nextcloud server control server; the port in which the server listens on can
* be set with the $nextcloudTestServerControlPort parameter of the constructor.
*
* Drone services are available at "127.0.0.1", so the Nextcloud server is
* expected to see "127.0.0.1" as a trusted domain (which would be the case if
* it was installed by running "occ maintenance:install"). Note, however, that
* the Nextcloud server does not listen on port "80" but on port "8000" due to
* internal issues of the Nextcloud server control. In any case, the base URL to
* access the Nextcloud server can be got from "getBaseUrl".
*/
class NextcloudTestServerDroneHelper implements NextcloudTestServerHelper {
/**
* @var int
*/
private $nextcloudTestServerControlPort;
/**
* Creates a new NextcloudTestServerDroneHelper.
*
* @param int $nextcloudTestServerControlPort the port in which the
* Nextcloud server control is listening.
*/
public function __construct($nextcloudTestServerControlPort) {
$this->nextcloudTestServerControlPort = $nextcloudTestServerControlPort;
}
/**
* Sets up the Nextcloud test server.
*
* It resets the Nextcloud test server through the control system provided
* by its Drone service and waits for the Nextcloud test server to be
* started again; if the server can not be reset or if it does not start
* again after some time an exception is thrown (as it is just a warning for
* the test runner and nothing to be explicitly catched a plain base
* Exception is used).
*
* @throws \Exception if the Nextcloud test server in the Drone service can
* not be reset or started again.
*/
public function setUp() {
$resetNextcloudServerCallback = function($socket) {
Messenger::writeMessage($socket, Messenger::CODE_REQUEST_RESET);
$response = Messenger::readMessage($socket);
if ($response["code"] == Messenger::CODE_RESPONSE_FAILED) {
throw new Exception("Request to reset Nextcloud server failed: " . $response["data"]);
}
};
$this->sendRequestAndHandleResponse($resetNextcloudServerCallback);
$timeout = 60;
if (!Utils::waitForServer($this->getBaseUrl(), $timeout)) {
throw new Exception("Nextcloud test server could not be started");
}
}
/**
* Cleans up the Nextcloud test server.
*
* Nothing needs to be done when using the Drone service.
*/
public function cleanUp() {
}
/**
* Returns the base URL of the Nextcloud test server.
*
* @return string the base URL of the Nextcloud test server.
*/
public function getBaseUrl() {
return "http://127.0.0.1:8000/index.php";
}
/**
* Executes the given callback to communicate with the Nextcloud test server
* control.
*
* A socket is created with the Nextcloud test server control and passed to
* the callback to send the request and handle its response.
*
* @param \Closure $nextcloudServerControlCallback the callback to call with
* the communication socket.
* @throws \Exception if any socket-related operation fails.
*/
private function sendRequestAndHandleResponse($nextcloudServerControlCallback) {
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === false) {
throw new Exception("Request socket to reset Nextcloud server could not be created: " . socket_strerror(socket_last_error()));
}
try {
if (socket_connect($socket, "127.0.0.1", $this->nextcloudTestServerControlPort) === false) {
throw new Exception("Request socket to reset Nextcloud server could not be connected: " . socket_strerror(socket_last_error()));
}
$nextcloudServerControlCallback($socket);
} catch (SocketException $exception) {
throw new Exception("Request socket to reset Nextcloud server failed: " . $exception->getMessage());
} finally {
socket_close($socket);
}
}
}
}