Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 20 additions & 32 deletions sdk/python/pg0/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,38 +144,26 @@ def _run_pg0(*args: str, check: bool = True) -> subprocess.CompletedProcess:
"""Run a pg0 command."""
pg0_path = _find_pg0()
try:
if sys.platform == "win32":
# On Windows, `pg0 start` spawns PostgreSQL which inherits pg0's
# stdio handles. pg0 exits but PostgreSQL keeps writing to those
# handles, so Python's capture_output=True pipes never see EOF
# and subprocess.run hangs forever. Route stdio through real
# files instead — subprocess.run only waits on the process exit
# code, not on file handles held by grandchildren.
with tempfile.TemporaryFile() as out_f, tempfile.TemporaryFile() as err_f:
rc = subprocess.call(
[pg0_path, *args],
stdout=out_f,
stderr=err_f,
# pg0 is a console executable. Suppress the transient console
# window when it is launched from a GUI Python application.
creationflags=subprocess.CREATE_NO_WINDOW,
)
out_f.seek(0)
err_f.seek(0)
stdout = out_f.read().decode("utf-8", errors="replace")
stderr = err_f.read().decode("utf-8", errors="replace")
result = subprocess.CompletedProcess(
args=[pg0_path, *args],
returncode=rc,
stdout=stdout,
stderr=stderr,
)
else:
result = subprocess.run(
[pg0_path, *args],
capture_output=True,
text=True,
)
# `pg0 start` spawns PostgreSQL, which inherits pg0's stdio. pg0 exits
# but PostgreSQL keeps those handles open, so capture_output=True pipes
# never see EOF and subprocess.run hangs forever (seen on Windows and
# Linux). Route stdio through real files instead: subprocess.call only
# waits for pg0 to exit, not for handles held by grandchildren.
# pg0 is a console executable; on Windows, suppress the transient
# console window when launched from a GUI Python application.
extra = {"creationflags": subprocess.CREATE_NO_WINDOW} if sys.platform == "win32" else {}
with tempfile.TemporaryFile() as out_f, tempfile.TemporaryFile() as err_f:
rc = subprocess.call([pg0_path, *args], stdout=out_f, stderr=err_f, **extra)
out_f.seek(0)
err_f.seek(0)
stdout = out_f.read().decode("utf-8", errors="replace")
stderr = err_f.read().decode("utf-8", errors="replace")
result = subprocess.CompletedProcess(
args=[pg0_path, *args],
returncode=rc,
stdout=stdout,
stderr=stderr,
)
if check and result.returncode != 0:
stderr = result.stderr.strip()
if "already running" in stderr.lower():
Expand Down
18 changes: 18 additions & 0 deletions sdk/python/tests/test_pg0.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ def test_windows_runner_hides_console_window(self):

assert call.call_args.kwargs["creationflags"] == subprocess.CREATE_NO_WINDOW

@pytest.mark.skipif(sys.platform == "win32", reason="uses a POSIX shell script")
def test_runner_does_not_wait_for_children_holding_stdio(self, tmp_path):
"""pg0 exits while a child it spawned keeps stdout/stderr open, like the
daemonized PostgreSQL does. The wrapper must return when pg0 exits.

Regression test for https://github.com/vectorize-io/pg0/issues/45.
"""
fake = tmp_path / "pg0"
fake.write_text("#!/bin/sh\necho started\nsleep 30 &\nexit 0\n")
fake.chmod(0o755)

begin = time.monotonic()
with patch.object(pg0, "_find_pg0", return_value=str(fake)):
result = pg0._run_pg0("start")

assert time.monotonic() - begin < 10
assert result.stdout.strip() == "started"

def test_start_stop(self, clean_instance):
"""Test starting and stopping Pg0."""
pg = Pg0(name=TEST_NAME, port=TEST_PORT)
Expand Down
Loading