fix(app): Behebt das Hängenbleiben der UI und die falsche Grössenberechnung

Mehrere grundlegende Probleme in der Anwendungslogik wurden behoben:

- **UI-Verarbeitungsschleife:** Die Verarbeitung von Nachrichten aus Hintergrund-Threads wurde komplett überarbeitet. Zuvor führten zwei konkurrierende Schleifen zu einer Race Condition, bei der Nachrichten verloren gingen. Jetzt gibt es eine einzige, zentrale Verarbeitungsschleife, die Nachrichten in Stapeln verarbeitet. Dies behebt das Problem, dass die Benutzeroberfläche nach dem Löschen oder dem Abschluss eines Backups im "in Arbeit"-Zustand hängen blieb.

- **Backup-Grössenberechnung:** Die Ermittlung der Grösse von inkrementellen Backups wurde robuster gestaltet.
    - Die rsync-Ausgabe wird nun zuverlässig auf Englisch erzwungen, um Parsing-Fehler in anderen System-Locales zu vermeiden.
    - Die Grösse wird nun aus der `sent... received...` Zusammenfassungszeile von rsync ausgelesen, was auch bei Backups ohne Datenänderungen einen Wert ungleich Null liefert.
    - Es wird nun korrekt zwischen Voll-Backups (Anzeige der Gesamtgrösse) und inkrementellen Backups (Anzeige der Übertragungsgrösse) unterschieden.

- **Sonstige Korrekturen:**
    - Eine fehlende Übersetzung für die manuelle Ausschlussliste wurde hinzugefügt.
    - Ein überflüssiger Aufruf zum Starten der Verarbeitungsschleife wurde entfernt.
This commit is contained in:
2025-09-02 13:59:06 +02:00
parent 05500f0303
commit 988b0e8d1d
5 changed files with 151 additions and 117 deletions

View File

@@ -299,7 +299,11 @@ set -e
if status in ['success', 'warning'] and not is_dry_run:
info_filename_base = os.path.basename(dest_path)
final_size = transferred_size
if latest_backup_path is None: # This was a full backup
final_size = source_size
else: # This was an incremental backup
final_size = transferred_size
if is_compressed:
self.logger.log(f"Compression requested for {dest_path}")
@@ -369,8 +373,11 @@ set -e
transferred_size = 0
try:
try:
# Force C locale to ensure rsync output is in English for parsing
env = os.environ.copy()
env['LC_ALL'] = 'C'
self.process = subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, preexec_fn=os.setsid)
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, preexec_fn=os.setsid, env=env)
except FileNotFoundError:
self.logger.log(
"Error: 'pkexec' or 'rsync' command not found in PATH during Popen call.")
@@ -412,16 +419,53 @@ set -e
self.logger.log(f"Rsync Error: {stderr_output.strip()}")
output_lines.extend(stderr_output.strip().split('\n'))
# After process completion, parse the output for transferred size
for line in output_lines:
if line.startswith('Total transferred file size:'):
# After process completion, parse the output for transferred size.
# This is tricky because the output format can vary. We'll try to find the
# summary line from --info=progress2, which looks like "sent X bytes received Y bytes".
transferred_size = 0
summary_regex = re.compile(r"sent ([\d,.]+) bytes\s+received ([\d,.]+) bytes")
for line in reversed(output_lines): # Search from the end, as summary is usually last
match = summary_regex.search(line)
if match:
try:
size_str = line.split(':')[1].strip().split(' ')[0]
transferred_size = int(size_str.replace(',', '').replace('.', ''))
self.logger.log(f"Detected transferred size: {transferred_size} bytes")
break
except (ValueError, IndexError):
self.logger.log(f"Could not parse transferred size from line: {line}")
sent_str = match.group(1).replace(',', '').replace('.', '')
received_str = match.group(2).replace(',', '').replace('.', '')
bytes_sent = int(sent_str)
bytes_received = int(received_str)
transferred_size = bytes_sent + bytes_received
self.logger.log(
f"Detected total bytes transferred from summary: {transferred_size} bytes")
break # Found it
except (ValueError, IndexError) as e:
self.logger.log(
f"Could not parse sent/received bytes from line: '{line}'. Error: {e}")
if transferred_size == 0:
# Fallback for --stats format if the regex fails
bytes_sent = 0
bytes_received = 0
for line in output_lines:
if line.strip().startswith('Total bytes sent:'):
try:
size_str = line.split(':')[1].strip()
bytes_sent = int(size_str.replace(',', '').replace('.', ''))
except (ValueError, IndexError):
self.logger.log(f"Could not parse bytes sent from line: {line}")
elif line.strip().startswith('Total bytes received:'):
try:
size_str = line.split(':')[1].strip()
bytes_received = int(size_str.replace(',', '').replace('.', ''))
except (ValueError, IndexError):
self.logger.log(f"Could not parse bytes received from line: {line}")
if bytes_sent > 0 or bytes_received > 0:
transferred_size = bytes_sent + bytes_received
self.logger.log(
f"Detected total bytes transferred from --stats: {transferred_size} bytes")
else:
self.logger.log(
"Could not determine transferred size from rsync output. Size will be 0.")
except FileNotFoundError:
self.logger.log(

View File

@@ -202,87 +202,5 @@ class DataProcessing:
app_logger.log(f"An unexpected error occurred during incremental size calculation: {e}")
return 0
def process_queue(self):
try:
message = self.app.queue.get_nowait()
# Check for the new message format with status
calc_type, status = None, None
if len(message) == 5:
button_text, folder_size, mode_when_started, calc_type, status = message
elif len(message) == 3:
button_text, folder_size, mode_when_started = message
else:
return # Ignore malformed messages
if mode_when_started != self.app.mode:
if calc_type == 'accurate_incremental':
self.app.actions._set_ui_state(True) # Unlock UI
self.app.genaue_berechnung_var.set(False) # Uncheck the box
self.app.accurate_calculation_running = False
self.app.animated_icon.stop("DISABLE")
return # Discard stale result
# --- Update Main Canvas ---
current_folder_name = self.app.left_canvas_data.get('folder')
if current_folder_name == button_text:
if self.app.left_canvas_animation:
self.app.left_canvas_animation.stop()
self.app.left_canvas_animation.destroy()
self.app.left_canvas_animation = None
size_in_gb = folder_size / (1024**3)
size_str = f"{size_in_gb:.2f} GB" if size_in_gb >= 1 else f"{folder_size / (1024*1024):.2f} MB"
self.app.left_canvas_data['size'] = size_str
self.app.left_canvas_data['total_bytes'] = folder_size
self.app.left_canvas_data['calculating'] = False
self.app.drawing.redraw_left_canvas()
self.app.source_size_bytes = folder_size
# --- Update Bottom Canvases ---
if self.app.mode == 'backup':
# Ensure button_text is a valid key in FOLDER_PATHS
if button_text in AppConfig.FOLDER_PATHS:
total_disk_size, _, _ = shutil.disk_usage(AppConfig.FOLDER_PATHS[button_text])
if folder_size > total_disk_size:
self.app.source_larger_than_partition = True
else:
self.app.source_larger_than_partition = False
percentage = (folder_size / total_disk_size) * 100 if total_disk_size > 0 else 0
self.app.source_size_canvas.delete("all")
fill_width = (self.app.source_size_canvas.winfo_width() / 100) * percentage
self.app.source_size_canvas.create_rectangle(0, 0, fill_width, self.app.source_size_canvas.winfo_height(), fill="#0078d7", outline="")
self.app.source_size_label.config(text=f"{folder_size / (1024**3):.2f} GB / {total_disk_size / (1024**3):.2f} GB")
self.app.drawing.update_target_projection()
# --- Enable Start Button Logic ---
if self.app.mode == 'backup' and self.app.destination_path:
self.app.start_pause_button.config(state="normal")
# --- Handle Accurate Calculation Completion ---
if calc_type == 'accurate_incremental':
self.app.source_size_bytes = folder_size # Update the source size
self.app.drawing.update_target_projection() # Redraw the projection
self.app.animated_icon.stop("DISABLE")
self.app.task_progress.stop()
self.app.task_progress.config(mode="determinate", value=0)
self.app.actions._set_ui_state(True)
self.app.genaue_berechnung_var.set(False)
self.app.accurate_calculation_running = False
self.app.start_pause_button.config(text=Msg.STR["start"])
if status == 'success':
self.app.info_label.config(text=Msg.STR["accurate_size_success"], foreground="#0078d7")
self.app.current_file_label.config(text="")
else:
self.app.info_label.config(text=Msg.STR["accurate_size_failed"], foreground="#D32F2F") # Red for failed
self.app.current_file_label.config(text="")
except Empty:
pass
finally:
self.app.after(100, self.process_queue)
# The queue processing logic has been moved to main_app.py
# to fix a race condition and ensure all queue messages are handled correctly.

View File

@@ -372,7 +372,7 @@ class MainApplication(tk.Tk):
'folder', 'Computer')
self.after(100, self.actions.on_sidebar_button_click,
restore_dest_folder)
self.data_processing.process_queue()
self._process_queue()
def _setup_log_window(self):
self.log_frame = ttk.Frame(self.content_frame)
@@ -547,13 +547,91 @@ class MainApplication(tk.Tk):
app_logger.log(Msg.STR["app_quit"])
self.destroy()
def _process_backup_queue(self):
"""Processes messages from the backup thread queue to update the UI safely."""
def _process_queue(self):
"""
Processes all messages from background threads to update the UI safely.
This is the single, consolidated queue processing loop for the entire application.
It processes messages in batches to avoid freezing the UI.
"""
try:
while True:
for _ in range(100): # Process up to 100 messages at a time
message = self.queue.get_nowait()
if isinstance(message, tuple) and len(message) == 2:
# --- Size Calculation Message Handling (from data_processing) ---
if isinstance(message, tuple) and len(message) in [3, 5]:
calc_type, status = None, None
if len(message) == 5:
button_text, folder_size, mode_when_started, calc_type, status = message
else: # len == 3
button_text, folder_size, mode_when_started = message
if mode_when_started != self.mode:
if calc_type == 'accurate_incremental':
self.actions._set_ui_state(True)
self.genaue_berechnung_var.set(False)
self.accurate_calculation_running = False
self.animated_icon.stop("DISABLE")
else:
current_folder_name = self.left_canvas_data.get('folder')
if current_folder_name == button_text:
if self.left_canvas_animation:
self.left_canvas_animation.stop()
self.left_canvas_animation.destroy()
self.left_canvas_animation = None
size_in_gb = folder_size / (1024**3)
size_str = f"{size_in_gb:.2f} GB" if size_in_gb >= 1 else f"{folder_size / (1024*1024):.2f} MB"
self.left_canvas_data['size'] = size_str
self.left_canvas_data['total_bytes'] = folder_size
self.left_canvas_data['calculating'] = False
self.drawing.redraw_left_canvas()
self.source_size_bytes = folder_size
if self.mode == 'backup':
if button_text in AppConfig.FOLDER_PATHS:
total_disk_size, _, _ = shutil.disk_usage(
AppConfig.FOLDER_PATHS[button_text])
if folder_size > total_disk_size:
self.source_larger_than_partition = True
else:
self.source_larger_than_partition = False
percentage = (
folder_size / total_disk_size) * 100 if total_disk_size > 0 else 0
self.source_size_canvas.delete("all")
fill_width = (
self.source_size_canvas.winfo_width() / 100) * percentage
self.source_size_canvas.create_rectangle(
0, 0, fill_width, self.source_size_canvas.winfo_height(), fill="#0078d7", outline="")
self.source_size_label.config(
text=f"{folder_size / (1024**3):.2f} GB / {total_disk_size / (1024**3):.2f} GB")
self.drawing.update_target_projection()
if self.mode == 'backup' and self.destination_path:
self.start_pause_button.config(state="normal")
if calc_type == 'accurate_incremental':
self.source_size_bytes = folder_size
self.drawing.update_target_projection()
self.animated_icon.stop("DISABLE")
self.task_progress.stop()
self.task_progress.config(mode="determinate", value=0)
self.actions._set_ui_state(True)
self.genaue_berechnung_var.set(False)
self.accurate_calculation_running = False
self.start_pause_button.config(text=Msg.STR["start"])
if status == 'success':
self.info_label.config(
text=Msg.STR["accurate_size_success"], foreground="#0078d7")
self.current_file_label.config(text="")
else:
self.info_label.config(
text=Msg.STR["accurate_size_failed"], foreground="#D32F2F")
self.current_file_label.config(text="")
# --- Backup/Deletion Message Handling (from main_app) ---
elif isinstance(message, tuple) and len(message) == 2:
message_type, value = message
if message_type == 'progress':
@@ -575,6 +653,7 @@ class MainApplication(tk.Tk):
elif message_type == 'cancel_button_state':
self.start_pause_button.config(state=value)
elif message_type == 'deletion_complete':
self.actions._set_ui_state(True)
self.backup_content_frame.hide_deletion_status()
self.backup_content_frame.system_backups_frame._load_backup_content()
self.backup_content_frame.user_backups_frame._load_backup_content()
@@ -584,10 +663,10 @@ class MainApplication(tk.Tk):
self.backup_is_running = False
elif message_type == 'completion':
status_info = value
status = 'error' # Default
status = 'error'
if isinstance(status_info, dict):
status = status_info.get('status', 'error')
elif status_info is None: # Fallback for older logic
elif status_info is None:
status = 'success'
if status == 'success':
@@ -600,7 +679,6 @@ class MainApplication(tk.Tk):
self.info_label.config(
text=Msg.STR["backup_failed"])
elif status == 'cancelled':
# This is handled in actions.py, but we clean up here.
pass
self.animated_icon.stop("DISABLE")
@@ -625,21 +703,16 @@ class MainApplication(tk.Tk):
self.actions._set_ui_state(True)
self.backup_content_frame.system_backups_frame._load_backup_content()
elif message_type == 'completion_accurate':
if value == 'success':
self.info_label.config(
text=Msg.STR["accurate_size_success"])
else:
self.info_label.config(
text=Msg.STR["accurate_size_failed"])
self.actions._set_ui_state(True)
# This is now handled by the len=5 case above
pass
else:
self.queue.put(message)
break
app_logger.log(f"Unknown message in queue: {message}")
except Empty:
pass
self.after(100, self._process_backup_queue)
pass # The queue is empty, do nothing.
finally:
# Always schedule the next check.
self.after(100, self._process_queue)
def quit(self):
self.on_closing()

View File

@@ -275,6 +275,7 @@ class Msg:
"add_file_button": _("File"),
"system_excludes": _("System Excludes"),
"manual_excludes": _("Manual Excludes"),
"manual_excludes_info": _("Here, manually add files or folders to be excluded from the backup. Each entry should be on a new line."),
# Menus
"file_menu": _("File"),

View File

@@ -624,8 +624,6 @@ class Actions:
self.app.animated_icon.start()
self.app._process_backup_queue()
if self.app.mode == "backup":
source_size_bytes = self.app.source_size_bytes
if self.app.vollbackup_var.get():