From 7db3ee073c95027089e93dffe4d7f046168c58e7 Mon Sep 17 00:00:00 2001 From: Nilay Yadav Date: Wed, 29 Jul 2026 04:14:21 +0530 Subject: [PATCH] Match a pre-upgrade studio.pid to the blocked port before falling back --- studio/backend/run.py | 33 +++++++++++- studio/backend/tests/test_studio_pid_files.py | 51 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/studio/backend/run.py b/studio/backend/run.py index b85b01a81b..8c347da0cc 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -875,7 +875,38 @@ def _own_studio_on_port(port: int, host: str) -> "int | None": # None (unverifiable) counts as ours: refusing beats a silent duplicate. if _pid_is_studio_backend(pid, [created]) is not False: return pid - return None + return _legacy_studio_on_port(port) + + +def _legacy_studio_on_port(port: int) -> "int | None": + """A pre-upgrade server recorded only its PID, so match it to the listener. + + Falling back past one leaves it running while `_write_pid_file` overwrites the + only record of it. When the listener is unknowable, assume it is ours. + """ + record = _read_pid_record(_PID_FILE) + if record is None: + return None + pid, created, _address = record + if not _pid_alive(pid): + return None + # A current build writes a per-port file too, so its port is already known -- + # and this port's records were just checked. Only unported records get here. + if any(r and r[0] == pid for r in _per_port_records()): + return None + blocker = _get_pid_on_port(port) + if blocker is not None and blocker[0] != pid: + return None + if _pid_is_studio_backend(pid, [created]) is False: + return None + return pid + + +def _per_port_records() -> "list[tuple[int, float | None, str | None] | None]": + try: + return [_read_pid_record(p) for p in _studio_root().glob(PID_FILE_GLOB)] + except OSError: + return [] def _abort_already_running(pid: int, port: int) -> "NoReturn": diff --git a/studio/backend/tests/test_studio_pid_files.py b/studio/backend/tests/test_studio_pid_files.py index 87e248c5b3..1d4d1e86cf 100644 --- a/studio/backend/tests/test_studio_pid_files.py +++ b/studio/backend/tests/test_studio_pid_files.py @@ -228,6 +228,57 @@ def test_legacy_records_do_not_match_a_training_run(monkeypatch): assert run._pid_is_studio_backend(9999) is False +def test_a_legacy_server_on_the_port_is_recognised(tmp_path, monkeypatch): + # Pre-upgrade servers wrote only studio.pid. Falling back past one strands it + # and then overwrites its record. + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: (8550, "python")) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8550 + + +def test_a_legacy_record_for_a_different_listener_falls_back(tmp_path, monkeypatch): + # jupyter holds the port; the legacy server is elsewhere. Keep falling back. + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: (117, "jupyter-lab")) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") is None + + +def test_an_unknowable_listener_treats_the_legacy_record_as_ours(tmp_path, monkeypatch): + # No psutil: _get_pid_on_port can't say. Refusing beats a silent duplicate. + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: None) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8550 + + +def test_a_dead_legacy_record_falls_back(tmp_path, monkeypatch): + monkeypatch.setattr(run, "_pid_alive", lambda pid: False) + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: None) + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") is None + + +def test_a_current_server_elsewhere_does_not_block_a_foreign_port(tmp_path, monkeypatch): + # Current builds write studio.pid too. Without psutil the legacy check can't + # see the listener, so it must not claim our 8901 server holds jupyter's 8888. + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: None) + (tmp_path / "studio-8901-5000.pid").write_text("5000\n\n127.0.0.1", encoding = "utf-8") + (tmp_path / "studio.pid").write_text("5000", encoding = "utf-8") + + assert run._own_studio_on_port(8888, "127.0.0.1") is None + + +def test_a_per_port_record_is_preferred_over_the_legacy_one(tmp_path, monkeypatch): + monkeypatch.setattr(run, "_get_pid_on_port", lambda p: (8550, "python")) + (tmp_path / "studio-8901-8600.pid").write_text("8600\n\n127.0.0.1", encoding = "utf-8") + (tmp_path / "studio.pid").write_text("8550", encoding = "utf-8") + + assert run._own_studio_on_port(8901, "127.0.0.1") == 8600 + + def test_our_studio_on_another_bind_address_does_not_abort(tmp_path): # Our server holds ::1:8889; binding 127.0.0.1:8889 is not a conflict with us, # so fall through to the next port instead of refusing.