11import { spawn , type ChildProcess } from "node:child_process"
2- import { mkdirSync , rmSync } from "node:fs"
2+ import { existsSync , mkdirSync , readFileSync , rmSync } from "node:fs"
33import { join } from "node:path"
44import { randomUUID } from "node:crypto"
55
@@ -8,17 +8,18 @@ import { Duration, Effect } from "effect"
88import type { PanelCloudflareTunnelSession , StartPanelCloudflareTunnelRequest } from "../api/contracts.js"
99import { ApiBadRequestError , ApiInternalError } from "../api/errors.js"
1010import {
11- defaultPanelTunnelLocalhostHost ,
1211 parseTryCloudflareUrl ,
1312 resolvePanelTunnelTargetUrl
1413} from "./panel-cloudflare-tunnel-core.js"
14+ import { parseLinuxDefaultGatewayIp } from "./project-port-proxy-core.js"
1515
1616type PanelCloudflareTunnelRecord = {
1717 readonly homeDir : string
1818 logLines : ReadonlyArray < string >
1919 process : ChildProcess | null
2020 session : PanelCloudflareTunnelSession
2121 stderrRemainder : string
22+ stopping : boolean
2223 stdoutRemainder : string
2324}
2425
@@ -112,11 +113,29 @@ const consumeChunk = (
112113const processEnv = (
113114 homeDir : string
114115) : Readonly < Record < string , string | undefined > > => ( {
115- ...process . env ,
116+ PATH : process . env [ "PATH" ] ,
117+ SSL_CERT_DIR : process . env [ "SSL_CERT_DIR" ] ,
118+ SSL_CERT_FILE : process . env [ "SSL_CERT_FILE" ] ,
116119 HOME : homeDir ,
117120 NO_COLOR : "1"
118121} )
119122
123+ const readDefaultGatewayIp = ( ) : string | null => {
124+ try {
125+ return parseLinuxDefaultGatewayIp ( readFileSync ( "/proc/net/route" , "utf8" ) )
126+ } catch {
127+ return null
128+ }
129+ }
130+
131+ const defaultPanelTunnelLocalhostHost = ( ) : string => {
132+ const configured = process . env [ "DOCKER_GIT_PANEL_TUNNEL_LOCALHOST_HOST" ] ?. trim ( )
133+ if ( configured !== undefined && configured . length > 0 ) {
134+ return configured
135+ }
136+ return existsSync ( "/.dockerenv" ) ? readDefaultGatewayIp ( ) ?? "172.17.0.1" : "127.0.0.1"
137+ }
138+
120139const createStartingRecord = (
121140 panelUrl : string
122141) : PanelCloudflareTunnelRecord => {
@@ -138,6 +157,7 @@ const createStartingRecord = (
138157 stoppedAt : null
139158 } ,
140159 stderrRemainder : "" ,
160+ stopping : false ,
141161 stdoutRemainder : ""
142162 }
143163}
@@ -150,36 +170,87 @@ const removeTunnelHomeBestEffort = (record: PanelCloudflareTunnelRecord): void =
150170 }
151171}
152172
153- const stopRecord = (
173+ const finishStoppedRecord = (
154174 record : PanelCloudflareTunnelRecord ,
155175 error : string | null = null
156176) : PanelCloudflareTunnelSession => {
157- const process = record . process
158177 record . process = null
178+ record . stopping = false
159179 const session = updateRecord ( record , {
160180 error,
161181 status : error === null ? "stopped" : "failed" ,
162182 stoppedAt : nowIso ( )
163183 } )
164- if ( process !== null && ! process . killed ) {
165- try {
166- process . kill ( "SIGTERM" )
167- } catch {
168- // process already exited
184+ removeTunnelHomeBestEffort ( record )
185+ return session
186+ }
187+
188+ const waitForChildClose = ( child : ChildProcess ) : Effect . Effect < void > => {
189+ if ( child . exitCode !== null || child . signalCode !== null ) {
190+ return Effect . void
191+ }
192+
193+ return Effect . async ( ( resume ) => {
194+ let completed = false
195+ let killTimer : ReturnType < typeof setTimeout > | null = null
196+ const complete = ( ) : void => {
197+ if ( completed ) {
198+ return
199+ }
200+ completed = true
201+ child . off ( "close" , complete )
202+ child . off ( "error" , complete )
203+ child . off ( "exit" , complete )
204+ if ( killTimer !== null ) {
205+ clearTimeout ( killTimer )
206+ }
207+ resume ( Effect . void )
169208 }
170- const killTimer = setTimeout ( ( ) => {
209+
210+ child . once ( "close" , complete )
211+ child . once ( "error" , complete )
212+ child . once ( "exit" , complete )
213+ if ( ! child . killed ) {
171214 try {
172- if ( process . exitCode === null && process . signalCode === null ) {
173- process . kill ( "SIGKILL" )
215+ child . kill ( "SIGTERM" )
216+ } catch {
217+ complete ( )
218+ return
219+ }
220+ }
221+ killTimer = setTimeout ( ( ) => {
222+ try {
223+ if ( child . exitCode === null && child . signalCode === null ) {
224+ child . kill ( "SIGKILL" )
174225 }
175226 } catch {
176- // process already exited
227+ complete ( )
177228 }
178229 } , 2_000 )
179230 killTimer . unref ( )
231+ } )
232+ }
233+
234+ const stopRecord = (
235+ record : PanelCloudflareTunnelRecord ,
236+ error : string | null = null
237+ ) : Effect . Effect < PanelCloudflareTunnelSession > => {
238+ const child = record . process
239+ record . process = null
240+ record . stopping = true
241+ return ( child === null ? Effect . void : waitForChildClose ( child ) ) . pipe (
242+ Effect . map ( ( ) => finishStoppedRecord ( record , error ) )
243+ )
244+ }
245+
246+ const runStopRecord = (
247+ record : PanelCloudflareTunnelRecord ,
248+ error : string
249+ ) : void => {
250+ if ( record . stopping || record . session . status === "stopped" || record . session . status === "failed" ) {
251+ return
180252 }
181- removeTunnelHomeBestEffort ( record )
182- return session
253+ Effect . runFork ( stopRecord ( record , error ) )
183254}
184255
185256const attachProcessHandlers = (
@@ -194,16 +265,16 @@ const attachProcessHandlers = (
194265 consumeChunk ( record , "stderr" , chunk )
195266 } )
196267 child . on ( "error" , ( error ) => {
197- stopRecord ( record , `cloudflared failed to start: ${ error . message } ` )
268+ runStopRecord ( record , `cloudflared failed to start: ${ error . message } ` )
198269 } )
199270 child . on ( "close" , ( exitCode , signal ) => {
200271 flushRemainder ( record , "stdout" )
201272 flushRemainder ( record , "stderr" )
202- if ( record . session . status === "stopped" || record . session . status === "failed" ) {
273+ if ( record . stopping || record . session . status === "stopped" || record . session . status === "failed" ) {
203274 return
204275 }
205276 const details = exitCode === null ? `signal ${ signal ?? "unknown" } ` : `exit code ${ exitCode } `
206- stopRecord ( record , `cloudflared exited before the tunnel was stopped (${ details } ).` )
277+ runStopRecord ( record , `cloudflared exited before the tunnel was stopped (${ details } ).` )
207278 } )
208279}
209280
@@ -303,7 +374,7 @@ const waitForRecordId = (
303374
304375 yield * _ ( preflightPanelTarget ( resolved . targetUrl ) )
305376 if ( currentRecord !== null ) {
306- stopRecord ( currentRecord )
377+ yield * _ ( stopRecord ( currentRecord ) )
307378 }
308379
309380 const record = createStartingRecord ( resolved . panelUrl )
@@ -324,4 +395,6 @@ export const startPanelCloudflareTunnel = (
324395 } )
325396
326397export const stopPanelCloudflareTunnel = ( ) : Effect . Effect < PanelCloudflareTunnelSession | null > =>
327- Effect . sync ( ( ) => currentRecord === null ? null : stopRecord ( currentRecord ) ) . pipe ( tunnelRecordLock . withPermits ( 1 ) )
398+ Effect . gen ( function * ( _ ) {
399+ return currentRecord === null ? null : yield * _ ( stopRecord ( currentRecord ) )
400+ } ) . pipe ( tunnelRecordLock . withPermits ( 1 ) )
0 commit comments