# -*- coding: utf-8 -*- """ state.py — Управление состоянием (FSM) и resume Состояние хранится в JSON файле рядом со скриптом (vars/lib) """ import os import json import sys import time from datetime import datetime STATE_FILE = None def _state_path(): global STATE_FILE if STATE_FILE: return STATE_FILE # Ищем рядом со скриптом или в /var/lib/migrate candidates = [ os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".migrate-state.json"), os.path.join(os.path.expanduser("~"), ".migrate", "state.json"), "/var/lib/migrate/state.json", ] for c in candidates: d = os.path.dirname(c) if os.path.isdir(d): STATE_FILE = c return c try: os.makedirs(d, exist_ok=True) STATE_FILE = c return c except PermissionError: continue fallback = os.path.join(os.getcwd(), ".migrate-state.json") STATE_FILE = fallback return fallback def load_state(): sp = _state_path() if os.path.isfile(sp): try: with open(sp, "r", encoding="utf-8") as f: return json.load(f) except json.JSONDecodeError: pass return _default_state() def _default_state(): return { "stage": "INIT", "mode": None, "manifest_path": None, "archive_path": None, "target_host": None, "target_user": None, "target_port": 22, "last_error": None, "completed_steps": [], "paused": False, "created_at": datetime.now().isoformat(), } def save_state(state): sp = _state_path() state["updated_at"] = datetime.now().isoformat() with open(sp, "w", encoding="utf-8") as f: json.dump(state, f, indent=2, ensure_ascii=False) def set_stage(stage, **kwargs): state = load_state() state["stage"] = stage for k, v in kwargs.items(): state[k] = v save_state(state) def mark_completed(step): state = load_state() if step not in state["completed_steps"]: state["completed_steps"].append(step) save_state(state) def is_completed(step): return step in load_state().get("completed_steps", []) def set_error(step, stdout, stderr, suggestion=""): state = load_state() state["paused"] = True state["last_error"] = { "step": step, "timestamp": datetime.now().isoformat(), "stdout": stdout, "stderr": stderr, "suggestion": suggestion, } save_state(state) def clear_error(): state = load_state() state["paused"] = False state["last_error"] = None save_state(state) def show_status(): from core.color import info, warn, error as cerror, bold, divider state = load_state() div = divider print("\n") div() info(f"Текущий stage: {bold(state.get('stage', 'INIT'))}") info(f"Режим: {bold(state.get('mode') or 'не выбран')}") info(f"Completed steps: {', '.join(state.get('completed_steps', [])) or '(none)'}") if state.get("paused"): warn("Скрипт на паузе из-за ошибки") if state.get("manifest_path"): info(f"Manifest: {state['manifest_path']}") if state.get("archive_path"): info(f"Archive: {state['archive_path']}") if state.get("last_error"): err = state["last_error"] cerror(f"Последняя ошибка на шаге: {bold(err['step'])}") print(f" Время: {err['timestamp']}") if err.get("suggestion"): print(f" Рекомендация: {err['suggestion']}") div() def show_logs(): state = load_state() err = state.get("last_error") if not err: print("Нет сохранённых ошибок") return from core.color import error as cerror, bold cerror(f"=== Лог ошибки (шаг: {bold(err['step'])}) ===") print(f"Время: {err['timestamp']}\n") if err.get("stdout"): print("STDOUT:") print(err["stdout"]) if err.get("stderr"): print("\nSTDERR:") print(err["stderr"]) if err.get("suggestion"): print(f"\nРекомендация:") print(err["suggestion"])