159 lines
6.4 KiB
PHP
159 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace MageSail\Magesail\Model;
|
|
|
|
use Magento\Framework\App\Config\ScopeConfigInterface;
|
|
use Magento\Framework\Shell;
|
|
use Magento\Framework\Event\ManagerInterface as EventManager;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* Start/stop tunnel processes per tunnel id; shared by admin Index and cron auto-restart.
|
|
*/
|
|
class TunnelManager
|
|
{
|
|
private const XML_SCRIPT_PATH = 'magesail/settings/script_path';
|
|
private const XML_NODE_BINARY = 'magesail/settings/node_binary';
|
|
private const DEFAULT_SCRIPT_REL = 'app/code/MageSail/Magesail/scripts';
|
|
|
|
public function __construct(
|
|
private readonly Shell $shell,
|
|
private readonly TunnelStatus $tunnelStatus,
|
|
private readonly ScopeConfigInterface $scopeConfig,
|
|
private readonly EventManager $eventManager,
|
|
private readonly TunnelRegistry $tunnelRegistry,
|
|
private readonly TunnelProcessPaths $tunnelProcessPaths,
|
|
private readonly LoggerInterface $logger
|
|
) {
|
|
}
|
|
|
|
public function getScriptDir(): string
|
|
{
|
|
$configured = trim((string) $this->scopeConfig->getValue(self::XML_SCRIPT_PATH));
|
|
if ($configured !== '') {
|
|
return $configured;
|
|
}
|
|
return BP . '/' . self::DEFAULT_SCRIPT_REL;
|
|
}
|
|
|
|
/**
|
|
* Executable for Node (config or "node" on PATH). Shell-escaped when embedded in commands.
|
|
*/
|
|
private function getNodeBinaryForShell(): string
|
|
{
|
|
$configured = trim((string) $this->scopeConfig->getValue(self::XML_NODE_BINARY));
|
|
if ($configured === '' || strpbrk($configured, "\r\n") !== false) {
|
|
return escapeshellarg('node');
|
|
}
|
|
return escapeshellarg($configured);
|
|
}
|
|
|
|
/**
|
|
* Starts the Node process for an existing registry tunnel (uses persisted port and secure flag).
|
|
*
|
|
* @return array{ok: bool, pid?: int, error?: string}
|
|
*/
|
|
public function start(string $tunnelId): array
|
|
{
|
|
$tunnel = $this->tunnelRegistry->getById($tunnelId);
|
|
if ($tunnel === null) {
|
|
return ['ok' => false, 'error' => 'unknown_tunnel'];
|
|
}
|
|
$port = (int) ($tunnel['local_port'] ?? 0);
|
|
$secure = (bool) ($tunnel['secure'] ?? false);
|
|
if ($this->tunnelStatus->isRunning($tunnelId)) {
|
|
return ['ok' => false, 'error' => 'already_running'];
|
|
}
|
|
if ($port < 1 || $port > 65535) {
|
|
return ['ok' => false, 'error' => 'invalid_port'];
|
|
}
|
|
$dir = $this->tunnelProcessPaths->getVarTunnelDir();
|
|
if (!\is_dir($dir) && !@mkdir($dir, 0775, true) && !\is_dir($dir)) {
|
|
$this->logger->warning('MageSail: could not create var tunnel directory.');
|
|
return ['ok' => false, 'error' => 'could_not_create_var_dir'];
|
|
}
|
|
$scriptDir = $this->getScriptDir();
|
|
$startScript = rtrim($scriptDir, '/') . '/start-server.js';
|
|
if (!\is_readable($startScript)) {
|
|
$this->logger->warning('MageSail: start-server.js missing or not readable at {path}', ['path' => $startScript]);
|
|
return ['ok' => false, 'error' => 'script_not_found', 'detail' => $startScript];
|
|
}
|
|
$nodeModules = rtrim($scriptDir, '/') . '/node_modules';
|
|
if (!\is_dir($nodeModules)) {
|
|
$this->logger->warning('MageSail: npm dependencies missing (no node_modules in scripts directory). Path: {path}', ['path' => $scriptDir]);
|
|
return ['ok' => false, 'error' => 'node_modules_missing', 'detail' => $scriptDir];
|
|
}
|
|
$keyFile = $this->tunnelProcessPaths->getKeyFilePath($tunnelId);
|
|
$logFile = $this->tunnelProcessPaths->getLogFilePath($tunnelId);
|
|
$pidFile = $this->tunnelProcessPaths->getPidFilePath($tunnelId);
|
|
$jsArgs = json_encode([
|
|
'port' => $port,
|
|
'secure' => $secure,
|
|
'keyfile' => $keyFile,
|
|
'logfile' => $logFile,
|
|
], JSON_THROW_ON_ERROR);
|
|
$arg = escapeshellarg($jsArgs);
|
|
$dirEscaped = escapeshellarg($scriptDir);
|
|
$log = escapeshellarg($logFile);
|
|
$nodeBin = $this->getNodeBinaryForShell();
|
|
$fullCmd = "cd $dirEscaped && nohup $nodeBin start-server.js $arg > $log 2>&1 & echo \$!";
|
|
try {
|
|
$output = $this->shell->execute($fullCmd);
|
|
$pid = (int) trim($output);
|
|
if ($pid > 0) {
|
|
file_put_contents($pidFile, (string) $pid);
|
|
$this->eventManager->dispatch('magesail_tunnel_start_after', [
|
|
'tunnel_id' => $tunnelId,
|
|
'store_code' => $tunnel['store_code'] ?? '',
|
|
'pid' => $pid,
|
|
'port' => $port,
|
|
'secure' => $secure,
|
|
]);
|
|
return ['ok' => true, 'pid' => $pid];
|
|
}
|
|
$this->logger->warning('MageSail: failed to capture PID after start. Output: {output}', ['output' => $output]);
|
|
return ['ok' => false, 'error' => 'pid_capture_failed', 'detail' => $output];
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error('MageSail: tunnel start exception: ' . $e->getMessage(), ['exception' => $e]);
|
|
return ['ok' => false, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array{ok: bool, error?: string}
|
|
*/
|
|
public function stop(string $tunnelId): array
|
|
{
|
|
$pidFile = $this->tunnelProcessPaths->getPidFilePath($tunnelId);
|
|
$keyFile = $this->tunnelProcessPaths->getKeyFilePath($tunnelId);
|
|
if (!\is_file($pidFile)) {
|
|
return ['ok' => false, 'error' => 'not_running'];
|
|
}
|
|
$pid = (int) trim((string) file_get_contents($pidFile));
|
|
if ($pid <= 0 || !$this->tunnelStatus->isProcessAlive($pid)) {
|
|
@unlink($pidFile);
|
|
if (\is_file($keyFile)) {
|
|
@unlink($keyFile);
|
|
}
|
|
return ['ok' => false, 'error' => 'not_running'];
|
|
}
|
|
try {
|
|
$this->shell->execute('kill %s', [$pid]);
|
|
@unlink($pidFile);
|
|
if (\is_file($keyFile)) {
|
|
@unlink($keyFile);
|
|
}
|
|
$this->eventManager->dispatch('magesail_tunnel_stop_after', [
|
|
'tunnel_id' => $tunnelId,
|
|
'pid' => $pid,
|
|
]);
|
|
return ['ok' => true];
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error('MageSail: tunnel stop exception: ' . $e->getMessage(), ['exception' => $e]);
|
|
return ['ok' => false, 'error' => $e->getMessage()];
|
|
}
|
|
}
|
|
}
|