Files
docker-migrate/core/main.py

197 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-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:
sys.path.insert(0, _PROJECT_ROOT)
from core.color import banner, menu, prompt, confirm, 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():
# Инициализируем state сразу, чтобы _LAST_STATE был готов при любом выходe
state.load_state()
parser = argparse.ArgumentParser(description="Docker Service Migration Tool")
parser.add_argument("--mode", choices=["source", "target"], help="Режим работы")
parser.add_argument("--resume", action="store_true", help="Продолжить после ошибки")
parser.add_argument("--status", action="store_true", help="Показать статус")
parser.add_argument("--logs", action="store_true", help="Показать логи ошибки")
parser.add_argument("--remote-dir", help="Директория архива на target")
parser.add_argument("--no-color", action="store_true", help="Отключить цвета")
args = parser.parse_args()
if args.no_color:
os.environ["NO_COLOR"] = "1"
if args.status:
state.show_status()
sys.exit(0)
if args.logs:
state.show_logs()
sys.exit(0)
if args.resume:
_do_resume()
sys.exit(0)
if args.mode == "source":
from source.source import 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
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()
# Resume показываем только если есть незавершённая работа (stage != INIT, != DONE)
stage = st.get("stage", "INIT")
has_resume = bool(st.get("mode") and stage not in ("INIT", "DONE"))
menu(has_resume=has_resume)
try:
choice = prompt("Ваш выбор", default="0").strip()
except EOFError:
break
except KeyboardInterrupt:
_handle_keyboard_interrupt()
if choice == "1":
from source.source import run_source_mode
try:
run_source_mode()
except KeyboardInterrupt:
_handle_keyboard_interrupt()
break
elif choice == "2":
from target.target import run_target_mode
try:
run_target_mode()
except KeyboardInterrupt:
_handle_keyboard_interrupt()
break
elif choice == "3":
if not has_resume:
warn("Нет сохранённого состояния для продолжения")
continue
try:
_do_resume()
except KeyboardInterrupt:
_handle_keyboard_interrupt()
break
elif choice == "4":
_do_status_logs()
elif choice == "0":
info("Выход")
sys.exit(0)
else:
warn("Неверный выбор")
def _do_resume():
from core.fsm import FSM
st = state.load_state()
stage = st.get("stage")
mode = st.get("mode")
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}")
def _do_status_logs():
from core import state as stmod
while True:
print("\n")
print(" 1 Показать статус")
print(" 2 Показать лог последней ошибки")
print(" 0 Назад")
try:
c = prompt("Выбор", default="0").strip()
except EOFError:
break
except KeyboardInterrupt:
_handle_keyboard_interrupt()
if c == "1":
stmod.show_status()
elif c == "2":
stmod.show_logs()
elif c == "0":
break
if __name__ == "__main__":
main()