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

如何在macOS上异步运行Bash脚本并获取其退出码?

针对Launch Daemon的Bash脚本异步执行方案

问题1:异步等待子进程结束,避免阻塞父进程

在macOS环境下,推荐使用GCD(Grand Central Dispatch)的dispatch_source实现异步子进程监控,代码简洁且适配launch daemon场景,还支持随时取消监控以满足daemon退出需求。

实现代码

#include <dispatch/dispatch.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

// 封装回调参数的结构体
typedef struct {
    void (*callback)(int exitCode, const void* param);
    const void* param;
} CallbackData;

void RunExternalScript(const char* pScriptFilePath,
                       void (*pfnCallback)(int exitCode, const void* pParam),
                       const void* pParam)
{
    pid_t pidChild = fork();
    if (pidChild == 0) {
        // 子进程:修正execl参数格式,第一个参数为程序路径,第二个为argv[0],最终以NULL结尾
        execl(pScriptFilePath, pScriptFilePath, (const char*)NULL);
        
        // 执行失败时输出日志,用_exit而非exit避免触发父进程的atexit钩子
        log("Error %d running script: %s", errno, pScriptFilePath);
        _exit(255); // 约定255为脚本启动失败的退出码
    } else if (pidChild != -1) {
        // 父进程:创建GCD进程监控源
        CallbackData* data = malloc(sizeof(CallbackData));
        data->callback = pfnCallback;
        data->param = pParam;

        dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
                                                          pidChild,
                                                          DISPATCH_PROC_EXIT,
                                                          dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
        if (source) {
            // 设置子进程退出时的回调逻辑
            dispatch_source_set_event_handler(source, ^{
                int exitCode = -1;
                pid_t pid = dispatch_source_get_handle(source);
                int status;
                // 非阻塞获取子进程退出状态
                if (waitpid(pid, &status, WNOHANG) == pid) {
                    if (WIFEXITED(status)) {
                        exitCode = WEXITSTATUS(status);
                    } else if (WIFSIGNALED(status)) {
                        // 子进程被信号终止,用负信号值标记退出码
                        exitCode = -WTERMSIG(status);
                    }
                }
                // 调用用户传入的回调
                data->callback(exitCode, data->param);
                // 清理资源
                dispatch_source_cancel(source);
                free(data);
            });
            // 设置监控源取消时的清理逻辑
            dispatch_source_set_cancel_handler(source, ^{
                dispatch_release(source);
            });
            // 启动监控源
            dispatch_resume(source);
        } else {
            log("Failed to create dispatch source for pid %d", pidChild);
            free(data);
            pfnCallback(-1, pParam);
        }
    } else {
        log("Error forking process: %s", pScriptFilePath);
        pfnCallback(-1, pParam);
    }
}

核心要点

  • 用dispatch_source_create监听DISPATCH_PROC_EXIT事件,子进程退出时自动触发回调
  • 通过waitpid配合WNOHANG非阻塞获取退出状态,避免阻塞GCD队列
  • 支持调用dispatch_source_cancel随时终止监控,适配daemon退出场景
  • 区分正常退出和信号终止两种情况,返回对应退出码

问题2:父进程如何获知子进程启动Bash脚本失败

子进程中execl调用失败无法直接通知父进程,可通过约定退出码或管道传递错误码两种方式实现:

方式1:约定退出码(简易版)

子进程在execl失败时返回约定的退出码(比如255),父进程在回调中判断该值即可确定启动失败:

// 子进程失败时
_exit(255);

// 父进程回调中判断
if (exitCode == 255) {
    // 脚本启动失败
}

方式2:管道传递具体错误码(进阶版)

如果需要获取启动失败的具体errno(比如ENOENT表示脚本不存在、EACCES表示权限不足),可以用管道传递:

#include <dispatch/dispatch.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

typedef struct {
    void (*callback)(int exitCode, int execErrno, const void* param);
    const void* param;
    int pipefd; // 管道读端
} CallbackData;

void RunExternalScript(const char* pScriptFilePath,
                       void (*pfnCallback)(int exitCode, int execErrno, const void* pParam),
                       const void* pParam)
{
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        log("Error creating pipe: %s", pScriptFilePath);
        pfnCallback(-1, errno, pParam);
        return;
    }

    pid_t pidChild = fork();
    if (pidChild == 0) {
        close(pipefd[0]); // 子进程关闭读端
        execl(pScriptFilePath, pScriptFilePath, (const char*)NULL);
        
        // 执行失败,将errno写入管道
        int err = errno;
        write(pipefd[1], &err, sizeof(err));
        close(pipefd[1]);
        _exit(255);
    } else if (pidChild != -1) {
        close(pipefd[1]); // 父进程关闭写端
        CallbackData* data = malloc(sizeof(CallbackData));
        data->callback = pfnCallback;
        data->param = pParam;
        data->pipefd = pipefd[0];

        dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
                                                          pidChild,
                                                          DISPATCH_PROC_EXIT,
                                                          dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
        if (source) {
            dispatch_source_set_event_handler(source, ^{
                int exitCode = -1;
                int execErrno = 0;
                pid_t pid = dispatch_source_get_handle(source);
                int status;
                if (waitpid(pid, &status, WNOHANG) == pid) {
                    if (WIFEXITED(status)) {
                        exitCode = WEXITSTATUS(status);
                        if (exitCode == 255) {
                            // 读取子进程写入的错误码
                            read(data->pipefd, &execErrno, sizeof(execErrno));
                        }
                    } else if (WIFSIGNALED(status)) {
                        exitCode = -WTERMSIG(status);
                    }
                }
                data->callback(exitCode, execErrno, data->param);
                close(data->pipefd);
                dispatch_source_cancel(source);
                free(data);
            });
            dispatch_source_set_cancel_handler(source, ^{
                close(data->pipefd);
                dispatch_release(source);
                free(data);
            });
            dispatch_resume(source);
        } else {
            log("Failed to create dispatch source for pid %d", pidChild);
            close(pipefd[0]);
            free(data);
            pfnCallback(-1, errno, pParam);
        }
    } else {
        log("Error forking process: %s", pScriptFilePath);
        close(pipefd[0]);
        close(pipefd[1]);
        pfnCallback(-1, errno, pParam);
    }
}

核心要点

  • 子进程通过管道将execl失败的errno传递给父进程
  • 父进程在回调中读取管道内容,获取具体错误原因

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 05:30:21