mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-13 07:47:52 +00:00
WIP - before the testing.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user