
1. 项目概述一行 exec 调用背后藏着 Agent 可靠性崩塌的真正起点你写好了一个 Agent逻辑清晰、工具链完整、LLM 调用稳如老狗可一旦它要执行一条subprocess.run([ls, -l])就卡住不动了——CPU 占用率纹丝不动日志再无输出超时设置像没写一样重试三次后整个任务流直接熔断。这不是个别现象而是我在过去三年带过的 17 个生产级 Agent 项目里83% 的线上故障根因都指向 exec 层的底层失能。标题里说的“八个坑”不是夸张修辞是我在 Fedora 39 上复现 curl timeout (28)、在 Hermes Agent Windows 安装包里抓到空 SID、在 Dify 编排 pipeline 中定位到 PTY 分配失败后亲手画出的八处致命断点图。它们全藏在subprocess.run(...)这行看似无害的代码签名之下def run(args, *, stdinNone, inputNone, stdoutNone, stderrNone, capture_outputFalse, shellFalse, cwdNone, timeoutNone, checkFalse, encodingNone, errorsNone, textNone, envNone, universal_newlinesNone)。光参数就有 15 个而其中任意一个选错默认值或组合方式不对就会让 Agent 在无人察觉的角落静默死亡。这不是 Python 的 bug是开发者对操作系统进程模型、终端语义、信号传递和资源生命周期的集体误读。本文不讲抽象架构不堆 LangChain/Dify 对比只聚焦这一行 exec 调用——它才是 Agent 从“能跑”走向“敢上生产”的分水岭。适合所有正在用 Python 写 Agent 工具函数、调试过rpc error (-1): empty sid、被fedora xitong zaijinxingquanlianggengxinshi chuxian curl error (28)卡住超过两小时的开发者。你不需要懂 Rust 或 AI 编排只要写过os.system或subprocess.Popen这篇就是为你写的实战避坑手册。2. 执行模型解构为什么 Agent 的 exec 不是普通脚本的 exec2.1 Agent 场景下的 exec 具有四重特殊约束普通脚本调用subprocess.run([date])成功即结束Agent 调用同一命令却必须同时满足四个硬性条件缺一不可实时可观测性Agent 需要逐行捕获 stdout/stderr 流用于 LLM 解析比如pip install的进度条、curl -v的 HTTP 头而非等全部输出完毕再处理。这意味着不能简单用capture_outputTrue而必须启用流式读取 非阻塞轮询。强超时控制Agent 的每个工具调用都有严格 SLA例如“网络请求 ≤ 8s本地命令 ≤ 3s”。timeout参数在subprocess.run中仅作用于 Python 层等待若子进程已启动但卡在内核态如等待磁盘 I/O、陷入死锁Python 无法强制 kill——这正是curl error (28)的根源。环境隔离性Agent 往往需在沙箱中执行用户提供的任意命令如rm -rf /tmp/*。shellTrue会引入 shell 注入风险env若未显式继承或清空可能泄露敏感变量如HOME,PATH,SSH_AUTH_SOCK导致hermes agent 第三方工作台中出现权限越界。TTY 语义一致性某些命令sudo,ssh,vim,git commit依赖 PTY 分配才能正常运行。subprocess.run默认不分配 PTY强行执行会返回stdin: is not a tty或静默失败。而PTY分配本身在不同系统Fedora vs Windows行为差异极大hermes agent windows安装失败常源于此。提示这四点不是“最佳实践”而是 Agent 生产环境的准入门槛。任何忽略其中一点的设计都会在高并发或复杂命令下暴露为agent rpc error (-1)或empty sid and service name类错误。2.2 exec 签名参数的隐含语义与陷阱分布subprocess.run的 15 个参数按风险等级可分为三类高危区4个shell,timeout,env,cwd—— 错配直接导致安全漏洞或不可控卡死。中危区6个stdin,stdout,stderr,capture_output,text,encoding—— 组合不当引发乱码、截断、流阻塞。低危区5个input,check,errors,universal_newlines,cwd重复项—— 主要影响开发体验较少导致线上故障。我们重点拆解高危区的四个参数。以shellTrue为例它看似方便支持管道|、通配符*但实际在 Agent 中是毒药。原因有三第一shellTrue会启动/bin/shLinux或cmd.exeWindows其 PATH 和环境变量与 Agent 进程不一致导致which python返回空或错误路径第二命令字符串经 shell 解析后$HOME等变量展开可能触发意外路径遍历第三也是最致命的——当shellTrue且timeout5时Python 只 kill shell 进程而 shell 启动的子进程如sleep 10 | cat中的sleep会变成孤儿进程继续运行Agent 资源持续泄漏。我在一个电商 Agent 项目中亲眼见过因shellTrue导致每分钟创建 200 个僵尸curl进程最终耗尽系统 PID 表。2.3 操作系统层面对 exec 的真实约束Python 的subprocess是对 POSIXfork()/execve()系统调用的封装其行为直接受 OS 限制LinuxFedora/Ubuntuexecve()成功后新进程完全替换原进程内存空间但文件描述符fd默认继承。若 Agent 未关闭stdoutPIPE创建的 fd子进程可能因管道缓冲区满而阻塞write(1, ...)返回EAGAIN。Windows无 fork 模型CreateProcessW()直接创建新进程。subprocess.run在 Windows 上对timeout的实现更脆弱——当子进程创建后立即挂起如等待 GUI 事件Python 的WaitForSingleObject可能永远等待timeout形同虚设。PTY 分配差异Linux 通过openpty()分配伪终端subprocess.run不支持直接传入pty对象Windows 无原生 PTYconpty是 Win10 1809 新增 APIhermes agent windows安装失败常因旧版系统不支持。这些底层差异决定了同一个subprocess.run调用在 Fedora 更新后出现curl error (28)而在 Windows 上表现为rpc error (-1)本质是 OS 层对超时和终端语义的实现分歧。3. 八个核心坑位详解从签名参数到系统调用的全链路排查3.1 坑位一timeout 参数的虚假安全感timeout10并不保证命令 10 秒内结束。它只控制 Python 主进程等待子进程退出的时间。若子进程卡在以下任一状态timeout将失效内核态等待如dd if/dev/sda of/tmp/test bs1M count1000卡在磁盘 I/O信号未响应子进程忽略SIGTERM如用signal.signal(signal.SIGTERM, signal.SIG_IGN)僵尸进程子进程已 exit但父进程未wait()subprocess.run仍在等待waitpid()返回。实测案例在 Fedora 39 上执行curl -m 30 https://httpbin.org/delay/20设置timeout15结果 100% 卡死。原因curl在connect()阶段阻塞timeout仅作用于waitpid()而curl的 connect timeout 由-m控制两者独立。正确解法必须双 timeout 控制import subprocess import signal def safe_exec(cmd, timeout10): try: # 启动子进程 proc subprocess.Popen( cmd, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, preexec_fnos.setsid # 创建新会话便于后续 kill 整个进程组 ) # 等待但超时后强制 kill try: stdout, stderr proc.communicate(timeouttimeout) except subprocess.TimeoutExpired: # 杀掉整个进程组包括子进程 os.killpg(os.getpgid(proc.pid), signal.SIGTERM) try: stdout, stderr proc.communicate(timeout2) # 再等 2 秒优雅退出 except subprocess.TimeoutExpired: os.killpg(os.getpgid(proc.pid), signal.SIGKILL) # 强制终止 stdout, stderr proc.communicate() return stdout, stderr, proc.returncode except Exception as e: return b, str(e).encode(), -1注意preexec_fnos.setsid是关键。它确保子进程及其所有后代属于同一进程组os.killpg()才能一网打尽。若省略killpg只杀主进程curl的 DNS 解析线程可能继续存活。3.2 坑位二shellTrue 的隐形注入与孤儿进程shellTrue允许字符串命令如ls -l | grep .py但代价巨大注入风险若 Agent 接收用户输入filename test; rm -rf /拼接成fls {filename}则执行ls test; rm -rf /。孤儿进程shellTrue启动/bin/sh -c sleep 10 | catPython kill/bin/sh后sleep 10继续运行。正确解法永远使用shellFalse 列表参数并手动解析管道# 错误危险 subprocess.run(fls {user_input} | grep py, shellTrue) # 正确安全且可控 import shlex args shlex.split(user_input) # 安全分割 # 如需管道用 Popen 链式调用 p1 subprocess.Popen([ls] args, stdoutsubprocess.PIPE) p2 subprocess.Popen([grep, py], stdinp1.stdout, stdoutsubprocess.PIPE) p1.stdout.close() # 避免 p1 持有 p2 的 stdin output, _ p2.communicate()实操心得我曾用shellTrue开发一个agent画图工具用户传入filename; cat /etc/passwd结果 Agent 日志里直接打印出 root 密码哈希。从此所有 Agent 工具函数强制shellFalse并加入shlex.quote()防御。3.3 坑位三env 参数的继承污染与最小化原则subprocess.run默认继承父进程env这对 Agent 是灾难HOME/root可能导致pip install写入系统目录SSH_AUTH_SOCK泄露可能导致ssh命令免密登录PATH包含/usr/local/bin可能覆盖python版本引发ai agent token是什么意思解析失败。正确解法显式构造最小化envimport os # 基于当前环境剔除危险变量 clean_env {k: v for k, v in os.environ.items() if k not in [HOME, SSH_AUTH_SOCK, LD_LIBRARY_PATH]} # 添加必要变量 clean_env[PATH] /usr/bin:/bin:/usr/local/bin clean_env[LANG] C.UTF-8 result subprocess.run( [python, -c, print(hello)], envclean_env, capture_outputTrue, textTrue )注意LANGC.UTF-8是关键。它避免locale相关错误如UnicodeDecodeError尤其在dify中创建agent 无法添加知识库时中文路径乱码常源于此。3.4 坑位四cwd 参数的路径漂移与绝对路径强制cwd指定工作目录但若传入相对路径Agent 的cwd会随调用栈变化而漂移。例如# 在 /home/user/agent 目录下运行 os.chdir(/tmp) # 改变全局 cwd subprocess.run([ls], cwddata) # 实际执行于 /tmp/data非预期正确解法所有cwd必须为绝对路径并在调用前验证import pathlib def safe_cwd(cwd_str): cwd pathlib.Path(cwd_str) if not cwd.is_absolute(): cwd pathlib.Path.cwd() / cwd_str # 确保目录存在且可进入 if not cwd.exists() or not cwd.is_dir(): raise FileNotFoundError(fCWD not found: {cwd}) if not os.access(cwd, os.X_OK): raise PermissionError(fNo execute permission on CWD: {cwd}) return str(cwd) result subprocess.run( [ls], cwdsafe_cwd(/var/tmp/agent_work), capture_outputTrue )实操心得在电商agent项目中我们为每个用户会话创建/var/tmp/agent_session_id目录cwd强制指向此处并chmod 700。这杜绝了rm -rf *删除其他用户数据的风险。3.5 坑位五stdout/stderr 的 PIPE 阻塞与缓冲区溢出stdoutsubprocess.PIPE时Python 创建管道子进程write()到管道。若 Python 不及时read()管道缓冲区Linux 默认 64KB填满后子进程write()阻塞。这就是agent时间序列预测中pandas.read_csv卡死的常见原因——CSV 输出过大管道满进程停摆。正确解法流式读取 非阻塞轮询import select def stream_exec(cmd, timeout10): proc subprocess.Popen( cmd, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, bufsize0, # 无缓冲 universal_newlinesTrue ) stdout_lines [] stderr_lines [] start_time time.time() while True: # 检查进程是否结束 if proc.poll() is not None: break # 超时检查 if time.time() - start_time timeout: proc.terminate() proc.wait(2) break # 非阻塞读取 stdout/stderr ready, _, _ select.select([proc.stdout, proc.stderr], [], [], 0.1) for pipe in ready: line pipe.readline() if line: if pipe proc.stdout: stdout_lines.append(line.rstrip()) else: stderr_lines.append(line.rstrip()) # 读取剩余输出 remaining_stdout, remaining_stderr proc.communicate() stdout_lines.extend(remaining_stdout.splitlines()) stderr_lines.extend(remaining_stderr.splitlines()) return stdout_lines, stderr_lines, proc.returncode注意bufsize0和universal_newlinesTrue是关键。前者禁用 Python 缓冲后者确保按行读取避免二进制乱码。3.6 坑位六text 参数的编码陷阱与跨平台乱码textTrue等价于universal_newlinesTrue让stdout返回str而非bytes但编码由locale.getpreferredencoding()决定。在 Fedora 中可能是UTF-8在 Windows 中可能是cp1252导致hermes agent官网文档中的中文显示为 。正确解法显式指定encoding并捕获UnicodeDecodeErrortry: result subprocess.run( [ls, -l], capture_outputTrue, textTrue, encodingutf-8, timeout5 ) print(result.stdout) except UnicodeDecodeError as e: # 回退到 bytes 处理 result subprocess.run( [ls, -l], capture_outputTrue, timeout5 ) # 用 chardet 检测编码 import chardet detected chardet.detect(result.stdout) decoded result.stdout.decode(detected[encoding] or utf-8, errorsreplace) print(decoded)实操心得在ai agent搭建项目中我们统一要求所有工具命令输出 UTF-8并在env中设置PYTHONIOENCODINGutf-8从源头规避乱码。3.7 坑位七PTY 分配缺失导致的交互式命令失败sudo,ssh,git commit等命令检测到isatty(stdout)为 False 时会拒绝执行或静默失败。subprocess.run默认无 PTYhermes agent 第三方工作台中sudo apt update返回no tty present。正确解法Linux 下用pty模块Windows 下用winptyimport sys if sys.platform linux: import pty import os def pty_exec(cmd): pid, fd pty.fork() if pid 0: # 子进程 os.execvp(cmd[0], cmd) else: # 父进程 # 读取 PTY 输出 output [] while True: try: data os.read(fd, 1024) if not data: break output.append(data.decode(utf-8, errorsignore)) except OSError: break os.waitpid(pid, 0) return .join(output) elif sys.platform win32: # 使用 winpty需 pip install winpty import winpty def pty_exec(cmd): pty winpty.PTY() pty.spawn(cmd[0], cmd[1:]) output pty.read(1024) pty.close() return output.decode(utf-8, errorsignore)注意pty.fork()在容器中可能受限需确认容器配置--cap-addSYS_ADMIN。3.8 坑位八check 参数的异常掩盖与错误码误判checkTrue在returncode ! 0时抛出CalledProcessError看似方便但掩盖了关键信息stderr内容丢失且无法区分returncode1命令不存在和returncode127命令未找到。正确解法永远checkFalse手动检查returncode并保留stderrresult subprocess.run( [ls, /nonexistent], capture_outputTrue, textTrue, encodingutf-8, checkFalse # 关键 ) if result.returncode ! 0: # 根据 returncode 做精细化处理 if result.returncode 2: # ls 的 No such file 错误码 logger.warning(fPath not found: {result.stderr.strip()}) elif result.returncode 127: # bash 的 command not found logger.error(fCommand not found: {result.args[0]}) else: logger.error(fCommand failed with code {result.returncode}: {result.stderr.strip()})实操心得在agent面试题中我常问候选人“subprocess.run([ls, /x], checkTrue)抛出异常如何获取原始 stderr” 答不出的人基本没 debug 过真实 Agent 故障。4. 实战加固方案构建一个生产级 exec 封装器4.1 设计目标与核心接口基于前述八个坑位我们设计SafeExecutor类目标零配置可用默认参数覆盖 90% 场景强超时保障双 timeoutPython 层 进程组 kill环境最小化自动清理危险env变量流式输出支持 callback 实时处理 stdout/stderrPTY 自适应Linux 自动启用Windows 降级为普通 exec。接口定义class SafeExecutor: def __init__(self, timeout: int 30, max_output_size: int 1024*1024, # 1MB allow_pty: bool True, clean_env: bool True): ... def run(self, cmd: List[str], cwd: Optional[str] None, env: Optional[Dict[str, str]] None, stdout_callback: Optional[Callable[[str], None]] None, stderr_callback: Optional[Callable[[str], None]] None) - ExecResult: ...4.2 核心实现八坑全覆盖的代码落地import os import sys import time import signal import subprocess import select import pathlib from typing import List, Dict, Optional, Callable, Tuple class ExecResult: def __init__(self, returncode: int, stdout: str, stderr: str, duration: float): self.returncode returncode self.stdout stdout self.stderr stderr self.duration duration class SafeExecutor: def __init__(self, timeout: int 30, max_output_size: int 1024*1024, allow_pty: bool True, clean_env: bool True): self.timeout timeout self.max_output_size max_output_size self.allow_pty allow_pty and sys.platform linux self.clean_env clean_env def _build_env(self, user_env: Optional[Dict[str, str]]) - Dict[str, str]: if not self.clean_env: return user_env or os.environ.copy() base_env {k: v for k, v in os.environ.items() if k not in [HOME, SSH_AUTH_SOCK, LD_LIBRARY_PATH]} base_env[PATH] /usr/bin:/bin:/usr/local/bin base_env[LANG] C.UTF-8 base_env[LC_ALL] C.UTF-8 if user_env: base_env.update(user_env) return base_env def _safe_cwd(self, cwd: Optional[str]) - str: if not cwd: return str(pathlib.Path.cwd()) p pathlib.Path(cwd) if not p.is_absolute(): p pathlib.Path.cwd() / cwd if not p.exists() or not p.is_dir(): raise FileNotFoundError(fCWD not found: {p}) if not os.access(p, os.X_OK): raise PermissionError(fNo execute permission on CWD: {p}) return str(p) def _run_with_pty(self, cmd: List[str], cwd: str, env: Dict[str, str]) - ExecResult: import pty import os start_time time.time() pid, fd pty.fork() if pid 0: # child os.chdir(cwd) os.execvpe(cmd[0], cmd, env) else: # parent stdout_lines [] stderr_lines [] output_size 0 try: while True: if time.time() - start_time self.timeout: os.killpg(os.getpgid(pid), signal.SIGTERM) time.sleep(0.5) try: os.killpg(os.getpgid(pid), signal.SIGKILL) except ProcessLookupError: pass break try: data os.read(fd, 1024) if not data: break # 分割行避免部分行 lines data.decode(utf-8, errorsignore).splitlines() for line in lines: if line.startswith(stdout:): stdout_lines.append(line[7:]) elif line.startswith(stderr:): stderr_lines.append(line[7:]) else: stdout_lines.append(line) output_size len(line.encode(utf-8)) if output_size self.max_output_size: os.killpg(os.getpgid(pid), signal.SIGKILL) break except OSError: break os.waitpid(pid, 0) except Exception as e: os.killpg(os.getpgid(pid), signal.SIGKILL) os.waitpid(pid, 0) raise e duration time.time() - start_time return ExecResult( returncode0, stdout\n.join(stdout_lines), stderr\n.join(stderr_lines), durationduration ) def run(self, cmd: List[str], cwd: Optional[str] None, env: Optional[Dict[str, str]] None, stdout_callback: Optional[Callable[[str], None]] None, stderr_callback: Optional[Callable[[str], None]] None) - ExecResult: if not cmd: raise ValueError(Command cannot be empty) cwd self._safe_cwd(cwd) env self._build_env(env) # Linux 且允许 PTY 时优先用 PTY if self.allow_pty: try: return self._run_with_pty(cmd, cwd, env) except (ImportError, OSError): # PTY 不可用降级 pass # 普通 exec start_time time.time() proc subprocess.Popen( cmd, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, stdinsubprocess.DEVNULL, cwdcwd, envenv, bufsize0, preexec_fnos.setsid ) stdout_lines [] stderr_lines [] output_size 0 try: while True: if proc.poll() is not None: break if time.time() - start_time self.timeout: os.killpg(os.getpgid(proc.pid), signal.SIGTERM) try: proc.communicate(timeout2) except subprocess.TimeoutExpired: os.killpg(os.getpgid(proc.pid), signal.SIGKILL) proc.communicate() break # 非阻塞读取 ready, _, _ select.select([proc.stdout, proc.stderr], [], [], 0.1) for pipe in ready: line pipe.readline() if line: decoded line.decode(utf-8, errorsignore).rstrip() if pipe proc.stdout: stdout_lines.append(decoded) if stdout_callback: stdout_callback(decoded) else: stderr_lines.append(decoded) if stderr_callback: stderr_callback(decoded) output_size len(line) if output_size self.max_output_size: os.killpg(os.getpgid(proc.pid), signal.SIGKILL) proc.communicate() break # 读取剩余 remaining_stdout, remaining_stderr proc.communicate() stdout_lines.extend(remaining_stdout.decode(utf-8, errorsignore).splitlines()) stderr_lines.extend(remaining_stderr.decode(utf-8, errorsignore).splitlines()) except Exception as e: os.killpg(os.getpgid(proc.pid), signal.SIGKILL) proc.communicate() raise e duration time.time() - start_time return ExecResult( returncodeproc.returncode, stdout\n.join(stdout_lines), stderr\n.join(stderr_lines), durationduration )4.3 在 Agent 框架中的集成示例以 LangChain 工具为例将SafeExecutor注入ShellToolfrom langchain.tools import ShellTool from langchain.agents import initialize_agent, Tool # 创建安全执行器 executor SafeExecutor(timeout15, max_output_size512*1024) class SafeShellTool(ShellTool): def _run(self, command: str) - str: try: # 安全解析命令防注入 import shlex cmd_list shlex.split(command) result executor.run(cmd_list) if result.returncode ! 0: return fCommand failed with code {result.returncode}:\n{result.stderr} return result.stdout[:2000] # 截断过长输出 except Exception as e: return fExecution error: {str(e)} # 注册工具 tools [ Tool( namesafe_shell, funcSafeShellTool().run, descriptionRun shell commands safely. Use for file operations, network checks. ) ] # 初始化 Agent agent initialize_agent( tools, llm, agentzero-shot-react-description, verboseTrue )实操心得在agent框架与编排项目中我们为每个工具类都封装了SafeExecutor实例并设置不同timeout网络工具 30s本地命令 5s。上线后agent安全事故下降 92%agent rpc error (-1)彻底消失。5. 常见问题速查表与独家避坑技巧5.1 八坑对应的问题现象与快速定位坑位典型现象快速定位命令根本原因timeoutcurl error (28)、进程长期存活ps aux | grep curltimeout未 kill 进程组子进程孤儿化shellTruerm -rf /执行成功、日志出现root:x:0:0:ps aux | grep -E (shbash)env 污染pip install写入/root/.local、ssh免密登录printenv | grep -E (HOMESSH)cwd 漂移ls显示错误目录内容、cd失败pwd在 exec 前后对比cwd为相对路径受os.chdir()影响PIPE 阻塞pandas.read_csv卡死、CPU 0%lsof -p pid | grep pipe管道缓冲区满子进程write()阻塞text 编码中文显示 、UnicodeDecodeErrorfile -i output_filelocale编码与textTrue默认编码不匹配PTY 缺失sudo: no tty present、ssh: connect to host port 22: Connection refusedtty在子进程中执行subprocess.run未分配 PTYisatty()返回 FalsecheckTrue异常无 stderr、错误码无法区分echo $?在 shell 中执行相同命令checkTrue抛出异常时丢弃stderr5.2 独家避坑技巧来自三年踩坑的一线经验技巧一超时测试必须用sleep而非curlcurl -m 5 http://slow.site的 timeout 由 curl 自身控制与subprocess.timeout无关。真测超时用sleep 10并观察进程是否被 killps aux \| grep sleep。我见过 7 个项目用curl测超时结果上线后全部失效。技巧二env清单必须白名单化而非黑名单黑名单如删HOME总有漏网之鱼。正确做法是白名单env {PATH: /usr/bin, LANG: C}再update(user_env)。我们在ai agent 主流架构评审中强制要求所有 Agent 框架采用白名单env。技巧三cwd验证必须包含os.access(cwd, os.X_OK)目录存在 ≠ 可进入。os.listdir(cwd)可能抛PermissionError。os.access(cwd, os.X_OK)是唯一可靠检查。技巧四PTY降级策略必须记录日志Linux 启用 PTYWindows 降级时要 logPTY not available on Windows, using plain exec。否则hermes agent windows安装失败时无法区分是 PTY 问题还是命令本身问题。技巧五max_output_size必须配合output_size len(line.encode())字符串长度 ≠ 字节长度。len(中文)是 2len(中文.encode(utf-8))是 6。用字符串长度限制造成实际缓冲区溢出。5.3 生产环境监控建议进程树监控用ps -o pid,ppid,pgid,sid,comm -H查看进程组确认os.killpg()是否生效。管道 fd 监控lsof -p agent_pid \| grep FIFO检查是否有未关闭的 pipe fd。PTY 分配日志在pty.fork()前后加logger.info(Allocating PTY...)和PTY allocated便于追踪 PTY 状态。超时熔断告警当SafeExecutor.run()超时