pccontrol.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // ============================================================================
  2. // pccontrol.go — 电源控制
  3. //
  4. // 关机 / 重启 / 休眠 / 锁屏命令(Windows 下用 shutdown 与 rundll32),
  5. // 支持延迟执行与取消,并提供当前命令状态查询。
  6. // ============================================================================
  7. package main
  8. import (
  9. "fmt"
  10. "net/http"
  11. "os/exec"
  12. "runtime"
  13. "strconv"
  14. "sync"
  15. "time"
  16. "github.com/gen2brain/beeep"
  17. "github.com/gin-gonic/gin"
  18. )
  19. // 电源命令默认延迟执行时间(Windows 下交给 shutdown /t 系统计时,服务崩溃倒计时仍有效)
  20. const pcShutdownDelay = 30 * time.Second
  21. // pcCommand 电源命令类型
  22. type pcCommand string
  23. const (
  24. pcPoweroff pcCommand = "poweroff" // 关机
  25. pcRestart pcCommand = "restart" // 重启
  26. pcSleep pcCommand = "sleep" // 休眠
  27. pcLock pcCommand = "lock" // 锁屏
  28. )
  29. // PCStatus 电源命令状态快照,供 API 与「主机」页查询
  30. type PCStatus struct {
  31. Command string `json:"command"` // 最近一次命令类型
  32. Pending bool `json:"pending"` // 是否有待执行命令
  33. ExecuteAt string `json:"executeAt"` // 计划执行时间
  34. RemainingSecs int `json:"remainingSecs"` // 剩余秒数
  35. ClientIP string `json:"clientIp"` // 最近一次请求的客户端 IP
  36. RequestTime string `json:"requestTime"` // 最近一次请求时间
  37. Requested bool `json:"requested"` // 是否收到过请求
  38. }
  39. var (
  40. pcMu sync.Mutex
  41. pcTimer *time.Timer // 休眠 / 锁屏等不支持系统计时的命令用 Go 计时
  42. pcExecuteAt time.Time
  43. pcPending bool
  44. pcPendingCmd pcCommand
  45. pcSystemTimer bool // 是否已交给系统计时(shutdown /t)
  46. pcLastClientIP string
  47. pcLastTime time.Time
  48. pcLastCmd pcCommand
  49. pcRequested bool
  50. )
  51. // pcSystemCommand 构造系统命令;返回 nil 表示该命令需 Go 计时后立即执行
  52. func pcSystemCommand(cmd pcCommand, delaySec int) *exec.Cmd {
  53. switch runtime.GOOS {
  54. case "windows":
  55. switch cmd {
  56. case pcPoweroff:
  57. return exec.Command("shutdown", "/s", "/t", strconv.Itoa(delaySec), "/f")
  58. case pcRestart:
  59. return exec.Command("shutdown", "/r", "/t", strconv.Itoa(delaySec), "/f")
  60. }
  61. case "darwin":
  62. switch cmd {
  63. case pcPoweroff:
  64. return exec.Command("sudo", "shutdown", "-h", "+0")
  65. case pcRestart:
  66. return exec.Command("sudo", "shutdown", "-r", "+0")
  67. }
  68. case "linux":
  69. switch cmd {
  70. case pcPoweroff:
  71. return exec.Command("shutdown", "-h", "+0")
  72. case pcRestart:
  73. return exec.Command("shutdown", "-r", "+0")
  74. }
  75. }
  76. return nil
  77. }
  78. // pcExecuteNow 立即执行休眠 / 锁屏(这类命令无系统计时,直接执行)
  79. func pcExecuteNow(cmd pcCommand) {
  80. var execCmd *exec.Cmd
  81. switch cmd {
  82. case pcSleep:
  83. switch runtime.GOOS {
  84. case "windows":
  85. execCmd = exec.Command("rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0")
  86. case "darwin":
  87. execCmd = exec.Command("pmset", "sleepnow")
  88. default:
  89. execCmd = exec.Command("systemctl", "suspend")
  90. }
  91. case pcLock:
  92. switch runtime.GOOS {
  93. case "windows":
  94. execCmd = exec.Command("rundll32.exe", "user32.dll,LockWorkStation")
  95. default:
  96. execCmd = nil
  97. }
  98. }
  99. if execCmd == nil {
  100. fmt.Fprintf(apiLogRing, "[SYS] 不支持的命令 %s (OS: %s)\n", cmd, runtime.GOOS)
  101. return
  102. }
  103. if output, err := execCmd.CombinedOutput(); err != nil {
  104. fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 执行失败: %v | %s\n", cmd, err, string(output))
  105. } else {
  106. fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 执行成功\n", cmd)
  107. }
  108. }
  109. // pcAbortShutdown 取消系统关机 / 重启倒计时
  110. func pcAbortShutdown() {
  111. if runtime.GOOS == "windows" {
  112. if output, err := exec.Command("shutdown", "/a").CombinedOutput(); err != nil {
  113. fmt.Fprintf(apiLogRing, "[SYS] 取消关机失败: %v | %s\n", err, string(output))
  114. } else {
  115. fmt.Fprintln(apiLogRing, "[SYS] 系统关机已取消")
  116. }
  117. } else {
  118. _ = exec.Command("shutdown", "-c").Run()
  119. }
  120. }
  121. // getPCStatus 返回当前电源命令状态快照
  122. func getPCStatus() PCStatus {
  123. pcMu.Lock()
  124. defer pcMu.Unlock()
  125. status := PCStatus{
  126. Command: string(pcLastCmd),
  127. Pending: pcPending,
  128. ClientIP: pcLastClientIP,
  129. Requested: pcRequested,
  130. }
  131. if !pcLastTime.IsZero() {
  132. status.RequestTime = pcLastTime.Format("01-02 15:04:05")
  133. }
  134. if pcPending {
  135. status.ExecuteAt = pcExecuteAt.Format("01-02 15:04:05")
  136. remaining := int(time.Until(pcExecuteAt).Seconds())
  137. if remaining < 0 {
  138. remaining = 0
  139. }
  140. status.RemainingSecs = remaining
  141. }
  142. return status
  143. }
  144. // handleDelayedCommand 通用延迟命令处理:优先交给系统计时,不支持则用 Go 计时
  145. func handleDelayedCommand(c *gin.Context, cmd pcCommand, delay time.Duration, title string) {
  146. clientIP := c.ClientIP()
  147. delaySec := int(delay.Seconds())
  148. pcMu.Lock()
  149. pcLastClientIP = clientIP
  150. pcLastTime = time.Now()
  151. pcLastCmd = cmd
  152. pcRequested = true
  153. if pcPending {
  154. remaining := time.Until(pcExecuteAt).Round(time.Second)
  155. pendingCmd := pcPendingCmd
  156. pcMu.Unlock()
  157. c.String(http.StatusOK, "命令 %s 已在待执行,剩余 %v\n", pendingCmd, remaining)
  158. return
  159. }
  160. pcPending = true
  161. pcPendingCmd = cmd
  162. pcExecuteAt = time.Now().Add(delay)
  163. sysCmd := pcSystemCommand(cmd, delaySec)
  164. if sysCmd != nil {
  165. pcSystemTimer = true
  166. pcMu.Unlock()
  167. // 系统计时:立即下发 shutdown /t N,由系统自己倒计时
  168. if output, err := sysCmd.CombinedOutput(); err != nil {
  169. pcMu.Lock()
  170. pcPending = false
  171. pcPendingCmd = ""
  172. pcExecuteAt = time.Time{}
  173. pcSystemTimer = false
  174. pcMu.Unlock()
  175. fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 下发失败: %v | %s\n", cmd, err, string(output))
  176. c.String(http.StatusInternalServerError, "命令执行失败: %v\n", err)
  177. return
  178. }
  179. fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 交由系统计时 | %ds | 客户端 %s\n", cmd, delaySec, clientIP)
  180. } else {
  181. // Go 计时:延迟到点后立即执行
  182. pcSystemTimer = false
  183. cmdToExec := cmd
  184. pcTimer = time.AfterFunc(delay, func() {
  185. pcMu.Lock()
  186. pcPending = false
  187. pcPendingCmd = ""
  188. pcMu.Unlock()
  189. pcExecuteNow(cmdToExec)
  190. })
  191. pcMu.Unlock()
  192. fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 使用 Go 计时 | %ds | 客户端 %s\n", cmd, delaySec, clientIP)
  193. }
  194. _ = beeep.Notify("⚠ "+title, fmt.Sprintf("来自: %s\n将在 %d 秒后执行\n时间: %s", clientIP, delaySec, time.Now().Format("15:04:05")), "")
  195. c.String(http.StatusOK, "OK: %s\n", title)
  196. }
  197. // handlePoweroff 处理关机请求:延迟 pcShutdownDelay 秒后关机
  198. func handlePoweroff(c *gin.Context) {
  199. handleDelayedCommand(c, pcPoweroff, pcShutdownDelay, "收到关机请求")
  200. }
  201. // handleRestart 处理重启请求:延迟 pcShutdownDelay 秒后重启
  202. func handleRestart(c *gin.Context) {
  203. handleDelayedCommand(c, pcRestart, pcShutdownDelay, "收到重启请求")
  204. }
  205. // handleSleep 处理休眠请求:延迟 pcShutdownDelay 秒后休眠
  206. func handleSleep(c *gin.Context) {
  207. handleDelayedCommand(c, pcSleep, pcShutdownDelay, "收到休眠请求")
  208. }
  209. // handleLock 锁屏不需要倒计时,直接执行
  210. func handleLock(c *gin.Context) {
  211. pcMu.Lock()
  212. pcLastClientIP = c.ClientIP()
  213. pcLastTime = time.Now()
  214. pcLastCmd = pcLock
  215. pcRequested = true
  216. pcMu.Unlock()
  217. go pcExecuteNow(pcLock)
  218. c.String(http.StatusOK, "OK: 已锁屏\n")
  219. }
  220. // handleCancel 取消待执行命令:系统计时调 shutdown /a,Go 计时停定时器
  221. func handleCancel(c *gin.Context) {
  222. pcMu.Lock()
  223. if !pcPending {
  224. pcMu.Unlock()
  225. c.String(http.StatusOK, "无待执行命令\n")
  226. return
  227. }
  228. cancelled := pcPendingCmd
  229. wasSystemTimer := pcSystemTimer
  230. pcPending = false
  231. pcPendingCmd = ""
  232. pcExecuteAt = time.Time{}
  233. pcSystemTimer = false
  234. if pcTimer != nil {
  235. pcTimer.Stop()
  236. pcTimer = nil
  237. }
  238. pcMu.Unlock()
  239. if wasSystemTimer {
  240. pcAbortShutdown() // 系统计时的倒计时由 shutdown /a 取消
  241. }
  242. fmt.Fprintf(apiLogRing, "[SYS] 命令 %s 已取消 | 客户端 %s\n", cancelled, c.ClientIP())
  243. _ = beeep.Notify("✅ 命令已取消", fmt.Sprintf("%s 由 %s 取消", cancelled, c.ClientIP()), "")
  244. c.String(http.StatusOK, "命令已取消\n")
  245. }
  246. // handlePCStatus 返回当前命令状态(JSON)
  247. func handlePCStatus(c *gin.Context) {
  248. c.JSON(http.StatusOK, getPCStatus())
  249. }
  250. // handleTimeSync 手动触发时间同步,返回具体结果
  251. func handleTimeSync(c *gin.Context) {
  252. if err := syncSystemTime(); err != nil {
  253. c.String(http.StatusInternalServerError, "时间同步失败: %v\n", err)
  254. return
  255. }
  256. c.String(http.StatusOK, "时间同步成功\n")
  257. }