Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
57 changes: 50 additions & 7 deletions windows/db_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@

_USE_PGSERVER = None

_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
Comment thread
NeptuneHub marked this conversation as resolved.


def _check_pgserver():
"""Return True if pgserver can be imported (has a Windows wheel)."""
Expand Down Expand Up @@ -91,6 +93,7 @@ def _preinit_scram(data_dir, password):
"--auth-host=scram-sha-256", "--auth-local=scram-sha-256",
"--encoding=utf8", f"--pwfile={pwfile}"],
check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
stdin=subprocess.DEVNULL, creationflags=_NO_WINDOW,
)
finally:
if pwfile and os.path.exists(pwfile):
Expand Down Expand Up @@ -159,9 +162,49 @@ def _clear_stale_data_dir(data_dir):
shutil.rmtree(data_dir, ignore_errors=True)


def _patch_pgserver_pg_ctl():
"""Make pgserver's ``pg_ctl start`` survive a double-click (tray) launch.

The bundled pgserver starts the cluster with ``pg_ctl -w start`` under a
hardcoded 10s :func:`subprocess.run` timeout and with no Windows creation
flags, so the spawned ``postgres`` inherits the launching console. That is
fine for ``AudioMuse-AI.exe start`` (supervisor on the main thread of a real
console), but not for a double-click, where the supervisor boots from a
daemon thread under a hidden console: the inherited console stalls
``pg_ctl``'s readiness wait past 10s, the timeout fires, and the half-started
postgres is left orphaned holding the data dir -- bricking every later start.

Replace the ``pg_ctl`` symbol pgserver calls with a wrapper that runs every
``start`` detached from any console and with a generous timeout. Idempotent;
a no-op off Windows.
"""
if os.name != "nt":
return
import pgserver.postgres_server as ps
if getattr(ps, "_audiomuse_pg_ctl_patched", False):
return
original = ps.pg_ctl
timeout = paths.pg_start_timeout()

def pg_ctl(args, **kwargs):
if "start" in args:
if kwargs.get("timeout") is None or kwargs["timeout"] < timeout:
kwargs["timeout"] = timeout
kwargs.setdefault("stdin", subprocess.DEVNULL)
kwargs["creationflags"] = kwargs.get("creationflags", 0) | _NO_WINDOW
env = dict(kwargs.get("env") or os.environ)
env.setdefault("PGCTLTIMEOUT", str(timeout))
kwargs["env"] = env
return original(args, **kwargs)

ps.pg_ctl = pg_ctl
ps._audiomuse_pg_ctl_patched = True
Comment thread
NeptuneHub marked this conversation as resolved.
Outdated


def start_embedded(data_dir):
pw = paths.db_password()
if _check_pgserver():
_patch_pgserver_pg_ctl()
import database
fresh = not os.path.exists(os.path.join(data_dir, "PG_VERSION"))
if fresh:
Expand All @@ -170,14 +213,13 @@ def start_embedded(data_dir):
try:
uri = database.start_embedded(data_dir)
except Exception:
if os.path.isdir(data_dir):
logger.warning("PostgreSQL data dir stale after crash — clearing and retrying")
import shutil
shutil.rmtree(data_dir, ignore_errors=True)
_preinit_scram(data_dir, pw)
uri = database.start_embedded(data_dir)
else:
if not fresh:
raise
logger.warning("Fresh PostgreSQL cluster failed to start — clearing and retrying once")
import shutil
shutil.rmtree(data_dir, ignore_errors=True)
_preinit_scram(data_dir, pw)
uri = database.start_embedded(data_dir)
if not fresh:
_harden_existing(data_dir, pw, uri)
return _conn_from_uri(uri, pw)
Expand All @@ -188,6 +230,7 @@ def start_embedded(data_dir):
def ensure_embedded_running(data_dir):
pw = paths.db_password()
if _check_pgserver():
_patch_pgserver_pg_ctl()
import database
return _conn_from_uri(database.ensure_embedded_running(data_dir), pw)
from windows import embedded_pg
Expand Down
12 changes: 12 additions & 0 deletions windows/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ def pg_port():
return 5432


def pg_start_timeout():
"""Seconds to wait for embedded PostgreSQL to report ready before giving up.

pgserver's built-in 10s is too short when the supervisor boots from the tray
app's daemon thread under a hidden console, and on a cold first start while
Windows Defender scans the freshly extracted binaries. A premature timeout
orphans postgres.exe holding the data dir and bricks the next start, so allow
a generous window.
"""
return 120


def redis_url():
"""Password-bearing URL for the embedded Redis (loopback TCP, scram-equivalent gate)."""
return f"redis://:{quote(redis_password(), safe='')}@127.0.0.1:{redis_port()}/0"
Expand Down
Loading