diff --git a/README.md b/README.md index 1a90a2f..9425907 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# tabfs +# TabFS ## Setup -You need to both install the Chrome extension and run the native -filesystem. +You need to compile the FUSE filesystem (written in C), then install +the browser extension which runs it and talks to it. ### Run the C filesystem @@ -19,7 +19,21 @@ $ mkdir mnt $ make ``` -### Install the Chrome extension +Now install the native messaging host into your browser, so the +extension can launch and talk to the filesystem: + +``` +$ ./install.sh [chrome | chromium | firefox] +``` + +### Install the browser extension + +I think it will work on Edge or Opera or whatever, too. You'll need to +change the native messaging path in install.sh + +#### Firefox + +#### Chrome Go to the [Chrome extensions page](chrome://extensions). @@ -56,3 +70,25 @@ When you, say, `cat` a file in the tab filesystem: marshalling) TODO: make diagrams? + +## hmm + +it's way too hard to make an extension. even 'make an extension' is +a bad framing + +open input space -- filesystem + +now you have this whole 'language', this whole toolset, to control and +automate your browser + +OSQuery + +fake filesystems talk + +Screenotate + +processes as files. the real process is the browser. + +browser and Unix + +rmdir a non-empty directory diff --git a/extension/background.js b/extension/background.js index 75520d9..eb120a7 100644 --- a/extension/background.js +++ b/extension/background.js @@ -1,3 +1,5 @@ +const TESTING = (typeof chrome === 'undefined'); + const unix = { EPERM: 1, ENOENT: 2, @@ -24,26 +26,6 @@ function UnixError(error) { } UnixError.prototype = Error.prototype; -async function debugTab(tabId) { - if (!debugged[tabId]) { - await new Promise(resolve => chrome.debugger.attach({tabId}, "1.3", resolve)); - debugged[tabId] = 0; - } - debugged[tabId] += 1; -} -function sendDebuggerCommand(tabId, method, commandParams) { - return new Promise((resolve, reject) => - chrome.debugger.sendCommand({tabId}, method, commandParams, result => { - console.log(method, result); - if (result) { - resolve(result); - } else { - reject(chrome.runtime.lastError); - } - }) - ); -} - // tabs/by-id/ID/title // tabs/by-id/ID/url // tabs/by-id/ID/console @@ -63,10 +45,72 @@ function sanitize(s) { return s.replace(/[^A-Za-z0-9_\-\.]/gm, '_'); } -const debugged = {}; +/* if I could specify a custom editor interface for all the routing + below ... I would highlight the route names in blocks of some color + that sticks out, and let you collapse them. then you could get a + view of what the whole filesystem looks like at a glance. */ +const router = {}; + +async function withTab(handler) { + return { + async read(path, fh, size, offset) { + const tab = await browser.tabs.get(parseInt(pathComponent(path, -2))); + return handler(tab); + } + }; +} +async function fromScript(code) { + return { + async read(path, fh, size, offset) { + const tabId = parseInt(pathComponent(path, -2)); + return browser.tabs.executeScript(tabId, {code}); + } + }; +} + +router["/tabs/by-id"] = { + async entries() { + const tabs = await browser.tabs.query({}); + return tabs.map(tab => String(tab.id)); + } +} +router["/tabs/by-id/*/url"] = withTab(tab => tab.url + "\n"); +router["/tabs/by-id/*/title"] = withTab(tab => tab.title + "\n"); +router["/tabs/by-id/*/text"] = fromScript(`document.body.innerText`); +router["/tabs/by-id/*/control"] = { + async write(path, buf) { + const tabId = parseInt(pathComponent(path, -2)); + if (buf.trim() === 'close') { + await new Promise(resolve => chrome.tabs.remove(tabId, resolve)); + } else { + throw new UnixError(unix.EIO); + } + } +}; + +router["/tabs/by-title"] = { + async entries() { + const tabs = await browser.tabs.query({}); + return tabs.map(tab => sanitize(String(tab.title).slice(0, 200)) + "_" + String(tab.id)); + } +}; +router["/tabs/by-title/*"] = { + async getattr(path) { + const st_size = (await this.readlink(path)).length + 1; + return { + st_mode: unix.S_IFLNK | 0444, + st_nlink: 1, + // You _must_ return correct linkee path length from getattr! + st_size + }; + }, + async readlink(path) { + const parts = path.split("_"); + const id = parts[parts.length - 1]; + return "../by-id/" + id; + } +}; -const router = { - "tabs": { /* "last-focused": { * // FIXME: symlink to tab by id. * async readlink() { @@ -74,283 +118,134 @@ const router = { * } * }, */ - "by-title": { - async readdir() { - const tabs = await browser.tabs.query({}); - return tabs.map(tab => sanitize(String(tab.title).slice(0, 200)) + "_" + String(tab.id)); - }, - "*": { - async getattr(path) { - const st_size = (await this.readlink(path)).length + 1; - return { - st_mode: unix.S_IFLNK | 0444, - st_nlink: 1, - // You _must_ return correct linkee path length from getattr! - st_size - }; - }, - async readlink(path) { - const parts = path.split("_"); - const id = parts[parts.length - 1]; - return "../by-id/" + id; - } - } - }, - "by-id": { - async readdir() { - const tabs = await browser.tabs.query({}); - return tabs.map(tab => String(tab.id)); - }, - "*": { - "url": { - async read(path, fh, size, offset) { - const tab = await browser.tabs.get(parseInt(pathComponent(path, -2))); - return (tab.url + "\n").substr(offset, size); - } - }, - "title": { - async read(path, fh, size, offset) { - const tab = await browser.tabs.get(parseInt(pathComponent(path, -2))); - return (tab.title + "\n").substr(offset, size); - } - }, - "text": { - async read(path, fh, size, offset) { - const tabId = parseInt(pathComponent(path, -2)); - const [result] = await browser.tabs.executeScript(tabId, {code: "document.body.innerText"}); - return result.substr(offset, size) - } - }, - "snapshot.mhtml": { - async read(path, fh, size, offset) { - const tabId = parseInt(pathComponent(path, -2)); - await debugTab(tabId); - await sendDebuggerCommand(tabId, "Page.enable", {}); +// ensure that there are entries for all parents +for (let key in router) { + let path = key; + while (path !== "/") { // walk upward through the path + path = path.substr(0, path.lastIndexOf("/")); - const {data} = await sendDebuggerCommand(tabId, "Page.captureSnapshot"); - return data.substr(offset, size) - } - }, - "screenshot.png": { - // Broken. Filesystem hangs (? in JS?) and needs to be killed if you read this. - async read(path, fh, size, offset) { - const tabId = parseInt(pathComponent(path, -2)); - await debugTab(tabId); - await sendDebuggerCommand(tabId, "Page.enable", {}); + if (!router[path]) { + // find all direct children + const children = Object.keys(router) + .filter(k => k.startsWith(path) && + (k.match(/\//g) || []).length === + (path.match(/\//g) || []).length + 1) + .map(k => k.substr(path.length + 1)) - const {data} = await sendDebuggerCommand(tabId, "Page.captureScreenshot"); - const buf = btoa(atob(data).substr(offset, size)); - return { buf, base64Encoded: true }; - } - }, - - "resources": { - async opendir(path) { - const tabId = parseInt(pathComponent(path, -2)); - await debugTab(tabId); - return 0; - }, - async readdir(path) { - const tabId = parseInt(pathComponent(path, -2)); - if (!debugged[tabId]) throw new UnixError(unix.EIO); - - const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {}); - return frameTree.resources.map(r => sanitize(String(r.url).slice(0, 200))); - }, - async releasedir(path) { - return 0; - }, - - "*": { - async read(path, fh, size, offset) { - const tabId = parseInt(pathComponent(path, -3)); - const suffix = pathComponent(path, -1); - - if (!debugged[tabId]) throw new UnixError(unix.EIO); - - await sendDebuggerCommand(tabId, "Page.enable", {}); - - const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {}); - for (let resource of frameTree.resources) { - const resourceSuffix = sanitize(String(resource.url).slice(0, 200)); - if (resourceSuffix === suffix) { - let {base64Encoded, content} = await sendDebuggerCommand(tabId, "Page.getResourceContent", { - frameId: frameTree.frame.id, - url: resource.url - }); - if (base64Encoded) { - const buf = btoa(atob(content).substr(offset, size)); - return { buf, base64Encoded: true }; - } - return content.substr(offset, size); - } - } - throw new UnixError(unix.ENOENT); - } - } - }, - - "control": { - async write(path, buf) { - const tabId = parseInt(pathComponent(path, -2)); - if (buf.trim() === 'close') { - await new Promise(resolve => chrome.tabs.remove(tabId, resolve)); - } else { - throw new UnixError(unix.EIO); - } - } - } - } + if (path == '') path = '/'; + router[path] = {entries() { + return children; + }} } } -}; +} +if (TESTING) { + const assert = require('assert'); + (async () => { + assert.deepEqual(await router['/tabs/by-id/*'].entries(), ['url', 'title', 'text', 'control']); + assert.deepEqual(await router['/'].entries(), ['tabs']); + })() +} +console.log(router); function findRoute(path) { - let route = router; let pathSegments = path.split("/"); + if (pathSegments[pathSegments.length - 1].startsWith("._")) { throw new UnixError(unix.ENOTSUP); // Apple Double file for xattrs } + + let routingPath = ""; for (let segment of pathSegments) { - if (segment === "") continue; - route = route[segment] || route["*"]; + if (router[routingPath + "/" + segment]) { + routingPath += "/" + segment; + } else { + routingPath += "/*"; + } - if (!route) throw new UnixError(unix.ENOENT); + if (!router[routingPath]) throw new UnixError(unix.ENOENT); } - return route; + return router[routingPath]; } -async function getattr(path) { - let route = findRoute(path); - if (route.getattr) { - return route.getattr(path); - } else if (route.read || route.write) { - // default file attrs - return { - st_mode: unix.S_IFREG | ((route.read && 0444) || (route.write && 0222)), - st_nlink: 1, - st_size: 100 // FIXME - }; - } else { - // default dir attrs - return { - st_mode: unix.S_IFDIR | 0755, - st_nlink: 3 - }; +const ops = { + async getattr({path}) { + let route = findRoute(path); + if (route.getattr) { + return { + st_mode: 0, + st_nlink: 0, + st_size: 0, + ...(await route.getattr(path)) + }; + } else if (route.read || route.write) { + // default file attrs + return { + st_mode: unix.S_IFREG | ((route.read && 0444) || (route.write && 0222)), + st_nlink: 1, + st_size: 100 // FIXME + }; + } else { + // default dir attrs + return { + st_mode: unix.S_IFDIR | 0755, + st_nlink: 3, + st_size: 0 + }; + } + }, + + async open({path}) { + let route = findRoute(path); + if (route.open) return { fh: await route.open(path) }; + else return { fh: 0 }; // empty fh + }, + + async read({path, fh, size, offset}) { + let route = findRoute(path); + if (route.read) return { buf: await route.read(path, fh, size, offset) }; + }, + async write({path, buf, offset}) { + let route = findRoute(path); + if (route.write) return route.write(path, atob(buf), offset); + }, + async release({path, fh}) { + let route = findRoute(path); + if (route.release) return route.release(path, fh); + }, + + async readlink({path}) { + let route = findRoute(path); + if (route.readlink) return { buf: await route.readlink(path) }; + }, + + async opendir({path}) { + let route = findRoute(path); + if (route.opendir) return { fh: await route.opendir(path) }; + else return { fh: 0 }; // empty fh + }, + async readdir({path}) { + let route = findRoute(path); + if (route.readdir) return { entries: await route.readdir(path) }; + return { entries: [".", "..", ...Object.keys(route)] }; + }, + async releasedir({path}) { + let route = findRoute(path); + if (route.releasedir) return route.releasedir(path); } -} - -async function open(path) { - let route = findRoute(path); - if (route.open) return route.open(path); - else return 0; // empty fh -} - -async function read(path, fh, size, offset) { - let route = findRoute(path); - if (route.read) return route.read(path, fh, size, offset); -} -async function write(path, buf, offset) { - let route = findRoute(path); - if (route.write) return route.write(path, buf, offset); -} -async function release(path, fh) { - let route = findRoute(path); - if (route.release) return route.release(path, fh); -} - -async function readlink(path) { - let route = findRoute(path); - if (route.readlink) return route.readlink(path); -} - -async function opendir(path) { - let route = findRoute(path); - if (route.opendir) return route.opendir(path); - else return 0; // empty fh -} -async function readdir(path) { - let route = findRoute(path); - if (route.readdir) return route.readdir(path); - return Object.keys(route); -} -async function releasedir(path) { - let route = findRoute(path); - if (route.releasedir) return route.releasedir(path); -} - -function log(...ss) { - console.log(...ss); -} +}; let port; async function onMessage(req) { - log('req', req); + console.log('req', req); let response = { op: req.op, error: unix.EIO }; /* console.time(req.op + ':' + req.path);*/ try { - if (req.op === 'getattr') { - response = { - op: 'getattr', - st_mode: 0, - st_nlink: 0, - st_size: 0, - ...(await getattr(req.path)) - }; - } else if (req.op === 'open') { - response = { - op: 'open', - fh: await open(req.path) - }; + response = await ops[req.op](req); + response.op = req.op; - } else if (req.op === 'read') { - const ret = await read(req.path, req.fh, req.size, req.offset) - const buf = typeof ret === 'string' ? ret : ret.buf; - response = { - op: 'read', - buf - }; - if (ret.base64Encoded) response.base64Encoded = ret.base64Encoded; - - } else if (req.op === 'write') { - // FIXME: decide whether base64 should be handled here - // or in a higher layer? - const ret = await write(req.path, atob(req.buf), req.offset) - response = { - op: 'write' - }; - - } else if (req.op === 'release') { - await release(req.path, req.fh); - response = { - op: 'release' - }; - - } else if (req.op === 'readlink') { - const buf = await readlink(req.path) - response = { - op: 'readlink', - buf - }; - - } else if (req.op === 'opendir') { - response = { - op: 'opendir', - fh: await opendir(req.path) - }; - - } else if (req.op === 'readdir') { - response = { - op: 'readdir', - entries: [".", "..", ...(await readdir(req.path))] - }; - - } else if (req.op === 'releasedir') { - await releasedir(req.path, req.fh); - response = { op: 'releasedir' }; - } } catch (e) { console.error(e); response = { @@ -360,7 +255,7 @@ async function onMessage(req) { } /* console.timeEnd(req.op + ':' + req.path);*/ - log('resp', response); + console.log('resp', response); port.postMessage(response); }; @@ -369,7 +264,7 @@ function tryConnect() { /* console.log('hello', port);*/ /* updateToolbarIcon();*/ port.onMessage.addListener(onMessage); - port.onDisconnect.addListener(p => {log('disconnect', p)}); + port.onDisconnect.addListener(p => {console.log('disconnect', p)}); /* ws = new WebSocket("ws://localhost:8888"); * updateToolbarIcon(); @@ -387,7 +282,9 @@ function updateToolbarIcon() { } } -tryConnect(); -chrome.browserAction.onClicked.addListener(function() { +if (!TESTING) { tryConnect(); -}); + chrome.browserAction.onClicked.addListener(function() { + tryConnect(); + }); +} diff --git a/fs/tabfs.c b/fs/tabfs.c index 3e5c243..8c6f82d 100644 --- a/fs/tabfs.c +++ b/fs/tabfs.c @@ -234,12 +234,16 @@ main(int argc, char **argv) char killcmd[1000]; sprintf(killcmd, "pgrep tabfs | grep -v %d | xargs kill -9", getpid()); system(killcmd); +#ifdef __APPLE__ system("diskutil umount force mnt > /dev/null"); +#else + system("fusermount -u mnt"); +#endif FILE* log = fopen("log.txt", "w"); for (int i = 0; i < argc; i++) { fprintf(log, "arg%d: [%s]\n", i, argv[i]); fflush(log); } - char* fuse_argv[] = {argv[0], "-odirect_io,noappledouble", "-s", "-f", "mnt"}; + char* fuse_argv[] = {argv[0], "-odirect_io", "-s", "-f", "mnt"}; return fuse_main(5, fuse_argv, &tabfs_filesystem_operations, NULL); }