17417: Merge branch 'main' into 17417-add-arm64
[arvados.git] / services / fuse / arvados_fuse / fusefile.py
index d33f9f9e41655eede0e794027de9d8d4546248b4..45d3db16fe00d7edb802f8d279334b312d8fcc48 100644 (file)
@@ -1,15 +1,24 @@
-import logging
-import re
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+from __future__ import absolute_import
+from builtins import bytes
 import json
 import llfuse
+import logging
+import re
+import time
 
-from fresh import FreshBase, convertTime
+from .fresh import FreshBase, convertTime
 
 _logger = logging.getLogger('arvados.arvados_fuse')
 
 class File(FreshBase):
     """Base for file objects."""
 
+    __slots__ = ("inode", "parent_inode", "_mtime")
+
     def __init__(self, parent_inode, _mtime=0):
         super(File, self).__init__()
         self.inode = None
@@ -28,8 +37,8 @@ class File(FreshBase):
     def mtime(self):
         return self._mtime
 
-    def clear(self, force=False):
-        return True
+    def clear(self):
+        pass
 
     def writable(self):
         return False
@@ -41,12 +50,16 @@ class File(FreshBase):
 class FuseArvadosFile(File):
     """Wraps a ArvadosFile."""
 
-    def __init__(self, parent_inode, arvfile, _mtime):
+    __slots__ = ('arvfile', '_enable_write')
+
+    def __init__(self, parent_inode, arvfile, _mtime, enable_write):
         super(FuseArvadosFile, self).__init__(parent_inode, _mtime)
         self.arvfile = arvfile
+        self._enable_write = enable_write
 
     def size(self):
-        return self.arvfile.size()
+        with llfuse.lock_released:
+            return self.arvfile.size()
 
     def readfrom(self, off, size, num_retries=0):
         with llfuse.lock_released:
@@ -60,7 +73,7 @@ class FuseArvadosFile(File):
         return False
 
     def writable(self):
-        return self.arvfile.writable()
+        return self._enable_write and self.arvfile.writable()
 
     def flush(self):
         with llfuse.lock_released:
@@ -78,7 +91,7 @@ class StringFile(File):
         return len(self.contents)
 
     def readfrom(self, off, size, num_retries=0):
-        return self.contents[off:(off+size)]
+        return bytes(self.contents[off:(off+size)], encoding='utf-8')
 
 
 class ObjectFile(StringFile):
@@ -93,8 +106,47 @@ class ObjectFile(StringFile):
         return self.object_uuid
 
     def update(self, obj=None):
+        if obj is None:
+            # TODO: retrieve the current record for self.object_uuid
+            # from the server. For now, at least don't crash when
+            # someone tells us it's a good time to update but doesn't
+            # pass us a fresh obj. See #8345
+            return
         self._mtime = convertTime(obj['modified_at']) if 'modified_at' in obj else 0
         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
 
     def persisted(self):
         return True
+
+
+class FuncToJSONFile(StringFile):
+    """File content is the return value of a given function, encoded as JSON.
+
+    The function is called at the time the file is read. The result is
+    cached until invalidate() is called.
+    """
+    def __init__(self, parent_inode, func):
+        super(FuncToJSONFile, self).__init__(parent_inode, "", 0)
+        self.func = func
+
+        # invalidate_inode() is asynchronous with no callback to wait for. In
+        # order to guarantee userspace programs don't get stale data that was
+        # generated before the last invalidate(), we must disallow inode
+        # caching entirely.
+        self.allow_attr_cache = False
+
+    def size(self):
+        self._update()
+        return super(FuncToJSONFile, self).size()
+
+    def readfrom(self, *args, **kwargs):
+        self._update()
+        return super(FuncToJSONFile, self).readfrom(*args, **kwargs)
+
+    def _update(self):
+        if not self.stale():
+            return
+        self._mtime = time.time()
+        obj = self.func()
+        self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
+        self.fresh()