fix log dump race and unclosed HDF5 data pool file in DamarisGUI

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