| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- // ============================================================================
- // 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")
- }
|