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
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

View file

@ -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();
});
}

View file

@ -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);
}