Skip to content

Commit

Permalink
add option to stop target process while reading its trace
Browse files Browse the repository at this point in the history
  • Loading branch information
sj-i committed Aug 10, 2021
1 parent 09391ad commit 2c1a078
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 8 deletions.
10 changes: 10 additions & 0 deletions src/Command/Inspector/GetTraceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use PhpProfiler\Lib\PhpProcessReader\PhpGlobalsFinder;
use PhpProfiler\Lib\Process\MemoryReader\MemoryReaderException;
use PhpProfiler\Lib\PhpProcessReader\PhpMemoryReader\ExecutorGlobalsReader;
use PhpProfiler\Lib\Process\ProcessStopper\ProcessStopper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
Expand All @@ -43,6 +44,7 @@ public function __construct(
private TraceLoopSettingsFromConsoleInput $trace_loop_settings_from_console_input,
private TemplateSettingsFromConsoleInput $template_settings_from_console_input,
private TemplatedTraceFormatterFactory $templated_trace_formatter_factory,
private ProcessStopper $process_stopper,
) {
parent::__construct();
}
Expand Down Expand Up @@ -84,16 +86,24 @@ function () use (
$get_trace_settings,
$target_process_settings,
$target_php_settings,
$loop_settings,
$eg_address,
$output,
$formatter
): bool {
$is_target_stopped = false;
if ($loop_settings->stop_process) {
$is_target_stopped = $this->process_stopper->stop($target_process_settings->pid);
}
$call_trace = $this->executor_globals_reader->readCallTrace(
$target_process_settings->pid,
$target_php_settings->php_version,
$eg_address,
$get_trace_settings->depth
);
if ($loop_settings->stop_process and $is_target_stopped) {
$this->process_stopper->resume($target_process_settings->pid);
}
$output->write($formatter->format($call_trace) . PHP_EOL);
return true;
},
Expand Down
11 changes: 10 additions & 1 deletion src/Inspector/Daemon/Reader/Worker/PhpReaderTraceLoop.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ final class PhpReaderTraceLoop implements PhpReaderTraceLoopInterface
public function __construct(
private PhpGlobalsFinder $php_globals_finder,
private ExecutorGlobalsReader $executor_globals_reader,
private ReaderLoopProvider $reader_loop_provider
private ReaderLoopProvider $reader_loop_provider,
private ProcessStopper $process_stopper,
) {
}

Expand Down Expand Up @@ -55,14 +56,22 @@ function () use (
$get_trace_settings,
$target_process_settings,
$target_php_settings,
$loop_settings,
$eg_address
): \Generator {
$is_target_stopped = false;
if ($loop_settings->stop_process) {
$is_target_stopped = $this->process_stopper->stop($target_process_settings->pid);
}
$call_trace = $this->executor_globals_reader->readCallTrace(
$target_process_settings->pid,
$target_php_settings->php_version,
$eg_address,
$get_trace_settings->depth
);
if ($loop_settings->stop_process and $is_target_stopped) {
$this->process_stopper->resume($target_process_settings->pid);
}
yield new TraceMessage($call_trace);
},
$loop_settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ final class TraceLoopSettings
public function __construct(
public int $sleep_nano_seconds,
public string $cancel_key,
public int $max_retries
public int $max_retries,
public bool $stop_process,
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ final class TraceLoopSettingsException extends InspectorSettingsException
{
public const SLEEP_NS_IS_NOT_INTEGER = 1;
public const MAX_RETRY_IS_NOT_INTEGER = 2;
public const STOP_PROCESS_IS_NOT_BOOLEAN = 3;

protected const ERRORS = [
self::SLEEP_NS_IS_NOT_INTEGER => 'sleep-ns is not integer',
self::MAX_RETRY_IS_NOT_INTEGER => 'max-retries is not integer',
self::STOP_PROCESS_IS_NOT_BOOLEAN => 'stop-process is not boolean',
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public function setOptions(Command $command): void
InputOption::VALUE_OPTIONAL,
'max retries on contiguous errors of read (default: 10)'
)
->addOption(
'stop-process',
'S',
InputOption::VALUE_OPTIONAL,
'stop the target process while reading its trace (default: off)'
)
;
}

Expand Down Expand Up @@ -71,6 +77,22 @@ public function createSettings(InputInterface $input): TraceLoopSettings
);
}

return new TraceLoopSettings($sleep_nano_seconds, TraceLoopSettings::CANCEL_KEY_DEFAULT, $max_retries);
$stop_process = NullableCast::toString($input->getOption('stop-process'));
if (is_null($stop_process)) {
$stop_process = false;
}
$stop_process = filter_var($stop_process, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($stop_process === null) {
throw TraceLoopSettingsException::create(
TraceLoopSettingsException::STOP_PROCESS_IS_NOT_BOOLEAN
);
}

return new TraceLoopSettings(
$sleep_nano_seconds,
TraceLoopSettings::CANCEL_KEY_DEFAULT,
$max_retries,
$stop_process,
);
}
}
127 changes: 127 additions & 0 deletions src/Lib/Process/ProcessStopper/ProcessStopper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

/**
* This file is part of the sj-i/php-profiler package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace PhpProfiler\Lib\Process\ProcessStopper;

use FFI\CInteger;

final class ProcessStopper
{
private \FFI $ffi;

private const PTRACE_ATTACH = 16;
private const PTRACE_DETACH = 17;

public function __construct()
{
$this->ffi = \FFI::cdef('
struct user_regs_struct {
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
unsigned long fs_base;
unsigned long gs_base;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
};
typedef int pid_t;
enum __ptrace_request
{
PTRACE_TRACEME = 0,
PTRACE_PEEKTEXT = 1,
PTRACE_PEEKDATA = 2,
PTRACE_PEEKUSER = 3,
PTRACE_POKETEXT = 4,
PTRACE_POKEDATA = 5,
PTRACE_POKEUSER = 6,
PTRACE_CONT = 7,
PTRACE_KILL = 8,
PTRACE_SINGLESTEP = 9,
PTRACE_GETREGS = 12,
PTRACE_SETREGS = 13,
PTRACE_GETFPREGS = 14,
PTRACE_SETFPREGS = 15,
PTRACE_ATTACH = 16,
PTRACE_DETACH = 17,
PTRACE_GETFPXREGS = 18,
PTRACE_SETFPXREGS = 19,
PTRACE_SYSCALL = 24,
PTRACE_SETOPTIONS = 0x4200,
PTRACE_GETEVENTMSG = 0x4201,
PTRACE_GETSIGINFO = 0x4202,
PTRACE_SETSIGINFO = 0x4203
};
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
int errno;
', 'libc.so.6');
}

public function stop(int $pid): bool
{
/** @var CInteger $zero */
$zero = $this->ffi->new('long');
$zero->cdata = 0;
$null = \FFI::cast('void *', $zero);

/** @var \FFI\Libc\ptrace_ffi $this->ffi */
$attach = $this->ffi->ptrace(self::PTRACE_ATTACH, $pid, $null, $null);

if ($attach === -1) {
/** @var int $errno */
$errno = $this->ffi->errno;
if ($errno) {
return false;
}
}
pcntl_waitpid($pid, $status, WUNTRACED);
return true;
}

public function resume(int $pid): void
{
/** @var CInteger $zero */
$zero = $this->ffi->new('long');
$zero->cdata = 0;
$null = \FFI::cast('void *', $zero);

/** @var \FFI\Libc\ptrace_ffi $this->ffi */
$detach = $this->ffi->ptrace(self::PTRACE_DETACH, $pid, $null, $null);
if ($detach === -1) {
/** @var int $errno */
$errno = $this->ffi->errno;
if ($errno) {
return;
}
}
}
}
2 changes: 1 addition & 1 deletion tests/Inspector/Daemon/Dispatcher/DispatchTableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function testUpdateTarget()
$dispatch_table = new DispatchTable(
$worker_pool,
new TargetPhpSettings(),
new TraceLoopSettings(1, 'test', 1),
new TraceLoopSettings(1, 'test', 1, false),
new GetTraceSettings(1)
);

Expand Down
2 changes: 1 addition & 1 deletion tests/Inspector/Daemon/Dispatcher/WorkerPoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class WorkerPoolTest extends TestCase
public function testCreate()
{
$php_settings = new TargetPhpSettings();
$trace_settings = new TraceLoopSettings(1, 'q', 1);
$trace_settings = new TraceLoopSettings(1, 'q', 1, false);
$get_trace_settings = new GetTraceSettings(1);

$reader_context = Mockery::mock(PhpReaderControllerInterface::class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function testIsRunning(): void
public function testSendSettings(): void
{
$target_php_settings = new TargetPhpSettings();
$trace_loop_settings = new TraceLoopSettings(1, 'q', 1);
$trace_loop_settings = new TraceLoopSettings(1, 'q', 1, false);
$get_trace_settings = new GetTraceSettings(1);

$expected = new SetSettingsMessage(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ function (
$this->assertEquals(
[
new TargetProcessSettings(123),
new TraceLoopSettings(1, 'q', 10),
new TraceLoopSettings(1, 'q', 10, false),
new TargetPhpSettings(),
new GetTraceSettings(PHP_INT_MAX),
],
Expand Down Expand Up @@ -111,7 +111,7 @@ function (
$promise = $generator->send(
new SetSettingsMessage(
new TargetPhpSettings(),
new TraceLoopSettings(1, 'q', 10),
new TraceLoopSettings(1, 'q', 10, false),
new GetTraceSettings(PHP_INT_MAX)
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@ public function testFromConsoleInput(): void
$input = Mockery::mock(InputInterface::class);
$input->expects()->getOption('sleep-ns')->andReturns(20000000);
$input->expects()->getOption('max-retries')->andReturns(20);
$input->expects()->getOption('stop-process')->andReturns('off');

$settings = (new TraceLoopSettingsFromConsoleInput())->createSettings($input);

$this->assertSame(20000000, $settings->sleep_nano_seconds);
$this->assertSame(20, $settings->max_retries);
$this->assertSame('q', $settings->cancel_key);
$this->assertSame(false, $settings->stop_process);
}

public function testFromConsoleInputDefault(): void
{
$input = Mockery::mock(InputInterface::class);
$input->expects()->getOption('sleep-ns')->andReturns(null);
$input->expects()->getOption('max-retries')->andReturns(null);
$input->expects()->getOption('stop-process')->andReturns(null);
(new TraceLoopSettingsFromConsoleInput())->createSettings($input);
}

Expand All @@ -45,6 +48,7 @@ public function testFromConsoleInputSleepNsNotInteger(): void
$input = Mockery::mock(InputInterface::class);
$input->expects()->getOption('sleep-ns')->andReturns('abc');
$input->expects()->getOption('max-retries')->andReturns(null);
$input->expects()->getOption('stop-process')->andReturns(null);
$this->expectException(TraceLoopSettingsException::class);
(new TraceLoopSettingsFromConsoleInput())->createSettings($input);
}
Expand All @@ -54,6 +58,17 @@ public function testFromConsoleInputMaxRetriesNotInteger(): void
$input = Mockery::mock(InputInterface::class);
$input->expects()->getOption('sleep-ns')->andReturns(null);
$input->expects()->getOption('max-retries')->andReturns('abc');
$input->expects()->getOption('stop-process')->andReturns(null);
$this->expectException(TraceLoopSettingsException::class);
(new TraceLoopSettingsFromConsoleInput())->createSettings($input);
}

public function testFromConsoleInputStopProcessNotBoolean(): void
{
$input = Mockery::mock(InputInterface::class);
$input->expects()->getOption('sleep-ns')->andReturns(null);
$input->expects()->getOption('max-retries')->andReturns(null);
$input->expects()->getOption('stop-process')->andReturns('abc');
$this->expectException(TraceLoopSettingsException::class);
(new TraceLoopSettingsFromConsoleInput())->createSettings($input);
}
Expand Down

0 comments on commit 2c1a078

Please sign in to comment.