How I Unblocked a Hanging WinForms + psql Tool
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/1162
Keywords:
ProcessStartInfo
, pipe dead-lock, stdout / stderr,-w
, async I/O
Background
-
App: a custom WinForms “PgBackupRestoreTool”
-
Check: on Connect it spawns
psql -l
to test the connection -
Stack: PostgreSQL 15 on Windows 10
Everything worked on PC-A, but on PC-B the progress bar spun forever.
Manual CLI tests succeeded:
$env:PGPASSWORD = 'admin'
psql -U admin -h localhost -d bks -l # instant response
So the server, credentials and psql
itself were fine.
Re-creating the Hang
string err = p.StandardError.ReadToEnd(); // never returns
-
psql.exe
sat at 0 % CPU – the OS had suspended it. -
No “Password:” prompt appeared.
-
Both machines used PostgreSQL 15; no antivirus alerts.
The only plausible culprit: stdout had filled its pipe buffer, blocking psql, while the parent process waited on stderr.
Root Cause
Step | Action | Result |
---|---|---|
① Tool launches psql -l |
-l prints all database rows to stdout |
More databases → more output |
② Code redirects stderr only | No one consumes stdout | Buffer ≈ 4 KB fills |
③ Buffer full → OS suspends psql | stderr has nothing to read | Parent blocked in ReadToEnd() |
④ UI spins | psql frozen | Looks like a password wait, but it’s a pipe dead-lock |
PC-A simply had fewer databases, so stdout never filled; after I created extra DBs it reproduced the hang.
Problem code (Before)
private bool RunProcessAndCheckSuccess(string exe, IEnumerable<string> args)
{
bool ok = false;
try
{
var psi = new ProcessStartInfo
{
FileName = exe,
RedirectStandardError = true, // stderr only
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)!;
// stdout is NOT redirected → pipe can fill → psql freezes
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;
}
Fixed version (After)
private async Task<bool> RunProcessAndCheckSuccessAsync(string exe, IEnumerable<string> args)
{
try
{
var psi = new ProcessStartInfo
{
FileName = exe,
RedirectStandardOutput = true, // redirect stdout as well
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
psi.Environment["PGPASSWORD"] = PG_PASSWORD;
psi.Environment["PGCLIENTENCODING"] = "UTF8";
psi.ArgumentList.Add("-w"); // skip 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;
}
}
Caller example
// The caller must be async
bool ok = await RunProcessAndCheckSuccessAsync("psql", args);
Key changes
-
Redirect both stdout and stderr to prevent pipe overflow dead-locks.
-
Use
ReadToEndAsync
+WaitForExitAsync
for non-blocking, dead-lock-free I/O. -
Add
-w
(or--no-password
) so psql exits immediately if the password is missing. -
Method signature switched to
async Task<bool>
; a synchronous caller can use
.GetAwaiter().GetResult()
if truly necessary.
After this patch the tool connects instantly, no more infinite spinner regardless of how many databases exist.
Verification
Test | PC-A | PC-B |
---|---|---|
Original DB count | < 200 ms | < 200 ms |
+100 empty DBs | still < 200 ms | still < 200 ms |
No more zombie psql processes.
Lessons Learned
-
Always redirect both stdout and stderr when spawning CLI tools.
-
Prefer async I/O (
ReadToEndAsync
,WaitForExitAsync
) over synchronous reads. -
Use
-w
(or--no-password
) to prevent interactive stalls. -
If an issue appears on one PC only, clone the setup to another machine and diff—environmental gaps surface fast.
Problem solved, tool back to instant connects.🏁
This article was last edited at