//go:build darwin package main import ( "bufio" "errors" "fmt" "io" "net" "os" "os/exec" "path/filepath" "strings" "syscall" "time" ) const ( daemonShow = "__show__" daemonExit = "__exit__" ) func runClient(protocolArg string) { socketPath, err := darwinSocketPath() if err != nil { showError("DCAuth 启动失败", err.Error()) return } if hasCommandLineArg("--exit") { if forwardToDarwinDaemon(socketPath, daemonExit) { return } showError("DCAuth 未运行", "当前没有正在运行的 DCAuth 后台进程。") return } message := protocolArg if message == "" { message = daemonShow } if forwardToDarwinDaemon(socketPath, message) { return } if os.Getenv("DCAUTH_BACKGROUND") == "1" { runDarwinDaemon(protocolArg) return } if err := startDarwinDaemon(protocolArg); err != nil { showError("DCAuth 启动失败", err.Error()) } } func runDarwinDaemon(protocolArg string) { lockPath, socketPath, err := darwinDaemonPaths() if err != nil { showError("DCAuth 启动失败", err.Error()) return } lockFile, acquired, err := acquireDaemonLock(lockPath) if err != nil { showError("DCAuth 启动失败", err.Error()) return } if !acquired { message := protocolArg if message == "" { message = daemonShow } if !waitForDarwinDaemon(socketPath, message, 10*time.Second) { showError("DCAuth 启动失败", "已有后台进程正在启动,但暂时无法连接。") } return } defer lockFile.Close() identity, err := loadOrCreateIdentity() if err != nil { showError("DCAuth 初始化失败", err.Error()) return } _ = os.Remove(socketPath) listener, err := net.Listen("unix", socketPath) if err != nil { showError("DCAuth 启动失败", fmt.Sprintf("无法创建后台通信通道:%v", err)) return } defer listener.Close() defer os.Remove(socketPath) _ = os.Chmod(socketPath, 0o600) requests := make(chan string, 16) go acceptDarwinRequests(listener, requests) if protocolArg != "" { handleDarwinAuthorization(identity, protocolArg) } else { notifyDarwin("DCAuth v"+clientVersion, "DCAuth 已在后台运行。首次打开会更新协议绑定。") } for request := range requests { switch request { case daemonExit: return case daemonShow: showDarwinDeviceInfo(identity) default: handleDarwinAuthorization(identity, request) } } } func acceptDarwinRequests(listener net.Listener, requests chan<- string) { for { connection, err := listener.Accept() if err != nil { return } go func(conn net.Conn) { defer conn.Close() scanner := bufio.NewScanner(conn) scanner.Buffer(make([]byte, 1024), 1024*1024) for scanner.Scan() { request := strings.TrimSpace(scanner.Text()) if request != "" { requests <- request } } }(connection) } } func handleDarwinAuthorization(identity identity, raw string) { target, err := parseAuthorizeURL(raw) if err != nil { showError("DCAuth 协议无效", err.Error()) return } app := appRegistry[target.App] if !confirmDarwin( "DCAuth v"+clientVersion+" · "+app.DisplayName+" 本机登录确认", fmt.Sprintf( "是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n\n点击“允许登录”完成验证,点击“取消”拒绝。", app.DisplayName, clientVersion, identity.deviceID, ), ) { return } if err := submitAuthorization(identity, target); err != nil { showError("本机验证失败", err.Error()) return } notifyDarwin("DCAuth v"+clientVersion+" 验证成功", "已确认本机设备,请返回浏览器继续登录。") } func showDarwinDeviceInfo(identity identity) { copyToClipboard(identity.deviceID) showInfo( "DCAuth v"+clientVersion+" 设备信息", fmt.Sprintf( "版本:v%s\n\n设备码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码已复制到剪贴板。", clientVersion, identity.deviceID, ), ) } func startDarwinDaemon(protocolArg string) error { executable, err := os.Executable() if err != nil { return fmt.Errorf("无法定位 DCAuth 程序:%w", err) } args := make([]string, 0, 1) if protocolArg != "" { args = append(args, protocolArg) } command := exec.Command(executable, args...) command.Env = append(os.Environ(), "DCAUTH_BACKGROUND=1") command.SysProcAttr = &syscall.SysProcAttr{Setsid: true} logPath, err := darwinLogPath() if err == nil { if logFile, openErr := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); openErr == nil { defer logFile.Close() command.Stdout = logFile command.Stderr = logFile } } if err := command.Start(); err != nil { return fmt.Errorf("无法启动后台进程:%w", err) } return command.Process.Release() } func acquireDaemonLock(path string) (*os.File, bool, error) { file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600) if err != nil { return nil, false, fmt.Errorf("无法创建后台锁文件:%w", err) } if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { _ = file.Close() if errors.Is(err, syscall.EWOULDBLOCK) || errors.Is(err, syscall.EAGAIN) { return nil, false, nil } return nil, false, fmt.Errorf("无法锁定后台进程:%w", err) } return file, true, nil } func forwardToDarwinDaemon(socketPath, message string) bool { connection, err := net.DialTimeout("unix", socketPath, 250*time.Millisecond) if err != nil { return false } defer connection.Close() _ = connection.SetWriteDeadline(time.Now().Add(time.Second)) _, err = io.WriteString(connection, message+"\n") return err == nil } func waitForDarwinDaemon(socketPath, message string, timeout time.Duration) bool { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if forwardToDarwinDaemon(socketPath, message) { return true } time.Sleep(100 * time.Millisecond) } return false } func darwinDaemonPaths() (string, string, error) { directory, err := darwinDataDir() if err != nil { return "", "", err } return filepath.Join(directory, "daemon.lock"), filepath.Join(directory, "dcauth.sock"), nil } func darwinSocketPath() (string, error) { _, socketPath, err := darwinDaemonPaths() return socketPath, err } func darwinLogPath() (string, error) { directory, err := darwinDataDir() if err != nil { return "", err } return filepath.Join(directory, "dcauth.log"), nil } func darwinDataDir() (string, error) { base, err := os.UserConfigDir() if err != nil { return "", fmt.Errorf("无法定位用户配置目录:%w", err) } directory := filepath.Join(base, "DCAuth") if err := os.MkdirAll(directory, 0o700); err != nil { return "", fmt.Errorf("无法创建 DCAuth 配置目录:%w", err) } return directory, nil } func hasCommandLineArg(target string) bool { for _, arg := range os.Args[1:] { if strings.EqualFold(strings.TrimSpace(arg), target) { return true } } return false } func showInfo(title, text string) { _, _ = runAppleScript( "display dialog " + appleScriptString(text) + " with title " + appleScriptString(title) + " buttons {\"好\"} default button \"好\" with icon note", ) } func showError(title, text string) { _, _ = runAppleScript( "display alert " + appleScriptString(title) + " message " + appleScriptString(text) + " as critical buttons {\"好\"} default button \"好\"", ) } func confirmDarwin(title, text string) bool { output, err := runAppleScript( "display dialog " + appleScriptString(text) + " with title " + appleScriptString(title) + " buttons {\"取消\", \"允许登录\"} default button \"允许登录\" cancel button \"取消\" with icon caution", ) return err == nil && strings.Contains(output, "允许登录") } func notifyDarwin(title, text string) { _, _ = runAppleScript( "display notification " + appleScriptString(text) + " with title " + appleScriptString(title), ) } func runAppleScript(statement string) (string, error) { output, err := exec.Command("osascript", "-e", statement).CombinedOutput() return strings.TrimSpace(string(output)), err } func appleScriptString(text string) string { replacer := strings.NewReplacer( "\\", "\\\\", "\"", "\\\"", "\r", "", "\n", "\\n", ) return `"` + replacer.Replace(text) + `"` } func copyToClipboard(text string) { command := exec.Command("pbcopy") command.Stdin = strings.NewReader(text) _ = command.Run() }