From 9121e8822c31634675c2ee793175b777ede88a3a Mon Sep 17 00:00:00 2001 From: Andrew Osheroff Date: Wed, 31 Jul 2019 14:44:22 +0200 Subject: [PATCH] added unmount + better example --- example.js | 84 ++++++++++++++ fuse-native.c | 23 +++- index.js | 300 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 352 insertions(+), 55 deletions(-) create mode 100644 example.js diff --git a/example.js b/example.js new file mode 100644 index 0000000..d2311a2 --- /dev/null +++ b/example.js @@ -0,0 +1,84 @@ +const Fuse = require('./') + +const ops = { + readdir: function (path, cb) { + console.log('readdir(%s)', path) + if (path === '/') return process.nextTick(cb, 0, ['test']) + return process.nextTick(cb, 0) + }, + /* + access: function (path, cb) { + return process.nextTick(cb, 0) + }, + */ + getattr: function (path, cb) { + console.log('getattr(%s)', path) + if (path === '/') { + return process.nextTick(cb, 0, { + mtime: new Date(), + atime: new Date(), + ctime: new Date(), + nlink: 1, + size: 100, + mode: 16877, + uid: process.getuid ? process.getuid() : 0, + gid: process.getgid ? process.getgid() : 0 + }) + } + + if (path === '/test') { + return process.nextTick(cb, 0, { + mtime: new Date(), + atime: new Date(), + ctime: new Date(), + nlink: 1, + size: 12, + mode: 33188, + uid: process.getuid ? process.getuid() : 0, + gid: process.getgid ? process.getgid() : 0 + }) + } + + return process.nextTick(cb, Fuse.ENOENT) + }, + open: function (path, flags, cb) { + console.log('open(%s, %d)', path, flags) + return process.nextTick(cb, 0, 42) // 42 is an fd + }, + read: function (path, fd, buf, len, pos, cb) { + console.log('read(%s, %d, %d, %d)', path, fd, len, pos) + var str = 'hello world\n'.slice(pos) + if (!str) return process.nextTick(cb, 0) + buf.write(str) + return process.nextTick(cb, str.length) + } +} + +const fuse = new Fuse('./mnt', ops, { debug: true }) +fuse.mount(err => { + if (err) throw err + console.log('filesystem mounted on ' + fuse.mnt) +}) + +process.on('SIGINT', function () { + fuse.unmount(err => { + if (err) { + console.log('filesystem at ' + fuse.mnt + ' not unmounted', err) + } else { + console.log('filesystem at ' + fuse.mnt + ' unmounted') + } + }) +}) + +function emptyStat (mode) { + return { + mtime: new Date(), + atime: new Date(), + ctime: new Date(), + nlink: 1, + size: 100, + mode: mode, + uid: process.getuid ? process.getuid() : 0, + gid: process.getgid ? process.getgid() : 0 + } +} diff --git a/fuse-native.c b/fuse-native.c index 84996cd..9c47902 100644 --- a/fuse-native.c +++ b/fuse-native.c @@ -90,6 +90,8 @@ typedef struct { napi_ref on_symlink; struct fuse *fuse; + struct fuse_chan *ch; + bool mounted; uv_async_t async; } fuse_thread_t; @@ -637,7 +639,7 @@ NAPI_METHOD(fuse_native_signal_readdir) { } NAPI_METHOD(fuse_native_mount) { - NAPI_ARGV(10) + NAPI_ARGV(11) NAPI_ARGV_UTF8(mnt, 1024, 0); NAPI_ARGV_UTF8(mntopts, 1024, 1); @@ -711,6 +713,8 @@ NAPI_METHOD(fuse_native_mount) { struct fuse *fuse = fuse_new(ch, &args, &ops, sizeof(struct fuse_operations), ft); ft->fuse = fuse; + ft->ch = ch; + ft->mounted = true; if (fuse == NULL) { napi_throw_error(env, "fuse failed", "fuse failed"); @@ -723,10 +727,27 @@ NAPI_METHOD(fuse_native_mount) { return NULL; } +NAPI_METHOD(fuse_native_unmount) { + NAPI_ARGV(2) + NAPI_ARGV_UTF8(mnt, 1024, 0); + NAPI_ARGV_BUFFER_CAST(fuse_thread_t *, ft, 1); + + if (ft != NULL && ft->mounted) { + fuse_unmount(mnt, ft->ch); + printf("joining\n"); + pthread_join(ft->thread, NULL); + printf("joined\n"); + } + ft->mounted = false; + + return NULL; +} + NAPI_INIT() { pthread_key_create(&(thread_locals_key), NULL); // TODO: add destructor NAPI_EXPORT_FUNCTION(fuse_native_mount) + NAPI_EXPORT_FUNCTION(fuse_native_unmount) NAPI_EXPORT_FUNCTION(fuse_native_signal_path) NAPI_EXPORT_FUNCTION(fuse_native_signal_stat) NAPI_EXPORT_FUNCTION(fuse_native_signal_statfs) diff --git a/index.js b/index.js index 42b2b47..2896f8d 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,13 @@ -const binding = require('node-gyp-build')(__dirname) +const os = require('os') +const fs = require('fs') +const path = require('path') + +const IS_OSX = os.platform() === 'darwin' +const OSX_FOLDER_ICON = '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericFolderIcon.icns' + +const HAS_FOLDER_ICON = IS_OSX && fs.existsSync(OSX_FOLDER_ICON) +const binding = require('node-gyp-build')(__dirname) const Opcodes = new Map([ [ 'init', 0 ], [ 'error', 1 ], @@ -41,12 +49,14 @@ const Opcodes = new Map([ class Fuse { constructor (mnt, ops, opts = {}) { this.opts = opts - this.ops = ops - this.mnt = mnt + this.mnt = path.resolve(mnt) + this.ops = ops this._thread = Buffer.alloc(binding.sizeof_fuse_thread_t) + // Keep the process alive while fuse is mounted. + this._timer = null - const implemented = [] + const implemented = ['init', 'error', 'getattr'] if (ops) { for (const [name, code] of Opcodes) { if (ops[name]) implemented.push(code) @@ -58,6 +68,74 @@ class Fuse { this._sync = true } + _withDefaultOps (cb) { + const withDefaults = { ...this.ops } + + const callback = function (err) { + callback = noop + setImmediate(cb.bind(null, err)) + } + + var init = this.ops.init || call + withDefaults.init = function (next) { + console.log('IN INIT') + callback() + if (init.length > 1) init(this.mnt, next) // backwards compat for now + else init(next) + } + + var error = this.ops.error || call + withDefaults.error = function (next) { + console.log('IN ERROR') + callback(new Error('Mount failed')) + error(next) + } + + if (!this.ops.getattr) { // we need this for unmount to work on osx + withDefaults.getattr = function (path, cb) { + if (path !== '/') return cb(Fuse.EPERM) + cb(null, { mtime: new Date(0), atime: new Date(0), ctime: new Date(0), mode: 16877, size: 4096 }) + } + } + + return withDefaults + } + + _fuseOptions () { + const options = [] + + if ((/\*|(^,)fuse-bindings(,$)/.test(process.env.DEBUG)) || this.opts.debug) options.push('debug') + if (this.opts.allowOther) options.push('allow_other') + if (this.opts.allowRoot) options.push('allow_root') + if (this.opts.autoUnmount) options.push('auto_unmount') + if (this.opts.defaultPermissions) options.push('default_permissions') + if (this.opts.blkdev) options.push('blkdev') + if (this.opts.blksize) options.push('blksize=' + this.opts.blksize) + if (this.opts.maxRead) options.push('max_read=' + this.opts.maxRead) + if (this.opts.fd) options.push('fd=' + this.opts.fd) + if (this.opts.userId) options.push('user_id=', this.opts.userId) + if (this.opts.fsname) options.push('fsname=' + this.opts.fsname) + if (this.opts.subtype) options.push('subtype=' + this.opts.subtype) + if (this.opts.kernelCache) options.push('kernel_cache') + if (this.opts.autoCache) options.push('auto_cache') + if (this.opts.umask) options.push('umask=' + this.opts.umask) + if (this.opts.uid) options.push('uid=' + this.opts.uid) + if (this.opts.gid) options.push('gid=' + this.opts.gid) + if (this.opts.entryTimeout) options.push('entry_timeout=' + this.opts.entryTimeout) + if (this.opts.attrTimeout) options.push('attr_timeout=' + this.opts.attrTimeout) + if (this.opts.acAttrTimeout) options.push('ac_attr_timeout=' + this.opts.acAttrTimeout) + if (this.opts.noforget) options.push('noforget') + if (this.opts.remember) options.push('remember=' + this.opts.remember) + if (this.opts.modules) options.push('modules=' + this.opts.modules) + + if (this.opts.displayFolder && IS_OSX) { // only works on osx + options.push('volname=' + path.basename(this.mnt)) + if (HAS_FOLDER_ICON) options.push('volicon=' + OSX_FOLDER_ICON) + } + + return options.map(o => '-o' + o).join(' ') + } + _signal (signalFunc, args) { /* if (this._sync) process.nextTick(() => signalFunc.apply(null, args)) @@ -66,14 +144,43 @@ class Fuse { process.nextTick(() => signalFunc.apply(null, args)) } - mount () { - binding.fuse_native_mount(this.mnt, '-odebug', this._thread, this, - this.on_path_op, this.on_stat_op, this.on_fd_op, this.on_xattr_op, - this.on_statfs, this.on_readdir, this.on_symlink) + mount (cb) { + this.ops = this._withDefaultOps(this.ops, cb) + const opts = this._fuseOptions() + + fs.stat(this.mnt, (err, stat) => { + if (err) return cb(new Error('Mountpoint does not exist')) + if (!stat.isDirectory()) return cb(new Error('Mountpoint is not a directory')) + fs.stat(path.join(this.mnt, '..'), (_, parent) => { + if (parent && parent.dev !== stat.dev) return cb(new Error('Mountpoint in use')) + try { + // TODO: asyncify + binding.fuse_native_mount(this.mnt, opts, this._thread, this, + this.on_path_op, this.on_stat_op, this.on_fd_op, this.on_xattr_op, + this.on_statfs, this.on_readdir, this.on_symlink) + } catch (err) { + return cb(err) + } + this._timer = setInterval(() => {}, 10000) + return cb(null) + }) + }) + } + + unmount (cb) { + // TODO: asyncify + try { + binding.fuse_native_unmount(this.mnt, this._thread) + } catch (err) { + clearInterval(this._timer) + return process.nextTick(cb, err) + } + clearInterval(this._timer) + return process.nextTick(cb, null) } - unmount () { - binding.fuse_native_unmount(this.mnt) + errno (code) { + return (code && Fuse[code.toUpperCase()]) || -1 } on_symlink (handle, op, path, target) { @@ -201,6 +308,133 @@ class Fuse { } } +Fuse.EPERM = -1 +Fuse.ENOENT = -2 +Fuse.ESRCH = -3 +Fuse.EINTR = -4 +Fuse.EIO = -5 +Fuse.ENXIO = -6 +Fuse.E2BIG = -7 +Fuse.ENOEXEC = -8 +Fuse.EBADF = -9 +Fuse.ECHILD = -10 +Fuse.EAGAIN = -11 +Fuse.ENOMEM = -12 +Fuse.EACCES = -13 +Fuse.EFAULT = -14 +Fuse.ENOTBLK = -15 +Fuse.EBUSY = -16 +Fuse.EEXIST = -17 +Fuse.EXDEV = -18 +Fuse.ENODEV = -19 +Fuse.ENOTDIR = -20 +Fuse.EISDIR = -21 +Fuse.EINVAL = -22 +Fuse.ENFILE = -23 +Fuse.EMFILE = -24 +Fuse.ENOTTY = -25 +Fuse.ETXTBSY = -26 +Fuse.EFBIG = -27 +Fuse.ENOSPC = -28 +Fuse.ESPIPE = -29 +Fuse.EROFS = -30 +Fuse.EMLINK = -31 +Fuse.EPIPE = -32 +Fuse.EDOM = -33 +Fuse.ERANGE = -34 +Fuse.EDEADLK = -35 +Fuse.ENAMETOOLONG = -36 +Fuse.ENOLCK = -37 +Fuse.ENOSYS = -38 +Fuse.ENOTEMPTY = -39 +Fuse.ELOOP = -40 +Fuse.EWOULDBLOCK = -11 +Fuse.ENOMSG = -42 +Fuse.EIDRM = -43 +Fuse.ECHRNG = -44 +Fuse.EL2NSYNC = -45 +Fuse.EL3HLT = -46 +Fuse.EL3RST = -47 +Fuse.ELNRNG = -48 +Fuse.EUNATCH = -49 +Fuse.ENOCSI = -50 +Fuse.EL2HLT = -51 +Fuse.EBADE = -52 +Fuse.EBADR = -53 +Fuse.EXFULL = -54 +Fuse.ENOANO = -55 +Fuse.EBADRQC = -56 +Fuse.EBADSLT = -57 +Fuse.EDEADLOCK = -35 +Fuse.EBFONT = -59 +Fuse.ENOSTR = -60 +Fuse.ENODATA = -61 +Fuse.ETIME = -62 +Fuse.ENOSR = -63 +Fuse.ENONET = -64 +Fuse.ENOPKG = -65 +Fuse.EREMOTE = -66 +Fuse.ENOLINK = -67 +Fuse.EADV = -68 +Fuse.ESRMNT = -69 +Fuse.ECOMM = -70 +Fuse.EPROTO = -71 +Fuse.EMULTIHOP = -72 +Fuse.EDOTDOT = -73 +Fuse.EBADMSG = -74 +Fuse.EOVERFLOW = -75 +Fuse.ENOTUNIQ = -76 +Fuse.EBADFD = -77 +Fuse.EREMCHG = -78 +Fuse.ELIBACC = -79 +Fuse.ELIBBAD = -80 +Fuse.ELIBSCN = -81 +Fuse.ELIBMAX = -82 +Fuse.ELIBEXEC = -83 +Fuse.EILSEQ = -84 +Fuse.ERESTART = -85 +Fuse.ESTRPIPE = -86 +Fuse.EUSERS = -87 +Fuse.ENOTSOCK = -88 +Fuse.EDESTADDRREQ = -89 +Fuse.EMSGSIZE = -90 +Fuse.EPROTOTYPE = -91 +Fuse.ENOPROTOOPT = -92 +Fuse.EPROTONOSUPPORT = -93 +Fuse.ESOCKTNOSUPPORT = -94 +Fuse.EOPNOTSUPP = -95 +Fuse.EPFNOSUPPORT = -96 +Fuse.EAFNOSUPPORT = -97 +Fuse.EADDRINUSE = -98 +Fuse.EADDRNOTAVAIL = -99 +Fuse.ENETDOWN = -100 +Fuse.ENETUNREACH = -101 +Fuse.ENETRESET = -102 +Fuse.ECONNABORTED = -103 +Fuse.ECONNRESET = -104 +Fuse.ENOBUFS = -105 +Fuse.EISCONN = -106 +Fuse.ENOTCONN = -107 +Fuse.ESHUTDOWN = -108 +Fuse.ETOOMANYREFS = -109 +Fuse.ETIMEDOUT = -110 +Fuse.ECONNREFUSED = -111 +Fuse.EHOSTDOWN = -112 +Fuse.EHOSTUNREACH = -113 +Fuse.EALREADY = -114 +Fuse.EINPROGRESS = -115 +Fuse.ESTALE = -116 +Fuse.EUCLEAN = -117 +Fuse.ENOTNAM = -118 +Fuse.ENAVAIL = -119 +Fuse.EISNAM = -120 +Fuse.EREMOTEIO = -121 +Fuse.EDQUOT = -122 +Fuse.ENOMEDIUM = -123 +Fuse.EMEDIUMTYPE = -124 + +module.exports = Fuse + function getStatfsArray (statfs) { const ints = new Uint32Array(11) @@ -251,47 +485,5 @@ function getStatArray (stat) { return ints } -function emptyStat (mode) { - return { - mtime: new Date(), - atime: new Date(), - ctime: new Date(), - nlink: 1, - size: 100, - mode: mode, - uid: process.getuid ? process.getuid() : 0, - gid: process.getgid ? process.getgid() : 0 - } -} - -const f = new Fuse('mnt', { - getattr: (path, cb) => { - if (path === '/') return cb(0, emptyStat(16877)) - return cb(0, emptyStat(33188)) - }, - access: (path, mode, cb) => { - return cb(0, 0) - }, - setxattr: (path, name, buffer, length, offset, cb) => { - return cb(0) - }, - utimens: (path, atim, mtim, cb) => { - return cb(0) - }, - readdir: (path, cb) => { - if (path === '/') { - return cb(0, ['a', 'b', 'c'], Array(3).fill('a').map(() => emptyStat(16877))) - } - return cb(0, [], []) - } -}) -f.mount() - -setInterval(() => { - if (global.gc) gc() -}, 1000) - -// setTimeout(function () { -// foo = null -// console.log('now!') -// }, 5000) +function noop () {} +function call (cb) { cb() }