diff --git a/fs_irods/iRODSFS.py b/fs_irods/iRODSFS.py index cb7ea36..4b0a625 100644 --- a/fs_irods/iRODSFS.py +++ b/fs_irods/iRODSFS.py @@ -1,10 +1,13 @@ +import atexit import datetime from io import BufferedRandom import io +import logging import os from multiprocessing import RLock from typing import Text +from weakref import WeakKeyDictionary from fs.base import FS from fs.info import Info from fs.permissions import Permissions @@ -18,6 +21,24 @@ from fs_irods.utils import can_create +fses = WeakKeyDictionary() +_logger = logging.getLogger(__name__) + +# Close out dangling file handles. +def finalize(): + for fs in list(fses): + fs._finalize_files() + +try: + # (see python-irodsclient issue #614) + from irods.at_client_exit import ( + register as register_cleanup_function, + BEFORE_PRC) + register_cleanup_function(BEFORE_PRC, finalize) +except ImportError: + _logger.info("Content written to iRODSFS file handles may not be automatically saved at process exit [#18]." + " Recommend upgrading to v2.1.1 of the Python iRODS Client.") + _utc=datetime.timezone(datetime.timedelta(0)) class iRODSFS(FS): @@ -27,8 +48,9 @@ def __init__(self, session: iRODSSession) -> None: self._host = session.host self._port = session.port self._zone = session.zone - self._session = session + self.files = WeakKeyDictionary() + fses[self] = None def wrap(self, path: str) -> str: if path.startswith(f"/{self._zone}"): @@ -112,6 +134,27 @@ def makedir(self, path: str, permissions: Permissions|None = None, recreate: boo with self._lock: self._session.collections.create(self.wrap(path), recurse=False) + # Allow Python iRODS Client to preemptively close handles to data object (aka "file") handles opened via + # iRODSFS, if this is happening at interpreter exit, so it can ensure shutdown happen in the proper order. + def _finalize_files(self): + self._files_finalized = 1 + l = list(self.files) + while l: + f = l.pop() + if not f.closed: + f.close() + + def __del__(self): + if not getattr(self,'_files_finalized',None): + self._finalize_files() + + # Store weak references to open file handles that maintain a hard reference to the iRODSFS object. + # In this way, the iRODSFS can only be destructed once these file handles are gone. + def open(self,*a,**kw): + fd = super().open(*a,**kw) + self.files[fd] = self + return fd + def openbin(self, path: str, mode:str = "r", buffering: int = -1, **options) -> BufferedRandom: """Open a binary file-like object on the filesystem. Args: diff --git a/pyproject.toml b/pyproject.toml index 8e2c2c9..0c46312 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.8" fs = "^2.4.16" -python-irodsclient = "^1.1.9" +python-irodsclient = ">=1.1.9" [tool.poetry.group.dev.dependencies]