From da1dc4a088e5d4c0e0b448b419260e1179f333e2 Mon Sep 17 00:00:00 2001 From: Markus Rosenstihl Date: Sun, 18 Oct 2026 11:11:14 +0200 Subject: [PATCH] fix log dump race and unclosed HDF5 data pool file in DamarisGUI --- src/damaris/gui/DamarisGUI.py | 230 +++++++++++++++++----------------- 1 file changed, 116 insertions(+), 114 deletions(-) diff --git a/src/damaris/gui/DamarisGUI.py b/src/damaris/gui/DamarisGUI.py index 101eda4..ee495a0 100644 --- a/src/damaris/gui/DamarisGUI.py +++ b/src/damaris/gui/DamarisGUI.py @@ -613,8 +613,9 @@ class DamarisGUI: logger.debug("Data cleanup took %.3fs", time.time() - data_cleanup_start) # set the text mark for hdf logging - if self.log.textbuffer.get_mark( "lastdumped" ) is None: - self.log.textbuffer.create_mark( "lastdumped", self.log.textbuffer.get_end_iter( ), left_gravity=True ) + with self.log.lock: + if self.log.textbuffer.get_mark( "lastdumped" ) is None: + self.log.textbuffer.create_mark( "lastdumped", self.log.textbuffer.get_end_iter( ), left_gravity=True ) loop_run=0 #user shorter interval because this blocks user events from the GUI @@ -991,125 +992,124 @@ class DamarisGUI: # now it's time to create the hdf file dump_file = None - if not os.path.isfile( self.dump_filename ): - if not init: - logger.warning("Dump file \"%s\" vanished unexpectedly, creating new one", self.dump_filename) - # have a look to the path and create necessary directories - dir_stack = [ ] - dir_trunk = os.path.dirname( os.path.abspath( self.dump_filename ) ) - while dir_trunk != "" and not os.path.isdir( dir_trunk ): - dir_stack.append( os.path.basename( dir_trunk ) ) - dir_trunk = os.path.dirname( dir_trunk ) - try: - while len( dir_stack ): - dir_trunk = os.path.join( dir_trunk, dir_stack.pop( ) ) - if os.path.isdir( dir_trunk ): - continue - os.mkdir( dir_trunk ) - except OSError as e: - logger.error("Could not create dump directory '%s', so hdf5 dumps disabled: %s", dir_trunk, e) - self.dump_filename = "" - self.dump_timeinterval = 0 - return True + try: + if not os.path.isfile( self.dump_filename ): + if not init: + logger.warning("Dump file \"%s\" vanished unexpectedly, creating new one", self.dump_filename) + # have a look to the path and create necessary directories + dir_stack = [ ] + dir_trunk = os.path.dirname( os.path.abspath( self.dump_filename ) ) + while dir_trunk != "" and not os.path.isdir( dir_trunk ): + dir_stack.append( os.path.basename( dir_trunk ) ) + dir_trunk = os.path.dirname( dir_trunk ) + try: + while len( dir_stack ): + dir_trunk = os.path.join( dir_trunk, dir_stack.pop( ) ) + if os.path.isdir( dir_trunk ): + continue + os.mkdir( dir_trunk ) + except OSError as e: + logger.error("Could not create dump directory '%s', so hdf5 dumps disabled: %s", dir_trunk, e) + self.dump_filename = "" + self.dump_timeinterval = 0 + return True - # create new dump file - dump_file = tables.open_file( self.dump_filename, mode="w", title="DAMARIS experiment data" ) - # write scripts and other useful information - scriptgroup = dump_file.create_group( "/", "scripts", "Used Scripts" ) - exp_text, res_text = self.sw.get_scripts( ) - if self.si.exp_script: - dump_file.create_array( scriptgroup, "experiment_script", exp_text.encode('utf-8') ) - if self.si.res_script: - dump_file.create_array( scriptgroup, "result_script", res_text.encode('utf-8') ) - if self.si.backend_executable: - dump_file.create_array( scriptgroup, "backend_executable", self.si.backend_executable.encode('utf-8') ) - if self.spool_dir: - dump_file.create_array( scriptgroup, "spool_directory", self.spool_dir.encode('utf-8') ) - timeline_tablecols = numpy.recarray( 0, dtype=([ ("time", "S17"), - ("experiments", "int64"), - ("results", "int64") ]) ) - timeline_table = dump_file.create_table( "/", "timeline", timeline_tablecols, - title="Timeline of Experiment" ) - if tables.__version__[ 0 ] == "1": - logarray = dump_file.create_vlarray( where=dump_file.root, - name="log", - atom=tables.StringAtom( length=120 ), - title="log messages", - filters=tables.Filters( complevel=9, complib='zlib' ) ) - else: - logarray = dump_file.create_earray( where=dump_file.root, - name="log", - atom=tables.StringAtom( itemsize=120 ), - shape=(0,), - title="log messages", - filters=tables.Filters( complevel=9, complib='zlib' ) ) + # create new dump file + dump_file = tables.open_file( self.dump_filename, mode="w", title="DAMARIS experiment data" ) + # write scripts and other useful information + scriptgroup = dump_file.create_group( "/", "scripts", "Used Scripts" ) + exp_text, res_text = self.sw.get_scripts( ) + if self.si.exp_script: + dump_file.create_array( scriptgroup, "experiment_script", exp_text.encode('utf-8') ) + if self.si.res_script: + dump_file.create_array( scriptgroup, "result_script", res_text.encode('utf-8') ) + if self.si.backend_executable: + dump_file.create_array( scriptgroup, "backend_executable", self.si.backend_executable.encode('utf-8') ) + if self.spool_dir: + dump_file.create_array( scriptgroup, "spool_directory", self.spool_dir.encode('utf-8') ) + timeline_tablecols = numpy.recarray( 0, dtype=([ ("time", "S17"), + ("experiments", "int64"), + ("results", "int64") ]) ) + timeline_table = dump_file.create_table( "/", "timeline", timeline_tablecols, + title="Timeline of Experiment" ) + if tables.__version__[ 0 ] == "1": + logarray = dump_file.create_vlarray( where=dump_file.root, + name="log", + atom=tables.StringAtom( length=120 ), + title="log messages", + filters=tables.Filters( complevel=9, complib='zlib' ) ) + else: + logarray = dump_file.create_earray( where=dump_file.root, + name="log", + atom=tables.StringAtom( itemsize=120 ), + shape=(0,), + title="log messages", + filters=tables.Filters( complevel=9, complib='zlib' ) ) - if dump_file is None and os.path.isfile( self.dump_filename ) and tables.is_pytables_file( self.dump_filename ): - # prepare for update - dump_file = tables.open_file( self.dump_filename, mode="r+" ) + if dump_file is None and os.path.isfile( self.dump_filename ) and tables.is_pytables_file( self.dump_filename ): + # prepare for update + dump_file = tables.open_file( self.dump_filename, mode="r+" ) - if dump_file is None: - # exit! - logger.error("Could not create dump directory '%s', so hdf5 dumps disabled", dir_trunk) - self.dump_filename = "" - self.dump_timeinterval = 0 - return True + if dump_file is None: + # exit! + logger.error("Could not create dump directory '%s', so hdf5 dumps disabled", dir_trunk) + self.dump_filename = "" + self.dump_timeinterval = 0 + return True - # no undo please! - if dump_file.is_undo_enabled( ): - dump_file.disable_undo( ) + # no undo please! + if dump_file.is_undo_enabled( ): + dump_file.disable_undo( ) - # save the data! - if self.data is not None: - self.data.write_hdf5( dump_file, where="/", name="data_pool", - complib=self.dump_complib, complevel=self.dump_complevel, - incremental=not init, write_interval=self.dump_timeinterval ) + # save the data! + if self.data is not None: + self.data.write_hdf5( dump_file, where="/", name="data_pool", + complib=self.dump_complib, complevel=self.dump_complevel, + incremental=not init, write_interval=self.dump_timeinterval ) - # Re-enable run button if this was called from save thread - # NO: this is dangerous as it allows starting a new experiment before cleanup is finished. - # it will be re-enabled in observe_running_experiment after joining this thread. - # if not init and threading.current_thread().name == "dump states": - # gdk.threads_enter() - # self.toolbar_run_button.set_sensitive(True) - # gdk.threads_leave() + # Re-enable run button if this was called from save thread + # NO: this is dangerous as it allows starting a new experiment before cleanup is finished. + # it will be re-enabled in observe_running_experiment after joining this thread. + # if not init and threading.current_thread().name == "dump states": + # gdk.threads_enter() + # self.toolbar_run_button.set_sensitive(True) + # gdk.threads_leave() - # now save additional information - e = 0 - r = 0 - if self.si is not None: - e = self.si.data.get( "__recentexperiment", -1 ) + 1 - r = self.si.data.get( "__recentresult", -1 ) + 1 - timeline_table = dump_file.root.timeline - timeline_row = timeline_table.row - timeline_row[ "time" ] = time.strftime( "%Y%m%d %H:%M:%S" ) - timeline_row[ "experiments" ] = e - timeline_row[ "results" ] = r - timeline_row.append( ) - timeline_table.flush( ) + # now save additional information + e = 0 + r = 0 + if self.si is not None: + e = self.si.data.get( "__recentexperiment", -1 ) + 1 + r = self.si.data.get( "__recentresult", -1 ) + 1 + timeline_table = dump_file.root.timeline + timeline_row = timeline_table.row + timeline_row[ "time" ] = time.strftime( "%Y%m%d %H:%M:%S" ) + timeline_row[ "experiments" ] = e + timeline_row[ "results" ] = r + timeline_row.append( ) + timeline_table.flush( ) - # save log window information: - # also save backend's logfile information? - logtextbuffer = self.log.textbuffer - last_end = logtextbuffer.get_mark( "lastdumped" ) - if last_end is None: - last_end = logtextbuffer.create_mark( "lastdumped", logtextbuffer.get_start_iter( ), left_gravity=True ) - #logtext_start = logtextbuffer.get_iter_at_mark( last_end ) - logtext_end = logtextbuffer.get_end_iter( ) - logtextbuffer.move_mark( last_end, logtext_end ) - # Create new iterators after the mark move - logtext_start = logtextbuffer.get_iter_at_mark( last_end ) - logtext_end = logtextbuffer.get_end_iter( ) - # recode from unicode - logtext = logtextbuffer.get_text( logtext_start, logtext_end, True ) - # avoid circular references (seems to be necessary with gtk-2.12) - # del logtextbuffer, logtext_start, logtext_end, last_end - for l in logtext.splitlines( ): - dump_file.root.log.append( numpy.array( [ l ], dtype="S120" ) ) + # save log window information: + # also save backend's logfile information? + # the dump thread must not race with the GUI thread appending messages + with self.log.lock: + logtextbuffer = self.log.textbuffer + last_end = logtextbuffer.get_mark( "lastdumped" ) + if last_end is None: + last_end = logtextbuffer.create_mark( "lastdumped", logtextbuffer.get_start_iter( ), left_gravity=True ) + logtext_start = logtextbuffer.get_iter_at_mark( last_end ) + logtext_end = logtextbuffer.get_end_iter( ) + logtext = logtextbuffer.get_text( logtext_start, logtext_end, True ) + logtextbuffer.move_mark( "lastdumped", logtext_end ) + if logtext is not None: + for l in logtext.splitlines( ): + dump_file.root.log.append( numpy.array( [ l ], dtype="S120" ) ) - dump_file.flush( ) - dump_file.close( ) + dump_file.flush( ) + finally: + if dump_file is not None: + dump_file.close( ) self.last_dumped = time.time( ) - del dump_file return True @@ -1351,6 +1351,7 @@ class LogWindow: self.textview.connect( "key-press-event", self.textview_keypress) apply_font_css(self.textview, "Monospace 10") self.textbuffer = self.textview.get_buffer( ) + self.lock = threading.Lock( ) self.last_timetag = None self( "Started in directory %s\n" % os.getcwd( ) ) @@ -1365,9 +1366,10 @@ class LogWindow: timetag_int = math.floor( timetag ) timetag_ms = int( (timetag - timetag_int) * 1000 ) date_tag = time.strftime( "%Y%m%d %H:%M:%S.%%03d:\n", time.localtime( timetag_int ) ) % timetag_ms - self.textbuffer.place_cursor( self.textbuffer.get_end_iter( ) ) - self.textbuffer.insert_at_cursor( date_tag + str( message ) ) - self.textview.scroll_to_mark( self.textbuffer.get_insert( ), 0.1 , False, 0.5, 0.5) + with self.lock: + self.textbuffer.place_cursor( self.textbuffer.get_end_iter( ) ) + self.textbuffer.insert_at_cursor( date_tag + str( message ) ) + self.textview.scroll_to_mark( self.textbuffer.get_insert( ), 0.1 , False, 0.5, 0.5) def textview_keypress( self, widget, event, data=None ): if event.state & gdk.ModifierType.CONTROL_MASK != 0: