diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9a617f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +BINARY := findbaseport + +.PHONY: all fmt vet lint build clean + +all: fmt vet lint build + +fmt: + go fmt ./... + +vet: + go vet ./... + +lint: + golangci-lint run + +build: + mkdir -p bin + go build -o bin/$(BINARY) + +clean: + rm -rf bin/ diff --git a/README.md b/README.md index 33b5d60..2c917ed 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,164 @@ -# findcports +# findbaseport + +`findbaseport` is a CLI tool that searches a specified port range and returns the starting port of the first contiguous block of available ports with the requested size. + +## Motivation + +This tool was created to simplify finding a suitable base port for a [CryoSPARC](https://cryosparc.com/) installation. + +CryoSPARC requires a base port and expects a range of **10 contiguous ports** to be available starting from that port. Finding an appropriate base port manually can be inconvenient, particularly on systems where some ports are already in use. + +`findbaseport` automates this process by searching a specified port range and returning the first port from which the requested number of contiguous ports are available. + +For example, when looking for a base port with the default requirement of 10 contiguous ports, the tool searches for a sequence such as: + +```text +8000 8001 8002 8003 8004 8005 8006 8007 8008 8009 +``` + +If all 10 ports are available, `8000` can be used as the base port. If any of those ports are unavailable, the tool continues searching until it finds the next suitable contiguous block. + +## Description + +The tool searches an **inclusive** port range for available ports. + +Given a range and a requested block size, `findbaseport` finds the first sequence of contiguous available ports that is large enough and returns the starting port of that block. + +For example, if the requested block size is `3` in a range of `8000` to `8020`: +```bash +findbaseport --start 8000 --end 8020 --count 3 +``` + +and ports `8000`, `8001`, and `8002` are available, the command returns: + +```text +8000 +``` + +If those ports are unavailable but `8010`, `8011`, and `8012` are available, it returns: + +```text +8010 +``` + +## Installation + +### Build from source + +Clone the repository and use the included Makefile: + +```bash +git clone +cd + +make build +``` + +The resulting binary is placed in: + +```text +bin/findbaseport +``` + +You can then run it with: + +```bash +./bin/findbaseport --help +``` + +## Usage + +```bash +./bin/findbaseport --help +``` + +The help output describes the available command-line options and arguments. + +The command searches within the specified port range and returns the start port of the first contiguous block of available ports with the requested size. + +## Development + +The project provides a Makefile with commands for formatting, static analysis, linting, and building. + +### Run everything + +```bash +make +``` + +The default target runs: + +1. `fmt` +2. `vet` +3. `lint` +4. `build` + +### Format + +Format all Go source files: + +```bash +make fmt +``` + +### Vet + +Run Go's static analysis: + +```bash +make vet +``` + +### Lint + +Run `golangci-lint`: + +```bash +make lint +``` + +Make sure `golangci-lint` is installed and available in your `PATH`. + +### Build + +Build the CLI: + +```bash +make build +``` + +The binary is written to: + +```text +bin/findbaseport +``` + +### Clean + +Remove build artifacts: + +```bash +make clean +``` + +## Requirements + +* Go +* `make` +* `golangci-lint` for linting + +## Make Targets + +| Target | Description | +| ------------ | ---------------------------------- | +| `make` | Format, vet, lint, and build | +| `make fmt` | Format Go source code | +| `make vet` | Run `go vet` | +| `make lint` | Run `golangci-lint` | +| `make build` | Build `findbaseport` into `bin/` | +| `make clean` | Remove the `bin/` directory | + +## License + +See the repository's `LICENSE` file for licensing information. -A CLI tool that searches within a specified port range and returns the start and end ports of the first contiguous block of available ports with the requested size. diff --git a/go.mod b/go.mod index d093ac3..d22e10a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module findcports +module findbaseport go 1.25.8 @@ -6,7 +6,6 @@ require ( charm.land/lipgloss/v2 v2.0.5 github.com/charmbracelet/fang v1.0.0 github.com/charmbracelet/x/exp/charmtone v0.0.0-20260727090823-41c9e6be3365 - github.com/prometheus/common v0.70.1 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 ) @@ -29,6 +28,7 @@ require ( github.com/muesli/mango-pflag v0.1.0 // indirect github.com/muesli/roff v0.1.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect diff --git a/go.sum b/go.sum index cf99bc6..0a81d6b 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,6 @@ github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= -github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/main.go b/main.go index d6355f3..687ab09 100644 --- a/main.go +++ b/main.go @@ -3,14 +3,13 @@ package main import ( "context" "fmt" + "log" "net" "os" - "strconv" "charm.land/lipgloss/v2" "github.com/charmbracelet/fang" "github.com/charmbracelet/x/exp/charmtone" - "github.com/prometheus/common/version" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -21,7 +20,12 @@ func isPortFree(port uint) bool { if err != nil { return false } - l.Close() + defer func() { + if err := l.Close(); err != nil { + log.Printf("failed to close Listener: %v", err) + } + }() + return true } @@ -33,10 +37,6 @@ func FindAvailablePorts(start, end, count uint) (uint, uint, error) { err := fmt.Errorf("start port must not exceed end port") return 0, 0, err } - if start == 0 { - err := fmt.Errorf("start port must be greater than zero") - return 0, 0, err - } if end == 0 { err := fmt.Errorf("end port must be greater than zero") return 0, 0, err @@ -64,14 +64,15 @@ func FindAvailablePorts(start, end, count uint) (uint, uint, error) { } } - return 0, 0, fmt.Errorf("no contiguous range of %d ports found", count) + return 0, 0, fmt.Errorf( + "no contiguous range of %d ports found between %d and %d", + count, start, end) } -// Color and column style variables +// Color style variables var ( - rangePortStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) - returnPortStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("5")).Bold(true) - helpTitleStyle = lipgloss.NewStyle().Foreground(charmtone.Charple).Bold(true) + helpTitleStyle = lipgloss.NewStyle().Foreground(charmtone.Charple). + Bold(true) ) // Flag variables @@ -83,6 +84,9 @@ type config struct { } } +// Setting the port limit +var portLimit uint = 65535 + var cfg config func main() { @@ -92,12 +96,12 @@ func main() { // CLI Create a new cobra Command cobraCmd := &cobra.Command{ - Use: "findcports", + Use: "findbaseport", Long: fmt.Sprintf(`%s -Searches within a specified port range and returns the -start and end ports of the first contiguous block of -available ports with the requested size.`, +Searches within a specified port range (inclusive) and returns the +start port of the first contiguous block of available +ports with the requested size.`, helpTitleStyle.Render("DESCRIPTION")), RunE: runApp, } @@ -107,15 +111,15 @@ available ports with the requested size.`, // ============================================= // ports - portsFS.UintVarP(&cfg.ports.start, "start", "s", 20000, + portsFS.UintVarP(&cfg.ports.start, "start", "s", 0, "First port in the search range", ) - portsFS.UintVarP(&cfg.ports.end, "end", "e", 20999, + portsFS.UintVarP(&cfg.ports.end, "end", "e", portLimit, "Last port in the search range", ) - portsFS.UintVarP(&cfg.ports.count, "count", "c", 10, + portsFS.UintVarP(&cfg.ports.count, "count", "c", 0, "Size of the contiguous port block to find", ) @@ -128,47 +132,42 @@ available ports with the requested size.`, // Set new flag display settings cobraCmd.Flags().SetInterspersed(false) cobraCmd.Flags().SortFlags = false - cobraCmd.Flags().PrintDefaults() // Remove completion and help commands (--help is kept) cobraCmd.CompletionOptions.DisableDefaultCmd = true cobraCmd.SetHelpCommand(&cobra.Command{Hidden: true}) - // CLI Version - cobraCmd.Version = version.Print("findcports") - // Execute cobra command if err := fang.Execute(context.Background(), cobraCmd); err != nil { os.Exit(1) } } -func runApp(cobraCmd *cobra.Command, args []string) error { +func runApp(cmd *cobra.Command, _ []string) error { - startPort, endPort, err := FindAvailablePorts( + // Check if --count was set with either --start or --end + if !cmd.Flags().Changed("count") || + (!cmd.Flags().Changed("start") && !cmd.Flags().Changed("end")) { + return fmt.Errorf( + "flag --count with either --start or --end is required") + + // Ports cannot exceed a limit + } else if cfg.ports.count > portLimit+1 || + cfg.ports.start > portLimit || + cfg.ports.end > portLimit { + return fmt.Errorf( + "the TCP port number cannot exceed %d", portLimit) + } + + startPort, _, err := FindAvailablePorts( cfg.ports.start, cfg.ports.end, cfg.ports.count) if err != nil { return err + } else { + fmt.Printf("%d\n", startPort) } - if startPort == endPort { - fmt.Printf( - "Found the first available port between %s and %s:\n%s\n", - rangePortStyle.Render(strconv.FormatUint(uint64(cfg.ports.start), 10)), - rangePortStyle.Render(strconv.FormatUint(uint64(cfg.ports.end), 10)), - returnPortStyle.Render(strconv.FormatUint(uint64(startPort), 10)), - ) - } else { - fmt.Printf( - "Found the first available continuous ports between %s and %s:\n%s-%s (%d ports)\n", - rangePortStyle.Render(strconv.FormatUint(uint64(cfg.ports.start), 10)), - rangePortStyle.Render(strconv.FormatUint(uint64(cfg.ports.end), 10)), - returnPortStyle.Render(strconv.FormatUint(uint64(startPort), 10)), - returnPortStyle.Render(strconv.FormatUint(uint64(endPort), 10)), - cfg.ports.count, - ) - } return nil }