Files
MageSail/Magesail/Model/TunnelManager.php
T
snxraven ce2c11af59 ocs(admin): expand How-To and About pages
- How-To: recommend Holesail Browser; NGINX/Apache/NPM/cURL examples
- Add magesail-howto.css and layout head include
- About: module overview, capabilities, docs pointer; LIVEPORT copyright
- Add getCopyrightNotice(), magesail-about.css; update admin-ui.md
2026-03-21 02:21:25 -05:00

142 lines
5.2 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;
/**
* 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
) {
}
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)) {
return ['ok' => false, 'error' => 'could_not_create_var_dir'];
}
$scriptDir = $this->getScriptDir();
$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];
}
return ['ok' => false, 'error' => 'pid_capture_failed', 'detail' => $output];
} catch (\Throwable $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) {
return ['ok' => false, 'error' => $e->getMessage()];
}
}
}