112 lines
4.2 KiB
PHP
112 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Box\Mod\Mailgateway;
|
|
|
|
use FOSSBilling\InformationException;
|
|
|
|
class DependencyInstaller
|
|
{
|
|
public function status(): array
|
|
{
|
|
return ['installed' => $this->installed(), 'available' => function_exists('proc_open')
|
|
&& $this->php() !== null && $this->composer() !== null && is_writable(__DIR__),
|
|
'request_timeout_ms' => (RuntimeConfig::get('composer_timeout_seconds') + 30) * 1000];
|
|
}
|
|
|
|
public function install(bool $force = false): array
|
|
{
|
|
if (!$force && $this->installed()) {
|
|
return ['installed' => true];
|
|
}
|
|
$php = $this->php();
|
|
$composer = $this->composer();
|
|
if (!function_exists('proc_open') || $php === null || $composer === null || !is_writable(__DIR__)) {
|
|
throw new InformationException('Web installation is unavailable. Run composer install from the Mailgateway directory using SSH.');
|
|
}
|
|
if (!is_file(__DIR__ . '/composer.lock')) {
|
|
throw new InformationException('The module composer.lock is missing. Restore it before installing dependencies.');
|
|
}
|
|
$command = [$php, $composer, 'install', '--no-dev', '--prefer-dist', '--no-interaction',
|
|
'--no-progress', '--no-scripts', '--no-plugins', '--optimize-autoloader'];
|
|
$result = $this->run($command);
|
|
if ($result['timeout']) {
|
|
throw new InformationException('Dependency installation exceeded its configured timeout. Run composer install from the Mailgateway directory using SSH.');
|
|
}
|
|
if ($result['exit'] !== 0 || !$this->installed()) {
|
|
throw new InformationException('Composer installation failed. Run composer install from the Mailgateway directory using SSH to see the detailed error.');
|
|
}
|
|
return ['installed' => true];
|
|
}
|
|
|
|
private function installed(): bool
|
|
{
|
|
return is_file(__DIR__ . '/vendor/autoload.php')
|
|
&& is_file(__DIR__ . '/vendor/webklex/php-imap/src/ClientManager.php');
|
|
}
|
|
|
|
private function php(): ?string
|
|
{
|
|
foreach (RuntimeConfig::get('php_cli_paths') as $path) {
|
|
if (is_file($path) && is_executable($path)) {
|
|
return $path;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function composer(): ?string
|
|
{
|
|
foreach (RuntimeConfig::get('composer_paths') as $path) {
|
|
if (is_file($path) && is_readable($path)) {
|
|
return $path;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
protected function run(array $command): array
|
|
{
|
|
// PHP may terminate a web request before Composer completes otherwise.
|
|
@set_time_limit(RuntimeConfig::get('composer_timeout_seconds') + 15);
|
|
$process = proc_open($command, [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], $pipes, __DIR__);
|
|
if (!is_resource($process)) {
|
|
throw new InformationException('Composer could not be started. Run composer install using SSH.');
|
|
}
|
|
fclose($pipes[0]);
|
|
stream_set_blocking($pipes[1], false);
|
|
stream_set_blocking($pipes[2], false);
|
|
$deadline = microtime(true) + RuntimeConfig::get('composer_timeout_seconds');
|
|
$exit = -1;
|
|
$timeout = false;
|
|
try {
|
|
while (true) {
|
|
// Drain both pipes to avoid deadlock. Never expose Composer output:
|
|
// repository authentication and environment details can appear there.
|
|
stream_get_contents($pipes[1]);
|
|
stream_get_contents($pipes[2]);
|
|
$status = proc_get_status($process);
|
|
if (!$status['running']) {
|
|
$exit = $status['exitcode'];
|
|
break;
|
|
}
|
|
if (microtime(true) >= $deadline) {
|
|
$timeout = true;
|
|
proc_terminate($process, 9);
|
|
break;
|
|
}
|
|
usleep(100000);
|
|
}
|
|
} finally {
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
$closed = proc_close($process);
|
|
if ($exit === -1) {
|
|
$exit = $closed;
|
|
}
|
|
}
|
|
return ['exit' => $exit, 'timeout' => $timeout];
|
|
}
|
|
}
|