-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathmain.py
More file actions
739 lines (652 loc) · 38.7 KB
/
Copy pathmain.py
File metadata and controls
739 lines (652 loc) · 38.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
# main.py
# -*- coding: utf-8 -*-
import sys
import os
import logging
import argparse
import config
import platform
import glob
# --- Single Instance Constants (from lightweight config module) ---
from config import SHARED_MEM_KEY, LOCAL_SERVER_NAME
# --- Minimal PySide6 Imports for Single Instance Check ---
# These are deferred to GUI mode only (not needed for --backup silent mode)
# Importing at module level was causing silent failures with pythonw.exe shortcuts
# from PySide6.QtCore import QSharedMemory
# from PySide6.QtNetwork import QLocalSocket, QLocalServer
try:
import pyi_splash # type: ignore # This module only exists when the app is packaged with PyInstaller
except ImportError:
pyi_splash = None # Set to None if not found (e.g. when not running from a bundle)
# NOTE: Heavy imports (QApplication, MainWindow, settings_manager, etc.)
# are deferred until after single-instance check to speed up second instance detection
# --- Helper Function for Cleanup ---
def cleanup_instance_lock(local_server, shared_memory):
"""Closes the local server, releases the shared memory, and cleans up lock file on Linux."""
logging.debug("Executing instance cleanup (detach shared memory, close server, remove lock file)...")
try:
if local_server and local_server.isListening():
local_server.close()
logging.debug("Local server closed.")
else:
logging.debug("Local server was None or not listening.")
except Exception as e_server:
logging.error(f"Error closing local server: {e_server}")
try:
# Controlla se l'oggetto esiste ed è attached prima di detach
if shared_memory and shared_memory.isAttached():
if shared_memory.detach():
logging.debug("Shared memory detached.")
else:
logging.error(f"Failed to detach shared memory: {shared_memory.errorString()}")
elif shared_memory:
logging.debug("Shared memory object exists but was not attached.")
else:
logging.debug("Shared memory object was None.")
except Exception as e_mem:
logging.error(f"Error detaching shared memory: {e_mem}")
# Clean up file lock on Linux
try:
if platform.system() == "Linux":
lock_path = get_lock_file_path()
if os.path.exists(lock_path):
try:
with open(lock_path, 'r') as f:
stored_pid = int(f.read().strip())
if stored_pid == os.getpid():
os.unlink(lock_path)
logging.debug(f"Removed lock file: {lock_path}")
except Exception:
pass
except Exception as e_lock:
logging.debug(f"Error cleaning up lock file: {e_lock}")
def cleanup_stale_qt_ipc_artifacts(local_server_name: str, shared_memory_key: str) -> None:
"""Best-effort cleanup for stale Qt IPC artifacts on Linux.
This removes orphaned QLocalServer sockets and forces deletion of stale
QSharedMemory segments by attach+detach. It also tries to unlink known
temporary files created by Qt for shared memory and semaphores that can
persist after crashes.
"""
if platform.system() != "Linux":
return
# Import PySide6 here since this function is only called from GUI mode
try:
from PySide6.QtCore import QSharedMemory
from PySide6.QtNetwork import QLocalServer
except ImportError:
logging.debug("PySide6 not available for IPC cleanup, skipping.")
return
try:
# Remove potentially orphaned local server socket by name (Qt handles path resolution)
if QLocalServer.removeServer(local_server_name):
logging.warning(f"Removed orphaned local server '{local_server_name}' (pre-cleanup)")
except Exception as e:
logging.debug(f"QLocalServer.removeServer pre-cleanup failed: {e}")
# Try to remove runtime path variant as well (where Qt may store the socket)
try:
runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
if not runtime_dir:
try:
uid = os.getuid() # type: ignore[attr-defined]
runtime_dir = f"/run/user/{uid}"
except Exception:
runtime_dir = None
if runtime_dir:
server_path = os.path.join(runtime_dir, local_server_name)
if os.path.exists(server_path):
os.unlink(server_path)
logging.warning(f"Unlinked stale local socket: {server_path}")
except Exception as e:
logging.debug(f"Runtime socket unlink failed: {e}")
# Force attach/detach on shared memory to drop a stale segment when no other process holds it
try:
temp_mem = QSharedMemory(shared_memory_key)
if temp_mem.attach():
logging.warning("Attached to stale shared memory; detaching to release it...")
if not temp_mem.detach():
logging.error(f"Failed to detach stale shared memory: {temp_mem.errorString()}")
else:
logging.info("Stale shared memory released via attach+detach.")
except Exception as e:
logging.debug(f"Shared memory attach/detach cleanup skipped/failed: {e}")
# As a last resort, attempt to remove Qt's temporary IPC files for this app key
# Restrict patterns to this app by including 'SaveState' or the exact key
try:
tokens = [shared_memory_key, local_server_name, "SaveState"]
tmp_patterns = []
for token in tokens:
if token and isinstance(token, str):
tmp_patterns.append(f"/tmp/qipc_sharedmemory_*{token}*")
tmp_patterns.append(f"/tmp/qipc_systemsem_*{token}*")
removed_any = False
for pattern in tmp_patterns:
for path in glob.glob(pattern):
try:
if os.path.exists(path):
os.unlink(path)
removed_any = True
logging.warning(f"Removed stale Qt IPC temp file: {path}")
except Exception as e_rm:
logging.debug(f"Could not remove '{path}': {e_rm}")
if not removed_any:
logging.debug("No matching stale Qt IPC temp files found to remove.")
except Exception as e:
logging.debug(f"Temp files cleanup skipped/failed: {e}")
# Also check and clean stale file-based lock if the process is not running
try:
runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
if not runtime_dir:
try:
uid = os.getuid() # type: ignore[attr-defined]
runtime_dir = f"/run/user/{uid}"
except Exception:
runtime_dir = "/tmp"
lock_path = os.path.join(runtime_dir, "savestate.lock")
if os.path.exists(lock_path):
try:
with open(lock_path, 'r') as f:
content = f.read().strip()
if content:
old_pid = int(content)
# Check if process is running
try:
os.kill(old_pid, 0)
logging.debug(f"Lock file PID {old_pid} is still running.")
except OSError:
# Process not running - stale lock
os.unlink(lock_path)
logging.warning(f"Removed stale lock file with dead PID {old_pid}: {lock_path}")
except (ValueError, IOError) as e:
logging.debug(f"Could not read lock file during cleanup: {e}")
except Exception as e:
logging.debug(f"File lock cleanup skipped/failed: {e}")
# --- File-based Lock for Linux (fallback for more reliable single-instance detection) ---
def get_lock_file_path() -> str:
"""Get the path for the lock file on Linux."""
runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
if not runtime_dir:
try:
uid = os.getuid() # type: ignore[attr-defined]
runtime_dir = f"/run/user/{uid}"
except Exception:
runtime_dir = "/tmp"
return os.path.join(runtime_dir, "savestate.lock")
def is_process_running(pid: int) -> bool:
"""Check if a process with the given PID is still running."""
if platform.system() != "Linux":
return False
try:
# On Linux, sending signal 0 checks if process exists without killing it
os.kill(pid, 0)
return True
except OSError:
return False
except Exception:
return False
def check_and_create_lock_file() -> tuple:
"""Check if another instance is running via lock file. Returns (is_first_instance, lock_file_path).
On Linux, this provides an additional layer of single-instance detection
that survives crashes better than QSharedMemory alone.
"""
if platform.system() != "Linux":
return (True, None) # On non-Linux, skip file lock
lock_path = get_lock_file_path()
current_pid = os.getpid()
try:
# Check if lock file exists and contains a valid PID
if os.path.exists(lock_path):
try:
with open(lock_path, 'r') as f:
content = f.read().strip()
if content:
old_pid = int(content)
if is_process_running(old_pid):
logging.warning(f"Lock file exists with running PID {old_pid}")
return (False, lock_path)
else:
logging.info(f"Lock file exists but PID {old_pid} is not running. Cleaning up stale lock.")
os.unlink(lock_path)
except (ValueError, IOError) as e:
logging.debug(f"Could not read lock file, removing: {e}")
try:
os.unlink(lock_path)
except Exception:
pass
# Create new lock file with our PID
with open(lock_path, 'w') as f:
f.write(str(current_pid))
logging.debug(f"Created lock file at {lock_path} with PID {current_pid}")
return (True, lock_path)
except Exception as e:
logging.error(f"Error handling lock file: {e}")
return (True, None) # On error, allow the instance to run
def cleanup_lock_file():
"""Remove the lock file on exit."""
if platform.system() != "Linux":
return
try:
lock_path = get_lock_file_path()
if os.path.exists(lock_path):
# Only remove if it's our PID
try:
with open(lock_path, 'r') as f:
stored_pid = int(f.read().strip())
if stored_pid == os.getpid():
os.unlink(lock_path)
logging.debug(f"Removed lock file: {lock_path}")
except Exception:
pass
except Exception as e:
logging.debug(f"Error cleaning up lock file: {e}")
# --- Main Execution Block ---
if __name__ == "__main__":
# --- Basic Logging Configuration (console only, before heavy imports) ---
log_level = logging.INFO
log_format = '%(asctime)s - %(levelname)s - %(message)s'
log_datefmt = '%H:%M:%S'
log_formatter = logging.Formatter(log_format, log_datefmt)
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
# Remove existing handlers if necessary (e.g. in PyInstaller)
for handler in root_logger.handlers[:]:
try:
root_logger.removeHandler(handler)
handler.close()
except Exception as e_handler:
pass # Silent, we're in early startup
# Create console handler only (Qt handler added later after heavy imports)
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)
console_handler.setLevel(logging.INFO)
root_logger.addHandler(console_handler)
logging.debug("Basic logging configured (console only).")
# --- Parsing Arguments ---
parser = argparse.ArgumentParser(description='SaveState GUI or Backup Runner.')
parser.add_argument("--backup", help="Nome del profilo per cui eseguire un backup silenzioso.")
args = parser.parse_args()
# --- Execution Mode Check ---
if args.backup:
# === Silent Backup Mode ===
# Import heavy modules only for backup mode
from backup import backup_runner
profile_to_backup = args.backup
logging.info(f"Detected argument --backup '{profile_to_backup}'. Closing splash and starting silent backup...")
if pyi_splash:
try:
pyi_splash.close()
except Exception:
pass
try:
backup_success = backup_runner.run_silent_backup(profile_to_backup)
logging.info(f"Silent backup completed successfully: {backup_success}")
sys.exit(0 if backup_success else 1)
except Exception as e_backup:
logging.critical(f"Error during silent backup for '{profile_to_backup}': {e_backup}", exc_info=True)
sys.exit(1)
else:
# === Normal GUI Mode ===
# IMPORTANT: Do single-instance check BEFORE importing heavy modules!
# This makes launching a second instance much faster.
logging.debug("GUI mode. Performing fast single-instance check...")
# --- Import PySide6 for single-instance check (only in GUI mode) ---
from PySide6.QtCore import QSharedMemory
from PySide6.QtNetwork import QLocalSocket, QLocalServer
# --- Single Instance Logic ---
shared_memory = None # Initialize to None
local_socket = None
local_server = None
lock_file_path = None # For Linux file lock
app_should_run = True # Flag to decide whether to start the GUI
# Determine timeouts based on platform (Linux/Wayland may need longer)
is_linux = platform.system() == "Linux"
socket_connect_timeout = 1500 if is_linux else 500 # ms
socket_write_timeout = 1000 if is_linux else 500 # ms
try:
# Proactive cleanup of stale IPC artifacts on Linux to avoid "plugin xcb" and invalid socket issues
cleanup_stale_qt_ipc_artifacts(LOCAL_SERVER_NAME, SHARED_MEM_KEY)
# On Linux, also check file-based lock (more reliable after crashes)
if is_linux:
is_first_by_file, lock_file_path = check_and_create_lock_file()
if not is_first_by_file:
logging.warning("File lock indicates another instance is running. Attempting to activate it via socket...")
# Try to contact the existing instance
local_socket = QLocalSocket()
local_socket.connectToServer(LOCAL_SERVER_NAME)
if local_socket.waitForConnected(socket_connect_timeout):
logging.info("Connected to existing instance via socket. Sending 'show' signal.")
local_socket.write(b'show\n')
local_socket.waitForBytesWritten(socket_write_timeout)
local_socket.disconnectFromServer()
local_socket.close()
logging.info("Signal sent. Exiting this instance.")
# Clean up our lock file since we're not the primary instance
cleanup_lock_file()
sys.exit(0)
else:
logging.warning("Could not connect to existing instance despite file lock. Lock may be stale.")
# Don't exit yet - let the QSharedMemory check decide
shared_memory = QSharedMemory(SHARED_MEM_KEY)
# Tries to create shared memory. If it fails because it already exists...
if not shared_memory.create(1, QSharedMemory.AccessMode.ReadOnly):
if shared_memory.error() == QSharedMemory.SharedMemoryError.AlreadyExists:
logging.warning("Another instance of SaveState is already running (shared memory). Attempting to activate it.")
app_should_run = False # Assume another instance until proven stale
# Try to contact the other instance
local_socket = QLocalSocket()
local_socket.connectToServer(LOCAL_SERVER_NAME)
if local_socket.waitForConnected(socket_connect_timeout):
logging.info("Connected to existing instance. Sending 'show' signal.")
bytes_written = local_socket.write(b'show\n')
if bytes_written == -1:
logging.error(f"Failed to write to local socket: {local_socket.errorString()}")
elif not local_socket.waitForBytesWritten(socket_write_timeout):
logging.warning("Timeout waiting for bytes written to local socket.")
else:
logging.debug("Signal 'show' sent successfully.")
local_socket.disconnectFromServer()
local_socket.close()
logging.info("Signal sent. Exiting this instance.")
if shared_memory.isAttached():
shared_memory.detach()
# Clean up file lock on Linux
if is_linux:
cleanup_lock_file()
sys.exit(0)
else:
# Likely stale shared memory/socket; attempt automatic cleanup & retry
logging.error(f"Unable to connect to existing instance server '{LOCAL_SERVER_NAME}': {local_socket.errorString()} - attempting stale lock cleanup and retry...")
try:
local_socket.abort()
except Exception:
pass
cleanup_stale_qt_ipc_artifacts(LOCAL_SERVER_NAME, SHARED_MEM_KEY)
# Retry: allocate a fresh shared memory object and try to create again
retry_mem = QSharedMemory(SHARED_MEM_KEY)
if retry_mem.create(1, QSharedMemory.AccessMode.ReadOnly):
logging.warning("Stale locks cleaned. Proceeding as first instance after retry.")
shared_memory = retry_mem
app_should_run = True
else:
logging.critical(f"Retry create shared memory failed: {retry_mem.errorString()}")
if retry_mem.isAttached():
retry_mem.detach()
# As a last resort, remove server name and exit to avoid duplicate instances
QLocalServer.removeServer(LOCAL_SERVER_NAME)
if is_linux:
cleanup_lock_file()
sys.exit(1)
else:
# Other recoverable shared memory error
logging.error(f"QSharedMemory fatal error (create): {shared_memory.errorString()}")
app_should_run = False # Do not start the GUI
if shared_memory.isAttached(): shared_memory.detach() # Try to clean up
if is_linux:
cleanup_lock_file()
sys.exit(1) # Exit with error
# If we get here, the memory was created successfully (we are the first instance)
logging.debug(f"Shared memory segment '{SHARED_MEM_KEY}' created successfully.")
except Exception as e_shmem_init:
logging.critical(f"Unexpected error during SharedMemory initialization: {e_shmem_init}", exc_info=True)
app_should_run = False
if shared_memory and shared_memory.isAttached(): shared_memory.detach() # Try to clean up
if is_linux:
cleanup_lock_file()
sys.exit(1)
# If app_should_run is still True, we are the first GUI instance
if app_should_run:
logging.info("First GUI instance. Now importing heavy modules...")
# --- NOW import heavy modules (only for first instance) ---
from PySide6.QtWidgets import QApplication, QMessageBox, QDialog, QWidget
from PySide6.QtGui import QIcon
from core import settings_manager
from gui.gui_utils import QtLogHandler
from common.utils import resource_path
from SaveState_gui import MainWindow
# Add Qt log handler now that gui_utils is imported
qt_log_handler = QtLogHandler()
qt_log_handler.setFormatter(log_formatter)
qt_log_handler.setLevel(logging.INFO)
root_logger.addHandler(qt_log_handler)
logging.info("Heavy modules loaded. Starting application...")
# --- Initialize QApplication and Splash Screen EARLY ---
app = None # Initialize to None
splash = None # Initialize splash to None
try:
# QApplication initialization
app = QApplication.instance() # Check if it already exists (e.g. from backup_runner)
if not app:
logging.debug("No existing QApplication, creating one for SaveState GUI.")
# Pass sys.argv, or a default if not available (e.g. when frozen)
app_args = sys.argv if hasattr(sys, 'argv') and sys.argv else ['SaveStateGUI']
app = QApplication(app_args)
QApplication.setApplicationName("SaveState"); QApplication.setApplicationVersion(config.APP_VERSION); QApplication.setOrganizationName("Matteo")
# Set window icon for the app globally (effective for dialogs like QMessageBox)
app_icon_path = resource_path(os.path.join("icons", "SaveStateIconBK.ico")) # Corrected filename
if not os.path.exists(app_icon_path):
logging.warning(f"Application icon not found at {app_icon_path}. Using default icon.")
else:
app.setWindowIcon(QIcon(app_icon_path))
created_main_app_instance = True
else:
logging.debug("Existing QApplication instance found.")
created_main_app_instance = False
except Exception as e_app_init:
# Critical error during QApplication init, cannot proceed
logging.critical(f"Critical application init error: {e_app_init}", exc_info=True)
# Try to show a Qt message box
try:
QMessageBox.critical(None, "Critical Startup Error", f"Unable to initialize the graphical environment.\n{e_app_init}")
except Exception as e_msgbox:
logging.error(f"Failed to show critical error QMessageBox: {e_msgbox}")
sys.exit(1) # Exit immediately
# --- NOW Check Single Instance Lock and Start Local Server ---
logging.info("Checking single instance lock and starting local server...")
# Ensure shared memory is attached (even though we created it)
# This is more of a sanity check.
if not shared_memory.isAttached():
logging.warning("Shared memory segment was created but not attached? Trying to attach...")
if not shared_memory.attach():
logging.critical(f"Failed to attach to own shared memory: {shared_memory.errorString()}")
# We cannot continue without shared memory
# if splash: splash.close() # Close splash before exit
sys.exit(1)
# Create local server to receive signals from other instances
local_server = QLocalServer()
# Remove any previous orphaned servers with the same name
if QLocalServer.removeServer(LOCAL_SERVER_NAME):
logging.warning(f"Removed potentially orphaned local server '{LOCAL_SERVER_NAME}'")
if not local_server.listen(LOCAL_SERVER_NAME):
logging.error(f"Unable to start local server '{LOCAL_SERVER_NAME}': {local_server.errorString()}")
# Try a last cleanup pass and retry once
cleanup_stale_qt_ipc_artifacts(LOCAL_SERVER_NAME, SHARED_MEM_KEY)
if not local_server.listen(LOCAL_SERVER_NAME):
logging.error(f"Retry listen failed for '{LOCAL_SERVER_NAME}': {local_server.errorString()}")
cleanup_instance_lock(local_server, shared_memory)
sys.exit(1)
else:
logging.info(f"Local server listening on: {local_server.fullServerName()}")
# --- Continue with the rest of the initialization ---
window = None # Inizializza a None
exit_code = 1 # Default exit code in caso di errore
try:
# Connect cleanup to QApplication exit (DO THIS EARLY)
app.aboutToQuit.connect(lambda: cleanup_instance_lock(local_server, shared_memory))
# --- Caricamento Impostazioni (senza applicazione traduttore qui) ---
# if splash: # Aggiorna messaggio se lo splash è attivo
# splash.showMessage("Caricamento impostazioni...", Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignCenter, Qt.GlobalColor.white)
# app.processEvents()
logging.info("Loading settings...")
current_settings, is_first_launch = settings_manager.load_settings()
logging.info("Settings loaded.")
# Ensure secondary mirror (.savestate) is up-to-date when not in portable mode
try:
settings_manager.sync_secondary_config_mirror(current_settings)
except Exception:
pass
# --- Fine Caricamento Impostazioni ---
# --- Validate Backup Directory (before creating MainWindow) ---
# This catches the case where the backup folder was moved/deleted
if not is_first_launch: # Skip for first launch (will be configured in settings dialog)
backup_dir = current_settings.get("backup_base_dir", "")
if backup_dir and not os.path.isdir(backup_dir):
logging.warning(f"Backup directory not found: {backup_dir}")
try:
from backup.backup_dir_validator import check_and_fix_backup_directory
# Create a temporary parent widget for the dialog
temp_parent = QWidget()
temp_parent.setWindowTitle("SaveState")
success, current_settings = check_and_fix_backup_directory(current_settings, temp_parent)
if not success:
logging.error("User cancelled backup directory selection. Exiting.")
QMessageBox.warning(
None,
"SaveState - Startup Cancelled",
"The application cannot start without a valid backup folder.\n\n"
"Please restart SaveState and select a valid backup location."
)
cleanup_instance_lock(local_server, shared_memory)
sys.exit(0)
# Save the updated settings if the path was changed
if current_settings.get("backup_base_dir") != backup_dir:
logging.info(f"Saving updated backup directory: {current_settings.get('backup_base_dir')}")
settings_manager.save_settings(current_settings)
except ImportError as e_import:
logging.error(f"Could not from backup import backup_dir_validator: {e_import}")
# Try to create the directory anyway
try:
os.makedirs(backup_dir, exist_ok=True)
except Exception:
pass
except Exception as e_validate:
logging.error(f"Error validating backup directory: {e_validate}", exc_info=True)
# --- End Validate Backup Directory ---
# --- Creazione Finestra Principale e gestione primo avvio ---
# if splash: # Aggiorna messaggio
# splash.showMessage("Creazione interfaccia utente...", Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignCenter, Qt.GlobalColor.white)
# app.processEvents()
# Crea la finestra principale, passando gli handler log
# qt_log_handler è già definito sopra
# console_handler è definito sopra
logging.debug("Creating MainWindow instance...")
# Pass the handlers created here to MainWindow
window = MainWindow(current_settings, console_handler, qt_log_handler, settings_manager)
logging.debug("MainWindow instance created.")
# Language handling removed - application is now English-only
if is_first_launch:
# if splash: # Update message
# splash.showMessage("Configurazione iniziale...", Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignCenter, Qt.GlobalColor.white)
# app.processEvents()
logging.info("First launch detected, showing settings dialog.")
# Import SettingsDialog here to avoid circular module dependencies
from dialogs.settings_dialog import SettingsDialog
settings_dialog = SettingsDialog(current_settings.copy(), window, is_initial_setup=True) # Passa copia e parent
if settings_dialog.exec() == QDialog.Accepted:
new_settings = settings_dialog.get_settings()
# NOTE: If the user restored configs from backup, the dialog may have updated
# window state already. Reload from disk to ensure favorites/profiles/settings are current.
if settings_manager.save_settings(new_settings):
# Reload from disk to ensure we pick up any files restored by dialog
window.current_settings, _first = settings_manager.load_settings()
# Language handling removed - application is now English-only
window.theme_manager.update_theme() # Applica tema
# Reload favorites cache to reflect restored favorites immediately
try:
import importlib
from gui_components import favorites_manager as _fav
# Ensure module paths reflect the (possibly) new active config dir
_fav._cache_loaded = False
importlib.reload(_fav)
_fav._cache_loaded = False
_fav.load_favorites()
except Exception:
pass
# Reload profiles from disk and refresh table
try:
import importlib
from core import core_logic as _cl
importlib.reload(_cl)
window.profiles = _cl.load_profiles()
except Exception:
window.profiles = backup_runner.core_logic.load_profiles() if hasattr(backup_runner, 'core_logic') else __import__('core.core_logic', fromlist=['core_logic']).load_profiles()
window.updateUiText() # Aggiorna UI
window.profile_table_manager.update_profile_table() # Aggiorna tabella
logging.info("Initial settings configured and saved by user.")
else:
QMessageBox.critical(window, "Error", "Unable to save initial settings.")
else: # User cancelled the first launch dialog
reply = QMessageBox.question(window, "Default Settings",
"No specific settings saved. Use default settings and continue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel,
QMessageBox.StandardButton.Yes)
if reply != QMessageBox.StandardButton.Yes:
logging.info("Exit requested by user on first launch cancel.")
# Cleanup before exiting
cleanup_instance_lock(local_server, shared_memory)
sys.exit(0)
else: # Save defaults if user accepts to continue
if not settings_manager.save_settings(current_settings): # Save the loaded defaults
QMessageBox.warning(window, "Error", "Unable to save default settings.")
# Continue with the loaded defaults in memory
# Connect the local server signal to the window slot
# The 'window' instance now definitely exists
# The slot `activateExistingInstance` is defined in MainWindow
local_server.newConnection.connect(window.activateExistingInstance)
logging.debug("Connected local server newConnection signal to window.activateExistingInstance slot.")
window.show()
# Perform startup cloud actions (auto-connect, auto-sync) after UI is loaded
# Use QTimer.singleShot to ensure UI is fully rendered before starting
from PySide6.QtCore import QTimer
QTimer.singleShot(500, window.cloud_panel.perform_startup_actions)
# Startup update check (opt-in). Delayed slightly so the main
# window is painted before any potential dialog activity and
# so cloud startup isn't competing for the network at the same
# exact moment.
if hasattr(window, "maybe_check_updates_on_startup"):
QTimer.singleShot(1500, window.maybe_check_updates_on_startup)
# Log system locale information for date formatting
from PySide6.QtCore import QLocale, QDateTime
system_locale = QLocale.system()
locale_name = system_locale.name()
# Get date format string and determine format type
date_format_str = system_locale.dateFormat(QLocale.FormatType.ShortFormat)
date_format_type = "Unknown"
if date_format_str.startswith("d"):
date_format_type = "European (day first)"
elif date_format_str.startswith("M"):
date_format_type = "American (month first)"
elif date_format_str.startswith("y"):
date_format_type = "ISO (year first)"
# Log concise information
logging.info(f"System locale: {locale_name} - Using {date_format_type} date format")
logging.info("Starting Qt application event loop...")
if pyi_splash:
logging.debug("Closing PyInstaller splash screen...")
pyi_splash.close()
logging.debug("Splash screen close command sent.")
exit_code = app.exec() # Avvia loop eventi GUI
logging.info(f"Qt application event loop finished with exit code: {exit_code}")
except ImportError as e_imp:
# CRITICAL: Library import error, application cannot start.
logging.critical(f"Missing library: {e_imp}. Application cannot start.", exc_info=True)
QMessageBox.critical(None, "Import Error", f"Critical error: missing library.\n{e_imp}\nThe application cannot start.")
sys.exit(1)
except Exception as e_gui_init:
logging.critical(f"Fatal GUI initialization error: {e_gui_init}", exc_info=True)
try:
QMessageBox.critical(None, "Startup Error", f"Fatal error during GUI initialization:\n{e_gui_init}")
except Exception as e_final_msgbox:
logging.error(f"Failed to show fatal GUI error QMessageBox: {e_final_msgbox}")
sys.exit(1)
finally:
# The cleanup is already called from app.aboutToQuit.connect
# It's not necessary to call it again here unless app.exec() is never reached
# But in that case, the aboutToQuit connection would not be triggered.
# If the app was not created or exec was not called, perform cleanup manually.
if app is None or exit_code != 0 and not app.closingDown():
logging.warning("Performing manual cleanup due to early exit or error before event loop.")
cleanup_instance_lock(local_server, shared_memory)
sys.exit(exit_code) # Exit with the appropriate code
else:
# This case should not be reached if the logic above is correct
logging.error("Reached unexpected state where app_should_run is False but execution continued.")
sys.exit(1)