fix: drain stdout pipe to prevent project listing deadlock (#106)

The list_gcloud_projects() function piped stdout from `gcloud projects
list` but only read it after the child process exited. When a user has
enough GCP projects that the output exceeds the OS pipe buffer (~64KB),
gcloud blocks on write, the parent blocks waiting for exit, and neither
side makes progress — hitting the 10s timeout.

Spawn a thread to drain stdout concurrently so the pipe buffer never
fills up while the main thread polls for process completion.

Fixes googleworkspace/cli#96
This commit is contained in:
andrew-kline
2026-03-04 22:28:56 -08:00
committed by GitHub
parent d1825f9385
commit a1be14f0c7
2 changed files with 22 additions and 9 deletions
@@ -0,0 +1,11 @@
---
"@googleworkspace/cli": patch
---
fix: drain stdout pipe to prevent project listing timeout during auth setup
Fixed `gws auth setup` timing out at step 3 (GCP project selection) for users
with many projects. The `gcloud projects list` stdout pipe was only read after
the child process exited, causing a deadlock when output exceeded the OS pipe
buffer (~64 KB). Stdout is now drained in a background thread to prevent the
pipe from filling up.
+11 -9
View File
@@ -545,6 +545,16 @@ fn list_gcloud_projects() -> (Vec<(String, String)>, Option<String>) {
Err(e) => return (Vec::new(), Some(format!("Failed to run gcloud: {e}"))),
};
// Drain stdout in a background thread to prevent pipe buffer deadlock.
// Without this, gcloud blocks once the OS pipe buffer (~64 KB) fills up,
// and the parent blocks waiting for gcloud to exit — a classic deadlock.
let stdout = child.stdout.take().expect("stdout was piped");
let reader_handle = std::thread::spawn(move || {
let mut buf = String::new();
std::io::Read::read_to_string(&mut { stdout }, &mut buf).ok();
buf
});
// Wait with timeout
let timeout = std::time::Duration::from_secs(10);
let start = std::time::Instant::now();
@@ -552,15 +562,7 @@ fn list_gcloud_projects() -> (Vec<(String, String)>, Option<String>) {
match child.try_wait() {
Ok(Some(status)) => {
if status.success() {
let stdout = child
.stdout
.take()
.map(|mut s| {
let mut buf = String::new();
std::io::Read::read_to_string(&mut s, &mut buf).ok();
buf
})
.unwrap_or_default();
let stdout = reader_handle.join().unwrap_or_default();
let projects = stdout
.lines()
.filter_map(|line| {