WinForms + psql 卡死排查全纪录

| .NET | 0 Reads

关键词ProcessStartInfoWaitForExit、管道缓冲区、stdout/stderr、-w、异步读取


背景

  • 工具:自写的 WinForms “PgBackupRestoreTool”

  • 功能:按钮点击 → 调用 psql -l 检测连接

  • 环境:PostgreSQL 15,Windows 10


问题现象

电脑 行为
A(旧) 一切正常,点 Connect 秒出数据库列表
B(新) 进度条一直转。任务管理器里堆着 0 % CPU 的 psql.exe

手动在 PowerShell 中:

$env:PGPASSWORD='admin'
psql -U admin -h localhost -d bks -l      # 秒连

—— 服务器与账号密码均无异常。


初步定位

  1. 阻塞语句

    string err = p.StandardError.ReadToEnd();   // 不返回
    
  2. 子进程状态

    • psql.exe CPU 0 % → OS 把它挂起

    • 没有弹出“Password”提示

  3. 环境变量对比
    没有 PGPASSWORD 冲突,psql 版本皆为 15.x

排除了“密码/版本/杀毒”后,怀疑 stdout 写满无人读 → 管道死锁


核心原因

  • psql -l 会把 所有数据库 列表写到 stdout

  • 原代码只重定向 stderr,stdout 留给默认管道;

  • 数据库增多后,stdout ≈ 4 KB 填满缓冲区 → psql 被 OS 挂起;

  • 父进程在 ReadToEnd()stderr 结束,双方互卡。


解决方案

  1. 同时重定向 stdout & stderr

  2. 异步并发读取,永不堵塞

  3. 强制 -w,若密码没带上立即失败

  4. 如不需要数据库列表,可改用 -qAt -c "SELECT 1",输出更小

【问题代码(Before)】

private bool RunProcessAndCheckSuccess(string exe, IEnumerable<string> args)
{
    bool ok = false;
    try
    {
        var psi = new ProcessStartInfo
        {
            FileName = exe,
            RedirectStandardError = true,   // 只重定向 stderr
            UseShellExecute = false,
            CreateNoWindow = true
        };

        psi.Environment["PGPASSWORD"]       = PG_PASSWORD;
        psi.Environment["PGCLIENTENCODING"] = "UTF8";
        foreach (var a in args) psi.ArgumentList.Add(a);

        using var p = Process.Start(psi)!;

        // 只读 stderr,stdout 无人读取,输出大时会堵塞
        string err = p.StandardError.ReadToEnd();
        p.WaitForExit();

        ok = p.ExitCode == 0;
        if (!ok && !string.IsNullOrEmpty(err))
            Log(err.Trim(), Color.Red);
    }
    catch (Exception ex)
    {
        Log($"RunProcessAndCheckSuccess error: {ex.Message}", Color.Red);
    }
    return ok;
}

【修正版(After)】

private async Task<bool> RunProcessAndCheckSuccessAsync(string exe, IEnumerable<string> args)
{
    try
    {
        var psi = new ProcessStartInfo
        {
            FileName               = exe,
            RedirectStandardOutput = true,   // 同时重定向 stdout
            RedirectStandardError  = true,
            UseShellExecute        = false,
            CreateNoWindow         = true
        };

        psi.Environment["PGPASSWORD"]       = PG_PASSWORD;
        psi.Environment["PGCLIENTENCODING"] = "UTF8";

        psi.ArgumentList.Add("-w");                 // 不提示密码
        foreach (var a in args) psi.ArgumentList.Add(a);

        using var p = Process.Start(psi)!;

        var stdTask = p.StandardOutput.ReadToEndAsync();
        var errTask = p.StandardError .ReadToEndAsync();
        await Task.WhenAll(stdTask, errTask, p.WaitForExitAsync());

        if (!string.IsNullOrWhiteSpace(stdTask.Result))
            Log(stdTask.Result.Trim(), Color.Black);

        if (p.ExitCode != 0 && !string.IsNullOrWhiteSpace(errTask.Result))
            Log(errTask.Result.Trim(), Color.Red);

        return p.ExitCode == 0;
    }
    catch (Exception ex)
    {
        Log($"RunProcessAndCheckSuccessAsync error: {ex.Message}", Color.Red);
        return false;
    }
}

【调用示例】

// 调用方法本身需为 async
bool ok = await RunProcessAndCheckSuccessAsync("psql", args);

【核心变化】

  1. 同时重定向 stdout 和 stderr,避免 stdout 填满管道导致死锁。

  2. 使用 ReadToEndAsync + WaitForExitAsync 并发读取,UI 不再阻塞。

  3. -w,缺少密码时立即返回错误,不进入交互模式。

  4. 方法签名改为 async Task<bool>;同步代码可用 .GetAwaiter().GetResult() 强行等待。

替换后,无论数据库数量多少,工具都能瞬间完成连接测试,不再出现进度条无限旋转。

 

最终代码(精简版)

private async Task<bool> RunProcessAndCheckSuccessAsync(string exe, IEnumerable<string> args)
{
    var psi = new ProcessStartInfo
    {
        FileName               = exe,
        RedirectStandardOutput = true,
        RedirectStandardError  = true,
        UseShellExecute        = false,
        CreateNoWindow         = true
    };

    psi.Environment["PGPASSWORD"]       = PG_PASSWORD;
    psi.Environment["PGCLIENTENCODING"] = "UTF8";

    psi.ArgumentList.Add("-w");                 // no password prompt
    foreach (var a in args) psi.ArgumentList.Add(a);

    using var p = Process.Start(psi)!;

    var stdTask = p.StandardOutput.ReadToEndAsync();
    var errTask = p.StandardError .ReadToEndAsync();
    await Task.WhenAll(stdTask, errTask, p.WaitForExitAsync());

    if (!string.IsNullOrWhiteSpace(stdTask.Result))
        Log(stdTask.Result.Trim(), Color.Black);

    if (p.ExitCode != 0 && !string.IsNullOrWhiteSpace(errTask.Result))
        Log(errTask.Result.Trim(), Color.Red);

    return p.ExitCode == 0;
}

调用:

bool ok = await RunProcessAndCheckSuccessAsync("psql", args);

复测结果

步骤 电脑 A 电脑 B
点击 Connect <200 ms 出结果 同步 <200 ms 出结果
新建 100 个空库再测 仍然秒级返回 仍然秒级返回

再也没有卡死现象。


教训 & 建议

  1. 父子进程通信

    • 只要 RedirectStandardError=true最好同时重定向 stdout;

    • 否则输出量大时极易写满缓冲区。

  2. 永远用异步 I/O
    ReadToEndAsync + WaitForExitAsync 可彻底规避死锁。

  3. -w 是免费保险
    忘带密码时不再进入交互,而是立即返回错误。

  4. 问题复现越多机越快
    单机调不好时,拷贝到另一台对比差异是排雷捷径。

就此,WinForms + pg 工具的“卡圈圈”事件完美收官 🏁

This article was last edited at