WinForms+psql 工具卡死完整排查筆記
Copyright Notice: This article is an original work licensed under the CC 4.0 BY-NC-ND license.
If you wish to repost this article, please include the original source link and this copyright notice.
Source link: https://v2know.com/article/1161
關鍵詞:
ProcessStartInfo
、管道死鎖、stdout/stderr、-w
、非同步 I/O
背景
-
工具:自行開發的 WinForms「PgBackupRestoreTool」
-
功能:點擊 Connect → 呼叫
psql -l
測試連線 -
環境:PostgreSQL 15、Windows 10
同一程式在 PC-A 正常、在 PC-B 進度條無限旋轉。
手動 CLI 測試卻一切 OK:
$env:PGPASSWORD = 'admin'
psql -U admin -h localhost -d bks -l # 立即回應
伺服器與帳號密碼皆正常。
重現卡死
string err = p.StandardError.ReadToEnd(); // 永遠不返回
-
psql.exe
CPU 0 % → OS 將其暫停 -
未出現「Password:」提示
-
兩機
psql
版本相同,無防毒攔截
判斷為 stdout 填滿匿名管道→psql 被掛起,而父行程卡在 ReadToEnd()
。
根本原因
階段 | 動作 | 結果 |
---|---|---|
① 工具執行 psql -l |
-l 把所有資料庫列表寫到 stdout |
庫數愈多、輸出愈大 |
② 程式僅重定向 stderr | stdout 無人讀取 | 約 4 KB 填滿管道 |
③ 管道滿 → OS 停 psql | stderr 無資料可讀 | Parent 卡在 ReadToEnd() |
④ UI 只剩轉圈 | psql 僵住 | 看似等密碼,實為死鎖 |
PC-A 庫較少,stdout 未塞滿;新建多個 DB 後亦能重現。
修正要點
-
同時重定向 stdout 和 stderr
-
非同步並發讀流,杜絕阻塞
-
加上
-w
,若缺密碼立刻失敗 -
若不需 DB 列表,可改
-qAt -c "SELECT 1"
,輸出極小
下面把 「問題程式碼」(Before) 與 「修正版」(After) 同時列出,方便「前後對照」。
Before - 會卡在 ReadToEnd()
的版本
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();
// ② psql stdout 寫滿後被 OS 暫停 → 這裡永遠不往下走
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"); // 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;
}
catch (Exception ex)
{
Log($"RunProcessAndCheckSuccessAsync error: {ex.Message}", Color.Red);
return false;
}
}
呼叫端範例
// 在 async 方法內
bool ok = await RunProcessAndCheckSuccessAsync("psql", args);
核心差異一覽
舊版 | 新版 |
---|---|
只重定向 stderr | 同時重定向 stdout / stderr |
同步 ReadToEnd() → 容易堵塞 |
ReadToEndAsync() + WaitForExitAsync() |
無 -w → 密碼缺失時進入互動等待 |
-w 讓 psql 立即報錯退出 |
方法為同步 (bool ) |
方法改為 async Task<bool> ,不阻塞 UI |
將這段 After 取代舊實作、並把舊呼叫改成 await RunProcessAndCheckSuccessAsync(...)
即可完全解決「進度條無限轉」問題。
最終程式碼(純英文註解)
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");
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);
驗證
測試 | PC-A | PC-B |
---|---|---|
原本庫數 | < 200 ms | < 200 ms |
+100 空庫 | 同樣 < 200 ms | 同樣 < 200 ms |
再無僵屍 psql 行程。
經驗總結
-
啟動 CLI 工具時,stdout 與 stderr 最好一起重定向。
-
優先使用非同步 I/O:
ReadToEndAsync
+WaitForExitAsync
。 -
-w
/--no-password
能防止未帶密碼時進入互動等待。 -
單機抓不到 bug 時,把程式搬到另一台機器比對環境差異,很快能定位問題。
問題圓滿解決,工具再次秒速連線 🏁
This article was last edited at