From 02a7a54736fe172af0b845698cc3485d99bc6cd0 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Mon, 19 Jan 2026 23:36:40 +0100 Subject: [PATCH] feat: auto-discover available ports when defaults are in use (#614) * feat: auto-discover available ports when defaults are in use Instead of prompting the user to kill processes or manually enter alternative ports, the launcher now automatically finds the next available ports when the defaults (3007/3008) are already in use. This enables running the built Electron app alongside web development mode without conflicts - web dev will automatically use the next available ports (e.g., 3009/3010) when Electron is running. Changes: - Add find_next_available_port() function that searches up to 100 ports - Update resolve_port_conflicts() to auto-select ports without prompts - Update check_ports() for consistency (currently unused but kept) - Add safety check to ensure web and server ports don't conflict * fix: sanitize PIDs to single line for centered display * feat: add user choice for port conflicts with auto-select as default When ports are in use, users can now choose: - [Enter] Auto-select available ports (default, recommended) - [K] Kill processes and use default ports - [C] Choose custom ports manually - [X] Cancel Pressing Enter without typing anything will auto-select the next available ports, making it easy to quickly continue when running alongside an existing Electron instance. * fix: improve port discovery error handling and code quality Address PR review feedback: - Extract magic number 100 to PORT_SEARCH_MAX_ATTEMPTS constant - Fix find_next_available_port to return nothing on failure instead of the busy port, preventing misleading "auto-selected" messages - Update all callers to handle port discovery failure with clear error messages showing the searched range - Simplify PID formatting using xargs instead of tr|sed|sed pipeline --- start-automaker.sh | 163 ++++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 69 deletions(-) diff --git a/start-automaker.sh b/start-automaker.sh index ecb499b9..5d9a30a4 100755 --- a/start-automaker.sh +++ b/start-automaker.sh @@ -34,6 +34,7 @@ fi # Port configuration DEFAULT_WEB_PORT=3007 DEFAULT_SERVER_PORT=3008 +PORT_SEARCH_MAX_ATTEMPTS=100 WEB_PORT=$DEFAULT_WEB_PORT SERVER_PORT=$DEFAULT_SERVER_PORT @@ -453,6 +454,25 @@ is_port_in_use() { [ -n "$pids" ] && [ "$pids" != " " ] } +# Find the next available port starting from a given port +# Returns the port on stdout if found, nothing if all ports in range are busy +# Exit code: 0 if found, 1 if no available port in range +find_next_available_port() { + local start_port=$1 + local port=$start_port + + for ((i=0; i/dev/null || true - + # Auto-discover available ports (no user interaction required) local web_in_use=false local server_in_use=false @@ -506,72 +524,46 @@ check_ports() { if [ "$web_in_use" = true ] || [ "$server_in_use" = true ]; then echo "" + local max_port if [ "$web_in_use" = true ]; then local pids - pids=$(get_pids_on_port "$DEFAULT_WEB_PORT") - echo "${C_YELLOW}⚠${RESET} Port $DEFAULT_WEB_PORT is in use by process(es): $pids" + # Get PIDs and convert newlines to spaces for display + pids=$(get_pids_on_port "$DEFAULT_WEB_PORT" | xargs) + echo "${C_YELLOW}Port $DEFAULT_WEB_PORT in use (PID: $pids), finding alternative...${RESET}" + max_port=$((DEFAULT_WEB_PORT + PORT_SEARCH_MAX_ATTEMPTS - 1)) + if ! WEB_PORT=$(find_next_available_port "$DEFAULT_WEB_PORT"); then + echo "${C_RED}Error: No free web port in range ${DEFAULT_WEB_PORT}-${max_port}${RESET}" + exit 1 + fi fi if [ "$server_in_use" = true ]; then local pids - pids=$(get_pids_on_port "$DEFAULT_SERVER_PORT") - echo "${C_YELLOW}⚠${RESET} Port $DEFAULT_SERVER_PORT is in use by process(es): $pids" + # Get PIDs and convert newlines to spaces for display + pids=$(get_pids_on_port "$DEFAULT_SERVER_PORT" | xargs) + echo "${C_YELLOW}Port $DEFAULT_SERVER_PORT in use (PID: $pids), finding alternative...${RESET}" + max_port=$((DEFAULT_SERVER_PORT + PORT_SEARCH_MAX_ATTEMPTS - 1)) + if ! SERVER_PORT=$(find_next_available_port "$DEFAULT_SERVER_PORT"); then + echo "${C_RED}Error: No free server port in range ${DEFAULT_SERVER_PORT}-${max_port}${RESET}" + exit 1 + fi fi + + # Ensure web and server ports don't conflict with each other + if [ "$WEB_PORT" -eq "$SERVER_PORT" ]; then + local conflict_start=$((SERVER_PORT + 1)) + max_port=$((conflict_start + PORT_SEARCH_MAX_ATTEMPTS - 1)) + if ! SERVER_PORT=$(find_next_available_port "$conflict_start"); then + echo "${C_RED}Error: No free server port in range ${conflict_start}-${max_port}${RESET}" + exit 1 + fi + fi + echo "" - - while true; do - read -r -p "What would you like to do? (k)ill processes, (u)se different ports, or (c)ancel: " choice - case "$choice" in - [kK]|[kK][iI][lL][lL]) - if [ "$web_in_use" = true ]; then - kill_port "$DEFAULT_WEB_PORT" - else - echo "${C_GREEN}✓${RESET} Port $DEFAULT_WEB_PORT is available" - fi - if [ "$server_in_use" = true ]; then - kill_port "$DEFAULT_SERVER_PORT" - else - echo "${C_GREEN}✓${RESET} Port $DEFAULT_SERVER_PORT is available" - fi - break - ;; - [uU]|[uU][sS][eE]) - # Collect both ports first - read -r -p "Enter web port (default $DEFAULT_WEB_PORT): " input_web - input_web=${input_web:-$DEFAULT_WEB_PORT} - read -r -p "Enter server port (default $DEFAULT_SERVER_PORT): " input_server - input_server=${input_server:-$DEFAULT_SERVER_PORT} - - # Validate both before assigning either - if ! validate_port "$input_web" "Web port"; then - continue - fi - if ! validate_port "$input_server" "Server port"; then - continue - fi - - # Assign atomically after both validated - WEB_PORT=$input_web - SERVER_PORT=$input_server - echo "${C_GREEN}Using ports: Web=$WEB_PORT, Server=$SERVER_PORT${RESET}" - break - ;; - [cC]|[cC][aA][nN][cC][eE][lL]) - echo "${C_MUTE}Cancelled.${RESET}" - exit 0 - ;; - *) - echo "${C_RED}Invalid choice. Please enter k, u, or c.${RESET}" - ;; - esac - done - echo "" + echo "${C_GREEN}✓ Auto-selected available ports: Web=$WEB_PORT, Server=$SERVER_PORT${RESET}" else echo "${C_GREEN}✓${RESET} Port $DEFAULT_WEB_PORT is available" echo "${C_GREEN}✓${RESET} Port $DEFAULT_SERVER_PORT is available" fi - - hide_cursor - stty -echo -icanon 2>/dev/null || true } validate_terminal_size() { @@ -791,37 +783,70 @@ resolve_port_conflicts() { if is_port_in_use "$DEFAULT_WEB_PORT"; then web_in_use=true - web_pids=$(get_pids_on_port "$DEFAULT_WEB_PORT") + # Get PIDs and convert newlines to spaces for display + web_pids=$(get_pids_on_port "$DEFAULT_WEB_PORT" | xargs) fi if is_port_in_use "$DEFAULT_SERVER_PORT"; then server_in_use=true - server_pids=$(get_pids_on_port "$DEFAULT_SERVER_PORT") + # Get PIDs and convert newlines to spaces for display + server_pids=$(get_pids_on_port "$DEFAULT_SERVER_PORT" | xargs) fi if [ "$web_in_use" = true ] || [ "$server_in_use" = true ]; then echo "" if [ "$web_in_use" = true ]; then - center_print "⚠ Port $DEFAULT_WEB_PORT is in use by process(es): $web_pids" "$C_YELLOW" + center_print "Port $DEFAULT_WEB_PORT in use (PID: $web_pids)" "$C_YELLOW" fi if [ "$server_in_use" = true ]; then - center_print "⚠ Port $DEFAULT_SERVER_PORT is in use by process(es): $server_pids" "$C_YELLOW" + center_print "Port $DEFAULT_SERVER_PORT in use (PID: $server_pids)" "$C_YELLOW" fi echo "" # Show options center_print "What would you like to do?" "$C_WHITE" echo "" - center_print "[K] Kill processes and continue" "$C_GREEN" - center_print "[U] Use different ports" "$C_MUTE" - center_print "[C] Cancel" "$C_RED" + center_print "[Enter] Auto-select available ports (Recommended)" "$C_GREEN" + center_print "[K] Kill processes and use default ports" "$C_MUTE" + center_print "[C] Choose custom ports" "$C_MUTE" + center_print "[X] Cancel" "$C_RED" echo "" while true; do local choice_pad=$(( (TERM_COLS - 20) / 2 )) printf "%${choice_pad}s" "" - read -r -p "Choice: " choice + read -r -p "Choice [Enter]: " choice case "$choice" in + ""|[aA]|[aA][uU][tT][oO]) + # Auto-select: find next available ports + echo "" + local max_port=$((DEFAULT_WEB_PORT + PORT_SEARCH_MAX_ATTEMPTS - 1)) + if [ "$web_in_use" = true ]; then + if ! WEB_PORT=$(find_next_available_port "$DEFAULT_WEB_PORT"); then + center_print "No free web port in range ${DEFAULT_WEB_PORT}-${max_port}" "$C_RED" + exit 1 + fi + fi + max_port=$((DEFAULT_SERVER_PORT + PORT_SEARCH_MAX_ATTEMPTS - 1)) + if [ "$server_in_use" = true ]; then + if ! SERVER_PORT=$(find_next_available_port "$DEFAULT_SERVER_PORT"); then + center_print "No free server port in range ${DEFAULT_SERVER_PORT}-${max_port}" "$C_RED" + exit 1 + fi + fi + # Ensure web and server ports don't conflict with each other + if [ "$WEB_PORT" -eq "$SERVER_PORT" ]; then + local conflict_start=$((SERVER_PORT + 1)) + max_port=$((conflict_start + PORT_SEARCH_MAX_ATTEMPTS - 1)) + if ! SERVER_PORT=$(find_next_available_port "$conflict_start"); then + center_print "No free server port in range ${conflict_start}-${max_port}" "$C_RED" + exit 1 + fi + fi + center_print "✓ Auto-selected available ports:" "$C_GREEN" + center_print " Web: $WEB_PORT | Server: $SERVER_PORT" "$C_PRI" + break + ;; [kK]|[kK][iI][lL][lL]) echo "" if [ "$web_in_use" = true ]; then @@ -836,7 +861,7 @@ resolve_port_conflicts() { fi break ;; - [uU]|[uU][sS][eE]) + [cC]|[cC][hH][oO][oO][sS][eE]) echo "" local input_pad=$(( (TERM_COLS - 40) / 2 )) # Collect both ports first @@ -861,14 +886,14 @@ resolve_port_conflicts() { center_print "Using ports: Web=$WEB_PORT, Server=$SERVER_PORT" "$C_GREEN" break ;; - [cC]|[cC][aA][nN][cC][eE][lL]) + [xX]|[xX][cC][aA][nN][cC][eE][lL]) echo "" center_print "Cancelled." "$C_MUTE" echo "" exit 0 ;; *) - center_print "Invalid choice. Please enter K, U, or C." "$C_RED" + center_print "Invalid choice. Press Enter for auto-select, or K/C/X." "$C_RED" ;; esac done