You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift命令行工具:主进程退出时终止子Process进程

解决Swift命令行工具主进程终止时子进程残留的问题

我完全懂你的困扰——手动终止主进程后,用Process启动的子进程(比如tail -f)还赖在系统里,确实挺闹心。这是因为默认情况下,子进程和主进程是独立的进程实体,主进程退出后子进程会被系统的init进程接管,不会自动跟着终止。下面结合你的现有代码,给你一套彻底解决的方案:

1. 监听终止信号,主动清理子进程

命令行工具被手动终止(Xcode停止按钮、终端Ctrl+C)时,会收到SIGINT、SIGTERM这类终止信号。我们可以注册信号处理逻辑,在收到信号时主动终止子进程,甚至连带它启动的所有子进程一起清理。

具体代码修改:

在你的MainClass里添加信号源存储和处理逻辑:

class MainClass {
    private var task: Process?
    private var errorHandler: FileHandle?
    private var outputHandler: FileHandle?
    // 保存信号源引用,防止被提前释放
    private var signalSources: [DispatchSourceSignal] = []

    init(...) {
        // 你的现有初始化代码(启动子进程、设置管道等)
        setupSignalHandlers()
    }

    private func setupSignalHandlers() {
        // 监听常见的终止信号
        let terminationSignals: [Int32] = [SIGINT, SIGTERM, SIGQUIT]
        for signal in terminationSignals {
            let signalSource = DispatchSource.makeSignalSource(signal: signal, queue: DispatchQueue.global())
            signalSource.setEventHandler { [weak self] in
                guard let self = self else { return }
                print("主进程收到终止信号,正在清理子进程...")
                // 先尝试优雅终止子进程
                self.task?.terminate()
                // 关键:如果子进程还启动了孙子进程(比如sh启动tail),杀掉整个进程组
                if let childPid = self.task?.processIdentifier {
                    // 先发送SIGTERM尝试优雅退出
                    killpg(childPid, SIGTERM)
                    // 0.5秒后强制发送SIGKILL,确保彻底终止
                    DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
                        killpg(childPid, SIGKILL)
                    }
                }
                exit(signal)
            }
            signalSource.resume()
            self.signalSources.append(signalSource)
        }
    }

    // 可选:类销毁时也尝试清理子进程
    deinit {
        task?.terminate()
        if let pid = task?.processIdentifier {
            killpg(pid, SIGTERM)
        }
    }

    // 你的其他现有方法...
}

2. 为什么要用killpg?

你启动的是/bin/sh tail -f,这里sh是直接子进程,tail是sh启动的孙子进程。如果只调用task.terminate(),只会杀掉sh,tail会变成孤儿进程被init接管,继续运行。

killpg会给整个进程组发送信号,sh和它启动的tail属于同一个进程组,这样就能一次性清理所有相关进程,彻底解决残留问题。

3. 保留原有逻辑的兼容性

你之前用DispatchSemaphore处理子进程出错的逻辑可以完全保留,现在加上信号处理后,不管是子进程出错主动触发退出,还是主进程被手动终止,子进程都会被正确清理。

测试一下:在Xcode里运行工具后点击停止按钮,或者终端运行后按Ctrl+C,去活动监视器里看看tail和sh是不是都消失了~

内容的提问来源于stack exchange,提问作者Lupurus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 03:27:31