proc_windows.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. //go:build windows
  2. package main
  3. import (
  4. "context"
  5. "fmt"
  6. "os"
  7. "os/exec"
  8. "strconv"
  9. "syscall"
  10. "time"
  11. "unsafe"
  12. "golang.org/x/sys/windows"
  13. )
  14. const createNoWindow = 0x08000000
  15. // hideWindow 让子进程完全不显示终端窗口(CREATE_NO_WINDOW + HideWindow)。
  16. func hideWindow(cmd *exec.Cmd) {
  17. if cmd.SysProcAttr == nil {
  18. cmd.SysProcAttr = &syscall.SysProcAttr{}
  19. }
  20. cmd.SysProcAttr.HideWindow = true
  21. cmd.SysProcAttr.CreationFlags |= createNoWindow
  22. }
  23. // killTree 强制结束进程树(含 node 派生的子进程),带超时防止卡住。
  24. // taskkill 不可用或超时时,退化为直接终止目标进程。
  25. func killTree(pid int) error {
  26. if pid <= 0 {
  27. return nil
  28. }
  29. ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
  30. defer cancel()
  31. cmd := exec.CommandContext(ctx, "taskkill", "/PID", strconv.Itoa(pid), "/T", "/F")
  32. hideWindow(cmd)
  33. if err := cmd.Run(); err == nil {
  34. return nil
  35. }
  36. proc, err := os.FindProcess(pid)
  37. if err != nil {
  38. return err
  39. }
  40. return proc.Kill()
  41. }
  42. // processAlive 判断进程是否仍在运行。
  43. func processAlive(pid int) bool {
  44. if pid <= 0 {
  45. return false
  46. }
  47. h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid))
  48. if err != nil {
  49. return false
  50. }
  51. defer windows.CloseHandle(h)
  52. var code uint32
  53. if err := windows.GetExitCodeProcess(h, &code); err != nil {
  54. return false
  55. }
  56. return code == 259 // STILL_ACTIVE
  57. }
  58. var (
  59. kernel32 = windows.NewLazySystemDLL("kernel32.dll")
  60. shell32 = windows.NewLazySystemDLL("shell32.dll")
  61. procCreateMutex = kernel32.NewProc("CreateMutexW")
  62. procAttachConsole = kernel32.NewProc("AttachConsole")
  63. procShellExecuteW = shell32.NewProc("ShellExecuteW")
  64. )
  65. // openExternal 用系统 shell 打开 URL / 文件 / 目录。
  66. // 注意:这里绝不能设置 HideWindow / CREATE_NO_WINDOW —— explorer 打开的目录窗口
  67. // 会被 SW_HIDE 隐藏(表现为"点击菜单无反应")。
  68. func openExternal(target string) error {
  69. if target == "" {
  70. return nil
  71. }
  72. verb, err := windows.UTF16PtrFromString("open")
  73. if err != nil {
  74. return err
  75. }
  76. file, err := windows.UTF16PtrFromString(target)
  77. if err != nil {
  78. return err
  79. }
  80. const swShowNormal = 1
  81. ret, _, callErr := procShellExecuteW.Call(
  82. 0,
  83. uintptr(unsafe.Pointer(verb)),
  84. uintptr(unsafe.Pointer(file)),
  85. 0, 0,
  86. swShowNormal,
  87. )
  88. if ret <= 32 { // ShellExecuteW 约定:<=32 表示失败
  89. return fmt.Errorf("ShellExecuteW(%q) 失败: code=%d err=%v", target, ret, callErr)
  90. }
  91. return nil
  92. }
  93. // singleInstance 基于命名互斥体的单实例保护。
  94. type singleInstance struct {
  95. handle windows.Handle
  96. }
  97. func acquireSingleInstance(name string) (*singleInstance, bool) {
  98. n, err := windows.UTF16PtrFromString(name)
  99. if err != nil {
  100. return nil, true
  101. }
  102. h, _, callErr := procCreateMutex.Call(0, 0, uintptr(unsafe.Pointer(n)))
  103. if h == 0 {
  104. return nil, true // 创建失败时放行,避免误伤
  105. }
  106. if errno, ok := callErr.(syscall.Errno); ok && errno == syscall.ERROR_ALREADY_EXISTS {
  107. _ = windows.CloseHandle(windows.Handle(h))
  108. return nil, false
  109. }
  110. return &singleInstance{handle: windows.Handle(h)}, true
  111. }
  112. func (s *singleInstance) release() {
  113. if s != nil && s.handle != 0 {
  114. _ = windows.CloseHandle(s.handle)
  115. s.handle = 0
  116. }
  117. }
  118. // copyToClipboard 把文本放入系统剪贴板。
  119. // 实现:写临时文件后交给 clip.exe 重定向读入——文本内容不经过 shell 解析;
  120. // 临时文件用后即删(内容含访问 token)。
  121. func copyToClipboard(text string) error {
  122. if text == "" {
  123. return fmt.Errorf("无可复制内容")
  124. }
  125. tmp := os.TempDir() + "\\deepseek-tray-url.txt"
  126. if err := os.WriteFile(tmp, []byte(text), 0o600); err != nil {
  127. return err
  128. }
  129. defer os.Remove(tmp)
  130. f, err := os.Open(tmp)
  131. if err != nil {
  132. return err
  133. }
  134. defer f.Close()
  135. cmd := exec.Command("cmd", "/c", "clip")
  136. cmd.Stdin = f
  137. hideWindow(cmd)
  138. if err := cmd.Run(); err != nil {
  139. return fmt.Errorf("写入剪贴板失败: %w", err)
  140. }
  141. return nil
  142. }
  143. // attachConsole 让 windowsgui 子系统程序能输出到调用它的终端(仅 CLI 子命令使用)。
  144. func attachConsole() {
  145. const attachParentProcess = 0xFFFFFFFF
  146. ret, _, _ := procAttachConsole.Call(uintptr(attachParentProcess))
  147. if ret == 0 {
  148. return
  149. }
  150. if f, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0); err == nil {
  151. os.Stdout = f
  152. os.Stderr = f
  153. }
  154. }