// ============================================================================ // pccontrol.go — 电源控制 // // 关机 / 重启 / 休眠 / 锁屏命令(Windows 下用 shutdown 与 rundll32), // 支持延迟执行与取消,并提供当前命令状态查询。 // ============================================================================ package main import ( "fmt" "net/http" "os/exec" "runtime" "strconv" "sync" "time" "github.com/gen2brain/beeep" "github.com/gin-gonic/gin" ) // 电源命令默认延迟执行时间(Windows 下交给 shutdown /t 系统计时,服务崩溃倒计时仍有效) const pcShutdownDelay = 30 * time.Second // pcCommand 电源命令类型 type pcCommand string const ( pcPoweroff pcCommand = "poweroff" // 关机 pcRestart pcCommand = "restart" // 重启 pcSleep pcCommand = "sleep" // 休眠 pcLock pcCommand = "lock" // 锁屏 ) // PCStatus 电源命令状态快照,供 API 与「主机」页查询 type PCStatus struct { Command string `json:"command"` // 最近一次命令类型 Pending bool `json:"pending"` // 是否有待执行命令 ExecuteAt string `json:"executeAt"` // 计划执行时间 RemainingSecs int `json:"remainingSecs"` // 剩余秒数 ClientIP string `json:"clientIp"` // 最近一次请求的客户端 IP RequestTime string `json:"requestTime"` // 最近一次请求时间 Requested bool `json:"requested"` // 是否收到过请求 } var ( pcMu sync.Mutex pcTimer *time.Timer // 休眠 / 锁屏等不支持系统计时的命令用 Go 计时 pcExecuteAt time.Time pcPending bool pcPendingCmd pcCommand pcSystemTimer bool // 是否已交给系统计时(shutdown /t) pcLastClientIP string pcLastTime time.Time pcLastCmd pcCommand pcRequested bool ) // pcSystemCommand 构造系统命令;返回 nil 表示该命令需 Go 计时后立即执行 func pcSystemCommand(cmd pcCommand, delaySec int) *exec.Cmd { switch runtime.GOOS { case "windows": switch cmd { case pcPoweroff: return exec.Command("shutdown", "/s", "/t", strconv.Itoa(delaySec), "/f") case pcRestart: return exec.Command("shutdown", "/r", "/t", strconv.Itoa(delaySec), "/f") } case "darwin": switch cmd { case pcPoweroff: return exec.Command("sudo", "shutdown", "-h", "+0") case pcRestart: return exec.Command("sudo", "shutdown", "-r", "+0") } case "linux": switch cmd { case pcPoweroff: return exec.Command("shutdown", "-h", "+0") case pcRestart: return exec.Command("shutdown", "-r", "+0") } } return nil } // pcExecuteNow 立即执行休眠 / 锁屏(这类命令无系统计时,直接执行) func pcExecuteNow(cmd pcCommand) { var execCmd *exec.Cmd switch cmd { case pcSleep: switch runtime.GOOS { case "windows": execCmd = exec.Command("rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0") case "darwin": execCmd = exec.Command("pmset", "sleepnow") default: execCmd = exec.Command("systemctl", "suspend") } case pcLock: switch runtime.GOOS { case "windows": execCmd = exec.Command("rundll32.exe", "user32.dll,LockWorkStation") default: execCmd = nil } } if execCmd == nil { fmt.Fprintf(apiLogRing, "[SYS] 不支持的命令 %s (OS: %s)\n", cmd, runtime.GOOS) return } if output, err := execCmd.CombinedOutput(); err != nil { fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 执行失败: %v | %s\n", cmd, err, string(output)) } else { fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 执行成功\n", cmd) } } // pcAbortShutdown 取消系统关机 / 重启倒计时 func pcAbortShutdown() { if runtime.GOOS == "windows" { if output, err := exec.Command("shutdown", "/a").CombinedOutput(); err != nil { fmt.Fprintf(apiLogRing, "[SYS] 取消关机失败: %v | %s\n", err, string(output)) } else { fmt.Fprintln(apiLogRing, "[SYS] 系统关机已取消") } } else { _ = exec.Command("shutdown", "-c").Run() } } // getPCStatus 返回当前电源命令状态快照 func getPCStatus() PCStatus { pcMu.Lock() defer pcMu.Unlock() status := PCStatus{ Command: string(pcLastCmd), Pending: pcPending, ClientIP: pcLastClientIP, Requested: pcRequested, } if !pcLastTime.IsZero() { status.RequestTime = pcLastTime.Format("01-02 15:04:05") } if pcPending { status.ExecuteAt = pcExecuteAt.Format("01-02 15:04:05") remaining := int(time.Until(pcExecuteAt).Seconds()) if remaining < 0 { remaining = 0 } status.RemainingSecs = remaining } return status } // handleDelayedCommand 通用延迟命令处理:优先交给系统计时,不支持则用 Go 计时 func handleDelayedCommand(c *gin.Context, cmd pcCommand, delay time.Duration, title string) { clientIP := c.ClientIP() delaySec := int(delay.Seconds()) pcMu.Lock() pcLastClientIP = clientIP pcLastTime = time.Now() pcLastCmd = cmd pcRequested = true if pcPending { remaining := time.Until(pcExecuteAt).Round(time.Second) pendingCmd := pcPendingCmd pcMu.Unlock() c.String(http.StatusOK, "命令 %s 已在待执行,剩余 %v\n", pendingCmd, remaining) return } pcPending = true pcPendingCmd = cmd pcExecuteAt = time.Now().Add(delay) sysCmd := pcSystemCommand(cmd, delaySec) if sysCmd != nil { pcSystemTimer = true pcMu.Unlock() // 系统计时:立即下发 shutdown /t N,由系统自己倒计时 if output, err := sysCmd.CombinedOutput(); err != nil { pcMu.Lock() pcPending = false pcPendingCmd = "" pcExecuteAt = time.Time{} pcSystemTimer = false pcMu.Unlock() fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 下发失败: %v | %s\n", cmd, err, string(output)) c.String(http.StatusInternalServerError, "命令执行失败: %v\n", err) return } fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 交由系统计时 | %ds | 客户端 %s\n", cmd, delaySec, clientIP) } else { // Go 计时:延迟到点后立即执行 pcSystemTimer = false cmdToExec := cmd pcTimer = time.AfterFunc(delay, func() { pcMu.Lock() pcPending = false pcPendingCmd = "" pcMu.Unlock() pcExecuteNow(cmdToExec) }) pcMu.Unlock() fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 使用 Go 计时 | %ds | 客户端 %s\n", cmd, delaySec, clientIP) } _ = beeep.Notify("⚠ "+title, fmt.Sprintf("来自: %s\n将在 %d 秒后执行\n时间: %s", clientIP, delaySec, time.Now().Format("15:04:05")), "") c.String(http.StatusOK, "OK: %s\n", title) } // handlePoweroff 处理关机请求:延迟 pcShutdownDelay 秒后关机 func handlePoweroff(c *gin.Context) { handleDelayedCommand(c, pcPoweroff, pcShutdownDelay, "收到关机请求") } // handleRestart 处理重启请求:延迟 pcShutdownDelay 秒后重启 func handleRestart(c *gin.Context) { handleDelayedCommand(c, pcRestart, pcShutdownDelay, "收到重启请求") } // handleSleep 处理休眠请求:延迟 pcShutdownDelay 秒后休眠 func handleSleep(c *gin.Context) { handleDelayedCommand(c, pcSleep, pcShutdownDelay, "收到休眠请求") } // handleLock 锁屏不需要倒计时,直接执行 func handleLock(c *gin.Context) { pcMu.Lock() pcLastClientIP = c.ClientIP() pcLastTime = time.Now() pcLastCmd = pcLock pcRequested = true pcMu.Unlock() go pcExecuteNow(pcLock) c.String(http.StatusOK, "OK: 已锁屏\n") } // handleCancel 取消待执行命令:系统计时调 shutdown /a,Go 计时停定时器 func handleCancel(c *gin.Context) { pcMu.Lock() if !pcPending { pcMu.Unlock() c.String(http.StatusOK, "无待执行命令\n") return } cancelled := pcPendingCmd wasSystemTimer := pcSystemTimer pcPending = false pcPendingCmd = "" pcExecuteAt = time.Time{} pcSystemTimer = false if pcTimer != nil { pcTimer.Stop() pcTimer = nil } pcMu.Unlock() if wasSystemTimer { pcAbortShutdown() // 系统计时的倒计时由 shutdown /a 取消 } fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 已取消 | 客户端 %s\n", cancelled, c.ClientIP()) _ = beeep.Notify("✅ 命令已取消", fmt.Sprintf("%s 由 %s 取消", cancelled, c.ClientIP()), "") c.String(http.StatusOK, "命令已取消\n") } // handlePCStatus 返回当前命令状态(JSON) func handlePCStatus(c *gin.Context) { c.JSON(http.StatusOK, getPCStatus()) } // handleTimeSync 手动触发时间同步,返回具体结果 func handleTimeSync(c *gin.Context) { if err := syncSystemTime(); err != nil { c.String(http.StatusInternalServerError, "时间同步失败: %v\n", err) return } c.String(http.StatusOK, "时间同步成功\n") }