Add built-in service targets for SSH on 127.0.0.1 (ports 22 and 2223) and a
CI / php (push) Successful in 1m1s

Custom Port option with ssh_custom_port validation in Admin.
List env.php-discovered services first; SSH shortcuts and Custom Port last.
Rename the custom option label to "Custom Port" and align tunnel labels/i18n.
Update tunnel UI (phtml/JS), EnvServicePortResolver, controller, tests, and docs.
This commit is contained in:
2026-04-01 20:13:11 -05:00
parent 0c769079aa
commit ff9447f0af
12 changed files with 222 additions and 29 deletions
+7
View File
@@ -98,6 +98,11 @@ class Tunnel extends Template
return $this->envServicePortResolver->getDiscoveredServices();
}
public function hasEnvDiscoveredServices(): bool
{
return $this->envServicePortResolver->hasEnvDiscoveredServices();
}
/**
* @return list<array{value: string, label: string}>
*/
@@ -259,6 +264,7 @@ class Tunnel extends Template
'groupsByWebsite' => \is_array($groups) ? $groups : [],
'defaultTab' => \count($this->listTunnelsFromRegistry()) === 0 ? 'add' : 'list',
'discoveredServices' => $this->envServicePortResolver->getDiscoveredServices(),
'sshCustomServiceKey' => EnvServicePortResolver::SERVICE_KEY_SSH_CUSTOM,
'translations' => [
'copied' => (string) __('Copied!'),
'copyFailed' => (string) __('Copy failed'),
@@ -288,6 +294,7 @@ class Tunnel extends Template
'selectWebsiteGroup' => (string) __('Select website and store group.'),
'enterHostname' => (string) __('Enter the tunnel hostname.'),
'invalidPort' => (string) __('Enter a valid local port (165535).'),
'sshPortRequired' => (string) __('Enter a valid SSH port (165535).'),
'runningPidTpl' => (string) __('Running (PID %1)', 'X'),
'clientLabel' => (string) __('Client:'),
],
@@ -318,6 +318,18 @@ class Index extends Action
return $this->_redirect('*/*/index');
}
$resolved = $this->envServicePortResolver->resolveService($serviceKey);
if ($resolved === null && $serviceKey === EnvServicePortResolver::SERVICE_KEY_SSH_CUSTOM) {
$customPort = (int) $this->getRequest()->getParam('ssh_custom_port', 0);
if ($customPort < 1 || $customPort > 65535) {
$msg = (string) __('Enter a valid SSH port (165535).');
if ($wantJson) {
return $this->jsonPayload(false, $msg);
}
$this->messageManager->addErrorMessage($msg);
return $this->_redirect('*/*/index');
}
$resolved = ['host' => '127.0.0.1', 'port' => $customPort];
}
if ($resolved === null) {
$msg = (string) __(
'That service is not available anymore. Refresh the page to reload the list from env.php.'
@@ -335,6 +347,9 @@ class Index extends Action
break;
}
}
if ($serviceKey === EnvServicePortResolver::SERVICE_KEY_SSH_CUSTOM) {
$serviceLabel = (string) __('Custom Port — 127.0.0.1:%1', $resolved['port']);
}
try {
$provision = $this->tunnelStoreProvisioner->provisionServiceTunnel(
+84 -15
View File
@@ -7,10 +7,17 @@ namespace MageSail\Magesail\Model;
use Magento\Framework\App\DeploymentConfig;
/**
* Discovers TCP endpoints from app/etc/env.php (via DeploymentConfig) for service-type Holesail tunnels.
* Discovers TCP endpoints from app/etc/env.php (via DeploymentConfig) for service-type Holesail tunnels,
* plus built-in SSH shortcuts to 127.0.0.1 (fixed ports or custom port from the Admin request).
*/
class EnvServicePortResolver
{
public const SERVICE_KEY_SSH_22 = 'magesail_ssh_22';
public const SERVICE_KEY_SSH_2223 = 'magesail_ssh_2223';
public const SERVICE_KEY_SSH_CUSTOM = 'magesail_ssh_custom';
private const MYSQL_DEFAULT_KEY = 'mysql_default';
private const SESSION_REDIS_KEY = 'session_redis';
@@ -22,10 +29,86 @@ class EnvServicePortResolver
) {
}
public static function isSshShortcutServiceKey(string $serviceKey): bool
{
return \in_array(
$serviceKey,
[self::SERVICE_KEY_SSH_22, self::SERVICE_KEY_SSH_2223, self::SERVICE_KEY_SSH_CUSTOM],
true
);
}
/**
* @return list<array{service_key: string, label: string, host: string, port: int}>
*/
public function getDiscoveredServices(): array
{
return array_merge($this->collectEnvDiscoveredServices(), $this->getSshShortcutDefinitions());
}
/**
* True when env.php yields at least one TCP endpoint (excluding built-in SSH shortcuts).
*/
public function hasEnvDiscoveredServices(): bool
{
return \count($this->collectEnvDiscoveredServices()) > 0;
}
/**
* @return list<array{service_key: string, label: string, host: string, port: int}>
*/
private function getSshShortcutDefinitions(): array
{
return [
[
'service_key' => self::SERVICE_KEY_SSH_22,
'label' => 'SSH: 22 (127.0.0.1)',
'host' => '127.0.0.1',
'port' => 22,
],
[
'service_key' => self::SERVICE_KEY_SSH_2223,
'label' => 'SSH: 2223 (127.0.0.1)',
'host' => '127.0.0.1',
'port' => 2223,
],
[
'service_key' => self::SERVICE_KEY_SSH_CUSTOM,
'label' => 'Custom Port',
'host' => '127.0.0.1',
'port' => 0,
],
];
}
/**
* @return array{host: string, port: int}|null
*/
public function resolveService(string $serviceKey): ?array
{
if ($serviceKey === self::SERVICE_KEY_SSH_22) {
return ['host' => '127.0.0.1', 'port' => 22];
}
if ($serviceKey === self::SERVICE_KEY_SSH_2223) {
return ['host' => '127.0.0.1', 'port' => 2223];
}
if ($serviceKey === self::SERVICE_KEY_SSH_CUSTOM) {
return null;
}
foreach ($this->collectEnvDiscoveredServices() as $row) {
if ($row['service_key'] === $serviceKey) {
return ['host' => $row['host'], 'port' => $row['port']];
}
}
return null;
}
/**
* @return list<array{service_key: string, label: string, host: string, port: int}>
*/
private function collectEnvDiscoveredServices(): array
{
$out = [];
foreach ($this->discoverMysql() as $row) {
@@ -50,20 +133,6 @@ class EnvServicePortResolver
return $out;
}
/**
* @return array{host: string, port: int}|null
*/
public function resolveService(string $serviceKey): ?array
{
foreach ($this->getDiscoveredServices() as $row) {
if ($row['service_key'] === $serviceKey) {
return ['host' => $row['host'], 'port' => $row['port']];
}
}
return null;
}
/**
* @return list<array{service_key: string, label: string, host: string, port: int}>
*/
@@ -102,4 +102,57 @@ class EnvServicePortResolverTest extends TestCase
]);
$this->assertNull($r->resolveService('does_not_exist'));
}
public function testSshShortcutsAreListedAndResolveExceptCustom(): void
{
$r = $this->resolverWithFlatConfig([]);
$keys = [];
foreach ($r->getDiscoveredServices() as $s) {
$keys[] = $s['service_key'];
}
$this->assertContains(EnvServicePortResolver::SERVICE_KEY_SSH_22, $keys);
$this->assertContains(EnvServicePortResolver::SERVICE_KEY_SSH_2223, $keys);
$this->assertContains(EnvServicePortResolver::SERVICE_KEY_SSH_CUSTOM, $keys);
$this->assertFalse($r->hasEnvDiscoveredServices());
$r22 = $r->resolveService(EnvServicePortResolver::SERVICE_KEY_SSH_22);
$this->assertSame(['host' => '127.0.0.1', 'port' => 22], $r22);
$r2223 = $r->resolveService(EnvServicePortResolver::SERVICE_KEY_SSH_2223);
$this->assertSame(['host' => '127.0.0.1', 'port' => 2223], $r2223);
$this->assertNull($r->resolveService(EnvServicePortResolver::SERVICE_KEY_SSH_CUSTOM));
}
public function testHasEnvDiscoveredServicesWhenMysqlPresent(): void
{
$r = $this->resolverWithFlatConfig([
'db/connection/default' => ['host' => '127.0.0.1'],
]);
$this->assertTrue($r->hasEnvDiscoveredServices());
}
public function testIsSshShortcutServiceKeyDetectsBuiltIns(): void
{
$this->assertTrue(EnvServicePortResolver::isSshShortcutServiceKey(EnvServicePortResolver::SERVICE_KEY_SSH_22));
$this->assertFalse(EnvServicePortResolver::isSshShortcutServiceKey('mysql_default'));
}
public function testSshShortcutsAreLastInDiscoveredListAfterEnvServices(): void
{
$r = $this->resolverWithFlatConfig([
'db/connection/default' => ['host' => '127.0.0.1'],
]);
$keys = array_column($r->getDiscoveredServices(), 'service_key');
$iMysql = array_search('mysql_default', $keys, true);
$i22 = array_search(EnvServicePortResolver::SERVICE_KEY_SSH_22, $keys, true);
$i2223 = array_search(EnvServicePortResolver::SERVICE_KEY_SSH_2223, $keys, true);
$iCustom = array_search(EnvServicePortResolver::SERVICE_KEY_SSH_CUSTOM, $keys, true);
$this->assertNotFalse($iMysql);
$this->assertNotFalse($i22);
$this->assertNotFalse($i2223);
$this->assertNotFalse($iCustom);
$this->assertLessThan($i22, $iMysql);
$this->assertLessThan($i2223, $i22);
$this->assertLessThan($iCustom, $i2223);
$this->assertSame(\count($keys) - 1, $iCustom);
}
}
+7
View File
@@ -27,3 +27,10 @@
"Error stopping tunnel: %1","Error stopping tunnel: %1"
"Holesail Tunnel","Holesail Tunnel"
"Configuration","Configuration"
"Enter a valid SSH port (165535).","Enter a valid SSH port (165535)."
"Custom Port — 127.0.0.1:%1","Custom Port — 127.0.0.1:%1"
"Custom Port","Custom Port"
"Forwards TCP to 127.0.0.1 on this port (e.g. alternate sshd).","Forwards TCP to 127.0.0.1 on this port (e.g. alternate sshd)."
"No TCP services were found in env.php. SSH presets (127.0.0.1) are still available; otherwise check db, cache, session, queue, and http_cache_hosts.","No TCP services were found in env.php. SSH presets (127.0.0.1) are still available; otherwise check db, cache, session, queue, and http_cache_hosts."
"Website: Magento store + hostname routing. Service: TCP to env.php endpoints or built-in SSH presets on 127.0.0.1 (no store view).","Website: Magento store + hostname routing. Service: TCP to env.php endpoints or built-in SSH presets on 127.0.0.1 (no store view)."
"Website tunnels use a dedicated store view (e.g. mgtun_…) and optional NGINX map. Service tunnels forward TCP to SSH on 127.0.0.1 (ports 22, 2223, or custom) and/or hosts and ports from env.php (MySQL, Redis, AMQP, etc.)—no store view.","Website tunnels use a dedicated store view (e.g. mgtun_…) and optional NGINX map. Service tunnels forward TCP to SSH on 127.0.0.1 (ports 22, 2223, or custom) and/or hosts and ports from env.php (MySQL, Redis, AMQP, etc.)—no store view."
1 Holesail Tunnel Management Holesail Tunnel Management
27 Error stopping tunnel: %1 Error stopping tunnel: %1
28 Holesail Tunnel Holesail Tunnel
29 Configuration Configuration
30 Enter a valid SSH port (1–65535). Enter a valid SSH port (1–65535).
31 Custom Port — 127.0.0.1:%1 Custom Port — 127.0.0.1:%1
32 Custom Port Custom Port
33 Forwards TCP to 127.0.0.1 on this port (e.g. alternate sshd). Forwards TCP to 127.0.0.1 on this port (e.g. alternate sshd).
34 No TCP services were found in env.php. SSH presets (127.0.0.1) are still available; otherwise check db, cache, session, queue, and http_cache_hosts. No TCP services were found in env.php. SSH presets (127.0.0.1) are still available; otherwise check db, cache, session, queue, and http_cache_hosts.
35 Website: Magento store + hostname routing. Service: TCP to env.php endpoints or built-in SSH presets on 127.0.0.1 (no store view). Website: Magento store + hostname routing. Service: TCP to env.php endpoints or built-in SSH presets on 127.0.0.1 (no store view).
36 Website tunnels use a dedicated store view (e.g. mgtun_…) and optional NGINX map. Service tunnels forward TCP to SSH on 127.0.0.1 (ports 22, 2223, or custom) and/or hosts and ports from env.php (MySQL, Redis, AMQP, etc.)—no store view. Website tunnels use a dedicated store view (e.g. mgtun_…) and optional NGINX map. Service tunnels forward TCP to SSH on 127.0.0.1 (ports 22, 2223, or custom) and/or hosts and ports from env.php (MySQL, Redis, AMQP, etc.)—no store view.
@@ -13,7 +13,7 @@ $nginxReloadHint = (string) __(
);
$defaultMagesailTab = \count($registry) === 0 ? 'add' : 'list';
$discoveredServices = $block->getDiscoveredServices();
$hasDiscoveredServices = \count($discoveredServices) > 0;
$hasEnvDiscoveredServices = $block->hasEnvDiscoveredServices();
?>
<div class="magesail-tunnel" id="magesail-root">
<div class="page-title-wrapper">
@@ -60,7 +60,7 @@ $hasDiscoveredServices = \count($discoveredServices) > 0;
<?php endif; ?>
<div class="message message-notice notice" style="margin-bottom:1rem">
<?= $block->escapeHtml(__('Website tunnels use a dedicated store view (e.g. mgtun_…) and optional NGINX map. Service tunnels forward TCP to hosts and ports from env.php (MySQL, Redis, AMQP, etc.)—no store view.')) ?>
<?= $block->escapeHtml(__('Website tunnels use a dedicated store view (e.g. mgtun_…) and optional NGINX map. Service tunnels forward TCP to SSH on 127.0.0.1 (ports 22, 2223, or custom) and/or hosts and ports from env.php (MySQL, Redis, AMQP, etc.)—no store view.')) ?>
</div>
<table class="data-grid admin__table-primary" id="magesail-tunnel-table">
@@ -230,7 +230,7 @@ $hasDiscoveredServices = \count($discoveredServices) > 0;
<option value="service"><?= $block->escapeHtml(__('Service')) ?></option>
</select>
</div>
<p class="admin__field-note"><?= $block->escapeHtml(__('Website: Magento store + hostname routing. Service: TCP to a port from env.php (no store view).')) ?></p>
<p class="admin__field-note"><?= $block->escapeHtml(__('Website: Magento store + hostname routing. Service: TCP to env.php endpoints or built-in SSH presets on 127.0.0.1 (no store view).')) ?></p>
</div>
<div id="magesail-add-website-fields">
<div class="admin__field">
@@ -309,19 +309,33 @@ $hasDiscoveredServices = \count($discoveredServices) > 0;
<div class="admin__field _required">
<label class="admin__field-label" for="magesail-service-key"><span><?= $block->escapeHtml(__('Service to expose')) ?></span></label>
<div class="admin__field-control">
<select id="magesail-service-key" class="admin__control-select"<?= $hasDiscoveredServices ? '' : ' disabled="disabled"' ?>>
<select id="magesail-service-key" class="admin__control-select">
<option value=""><?= $block->escapeHtml(__('— Select a service —')) ?></option>
<?php foreach ($discoveredServices as $svc): ?>
<option value="<?= $block->escapeHtmlAttr($svc['service_key']) ?>"><?= $block->escapeHtml($svc['label']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if (!$hasDiscoveredServices): ?>
<p class="admin__field-note message message-warning warning" id="magesail-no-services-msg">
<?= $block->escapeHtml(__('No TCP services were found in env.php. Check db, cache, session, queue, and http_cache_hosts.')) ?>
<?php if (!$hasEnvDiscoveredServices): ?>
<p class="admin__field-note message message-notice notice" id="magesail-no-services-msg">
<?= $block->escapeHtml(__('No TCP services were found in env.php. SSH presets (127.0.0.1) are still available; otherwise check db, cache, session, queue, and http_cache_hosts.')) ?>
</p>
<?php endif; ?>
</div>
<div id="magesail-ssh-custom-port-wrap" class="admin__field _required" hidden="hidden">
<label class="admin__field-label" for="magesail-ssh-custom-port"><span><?= $block->escapeHtml(__('Custom Port')) ?></span></label>
<div class="admin__field-control">
<input type="number"
id="magesail-ssh-custom-port"
class="admin__control-text"
min="1"
max="65535"
autocomplete="off"
placeholder="<?= $block->escapeHtmlAttr(__('Port')) ?>"
style="max-width:8rem;"/>
<p class="admin__field-note"><?= $block->escapeHtml(__('Forwards TCP to 127.0.0.1 on this port (e.g. alternate sshd).')) ?></p>
</div>
</div>
</div>
<div class="admin__field admin__field-option">
<input type="checkbox" id="magesail-tunnel-holesail-secure" class="admin__control-checkbox" <?= $defaultHolesail === '1' ? 'checked="checked"' : '' ?>/>
@@ -96,12 +96,27 @@ define(['jquery', 'MageSail_Magesail/js/magesail-clipboard', 'domReady!'], funct
});
var discoveredCount = (config.discoveredServices || []).length;
var sshCustomServiceKey = config.sshCustomServiceKey || '';
function toggleSshCustomPort() {
var $wrap = $('#magesail-ssh-custom-port-wrap');
if (!$wrap.length) {
return;
}
var isCustom = sshCustomServiceKey !== '' && $('#magesail-service-key').val() === sshCustomServiceKey;
if (isCustom) {
$wrap.removeAttr('hidden');
} else {
$wrap.attr('hidden', 'hidden');
}
}
function toggleAddTunnelType() {
var v = $('#magesail-tunnel-add-type').val();
if (v === 'service') {
$('#magesail-add-website-fields').attr('hidden', 'hidden');
$('#magesail-add-service-fields').removeAttr('hidden');
toggleSshCustomPort();
} else {
$('#magesail-add-website-fields').removeAttr('hidden');
$('#magesail-add-service-fields').attr('hidden', 'hidden');
@@ -119,7 +134,9 @@ define(['jquery', 'MageSail_Magesail/js/magesail-clipboard', 'domReady!'], funct
}
$('#magesail-tunnel-add-type').on('change', toggleAddTunnelType);
$('#magesail-service-key').on('change', toggleSshCustomPort);
toggleAddTunnelType();
toggleSshCustomPort();
var $busyOverlay = $('#magesail-busy-overlay');
@@ -435,6 +452,14 @@ define(['jquery', 'MageSail_Magesail/js/magesail-clipboard', 'domReady!'], funct
flash(t.selectService || '', true);
return;
}
if (sshCustomServiceKey !== '' && sk === sshCustomServiceKey) {
var sp = parseInt($('#magesail-ssh-custom-port').val(), 10);
if (!sp || sp < 1 || sp > 65535) {
btn.prop('disabled', false);
flash(t.sshPortRequired || '', true);
return;
}
}
postData = {
form_key: formKey,
action: 'start',
@@ -444,6 +469,9 @@ define(['jquery', 'MageSail_Magesail/js/magesail-clipboard', 'domReady!'], funct
service_key: sk,
tunnel_holesail_secure: hs
};
if (sshCustomServiceKey !== '' && sk === sshCustomServiceKey) {
postData.ssh_custom_port = String(parseInt($('#magesail-ssh-custom-port').val(), 10));
}
} else {
var baseUrl = ($('#magesail-tunnel-base-url').val() || '').trim();
var wid = $('#magesail-website').val();
+1 -1
View File
@@ -15,7 +15,7 @@ MageSail is a Magento 2 extension that integrates **[Holesail](https://holesail.
- **Secure tunneling** — Private (authenticated) vs public Holesail modes, mapped to **Secure Tunnel** Yes/No in Admin.
- **Admin lifecycle** — Create, start, stop, and remove tunnels under **MageSail → Holesail Tunnel → Manage Tunnel**; status polling and per-tunnel log tail via AJAX.
- **Website tunnels** — **Type: Website** under **Add tunnel**: each tunnel gets a **generated store code** (e.g. `mgtun_…`) on the **website / store group** you choose, with base URLs you supply. **Remove** tears down the store and NGINX map line; **Stop** only stops the Node process (with safety rules if that store is the website default on delete).
- **Service tunnels** — **Type: Service**: TCP forward to endpoints **discovered from env** via `DeploymentConfig` (no fixed service list). No store view, no NGINX map row; **Remove** drops the registry row and PID/key only. See [docs/admin-ui.md](docs/admin-ui.md).
- **Service tunnels** — **Type: Service**: TCP forward to **SSH on 127.0.0.1** (presets **22**, **2223**, or a **custom** port) and/or endpoints **discovered from env** via `DeploymentConfig`. No store view, no NGINX map row; **Remove** drops the registry row and PID/key only. See [docs/admin-ui.md](docs/admin-ui.md).
- **NGINX global map** — Optional append/remove of `map $http_host $MAGE_RUN_CODE` / `$MAGE_RUN_TYPE` lines for **website** tunnels only, plus `nginx -t` and reload (or wrapper script). Admin screen: **MageSail → Holesail Tunnel → NGINX Global Map**.
- **Storefront / admin routing** — For **website** tunnels, plugins detect HTTP traffic via header **`X-MageSail-Store-Code`**, query **`magesail_store`**, `___store`, or **hostname** vs persisted base URLs (**`HTTP_HOST`** plus **`X-Forwarded-Host` / `HTTP_X_FORWARDED_HOST`** when the client host differs from what PHP sees); adjust `MAGE_RUN_*`, backend front name resolution, generated base URLs, and **skip redirect-to-base** where appropriate. **Service** tunnels do not participate in Magento HTTP routing.
- **Tunnel storefront content** — Each new **website** tunnel store copies **CMS home / no-route / no-cookies / front** paths and **theme** from that websites **default** store view, and mirrors **`cms_page_store` / `cms_block_store`** so pages and blocks assigned to the default storefront appear on the tunnel (see [docs/tunnel-store-and-multistore.md](docs/tunnel-store-and-multistore.md)).
+2 -2
View File
@@ -18,7 +18,7 @@ The same URL shows two in-page tabs: **Tunnels list** (grid, log tails, start/st
- Lists all tunnels from [`TunnelRegistry`](../Magesail/Model/TunnelRegistry.php) (`var/magesail_tunnels.json`): **Type** (**Website** vs **Service**), label, **store code** (em dash for service tunnels), **local port**, host/target, run/stop state, Holesail key (when running), and actions.
- **Website** tunnels — dedicated store view, base URLs, optional NGINX global map (same as before).
- **Service** tunnels — TCP forward to an endpoint discovered from `app/etc/env.php` by [`EnvServicePortResolver`](../Magesail/Model/EnvServicePortResolver.php): MySQL (`db/connection/default`, default port **3306** if omitted), each Redis `cache/frontend/*` backend, session Redis when `session.save` is `redis`, `queue/amqp`, each `http_cache_hosts` entry, and optional Elasticsearch/OpenSearch under `catalog/search` when host and port are set. No store view and no NGINX map row. The Node script forwards to `service_target_host:local_port` (see [`start-server.js`](../Magesail/scripts/start-server.js) `forwardHost`). **Security:** exposing databases, Redis, or AMQP over Holesail is sensitive—use **Holesail secure tunnel**, restrict Admin ACL, and use normal client credentials from env (passwords are not shown in the Admin UI).
- **Service** tunnels — TCP forward to an endpoint from [`EnvServicePortResolver`](../Magesail/Model/EnvServicePortResolver.php): **built-in SSH** to **`127.0.0.1`** on port **22**, **2223**, or a **custom** port you enter; plus endpoints discovered from `app/etc/env.php` (MySQL `db/connection/default`, default port **3306** if omitted, each Redis `cache/frontend/*` backend, session Redis when `session.save` is `redis`, `queue/amqp`, each `http_cache_hosts` entry, and optional Elasticsearch/OpenSearch under `catalog/search` when host and port are set). No store view and no NGINX map row. The Node script forwards to `service_target_host:local_port` (see [`start-server.js`](../Magesail/scripts/start-server.js) `forwardHost`). **Security:** exposing SSH, databases, Redis, or AMQP over Holesail is sensitive—use **Holesail secure tunnel**, restrict Admin ACL, and use normal client credentials from env (passwords are not shown in the Admin UI).
- **Start** — starts the Node process for that tunnel only (uses saved port and Holesail secure flag). No re-provisioning.
- **Stop** — stops the Node process and removes that tunnels PID/key files; **registry row, store view, and NGINX map line stay** so you can start again.
- **Remove****Website**: stops the process if needed, removes the NGINX map entry, deletes the store view when safe, and removes the tunnel from the registry. **Service**: removes registry row and PID/key files only (no store).
@@ -28,7 +28,7 @@ The same URL shows two in-page tabs: **Tunnels list** (grid, log tails, start/st
**Add tunnel** (Add a tunnel tab — new tunnel)
- **Type****Website** (default): current Magento tunnel form. **Service**: choose one discovered TCP service (dropdown built from env); optional label; **Holesail secure tunnel** applies to both. If nothing is discoverable in env, the Service flow cannot be submitted.
- **Type****Website** (default): current Magento tunnel form. **Service**: choose one TCP target (env-discovered services first, then **SSH: 22**, **SSH: 2223**, and **Custom Port** last for **127.0.0.1**); optional label; **Holesail secure tunnel** applies to both. The Service dropdown always includes those SSH shortcuts even when env.php has no other endpoints.
- **Website** and **Store group** — required when Type is **Website**; the new dedicated store view is created under that scope.
- **Local port** — preset **443**, **80**, or **8080**, or **Custom…** with any port **165535**. Multiple tunnels may use the same port in configuration; whether more than one Node process can bind depends on the OS (see field note in Admin).
- **Tunnel hostname (unsecure)** — required; hostname only (e.g. `dev.myshop.local`); stored as `http://host/`. Must match the browser / Holesail client vhost.
+2 -2
View File
@@ -3,7 +3,7 @@
MageSail exposes a local Magento instance (or **TCP services** from env) through **Holesail** P2P tunnels. Admins create **multiple tunnels** from **MageSail → Holesail Tunnel → Manage Tunnel**, each with its own **local port** (presets 443 / 80 / 8080 or custom).
- **Website** tunnels — **Dedicated store view** (generated code, e.g. `mgtun_…`), base URLs, and optional **NGINX `map`** lines so tunneled hostnames resolve to the correct store without manual multistore edits.
- **Service** tunnels — Forward to backends **discovered from `app/etc/env.php`** via [`EnvServicePortResolver`](../Magesail/Model/EnvServicePortResolver.php); **no** store view and **no** NGINX map updates.
- **Service** tunnels — Forward TCP to **SSH on `127.0.0.1`** (ports **22**, **2223**, or **custom**) and/or backends **discovered from `app/etc/env.php`** via [`EnvServicePortResolver`](../Magesail/Model/EnvServicePortResolver.php); **no** store view and **no** NGINX map updates.
## Components
@@ -15,7 +15,7 @@ MageSail exposes a local Magento instance (or **TCP services** from env) through
1. Admin configures **MageSail → Holesail Tunnel → Configuration** (default secure mode, scripts path, Node binary, monitor cron schedule, auto-restart, NGINX paths).
2. Under **Manage Tunnel → Add tunnel**, Admin chooses **Type**:
- **Website** — website/group, port, hostnames, options; the module creates a **new store view**, may append **hostname → store code** to the configured **global map**, writes **`var/magesail_tunnels.json`**, starts Node.
- **Service** — selected `service_key` from env discovery; writes **registry** only (with resolved host/port), starts Node; **no** store, **no** map.
- **Service** — selected `service_key` (SSH shortcuts or env discovery); writes **registry** only (with resolved host/port), starts Node; **no** store, **no** map.
3. For **website** tunnels, incoming HTTP(S) requests can bind to the tunnel store via **header**, **query**, **`___store`**, **hostname match** against persisted base URLs in the registry (using **`HTTP_HOST`** and, when present, **`X-Forwarded-Host` / `HTTP_X_FORWARDED_HOST`**), or **NGINX `MAGE_RUN_*`** when the map is active. **Non-existent** store codes from nginx or bookmarks are ignored so Magento can fall back (see [`StoreFromMageSailHeaderPlugin`](../Magesail/Plugin/App/Request/StoreFromMageSailHeaderPlugin.php)). **Service** tunnels are plain TCP; Magento HTTP routing does not apply. New **website** tunnels copy **CMS + theme defaults** from the websites **default** store view and mirror **`cms_page_store` / `cms_block_store`** assignments so the tunnel storefront matches that websites main demo (see [tunnel-store-and-multistore.md](tunnel-store-and-multistore.md)).
4. Plugins adjust **admin front name resolution**, **store base URLs** for links, and **skip redirect-to-base** for **website** tunnel traffic so local/proxy URLs stay usable.
+1 -1
View File
@@ -27,7 +27,7 @@ Module namespace: **`MageSail\Magesail`**. Paths are relative to [`Magesail/`](.
| `TunnelStatus` | `Model/TunnelStatus.php` | PID/key/log paths per tunnel; process alive checks; `cleanupStaleStateForTunnel` (may call full `teardownTunnel` when auto-restart is off). |
| `TunnelStoreProvisioner` | `Model/TunnelStoreProvisioner.php` | Provision/teardown **website** tunnel store views; `provisionServiceTunnel` for **service** rows (registry only); scoped base URLs; optional **redirect-to-base** disable per config; after create, copies **web/default** CMS paths and **design/theme/theme_id** from the website **default** store and **mirrors** `cms_page_store` / `cms_block_store` rows (`ResourceConnection`); NGINX map updates for website only; `teardownTunnel` branches on `tunnel_type`. |
| `TunnelRegistry` | `Model/TunnelRegistry.php` | Read/write `var/magesail_tunnels.json`; migrate legacy `magesail_tunnel_store.json`; normalize `tunnel_type`, `service_*`; hostname lookup skips **service** rows. |
| `EnvServicePortResolver` | `Model/EnvServicePortResolver.php` | Discover TCP endpoints from `DeploymentConfig` / `env.php`; stable `service_key`; `resolveService()` for Admin start validation. |
| `EnvServicePortResolver` | `Model/EnvServicePortResolver.php` | Discover TCP endpoints from `DeploymentConfig` / `env.php` plus built-in **SSH** shortcuts (`magesail_ssh_22`, `magesail_ssh_2223`, `magesail_ssh_custom` — custom port supplied on create in [`Tunnel/Index`](../Magesail/Controller/Adminhtml/Tunnel/Index.php)); `hasEnvDiscoveredServices()`; `resolveService()` for Admin start validation. |
| `TunnelProcessPaths` | `Model/TunnelProcessPaths.php` | Paths for `var/magesail_tunnels/{id}.pid`, `.key`, and per-tunnel logs. |
| `TunnelStoreState` | `Model/TunnelStoreState.php` | Legacy read of `var/magesail_tunnel_store.json` (migration only). |
| `TunnelRequestDetector` | `Model/TunnelRequestDetector.php` | Resolve tunnel store code from request (query, header, URI, hostname); returns `null` if the code does not exist in `StoreRepository`. |
+1 -1
View File
@@ -8,7 +8,7 @@
| | `npm install` not run in `scripts/` | Run `npm install` in the configured **Scripts directory**. |
| **Invalid port** | Out of range or empty custom port | Use **165535**. In **Manage Tunnel**, pick preset **443**, **80**, **8080**, or **Custom** and enter a valid port. |
| **Tunnel hostname is required** | Empty unsecure field on start | For **Type: Website**, fill **Tunnel hostname (unsecure)** (e.g. `dev.myshop.local`) to match your browser/proxy host. |
| **No TCP services** / Service submit disabled | Nothing discoverable in `env.php` | **Type: Service** needs host+port in `db`, `cache/frontend`, `session/redis`, `queue/amqp`, `http_cache_hosts`, or search config as implemented in [`EnvServicePortResolver`](../Magesail/Model/EnvServicePortResolver.php). Fix or complete env; refresh Admin. |
| **No TCP services** in env | Nothing discoverable in `env.php` | **Type: Service** still offers **SSH** to `127.0.0.1` (ports 22, 2223, or custom). For MySQL/Redis/etc., add host+port in `db`, `cache/frontend`, `session/redis`, `queue/amqp`, `http_cache_hosts`, or search config as implemented in [`EnvServicePortResolver`](../Magesail/Model/EnvServicePortResolver.php); refresh Admin. |
| Unknown or invalid **service** | Stale `service_key` after env edit | Refresh **Manage Tunnel** and pick a service from the current list; env must still resolve the key at start time. |
## PID / process