// croc_service.go — Croc 命令行传输工具的 Wails 服务。 // // croc 是一款点对点加密文件传输 CLI:发送端执行 `croc send <文件>` 会生成 // 一个传输码(如 mug-aids-amend),接收端执行 `croc <传输码>` 即可接收。 // // 本服务自动在系统 PATH 中查找 croc(exec.LookPath),找到才允许前端展示 // 「Croc 传送」工具卡片;发送/接收共用的密码保存在设置中(AppSettings.Croc)。 // 设置了密码时,发送改用 `croc send --code <密码>`,接收端用同一密码作为 // 传输码,双方只要密码一致即可完成传输。 // 注意:croc 的 `--pass` 是中继服务器密码(公共中继固定为 pass123),不能用它 // 承载用户密码,否则会连不上公共中继。 package main import ( "bufio" "bytes" "context" "errors" "fmt" "io" "os/exec" "regexp" "strings" "sync" "time" ) // CrocConfig 是 Croc 传送的配置(发送/接收共用密码)。 type CrocConfig struct { Password string `json:"password"` // 发送/接收共用密码:作为 croc 传输码(--code),空=使用随机传输码 Dir string `json:"dir"` // 接收保存目录,空=当前工作目录 } // CrocInfo 描述本机 croc 的可用性,用于前端决定是否展示工具卡片。 type CrocInfo struct { Available bool `json:"available"` // PATH 中是否找到 croc Path string `json:"path"` // croc 可执行文件路径 Version string `json:"version"` // 版本信息(croc --version 输出) } // CrocService 是 Croc 传送功能的 Wails 服务。 type CrocService struct { mu sync.Mutex sendCancel context.CancelFunc recvCancel context.CancelFunc } // crocAnsiRe 匹配终端颜色控制符,输出到界面前先剥离。 var crocAnsiRe = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]") // NewCrocService 创建 Croc 服务实例。 func NewCrocService() *CrocService { return &CrocService{} } // Available 检测 PATH 中是否存在 croc,并返回其路径与版本。 func (s *CrocService) Available() CrocInfo { path, err := exec.LookPath("croc") if err != nil || path == "" { return CrocInfo{} } info := CrocInfo{Available: true, Path: path} ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() cmd := exec.CommandContext(ctx, path, "--version") hideProcessWindow(cmd) if out, err := cmd.Output(); err == nil { info.Version = strings.TrimSpace(string(out)) } return info } // GetConfig 读取当前 Croc 配置(每次从设置文件读取,保证与设置页保存结果一致)。 func (s *CrocService) GetConfig() CrocConfig { return loadSettings().Croc } // StartSend 开始发送文件:croc --yes [--code 密码] send <文件...>。 // 设置了密码时用密码作为传输码(双方一致即可),否则由 croc 生成随机传输码。 // 日志通过 croc:log 事件推送。 func (s *CrocService) StartSend(files []string) error { if len(files) == 0 { return errors.New("请先选择要发送的文件") } path, err := exec.LookPath("croc") if err != nil { return errors.New("未找到 croc,请先安装并加入系统 PATH") } s.mu.Lock() if s.sendCancel != nil { s.mu.Unlock() return errors.New("已有发送任务正在进行") } ctx, cancel := context.WithCancel(context.Background()) s.sendCancel = cancel args := append(s.baseArgs(), "send") if pw := s.password(); pw != "" { if len(pw) < 6 { s.sendCancel = nil cancel() s.mu.Unlock() return errors.New("Croc 密码至少 6 个字符(croc 对传输码长度的限制)") } args = append(args, "--code", pw) // 密码即传输码:发送/接收共用 } args = append(args, files...) cmd := exec.CommandContext(ctx, path, args...) hideProcessWindow(cmd) if err := s.streamProcess(cmd, "send"); err != nil { s.sendCancel = nil cancel() s.mu.Unlock() return fmt.Errorf("启动发送失败:%w", err) } s.mu.Unlock() return nil } // StartReceive 开始接收:croc --yes [--out 目录] <传输码>。 // 传输码为空但设置中配置了密码时,自动把密码当作传输码,便于双方只用同一密码。 func (s *CrocService) StartReceive(code, dir string) error { code = strings.TrimSpace(code) if code == "" { code = s.password() // 密码即传输码:接收端可只配置密码 } if code == "" { return errors.New("请输入对方发来的传输码(或在设置中配置 Croc 传送密码)") } if code == s.password() && len(code) < 6 { return errors.New("Croc 密码至少 6 个字符(croc 对传输码长度的限制)") } path, err := exec.LookPath("croc") if err != nil { return errors.New("未找到 croc,请先安装并加入系统 PATH") } s.mu.Lock() if s.recvCancel != nil { s.mu.Unlock() return errors.New("已有接收任务正在进行") } ctx, cancel := context.WithCancel(context.Background()) s.recvCancel = cancel args := s.baseArgs() if dir = strings.TrimSpace(dir); dir != "" { args = append(args, "--out", dir) } args = append(args, code) cmd := exec.CommandContext(ctx, path, args...) hideProcessWindow(cmd) if err := s.streamProcess(cmd, "recv"); err != nil { s.recvCancel = nil cancel() s.mu.Unlock() return fmt.Errorf("启动接收失败:%w", err) } s.mu.Unlock() return nil } // StopSend 终止发送任务(若在运行)。 func (s *CrocService) StopSend() { s.mu.Lock() cancel := s.sendCancel s.mu.Unlock() if cancel != nil { cancel() } } // StopReceive 终止接收任务(若在运行)。 func (s *CrocService) StopReceive() { s.mu.Lock() cancel := s.recvCancel s.mu.Unlock() if cancel != nil { cancel() } } // baseArgs 返回发送/接收共用的命令行参数:自动确认所有提示。 func (s *CrocService) baseArgs() []string { return []string{"--yes"} } // password 返回设置中保存的发送/接收共用密码(已去空格)。 func (s *CrocService) password() string { return strings.TrimSpace(s.GetConfig().Password) } // streamProcess 启动进程并把 stdout/stderr 逐行推送给前端(croc:log)。 // 调用方需持有 s.mu。 func (s *CrocService) streamProcess(cmd *exec.Cmd, kind string) error { stdout, err := cmd.StdoutPipe() if err != nil { return err } stderr, err := cmd.StderrPipe() if err != nil { return err } if err := cmd.Start(); err != nil { return err } emit := func(raw string) { line := strings.TrimSpace(crocAnsiRe.ReplaceAllString(raw, "")) if line == "" { return } app.Event.Emit("croc:log", map[string]string{"kind": kind, "line": line}) } go scanCrocStream(stdout, emit) go scanCrocStream(stderr, emit) go func() { waitErr := cmd.Wait() exitCode := 0 if waitErr != nil { var ee *exec.ExitError if errors.As(waitErr, &ee) { exitCode = ee.ExitCode() } else { exitCode = -1 } } s.mu.Lock() if kind == "send" { s.sendCancel = nil } else { s.recvCancel = nil } s.mu.Unlock() app.Event.Emit("croc:exit", map[string]any{"kind": kind, "code": exitCode}) }() return nil } // scanCrocStream 按 \r 或 \n 切分输出(croc 的进度用 \r 刷新),逐段回调。 func scanCrocStream(r io.Reader, emit func(string)) { scanner := bufio.NewScanner(r) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) { if atEOF && len(data) == 0 { return 0, nil, nil } if i := bytes.IndexAny(data, "\r\n"); i >= 0 { return i + 1, data[:i], nil } if atEOF { return len(data), data, nil } return 0, nil, nil }) for scanner.Scan() { emit(scanner.Text()) } }