1# Copyright (c) 2008 Giorgos Verigakis <verigak@gmail.com>
2#
3# Permission to use, copy, modify, and distribute this software for any
4# purpose with or without fee is hereby granted, provided that the above
5# copyright notice and this permission notice appear in all copies.
6#
7# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
15from ctypes import *
16from ctypes.util import find_library
17from errno import *
18from functools import partial
19from platform import machine, system
20from stat import S_IFDIR
21from traceback import print_exc
22
23import logging
24
25
26class c_timespec(Structure):
27    _fields_ = [('tv_sec', c_long), ('tv_nsec', c_long)]
28
29class c_utimbuf(Structure):
30    _fields_ = [('actime', c_timespec), ('modtime', c_timespec)]
31
32class c_stat(Structure):
33    pass    # Platform dependent
34
35_system = system()
36if _system in ('Darwin', 'FreeBSD'):
37    _libiconv = CDLL(find_library("iconv"), RTLD_GLOBAL)     # libfuse dependency
38    ENOTSUP = 45
39    c_dev_t = c_int32
40    c_fsblkcnt_t = c_ulong
41    c_fsfilcnt_t = c_ulong
42    c_gid_t = c_uint32
43    c_mode_t = c_uint16
44    c_off_t = c_int64
45    c_pid_t = c_int32
46    c_uid_t = c_uint32
47    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte),
48        c_size_t, c_int, c_uint32)
49    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte),
50        c_size_t, c_uint32)
51    c_stat._fields_ = [
52        ('st_dev', c_dev_t),
53        ('st_ino', c_uint32),
54        ('st_mode', c_mode_t),
55        ('st_nlink', c_uint16),
56        ('st_uid', c_uid_t),
57        ('st_gid', c_gid_t),
58        ('st_rdev', c_dev_t),
59        ('st_atimespec', c_timespec),
60        ('st_mtimespec', c_timespec),
61        ('st_ctimespec', c_timespec),
62        ('st_size', c_off_t),
63        ('st_blocks', c_int64),
64        ('st_blksize', c_int32)]
65elif _system == 'Linux':
66    ENOTSUP = 95
67    c_dev_t = c_ulonglong
68    c_fsblkcnt_t = c_ulonglong
69    c_fsfilcnt_t = c_ulonglong
70    c_gid_t = c_uint
71    c_mode_t = c_uint
72    c_off_t = c_longlong
73    c_pid_t = c_int
74    c_uid_t = c_uint
75    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int)
76    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t)
77
78    _machine = machine()
79    if _machine == 'x86_64':
80        c_stat._fields_ = [
81            ('st_dev', c_dev_t),
82            ('st_ino', c_ulong),
83            ('st_nlink', c_ulong),
84            ('st_mode', c_mode_t),
85            ('st_uid', c_uid_t),
86            ('st_gid', c_gid_t),
87            ('__pad0', c_int),
88            ('st_rdev', c_dev_t),
89            ('st_size', c_off_t),
90            ('st_blksize', c_long),
91            ('st_blocks', c_long),
92            ('st_atimespec', c_timespec),
93            ('st_mtimespec', c_timespec),
94            ('st_ctimespec', c_timespec)]
95    elif _machine == 'ppc':
96        c_stat._fields_ = [
97            ('st_dev', c_dev_t),
98            ('st_ino', c_ulonglong),
99            ('st_mode', c_mode_t),
100            ('st_nlink', c_uint),
101            ('st_uid', c_uid_t),
102            ('st_gid', c_gid_t),
103            ('st_rdev', c_dev_t),
104            ('__pad2', c_ushort),
105            ('st_size', c_off_t),
106            ('st_blksize', c_long),
107            ('st_blocks', c_longlong),
108            ('st_atimespec', c_timespec),
109            ('st_mtimespec', c_timespec),
110            ('st_ctimespec', c_timespec)]
111    else:
112        # i686, use as fallback for everything else
113        c_stat._fields_ = [
114            ('st_dev', c_dev_t),
115            ('__pad1', c_ushort),
116            ('__st_ino', c_ulong),
117            ('st_mode', c_mode_t),
118            ('st_nlink', c_uint),
119            ('st_uid', c_uid_t),
120            ('st_gid', c_gid_t),
121            ('st_rdev', c_dev_t),
122            ('__pad2', c_ushort),
123            ('st_size', c_off_t),
124            ('st_blksize', c_long),
125            ('st_blocks', c_longlong),
126            ('st_atimespec', c_timespec),
127            ('st_mtimespec', c_timespec),
128            ('st_ctimespec', c_timespec),
129            ('st_ino', c_ulonglong)]
130else:
131    raise NotImplementedError('%s is not supported.' % _system)
132
133
134class c_statvfs(Structure):
135    _fields_ = [
136        ('f_bsize', c_ulong),
137        ('f_frsize', c_ulong),
138        ('f_blocks', c_fsblkcnt_t),
139        ('f_bfree', c_fsblkcnt_t),
140        ('f_bavail', c_fsblkcnt_t),
141        ('f_files', c_fsfilcnt_t),
142        ('f_ffree', c_fsfilcnt_t),
143        ('f_favail', c_fsfilcnt_t)]
144
145if _system == 'FreeBSD':
146    c_fsblkcnt_t = c_uint64
147    c_fsfilcnt_t = c_uint64
148    setxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t, c_int)
149    getxattr_t = CFUNCTYPE(c_int, c_char_p, c_char_p, POINTER(c_byte), c_size_t)
150    class c_statvfs(Structure):
151        _fields_ = [
152            ('f_bavail', c_fsblkcnt_t),
153            ('f_bfree', c_fsblkcnt_t),
154            ('f_blocks', c_fsblkcnt_t),
155            ('f_favail', c_fsfilcnt_t),
156            ('f_ffree', c_fsfilcnt_t),
157            ('f_files', c_fsfilcnt_t),
158            ('f_bsize', c_ulong),
159            ('f_flag', c_ulong),
160            ('f_frsize', c_ulong)]
161
162class fuse_file_info(Structure):
163    _fields_ = [
164        ('flags', c_int),
165        ('fh_old', c_ulong),
166        ('writepage', c_int),
167        ('direct_io', c_uint, 1),
168        ('keep_cache', c_uint, 1),
169        ('flush', c_uint, 1),
170        ('padding', c_uint, 29),
171        ('fh', c_uint64),
172        ('lock_owner', c_uint64)]
173
174class fuse_context(Structure):
175    _fields_ = [
176        ('fuse', c_voidp),
177        ('uid', c_uid_t),
178        ('gid', c_gid_t),
179        ('pid', c_pid_t),
180        ('private_data', c_voidp)]
181
182class fuse_operations(Structure):
183    _fields_ = [
184        ('getattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat))),
185        ('readlink', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)),
186        ('getdir', c_voidp),    # Deprecated, use readdir
187        ('mknod', CFUNCTYPE(c_int, c_char_p, c_mode_t, c_dev_t)),
188        ('mkdir', CFUNCTYPE(c_int, c_char_p, c_mode_t)),
189        ('unlink', CFUNCTYPE(c_int, c_char_p)),
190        ('rmdir', CFUNCTYPE(c_int, c_char_p)),
191        ('symlink', CFUNCTYPE(c_int, c_char_p, c_char_p)),
192        ('rename', CFUNCTYPE(c_int, c_char_p, c_char_p)),
193        ('link', CFUNCTYPE(c_int, c_char_p, c_char_p)),
194        ('chmod', CFUNCTYPE(c_int, c_char_p, c_mode_t)),
195        ('chown', CFUNCTYPE(c_int, c_char_p, c_uid_t, c_gid_t)),
196        ('truncate', CFUNCTYPE(c_int, c_char_p, c_off_t)),
197        ('utime', c_voidp),     # Deprecated, use utimens
198        ('open', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
199        ('read', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t,
200            POINTER(fuse_file_info))),
201        ('write', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t, c_off_t,
202            POINTER(fuse_file_info))),
203        ('statfs', CFUNCTYPE(c_int, c_char_p, POINTER(c_statvfs))),
204        ('flush', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
205        ('release', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
206        ('fsync', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))),
207        ('setxattr', setxattr_t),
208        ('getxattr', getxattr_t),
209        ('listxattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_byte), c_size_t)),
210        ('removexattr', CFUNCTYPE(c_int, c_char_p, c_char_p)),
211        ('opendir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
212        ('readdir', CFUNCTYPE(c_int, c_char_p, c_voidp, CFUNCTYPE(c_int, c_voidp,
213            c_char_p, POINTER(c_stat), c_off_t), c_off_t, POINTER(fuse_file_info))),
214        ('releasedir', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info))),
215        ('fsyncdir', CFUNCTYPE(c_int, c_char_p, c_int, POINTER(fuse_file_info))),
216        ('init', CFUNCTYPE(c_voidp, c_voidp)),
217        ('destroy', CFUNCTYPE(c_voidp, c_voidp)),
218        ('access', CFUNCTYPE(c_int, c_char_p, c_int)),
219        ('create', CFUNCTYPE(c_int, c_char_p, c_mode_t, POINTER(fuse_file_info))),
220        ('ftruncate', CFUNCTYPE(c_int, c_char_p, c_off_t, POINTER(fuse_file_info))),
221        ('fgetattr', CFUNCTYPE(c_int, c_char_p, POINTER(c_stat),
222            POINTER(fuse_file_info))),
223        ('lock', CFUNCTYPE(c_int, c_char_p, POINTER(fuse_file_info), c_int, c_voidp)),
224        ('utimens', CFUNCTYPE(c_int, c_char_p, POINTER(c_utimbuf))),
225        ('bmap', CFUNCTYPE(c_int, c_char_p, c_size_t, POINTER(c_ulonglong)))]
226
227
228def time_of_timespec(ts):
229    return ts.tv_sec + ts.tv_nsec / 10 ** 9
230
231def set_st_attrs(st, attrs):
232    for key, val in attrs.items():
233        if key in ('st_atime', 'st_mtime', 'st_ctime'):
234            timespec = getattr(st, key + 'spec')
235            timespec.tv_sec = int(val)
236            timespec.tv_nsec = int((val - timespec.tv_sec) * 10 ** 9)
237        elif hasattr(st, key):
238            setattr(st, key, val)
239
240
241_libfuse_path = find_library('fuse')
242if not _libfuse_path:
243    raise EnvironmentError('Unable to find libfuse')
244_libfuse = CDLL(_libfuse_path)
245_libfuse.fuse_get_context.restype = POINTER(fuse_context)
246
247
248def fuse_get_context():
249    """Returns a (uid, gid, pid) tuple"""
250    ctxp = _libfuse.fuse_get_context()
251    ctx = ctxp.contents
252    return ctx.uid, ctx.gid, ctx.pid
253
254
255class FUSE(object):
256    """This class is the lower level interface and should not be subclassed
257       under normal use. Its methods are called by fuse.
258       Assumes API version 2.6 or later."""
259
260    def __init__(self, operations, mountpoint, raw_fi=False, **kwargs):
261        """Setting raw_fi to True will cause FUSE to pass the fuse_file_info
262           class as is to Operations, instead of just the fh field.
263           This gives you access to direct_io, keep_cache, etc."""
264
265        self.operations = operations
266        self.raw_fi = raw_fi
267        args = ['fuse']
268        if kwargs.pop('foreground', False):
269            args.append('-f')
270        if kwargs.pop('debug', False):
271            args.append('-d')
272        if kwargs.pop('nothreads', False):
273            args.append('-s')
274        kwargs.setdefault('fsname', operations.__class__.__name__)
275        args.append('-o')
276        args.append(','.join(key if val == True else '%s=%s' % (key, val)
277            for key, val in kwargs.items()))
278        args.append(mountpoint)
279        argv = (c_char_p * len(args))(*args)
280
281        fuse_ops = fuse_operations()
282        for name, prototype in fuse_operations._fields_:
283            if prototype != c_voidp and getattr(operations, name, None):
284                op = partial(self._wrapper_, getattr(self, name))
285                setattr(fuse_ops, name, prototype(op))
286        _libfuse.fuse_main_real(len(args), argv, pointer(fuse_ops),
287            sizeof(fuse_ops), None)
288        del self.operations     # Invoke the destructor
289
290    def _wrapper_(self, func, *args, **kwargs):
291        """Decorator for the methods that follow"""
292        try:
293            return func(*args, **kwargs) or 0
294        except OSError as e:
295            return -(e.errno or EFAULT)
296        except:
297            print_exc()
298            return -EFAULT
299
300    def getattr(self, path, buf):
301        return self.fgetattr(path, buf, None)
302
303    def readlink(self, path, buf, bufsize):
304        ret = self.operations('readlink', path).encode('utf-8')
305        data = create_string_buffer(ret[:bufsize - 1])
306        memmove(buf, data, len(data))
307        return 0
308
309    def mknod(self, path, mode, dev):
310        return self.operations('mknod', path, mode, dev)
311
312    def mkdir(self, path, mode):
313        return self.operations('mkdir', path, mode)
314
315    def unlink(self, path):
316        return self.operations('unlink', path)
317
318    def rmdir(self, path):
319        return self.operations('rmdir', path)
320
321    def symlink(self, source, target):
322        return self.operations('symlink', target, source)
323
324    def rename(self, old, new):
325        return self.operations('rename', old, new)
326
327    def link(self, source, target):
328        return self.operations('link', target, source)
329
330    def chmod(self, path, mode):
331        return self.operations('chmod', path, mode)
332
333    def chown(self, path, uid, gid):
334        return self.operations('chown', path, uid, gid)
335
336    def truncate(self, path, length):
337        return self.operations('truncate', path, length)
338
339    def open(self, path, fip):
340        fi = fip.contents
341        if self.raw_fi:
342            return self.operations('open', path, fi)
343        else:
344            fi.fh = self.operations('open', path, fi.flags)
345            return 0
346
347    def read(self, path, buf, size, offset, fip):
348        fh = fip.contents if self.raw_fi else fip.contents.fh
349        ret = self.operations('read', path, size, offset, fh)
350        if not ret:
351            return 0
352        data = create_string_buffer(ret[:size], size)
353        memmove(buf, data, size)
354        return size
355
356    def write(self, path, buf, size, offset, fip):
357        data = string_at(buf, size)
358        fh = fip.contents if self.raw_fi else fip.contents.fh
359        return self.operations('write', path, data, offset, fh)
360
361    def statfs(self, path, buf):
362        stv = buf.contents
363        attrs = self.operations('statfs', path)
364        for key, val in attrs.items():
365            if hasattr(stv, key):
366                setattr(stv, key, val)
367        return 0
368
369    def flush(self, path, fip):
370        fh = fip.contents if self.raw_fi else fip.contents.fh
371        return self.operations('flush', path, fh)
372
373    def release(self, path, fip):
374        fh = fip.contents if self.raw_fi else fip.contents.fh
375        return self.operations('release', path, fh)
376
377    def fsync(self, path, datasync, fip):
378        fh = fip.contents if self.raw_fi else fip.contents.fh
379        return self.operations('fsync', path, datasync, fh)
380
381    def setxattr(self, path, name, value, size, options, *args):
382        data = string_at(value, size)
383        return self.operations('setxattr', path, name, data, options, *args)
384
385    def getxattr(self, path, name, value, size, *args):
386        ret = self.operations('getxattr', path, name, *args)
387        retsize = len(ret)
388        buf = create_string_buffer(ret, retsize)    # Does not add trailing 0
389        if bool(value):
390            if retsize > size:
391                return -ERANGE
392            memmove(value, buf, retsize)
393        return retsize
394
395    def listxattr(self, path, namebuf, size):
396        ret = self.operations('listxattr', path)
397        buf = create_string_buffer('\x00'.join(ret)) if ret else ''
398        bufsize = len(buf)
399        if bool(namebuf):
400            if bufsize > size:
401                return -ERANGE
402            memmove(namebuf, buf, bufsize)
403        return bufsize
404
405    def removexattr(self, path, name):
406        return self.operations('removexattr', path, name)
407
408    def opendir(self, path, fip):
409        # Ignore raw_fi
410        fip.contents.fh = self.operations('opendir', path)
411        return 0
412
413    def readdir(self, path, buf, filler, offset, fip):
414        # Ignore raw_fi
415        for item in self.operations('readdir', path, fip.contents.fh):
416            if isinstance(item, str):
417                name, st, offset = item, None, 0
418                name = name.encode('utf-8')
419            else:
420                name, attrs, offset = item
421                if attrs:
422                    st = c_stat()
423                    set_st_attrs(st, attrs)
424                else:
425                    st = None
426            if filler(buf, name, st, offset) != 0:
427                break
428        return 0
429
430    def releasedir(self, path, fip):
431        # Ignore raw_fi
432        return self.operations('releasedir', path, fip.contents.fh)
433
434    def fsyncdir(self, path, datasync, fip):
435        # Ignore raw_fi
436        return self.operations('fsyncdir', path, datasync, fip.contents.fh)
437
438    def init(self, conn):
439        return self.operations('init', '/')
440
441    def destroy(self, private_data):
442        return self.operations('destroy', '/')
443
444    def access(self, path, amode):
445        return self.operations('access', path, amode)
446
447    def create(self, path, mode, fip):
448        fi = fip.contents
449        if self.raw_fi:
450            return self.operations('create', path, mode, fi)
451        else:
452            fi.fh = self.operations('create', path, mode)
453            return 0
454
455    def ftruncate(self, path, length, fip):
456        fh = fip.contents if self.raw_fi else fip.contents.fh
457        return self.operations('truncate', path, length, fh)
458
459    def fgetattr(self, path, buf, fip):
460        memset(buf, 0, sizeof(c_stat))
461        st = buf.contents
462        fh = fip and (fip.contents if self.raw_fi else fip.contents.fh)
463        attrs = self.operations('getattr', path, fh)
464        set_st_attrs(st, attrs)
465        return 0
466
467    def lock(self, path, fip, cmd, lock):
468        fh = fip.contents if self.raw_fi else fip.contents.fh
469        return self.operations('lock', path, fh, cmd, lock)
470
471    def utimens(self, path, buf):
472        if buf:
473            atime = time_of_timespec(buf.contents.actime)
474            mtime = time_of_timespec(buf.contents.modtime)
475            times = (atime, mtime)
476        else:
477            times = None
478        return self.operations('utimens', path, times)
479
480    def bmap(self, path, blocksize, idx):
481        return self.operations('bmap', path, blocksize, idx)
482
483
484class Operations(object):
485    """This class should be subclassed and passed as an argument to FUSE on
486       initialization. All operations should raise an OSError exception on
487       error.
488
489       When in doubt of what an operation should do, check the FUSE header
490       file or the corresponding system call man page."""
491
492    def __call__(self, op, *args):
493        if not hasattr(self, op):
494            raise OSError(EFAULT, '')
495        return getattr(self, op)(*args)
496
497    def access(self, path, amode):
498        return 0
499
500    bmap = None
501
502    def chmod(self, path, mode):
503        raise OSError(EROFS, '')
504
505    def chown(self, path, uid, gid):
506        raise OSError(EROFS, '')
507
508    def create(self, path, mode, fi=None):
509        """When raw_fi is False (default case), fi is None and create should
510           return a numerical file handle.
511           When raw_fi is True the file handle should be set directly by create
512           and return 0."""
513        raise OSError(EROFS, '')
514
515    def destroy(self, path):
516        """Called on filesystem destruction. Path is always /"""
517        pass
518
519    def flush(self, path, fh):
520        return 0
521
522    def fsync(self, path, datasync, fh):
523        return 0
524
525    def fsyncdir(self, path, datasync, fh):
526        return 0
527
528    def getattr(self, path, fh=None):
529        """Returns a dictionary with keys identical to the stat C structure
530           of stat(2).
531           st_atime, st_mtime and st_ctime should be floats.
532           NOTE: There is an incombatibility between Linux and Mac OS X concerning
533           st_nlink of directories. Mac OS X counts all files inside the directory,
534           while Linux counts only the subdirectories."""
535
536        if path != '/':
537            raise OSError(ENOENT, '')
538        return dict(st_mode=(S_IFDIR | 0o755), st_nlink=2)
539
540    def getxattr(self, path, name, position=0):
541        raise OSError(ENOTSUP, '')
542
543    def init(self, path):
544        """Called on filesystem initialization. Path is always /
545           Use it instead of __init__ if you start threads on initialization."""
546        pass
547
548    def link(self, target, source):
549        raise OSError(EROFS, '')
550
551    def listxattr(self, path):
552        return []
553
554    lock = None
555
556    def mkdir(self, path, mode):
557        raise OSError(EROFS, '')
558
559    def mknod(self, path, mode, dev):
560        raise OSError(EROFS, '')
561
562    def open(self, path, flags):
563        """When raw_fi is False (default case), open should return a numerical
564           file handle.
565           When raw_fi is True the signature of open becomes:
566               open(self, path, fi)
567           and the file handle should be set directly."""
568        return 0
569
570    def opendir(self, path):
571        """Returns a numerical file handle."""
572        return 0
573
574    def read(self, path, size, offset, fh):
575        """Returns a string containing the data requested."""
576        raise OSError(ENOENT, '')
577
578    def readdir(self, path, fh):
579        """Can return either a list of names, or a list of (name, attrs, offset)
580           tuples. attrs is a dict as in getattr."""
581        return ['.', '..']
582
583    def readlink(self, path):
584        raise OSError(ENOENT, '')
585
586    def release(self, path, fh):
587        return 0
588
589    def releasedir(self, path, fh):
590        return 0
591
592    def removexattr(self, path, name):
593        raise OSError(ENOTSUP, '')
594
595    def rename(self, old, new):
596        raise OSError(EROFS, '')
597
598    def rmdir(self, path):
599        raise OSError(EROFS, '')
600
601    def setxattr(self, path, name, value, options, position=0):
602        raise OSError(ENOTSUP, '')
603
604    def statfs(self, path):
605        """Returns a dictionary with keys identical to the statvfs C structure
606           of statvfs(3).
607           On Mac OS X f_bsize and f_frsize must be a power of 2 (minimum 512)."""
608        return {}
609
610    def symlink(self, target, source):
611        raise OSError(EROFS, '')
612
613    def truncate(self, path, length, fh=None):
614        raise OSError(EROFS, '')
615
616    def unlink(self, path):
617        raise OSError(EROFS, '')
618
619    def utimens(self, path, times=None):
620        """Times is a (atime, mtime) tuple. If None use current time."""
621        return 0
622
623    def write(self, path, data, offset, fh):
624        raise OSError(EROFS, '')
625
626
627class LoggingMixIn:
628    def __call__(self, op, path, *args):
629        logging.debug('-> %s %s %s', op, path, repr(args))
630        ret = '[Unknown Error]'
631        try:
632            ret = getattr(self, op)(path, *args)
633            return ret
634        except OSError as e:
635            ret = str(e)
636            raise
637        finally:
638            logging.debug('<- %s %s', op, repr(ret))
639