PHP使用exec执行bash脚本时如何展示命令执行进度提示
基于exec的实时输出/进度展示实现方案
原生exec()默认会阻塞到命令执行完毕才返回全量输出,且你提到proc_open存在兼容问题,最稳定的实现方式是后台运行脚本+输出落盘+前端轮询拉取,完全不依赖proc_open,可兼容for循环、嵌套脚本调用等所有复杂bash场景。
核心实现步骤
1. 改造任务触发逻辑
不要把整个脚本内容转义后传给bash -c(这种方式很容易因为转义问题导致循环、嵌套脚本执行中断),改为直接执行脚本文件,将输出重定向到独立临时日志文件,让脚本在后台运行不阻塞当前PHP请求:
// 生成唯一任务ID,绑定独立日志文件避免多用户冲突 $taskId = uniqid('bash_task_', true); $logPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $taskId . '.log'; // 后台启动脚本,全量输出写入日志,同时返回进程PID $execCmd = sprintf( '/usr/local/bin/bash %s > %s 2>&1 & echo $!', escapeshellarg(realpath('../myApp.sh')), escapeshellarg($logPath) ); $processPid = trim(exec($execCmd)); // 任务信息存入会话,供后续状态查询使用 session_start(); $_SESSION['running_task'] = [ 'task_id' => $taskId, 'pid' => $processPid, 'log_path' => $logPath, 'start_at' => time() ]; // 直接返回任务ID给前端,无需等待脚本执行 echo json_encode(['code' => 0, 'task_id' => $taskId]); exit;
2. 编写任务状态查询接口
新增独立接口供前端轮询调用,返回当前任务的运行状态、已生成的日志内容:
session_start(); $task = $_SESSION['running_task'] ?? null; if (!$task) { echo json_encode(['code' => 404, 'msg' => '任务不存在']); exit; } // 检测进程是否仍在运行 $isRunning = file_exists('/proc/' . $task['pid']) ? true : stripos(exec(sprintf('ps -p %d', $task['pid'])), (string)$task['pid']) !== false; // 读取当前已写入的日志内容 $logContent = file_exists($task['log_path']) ? file_get_contents($task['log_path']) : ''; // 任务结束后清理临时文件和会话缓存 if (!$isRunning) { @unlink($task['log_path']); unset($_SESSION['running_task']); } echo json_encode([ 'code' => 0, 'status' => $isRunning ? 'running' : 'finished', 'log' => $logContent, 'cost_time' => time() - $task['start_at'] ]); exit;
3. 前端实时展示实现
页面发起任务启动请求后,通过定时器每秒轮询一次状态接口,将返回的日志实时渲染到页面即可:
- 如果要做纯实时输出:直接把接口返回的
log字段内容追加/替换到页面的日志展示区域即可,效果和终端实时输出一致。 - 如果要做百分比进度条,有两种可选实现方式:
- 低侵入方式:在
myApp.sh的关键执行节点(比如for循环每完成一轮、数据库构建完成、数据导入完成、嵌套脚本调用完成)输出固定格式的进度标记,例如echo "[PROGRESS:25] 数据库构建完成",前端拿到日志后通过正则匹配[PROGRESS:(\d+)]提取百分比,同步更新进度条宽度即可。 - 无侵入方式:提前梳理脚本的总执行步骤,前端匹配日志中对应步骤的关键字(比如匹配到"数据库构建完成"就累加20%进度,匹配到"数据导入完成"累加40%进度),不需要修改原有bash脚本。
- 低侵入方式:在
注意事项
- 运行PHP的进程用户(通常为www-data、nginx)必须拥有
myApp.sh的读/执行权限、临时目录的写入权限,否则脚本无法正常启动。 - 轮询拉取日志时如果日志体积较大,可以优化为按偏移量读取:每次拉取时记录当前日志文件的大小,下次拉取仅读取该偏移量之后的新增内容,减少传输开销。
- 触发任务前先检查会话中是否存在正在运行的任务,避免用户重复刷新页面导致重复启动脚本浪费资源。
- 不要使用把整个脚本内容转义后传入
bash -c的调用方式,这种方式遇到脚本内嵌套引号、循环、外部脚本调用时很容易出现转义错误导致执行中断,直接执行脚本文件本身稳定性高很多。
内容的提问来源于stack exchange,提问作者sysSTD
相关产品推荐
相关产品推荐

