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:
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
113
main_app.py
113
main_app.py
@@ -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()
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user