portguard

package
v1.53.1 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: AGPL-3.0 Imports: 9 Imported by: 0

Documentation

Overview

Package portguard ensures a set of TCP ports are actually free — and, if a matching stale process is still holding one, terminates it — before a caller starts a new process that needs to bind them.

This replaces two independent shell implementations of the same precondition (scripts/lib/wait_for_port_release.sh, reimplemented ad hoc by scripts/dev-restart-guard.sh before it was consolidated back onto the shared helper) that each accumulated their own incident history: a 10s "wait, then proceed anyway" timeout that left a genuinely stuck process racing the next instance (2026-09-08), a pkill pattern that once matched the live launchd-managed service as collateral damage (2026-09-04), and a launchctl bootstrap/load domain mismatch that silently broke the rollback path (2026-08-18). None of those were caught by a test, because shell has no practical way to exercise "spawn a real process holding a real port, then assert the reaper actually frees it" — see portguard_test.go, which does exactly that against this package.

Index

Constants

View Source
const (
	// DefaultTimeout is how long EnsureReleased waits for a SIGTERM'd
	// process to exit and release its ports before escalating to SIGKILL.
	DefaultTimeout = 10 * time.Second

	// DefaultPollInterval is how often port state is rechecked while waiting.
	DefaultPollInterval = 200 * time.Millisecond
)

Variables

This section is empty.

Functions

func EnsureReleased

func EnsureReleased(ctx context.Context, opts Options) error

EnsureReleased blocks until every port in opts.Ports is free, actively terminating matching processes rather than just waiting and giving up.

Sequence: poll for natural release up to opts.Timeout. If ports are still held, find processes matching opts.ProcessNameContains that are actually listening on one of opts.Ports, SIGTERM them, and poll again for the same duration. If still held, SIGKILL the survivors and poll up to killGracePeriod. Returns an error only if ports remain bound after all of that — the caller then knows definitively that starting the next process would race a real, unkillable holder (e.g. permission denied) rather than silently proceeding into a crash loop.

func PortFree

func PortFree(port int) bool

PortFree reports whether nothing is listening on port on any interface. Implemented as a real bind attempt (not a parse of lsof/ss output) so it needs no external tool and can't be fooled by a platform's differing lsof/ss output format.

Types

type Options

type Options struct {
	// Ports are the TCP ports that must be unbound before EnsureReleased
	// returns successfully.
	Ports []int

	// ProcessNameContains matches candidate processes by substring against
	// each process's executable name (not full cmdline — a full-cmdline
	// substring match is what let a prior shell pkill pattern accidentally
	// match unrelated processes). Only processes whose listening port
	// appears in Ports are ever considered, so this is a second, narrower
	// gate on top of that, not the primary filter.
	ProcessNameContains string

	// ExcludePID is never signaled, even if it happens to hold one of the
	// target ports (e.g. the caller's own process during a self-test).
	ExcludePID int32

	// Timeout bounds the initial graceful-exit wait after a SIGTERM. Zero
	// uses DefaultTimeout.
	Timeout time.Duration

	// PollInterval bounds how often port state is rechecked. Zero uses
	// DefaultPollInterval.
	PollInterval time.Duration
}

Options configures a single EnsureReleased call.

Jump to

Keyboard shortcuts

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