Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/PyMca5/PyMcaCore/NexusDataSource.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,15 @@ def refresh(self):
phynxInstance._sourceName = pattern
self.__lastKeyInfo = {}

def close(self):
"""Close every open HDF5 handle held by this source."""
for instance in self._sourceObjectList:
try:
instance.close()
except Exception as e:
_logger.debug("Error closing HDF5 source: %s", e)
self._sourceObjectList = []

def getSourceInfo(self):
"""
Returns a dictionary with the key "KeyList" (list of all available keys
Expand Down
12 changes: 11 additions & 1 deletion src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,11 +425,14 @@ class FileModel(qt.QAbstractItemModel):
"""
sigFileUpdated = qt.pyqtSignal(object)
sigFileAppended = qt.pyqtSignal(object)
sigReadFailed = qt.pyqtSignal(object)

def __init__(self, parent=None):
qt.QAbstractItemModel.__init__(self, parent)
self.rootItem = RootItem(['File/Group/Dataset', 'Description', 'Shape', 'DType'])
self._idMap = {qt.QModelIndex().internalId(): self.rootItem}
# to warn only once about read error, can be reset intentionally
self._readErrorReported = False

def sort(self, column, order):
#print("FileModel sort called with ", column, order)
Expand Down Expand Up @@ -572,7 +575,14 @@ def parent(self, index):
return self.createIndex(parent.row, 0, parent)

def rowCount(self, index):
return len(self.getProxyFromIndex(index))
try:
# the `len(self.children)` can fail.
return len(self.getProxyFromIndex(index))
except Exception:
if not self._readErrorReported:
self._readErrorReported = True

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FileModel is per source so you will have the error only once per source. A better solution is needed: perhaps refresh at failure is the solution.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See new _recoverFromReadError in QNexusWidget

self.sigReadFailed.emit({"event": "readError", "index": index})
return 0

def openFile(self, filename, weakreference=False):
gc.collect()
Expand Down
27 changes: 27 additions & 0 deletions src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ def dataSourceDestroyed(weakrefReference):
# stays visible and the tree does not collapse.
try:
newModel = HDF5Widget.FileModel()
newModel.sigReadFailed.connect(self._hdf5ReadFailed)
for source in self.data._sourceObjectList:
newModel.appendPhynxFile(source, weakreference=True)
self._modelDict[ref] = newModel
Expand All @@ -515,6 +516,32 @@ def dataSourceDestroyed(weakrefReference):
if hasattr(self.hdf5Widget, "expandToDepth"):
self.hdf5Widget.expandToDepth(0)

def _hdf5ReadFailed(self, ddict):
self._recoverFromReadError(ddict.get("index"))
self._showReadFailedMessage()

def _recoverFromReadError(self, index):
model = self.hdf5Widget.model()
# Collapse the node that failed to be read
if index is not None and index.isValid():
self.hdf5Widget.collapse(index)
# A later failed expansion should warn again
if hasattr(model, "_readErrorReported"):
model._readErrorReported = False

def _showReadFailedMessage(self):
msg = qt.QMessageBox(self)
msg.setIcon(qt.QMessageBox.Warning)
msg.setWindowTitle("Cannot read HDF5 file")
msg.setText("The selected file could not be read.")
msg.setInformativeText(
"It may be being written now. Try again later." \
"If it is stuck, try to refresh (F5) or close and open the file again.")
# `open()` (not `exec()`) blocks the window but not the code
# `WA_DeleteOnClose` to delete itself on close (to avoid a leak)
msg.setAttribute(qt.Qt.WA_DeleteOnClose)
msg.open()

def _autoRefreshDatasets(self, source=None, moveToLastSlice=True):
"""
Auto-refresh: re-read datasets and re-plot without re-building the tree.
Expand Down
39 changes: 23 additions & 16 deletions src/PyMca5/PyMcaGui/pymca/QDispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,23 +365,30 @@ def _sourceSelectorSlot(self, ddict):
if not found:
_logger.debug("WARNING: source not found")
return
sourceType = source.sourceType
del self.sourceList[self.sourceList.index(source)]
for source in self.sourceList:
if sourceType == source.sourceType:
closedSource = source
sourceType = closedSource.sourceType
del self.sourceList[self.sourceList.index(closedSource)]
try:
for source in self.sourceList:
if sourceType == source.sourceType:
self.selectorWidget[sourceType].setDataSource(source)
self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType])
return
#there is no other selection of that type
if len(self.sourceList):
source = self.sourceList[0]
sourceType = source.sourceType
self.selectorWidget[sourceType].setDataSource(source)
self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType])
return
#there is no other selection of that type
if len(self.sourceList):
source = self.sourceList[0]
sourceType = source.sourceType
self.selectorWidget[sourceType].setDataSource(source)
else:
self.selectorWidget[sourceType].setDataSource(None)
self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType])
elif ddict["event"] == "SourceClosed":
_logger.debug("not implemented yet")
else:
self.selectorWidget[sourceType].setDataSource(None)
self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType])
finally:
# Without this the file stays "open" in the process
# and could not be reopened until PyMca restarted.
try:
closedSource.close()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If self.sourceList is the only object that holds a reference to the h5py.File object then python's garbage collector will close the file at some point after this function (_sourceSelectorSlot) exists.

I don't mind making sure we close the file here in a finally block. Probably better in case there are weird situations (like a logged error callstack that keeps the h5py.File object alive).

except Exception as e:
_logger.debug("Error closing source: %s", e)

def _selectionUpdatedSlot(self, ddict):
_logger.debug("_selectionUpdatedSlot(self, dict=%s)", ddict)
Expand Down
Loading