working on refactor of fs spec. also Linux port and README stuff.

This commit is contained in:
Omar Rizwan 2020-10-27 20:53:39 -07:00
parent 2426e9f7a5
commit a9cb8e14af
3 changed files with 222 additions and 285 deletions

View file

@ -1,9 +1,9 @@
# tabfs # TabFS
## Setup ## Setup
You need to both install the Chrome extension and run the native You need to compile the FUSE filesystem (written in C), then install
filesystem. the browser extension which runs it and talks to it.
### Run the C filesystem ### Run the C filesystem
@ -19,7 +19,21 @@ $ mkdir mnt
$ make $ 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). Go to the [Chrome extensions page](chrome://extensions).
@ -56,3 +70,25 @@ When you, say, `cat` a file in the tab filesystem:
marshalling) marshalling)
TODO: make diagrams? 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

View file

@ -1,3 +1,5 @@
const TESTING = (typeof chrome === 'undefined');
const unix = { const unix = {
EPERM: 1, EPERM: 1,
ENOENT: 2, ENOENT: 2,
@ -24,26 +26,6 @@ function UnixError(error) {
} }
UnixError.prototype = Error.prototype; 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/title
// tabs/by-id/ID/url // tabs/by-id/ID/url
// tabs/by-id/ID/console // tabs/by-id/ID/console
@ -63,23 +45,56 @@ function sanitize(s) {
return s.replace(/[^A-Za-z0-9_\-\.]/gm, '_'); 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 = {};
const router = { async function withTab(handler) {
"tabs": { return {
/* "last-focused": { async read(path, fh, size, offset) {
* // FIXME: symlink to tab by id. const tab = await browser.tabs.get(parseInt(pathComponent(path, -2)));
* async readlink() { return handler(tab);
* return "../windows/last-focused/selected-tab" }
* } };
* }, }
*/ async function fromScript(code) {
"by-title": { return {
async readdir() { 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({}); const tabs = await browser.tabs.query({});
return tabs.map(tab => sanitize(String(tab.title).slice(0, 200)) + "_" + String(tab.id)); return tabs.map(tab => sanitize(String(tab.title).slice(0, 200)) + "_" + String(tab.id));
}, }
"*": { };
router["/tabs/by-title/*"] = {
async getattr(path) { async getattr(path) {
const st_size = (await this.readlink(path)).length + 1; const st_size = (await this.readlink(path)).length + 1;
return { return {
@ -94,137 +109,76 @@ const router = {
const id = parts[parts.length - 1]; const id = parts[parts.length - 1];
return "../by-id/" + id; 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", {});
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", {});
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);
}
}
}
}
}
}
}; };
/* "last-focused": {
* // FIXME: symlink to tab by id.
* async readlink() {
* return "../windows/last-focused/selected-tab"
* }
* },
*/
// 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("/"));
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))
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) { function findRoute(path) {
let route = router;
let pathSegments = path.split("/"); let pathSegments = path.split("/");
if (pathSegments[pathSegments.length - 1].startsWith("._")) { if (pathSegments[pathSegments.length - 1].startsWith("._")) {
throw new UnixError(unix.ENOTSUP); // Apple Double file for xattrs throw new UnixError(unix.ENOTSUP); // Apple Double file for xattrs
} }
let routingPath = "";
for (let segment of pathSegments) { for (let segment of pathSegments) {
if (segment === "") continue; if (router[routingPath + "/" + segment]) {
route = route[segment] || route["*"]; routingPath += "/" + segment;
} else {
if (!route) throw new UnixError(unix.ENOENT); routingPath += "/*";
}
return route;
} }
async function getattr(path) { if (!router[routingPath]) throw new UnixError(unix.ENOENT);
}
return router[routingPath];
}
const ops = {
async getattr({path}) {
let route = findRoute(path); let route = findRoute(path);
if (route.getattr) { if (route.getattr) {
return route.getattr(path); return {
st_mode: 0,
st_nlink: 0,
st_size: 0,
...(await route.getattr(path))
};
} else if (route.read || route.write) { } else if (route.read || route.write) {
// default file attrs // default file attrs
return { return {
@ -236,121 +190,62 @@ async function getattr(path) {
// default dir attrs // default dir attrs
return { return {
st_mode: unix.S_IFDIR | 0755, st_mode: unix.S_IFDIR | 0755,
st_nlink: 3 st_nlink: 3,
st_size: 0
}; };
} }
} },
async function open(path) { async open({path}) {
let route = findRoute(path); let route = findRoute(path);
if (route.open) return route.open(path); if (route.open) return { fh: await route.open(path) };
else return 0; // empty fh else return { fh: 0 }; // empty fh
} },
async function read(path, fh, size, offset) { async read({path, fh, size, offset}) {
let route = findRoute(path); let route = findRoute(path);
if (route.read) return route.read(path, fh, size, offset); if (route.read) return { buf: await route.read(path, fh, size, offset) };
} },
async function write(path, buf, offset) { async write({path, buf, offset}) {
let route = findRoute(path); let route = findRoute(path);
if (route.write) return route.write(path, buf, offset); if (route.write) return route.write(path, atob(buf), offset);
} },
async function release(path, fh) { async release({path, fh}) {
let route = findRoute(path); let route = findRoute(path);
if (route.release) return route.release(path, fh); if (route.release) return route.release(path, fh);
} },
async function readlink(path) { async readlink({path}) {
let route = findRoute(path); let route = findRoute(path);
if (route.readlink) return route.readlink(path); if (route.readlink) return { buf: await route.readlink(path) };
} },
async function opendir(path) { async opendir({path}) {
let route = findRoute(path); let route = findRoute(path);
if (route.opendir) return route.opendir(path); if (route.opendir) return { fh: await route.opendir(path) };
else return 0; // empty fh else return { fh: 0 }; // empty fh
} },
async function readdir(path) { async readdir({path}) {
let route = findRoute(path); let route = findRoute(path);
if (route.readdir) return route.readdir(path); if (route.readdir) return { entries: await route.readdir(path) };
return Object.keys(route); return { entries: [".", "..", ...Object.keys(route)] };
} },
async function releasedir(path) { async releasedir({path}) {
let route = findRoute(path); let route = findRoute(path);
if (route.releasedir) return route.releasedir(path); if (route.releasedir) return route.releasedir(path);
} }
};
function log(...ss) {
console.log(...ss);
}
let port; let port;
async function onMessage(req) { async function onMessage(req) {
log('req', req); console.log('req', req);
let response = { op: req.op, error: unix.EIO }; let response = { op: req.op, error: unix.EIO };
/* console.time(req.op + ':' + req.path);*/ /* console.time(req.op + ':' + req.path);*/
try { try {
if (req.op === 'getattr') { response = await ops[req.op](req);
response = { response.op = req.op;
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)
};
} 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) { } catch (e) {
console.error(e); console.error(e);
response = { response = {
@ -360,7 +255,7 @@ async function onMessage(req) {
} }
/* console.timeEnd(req.op + ':' + req.path);*/ /* console.timeEnd(req.op + ':' + req.path);*/
log('resp', response); console.log('resp', response);
port.postMessage(response); port.postMessage(response);
}; };
@ -369,7 +264,7 @@ function tryConnect() {
/* console.log('hello', port);*/ /* console.log('hello', port);*/
/* updateToolbarIcon();*/ /* updateToolbarIcon();*/
port.onMessage.addListener(onMessage); 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"); /* ws = new WebSocket("ws://localhost:8888");
* updateToolbarIcon(); * updateToolbarIcon();
@ -387,7 +282,9 @@ function updateToolbarIcon() {
} }
} }
if (!TESTING) {
tryConnect(); tryConnect();
chrome.browserAction.onClicked.addListener(function() { chrome.browserAction.onClicked.addListener(function() {
tryConnect(); tryConnect();
}); });
}

View file

@ -234,12 +234,16 @@ main(int argc, char **argv)
char killcmd[1000]; char killcmd[1000];
sprintf(killcmd, "pgrep tabfs | grep -v %d | xargs kill -9", getpid()); sprintf(killcmd, "pgrep tabfs | grep -v %d | xargs kill -9", getpid());
system(killcmd); system(killcmd);
#ifdef __APPLE__
system("diskutil umount force mnt > /dev/null"); system("diskutil umount force mnt > /dev/null");
#else
system("fusermount -u mnt");
#endif
FILE* log = fopen("log.txt", "w"); FILE* log = fopen("log.txt", "w");
for (int i = 0; i < argc; i++) { for (int i = 0; i < argc; i++) {
fprintf(log, "arg%d: [%s]\n", i, argv[i]); fflush(log); 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); return fuse_main(5, fuse_argv, &tabfs_filesystem_operations, NULL);
} }