From f9b2d34400db2c6f782b820fd0f4da16298e25fb Mon Sep 17 00:00:00 2001 From: Marko Petzold Date: Sun, 1 Nov 2026 12:44:36 +0100 Subject: [PATCH 1/2] refactor: update port handling logic for development runs and improve test descriptions --- src/apps/app_manager.go | 6 +----- src/apps/dev_tunnels_test.go | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/apps/app_manager.go b/src/apps/app_manager.go index b008cb8..55b7290 100644 --- a/src/apps/app_manager.go +++ b/src/apps/app_manager.go @@ -274,10 +274,6 @@ func (am *AppManager) syncPortState(payload common.TransitionPayload, app *commo dialPort, dialPortKnown, reservationFailure = am.resolveTunnelHostPort(payload, portRule) } - // A development run tunnels every port of its template while it runs. - // Its tcp/udp ports have no switch: their tunnels come and go with the - // run, so a stale Active flag on such a rule does not hold them off. - active := portRule.Active || (payload.Stage == common.DEV && !isWebProtocol(portRule.Protocol)) // A DEV label is four characters longer than the PROD one, and an app // name of 40 characters can push it past what a DNS label may hold: // such a tunnel could never be reached, so it is not opened. Only an @@ -295,7 +291,7 @@ func (am *AppManager) syncPortState(payload common.TransitionPayload, app *commo // Said again at the next run. am.forgetLabelTooLong(tunnelID) } - } else if active { + } else if portRule.Active { if tunnelsAvailable && appRunning { if !dialPortKnown { // Fresh app that has not been started under managed ports diff --git a/src/apps/dev_tunnels_test.go b/src/apps/dev_tunnels_test.go index e7c0337..c5be447 100644 --- a/src/apps/dev_tunnels_test.go +++ b/src/apps/dev_tunnels_test.go @@ -48,10 +48,10 @@ func devEnvFile(t *testing.T, cfg *config.Config, app, name string) (string, boo return string(data), true } -// A running development run gets a tunnel for every port of its template. Its -// tcp/udp ports have no switch: a rule flagged inactive is tunneled all the -// same. Their remote ports reach the container through its env files, and -// stay on the rules so the next run asks for the same ports again. +// A running development run gets a tunnel for every active port of its +// template. The remote ports of its tcp/udp tunnels reach the container +// through its env files, and stay on the rules so the next run asks for the +// same ports again. func TestSyncPortStateDevTunnelsAllPorts(t *testing.T) { am, _, mockTunnel, appStore, _, cfg := amHarness(t) mockTunnel.EXPECT().TunnelCapable().Return(true).Maybe() @@ -63,7 +63,7 @@ func TestSyncPortStateDevTunnelsAllPorts(t *testing.T) { payload.Ports, _ = tunnel.PortForwardRuleToInterface([]common.PortForwardRule{ {RuleName: "ui", Port: 8080, Protocol: "http", Active: true}, {RuleName: "mqtt", Port: 1883, Protocol: "tcp", Active: true, RemotePortEnvironment: "MQTT_PORT"}, - {RuleName: "media", Port: 10000, Protocol: "udp", Active: false}, + {RuleName: "media", Port: 10000, Protocol: "udp", Active: true}, }) // The launch path published all three on host ports. @@ -111,7 +111,7 @@ func TestSyncPortStateDevTunnelsAllPorts(t *testing.T) { assert.Equal(t, uint64(41800), added[sub].LocalPort, "frpc dials the DEV container's host port") require.Contains(t, added, tcpSub) assert.Equal(t, uint64(41801), added[tcpSub].LocalPort) - require.Contains(t, added, udpSub, "a DEV udp port has no switch: an inactive flag does not hold it off") + require.Contains(t, added, udpSub) rules := spsRules(t, saved) require.Len(t, rules, 3) @@ -129,9 +129,9 @@ func TestSyncPortStateDevTunnelsAllPorts(t *testing.T) { assert.False(t, ok, "an http tunnel has no remote port") } -// Once a development run stops, its tcp/udp tunnel goes and so does the port -// announced to the container. The rule keeps the port, and the next run asks -// frps for it again. +// Once a development run stops, or its port is switched off, its tcp/udp +// tunnel goes and so does the port announced to the container. The rule keeps +// the port, and the next run asks frps for it again. func TestSyncPortStateDevTcpTunnelFollowsRun(t *testing.T) { am, _, mockTunnel, appStore, _, cfg := amHarness(t) mockTunnel.EXPECT().TunnelCapable().Return(true).Maybe() @@ -176,6 +176,18 @@ func TestSyncPortStateDevTcpTunnelFollowsRun(t *testing.T) { got, ok := devEnvFile(t, cfg, "devmedia", "REMOTE_PORT_FOR_10000") assert.True(t, ok) assert.Equal(t, "30200", got) + + // Switched off while it runs: the tunnel goes as on a stop. Nothing + // changed on the rule, so nothing is saved. + payload.Ports = spsPorts(t, common.PortForwardRule{RuleName: "media", Port: 10000, Protocol: "udp", Active: false, HostPort: 41850, RemotePort: 30200}) + mockTunnel.EXPECT().Get(id).Return(&tunnel.Tunnel{Config: tunnel.TunnelConfig{Subdomain: sub, RemotePort: 30200}}).Once() + mockTunnel.EXPECT().RemoveTunnel(mock.MatchedBy(func(c tunnel.TunnelConfig) bool { + return c.Subdomain == sub && c.Protocol == tunnel.UDP && c.Stage == common.DEV + })).Return(nil).Once() + mockTunnel.EXPECT().GetState().Return([]tunnel.TunnelState{}, nil).Once() + require.NoError(t, am.syncPortState(payload, app)) + _, ok = devEnvFile(t, cfg, "devmedia", "REMOTE_PORT_FOR_10000") + assert.False(t, ok, "a switched-off port is not announced") } // Only an http(s) tunnel is reached by its label: a tcp/udp port of an app From 6cbee068b9f6f7ffce42655d145075ab27997ab2 Mon Sep 17 00:00:00 2001 From: Marko Petzold Date: Sun, 1 Nov 2026 12:44:46 +0100 Subject: [PATCH 2/2] release reagent v1.4.12 --- availableVersions.json | 8 ++++---- src/release/version.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/availableVersions.json b/availableVersions.json index 07ab0f3..569b9d9 100644 --- a/availableVersions.json +++ b/availableVersions.json @@ -1,6 +1,6 @@ { - "production": "1.4.11", - "test": "1.4.11", - "local": "1.4.11", - "all": "1.4.11" + "production": "1.4.12", + "test": "1.4.12", + "local": "1.4.12", + "all": "1.4.12" } diff --git a/src/release/version.txt b/src/release/version.txt index 009e668..89755f3 100644 --- a/src/release/version.txt +++ b/src/release/version.txt @@ -1 +1 @@ -1.4.11 \ No newline at end of file +1.4.12 \ No newline at end of file