mirror of
https://github.com/osnr/TabFS.git
synced 2024-05-21 15:06:47 +02:00
remove doc/README. fix truncate on extensions/*/enabled. add timeout
This commit is contained in:
parent
2a67a62e21
commit
462a657bfd
238
README.md
238
README.md
|
@ -1,239 +1,3 @@
|
||||||
# TabFS
|
# TabFS
|
||||||
|
|
||||||
TabFS is a browser extension that mounts your browser tabs as a
|
See [https://omar.website/tabfs/].
|
||||||
filesystem on your computer.
|
|
||||||
|
|
||||||
Out of the box, it supports Chrome and (to a lesser extent) Firefox,
|
|
||||||
on macOS and Linux; it could probably be made to work on other
|
|
||||||
browsers like Safari and Opera that support the WebExtensions API, but
|
|
||||||
I haven't looked into it.
|
|
||||||
|
|
||||||
<img src="doc/finder.png" width="500">
|
|
||||||
|
|
||||||
Each of your open tabs is mapped to a folder with a bunch of files
|
|
||||||
inside it. These files directly reflect (and can control) the state of
|
|
||||||
that tab. (TODO: update as I add more)
|
|
||||||
|
|
||||||
<img src="doc/finder-contents.png" width="500">
|
|
||||||
|
|
||||||
This gives you a _ton_ of power, because now you can apply [all the
|
|
||||||
existing tools](https://twitter.com/rsnous/status/1018570020324962305)
|
|
||||||
on your computer that already know how to deal with files -- terminal
|
|
||||||
commands, scripting languages, etc -- and use them to control and draw
|
|
||||||
information out of your browser. You don't need to code up a browser
|
|
||||||
extension from scratch every time you want to do anything.
|
|
||||||
|
|
||||||
## Examples of stuff you can do!
|
|
||||||
|
|
||||||
(assuming your shell is in the `fs` subdirectory of this repo)
|
|
||||||
|
|
||||||
(TODO: more of these)
|
|
||||||
|
|
||||||
### List the titles of all the tabs you have open
|
|
||||||
|
|
||||||
```
|
|
||||||
$ cat mnt/tabs/by-id/*/title
|
|
||||||
GitHub
|
|
||||||
Extensions
|
|
||||||
TabFS/install.sh at master · osnr/TabFS
|
|
||||||
Alternative Extension Distribution Options - Google Chrome
|
|
||||||
Web Store Hosting and Updating - Google Chrome
|
|
||||||
Home / Twitter
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Close all Stack Overflow tabs
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rm mnt/tabs/by-title/*Stack_Overflow*
|
|
||||||
```
|
|
||||||
|
|
||||||
or (older)
|
|
||||||
|
|
||||||
```
|
|
||||||
$ echo remove | tee -a mnt/tabs/by-title/*Stack_Overflow*/control
|
|
||||||
```
|
|
||||||
|
|
||||||
### Save text of all tabs to a file
|
|
||||||
|
|
||||||
```
|
|
||||||
$ cat mnt/tabs/by-id/*/text > text.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### TODO: Reload an extension when you edit its source code
|
|
||||||
|
|
||||||
Making another extension?
|
|
||||||
|
|
||||||
SO post.
|
|
||||||
|
|
||||||
We can subsume that.
|
|
||||||
|
|
||||||
### TODO: Manage tabs in Emacs dired
|
|
||||||
|
|
||||||
I do this
|
|
||||||
|
|
||||||
### TODO: Live edit
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### TODO: Something with live view of variables
|
|
||||||
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
**disclaimer**: security, functionality, blah blah. applications may
|
|
||||||
freeze ... In some sense, the whole point of this extension is to
|
|
||||||
create a gigantic new surface area of communication between stuff
|
|
||||||
inside your browser and software on the rest of your computer.
|
|
||||||
|
|
||||||
First, install the browser extension.
|
|
||||||
|
|
||||||
Then, install the C filesystem.
|
|
||||||
|
|
||||||
### 1. Install the browser extension
|
|
||||||
|
|
||||||
(I think for Opera or whatever other Chromium-based browser, you could
|
|
||||||
get it to work, but you'd need to change the native messaging path in
|
|
||||||
install.sh. Not sure about Safari. maybe Edge too? if you also got
|
|
||||||
everything to compile for Windows)
|
|
||||||
|
|
||||||
#### in Chrome
|
|
||||||
|
|
||||||
Go to the [Chrome extensions page](chrome://extensions). Enable
|
|
||||||
Developer mode (top-right corner).
|
|
||||||
|
|
||||||
Load-unpacked the `extension/` folder in this repo.
|
|
||||||
|
|
||||||
**Make a note of the extension ID Chrome assigns.** Mine is
|
|
||||||
`jimpolemfaeckpjijgapgkmolankohgj`. We'll use this later.
|
|
||||||
|
|
||||||
#### in Firefox
|
|
||||||
|
|
||||||
You'll need to install as a "temporary extension", so it'll only last
|
|
||||||
in your current FF session.
|
|
||||||
|
|
||||||
Go to [about:debugging#/runtime/this-firefox](about:debugging#/runtime/this-firefox).
|
|
||||||
|
|
||||||
Load Temporary Add-on...
|
|
||||||
|
|
||||||
Choose manifest.json in the extension subfolder of this repo.
|
|
||||||
|
|
||||||
### 2. Install the C filesystem
|
|
||||||
|
|
||||||
First, make sure you `git submodule update --init` to get the
|
|
||||||
`fs/cJSON` and `fs/base64` dependencies.
|
|
||||||
|
|
||||||
And make sure you have FUSE. On Linux, for example, `sudo apt install
|
|
||||||
libfuse-dev`. On macOS, get FUSE for macOS.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ cd fs
|
|
||||||
$ mkdir mnt
|
|
||||||
$ make
|
|
||||||
```
|
|
||||||
|
|
||||||
Now install the native messaging host into your browser, so the
|
|
||||||
extension can launch and talk to the filesystem:
|
|
||||||
|
|
||||||
#### Chrome and Chromium
|
|
||||||
|
|
||||||
Substitute the extension ID you copied earlier for
|
|
||||||
`jimpolemfaeckpjijgapgkmolankohgj` in the command below.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./install.sh chrome jimpolemfaeckpjijgapgkmolankohgj
|
|
||||||
```
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./install.sh chromium jimpolemfaeckpjijgapgkmolankohgj
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Ready!
|
|
||||||
|
|
||||||
Go back to `chrome://extensions` or
|
|
||||||
`about:debugging#/runtime/this-firefox` and reload the extension.
|
|
||||||
|
|
||||||
Now your browser tabs should be mounted in `fs/mnt`!
|
|
||||||
|
|
||||||
Open the background page inspector to see the filesystem operations
|
|
||||||
stream in. (in Chrome, click "background page" next to "Inspect views"
|
|
||||||
in the extension's entry in the Chrome extensions page; in Firefox,
|
|
||||||
click "Inspect")
|
|
||||||
|
|
||||||
<img src="doc/inspector.png" width="600">
|
|
||||||
|
|
||||||
This console is also incredibly helpful for debugging anything that
|
|
||||||
goes wrong, which probably will happen.
|
|
||||||
|
|
||||||
(My OS and applications are pretty chatty! They do a lot of
|
|
||||||
operations, even when I don't feel like I'm actually doing anything.)
|
|
||||||
|
|
||||||
## Design
|
|
||||||
|
|
||||||
- `fs/`: Native FUSE filesystem, written in C
|
|
||||||
- [`tabfs.c`](fs/tabfs.c): Talks to FUSE, implements fs operations, talks to extension.
|
|
||||||
- `extension/`: Browser extension, written in JS
|
|
||||||
- [`background.js`](extension/background.js): **The most interesting
|
|
||||||
file**. Defines all the synthetic files and what browser
|
|
||||||
operations they invoke behind the scenes.
|
|
||||||
|
|
||||||
<!-- TODO: concretize this -->
|
|
||||||
|
|
||||||
When you, say, `cat` a file in the tab filesystem:
|
|
||||||
|
|
||||||
1. `cat` makes something like a `read` syscall,
|
|
||||||
|
|
||||||
2. which goes to the FUSE kernel module which backs that filesystem,
|
|
||||||
|
|
||||||
3. FUSE forwards it to the `tabfs_read` implementation in our
|
|
||||||
userspace filesystem in `fs/tabfs.c`,
|
|
||||||
|
|
||||||
4. then `tabfs_read` rephrases the request as a JSON string and
|
|
||||||
forwards it to the browser extension over 'native messaging',
|
|
||||||
|
|
||||||
6. our browser extension in `extension/background.js` handles the
|
|
||||||
incoming message and calls the browser APIs to construct the data
|
|
||||||
for that synthetic file;
|
|
||||||
|
|
||||||
7. then the data gets sent back in a JSON native message to `tabfs.c`
|
|
||||||
and and finally back to FUSE and the kernel and `cat`.
|
|
||||||
|
|
||||||
(very little actual work happened here, tbh. it's all just
|
|
||||||
marshalling)
|
|
||||||
|
|
||||||
TODO: make diagrams?
|
|
||||||
|
|
||||||
## license
|
|
||||||
|
|
||||||
GPLv3
|
|
||||||
|
|
||||||
## hmm
|
|
||||||
|
|
||||||
processes as files. the real process is the browser.
|
|
||||||
|
|
||||||
browser and Unix; the two operating systems
|
|
||||||
|
|
||||||
it's way too hard to make an extension. even 'make an extension' is a
|
|
||||||
bad framing; it suggests making an extension is a whole Thing, a whole
|
|
||||||
Project. like, why can't I just take a minute to ask my browser a
|
|
||||||
question or tell it to automate something? lightness
|
|
||||||
|
|
||||||
open input space -- filesystem
|
|
||||||
|
|
||||||
now you have this whole 'language', this whole toolset, to control and
|
|
||||||
automate your browser. there's this built-up existing capital where
|
|
||||||
lots of people already know the operations to work with files
|
|
||||||
|
|
||||||
this project is cool bc i immediately get a dataset i care about
|
|
||||||
|
|
||||||
OSQuery
|
|
||||||
|
|
||||||
fake filesystems talk
|
|
||||||
|
|
||||||
Screenotate
|
|
||||||
|
|
||||||
rmdir a non-empty directory
|
|
||||||
|
|
||||||
do you like setting up sockets? I don't
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 262 KiB |
BIN
doc/finder.png
BIN
doc/finder.png
Binary file not shown.
Before Width: | Height: | Size: 869 KiB |
Binary file not shown.
Before Width: | Height: | Size: 998 KiB |
|
@ -8,6 +8,7 @@ const unix = {
|
||||||
EIO: 5,
|
EIO: 5,
|
||||||
ENXIO: 6,
|
ENXIO: 6,
|
||||||
ENOTSUP: 45,
|
ENOTSUP: 45,
|
||||||
|
ETIMEDOUT: 110,
|
||||||
|
|
||||||
// Unix file types
|
// Unix file types
|
||||||
S_IFMT: 0170000, // type of file mask
|
S_IFMT: 0170000, // type of file mask
|
||||||
|
@ -215,6 +216,17 @@ router["/tabs/by-id"] = {
|
||||||
router["/tabs/by-id/*/url"] = withTab(tab => tab.url + "\n", buf => ({ url: buf }));
|
router["/tabs/by-id/*/url"] = withTab(tab => tab.url + "\n", buf => ({ url: buf }));
|
||||||
router["/tabs/by-id/*/title"] = withTab(tab => tab.title + "\n");
|
router["/tabs/by-id/*/title"] = withTab(tab => tab.title + "\n");
|
||||||
router["/tabs/by-id/*/text"] = fromScript(`document.body.innerText`);
|
router["/tabs/by-id/*/text"] = fromScript(`document.body.innerText`);
|
||||||
|
router["/tabs/by-id/*/console"] = {
|
||||||
|
open() {
|
||||||
|
// inject the console
|
||||||
|
},
|
||||||
|
read() {
|
||||||
|
|
||||||
|
},
|
||||||
|
write() {
|
||||||
|
// what does this even do?
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
router["/tabs/by-id/*/screenshot.png"] = defineFile(async path => {
|
router["/tabs/by-id/*/screenshot.png"] = defineFile(async path => {
|
||||||
const tabId = parseInt(pathComponent(path, -2));
|
const tabId = parseInt(pathComponent(path, -2));
|
||||||
|
@ -278,7 +290,6 @@ router["/tabs/by-id/*/scripts/*"] = defineFile(async path => {
|
||||||
await sendDebuggerCommand(tabId, "Debugger.setScriptSource", {scriptId, scriptSource: buf});
|
await sendDebuggerCommand(tabId, "Debugger.setScriptSource", {scriptId, scriptSource: buf});
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
router["/tabs/by-id/*/control"] = {
|
router["/tabs/by-id/*/control"] = {
|
||||||
// echo remove >> mnt/tabs/by-id/1644/control
|
// echo remove >> mnt/tabs/by-id/1644/control
|
||||||
async write({path, buf}) {
|
async write({path, buf}) {
|
||||||
|
@ -295,7 +306,7 @@ router["/tabs/by-id/*/control"] = {
|
||||||
router["/tabs/by-title"] = {
|
router["/tabs/by-title"] = {
|
||||||
getattr() {
|
getattr() {
|
||||||
return {
|
return {
|
||||||
st_mode: unix.S_IFDIR | 0777,
|
st_mode: unix.S_IFDIR | 0777, // writable
|
||||||
st_nlink: 3,
|
st_nlink: 3,
|
||||||
st_size: 0,
|
st_size: 0,
|
||||||
};
|
};
|
||||||
|
@ -311,7 +322,7 @@ router["/tabs/by-title/*"] = {
|
||||||
const parts = path.split("_"); const tabId = parts[parts.length - 1];
|
const parts = path.split("_"); const tabId = parts[parts.length - 1];
|
||||||
return { buf: "../by-id/" + tabId };
|
return { buf: "../by-id/" + tabId };
|
||||||
},
|
},
|
||||||
async unlink({path}) {
|
async unlink({path}) { // you can delete a by-title/TAB to close that tab
|
||||||
const parts = path.split("_"); const tabId = parseInt(parts[parts.length - 1]);
|
const parts = path.split("_"); const tabId = parseInt(parts[parts.length - 1]);
|
||||||
await browser.tabs.remove(tabId);
|
await browser.tabs.remove(tabId);
|
||||||
return {};
|
return {};
|
||||||
|
@ -332,16 +343,17 @@ router["/extensions"] = {
|
||||||
return { entries: [".", "..", ...infos.map(info => `${sanitize(info.name)}_${info.id}`)] };
|
return { entries: [".", "..", ...infos.map(info => `${sanitize(info.name)}_${info.id}`)] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
router["/extensions/*/enabled"] = defineFile(async path => {
|
router["/extensions/*/enabled"] = { ...defineFile(async path => {
|
||||||
const parts = pathComponent(path, -2).split('_'); const extensionId = parts[parts.length - 1];
|
const parts = pathComponent(path, -2).split('_'); const extensionId = parts[parts.length - 1];
|
||||||
const info = await browser.management.get(extensionId);
|
const info = await browser.management.get(extensionId);
|
||||||
return String(info.enabled) + '\n';
|
return String(info.enabled) + '\n';
|
||||||
|
|
||||||
}, async (path, buf) => {
|
}, async (path, buf) => {
|
||||||
console.log(buf);
|
|
||||||
const parts = pathComponent(path, -2).split('_'); const extensionId = parts[parts.length - 1];
|
const parts = pathComponent(path, -2).split('_'); const extensionId = parts[parts.length - 1];
|
||||||
await browser.management.setEnabled(extensionId, buf.trim() === "true");
|
await browser.management.setEnabled(extensionId, buf.trim() === "true");
|
||||||
});
|
|
||||||
|
// suppress truncate so it doesn't accidentally flip the state when you do, e.g., `echo true >`
|
||||||
|
}), truncate() { return {}; } };
|
||||||
|
|
||||||
router["/runtime/reload"] = {
|
router["/runtime/reload"] = {
|
||||||
async write({path, buf}) {
|
async write({path, buf}) {
|
||||||
|
@ -466,6 +478,13 @@ async function onMessage(req) {
|
||||||
console.log('req', req);
|
console.log('req', req);
|
||||||
|
|
||||||
let response = { op: req.op, error: unix.EIO };
|
let response = { op: req.op, error: unix.EIO };
|
||||||
|
let didTimeout = false, timeout = setTimeout(() => {
|
||||||
|
// timeout is very useful because some operations just hang
|
||||||
|
// (like trying to take a screenshot, until the tab is focused)
|
||||||
|
didTimeout = true; console.error('timeout');
|
||||||
|
port.postMessage({ op: req.op, error: unix.ETIMEDOUT });
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
/* console.time(req.op + ':' + req.path);*/
|
/* console.time(req.op + ':' + req.path);*/
|
||||||
try {
|
try {
|
||||||
response = await findRoute(req.path)[req.op](req);
|
response = await findRoute(req.path)[req.op](req);
|
||||||
|
@ -481,8 +500,12 @@ async function onMessage(req) {
|
||||||
}
|
}
|
||||||
/* console.timeEnd(req.op + ':' + req.path);*/
|
/* console.timeEnd(req.op + ':' + req.path);*/
|
||||||
|
|
||||||
console.log('resp', response);
|
if (!didTimeout) {
|
||||||
port.postMessage(response);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
console.log('resp', response);
|
||||||
|
port.postMessage(response);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function tryConnect() {
|
function tryConnect() {
|
||||||
|
|
2
test.c
2
test.c
|
@ -24,7 +24,7 @@ char* expand(char* phrase) {
|
||||||
int main() {
|
int main() {
|
||||||
assert(system("echo about:blank > fs/mnt/tabs/create") == 0);
|
assert(system("echo about:blank > fs/mnt/tabs/create") == 0);
|
||||||
assert(file_contents_equal("fs/mnt/tabs/last-focused/url", "about:blank"));
|
assert(file_contents_equal("fs/mnt/tabs/last-focused/url", "about:blank"));
|
||||||
/* assert(system("file fs/mnt/tabs/last-focused/screenshot.png") == 0); */
|
assert(system("file fs/mnt/tabs/last-focused/screenshot.png") == 0); // slow
|
||||||
assert(system("echo remove > fs/mnt/tabs/last-focused/control") == 0);
|
assert(system("echo remove > fs/mnt/tabs/last-focused/control") == 0);
|
||||||
|
|
||||||
assert(file_contents_equal(expand("fs/mnt/extensions/TabFS*/enabled"), "true"));
|
assert(file_contents_equal(expand("fs/mnt/extensions/TabFS*/enabled"), "true"));
|
||||||
|
|
Loading…
Reference in a new issue