Files
MageSail/Magesail/Controller/Adminhtml/Tunnel/Index.php
T
snxraven cc456a4cfa feat(magesail): multi-tunnel registry, per-tunnel stores, and admin UX
- Add TunnelRegistry (var/magesail_tunnels.json) and TunnelProcessPaths;
  migrate legacy magesail_tunnel_store.json and PID/key files
- Provision unique store codes per tunnel; website/group selection;
  stop vs remove; per-tunnel NGINX map, cron, status/logtail
- Fix log path: use DirectoryList::LOG (not LOG_DIR)
- Ignore missing store codes for MAGE_RUN_* (stale nginx / magesail_tunnel)
- Allow duplicate local ports; Admin port presets 443/80/8080 + custom;
  update docs and system.xml comments
2026-03-21 00:49:07 -05:00

298 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace MageSail\Magesail\Controller\Adminhtml\Tunnel;
use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Exception\LocalizedException;
use MageSail\Magesail\Model\MagesailLog;
use MageSail\Magesail\Model\TunnelManager;
use MageSail\Magesail\Model\TunnelStatus;
use MageSail\Magesail\Model\TunnelStoreProvisioner;
class Index extends Action
{
public function __construct(
Context $context,
private readonly ScopeConfigInterface $scopeConfig,
private readonly TunnelManager $tunnelManager,
private readonly TunnelStatus $tunnelStatus,
private readonly MagesailLog $magesailLog,
private readonly TunnelStoreProvisioner $tunnelStoreProvisioner
) {
parent::__construct($context);
}
public function execute()
{
$action = $this->getRequest()->getParam('action');
$wantJson = $this->isTunnelAjaxRequest();
if ($action === 'start') {
return $this->handleStart($wantJson);
}
if ($action === 'stop') {
return $this->handleStop($wantJson);
}
if ($action === 'delete') {
return $this->handleDelete($wantJson);
}
$resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE);
$resultPage->setActiveMenu('MageSail_Magesail::tunnel');
$resultPage->getConfig()->getTitle()->prepend(__('Holesail Tunnel'));
return $resultPage;
}
private function isTunnelAjaxRequest(): bool
{
return strtolower((string) $this->getRequest()->getHeader('X-Requested-With')) === 'xmlhttprequest'
|| (string) $this->getRequest()->getParam('ajax') === '1';
}
private function jsonPayload(bool $ok, string $message = '', array $extra = [])
{
$json = $this->resultFactory->create(ResultFactory::TYPE_JSON);
$json->setData(array_merge([
'success' => $ok,
'message' => $message,
'tunnels' => $this->tunnelStatus->getAllTunnelStatuses(),
], $extra));
return $json;
}
private function handleStart(bool $wantJson)
{
$tunnelId = trim((string) $this->getRequest()->getParam('tunnel_id', ''));
if ($tunnelId !== '') {
return $this->handleStartExisting($tunnelId, $wantJson);
}
return $this->handleStartNew($wantJson);
}
private function handleStartExisting(string $tunnelId, bool $wantJson)
{
if ($this->tunnelStatus->isRunning($tunnelId)) {
if ($wantJson) {
return $this->jsonPayload(false, (string) __('This tunnel is already running.'));
}
$this->messageManager->addErrorMessage(__('This tunnel is already running.'));
return $this->_redirect('*/*/index');
}
$result = $this->tunnelManager->start($tunnelId);
if ($result['ok']) {
$this->magesailLog->add(sprintf('Tunnel started %s PID %s', $tunnelId, $result['pid'] ?? ''));
$msg = (string) __(
'Tunnel started (PID: %1). Key will appear once ready.',
$result['pid'] ?? ''
);
if ($wantJson) {
return $this->jsonPayload(true, $msg);
}
$this->messageManager->addSuccessMessage($msg);
return $this->_redirect('*/*/index');
}
$msg = match ($result['error'] ?? '') {
'unknown_tunnel' => (string) __('Unknown tunnel.'),
'already_running' => (string) __('Tunnel is already running.'),
'invalid_port' => (string) __('Invalid local port for this tunnel.'),
'could_not_create_var_dir' => (string) __('Could not create var directory for tunnel state.'),
'pid_capture_failed' => (string) __('Failed to capture PID: %1', $result['detail'] ?? ''),
default => (string) __('Error starting tunnel: %1', $result['error'] ?? 'unknown'),
};
$this->magesailLog->add('Start failed: ' . ($result['error'] ?? 'unknown'));
if ($wantJson) {
return $this->jsonPayload(false, $msg);
}
$this->messageManager->addErrorMessage($msg);
return $this->_redirect('*/*/index');
}
private function handleStartNew(bool $wantJson)
{
$label = trim((string) $this->getRequest()->getParam('tunnel_label', ''));
$websiteId = (int) $this->getRequest()->getParam('website_id', 0);
$groupId = (int) $this->getRequest()->getParam('group_id', 0);
$tunnelBaseUrl = trim((string) $this->getRequest()->getParam('tunnel_base_url', ''));
$tunnelSecureBaseUrl = trim((string) $this->getRequest()->getParam('tunnel_secure_base_url', ''));
$useSecureUrls = $this->isTruthyParam($this->getRequest()->getParam('tunnel_use_secure_urls'));
$localPort = filter_var($this->getRequest()->getParam('local_port'), FILTER_VALIDATE_INT);
$holesailSecureParam = $this->getRequest()->getParam('tunnel_holesail_secure');
$secureHolesail = $holesailSecureParam === null
? (bool) $this->scopeConfig->getValue('magesail/settings/mode')
: $this->isTruthyParam($holesailSecureParam);
if ($websiteId <= 0 || $groupId <= 0) {
$msg = (string) __('Select a website and store group.');
if ($wantJson) {
return $this->jsonPayload(false, $msg);
}
$this->messageManager->addErrorMessage($msg);
return $this->_redirect('*/*/index');
}
if ($tunnelBaseUrl === '') {
$msg = (string) __('Tunnel hostname is required (e.g. dev.myshop.local — must match your holesail-browser virtual host).');
if ($wantJson) {
return $this->jsonPayload(false, $msg);
}
$this->messageManager->addErrorMessage($msg);
return $this->_redirect('*/*/index');
}
if (!$localPort || $localPort < 1 || $localPort > 65535) {
$msg = (string) __('Invalid local port (165535).');
if ($wantJson) {
return $this->jsonPayload(false, $msg);
}
$this->messageManager->addErrorMessage($msg);
return $this->_redirect('*/*/index');
}
try {
$provision = $this->tunnelStoreProvisioner->provisionNewTunnel(
$label,
$websiteId,
$groupId,
$tunnelBaseUrl,
$tunnelSecureBaseUrl,
$useSecureUrls,
(int) $localPort,
$secureHolesail
);
} catch (LocalizedException $e) {
$this->magesailLog->add('Tunnel store provision failed: ' . $e->getMessage());
if ($wantJson) {
return $this->jsonPayload(false, (string) $e->getMessage());
}
$this->messageManager->addErrorMessage($e->getMessage());
return $this->_redirect('*/*/index');
}
$newTunnelId = $provision['tunnel_id'];
$result = $this->tunnelManager->start($newTunnelId);
if (!$result['ok']) {
$this->tunnelStoreProvisioner->teardownTunnel($newTunnelId);
}
if ($result['ok']) {
$this->magesailLog->add(sprintf(
'Tunnel started %s PID %s port %s',
$newTunnelId,
$result['pid'] ?? '',
$localPort
));
$msg = (string) __(
'Tunnel started (PID: %1). Key will appear once ready.',
$result['pid'] ?? ''
);
$nginxReloadMsg = '';
if (empty($provision['nginx_reloaded'])) {
$nginxReloadMsg = (string) __(
'The NGINX global map was updated, but Magento could not reload nginx (common when the PHP user cannot signal the nginx master). ' .
'On the server run: sudo systemctl reload nginx, or set Reload wrapper script in Admin under Configuration for the Holesail Tunnel NGINX settings. ' .
'Until nginx reloads, your tunnel hostname may not route to the tunnel store.'
);
}
if ($wantJson) {
$extra = [];
if ($nginxReloadMsg !== '') {
$extra['nginx_reload_required'] = true;
$extra['nginx_reload_message'] = $nginxReloadMsg;
}
return $this->jsonPayload(true, $msg, $extra);
}
$this->messageManager->addSuccessMessage($msg);
if ($nginxReloadMsg !== '') {
$this->messageManager->addWarningMessage($nginxReloadMsg);
}
return $this->_redirect('*/*/index');
}
$msg = match ($result['error'] ?? '') {
'already_running' => (string) __('Tunnel is already running.'),
'invalid_port' => (string) __('Invalid local port.'),
'could_not_create_var_dir' => (string) __('Could not create var directory for tunnel state.'),
'pid_capture_failed' => (string) __('Failed to capture PID: %1', $result['detail'] ?? ''),
default => (string) __('Error starting tunnel: %1', $result['error'] ?? 'unknown'),
};
$this->magesailLog->add('Start failed: ' . ($result['error'] ?? 'unknown'));
if ($wantJson) {
return $this->jsonPayload(false, $msg);
}
$this->messageManager->addErrorMessage($msg);
return $this->_redirect('*/*/index');
}
private function handleStop(bool $wantJson)
{
$tunnelId = trim((string) $this->getRequest()->getParam('tunnel_id', ''));
if ($tunnelId === '') {
if ($wantJson) {
return $this->jsonPayload(false, (string) __('Missing tunnel id.'));
}
$this->messageManager->addErrorMessage(__('Missing tunnel id.'));
return $this->_redirect('*/*/index');
}
$stop = $this->tunnelManager->stop($tunnelId);
if ($stop['ok']) {
$this->magesailLog->add(sprintf('Tunnel stopped %s', $tunnelId));
if ($wantJson) {
return $this->jsonPayload(true, (string) __('Tunnel stopped.'), [
'message' => (string) __('Tunnel stopped.'),
]);
}
$this->messageManager->addSuccessMessage(__('Tunnel stopped.'));
} else {
if (($stop['error'] ?? '') === 'not_running') {
if ($wantJson) {
return $this->jsonPayload(true, (string) __('No process running for this tunnel.'));
}
$this->messageManager->addNoticeMessage(__('No process running for this tunnel.'));
} else {
if ($wantJson) {
return $this->jsonPayload(false, (string) __('Error stopping tunnel: %1', $stop['error'] ?? ''));
}
$this->messageManager->addErrorMessage(__('Error stopping tunnel: %1', $stop['error'] ?? ''));
}
}
return $wantJson ? $this->jsonPayload($stop['ok'], '') : $this->_redirect('*/*/index');
}
private function handleDelete(bool $wantJson)
{
$tunnelId = trim((string) $this->getRequest()->getParam('tunnel_id', ''));
if ($tunnelId === '') {
if ($wantJson) {
return $this->jsonPayload(false, (string) __('Missing tunnel id.'));
}
$this->messageManager->addErrorMessage(__('Missing tunnel id.'));
return $this->_redirect('*/*/index');
}
$this->tunnelManager->stop($tunnelId);
$this->tunnelStoreProvisioner->teardownTunnel($tunnelId);
$this->magesailLog->add(sprintf('Tunnel removed %s', $tunnelId));
if ($wantJson) {
return $this->jsonPayload(true, (string) __('Tunnel removed.'));
}
$this->messageManager->addSuccessMessage(__('Tunnel removed.'));
return $this->_redirect('*/*/index');
}
private function isTruthyParam(mixed $value): bool
{
if ($value === true || $value === 1) {
return true;
}
if ($value === false || $value === null || $value === '') {
return false;
}
$s = strtolower(trim((string) $value));
return \in_array($s, ['1', 'true', 'on', 'yes'], true);
}
protected function _isAllowed()
{
return $this->_authorization->isAllowed('MageSail_Magesail::tunnel');
}
}