Bind the serve listener directly, dropping the pre-flight workaround

go.mills.io/static now returns bind errors and accepts a listener, so
zs no longer has to probe the address with its own net.Listen and then
hand the address to a server that would bind it again -- a check with a
race between the two binds.

`serve` binds once and passes the listener to the server, which also
means the address logged is the one actually obtained: `zs serve -b
127.0.0.1:0` now reports the port the OS picked rather than ":0".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiPehVTCDGWgEBJ7H4G5VL
This commit is contained in:
James Mills
2026-09-01 00:48:31 +10:00
co-authored by Claude Opus 5
parent fc4d0d15b6
commit 44e5800422
3 changed files with 14 additions and 19 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
go.abhg.dev/goldmark/anchor v0.2.0
go.abhg.dev/goldmark/wikilink v0.6.0
go.mills.io/static v0.0.0-20250927134114-653d64f884cd
go.mills.io/static v0.0.0-20260831143852-78fb794ee31a
golang.org/x/sync v0.22.0
golang.org/x/text v0.41.0
gopkg.in/yaml.v2 v2.4.0
+2
View File
@@ -94,6 +94,8 @@ go.abhg.dev/goldmark/wikilink v0.6.0 h1:SKZANgMD7GMbaU0kBKTh52Ea9k3A3Y5ZifHoEPC1
go.abhg.dev/goldmark/wikilink v0.6.0/go.mod h1:Sfaovp00aAVJ5khqIeDTTgkIfZrcurmJGlbntCJUbJY=
go.mills.io/static v0.0.0-20250927134114-653d64f884cd h1:xaL5pVaQU2tMP4JzmwQR1U+wuW/GaUOFc2cdnC44c8w=
go.mills.io/static v0.0.0-20250927134114-653d64f884cd/go.mod h1:TmaEDwM+IgrCRyMxtVWtmSdoxLP3N6ehBa7AiOZj2Mk=
go.mills.io/static v0.0.0-20260831143852-78fb794ee31a h1:jrld620D5wdVAgpcMGAIiG5OT4YPEx0X3Rc5M4039xc=
go.mills.io/static v0.0.0-20260831143852-78fb794ee31a/go.mod h1:TmaEDwM+IgrCRyMxtVWtmSdoxLP3N6ehBa7AiOZj2Mk=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
+11 -18
View File
@@ -1202,37 +1202,30 @@ func serveURL(bind string) string {
return "http://" + net.JoinHostPort(host, port)
}
// checkBind reports whether bind can be listened on. go.mills.io/static only
// exposes Run(), which binds internally and logs (rather than returns) a bind
// failure, so this is checked up front: announcing a URL that was never bound
// and then idling forever is worse than failing.
func checkBind(bind string) error {
ln, err := net.Listen("tcp", bind)
if err != nil {
return err
}
return ln.Close()
}
// serve runs a static web server and builds and continuously watches for changes to rebuild
func serve(ctx context.Context, bind, root string) error {
os.Mkdir(root, 0755)
// Bind here rather than letting the server do it, so that a failure is
// reported before anything is announced, and so the address logged is the
// one actually obtained (with :0, the port the OS picked).
ln, err := net.Listen("tcp", bind)
if err != nil {
return fmt.Errorf("cannot listen on %s: %w", bind, err)
}
svr, err := static.NewServer(
static.WithBind(bind),
static.WithListener(ln),
static.WithRoot(root),
static.WithCGI(true),
static.WithDir(true),
)
if err != nil {
ln.Close()
return err
}
if err := checkBind(bind); err != nil {
return fmt.Errorf("cannot listen on %s: %w", bind, err)
}
log.Infof("zs %s server listening on %s", ShortVersion(), serveURL(bind))
log.Infof("zs %s server listening on %s", ShortVersion(), serveURL(ln.Addr().String()))
// A failure in either task cancels the other instead of idling until the
// user notices nothing is being served.