Skip to content

Commit

Permalink
Merge pull request #164 from scoutapp/additional-core-agent-launch-lo…
Browse files Browse the repository at this point in the history
…gging

Additional core agent launch logging
  • Loading branch information
asgrim authored Feb 18, 2020
2 parents d96b3b2 + 4fb263e commit 8afdcbf
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 137 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file, in reverse

### Added

- Nothing.
- [#164](https://github.com/scoutapp/scout-apm-php/pull/164) Added additional logging and testing around core-agent launching

### Changed

Expand Down
18 changes: 16 additions & 2 deletions src/Agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
use Scoutapm\Connector\SocketConnector;
use Scoutapm\CoreAgent\AutomaticDownloadAndLaunchManager;
use Scoutapm\CoreAgent\Downloader;
use Scoutapm\CoreAgent\Launcher;
use Scoutapm\CoreAgent\Verifier;
use Scoutapm\Events\Metadata;
use Scoutapm\Events\RegisterMessage;
use Scoutapm\Events\Request\Request;
Expand Down Expand Up @@ -190,15 +192,27 @@ public function connect() : void
$this->config->get(ConfigKey::APPLICATION_NAME),
$this->extensionVersion()
));
$manager = new AutomaticDownloadAndLaunchManager(
$coreAgentDownloadPath = $this->config->get(ConfigKey::CORE_AGENT_DIRECTORY) . '/' . $this->config->get(ConfigKey::CORE_AGENT_FULL_NAME);
$manager = new AutomaticDownloadAndLaunchManager(
$this->config,
$this->logger,
new Downloader(
$this->config->get(ConfigKey::CORE_AGENT_DIRECTORY) . '/' . $this->config->get(ConfigKey::CORE_AGENT_FULL_NAME),
$coreAgentDownloadPath,
$this->config->get(ConfigKey::CORE_AGENT_FULL_NAME),
$this->logger,
$this->config->get(ConfigKey::CORE_AGENT_DOWNLOAD_URL),
$this->config->get(ConfigKey::CORE_AGENT_PERMISSIONS)
),
new Launcher(
$this->logger,
$this->config->get(ConfigKey::CORE_AGENT_SOCKET_PATH),
$this->config->get(ConfigKey::CORE_AGENT_LOG_LEVEL),
$this->config->get(ConfigKey::CORE_AGENT_LOG_FILE),
$this->config->get(ConfigKey::CORE_AGENT_CONFIG_FILE)
),
new Verifier(
$this->logger,
$coreAgentDownloadPath
)
);
$manager->launch();
Expand Down
129 changes: 22 additions & 107 deletions src/CoreAgent/AutomaticDownloadAndLaunchManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,35 @@
use Psr\Log\LoggerInterface;
use Scoutapm\Config;
use Scoutapm\Config\ConfigKey;
use Throwable;
use function array_map;
use function exec;
use function file_get_contents;
use function hash;
use function implode;
use function sprintf;

/** @internal */
final class AutomaticDownloadAndLaunchManager implements Manager
{
/** @var Config */
private $config;

/** @var LoggerInterface */
private $logger;

/** @var string */
private $coreAgentDir;

/** @var Downloader */
private $downloader;

/** @var string|null */
private $coreAgentBinPath;

public function __construct(Config $config, LoggerInterface $logger, Downloader $downloader)
{
$this->config = $config;
$this->logger = $logger;
$this->coreAgentDir = $config->get(ConfigKey::CORE_AGENT_DIRECTORY) . '/' . $config->get(ConfigKey::CORE_AGENT_FULL_NAME);
/** @var Launcher */
private $launcher;
/** @var Verifier */
private $verifier;

public function __construct(
Config $config,
LoggerInterface $logger,
Downloader $downloader,
Launcher $launcher,
Verifier $verifier
) {
$this->config = $config;
$this->logger = $logger;

$this->downloader = $downloader;
$this->launcher = $launcher;
$this->verifier = $verifier;
}

public function launch() : bool
Expand All @@ -53,7 +49,8 @@ public function launch() : bool
return false;
}

if (! $this->verify()) {
$coreAgentBinPath = $this->verifier->verify();
if ($coreAgentBinPath === null) {
if (! $this->config->get(ConfigKey::CORE_AGENT_DOWNLOAD_ENABLED)) {
$this->logger->debug(sprintf(
"Not attempting to download Core Agent due to '%s' setting.",
Expand All @@ -63,100 +60,18 @@ public function launch() : bool
return false;
}

$this->download();
$this->downloader->download();
}

if (! $this->verify()) {
$coreAgentBinPath = $this->verifier->verify();
if ($coreAgentBinPath === null) {
$this->logger->debug(
'Failed to verify Core Agent. Not launching Core Agent.'
);

return false;
}

return $this->run();
}

/**
* Initiate download of the agent
*/
private function download() : void
{
$this->downloader->download();
}

private function verify() : bool
{
// Check for a well formed manifest
$manifest = new Manifest($this->coreAgentDir . '/manifest.json', $this->logger);
if (! $manifest->isValid()) {
$this->logger->debug('Core Agent verification failed: Manifest is not valid.');
$this->coreAgentBinPath = null;

return false;
}

// Check that the hash matches
$binPath = $this->coreAgentDir . '/' . $manifest->binaryName();
if (hash('sha256', file_get_contents($binPath)) === $manifest->hashOfBinary()) {
$this->coreAgentBinPath = $binPath;

return true;
}

$this->logger->debug('Core Agent verification failed: SHA mismatch.');
$this->coreAgentBinPath = null;

return false;
}

private function run() : bool
{
$this->logger->debug('Core Agent Launch in Progress');
try {
$logLevel = $this->config->get(ConfigKey::CORE_AGENT_LOG_LEVEL);
$logFile = $this->config->get(ConfigKey::CORE_AGENT_LOG_FILE);
$configFile = $this->config->get(ConfigKey::CORE_AGENT_CONFIG_FILE);

if ($logFile === null) {
$logFile = '/dev/null';
}

$commandParts = [
$this->coreAgentBinPath,
'start',
'--daemonize',
'true',
'--log-file',
$logFile,
];

if ($logLevel !== null) {
$commandParts[] = '--log-level';
$commandParts[] = $logLevel;
}

if ($configFile !== null) {
$commandParts[] = '--config-file';
$commandParts[] = $configFile;
}

$commandParts[] = '--socket';
$commandParts[] = $this->config->get(ConfigKey::CORE_AGENT_SOCKET_PATH);

$escapedCommand = implode(' ', array_map('escapeshellarg', $commandParts));

$this->logger->debug(sprintf('Launching core agent with command: %s', $escapedCommand));
exec($escapedCommand);

return true;
} catch (Throwable $e) {
$this->logger->debug(
sprintf('Failed to launch core agent - exception %s', $e->getMessage()),
['exception' => $e]
);

return false;
}
return $this->launcher->launch($coreAgentBinPath);
}
}
132 changes: 132 additions & 0 deletions src/CoreAgent/Launcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

namespace Scoutapm\CoreAgent;

use Psr\Log\LoggerInterface;
use RuntimeException;
use Throwable;
use function array_map;
use function exec;
use function explode;
use function function_exists;
use function implode;
use function in_array;
use function ini_get;
use function sprintf;
use function stripos;

/** @internal */
class Launcher
{
/** @var LoggerInterface */
private $logger;
/** @var string */
private $coreAgentSocketPath;
/** @var string|null */
private $coreAgentLogLevel;
/** @var string */
private $coreAgentLogFile;
/** @var string|null */
private $coreAgentConfigFile;

public function __construct(
LoggerInterface $logger,
string $coreAgentSocketPath,
?string $coreAgentLogLevel,
?string $coreAgentLogFile,
?string $coreAgentConfigFile
) {
$this->logger = $logger;
$this->coreAgentSocketPath = $coreAgentSocketPath;
$this->coreAgentLogLevel = $coreAgentLogLevel;
$this->coreAgentConfigFile = $coreAgentConfigFile;
$this->coreAgentLogFile = $coreAgentLogFile ?? '/dev/null';
}

public function launch(string $coreAgentBinaryPath) : bool
{
if (! $this->phpCanExec()) {
return false;
}

$this->logger->debug('Core Agent Launch in Progress');
try {
$commandParts = [
$coreAgentBinaryPath,
'start',
'--daemonize',
'true',
'--log-file',
$this->coreAgentLogFile,
];

if ($this->coreAgentLogLevel !== null) {
$commandParts[] = '--log-level';
$commandParts[] = $this->coreAgentLogLevel;
}

if ($this->coreAgentConfigFile !== null) {
$commandParts[] = '--config-file';
$commandParts[] = $this->coreAgentConfigFile;
}

$commandParts[] = '--socket';
$commandParts[] = $this->coreAgentSocketPath;

$escapedCommand = implode(' ', array_map('escapeshellarg', $commandParts));

$this->logger->debug(sprintf('Launching core agent with command: %s', $escapedCommand));

exec($escapedCommand . ' 2>&1', $output, $exitStatus);

$this->assertOutputDoesNotContainErrors(implode("\n", $output), $exitStatus);

return true;
} catch (Throwable $e) {
$this->logger->debug(
sprintf('Failed to launch core agent - exception %s', $e->getMessage()),
['exception' => $e]
);

return false;
}
}

private function phpCanExec() : bool
{
if (! function_exists('exec')) {
$this->logger->warning('PHP function exec is not available');

return false;
}

if (in_array('exec', array_map('trim', explode(',', ini_get('disable_functions'))))) {
$this->logger->warning('PHP function exec is in disabled_functions');

return false;
}

if (exec('echo scoutapm') !== 'scoutapm') {
$this->logger->warning('PHP function exec did not return expected value');

return false;
}

$this->logger->debug('exec is available');

return true;
}

private function assertOutputDoesNotContainErrors(string $output, int $exitStatus) : void
{
if (stripos($output, "version `GLIBC_2.18' not found") !== false) {
throw new RuntimeException('core-agent currently needs at least glibc 2.18. Output: ' . $output);
}

if ($exitStatus !== 0) {
throw new RuntimeException('core-agent exited with non-zero status. Output: ' . $output);
}
}
}
46 changes: 46 additions & 0 deletions src/CoreAgent/Verifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Scoutapm\CoreAgent;

use Psr\Log\LoggerInterface;
use function hash_equals;
use function hash_file;

/** @internal */
class Verifier
{
/** @var LoggerInterface */
private $logger;

/** @var string */
private $coreAgentDownloadPath;

public function __construct(LoggerInterface $logger, string $coreAgentDownloadPath)
{
$this->logger = $logger;
$this->coreAgentDownloadPath = $coreAgentDownloadPath;
}

public function verify() : ?string
{
// Check for a well formed manifest
$manifest = new Manifest($this->coreAgentDownloadPath . '/manifest.json', $this->logger);
if (! $manifest->isValid()) {
$this->logger->debug('Core Agent verification failed: Manifest is not valid.');

return null;
}

// Check that the hash matches
$binPath = $this->coreAgentDownloadPath . '/' . $manifest->binaryName();
if (hash_equals($manifest->hashOfBinary(), hash_file('sha256', $binPath))) {
return $binPath;
}

$this->logger->debug('Core Agent verification failed: SHA mismatch.');

return null;
}
}
Loading

0 comments on commit 8afdcbf

Please sign in to comment.