99 "net/netip"
1010 "net/url"
1111 "strconv"
12+ "sync"
1213
1314 "go.uber.org/zap"
1415
@@ -26,6 +27,8 @@ type Config struct {
2627}
2728
2829type Server struct {
30+ lifecycleMu sync.Mutex
31+
2932 listener net.Listener
3033 server * http.Server
3134 logger * zap.Logger
@@ -39,6 +42,10 @@ type Server struct {
3942 // isRunning is true if the server was started and is running
4043 isRunning bool
4144
45+ // cachedPort stores the port from the first successful bind when AddrPort used port 0 (ephemeral).
46+ // Reused on pause/resume so URLs remain valid across ToBackground/ToForeground cycles.
47+ cachedPort int
48+
4249 * timeoutManager
4350}
4451
@@ -91,14 +98,24 @@ func (s *Server) GetLogger() *zap.Logger {
9198 return s .logger
9299}
93100
101+ // getBindAddrPort returns the address to bind to. When config used port 0 (ephemeral)
102+ // and we have a cached port from a previous run, reuse it so URLs stay stable across pause/resume.
103+ func (s * Server ) getBindAddrPort () netip.AddrPort {
104+ if s .cachedPort != 0 && s .config .AddrPort .Port () == 0 {
105+ return netip .AddrPortFrom (s .config .AddrPort .Addr (), uint16 (s .cachedPort ))
106+ }
107+ return s .config .AddrPort
108+ }
109+
94110func (s * Server ) createListener () (net.Listener , error ) {
111+ addr := s .getBindAddrPort ()
95112 if s .config .Cert == nil {
96113 // HTTP mode
97- return net .Listen ("tcp" , s . config . AddrPort .String ())
114+ return net .Listen ("tcp" , addr .String ())
98115 }
99116
100117 // HTTPS mode
101- serverName := s . config . AddrPort .Addr ().String ()
118+ serverName := addr .Addr ().String ()
102119 if len (s .config .Cert .Leaf .DNSNames ) > 0 {
103120 serverName = s .config .Cert .Leaf .DNSNames [0 ]
104121 }
@@ -108,7 +125,7 @@ func (s *Server) createListener() (net.Listener, error) {
108125 ServerName : serverName ,
109126 MinVersion : tls .VersionTLS12 ,
110127 }
111- return tls .Listen ("tcp" , s . config . AddrPort .String (), cfg )
128+ return tls .Listen ("tcp" , addr .String (), cfg )
112129}
113130
114131func (s * Server ) listen () error {
@@ -122,6 +139,9 @@ func (s *Server) listen() error {
122139 }
123140
124141 s .address = s .listener .Addr ().(* net.TCPAddr )
142+ if s .config .AddrPort .Port () == 0 {
143+ s .cachedPort = s .address .Port
144+ }
125145
126146 s .StartTimeout (func () {
127147 err := s .Stop ()
@@ -136,7 +156,6 @@ func (s *Server) listen() error {
136156func (s * Server ) serve () {
137157 defer common .LogOnPanic ()
138158
139- s .isRunning = true
140159 defer func () {
141160 s .isRunning = false
142161 s .address = nil
@@ -171,6 +190,9 @@ func (s *Server) applyHandlers() {
171190}
172191
173192func (s * Server ) Start () error {
193+ s .lifecycleMu .Lock ()
194+ defer s .lifecycleMu .Unlock ()
195+
174196 if s .isRunning {
175197 return nil
176198 }
@@ -184,38 +206,43 @@ func (s *Server) Start() error {
184206 return err
185207 }
186208
209+ // Mark running synchronously to avoid pause/play races where ToBackground
210+ // can run before serve() goroutine has a chance to set the state.
211+ s .isRunning = true
187212 go s .serve ()
188213 return nil
189214}
190215
191216func (s * Server ) Stop () error {
217+ s .lifecycleMu .Lock ()
218+ defer s .lifecycleMu .Unlock ()
219+
192220 s .StopTimeout ()
193- if s . server ! = nil {
194- return s . server . Shutdown ( context . Background ())
221+ if ! s . isRunning || s . server = = nil {
222+ return nil
195223 }
196224
197- return nil
225+ // Flip state before shutdown so rapid foreground/background transitions
226+ // don't attempt a concurrent second bind to a cached port.
227+ s .isRunning = false
228+ return s .server .Shutdown (context .Background ())
198229}
199230
200231func (s * Server ) IsRunning () bool {
201232 return s .isRunning
202233}
203234
204235func (s * Server ) ToForeground () {
205- if ! s .isRunning && (s .server != nil ) {
206- err := s .Start ()
207- if err != nil {
208- s .logger .Error ("server start failed during foreground transition" , zap .Error (err ))
209- }
236+ err := s .Start ()
237+ if err != nil {
238+ s .logger .Error ("server start failed during foreground transition" , zap .Error (err ))
210239 }
211240}
212241
213242func (s * Server ) ToBackground () {
214- if s .isRunning {
215- err := s .Stop ()
216- if err != nil {
217- s .logger .Error ("server stop failed during background transition" , zap .Error (err ))
218- }
243+ err := s .Stop ()
244+ if err != nil {
245+ s .logger .Error ("server stop failed during background transition" , zap .Error (err ))
219246 }
220247}
221248
0 commit comments