fix: auto-save state on Ctrl+C/SIGTERM, detect interrupted runs

This commit is contained in:
2026-05-22 20:33:25 +04:00
parent 15bb88527d
commit 3222620d0b
2 changed files with 113 additions and 19 deletions
+69 -14
View File
@@ -6,6 +6,8 @@ main.py — Точка входа. Меню выбора режима.
import sys
import os
import argparse
import signal
import atexit
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _PROJECT_ROOT not in sys.path:
@@ -15,6 +17,31 @@ from core.color import banner, menu, prompt, info, error as cerror, warn
from core import state
def _save_on_exit():
"""Сохраняет состояние при любом завершении (Ctrl+C, SIGTERM, os._exit)."""
state.flush_state()
def _handle_sigterm(signum, frame):
"""Обработчик SIGTERM (kill, systemd stop)."""
state.set_interrupted()
state.flush_state()
sys.exit(128 + signum)
def _handle_keyboard_interrupt():
"""Обработчик Ctrl+C — сохраняет состояние до выхода."""
state.set_interrupted()
state.flush_state()
print()
warn("Прервано (Ctrl+C). Состояние сохранено. Запустите docker-migrate --resume для продолжения.")
sys.exit(130)
# Регистрируем сохранение state при любом завершении
atexit.register(_save_on_exit)
signal.signal(signal.SIGTERM, _handle_sigterm)
def main():
parser = argparse.ArgumentParser(description="Docker Service Migration Tool")
parser.add_argument("--mode", choices=["source", "target"], help="Режим работы")
@@ -42,44 +69,66 @@ def main():
if args.mode == "source":
from source.source import run_source_mode
run_source_mode()
try:
run_source_mode()
except KeyboardInterrupt:
_handle_keyboard_interrupt()
sys.exit(0)
elif args.mode == "target":
if args.remote_dir:
state.set_stage("INIT", mode="target", target_remote_dir=args.remote_dir)
from target.target import run_target_mode
run_target_mode()
try:
run_target_mode()
except KeyboardInterrupt:
_handle_keyboard_interrupt()
sys.exit(0)
# Интерактивный режим
# Проверим, не было ли прерывания в прошлом запуске
recovered = state.recover_after_interrupt()
if recovered:
banner()
warn(f"⚠ Обнаружено прерывание прошлого запуска!")
info(f" Шаг: {recovered['stage']}")
info(f" Режим: {recovered['mode']}")
print(f" {recovered['hint']}")
print()
if confirm("Продолжить с места прерывания", default="y"):
try:
_do_resume()
except KeyboardInterrupt:
_handle_keyboard_interrupt()
sys.exit(0)
else:
info("Сбрасываем прерванное состояние. Можете начать заново.")
state.set_stage("INIT", mode=None, interrupted_at=None, resumable_hint=None)
banner()
while True:
st = state.load_state()
has_resume = bool(st.get("mode") and st.get("stage") and st.get("stage") != "INIT")
# Это resume — если есть mode, stage != INIT, или было прерывание
has_resume = bool(st.get("mode") and st.get("stage") != "INIT")
menu(has_resume=has_resume)
try:
choice = prompt("Ваш выбор", default="0").strip()
except SystemExit:
except EOFError:
break
except KeyboardInterrupt:
_handle_keyboard_interrupt()
if choice == "1":
from source.source import run_source_mode
try:
run_source_mode()
except KeyboardInterrupt:
warn("Прервано")
except Exception as e:
cerror(f"Ошибка: {e}")
sys.exit(1)
_handle_keyboard_interrupt()
break
elif choice == "2":
from target.target import run_target_mode
try:
run_target_mode()
except KeyboardInterrupt:
warn("Прервано")
except Exception as e:
cerror(f"Ошибка: {e}")
sys.exit(1)
_handle_keyboard_interrupt()
break
elif choice == "3":
if not has_resume:
@@ -88,7 +137,7 @@ def main():
try:
_do_resume()
except KeyboardInterrupt:
warn("Прервано")
_handle_keyboard_interrupt()
break
elif choice == "4":
_do_status_logs()
@@ -107,9 +156,13 @@ def _do_resume():
if not mode or stage == "INIT":
info("Нет сохранённого состояния для продолжения")
return
# Сбрасываем флаг прерывания, если был
state.set_stage(stage, interrupted_at=None, resumable_hint=None)
fsm = FSM(mode=mode)
try:
fsm.resume_from(stage)
except KeyboardInterrupt:
_handle_keyboard_interrupt()
except Exception as e:
cerror(f"Ошибка при resume: {e}")
@@ -123,8 +176,10 @@ def _do_status_logs():
print(" 0 Назад")
try:
c = prompt("Выбор", default="0").strip()
except SystemExit:
except EOFError:
break
except KeyboardInterrupt:
_handle_keyboard_interrupt()
if c == "1":
stmod.show_status()
elif c == "2":