package services import ( "context" "encoding/json" "fmt" "lan-manager/server/db" "lan-manager/server/utils" "os/exec" "runtime" "strings" "time" "net" "golang.org/x/net/icmp" "golang.org/x/net/ipv4" ) func PingHost(ip string) bool { // Try system ping first (no special privileges needed on Linux) if ok := pingWithSystem(ip); ok { return true } // Fallback to raw ICMP return pingWithICMP(ip) } func pingWithSystem(ip string) bool { var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.Command("ping", "-n", "1", "-w", "3000", ip) } else if runtime.GOOS == "darwin" { // macOS ping -W is in milliseconds cmd = exec.Command("ping", "-c", "1", "-W", "3000", ip) } else { // Linux ping -W is in seconds cmd = exec.Command("ping", "-c", "1", "-W", "3", ip) } err := cmd.Run() return err == nil } func pingWithICMP(ip string) bool { c, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0") if err != nil { return false } defer c.Close() m := &icmp.Message{ Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ID: 1, Seq: 1, Data: []byte("lan-manager")}, } wb, err := m.Marshal(nil) if err != nil { return false } if _, err := c.WriteTo(wb, &net.IPAddr{IP: parseIP(ip)}); err != nil { return false } rb := make([]byte, 1500) _ = c.SetReadDeadline(time.Now().Add(3 * time.Second)) n, _, err := c.ReadFrom(rb) if err != nil { return false } rm, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply.Protocol(), rb[:n]) if err != nil { return false } return rm.Type == ipv4.ICMPTypeEchoReply } func parseIP(ip string) []byte { parts := strings.Split(ip, ".") if len(parts) != 4 { return nil } res := make([]byte, 4) for i, p := range parts { var v byte fmt.Sscanf(p, "%d", &v) res[i] = v } return res } func StartPingService(interval int) { ticker := time.NewTicker(time.Duration(interval) * time.Second) go func() { for range ticker.C { rows, err := db.DB.Query(`SELECT id, ip, ssh_port, ssh_username, ssh_password FROM machines`) if err != nil { fmt.Printf("[Ping] query error: %v\n", err) continue } type machinePing struct { id int64 ip string sshPort int sshUsername string sshPassword string } list := []machinePing{} for rows.Next() { var m machinePing if err := rows.Scan(&m.id, &m.ip, &m.sshPort, &m.sshUsername, &m.sshPassword); err == nil { list = append(list, m) } else { fmt.Printf("[Ping] scan error: %v\n", err) } } rows.Close() fmt.Printf("[Ping] tick processed %d machines\n", len(list)) for _, m := range list { online := PingHost(m.ip) var onlineInt int if online { onlineInt = 1 } _, dbErr := db.DB.Exec(`UPDATE machines SET is_online = ?, last_ping_at = CURRENT_TIMESTAMP WHERE id = ?`, onlineInt, m.id) if dbErr != nil { fmt.Printf("[Ping] update status error for %s: %v\n", m.ip, dbErr) } else { fmt.Printf("[Ping] %s -> online=%v\n", m.ip, online) } // Auto fetch SSH info if credentials are saved if m.sshUsername != "" && m.sshPassword != "" { go func(mid int64, mip string, mport int, muser, mpass string) { plainPass, decryptErr := utils.Decrypt(mpass) if decryptErr != nil { fmt.Printf("[SSH] decrypt failed for %s:%d: %v\n", mip, mport, decryptErr) return } result, err := GetSSHInfo(mip, mport, muser, plainPass) if err != nil { fmt.Printf("[SSH] auto-fetch failed for %s:%d: %v\n", mip, mport, err) return } portsStr := "" if len(result.ListenPorts) > 0 { b, _ := json.Marshal(result.ListenPorts) portsStr = string(b) if len(portsStr) > 2 { portsStr = portsStr[1 : len(portsStr)-1] } } _, dbErr := db.DB.Exec(`UPDATE machines SET cpu_info=?, memory_info=?, disk_info=?, uptime=?, listen_ports=?, ssh_synced_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id=?`, result.RawCPUInfo, result.RawMemoryInfo, result.RawDiskInfo, result.Uptime, portsStr, mid) if dbErr != nil { fmt.Printf("[SSH] auto-fetch db error for %s:%d: %v\n", mip, mport, dbErr) } else { fmt.Printf("[SSH] auto-fetch success for %s:%d\n", mip, mport) } }(m.id, m.ip, m.sshPort, m.sshUsername, m.sshPassword) } } } }() } func CleanupLogs(ctx context.Context, retentionDays int) { if retentionDays <= 0 { return } ticker := time.NewTicker(24 * time.Hour) go func() { for { select { case <-ctx.Done(): return case <-ticker.C: _, _ = db.DB.Exec(`DELETE FROM operation_logs WHERE created_at < datetime('now', '-`+fmt.Sprintf("%d", retentionDays)+` days')`) } } }() }