package dingtalkdiscovery import ( "bytes" "context" "fmt" "os" "os/exec" "strings" "time" ) // ExecRunner 是真的 dws 运行器。它只传 dws 需要的白名单环境(与取件器一致): // 漏掉 DWS_CONFIG_DIR / DWS_HOME 时 dws 会找不到配置目录与文件型 keychain,报"未登录"。 type ExecRunner struct { // Binary 是 dws 可执行文件;空值取 DWS_BIN,再取 PATH 里的 dws。 Binary string // Timeout 单条命令超时;为零取 25 秒。 Timeout time.Duration } // RunDWS 运行 `dws ` 并返回 stdout。 func (r ExecRunner) RunDWS(ctx context.Context, args ...string) ([]byte, error) { binary := r.Binary if binary == "" { binary = os.Getenv("DWS_BIN") } if binary == "" { found, err := exec.LookPath("dws") if err != nil { return nil, fmt.Errorf("dws binary not found: %w", err) } binary = found } timeout := r.Timeout if timeout <= 0 { timeout = 25 * time.Second } runCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() command := exec.CommandContext(runCtx, binary, args...) command.Env = dwsEnv() var stdout, stderr bytes.Buffer command.Stdout = &stdout command.Stderr = &stderr if err := command.Run(); err != nil { // 退出码非零时 dws 仍会把错误 JSON 写在 stdout 上,保留它便于上层判定。 if stdout.Len() > 0 { return stdout.Bytes(), fmt.Errorf("dws %s: %w", strings.Join(args, " "), err) } message := strings.TrimSpace(stderr.String()) if message == "" { message = err.Error() } return nil, fmt.Errorf("dws %s: %s", strings.Join(args, " "), message) } return stdout.Bytes(), nil } // dwsEnv 是给 dws 子进程的环境白名单:不把平台的库口令等变量带进子进程。 func dwsEnv() []string { env := make([]string, 0, 8) for _, name := range []string{"PATH", "SSL_CERT_FILE", "LANG"} { if value := os.Getenv(name); value != "" { env = append(env, name+"="+value) } } // dws 的文件型 keychain 固定在 $HOME/.local/share/dws-cli,而 keychain 是挂在 DWS_HOME // 下面的(容器自身的 HOME 指向别处)。因此只给 dws 子进程换 HOME,不动整个进程。 home := os.Getenv("DWS_HOME") if home == "" { home = os.Getenv("HOME") } if home != "" { env = append(env, "HOME="+home) } for _, name := range []string{"DWS_BIN", "DWS_CONFIG_DIR", "DWS_HOME", "DWS_DISABLE_KEYCHAIN"} { if value := os.Getenv(name); value != "" { env = append(env, name+"="+value) } } return env }