1. 求解器无输出且进程静默退出:定位入口参数与日志断点

现象拆解:静默退出发生在哪一层

求解器启动后终端无任何输出、进程以非零或零码直接退出,这类现象的本质是可观测性在某个环节被切断。常见切断点有三类:命令行参数解析阶段提前 exit、日志系统尚未初始化就发生异常、异常被过宽的 catch 边界吞掉。排障目标不是猜原因,而是按启动顺序逐层确认“最后一个有输出的位置”和“第一个失效的位置”。

启动链路:从进程入口到求解主循环 ① 参数解析 argv / 配置文件 --model --tol --log ② 日志初始化 open log file set level / sink ③ 模型加载 read / parse model build matrix ④ 求解主循环 presolve → iterate write solution 日志断点(应在此处看到输出) B1: 参数解析后 B2: 日志就绪后 B3: 模型加载后 B4: 进入主循环 断点判读:最后可见输出 → 首个失效环节 B1 之前无输出 参数解析提前 exit / 参数非法未打印 B1 有 B2 无 日志文件不可写 / 路径不存在 / 初始化抛异常 B2 有 B3 无 模型读取异常被 catch 吞掉 / 未刷新缓冲区 B3 有 B4 无 主循环入口条件不满足 / 预求解静默返回 异常捕获边界检查 catch(...) { return -1; } 吞异常 → 无日志 → 静默退出 修复:catch 内至少写 stderr 并 flush 日志缓冲

第一步:用参数解析自检确认入口是否放行

先排除“参数没被正确接收”这一层。以常见的 C++ 求解器入口为例,在解析完成后立即向 stderr 打印解析结果,不依赖日志系统,因为此时日志可能还没初始化:

int main(int argc, char** argv) {
    Options opt;
    try {
        opt = parseArgs(argc, argv);          // 可能抛异常或内部 exit
    } catch (const std::exception& e) {
        std::fprintf(stderr, "[FATAL] parseArgs: %s\n", e.what());
        return 2;
    }
    std::fprintf(stderr, "[TRACE] model=%s tol=%g log=%s\n",
                 opt.modelPath.c_str(), opt.tol, opt.logPath.c_str());
    // 后续日志初始化……
}

若连 [TRACE] 都看不到,说明进程在 parseArgs 内部就退出了。此时重点检查:是否有 std::exit() 被直接调用、是否在参数缺失时静默 return、是否 --help 分支误触发。把 parseArgs 中所有提前返回点改为抛出带消息的异常,是恢复可观测性的最小改动。

第二步:核对日志初始化顺序与可写性

日志初始化失败是“有参数、无输出”的高频原因。典型错误顺序是:先打开日志文件、再校验路径,或日志对象构造失败后异常被上层吞掉。正确的初始化应满足三点:

  • 日志路径在打开前先做存在性与可写性检查,失败时写 stderr 而非静默返回;
  • 日志初始化完成后立即写一条 [TRACE] logger ready 并 flush(),作为 B2 断点;
  • 日志级别默认不低于 INFO,避免 --quiet 类参数把全部输出压掉。
bool initLogger(const std::string& path) {
    std::ofstream probe(path, std::ios::app);
    if (!probe) {
        std::fprintf(stderr, "[FATAL] log not writable: %s\n", path.c_str());
        return false;
    }
    probe.close();
    Logger::instance().open(path);
    Logger::instance().setLevel(Level::INFO);
    LOG_INFO("logger ready, path=%s", path.c_str());
    Logger::instance().flush();   // 关键:确保断点可见
    return true;
}

第三步:收紧异常捕获边界,让首个失效点自己说话

静默退出的最后一层原因是过宽的 catch。检查主流程中是否存在 catch (...) { return -1; } 这类写法。修复原则是:捕获后必须至少向 stderr 输出异常类型与 what(),并在返回前 flush 所有日志 sink。对模型加载段,建议单独包裹并标注阶段名:

try {
    model = loadModel(opt.modelPath);
    LOG_INFO("model loaded: vars=%zu cons=%zu", model.numVars(), model.numCons());
} catch (const std::exception& e) {
    LOG_ERROR("stage=loadModel path=%s err=%s", opt.modelPath.c_str(), e.what());
    Logger::instance().flush();
    std::fprintf(stderr, "[FATAL] loadModel: %s\n", e.what());
    return 3;
}
定位顺序建议固定为:B1 参数解析后 → B2 日志就绪后 → B3 模型加载后 → B4 进入主循环。每次只加一个断点并重跑,第一个“看不到”的断点前一段就是失效环节。这样比一次性加满日志更容易判断因果。
若进程退出码为 0 且完全无输出,优先怀疑参数解析中的 --help/--version 分支被误命中,或日志级别被配置为 OFF。退出码非零但无消息,则优先检查 catch 吞异常与日志未 flush 两种情况。

验证恢复效果

完成上述三处修改后,正常启动应依次看到 [TRACE] model=...、logger ready、model loaded: vars=... cons=... 三条输出。若仍静默,用 strace -f -e trace=openat,write ./solver ... 观察是否在打开日志或模型文件时即失败,从而把问题从“代码逻辑”收敛到“文件系统权限或路径”。