3838import urllib .error as urllib_err
3939import webbrowser
4040from copy import copy
41- import time
42- import getpass
4341
4442from PyQt6 import QtCore , QtGui , QtWidgets
4543import qtawesome as qta
128126handler = RotatingFileHandler (logfile , maxBytes = log_maxBytes , backupCount = 2 )
129127logger .addHandler (handler )
130128
131- lock_timeout = 30.0 # in seconds. If a project lockfile is older (= has received no heartbeat for 30 seconds),
132- # it is assumed that the host process has died and the project is opened anyways
133- lock_heartbeat_interval = 5 # in seconds.
134-
135-
136- class ProjectLockHeartbeatWorker (QtCore .QObject ):
137- """
138- This worker thread is invoked on opening a project and will write a regular heartbeat (timestamp)
139- to the lock file to signify that the project is still in use and the host process did not crash.
140- """
141- finished = QtCore .pyqtSignal () # Signal for indicating completion
142- io_error = QtCore .pyqtSignal () # Signal indicating an error acessing the lock file to write the heartbeat
143-
144- def __init__ (self , app , lock_file_path ):
145- super ().__init__ ()
146- self .app = app
147- self .lock_file_path = lock_file_path
148- self .is_running = True
149- self .lost_connection = False
150-
151- def write_heartbeat (self ):
152- """Write heartbeat to the lock file every 10 seconds."""
153- last_heartbeat = time .time ()
154- while self .is_running :
155- if time .time () - last_heartbeat >= lock_heartbeat_interval :
156- last_heartbeat = time .time ()
157- try :
158- with open (self .lock_file_path , 'w' , encoding = 'utf-8' ) as lock_file :
159- lock_file .write (f"{ getpass .getuser ()} \n { str (time .time ())} " )
160- self .lost_connection = False
161- except Exception as e_ :
162- print (e_ )
163- if not self .lost_connection :
164- self .io_error .emit ()
165- self .lost_connection = True
166- time .sleep (0.1 )
167-
168- def stop (self ):
169- """Stop the heartbeat process."""
170- self .is_running = False
171- self .finished .emit ()
172-
173-
174129class ProjectEventBus (QtCore .QObject ):
175130 """Application-wide event bus for project database changes.
176131 This is used to notify other dialogs (e.g. reports) of changes to the project database,
@@ -1394,14 +1349,14 @@ def save_backup(self, suffix=""):
13941349 msg = ""
13951350 if self .settings ['backup_av_files' ] == 'True' :
13961351 try :
1397- shutil .copytree (self .project_path , backup , ignore = shutil . ignore_patterns ( '*.lock' ) )
1352+ shutil .copytree (self .project_path , backup )
13981353 except FileExistsError as err :
13991354 msg = _ ("There is already a backup with this name" )
14001355 print (f"{ err } \n msg" )
14011356 logger .warning (_ (msg ) + f"\n { err } " )
14021357 else :
14031358 shutil .copytree (self .project_path , backup ,
1404- ignore = shutil .ignore_patterns ('*.lock' , '*. mp3' , '*.wav' , '*.mp4' , '*.mov' , '*.ogg' ,
1359+ ignore = shutil .ignore_patterns ('*.mp3' , '*.wav' , '*.mp4' , '*.mov' , '*.ogg' ,
14051360 '*.wmv' , '*.MP3' ,
14061361 '*.WAV' , '*.MP4' , '*.MOV' , '*.OGG' , '*.WMV' ))
14071362 # self.ui.textEdit.append(_("WARNING: audio and video files NOT backed up. See settings."))
@@ -1450,9 +1405,6 @@ def __init__(self, app, force_quit=False):
14501405 self .force_quit = force_quit
14511406 self .journal_display = None
14521407
1453- self .heartbeat_thread = None
1454- self .heartbeat_worker = None
1455- self .lock_file_path = ''
14561408 self .ai_chat_window = None
14571409
14581410 if platform .system () == "Windows" and self .app .settings ['stylesheet' ] == "native" :
@@ -2559,71 +2511,6 @@ def project_memo(self):
25592511 self .ui .textEdit .append (_ ("Project memo entered." ))
25602512 self .app .delete_backup = False
25612513
2562- # lock file helper functions:
2563-
2564- def create_lock_file (self , break_existing_lock = False ):
2565- """Create the lock file.
2566- break_existing_lock: if True, the lock file will be created even if it already exists
2567- """
2568- if (not break_existing_lock ) and os .path .exists (self .lock_file_path ):
2569- return False
2570- try :
2571- mode = 'w' if break_existing_lock else 'x'
2572- with open (self .lock_file_path , mode , encoding = 'utf-8' ) as lock_file :
2573- lock_file .write (f"{ getpass .getuser ()} \n { str (time .time ())} " )
2574- return True
2575- except FileExistsError :
2576- return False
2577-
2578- def delete_lock_file (self ):
2579- """ Delete the lock file to release the lock. """
2580-
2581- try :
2582- if self .lock_file_path != '' :
2583- os .remove (self .lock_file_path )
2584- except Exception as e_ :
2585- print ("delete_lock_file" , e_ )
2586- logger .debug (e_ )
2587-
2588- def lock_file_io_error (self ):
2589- msg = _ ('An error occured while writing to the project folder. '
2590- 'Please close the project and try to open it again.' )
2591- msg_box = Message (self .app , _ ("I/O Error" ), msg , "critical" )
2592- btn_close = msg_box .addButton (_ ("Close" ), QtWidgets .QMessageBox .ButtonRole .AcceptRole )
2593- btn_ignore = msg_box .addButton ("Ignore" , QtWidgets .QMessageBox .ButtonRole .RejectRole )
2594- msg_box .setDefaultButton (btn_close )
2595- msg_box .exec ()
2596- if msg_box .clickedButton () == btn_close :
2597- self .close_project ()
2598- logger .debug (msg )
2599-
2600- def prepare_heartbeat_thread (self ):
2601- """ Prepare and start the heartbeat QThread. """
2602-
2603- self .heartbeat_thread = QtCore .QThread ()
2604- self .heartbeat_worker = ProjectLockHeartbeatWorker (self .app , self .lock_file_path )
2605- self .heartbeat_worker .moveToThread (self .heartbeat_thread )
2606- self .heartbeat_thread .started .connect (self .heartbeat_worker .write_heartbeat )
2607- self .heartbeat_worker .finished .connect (self .heartbeat_thread .quit )
2608- self .heartbeat_worker .finished .connect (self .heartbeat_worker .deleteLater )
2609- self .heartbeat_thread .finished .connect (self .heartbeat_thread .deleteLater )
2610- self .heartbeat_worker .io_error .connect (self .lock_file_io_error )
2611- self .heartbeat_thread .start ()
2612-
2613- def stop_heartbeat (self , wait = False ):
2614- """Stop the heartbeat and delete the lock file (if it exists). """
2615-
2616- if self .heartbeat_worker :
2617- try :
2618- self .heartbeat_worker .stop ()
2619- if wait :
2620- self .heartbeat_thread .wait () # Wait for the thread to properly finish
2621- except Exception as e_ :
2622- print (e_ )
2623- logger .debug (e_ )
2624- self .delete_lock_file ()
2625- self .lock_file_path = ''
2626-
26272514 def open_project (self , path_ = "" , newproject = "no" ):
26282515 """ Open an existing project.
26292516 if set, also save a backup datetime stamped copy at the same time.
@@ -2659,54 +2546,6 @@ def open_project(self, path_="", newproject="no"):
26592546 if len (path_split ) == 2 :
26602547 proj_path = path_split [1 ]
26612548 if len (path ) > 3 and proj_path [- 4 :] == ".qda" :
2662- # Lock file management
2663- self .lock_file_path = os .path .normpath (proj_path + '/project_in_use.lock' )
2664- if not self .create_lock_file ():
2665- # Lock file already exists. Checking if it has timed out or not.
2666- with open (self .lock_file_path , 'r' , encoding = 'utf-8' ) as lock_file :
2667- try :
2668- lock_user = lock_file .readline ()[:- 1 ]
2669- lock_timestamp = float (lock_file .readline ())
2670- except Exception as e_ : # TODO add specific exception
2671- print (e_ )
2672- logger .warning (e_ )
2673- # lock file seems corrupted/partially written. Retry once in case another instance was writing to the file at the same time:
2674- time .sleep (0.5 )
2675- try :
2676- lock_user = lock_file .readline ()[:- 1 ]
2677- lock_timestamp = float (lock_file .readline ())
2678- except Exception as e_ : # permanent error, break the lock
2679- print (e_ ) # TODO determine specific exception
2680- logger .warning (e_ )
2681- lock_user = 'unknown'
2682- lock_timestamp = 0.0
2683- if float (time .time ()) - lock_timestamp > lock_timeout :
2684- # has timed out, break the lock
2685- msg = _ (
2686- 'QualCoder detected that the project was not properly closed the last time it was used by "' ) + lock_user + '".\n '
2687- msg += _ (
2688- 'In most cases, you can still continue your work as usual. If you encounter any problems, search for a recent backup in the project folder.' )
2689- logger .warning (msg )
2690- msg_box = Message (self .app , _ ("Open file" ), msg , "information" )
2691- msg_box .setStandardButtons (
2692- QtWidgets .QMessageBox .StandardButton .Ok | QtWidgets .QMessageBox .StandardButton .Abort )
2693- msg_box .setDefaultButton (QtWidgets .QMessageBox .StandardButton .Ok )
2694- ret = msg_box .exec ()
2695- if ret == QtWidgets .QMessageBox .StandardButton .Abort :
2696- self .app .project_path = ""
2697- self .app .project_name = ""
2698- return
2699- self .create_lock_file (break_existing_lock = True )
2700- else :
2701- # lock is valid, project seems to be in use by other user
2702- msg = _ ('Project cannot be opened since it\' s already in use by "' ) + lock_user + _ (
2703- '". Please retry later.' )
2704- logger .warning (msg )
2705- Message (self .app , _ ("Cannot open file" ), msg , "critical" ).exec ()
2706- self .app .project_path = ""
2707- self .app .project_name = ""
2708- return
2709- self .prepare_heartbeat_thread ()
27102549 try :
27112550 self .app .create_connection (proj_path )
27122551 except Exception as err :
@@ -2716,7 +2555,6 @@ def open_project(self, path_="", newproject="no"):
27162555 if self .app .conn is None :
27172556 msg += "\n " + proj_path
27182557 Message (self .app , _ ("Cannot open file" ), msg , "critical" ).exec ()
2719- self .stop_heartbeat ()
27202558 self .app .project_path = ""
27212559 self .app .project_name = ""
27222560 return
@@ -3202,7 +3040,6 @@ def close_project(self):
32023040 print (e_ )
32033041 logger .warning (e_ )
32043042 self .app .conn = None
3205- self .stop_heartbeat (wait = True )
32063043 self .delete_backup_folders ()
32073044 self .fill_recent_projects_menu_actions ()
32083045 self .app .conn = None
0 commit comments