fix(sdlc-rename): cutover: ожидание порта после bootout, честное состояние шага 2
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
40cb66158f
commit
1394f39763
@ -204,6 +204,19 @@ def _port_open(port: int) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
_sleep = time.sleep
|
||||
_clock = time.monotonic
|
||||
|
||||
|
||||
def _wait_port_closed(port: int, timeout: float = 10.0, interval: float = 0.5) -> bool:
|
||||
deadline = _clock() + timeout
|
||||
while _port_open(port):
|
||||
if _clock() >= deadline:
|
||||
return False
|
||||
_sleep(interval)
|
||||
return True
|
||||
|
||||
|
||||
def _domain() -> str:
|
||||
return "gui/%d" % os.getuid()
|
||||
|
||||
@ -233,7 +246,7 @@ def steps(cfg: Config, leftovers: List[str] = None, backups: List[Path] = None)
|
||||
_run("launchctl", "bootout", "%s/%s" % (_domain(), OLD_LABEL), check=False) # не загружен - не ошибка
|
||||
if old_plist.exists():
|
||||
old_plist.unlink()
|
||||
if _port_open(cfg.port):
|
||||
if not _wait_port_closed(cfg.port):
|
||||
raise RuntimeError("после остановки %s порт %d всё ещё занят: хаб не остановлен" % (OLD_LABEL, cfg.port))
|
||||
|
||||
def move_data() -> None:
|
||||
@ -332,13 +345,14 @@ def main(argv: List[str]) -> int:
|
||||
print("остановлено на шаге %s: %s" % (desc.split(".")[0], e))
|
||||
print("Выполнено: " + ("; ".join(done) if done else "ничего"))
|
||||
print("Промежуточное состояние (по выполненным шагам; упавший шаг мог выполниться частично, проверьте):")
|
||||
for line in _state_lines(done):
|
||||
for line in _state_lines(done, desc):
|
||||
print(" " + line)
|
||||
print("Откат: остановить ru.aepif.sdlc-hub (launchctl bootout %s/%s), перенести каталоги обратно "
|
||||
"(данные %s -> %s, checkout %s -> %s, origin вернуть на aep/jira-hub), вернуть plist %s и "
|
||||
"launchctl bootstrap %s; раздел «Переключение с jira-hub» в README." % (
|
||||
"launchctl bootstrap %s %s; раздел «Переключение с jira-hub» в README." % (
|
||||
_domain(), NEW_LABEL, cfg.data_root / "sdlc-hub", cfg.data_root / "jira-hub",
|
||||
cfg.new_checkout, cfg.old_checkout, cfg.launch_agents / (OLD_LABEL + ".plist"), _domain()))
|
||||
cfg.new_checkout, cfg.old_checkout, cfg.launch_agents / (OLD_LABEL + ".plist"), _domain(),
|
||||
cfg.launch_agents / (OLD_LABEL + ".plist")))
|
||||
_print_tail(backups, leftovers)
|
||||
return 1
|
||||
done.append(desc)
|
||||
@ -350,7 +364,7 @@ def main(argv: List[str]) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _state_lines(done: List[str]) -> List[str]:
|
||||
def _state_lines(done: List[str], failed: str = "") -> List[str]:
|
||||
n = {d.split(".")[0] for d in done}
|
||||
out = []
|
||||
if "2" in n:
|
||||
@ -363,6 +377,11 @@ def _state_lines(done: List[str]) -> List[str]:
|
||||
out.append("ru.aepif.sdlc-hub установлен и запущен")
|
||||
if "6" in n:
|
||||
out.append("файлы проектов изменены (копии *%s)" % SUFFIX)
|
||||
if failed:
|
||||
if failed.startswith("2."):
|
||||
out.append("упавший шаг 2: хаб ru.aepif.jira-hub мог быть выгружен, его plist удалён")
|
||||
else:
|
||||
out.append("упавший шаг %s мог выполниться частично, проверьте" % failed.split(".")[0])
|
||||
return out or ["ничего не изменено"]
|
||||
|
||||
|
||||
|
||||
@ -185,12 +185,31 @@ def test_step7_add_before_remove_and_remove_not_fatal(env, monkeypatch, capsys):
|
||||
assert "ПРЕДУПРЕЖДЕНИЕ" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_step2_fails_if_port_still_open(env, monkeypatch):
|
||||
def test_step2_fails_if_port_still_open(env, monkeypatch, capsys):
|
||||
monkeypatch.setattr(cutover.subprocess, "run", _Fake())
|
||||
monkeypatch.setattr(cutover, "_port_open", lambda port: True)
|
||||
cfg = cutover.Config.from_env()
|
||||
with pytest.raises(RuntimeError, match="порт"):
|
||||
cutover.steps(cfg)[0][1]()
|
||||
t = [0.0]
|
||||
monkeypatch.setattr(cutover, "_clock", lambda: t[0])
|
||||
monkeypatch.setattr(cutover, "_sleep", lambda d: t.__setitem__(0, t[0] + d))
|
||||
monkeypatch.setattr(cutover, "preflight", lambda cfg: [])
|
||||
assert cutover.main(["--apply"]) == 1
|
||||
out = capsys.readouterr().out
|
||||
assert "порт" in out and "остановлено на шаге 2" in out
|
||||
assert "мог быть выгружен" in out and "plist удалён" in out
|
||||
assert "ничего не изменено" not in out
|
||||
assert "launchctl bootstrap gui/%d %s" % (os.getuid(), env["tmp"] / "la" / "ru.aepif.jira-hub.plist") in out
|
||||
assert t[0] >= 10
|
||||
|
||||
|
||||
def test_step2_waits_for_port_to_close(env, monkeypatch):
|
||||
monkeypatch.setattr(cutover.subprocess, "run", _Fake())
|
||||
answers = iter([True, True, True, False])
|
||||
monkeypatch.setattr(cutover, "_port_open", lambda port: next(answers))
|
||||
t = [0.0]
|
||||
monkeypatch.setattr(cutover, "_clock", lambda: t[0])
|
||||
monkeypatch.setattr(cutover, "_sleep", lambda d: t.__setitem__(0, t[0] + d))
|
||||
cutover.steps(cutover.Config.from_env())[0][1]()
|
||||
assert 0 < t[0] < 10
|
||||
|
||||
|
||||
def test_failure_prints_state_and_rollback(env, monkeypatch, capsys):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user