croc_service.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. // croc_service.go — Croc 命令行传输工具的 Wails 服务。
  2. //
  3. // croc 是一款点对点加密文件传输 CLI:发送端执行 `croc send <文件>` 会生成
  4. // 一个传输码(如 mug-aids-amend),接收端执行 `croc <传输码>` 即可接收。
  5. //
  6. // 本服务自动在系统 PATH 中查找 croc(exec.LookPath),找到才允许前端展示
  7. // 「Croc 传送」工具卡片;发送/接收共用的密码保存在设置中(AppSettings.Croc)。
  8. // 设置了密码时,发送改用 `croc send --code <密码>`,接收端用同一密码作为
  9. // 传输码,双方只要密码一致即可完成传输。
  10. // 注意:croc 的 `--pass` 是中继服务器密码(公共中继固定为 pass123),不能用它
  11. // 承载用户密码,否则会连不上公共中继。
  12. package main
  13. import (
  14. "bufio"
  15. "bytes"
  16. "context"
  17. "errors"
  18. "fmt"
  19. "io"
  20. "os/exec"
  21. "regexp"
  22. "strings"
  23. "sync"
  24. "time"
  25. )
  26. // CrocConfig 是 Croc 传送的配置(发送/接收共用密码)。
  27. type CrocConfig struct {
  28. Password string `json:"password"` // 发送/接收共用密码:作为 croc 传输码(--code),空=使用随机传输码
  29. Dir string `json:"dir"` // 接收保存目录,空=当前工作目录
  30. }
  31. // CrocInfo 描述本机 croc 的可用性,用于前端决定是否展示工具卡片。
  32. type CrocInfo struct {
  33. Available bool `json:"available"` // PATH 中是否找到 croc
  34. Path string `json:"path"` // croc 可执行文件路径
  35. Version string `json:"version"` // 版本信息(croc --version 输出)
  36. }
  37. // CrocService 是 Croc 传送功能的 Wails 服务。
  38. type CrocService struct {
  39. mu sync.Mutex
  40. sendCancel context.CancelFunc
  41. recvCancel context.CancelFunc
  42. }
  43. // crocAnsiRe 匹配终端颜色控制符,输出到界面前先剥离。
  44. var crocAnsiRe = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]")
  45. // NewCrocService 创建 Croc 服务实例。
  46. func NewCrocService() *CrocService {
  47. return &CrocService{}
  48. }
  49. // Available 检测 PATH 中是否存在 croc,并返回其路径与版本。
  50. func (s *CrocService) Available() CrocInfo {
  51. path, err := exec.LookPath("croc")
  52. if err != nil || path == "" {
  53. return CrocInfo{}
  54. }
  55. info := CrocInfo{Available: true, Path: path}
  56. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  57. defer cancel()
  58. cmd := exec.CommandContext(ctx, path, "--version")
  59. hideProcessWindow(cmd)
  60. if out, err := cmd.Output(); err == nil {
  61. info.Version = strings.TrimSpace(string(out))
  62. }
  63. return info
  64. }
  65. // GetConfig 读取当前 Croc 配置(每次从设置文件读取,保证与设置页保存结果一致)。
  66. func (s *CrocService) GetConfig() CrocConfig {
  67. return loadSettings().Croc
  68. }
  69. // StartSend 开始发送文件:croc --yes [--code 密码] send <文件...>。
  70. // 设置了密码时用密码作为传输码(双方一致即可),否则由 croc 生成随机传输码。
  71. // 日志通过 croc:log 事件推送。
  72. func (s *CrocService) StartSend(files []string) error {
  73. if len(files) == 0 {
  74. return errors.New("请先选择要发送的文件")
  75. }
  76. path, err := exec.LookPath("croc")
  77. if err != nil {
  78. return errors.New("未找到 croc,请先安装并加入系统 PATH")
  79. }
  80. s.mu.Lock()
  81. if s.sendCancel != nil {
  82. s.mu.Unlock()
  83. return errors.New("已有发送任务正在进行")
  84. }
  85. ctx, cancel := context.WithCancel(context.Background())
  86. s.sendCancel = cancel
  87. args := append(s.baseArgs(), "send")
  88. if pw := s.password(); pw != "" {
  89. if len(pw) < 6 {
  90. s.sendCancel = nil
  91. cancel()
  92. s.mu.Unlock()
  93. return errors.New("Croc 密码至少 6 个字符(croc 对传输码长度的限制)")
  94. }
  95. args = append(args, "--code", pw) // 密码即传输码:发送/接收共用
  96. }
  97. args = append(args, files...)
  98. cmd := exec.CommandContext(ctx, path, args...)
  99. hideProcessWindow(cmd)
  100. if err := s.streamProcess(cmd, "send"); err != nil {
  101. s.sendCancel = nil
  102. cancel()
  103. s.mu.Unlock()
  104. return fmt.Errorf("启动发送失败:%w", err)
  105. }
  106. s.mu.Unlock()
  107. return nil
  108. }
  109. // StartReceive 开始接收:croc --yes [--out 目录] <传输码>。
  110. // 传输码为空但设置中配置了密码时,自动把密码当作传输码,便于双方只用同一密码。
  111. func (s *CrocService) StartReceive(code, dir string) error {
  112. code = strings.TrimSpace(code)
  113. if code == "" {
  114. code = s.password() // 密码即传输码:接收端可只配置密码
  115. }
  116. if code == "" {
  117. return errors.New("请输入对方发来的传输码(或在设置中配置 Croc 传送密码)")
  118. }
  119. if code == s.password() && len(code) < 6 {
  120. return errors.New("Croc 密码至少 6 个字符(croc 对传输码长度的限制)")
  121. }
  122. path, err := exec.LookPath("croc")
  123. if err != nil {
  124. return errors.New("未找到 croc,请先安装并加入系统 PATH")
  125. }
  126. s.mu.Lock()
  127. if s.recvCancel != nil {
  128. s.mu.Unlock()
  129. return errors.New("已有接收任务正在进行")
  130. }
  131. ctx, cancel := context.WithCancel(context.Background())
  132. s.recvCancel = cancel
  133. args := s.baseArgs()
  134. if dir = strings.TrimSpace(dir); dir != "" {
  135. args = append(args, "--out", dir)
  136. }
  137. args = append(args, code)
  138. cmd := exec.CommandContext(ctx, path, args...)
  139. hideProcessWindow(cmd)
  140. if err := s.streamProcess(cmd, "recv"); err != nil {
  141. s.recvCancel = nil
  142. cancel()
  143. s.mu.Unlock()
  144. return fmt.Errorf("启动接收失败:%w", err)
  145. }
  146. s.mu.Unlock()
  147. return nil
  148. }
  149. // StopSend 终止发送任务(若在运行)。
  150. func (s *CrocService) StopSend() {
  151. s.mu.Lock()
  152. cancel := s.sendCancel
  153. s.mu.Unlock()
  154. if cancel != nil {
  155. cancel()
  156. }
  157. }
  158. // StopReceive 终止接收任务(若在运行)。
  159. func (s *CrocService) StopReceive() {
  160. s.mu.Lock()
  161. cancel := s.recvCancel
  162. s.mu.Unlock()
  163. if cancel != nil {
  164. cancel()
  165. }
  166. }
  167. // baseArgs 返回发送/接收共用的命令行参数:自动确认所有提示。
  168. func (s *CrocService) baseArgs() []string {
  169. return []string{"--yes"}
  170. }
  171. // password 返回设置中保存的发送/接收共用密码(已去空格)。
  172. func (s *CrocService) password() string {
  173. return strings.TrimSpace(s.GetConfig().Password)
  174. }
  175. // streamProcess 启动进程并把 stdout/stderr 逐行推送给前端(croc:log)。
  176. // 调用方需持有 s.mu。
  177. func (s *CrocService) streamProcess(cmd *exec.Cmd, kind string) error {
  178. stdout, err := cmd.StdoutPipe()
  179. if err != nil {
  180. return err
  181. }
  182. stderr, err := cmd.StderrPipe()
  183. if err != nil {
  184. return err
  185. }
  186. if err := cmd.Start(); err != nil {
  187. return err
  188. }
  189. emit := func(raw string) {
  190. line := strings.TrimSpace(crocAnsiRe.ReplaceAllString(raw, ""))
  191. if line == "" {
  192. return
  193. }
  194. app.Event.Emit("croc:log", map[string]string{"kind": kind, "line": line})
  195. }
  196. go scanCrocStream(stdout, emit)
  197. go scanCrocStream(stderr, emit)
  198. go func() {
  199. waitErr := cmd.Wait()
  200. exitCode := 0
  201. if waitErr != nil {
  202. var ee *exec.ExitError
  203. if errors.As(waitErr, &ee) {
  204. exitCode = ee.ExitCode()
  205. } else {
  206. exitCode = -1
  207. }
  208. }
  209. s.mu.Lock()
  210. if kind == "send" {
  211. s.sendCancel = nil
  212. } else {
  213. s.recvCancel = nil
  214. }
  215. s.mu.Unlock()
  216. app.Event.Emit("croc:exit", map[string]any{"kind": kind, "code": exitCode})
  217. }()
  218. return nil
  219. }
  220. // scanCrocStream 按 \r 或 \n 切分输出(croc 的进度用 \r 刷新),逐段回调。
  221. func scanCrocStream(r io.Reader, emit func(string)) {
  222. scanner := bufio.NewScanner(r)
  223. scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
  224. scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
  225. if atEOF && len(data) == 0 {
  226. return 0, nil, nil
  227. }
  228. if i := bytes.IndexAny(data, "\r\n"); i >= 0 {
  229. return i + 1, data[:i], nil
  230. }
  231. if atEOF {
  232. return len(data), data, nil
  233. }
  234. return 0, nil, nil
  235. })
  236. for scanner.Scan() {
  237. emit(scanner.Text())
  238. }
  239. }