183 lines
4.6 KiB
Go
183 lines
4.6 KiB
Go
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')`)
|
|
}
|
|
}
|
|
}()
|
|
}
|