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 is a browser extension that mounts your browser tabs as a
|
||||
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
|
||||
See [https://omar.website/tabfs/].
|
||||
|
|
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,
|
||||
ENXIO: 6,
|
||||
ENOTSUP: 45,
|
||||
ETIMEDOUT: 110,
|
||||
|
||||
// Unix file types
|
||||
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/*/title"] = withTab(tab => tab.title + "\n");
|
||||
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 => {
|
||||
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});
|
||||
return {};
|
||||
});
|
||||
|
||||
router["/tabs/by-id/*/control"] = {
|
||||
// echo remove >> mnt/tabs/by-id/1644/control
|
||||
async write({path, buf}) {
|
||||
|
@ -295,7 +306,7 @@ router["/tabs/by-id/*/control"] = {
|
|||
router["/tabs/by-title"] = {
|
||||
getattr() {
|
||||
return {
|
||||
st_mode: unix.S_IFDIR | 0777,
|
||||
st_mode: unix.S_IFDIR | 0777, // writable
|
||||
st_nlink: 3,
|
||||
st_size: 0,
|
||||
};
|
||||
|
@ -311,7 +322,7 @@ router["/tabs/by-title/*"] = {
|
|||
const parts = path.split("_"); const tabId = parts[parts.length - 1];
|
||||
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]);
|
||||
await browser.tabs.remove(tabId);
|
||||
return {};
|
||||
|
@ -332,16 +343,17 @@ router["/extensions"] = {
|
|||
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 info = await browser.management.get(extensionId);
|
||||
return String(info.enabled) + '\n';
|
||||
|
||||
}, async (path, buf) => {
|
||||
console.log(buf);
|
||||
const parts = pathComponent(path, -2).split('_'); const extensionId = parts[parts.length - 1];
|
||||
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"] = {
|
||||
async write({path, buf}) {
|
||||
|
@ -466,6 +478,13 @@ async function onMessage(req) {
|
|||
console.log('req', req);
|
||||
|
||||
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);*/
|
||||
try {
|
||||
response = await findRoute(req.path)[req.op](req);
|
||||
|
@ -481,8 +500,12 @@ async function onMessage(req) {
|
|||
}
|
||||
/* console.timeEnd(req.op + ':' + req.path);*/
|
||||
|
||||
if (!didTimeout) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
console.log('resp', response);
|
||||
port.postMessage(response);
|
||||
}
|
||||
};
|
||||
|
||||
function tryConnect() {
|
||||
|
|
2
test.c
2
test.c
|
@ -24,7 +24,7 @@ char* expand(char* phrase) {
|
|||
int main() {
|
||||
assert(system("echo about:blank > fs/mnt/tabs/create") == 0);
|
||||
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(file_contents_equal(expand("fs/mnt/extensions/TabFS*/enabled"), "true"));
|
||||
|
|
Loading…
Reference in a new issue