secure unmount on close the app

This commit is contained in:
2025-09-08 00:59:24 +02:00
parent 4aa38ab33d
commit a8cbfcb380
3 changed files with 86 additions and 12 deletions

View File

@@ -13,6 +13,7 @@ from typing import Optional, List, Tuple
from core.pbp_app_config import AppConfig
from pyimage_ui.password_dialog import PasswordDialog
import json
class EncryptionManager:
@@ -26,6 +27,31 @@ class EncryptionManager:
self.service_id = "py-backup-encryption"
self.mounted_destinations = set()
self.password_cache = {}
self.lock_file = AppConfig.LOCK_FILE_PATH
def _write_lock_file(self, data):
with open(self.lock_file, 'w') as f:
json.dump(data, f)
def _read_lock_file(self):
if not self.lock_file.exists():
return []
with open(self.lock_file, 'r') as f:
try:
return json.load(f)
except json.JSONDecodeError:
return []
def add_to_lock_file(self, base_path, mapper_name):
locks = self._read_lock_file()
if not any(lock['base_path'] == base_path for lock in locks):
locks.append({"base_path": base_path, "mapper_name": mapper_name})
self._write_lock_file(locks)
def remove_from_lock_file(self, base_path):
locks = self._read_lock_file()
updated_locks = [lock for lock in locks if lock['base_path'] != base_path]
self._write_lock_file(updated_locks)
def get_password_from_keyring(self, username: str) -> Optional[str]:
try:
@@ -252,7 +278,10 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
{chown_cmd}
"""
return self._execute_as_root(script, password)
if self._execute_as_root(script, password):
self.add_to_lock_file(base_dest_path, mapper_name)
return True
return False
def unmount_and_reset_owner(self, base_dest_path: str, force_unmap=False):
username = os.path.basename(base_dest_path.rstrip('/'))
@@ -268,11 +297,12 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\"
script = f"""
chown root:root \"{mount_point}\" || true
umount -l \"{mount_point}\" || true
cryptsetup luksClose {mapper_name} || true
umount -l \"{mount_point}\"
cryptsetup luksClose {mapper_name}
"""
password = self.password_cache.get(username)
self._execute_as_root(script, password)
self.remove_from_lock_file(base_dest_path)
if base_dest_path in self.mounted_destinations:
self.mounted_destinations.remove(base_dest_path)

View File

@@ -10,13 +10,13 @@ class AppConfig:
# --- Core Paths ---
BASE_DIR: Path = Path.home()
CONFIG_DIR: Path = BASE_DIR / ".config/lx_pyimage"
SETTINGS_FILE: Path = CONFIG_DIR / "settings.json"
GENERATED_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / "rsync-generated-excludes.conf"
USER_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \
"rsync-user-excludes.conf" # Single file
MANUAL_EXCLUDE_LIST_PATH: Path = CONFIG_DIR / \
"rsync-manual-excludes.conf" # Single file
APP_DIR: Path = BASE_DIR / ".config/py_backup"
SETTINGS_FILE: Path = APP_DIR / "pbp_settings.json"
GENERATED_EXCLUDE_LIST_PATH: Path = APP_DIR / "rsync-generated-excludes.conf"
USER_EXCLUDE_LIST_PATH: Path = APP_DIR / "user_excludes.txt"
MANUAL_EXCLUDE_LIST_PATH: Path = APP_DIR / "manual_excludes.txt"
LOG_FILE_PATH: Path = APP_DIR / "py-backup.log"
LOCK_FILE_PATH: Path = APP_DIR / "pybackup.lock"
APP_ICONS_DIR: Path = Path(__file__).parent / "lx-icons"
# --- Application Info ---
@@ -125,8 +125,8 @@ class AppConfig:
@classmethod
def ensure_directories(cls) -> None:
"""Ensures that all required application directories exist."""
if not cls.CONFIG_DIR.exists():
cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
if not cls.APP_DIR.exists():
cls.APP_DIR.mkdir(parents=True, exist_ok=True)
# In the future, we can create a default settings file here
# Generate/update the final exclude list on every start

View File

@@ -5,6 +5,7 @@ import os
import datetime
from queue import Queue, Empty
import shutil
import signal
from shared_libs.log_window import LogWindow
from shared_libs.logger import app_logger
@@ -75,6 +76,10 @@ class MainApplication(tk.Tk):
self.backup_manager = BackupManager(app_logger, self)
self.queue = Queue()
self._check_for_stale_mounts()
signal.signal(signal.SIGTERM, self._handle_signal)
signal.signal(signal.SIGINT, self._handle_signal)
self.image_manager = IconManager()
self.config_manager = ConfigManager(AppConfig.SETTINGS_FILE)
@@ -336,6 +341,14 @@ class MainApplication(tk.Tk):
self.destination_total_bytes = total
self.destination_used_bytes = used
# If the destination is already mounted from a previous session,
# adopt it into the current session's state so it can be cleaned up properly.
if self.backup_manager.encryption_manager.is_mounted(backup_dest_path):
app_logger.log(
f"Adopting pre-existing mount for {backup_dest_path} into session.")
self.backup_manager.encryption_manager.mounted_destinations.add(
backup_dest_path)
if hasattr(self, 'header_frame'):
self.header_frame.refresh_status()
@@ -733,6 +746,37 @@ class MainApplication(tk.Tk):
def quit(self):
self.on_closing()
def _check_for_stale_mounts(self):
app_logger.log("Checking for stale mounts from previous sessions...")
try:
locks = self.backup_manager.encryption_manager._read_lock_file()
if not locks:
app_logger.log(
"No lock file found or lock file is empty. Clean state.")
return
stale_mounts_found = False
for lock in locks:
mapper_path = f"/dev/mapper/{lock['mapper_name']}"
if os.path.exists(mapper_path):
stale_mounts_found = True
app_logger.log(
f"Found stale mount: {lock['mapper_name']} for path {lock['base_path']}. Attempting to close.")
self.backup_manager.encryption_manager.unmount_and_reset_owner(
lock['base_path'], force_unmap=True)
if not stale_mounts_found:
app_logger.log("No stale mounts detected.")
if locks:
self.backup_manager.encryption_manager._write_lock_file([])
except Exception as e:
app_logger.log(f"Error during stale mount check: {e}")
def _handle_signal(self, signum, frame):
app_logger.log(f"Received signal {signum}. Cleaning up and exiting.")
self.on_closing()
def update_backup_options_from_config(self):
force_full = self.config_manager.get_setting(
"force_full_backup", False)