fix: auto-save state on Ctrl+C/SIGTERM, detect interrupted runs
This commit is contained in:
+44
-5
@@ -7,17 +7,17 @@ state.py — Управление состоянием (FSM) и resume
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import atexit
|
||||
from datetime import datetime
|
||||
|
||||
STATE_FILE = None
|
||||
|
||||
_LAST_STATE = 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"),
|
||||
@@ -40,14 +40,18 @@ def _state_path():
|
||||
|
||||
|
||||
def load_state():
|
||||
global _LAST_STATE
|
||||
sp = _state_path()
|
||||
if os.path.isfile(sp):
|
||||
try:
|
||||
with open(sp, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
data = json.load(f)
|
||||
_LAST_STATE = data
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return _default_state()
|
||||
_LAST_STATE = _default_state()
|
||||
return _LAST_STATE
|
||||
|
||||
|
||||
def _default_state():
|
||||
@@ -61,14 +65,25 @@ def _default_state():
|
||||
"target_port": 22,
|
||||
"last_error": None,
|
||||
"completed_steps": [],
|
||||
"interrupted_at": None,
|
||||
"paused": False,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def flush_state():
|
||||
"""Сохраняет текущее _LAST_STATE на диск."""
|
||||
global _LAST_STATE
|
||||
sp = _state_path()
|
||||
with open(sp, "w", encoding="utf-8") as f:
|
||||
json.dump(_LAST_STATE, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def save_state(state):
|
||||
global _LAST_STATE
|
||||
sp = _state_path()
|
||||
state["updated_at"] = datetime.now().isoformat()
|
||||
_LAST_STATE = state
|
||||
with open(sp, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||
|
||||
@@ -112,6 +127,26 @@ def clear_error():
|
||||
save_state(state)
|
||||
|
||||
|
||||
def set_interrupted():
|
||||
"""Помечает, что скрипт был прерван (Ctrl+C, SIGTERM)."""
|
||||
state = load_state()
|
||||
state["interrupted_at"] = datetime.now().isoformat()
|
||||
state["resumable_hint"] = f"Скрипт был прерван на шаге '{state.get('stage', 'INIT')}'. Запустите: docker-migrate --resume"
|
||||
save_state(state)
|
||||
|
||||
|
||||
def recover_after_interrupt():
|
||||
"""Проверяет, был ли предыдущий запуск прерван. Если да — сбрасывает флаг."""
|
||||
state = load_state()
|
||||
if state.get("interrupted_at"):
|
||||
return {
|
||||
"stage": state.get("stage"),
|
||||
"mode": state.get("mode"),
|
||||
"hint": state.pop("resumable_hint", ""),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def show_status():
|
||||
from core.color import info, warn, error as cerror, bold, divider
|
||||
state = load_state()
|
||||
@@ -123,6 +158,9 @@ def show_status():
|
||||
info(f"Completed steps: {', '.join(state.get('completed_steps', [])) or '(none)'}")
|
||||
if state.get("paused"):
|
||||
warn("Скрипт на паузе из-за ошибки")
|
||||
if state.get("interrupted_at"):
|
||||
warn(f"Последний запуск был прерван в {state['interrupted_at']} на шаге '{state.get('stage', 'INIT')}'")
|
||||
print(f" Запустите: docker-migrate --resume")
|
||||
if state.get("manifest_path"):
|
||||
info(f"Manifest: {state['manifest_path']}")
|
||||
if state.get("archive_path"):
|
||||
@@ -154,3 +192,4 @@ def show_logs():
|
||||
if err.get("suggestion"):
|
||||
print(f"\nРекомендация:")
|
||||
print(err["suggestion"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user