// Package freeport provides a helper for allocating free ports across multiple // processes on the same machine. package freeport import ( "fmt" "math/rand" "net" "sync" "time" "github.com/mitchellh/go-testing-interface" ) const ( // blockSize is the size of the allocated port block. ports are given out // consecutively from that block with roll-over for the lifetime of the // application/test run. blockSize = 100 // maxBlocks is the number of available port blocks. // lowPort + maxBlocks * blockSize must be less than 65535. maxBlocks = 10 // lowPort is the lowest port number that should be used. lowPort = 8000 // attempts is how often we try to allocate a port block // before giving up. attempts = 10 ) var ( // firstPort is the first port of the allocated block. firstPort int // lockLn is the system-wide mutex for the port block. lockLn net.Listener // mu guards nextPort mu sync.Mutex // once is used to do the initialization on the first call to retrieve free // ports once sync.Once // port is the last allocated port. port int ) // initialize is used to initialize freeport. func initialize() { if lowPort+maxBlocks*blockSize > 65535 { panic("freeport: block size too big or too many blocks requested") } rand.Seed(time.Now().UnixNano()) firstPort, lockLn = alloc() } // alloc reserves a port block for exclusive use for the lifetime of the // application. lockLn serves as a system-wide mutex for the port block and is // implemented as a TCP listener which is bound to the firstPort and which will // be automatically released when the application terminates. func alloc() (int, net.Listener) { for i := 0; i < attempts; i++ { block := int(rand.Int31n(int32(maxBlocks))) firstPort := lowPort + block*blockSize ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", firstPort)) if err != nil { continue } // log.Printf("[DEBUG] freeport: allocated port block %d (%d-%d)", block, firstPort, firstPort+blockSize-1) return firstPort, ln } panic("freeport: cannot allocate port block") } func tcpAddr(ip string, port int) *net.TCPAddr { return &net.TCPAddr{IP: net.ParseIP(ip), Port: port} } // Get wraps the Free function and panics on any failure retrieving ports. func Get(n int) (ports []int) { ports, err := Free(n) if err != nil { panic(err) } return ports } // GetT is suitable for use when retrieving unused ports in tests. If there is // an error retrieving free ports, the test will be failed. func GetT(t testing.T, n int) (ports []int) { ports, err := Free(n) if err != nil { t.Fatalf("Failed retrieving free port: %v", err) } return ports } // Free returns a list of free ports from the allocated port block. It is safe // to call this method concurrently. Ports have been tested to be available on // 127.0.0.1 TCP but there is no guarantee that they will remain free in the // future. func Free(n int) (ports []int, err error) { mu.Lock() defer mu.Unlock() if n > blockSize-1 { return nil, fmt.Errorf("freeport: block size too small") } // Reserve a port block once.Do(initialize) for len(ports) < n { port++ // roll-over the port if port < firstPort+1 || port >= firstPort+blockSize { port = firstPort + 1 } // if the port is in use then skip it ln, err := net.ListenTCP("tcp", tcpAddr("127.0.0.1", port)) if err != nil { // log.Println("[DEBUG] freeport: port already in use: ", port) continue } ln.Close() ports = append(ports, port) } // log.Println("[DEBUG] freeport: free ports:", ports) return ports, nil }