Skip to content

Commit

Permalink
Merge pull request #20 from xp-forge/feature/development-server
Browse files Browse the repository at this point in the history
Support development webserver
  • Loading branch information
thekid authored Nov 12, 2017
2 parents 83829bc + 36d92ac commit 2f1c2e4
Show file tree
Hide file tree
Showing 8 changed files with 318 additions and 4 deletions.
9 changes: 9 additions & 0 deletions src/main/php/module.xp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php namespace xp\web;

module xp-forge/web {

/** Provide entry point for web-main.php */
public function initialize() {
class_alias(WebRunner::class, 'xp\scriptlet\Runner');
}
}
8 changes: 6 additions & 2 deletions src/main/php/web/Response.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ public function answer($status, $message= null) {
* Sets a header
*
* @param string $name
* @param string $value
* @param string $value Pass NULL to remove the header
* @return void
*/
public function header($name, $value) {
$this->headers[$name]= $value;
if (null === $value) {
unset($this->headers[$name]);
} else {
$this->headers[$name]= $value;
}
}

/** @return int */
Expand Down
102 changes: 102 additions & 0 deletions src/main/php/xp/web/Develop.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php namespace xp\web;

use util\cmd\Console;
use lang\Runtime;
use lang\RuntimeOptions;
use lang\CommandLine;
use lang\ClassLoader;
use lang\FileSystemClassLoader;
use lang\archive\ArchiveClassLoader;
use peer\Socket;
use io\IOException;

class Develop {
private $host, $port;

/**
* Creates a new instance
*
* @param string $host
* @param int $port
*/
public function __construct($host, $port) {
$this->host= $host;
$this->port= $port;
}

/**
* Serve requests
*
* @param string $source
* @param string $profile
* @param io.Path $webroot
* @param io.Path $docroot
* @param string[] $config
*/
public function serve($source, $profile, $webroot, $docroot, $config) {

// PHP doesn't start with a nonexistant document root
if (!$docroot->exists()) {
$docroot= getcwd();
}

// Inherit all currently loaded paths acceptable to bootstrapping
$include= '.'.PATH_SEPARATOR.PATH_SEPARATOR.'.';
foreach (ClassLoader::getLoaders() as $delegate) {
if ($delegate instanceof FileSystemClassLoader || $delegate instanceof ArchiveClassLoader) {
$include.= PATH_SEPARATOR.$delegate->path;
}
}

// Start `php -S`, the development webserver
$runtime= Runtime::getInstance();
$arguments= ['-S', $this->host.':'.$this->port, '-t', $docroot];
$cmd= CommandLine::forName(PHP_OS)->compose($runtime->getExecutable()->getFileName(), array_merge(
$arguments,
$runtime->startupOptions()->withSetting('user_dir', $docroot)->withSetting('include_path', $include)->asArguments(),
[$runtime->bootStrapScript('web')]
));

// Export environment
putenv('DOCUMENT_ROOT='.$docroot);
putenv('SERVER_PROFILE='.$profile);
putenv('WEB_SOURCE='.$source);
putenv('WEB_CONFIG='.implode('PATH_SEPARATOR', $config));
putenv('WEB_ROOT='.$webroot);

Console::writeLine("\e[33m@", nameof($this), "(HTTP @ `php ", implode(' ', $arguments), "`)\e[0m");
Console::writeLine("\e[1mServing ", $source, $config, "\e[0m");
Console::writeLine("\e[36m", str_repeat('', 72), "\e[0m");
Console::writeLine();

if (!($proc= proc_open($cmd, [STDIN, STDOUT, STDERR], $pipes, null, null, ['bypass_shell' => true]))) {
throw new IOException('Cannot execute `'.$runtime->getExecutable()->getFileName().'`');
}

Console::writeLine("\e[33;1m>\e[0m Server started: \e[35;4mhttp://$this->host:$this->port\e[0m (", date('r'), ')');
Console::writeLine(' PID ', getmypid(), ' : ', proc_get_status($proc)['pid'], '; press Enter to exit');
Console::writeLine();

// Inside `xp -supervise`, connect to signalling socket
if ($port= getenv('XP_SIGNAL')) {
$s= new Socket('127.0.0.1', $port);
$s->connect();
$s->canRead(null) && $s->read();
$s->close();
} else {
Console::read();
Console::write('> Shut down ');
}

// Wait for shutdown
proc_terminate($proc, 2);
do {
Console::write('.');
$status= proc_get_status($proc);
} while ($status['running']);

proc_close($proc);
Console::writeLine();
Console::writeLine("\e[33;1m>\e[0m Server stopped. (", date('r'), ')');
}
}
3 changes: 2 additions & 1 deletion src/main/php/xp/web/HttpProtocol.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ public function handleData($socket) {

$request= new Request($input);
$response= new Response(new Output($socket));
$response->header('Date', gmdate('D, d M Y H:i:s T'));
$response->header('Host', $request->header('Host'));

try {
$this->application->service($request, $response);
Expand All @@ -108,7 +110,6 @@ public function handleData($socket) {
} catch (\Exception $e) { // PHP5
$this->sendError($request, $response, new InternalServerError($e));
} finally {
$response->header('Date', gmdate('D, d M Y H:i:s T'));
$response->flushed() || $response->flush();

if ('Keep-Alive' === $request->header('Connection') && !getenv('NO_KEEPALIVE')) {
Expand Down
100 changes: 100 additions & 0 deletions src/main/php/xp/web/SAPI.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php namespace xp\web;

/**
* Wrapper for PHP's Server API ("SAPI").
*
* @see http://php.net/reserved.variables.server
* @see http://php.net/wrappers.php
* @see http://php.net/php-sapi-name
*/
class SAPI extends \web\io\Output implements \web\io\Input {
private $in= null;
private $out;

/** @return string */
public function method() { return $_SERVER['REQUEST_METHOD']; }

/** @return string */
public function scheme() { return 'http'; }

/** @return string */
public function uri() { return $_SERVER['REQUEST_URI']; }

/** @return [:string] */
public function headers() { return getallheaders(); }

/** @return string */
public function readLine() {
if (null === $this->in) {
$this->in= fopen('php://input', 'rb');
}
return fgets($this->in, 8192);
}

/**
* Read from request
*
* @param int $length
* @return string
*/
public function read($length= -1) {
if (null === $this->in) {
$this->in= fopen('php://input', 'rb');
}

if (-1 === $length) {
$r= '';
while (!feof($this->in)) {
$r.= fread($this->in, 8192);
}
return $r;
} else {
return fread($this->in, $length);
}
}

/**
* Start response
*
* @param int $status
* @param string $message
* @param [:string] $headers
* @return void
*/
public function begin($status, $message, $headers) {
if ('cgi' === PHP_SAPI || 'cgi-fcgi' === PHP_SAPI) {
header('Status: '.$status.' '.$message);
} else {
header('HTTP/1.1 '.$status.' '.$message);
}

foreach ($headers as $name => $value) {
header($name.': '.$value);
}
$this->out= '';
}

/**
* Write to response
*
* @param int $bytes
* @return void
*/
public function write($bytes) {
$this->out.= $bytes;
}

/** @return void */
public function flush() {
echo $this->out;
$this->out= '';
}

/** @return void */
public function finish() {
if ($this->in) {
fclose($this->in);
}
echo $this->out;
}
}
2 changes: 1 addition & 1 deletion src/main/php/xp/web/Standalone.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function serve($source, $profile, $webroot, $docroot, $config) {
$this->server->init();

Console::writeLine("\e[33m@", nameof($this), '(HTTP @ ', $this->server->socket->toString(), ")\e[0m");
Console::writeLine("\e[1mServing application ", $application);
Console::writeLine("\e[1mServing ", $source, $config, "\e[0m");
Console::writeLine("\e[36m", str_repeat('', 72), "\e[0m");
Console::writeLine();

Expand Down
90 changes: 90 additions & 0 deletions src/main/php/xp/web/WebRunner.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php namespace xp\web;

use web\Environment;
use web\Request;
use web\Response;
use web\Error;
use web\Status;
use web\InternalServerError;
use lang\ClassLoader;

/**
* Entry point for web-main.php
*/
class WebRunner {

/**
* Logs a request
*
* @param web.Request $response
* @param web.Response $response
* @param string $message
* @return void
*/
private static function log($request, $response, $message= null) {
$query= $request->uri()->query();
fprintf(STDERR,
" \e[33m[%s %d %.3fkB]\e[0m %d %s %s %s\n",
date('Y-m-d H:i:s'),
getmypid(),
memory_get_usage() / 1024,
$response->status(),
$request->method(),
$request->uri()->path().($query ? '?'.$query : ''),
$message
);
}

/**
* Sends an error
*
* @param web.Request $response
* @param web.Response $response
* @param web.Error $error
* @param string $profile
* @return void
*/
private static function error($request, $response, $error, $profile) {
$loader= ClassLoader::getDefault();
$message= Status::message($error->status());

$response->answer($error->status(), $message);
foreach (['web/error-'.$profile.'.html', 'web/error.html'] as $variant) {
if (!$loader->providesResource($variant)) continue;
$response->send(sprintf(
$loader->getResource($variant),
$error->status(),
htmlspecialchars($message),
htmlspecialchars($error->getMessage()),
htmlspecialchars($error->toString())
));
break;
}
self::log($request, $response, $error->toString());
}

/** @param string[] $args */
public static function main($args) {
$env= new Environment($args[2], $args[0], $args[1], explode('PATH_SEPARATOR', getenv('WEB_CONFIG')));
$application= (new Source(getenv('WEB_SOURCE'), $env))->application();

$sapi= new SAPI();
$request= new Request($sapi);
$response= new Response($sapi);
$response->header('Date', gmdate('D, d M Y H:i:s T'));
$response->header('Host', $request->header('Host'));

try {
$application->service($request, $response);
self::log($request, $response);
} catch (Error $e) {
self::error($request, $response, $e);
} catch (\Throwable $e) { // PHP7
self::error($request, $response, new InternalServerError($e), $args[2]);
} catch (\Exception $e) { // PHP5
self::error($request, $response, new InternalServerError($e), $args[2]);
} finally {
$response->flushed() || $response->flush();
}
}
}
8 changes: 8 additions & 0 deletions src/test/php/web/unittest/ResponseTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ public function headers() {
);
}

#[@test]
public function remove_header() {
$res= new Response(new TestOutput());
$res->header('Content-Type', 'text/plain');
$res->header('Content-Type', null);
$this->assertEquals([], $res->headers());
}

#[@test, @values([
# [200, 'OK', 'HTTP/1.1 200 OK'],
# [404, 'Not Found', 'HTTP/1.1 404 Not Found'],
Expand Down

0 comments on commit 2f1c2e4

Please sign in to comment.