Help us shape the future of Go! Take the 2026 Go Developer Survey

listen

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package listen is the Studio command's port policy (plan B2): one default port, 7331, stable and reusable, never silently different.

Given the address the command wants, Choose either binds it or explains why not, in one line:

  • The address is free: it is bound.
  • It is busy and pinned (--addr or WEFT_STUDIO_ADDR set explicitly): an error naming the address. No probe, no fallback.
  • It is busy and a weft Studio answers GET /api/meta there with the same database file (db.path, which B5 serves to loopback and the dev token only): Choose binds nothing and returns a reuse verdict naming that Studio's URL and pid.
  • It is busy with anything else — another program, a Studio on another database, a Studio whose meta this token cannot read (401/403) or that serves no db.path — the next port of the range is tried (7331–7340 by default), and the Choice's Note says which addresses were skipped and why. All of them busy is an error naming the range.

The token goes to loopback only: the probe attaches Request.Token as a bearer when the host it dials is loopback (127.0.0.0/8, ::1, localhost; an unspecified host is dialled on 127.0.0.1). A busy non-loopback address is probed bare — a fixed token never leaves the machine for whatever holds that port — so it is never reused: the db.path a reuse needs is served to loopback or the token alone.

The policy is the command's, not the library's: studio.New gains nothing, and an app's embedded Studio (setup A) is the app's own listener on the app's own port. The logic lives here, apart from the command wiring (cmd/weft), so the commands share it.

Index

Examples

Constants

View Source
const (
	// DefaultAddr is Studio's one default address.
	DefaultAddr = "127.0.0.1:7331"
	// DefaultSpan is how many ports an unpinned Choose tries, from the
	// wanted one up: 7331–7340.
	DefaultSpan = 10
	// ProbeTimeout bounds each /api/meta probe: a port held by
	// something that accepts and never answers costs at most this,
	// never a hang. Shorter than the doctor's 5 s: a Studio on loopback
	// answers meta in milliseconds, and a start may probe the whole
	// range.
	ProbeTimeout = 2 * time.Second
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Choice

type Choice struct {
	// Listener is bound on Addr; nil when Reuse is set. The caller
	// serves on it and closes it.
	Listener net.Listener
	// Addr is the address listened on — or, with Reuse, the running
	// Studio's.
	Addr string
	// Reuse is set when a Studio on the same database already serves
	// Addr: nothing was bound.
	Reuse bool
	// PID is the reused Studio's process id (api/meta's pid; 0 when it
	// served none).
	PID int
	// Note is one line saying why the wanted address was not used —
	// each skipped address and what holds it — and "" when it was.
	Note string
}

Choice is Choose's verdict: a listener, or a Studio to reuse.

func Choose

func Choose(ctx context.Context, r Request) (Choice, error)

Choose applies the port policy to r. ctx bounds the probes.

Example

ExampleChoose starts a Studio the way the command does — Choose, then serve on the Choice's listener — and starts it again on the same database: the second Choose binds nothing and names the running one to reuse. (The command wants listen.DefaultAddr; the example takes an ephemeral port so it runs anywhere.)

package main

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/weftgo/weft/internal/listen"
	"github.com/weftgo/weft/studio"
)

func main() {
	dir, _ := os.MkdirTemp("", "listen")
	defer func() { _ = os.RemoveAll(dir) }()
	path := filepath.Join(dir, "weft.db")
	free, _ := net.Listen("tcp", "127.0.0.1:0")
	want := free.Addr().String()
	_ = free.Close()
	req := listen.Request{Addr: want, Span: 2, DBPath: path, Token: "dev-token"}

	first, err := listen.Choose(context.Background(), req)
	if err != nil {
		fmt.Println(err)
		return
	}
	srv := studio.New(studio.Base("/"), studio.Open(path), studio.Token("dev-token"))
	defer func() { _ = srv.Close() }()
	hs := &http.Server{Handler: srv.Handler(), ReadHeaderTimeout: time.Second}
	go func() { _ = hs.Serve(first.Listener) }()
	defer func() { _ = hs.Close() }()
	fmt.Println("first: listening on the wanted address:", first.Addr == want)

	second, err := listen.Choose(context.Background(), req)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("second: reuse:", second.Reuse, "bound:", second.Listener != nil, "same pid:", second.PID == os.Getpid())
}
Output:
first: listening on the wanted address: true
second: reuse: true bound: false same pid: true

func (Choice) ReuseLine

func (c Choice) ReuseLine() string

ReuseLine is the line the command prints for a reuse verdict.

func (Choice) URL

func (c Choice) URL() string

URL is the Choice's address as an http URL.

type Request

type Request struct {
	// Addr is the wanted host:port ("" = DefaultAddr).
	Addr string
	// Pinned is whether Addr was set explicitly (--addr,
	// WEFT_STUDIO_ADDR): a busy pinned address is an error, never a
	// probe or another port.
	Pinned bool
	// Span is how many consecutive ports, Addr's first, an unpinned
	// Choose tries (0 = DefaultSpan).
	Span int
	// DBPath is the command's own database file, absolute. "" (an
	// in-memory or fileless database) never matches a running Studio's.
	DBPath string
	// Token is the bearer the probe sends ("" sends none and relies on
	// the loopback rule): the command's own --token / WEFT_STUDIO_TOKEN,
	// never a dev token it generated (nobody else holds that).
	Token string
	// TokenFor, when set and Token is empty, is asked for the bearer of
	// each probed address (the dialled host:port, an unspecified host as
	// 127.0.0.1); its non-empty answer is that probe's bearer. A fixed
	// Token always wins and TokenFor is then never consulted. The
	// command answers with the database's stable token only for an
	// address its user's discovery file names (plan B3), so that
	// long-lived token never goes to whatever else holds a port.
	TokenFor func(addr string) string
	// Timeout bounds each probe (0 = ProbeTimeout).
	Timeout time.Duration
}

Request is what the command wants.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL