mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-11 00:09:31 +00:00
204 lines
5.5 KiB
Go
204 lines
5.5 KiB
Go
package forward
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os/exec"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// PortConflict represents a local port that is already in use.
|
|
type PortConflict struct {
|
|
Port int // The conflicting port number
|
|
Resource string // The forward resource that needs this port
|
|
UsedBy string // Process information (PID, command) using the port
|
|
}
|
|
|
|
// PortChecker checks port availability on the local system.
|
|
type PortChecker struct{}
|
|
|
|
// NewPortChecker creates a new PortChecker instance.
|
|
func NewPortChecker() *PortChecker {
|
|
return &PortChecker{}
|
|
}
|
|
|
|
// CheckAvailability checks if the given ports are available for binding.
|
|
// It returns a list of conflicts for ports that are already in use.
|
|
// The skipPorts map contains ports currently managed by kportal that should be excluded from the check.
|
|
func (pc *PortChecker) CheckAvailability(ports []int, skipPorts map[int]bool) []PortConflict {
|
|
var conflicts []PortConflict
|
|
|
|
for _, port := range ports {
|
|
// Skip ports that are already managed by kportal
|
|
if skipPorts[port] {
|
|
continue
|
|
}
|
|
|
|
// Try to bind to the port
|
|
if !pc.isPortAvailable(port) {
|
|
// Port is in use, get process info
|
|
usedBy := pc.getProcessUsingPort(port)
|
|
conflicts = append(conflicts, PortConflict{
|
|
Port: port,
|
|
UsedBy: usedBy,
|
|
})
|
|
}
|
|
}
|
|
|
|
return conflicts
|
|
}
|
|
|
|
// isPortAvailable checks if a port is available by attempting to bind to it.
|
|
func (pc *PortChecker) isPortAvailable(port int) bool {
|
|
// Try to listen on the port
|
|
addr := fmt.Sprintf(":%d", port)
|
|
listener, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
listener.Close()
|
|
return true
|
|
}
|
|
|
|
// getProcessUsingPort returns information about the process using the given port.
|
|
// Returns a string like "nginx (PID 1234)" or "unknown" if the process cannot be determined.
|
|
func (pc *PortChecker) getProcessUsingPort(port int) string {
|
|
switch runtime.GOOS {
|
|
case "darwin", "linux":
|
|
return pc.getProcessUsingPortUnix(port)
|
|
case "windows":
|
|
return pc.getProcessUsingPortWindows(port)
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// getProcessUsingPortUnix uses lsof to find the process using a port on Unix systems.
|
|
func (pc *PortChecker) getProcessUsingPortUnix(port int) string {
|
|
// Use lsof to find the process
|
|
// lsof -i :PORT -sTCP:LISTEN -t returns PIDs
|
|
cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN", "-t")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return "unknown"
|
|
}
|
|
|
|
pidStr := strings.TrimSpace(string(output))
|
|
if pidStr == "" {
|
|
return "unknown"
|
|
}
|
|
|
|
// Get the first PID if multiple are returned
|
|
pids := strings.Split(pidStr, "\n")
|
|
pid := pids[0]
|
|
|
|
// Get process name using ps
|
|
cmd = exec.Command("ps", "-p", pid, "-o", "comm=")
|
|
output, err = cmd.Output()
|
|
if err != nil {
|
|
return fmt.Sprintf("PID %s", pid)
|
|
}
|
|
|
|
procName := strings.TrimSpace(string(output))
|
|
if procName == "" {
|
|
return fmt.Sprintf("PID %s", pid)
|
|
}
|
|
|
|
return fmt.Sprintf("%s (PID %s)", procName, pid)
|
|
}
|
|
|
|
// getProcessUsingPortWindows uses netstat to find the process using a port on Windows.
|
|
func (pc *PortChecker) getProcessUsingPortWindows(port int) string {
|
|
// Use netstat to find the process
|
|
// netstat -ano | findstr :PORT
|
|
cmd := exec.Command("netstat", "-ano")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return "unknown"
|
|
}
|
|
|
|
lines := strings.Split(string(output), "\n")
|
|
portStr := fmt.Sprintf(":%d", port)
|
|
|
|
for _, line := range lines {
|
|
if !strings.Contains(line, portStr) {
|
|
continue
|
|
}
|
|
|
|
// Parse the line to extract PID
|
|
// Format: TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 1234
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 5 {
|
|
continue
|
|
}
|
|
|
|
// Check if this is a LISTENING state
|
|
if !strings.Contains(strings.ToUpper(line), "LISTENING") {
|
|
continue
|
|
}
|
|
|
|
pid := fields[len(fields)-1]
|
|
|
|
// Get process name using tasklist
|
|
cmd = exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %s", pid), "/FO", "CSV", "/NH")
|
|
output, err = cmd.Output()
|
|
if err != nil {
|
|
return fmt.Sprintf("PID %s", pid)
|
|
}
|
|
|
|
// Parse CSV output: "process.exe","1234","Console","1","12,345 K"
|
|
csvLine := strings.TrimSpace(string(output))
|
|
if csvLine == "" {
|
|
return fmt.Sprintf("PID %s", pid)
|
|
}
|
|
|
|
parts := strings.Split(csvLine, ",")
|
|
if len(parts) > 0 {
|
|
procName := strings.Trim(parts[0], "\"")
|
|
return fmt.Sprintf("%s (PID %s)", procName, pid)
|
|
}
|
|
|
|
return fmt.Sprintf("PID %s", pid)
|
|
}
|
|
|
|
return "unknown"
|
|
}
|
|
|
|
// FormatConflicts formats port conflicts into a human-readable error message.
|
|
func FormatConflicts(conflicts []PortConflict) string {
|
|
if len(conflicts) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("\nPort Conflicts Detected:\n")
|
|
sb.WriteString(strings.Repeat("=", 50) + "\n\n")
|
|
|
|
for _, conflict := range conflicts {
|
|
sb.WriteString(fmt.Sprintf("Port %d\n", conflict.Port))
|
|
if conflict.Resource != "" {
|
|
sb.WriteString(fmt.Sprintf(" Needed for: %s\n", conflict.Resource))
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" Currently used by: %s\n", conflict.UsedBy))
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
sb.WriteString("Action: Stop conflicting processes or change localPort in config.\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// GetPortsFromForwards extracts all local ports from a list of forward configurations.
|
|
func GetPortsFromForwards(forwards []interface{}) []int {
|
|
ports := make([]int, 0, len(forwards))
|
|
for _, fwd := range forwards {
|
|
// This function expects a generic interface to work with different forward types
|
|
// The actual implementation should use the Forward struct from config package
|
|
if f, ok := fwd.(interface{ GetLocalPort() int }); ok {
|
|
ports = append(ports, f.GetLocalPort())
|
|
}
|
|
}
|
|
return ports
|
|
}
|