scopeConfig->getValue(self::XML_SCRIPT_PATH)); if ($configured !== '') { return $configured; } return BP . '/' . self::DEFAULT_SCRIPT_REL; } /** * 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); $fullCmd = "cd $dirEscaped && nohup node 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()]; } } }