[VarDumper] Fix dumping by splitting Server/Connection out of Dumper/ServerDumper

This commit is contained in:
Nicolas Grekas 2018-06-15 20:19:15 +02:00
parent 6e0818db16
commit 1435d677be
14 changed files with 252 additions and 126 deletions

View File

@ -17,7 +17,6 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\VarDumper\Dumper\ServerDumper;
/**
* DebugExtension.
@ -43,20 +42,21 @@ class DebugExtension extends Extension
->addMethodCall('setMaxString', array($config['max_string_length']));
if (null === $config['dump_destination']) {
//no-op
$container->getDefinition('var_dumper.command.server_dump')
->setClass(ServerDumpPlaceholderCommand::class)
;
} elseif (0 === strpos($config['dump_destination'], 'tcp://')) {
$serverDumperHost = $config['dump_destination'];
$container->getDefinition('debug.dump_listener')
->replaceArgument(1, new Reference('var_dumper.server_dumper'))
->replaceArgument(2, new Reference('var_dumper.server_connection'))
;
$container->getDefinition('data_collector.dump')
->replaceArgument(4, new Reference('var_dumper.server_dumper'))
->replaceArgument(4, new Reference('var_dumper.server_connection'))
;
$container->getDefinition('var_dumper.dump_server')
->replaceArgument(0, $serverDumperHost)
->replaceArgument(0, $config['dump_destination'])
;
$container->getDefinition('var_dumper.server_dumper')
->replaceArgument(0, $serverDumperHost)
$container->getDefinition('var_dumper.server_connection')
->replaceArgument(0, $config['dump_destination'])
;
} else {
$container->getDefinition('var_dumper.cli_dumper')
@ -65,13 +65,9 @@ class DebugExtension extends Extension
$container->getDefinition('data_collector.dump')
->replaceArgument(4, new Reference('var_dumper.cli_dumper'))
;
}
if (!isset($serverDumperHost)) {
$container->getDefinition('var_dumper.command.server_dump')->setClass(ServerDumpPlaceholderCommand::class);
if (!class_exists(ServerDumper::class)) {
$container->removeDefinition('var_dumper.command.server_dump');
}
$container->getDefinition('var_dumper.command.server_dump')
->setClass(ServerDumpPlaceholderCommand::class)
;
}
}

View File

@ -23,13 +23,14 @@
<argument type="service" id="debug.file_link_formatter" on-invalid="ignore"></argument>
<argument>%kernel.charset%</argument>
<argument type="service" id="request_stack" />
<argument>null</argument><!-- var_dumper.cli_dumper or var_dumper.server_dumper when debug.dump_destination is set -->
<argument>null</argument><!-- var_dumper.cli_dumper or var_dumper.server_connection when debug.dump_destination is set -->
</service>
<service id="debug.dump_listener" class="Symfony\Component\HttpKernel\EventListener\DumpListener">
<tag name="kernel.event_subscriber" />
<argument type="service" id="var_dumper.cloner" />
<argument type="service" id="var_dumper.cli_dumper" />
<argument>null</argument>
</service>
<service id="var_dumper.cloner" class="Symfony\Component\VarDumper\Cloner\VarCloner" public="true" />
@ -50,9 +51,8 @@
</call>
</service>
<service id="var_dumper.server_dumper" class="Symfony\Component\VarDumper\Dumper\ServerDumper">
<argument>null</argument> <!-- server host -->
<argument type="service" id="var_dumper.cli_dumper" />
<service id="var_dumper.server_connection" class="Symfony\Component\VarDumper\Server\Connection">
<argument /> <!-- server host -->
<argument type="collection">
<argument type="service" key="source">
<service class="Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider">

View File

@ -20,7 +20,7 @@
"ext-xml": "*",
"symfony/http-kernel": "~3.4|~4.0",
"symfony/twig-bridge": "~3.4|~4.0",
"symfony/var-dumper": "~4.1"
"symfony/var-dumper": "^4.1.1"
},
"require-dev": {
"symfony/config": "~3.4|~4.0",

View File

@ -21,7 +21,7 @@ use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Symfony\Component\VarDumper\Dumper\DataDumperInterface;
use Symfony\Component\VarDumper\Dumper\ServerDumper;
use Symfony\Component\VarDumper\Server\Connection;
/**
* @author Nicolas Grekas <p@tchwork.com>
@ -38,17 +38,18 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
private $charset;
private $requestStack;
private $dumper;
private $dumperIsInjected;
private $sourceContextProvider;
public function __construct(Stopwatch $stopwatch = null, $fileLinkFormat = null, string $charset = null, RequestStack $requestStack = null, DataDumperInterface $dumper = null)
/**
* @param DataDumperInterface|Connection|null $dumper
*/
public function __construct(Stopwatch $stopwatch = null, $fileLinkFormat = null, string $charset = null, RequestStack $requestStack = null, $dumper = null)
{
$this->stopwatch = $stopwatch;
$this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format');
$this->charset = $charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8';
$this->requestStack = $requestStack;
$this->dumper = $dumper;
$this->dumperIsInjected = null !== $dumper;
// All clones share these properties by reference:
$this->rootRefs = array(
@ -58,7 +59,7 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
&$this->clonesCount,
);
$this->sourceContextProvider = $dumper instanceof ServerDumper && isset($dumper->getContextProviders()['source']) ? $dumper->getContextProviders()['source'] : new SourceContextProvider($this->charset);
$this->sourceContextProvider = $dumper instanceof Connection && isset($dumper->getContextProviders()['source']) ? $dumper->getContextProviders()['source'] : new SourceContextProvider($this->charset);
}
public function __clone()
@ -71,14 +72,17 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
if ($this->stopwatch) {
$this->stopwatch->start('dump');
}
if ($this->isCollected && !$this->dumper) {
$this->isCollected = false;
}
list('name' => $name, 'file' => $file, 'line' => $line, 'file_excerpt' => $fileExcerpt) = $this->sourceContextProvider->getContext();
if ($this->dumper) {
if ($this->dumper instanceof Connection) {
if (!$this->dumper->write($data)) {
$this->isCollected = false;
}
} elseif ($this->dumper) {
$this->doDump($this->dumper, $data, $name, $file, $line);
} else {
$this->isCollected = false;
}
$this->data[] = compact('data', 'name', 'file', 'line', 'fileExcerpt');
@ -141,9 +145,6 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
$this->data = array();
$this->dataCount = 0;
$this->isCollected = true;
if (!$this->dumperIsInjected) {
$this->dumper = null;
}
return $ser;
}
@ -245,7 +246,7 @@ class DumpDataCollector extends DataCollector implements DataDumperInterface
};
$contextDumper = $contextDumper->bindTo($dumper, $dumper);
$contextDumper($name, $file, $line, $this->fileLinkFormat);
} elseif (!$dumper instanceof ServerDumper) {
} else {
$cloner = new VarCloner();
$dumper->dump($cloner->cloneVar($name.' on line '.$line.':'));
}

View File

@ -15,6 +15,7 @@ use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\VarDumper\Cloner\ClonerInterface;
use Symfony\Component\VarDumper\Dumper\DataDumperInterface;
use Symfony\Component\VarDumper\Server\Connection;
use Symfony\Component\VarDumper\VarDumper;
/**
@ -26,20 +27,27 @@ class DumpListener implements EventSubscriberInterface
{
private $cloner;
private $dumper;
private $connection;
public function __construct(ClonerInterface $cloner, DataDumperInterface $dumper)
public function __construct(ClonerInterface $cloner, DataDumperInterface $dumper, Connection $connection = null)
{
$this->cloner = $cloner;
$this->dumper = $dumper;
$this->connection = $connection;
}
public function configure()
{
$cloner = $this->cloner;
$dumper = $this->dumper;
$connection = $this->connection;
VarDumper::setHandler(function ($var) use ($cloner, $dumper) {
$dumper->dump($cloner->cloneVar($var));
VarDumper::setHandler(static function ($var) use ($cloner, $dumper, $connection) {
$data = $cloner->cloneVar($var);
if (!$connection || !$connection->write($data)) {
$dumper->dump($data);
}
});
}

View File

@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use Symfony\Component\VarDumper\Dumper\ServerDumper;
use Symfony\Component\VarDumper\Server\Connection;
/**
* @author Nicolas Grekas <p@tchwork.com>
@ -57,13 +57,13 @@ class DumpDataCollectorTest extends TestCase
$this->assertSame('a:2:{i:0;b:0;i:1;s:5:"UTF-8";}', $collector->serialize());
}
public function testDumpWithServerDumper()
public function testDumpWithServerConnection()
{
$data = new Data(array(array(123)));
// Server is up, server dumper is used
$serverDumper = $this->getMockBuilder(ServerDumper::class)->disableOriginalConstructor()->getMock();
$serverDumper->expects($this->once())->method('dump');
$serverDumper = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
$serverDumper->expects($this->once())->method('write')->willReturn(true);
$collector = new DumpDataCollector(null, null, null, null, $serverDumper);
$collector->dump($data);

View File

@ -37,7 +37,7 @@
"symfony/stopwatch": "~3.4|~4.0",
"symfony/templating": "~3.4|~4.0",
"symfony/translation": "~3.4|~4.0",
"symfony/var-dumper": "~4.1",
"symfony/var-dumper": "^4.1.1",
"psr/cache": "~1.0"
},
"provide": {
@ -46,7 +46,7 @@
"conflict": {
"symfony/config": "<3.4",
"symfony/dependency-injection": "<4.1",
"symfony/var-dumper": "<4.1",
"symfony/var-dumper": "<4.1.1",
"twig/twig": "<1.34|<2.4,>=2"
},
"suggest": {

View File

@ -164,7 +164,7 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface
*/
protected function dumpLine($depth)
{
call_user_func($this->lineDumper, $this->line, $depth, $this->indentPad);
\call_user_func($this->lineDumper, $this->line, $depth, $this->indentPad);
$this->line = '';
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\VarDumper\Dumper;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
use Symfony\Component\VarDumper\Server\Connection;
/**
* ServerDumper forwards serialized Data clones to a server.
@ -21,10 +22,8 @@ use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
*/
class ServerDumper implements DataDumperInterface
{
private $host;
private $connection;
private $wrappedDumper;
private $contextProviders;
private $socket;
/**
* @param string $host The server host
@ -33,83 +32,22 @@ class ServerDumper implements DataDumperInterface
*/
public function __construct(string $host, DataDumperInterface $wrappedDumper = null, array $contextProviders = array())
{
if (false === strpos($host, '://')) {
$host = 'tcp://'.$host;
}
$this->host = $host;
$this->connection = new Connection($host, $contextProviders);
$this->wrappedDumper = $wrappedDumper;
$this->contextProviders = $contextProviders;
}
public function getContextProviders(): array
{
return $this->contextProviders;
return $this->connection->getContextProviders();
}
/**
* {@inheritdoc}
*/
public function dump(Data $data, $output = null): void
public function dump(Data $data)
{
set_error_handler(array(self::class, 'nullErrorHandler'));
$failed = false;
try {
if (!$this->socket = $this->socket ?: $this->createSocket()) {
$failed = true;
return;
}
} finally {
restore_error_handler();
if ($failed && $this->wrappedDumper) {
$this->wrappedDumper->dump($data);
}
if (!$this->connection->write($data) && $this->wrappedDumper) {
$this->wrappedDumper->dump($data);
}
set_error_handler(array(self::class, 'nullErrorHandler'));
$context = array('timestamp' => time());
foreach ($this->contextProviders as $name => $provider) {
$context[$name] = $provider->getContext();
}
$context = array_filter($context);
$encodedPayload = base64_encode(serialize(array($data, $context)))."\n";
$failed = false;
try {
$retry = 3;
while ($retry > 0 && $failed = (-1 === stream_socket_sendto($this->socket, $encodedPayload))) {
stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
if ($failed = !$this->socket = $this->createSocket()) {
break;
}
--$retry;
}
} finally {
restore_error_handler();
if ($failed && $this->wrappedDumper) {
$this->wrappedDumper->dump($data);
}
}
}
private static function nullErrorHandler()
{
// noop
}
private function createSocket()
{
$socket = stream_socket_client($this->host, $errno, $errstr, 1, STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT);
if ($socket) {
stream_set_blocking($socket, false);
}
return $socket;
}
}

View File

@ -22,14 +22,6 @@ a {
a:hover {
text-decoration: underline;
}
code {
color: #cc2255;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
border-radius: 3px;
margin-right: 5px;
padding: 0 3px;
}
.text-small {
font-size: 12px !important;
}
@ -60,6 +52,12 @@ article > header > .row > h2 {
article > header > .row > h2 > code {
white-space: nowrap;
user-select: none;
color: #cc2255;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
border-radius: 3px;
margin-right: 5px;
padding: 0 3px;
}
article > header > .row > time.col {
flex: 0;

View File

@ -0,0 +1,97 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\VarDumper\Server;
use Symfony\Component\VarDumper\Cloner\Data;
use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
/**
* Forwards serialized Data clones to a server.
*
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
*/
class Connection
{
private $host;
private $contextProviders;
private $socket;
/**
* @param string $host The server host
* @param ContextProviderInterface[] $contextProviders Context providers indexed by context name
*/
public function __construct(string $host, array $contextProviders = array())
{
if (false === strpos($host, '://')) {
$host = 'tcp://'.$host;
}
$this->host = $host;
$this->contextProviders = $contextProviders;
}
public function getContextProviders(): array
{
return $this->contextProviders;
}
public function write(Data $data): bool
{
$socketIsFresh = !$this->socket;
if (!$this->socket = $this->socket ?: $this->createSocket()) {
return false;
}
$context = array('timestamp' => microtime(true));
foreach ($this->contextProviders as $name => $provider) {
$context[$name] = $provider->getContext();
}
$context = array_filter($context);
$encodedPayload = base64_encode(serialize(array($data, $context)))."\n";
set_error_handler(array(self::class, 'nullErrorHandler'));
try {
if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) {
return true;
}
if (!$socketIsFresh) {
stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
fclose($this->socket);
$this->socket = $this->createSocket();
}
if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) {
return true;
}
} finally {
restore_error_handler();
}
return false;
}
private static function nullErrorHandler($t, $m)
{
// no-op
}
private function createSocket()
{
set_error_handler(array(self::class, 'nullErrorHandler'));
try {
return stream_socket_client($this->host, $errno, $errstr, 3, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
} finally {
restore_error_handler();
}
return $socket;
}
}

View File

@ -55,26 +55,24 @@ class ServerDumperTest extends TestCase
$dumped = null;
$process = $this->getServerProcess();
$process->start(function ($type, $buffer) use ($process, &$dumped) {
$process->start(function ($type, $buffer) use ($process, &$dumped, $dumper, $data) {
if (Process::ERR === $type) {
$process->stop();
$this->fail();
} elseif ("READY\n" === $buffer) {
$dumper->dump($data);
} else {
$dumped .= $buffer;
}
});
sleep(3);
$dumper->dump($data);
$process->wait();
$this->assertTrue($process->isSuccessful());
$this->assertStringMatchesFormat(<<<'DUMP'
(3) "foo"
[
"timestamp" => %d
"timestamp" => %d.%d
"foo_provider" => [
(3) "foo"
]

View File

@ -29,6 +29,8 @@ $server = new DumpServer(getenv('VAR_DUMPER_SERVER'));
$server->start();
echo "READY\n";
$server->listen(function (Data $data, array $context, $clientId) {
dump((string) $data, $context, $clientId);

View File

@ -0,0 +1,88 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\VarDumper\Tests\Server;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\PhpProcess;
use Symfony\Component\Process\Process;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface;
use Symfony\Component\VarDumper\Server\Connection;
class ConnectionTest extends TestCase
{
private const VAR_DUMPER_SERVER = 'tcp://127.0.0.1:9913';
public function testDump()
{
$cloner = new VarCloner();
$data = $cloner->cloneVar('foo');
$connection = new Connection(self::VAR_DUMPER_SERVER, array(
'foo_provider' => new class() implements ContextProviderInterface {
public function getContext(): ?array
{
return array('foo');
}
},
));
$dumped = null;
$process = $this->getServerProcess();
$process->start(function ($type, $buffer) use ($process, &$dumped, $connection, $data) {
if (Process::ERR === $type) {
$process->stop();
$this->fail();
} elseif ("READY\n" === $buffer) {
$connection->write($data);
} else {
$dumped .= $buffer;
}
});
$process->wait();
$this->assertTrue($process->isSuccessful());
$this->assertStringMatchesFormat(<<<'DUMP'
(3) "foo"
[
"timestamp" => %d.%d
"foo_provider" => [
(3) "foo"
]
]
%d
DUMP
, $dumped);
}
public function testNoServer()
{
$cloner = new VarCloner();
$data = $cloner->cloneVar('foo');
$connection = new Connection(self::VAR_DUMPER_SERVER);
$start = microtime(true);
$this->assertFalse($connection->write($data));
$this->assertLessThan(1, microtime(true) - $start);
}
private function getServerProcess(): Process
{
$process = new PhpProcess(file_get_contents(__DIR__.'/../Fixtures/dump_server.php'), null, array(
'COMPONENT_ROOT' => __DIR__.'/../../',
'VAR_DUMPER_SERVER' => self::VAR_DUMPER_SERVER,
));
$process->inheritEnvironmentVariables(true);
return $process->setTimeout(9);
}
}