diff --git a/App/lib/Phase/BuildPipeline.vp b/App/lib/Phase/BuildPipeline.vp index 350afb4..38c23f0 100644 --- a/App/lib/Phase/BuildPipeline.vp +++ b/App/lib/Phase/BuildPipeline.vp @@ -314,6 +314,44 @@ def write_ir_to_file(ir_buf: bytes, ir_len: sizet, output_dir: str, module_name: return 0 +# 外部命令(llc/opt)捕获输出打印上限(字节):超出即截断,避免一次失败刷爆日志 +CMD_OUTPUT_MAX: t.CDefine = 8192 + + +def _log_captured_output(log_path: str, module_sha1: str, cmd: str, + exit_code: int, tag: str) -> None: + """读取外部命令(llc/opt)的输出重定向文件并打印到日志。 + + 内容 = 该命令的 stdout|stderr(_run_cmd_wait 把二者合并重定向到同一文件)。 + 拿不到输出(无文件 / 打开失败 / 为空)时明确说明,不伪造。打印后删除该文件。 + """ + if log_path is None: + VLogger.error(f"命令输出未捕获(无重定向文件): module={module_sha1} rc={exit_code} cmd={cmd}", tag) + return + f: fileio.File | t.CPtr = fileio.File(log_path, fileio.MODE.R) + if f.closed: + VLogger.error(f"命令输出文件打开失败(无捕获): module={module_sha1} rc={exit_code} path={log_path} cmd={cmd}", tag) + w32.win32file.DeleteFileA(log_path) + return + sz: int64 = f.size() + cap: sizet = CMD_OUTPUT_MAX + 1 + buf: bytes = _mbuddy.alloc(cap) + if buf is None: + f.close() + VLogger.error(f"命令输出缓冲分配失败(无捕获): module={module_sha1} rc={exit_code} cmd={cmd}", tag) + w32.win32file.DeleteFileA(log_path) + return + n: int64 = f.read_all(buf, cap) + f.close() + if n <= 0: + VLogger.error(f"命令输出为空(stderr|stdout 均无内容): module={module_sha1} rc={exit_code} cmd={cmd}", tag) + elif sz > CMD_OUTPUT_MAX: + VLogger.error(f"命令输出(前 {CMD_OUTPUT_MAX} 字节 / 共 {sz} 字节,已截断)module={module_sha1} rc={exit_code}:\n{buf}\n命令: {cmd}", tag) + else: + VLogger.error(f"命令输出({sz} 字节)module={module_sha1} rc={exit_code}:\n{buf}\n命令: {cmd}", tag) + w32.win32file.DeleteFileA(log_path) + + def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: str, cc_flags: str) -> int: """调用 llc 将 .ll 编译为 .obj @@ -387,12 +425,20 @@ def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: s return 1 viperlib.snprintf(cmd, cmd_len, "%s %s -o %s %s", cc_cmd, cc_flags, obj_path, compile_input) - # 用 stdlib.system() 直接在控制台运行,输出直接显示 - # 避免 subprocess 管道捕获丢失输出 - sys_ret: int = stdlib.system(cmd) - if sys_ret != 0: - VLogger.error(f"编译失败 (module={module_name}, cmd={cmd})", "LLC") + # 运行 llc,捕获 stdout|stderr 到临时文件:失败时把输出 + 完整命令 + 退出码 + # 一并打印(此前用 stdlib.system 直连控制台,无法在日志里带出输出)。 + # _run_cmd_wait 用 CreateProcessA 直启目标程序(避开 cmd/c 在 asan 下挂起), + # 输出重定向到文件 —— 文件不会被写满阻塞,故无管道死锁;退出码同步返回。 + log_path: str = _mbuddy.alloc(len(compile_input) + 16) + if log_path is None: return 1 + viperlib.snprintf(log_path, len(compile_input) + 16, "%s.llc.log", compile_input) + sys_ret: int = _run_cmd_wait(cmd, log_path) + if sys_ret != 0: + VLogger.error(f"编译失败 (module={module_name})", "LLC") + _log_captured_output(log_path, module_name, cmd, sys_ret, "LLC") + return 1 + w32.win32file.DeleteFileA(log_path) return 0 @@ -402,8 +448,9 @@ def compile_ll_to_obj(ir_path: str, output_dir: str, module_name: str, cc_cmd: s # 线程安全约束: # - LlcTask 及命令串一律 stdlib.malloc:主池 mb 会被 reset_to(mark2) # 回收,线程运行期间字符串悬空 → 概率崩溃 -# - worker 只调 stdlib.system(),不做任何池分配(MemBuddy 分配非线程安全) -# - 依赖 CRT system() 的多线程安全性(ThreadingTest 已验证) +# - worker 只调 _run_cmd_wait(内部仅 stdlib.malloc/CreateProcessA/ +# CreateFileA/CloseHandle),不做任何池分配(MemBuddy 分配非线程安全) +# - 依赖 CreateProcessA + Win32 句柄接口的多线程安全性 # ============================================================ # llc 并行最大线程数(参考 TPC Phase2Translator n_workers=os.cpu_count(),本机 16 核) LLC_MAX_THREADS: t.CDefine = 16 @@ -415,6 +462,10 @@ class LlcTask: LlcCmd: bytes # llc 命令 ModuleSha1: bytes # 模块 SHA1(报错用) Result: int # worker 写:0 成功,非 0 失败 + # 以下字段追加在结尾:既有字段偏移不变;LlcTask.__sizeof__() 随之增大, + # 所有构造/初始化点均按 __sizeof__() 算首址并对整条 memset,无需改偏移。 + LogPath: bytes # worker 输出重定向文件路径(stdlib.malloc) + ExitCode: int # worker 写:失败命令退出码(成功为 0) def init_llc_task(task: LlcTask | t.CPtr, ir_path: str, output_dir: str, @@ -433,6 +484,12 @@ def init_llc_task(task: LlcTask | t.CPtr, ir_path: str, output_dir: str, if task.ModuleSha1 is None: return 1 string.strcpy(task.ModuleSha1, module_name) + # 输出重定向文件路径:.llc.log(与 .ll 同目录,每任务唯一) + lp_len: sizet = len(ir_path) + 16 + task.LogPath = stdlib.malloc(lp_len) + if task.LogPath is None: + return 1 + viperlib.snprintf(task.LogPath, lp_len, "%s.llc.log", ir_path) # llc 命令 cmd_len: sizet = len(cc_cmd) + len(cc_flags) \ + len(obj_path) + len(ir_path) + 64 @@ -471,6 +528,9 @@ def free_llc_task(task: LlcTask | t.CPtr): if task.ModuleSha1 is not None: stdlib.free(task.ModuleSha1) task.ModuleSha1 = None + if task.LogPath is not None: + stdlib.free(task.LogPath) + task.LogPath = None def _phase2_write_cmd(cmd: str) -> None: @@ -497,14 +557,17 @@ def _phase2_write_cmd(cmd: str) -> None: f.close() -def _run_cmd_wait(cmd: str) -> int: +def _run_cmd_wait(cmd: str, log_path: str = None) -> int: """CreateProcessA 同步执行命令并等待退出,返回退出码。 替代 stdlib.system:Windows asan 环境下 cmd /c 经 CreateProcess 受 RTL allocator hook 干扰可能挂起(LLVM issue #111166 同类,ClassArrayTest - llc 固定卡死根因);CreateProcessA 直接执行目标程序并继承父进程控制台 - (stdout/stderr 直接显示,同 system 语义)。全 stdlib.malloc/栈分配, - worker 线程安全。 + llc 固定卡死根因);CreateProcessA 直接执行目标程序。 + + log_path 非 None 时把子进程 stdout|stderr 重定向到该文件(STARTF_USE- + STDHANDLES,二者合并写同一文件),供失败时读取打印;为 None 时继承父控制台 + (输出直接显示,同 system 语义)。重定向只用 CreateFileA/CloseHandle,不做 + 池分配,worker 线程安全;文件不会写满阻塞,无管道死锁。 """ if cmd is None: return -1 @@ -527,11 +590,28 @@ def _run_cmd_wait(cmd: str) -> int: sa.lpSecurityDescriptor = t.CPtr(0) sa.bInheritHandle = 1 si.cb = win32process.STARTUPINFOA.__sizeof__() - # create_flags=0:子进程继承父控制台(输出直接显示,同 system 语义) + # 输出重定向:sa 可继承 → 文件句柄随 CreateProcessA 复制给子进程;打开失败 + # 则不设 STARTF_USESTDHANDLES,退回继承控制台(不伪造捕获,日志会明说)。 + log_fh: w32.win32base.HANDLE = w32.win32base.INVALID_HANDLE_VALUE + if log_path is not None: + log_fh = w32.win32file.CreateFileA(log_path, w32.win32file.GENERIC_WRITE, + w32.win32file.FILE_SHARE_READ, + t.CPtr(c.Addr(sa)), + w32.win32file.CREATE_ALWAYS, + w32.win32file.FILE_ATTRIBUTE_NORMAL, + t.CPtr(0)) + if log_fh != w32.win32base.INVALID_HANDLE_VALUE: + si.dwFlags = win32process.STARTF_USESTDHANDLES + si.hStdOutput = log_fh + si.hStdError = log_fh + # create_flags=0:无重定向时子进程继承父控制台;有重定向时 stdio 走文件 ok: int = win32process.CreateProcessA( t.CPtr(0), cmd_line, t.CPtr(0), t.CPtr(0), 1, 0, t.CPtr(0), t.CPtr(0), t.CPtr(c.Addr(si)), t.CPtr(c.Addr(pi))) stdlib.free(cmd_line) + # 子进程已持有文件句柄副本,父侧立即关闭,避免句柄泄漏 + if log_fh != w32.win32base.INVALID_HANDLE_VALUE: + w32.win32base.CloseHandle(log_fh) if ok == 0: return -1 win32sync.WaitForSingleObject(pi.hProcess, w32.win32base.INFINITE) @@ -543,17 +623,20 @@ def _run_cmd_wait(cmd: str) -> int: def _llc_task_worker(arg: t.CPtr) -> t.CPtr: - """llc 线程入口:只调 stdlib.system(),不做任何分配(线程安全) - Result 仅在命令链执行完后写入(初始 -1;join 超时保持 -1 → 报失败)""" + """llc 线程入口:只调 _run_cmd_wait(),不做任何池分配(线程安全) + Result 仅在命令链执行完后写入(初始 -1;join 超时保持 -1 → 报失败); + ExitCode 记录失败命令的退出码,供主线程打印""" task: LlcTask | t.CPtr = (LlcTask | t.CPtr)(arg) if task is None: return t.CPtr(0) if task.OptCmd is not None: - opt_rc: int = _run_cmd_wait(task.OptCmd) + opt_rc: int = _run_cmd_wait(task.OptCmd, task.LogPath) if opt_rc != 0: + task.ExitCode = opt_rc task.Result = 1 return t.CPtr(0) - llc_rc: int = _run_cmd_wait(task.LlcCmd) + llc_rc: int = _run_cmd_wait(task.LlcCmd, task.LogPath) + task.ExitCode = llc_rc if llc_rc != 0: task.Result = 1 else: @@ -620,8 +703,16 @@ def run_llc_parallel(tasks: bytes, task_count: int, max_threads: int) -> int: tsk2: LlcTask | t.CPtr = (LlcTask | t.CPtr)(t.CVoid( uint64(tasks) + sizet(batch_start + j) * slot_size, t.CPtr)) if tsk2 is not None and tsk2.Result != 0: - VLogger.error(f"llc 编译失败: {(tsk2.ModuleSha1 if tsk2.ModuleSha1 is not None else "?")} (cmd={(tsk2.LlcCmd if tsk2.LlcCmd is not None else "?")})", "LLC") + # 失败:打印模块 sha1 + 退出码 + 完整命令,再读重定向文件打印 + # llc 的 stderr|stdout(.ll 路径即命令末参数) + VLogger.error(f"llc 编译失败: {(tsk2.ModuleSha1 if tsk2.ModuleSha1 is not None else "?")} (exit={tsk2.ExitCode} cmd={(tsk2.LlcCmd if tsk2.LlcCmd is not None else "?")})", "LLC") + if tsk2.ModuleSha1 is not None and tsk2.LlcCmd is not None: + _log_captured_output(tsk2.LogPath, tsk2.ModuleSha1, tsk2.LlcCmd, tsk2.ExitCode, "LLC") ret = 1 + else: + # 成功:丢弃捕获文件(成功路径零额外输出) + if tsk2 is not None and tsk2.LogPath is not None: + w32.win32file.DeleteFileA(tsk2.LogPath) return ret