summaryrefslogtreecommitdiff
path: root/extension/background.js
diff options
context:
space:
mode:
Diffstat (limited to 'extension/background.js')
-rw-r--r--extension/background.js260
1 files changed, 155 insertions, 105 deletions
diff --git a/extension/background.js b/extension/background.js
index 150be85..e610d72 100644
--- a/extension/background.js
+++ b/extension/background.js
@@ -238,88 +238,110 @@ router["/tabs/by-id"] = {
router["/tabs/by-id/*/title.txt"] = withTab(tab => tab.title + "\n");
router["/tabs/by-id/*/text.txt"] = fromScript(`document.body.innerText`);
router["/tabs/by-id/*/source.html"] = fromScript(`document.body.innerHTML`);
+
+ // echo true > mnt/tabs/by-id/1644/active
+ // cat mnt/tabs/by-id/1644/active
+ router["/tabs/by-id/*/active"] = withTab(tab => JSON.stringify(tab.active) + '\n',
+ // WEIRD: we do startsWith because you might end up with buf
+ // being "truee" (if it was "false", then someone wrote "true")
+ buf => ({ active: buf.startsWith("true") }));
})();
(function() {
- let nextConsoleFh = 0; let consoleForFh = {};
- chrome.runtime.onMessage.addListener(data => {
- if (!consoleForFh[data.fh]) return;
- consoleForFh[data.fh].push(data.xs);
- });
- router["/tabs/by-id/*/console"] = {
- // this one is a bit weird. it doesn't start tracking until it's opened.
- // tail -f console
- async getattr() {
+ const evals = {};
+ router["/tabs/by-id/*/evals"] = {
+ async readdir({path}) {
+ const tabId = parseInt(pathComponent(path, -2));
+ return { entries: [".", "..",
+ ...Object.keys(evals[tabId] || {}),
+ ...Object.keys(evals[tabId] || {}).map(f => f + '.result')] };
+ },
+ getattr() {
return {
- st_mode: unix.S_IFREG | 0444,
- st_nlink: 1,
- st_size: 0 // FIXME
+ st_mode: unix.S_IFDIR | 0777, // writable so you can create/rm evals
+ st_nlink: 3,
+ st_size: 0,
};
},
- async open({path}) {
- const tabId = parseInt(pathComponent(path, -2));
- const fh = nextConsoleFh++;
- const code = `
-// runs in 'content script' context
-var script = document.createElement('script');
-var code = \`
- // will run both here in content script context and in
- // real Web page context (so we hook console.log for both)
- (function() {
- if (!console.__logOld) console.__logOld = console.log;
- if (!console.__logFhs) console.__logFhs = new Set();
- console.__logFhs.add(${fh});
- console.log = (...xs) => {
- console.__logOld(...xs);
- try {
- // TODO: use random event for security instead of this broadcast
- for (let fh of console.__logFhs) {
- window.postMessage({fh: ${fh}, xs: xs}, '*');
- }
- // error usually if one of xs is not serializable
- } catch (e) { console.error(e); }
- };
- })()
-\`;
-eval(code);
-script.appendChild(document.createTextNode(code));
-(document.body || document.head).appendChild(script);
-
-window.addEventListener('message', function({data}) {
- if (data.fh !== ${fh}) return;
- // forward to the background script
- chrome.runtime.sendMessage(null, data);
-});
-`;
- consoleForFh[fh] = [];
- await browser.tabs.executeScript(tabId, {code});
- return {fh};
+ };
+ router["/tabs/by-id/*/evals/*"] = {
+ // NOTE: eval runs in extension's content script, not in original page JS context
+ async mknod({path, mode}) {
+ const [tabId, name] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
+ evals[tabId] = evals[tabId] || {};
+ evals[tabId][name] = { code: '' };
+ return {};
},
- async read({path, fh, offset, size}) {
- const all = consoleForFh[fh].join('\n');
- // TODO: do this more incrementally ?
- // will probably break down if log is huge
- const buf = String.fromCharCode(...toUtf8Array(all).slice(offset, offset + size));
- return { buf };
+ async unlink({path}) {
+ const [tabId, name] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
+ delete evals[tabId][name]; // TODO: also delete evals[tabId] if empty
+ return {};
},
- async release({path, fh}) {
+
+ ...defineFile(async path => {
+ const [tabId, filename] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
+ const name = filename.replace(/\.result$/, '');
+ if (!evals[tabId] || !(name in evals[tabId])) { throw new UnixError(unix.ENOENT); }
+
+ if (filename.endsWith('.result')) {
+ return evals[tabId][name].result || '';
+ } else {
+ return evals[tabId][name].code;
+ }
+ }, async (path, buf) => {
+ const [tabId, name] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
+ if (name.endsWith('.result')) {
+ // FIXME
+
+ } else {
+ evals[tabId][name].code = buf;
+ evals[tabId][name].result = JSON.stringify((await browser.tabs.executeScript(tabId, {code: buf}))[0]) + '\n';
+ }
+ })
+ };
+})();
+(function() {
+ const watches = {};
+ router["/tabs/by-id/*/watches"] = {
+ async readdir({path}) {
const tabId = parseInt(pathComponent(path, -2));
- // TODO: clean up the hooks inside the contexts
- delete consoleForFh[fh];
+ return { entries: [".", "..", ...Object.keys(watches[tabId] || [])] };
+ },
+ getattr() {
+ return {
+ st_mode: unix.S_IFDIR | 0777, // writable so you can create/rm watches
+ st_nlink: 3,
+ st_size: 0,
+ };
+ },
+ };
+ router["/tabs/by-id/*/watches/*"] = {
+ // NOTE: eval runs in extension's content script, not in original page JS context
+ async mknod({path, mode}) {
+ const [tabId, expr] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
+ watches[tabId] = watches[tabId] || {};
+ watches[tabId][expr] = async function() {
+ return (await browser.tabs.executeScript(tabId, {code: expr}))[0];
+ };
return {};
- }
+ },
+ async unlink({path}) {
+ const [tabId, expr] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
+ delete watches[tabId][expr]; // TODO: also delete watches[tabId] if empty
+ return {};
+ },
+
+ ...defineFile(async path => {
+ const [tabId, expr] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
+ if (!watches[tabId] || !(expr in watches[tabId])) { throw new UnixError(unix.ENOENT); }
+ return JSON.stringify(await watches[tabId][expr]()) + '\n';
+ }, () => {
+ // setData handler -- only providing this so that getattr reports
+ // that the file is writable, so it can be deleted without annoying prompt.
+ throw new UnixError(unix.EPERM);
+ })
};
})();
-router["/tabs/by-id/*/execute-script"] = {
- // note: runs in a content script, _not_ in the Web page context
- async write({path, buf}) {
- // FIXME: chunk this properly (like if they write a script in
- // multiple chunks) and only execute when ready?
- const tabId = parseInt(pathComponent(path, -2));
- await browser.tabs.executeScript(tabId, {code: buf});
- return {size: stringToUtf8Array(buf).length};
- },
- async truncate({path, size}) { return {}; }
-};
+
// TODO: imports
// (function() {
// const imports = {};
@@ -333,9 +355,6 @@ router["/tabs/by-id/*/execute-script"] = {
// }
// };
// })();
-// TODO: watches
-// router["/tabs/by-id/*/watches"] = {
-// };
router["/tabs/by-id/*/window"] = {
// a symbolic link to /windows/[id for this window]
async readlink({path}) {
@@ -355,25 +374,6 @@ router["/tabs/by-id/*/control"] = {
},
async truncate({path, size}) { return {}; }
};
-router["/tabs/by-id/*/active"] = {
- // echo true > mnt/tabs/by-id/1644/active
- // cat mnt/tabs/by-id/1644/active
- async read({path, fh, offset, size}) {
- const tabId = parseInt(pathComponent(path, -2));
- const tab = await browser.tabs.get(tabId);
- const buf = (JSON.stringify(tab.active) + '\n').slice(offset, offset + size);
- return { buf };
- },
- async write({path, buf}) {
- if (buf.trim() === "true") {
- const tabId = parseInt(pathComponent(path, -2));
- await browser.tabs.update(tabId, { active: true });
- }
- return {size: stringToUtf8Array(buf).length};
- },
- async truncate({path, size}) { return {}; }
-};
-
// debugger/ : debugger-API-dependent (Chrome-only)
(function() {
if (!chrome.debugger) return;
@@ -449,23 +449,24 @@ router["/tabs/by-id/*/active"] = {
});
})();
-router["/tabs/by-id/*/textareas"] = {
+router["/tabs/by-id/*/inputs"] = {
async readdir({path}) {
const tabId = parseInt(pathComponent(path, -2));
- // TODO: assign new IDs to textareas without them?
- const code = `Array.from(document.querySelectorAll('textarea')).map(e => e.id).filter(id => id)`
+ // TODO: assign new IDs to inputs without them?
+ const code = `Array.from(document.querySelectorAll('textarea, input[type=text]')).map(e => e.id).filter(id => id)`
const ids = (await browser.tabs.executeScript(tabId, {code}))[0];
return { entries: [".", "..", ...ids.map(id => `${id}.txt`)] };
}
};
-router["/tabs/by-id/*/textareas/*"] = defineFile(async path => {
- const [tabId, textareaId] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1).slice(0, -4)];
- const code = `document.getElementById('${textareaId}').value`;
- const textareaValue = (await browser.tabs.executeScript(tabId, {code}))[0];
- return textareaValue;
+router["/tabs/by-id/*/inputs/*"] = defineFile(async path => {
+ const [tabId, inputId] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1).slice(0, -4)];
+ const code = `document.getElementById('${inputId}').value`;
+ const inputValue = (await browser.tabs.executeScript(tabId, {code}))[0];
+ if (inputValue === null) { throw new UnixError(unix.ENOENT); } /* FIXME: hack to deal with if inputId isn't valid */
+ return inputValue;
}, async (path, buf) => {
- const [tabId, textareaId] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1).slice(0, -4)];
- const code = `document.getElementById('${textareaId}').value = unescape('${escape(buf)}')`;
+ const [tabId, inputId] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1).slice(0, -4)];
+ const code = `document.getElementById('${inputId}').value = unescape('${escape(buf)}')`;
await browser.tabs.executeScript(tabId, {code});
});
@@ -515,8 +516,22 @@ router["/windows/last-focused"] = {
return { buf: windowId };
}
};
+(function() {
+ const withWindow = (readHandler, writeHandler) => defineFile(async path => {
+ const windowId = parseInt(pathComponent(path, -2));
+ const window = await browser.windows.get(windowId);
+ return readHandler(window);
+
+ }, writeHandler ? async (path, buf) => {
+ const windowId = parseInt(pathComponent(path, -2));
+ await browser.windows.update(windowId, writeHandler(buf));
+ } : undefined);
+
+ router["/windows/*/focused"] = withWindow(window => JSON.stringify(window.focused) + '\n',
+ buf => ({ focused: buf.startsWith('true') }));
+})();
router["/windows/*/visible-tab.png"] = { ...defineFile(async path => {
- // this is a window thing (rn, the _only_ window thing) because you
+ // screen capture is a window thing and not a tab thing because you
// can only capture the visible tab for each window anyway; you
// can't take a screenshot of just any arbitrary tab
const windowId = parseInt(pathComponent(path, -2));
@@ -633,7 +648,7 @@ for (let key in router) {
router[key] = {
async getattr() {
return {
- st_mode: unix.S_IFREG | ((router[key].read && 0444) || (router[key].write && 0222)),
+ st_mode: unix.S_IFREG | ((router[key].read && 0444) | (router[key].write && 0222)),
st_nlink: 1,
st_size: 100 // FIXME
};
@@ -673,6 +688,10 @@ function findRoute(path) {
let port;
async function onMessage(req) {
+ // Safari / Safari extension app API forces you to adopt their
+ // {name, userInfo} structure for the request.
+ if (req.name === 'ToSafari') req = req.userInfo;
+
if (req.buf) req.buf = atob(req.buf);
console.log('req', req);
@@ -709,6 +728,37 @@ async function onMessage(req) {
};
function tryConnect() {
+ // Safari is very weird -- it has this native app that we have to talk to,
+ // so we poke that app to wake it up, get it to start the TabFS process
+ // and boot a WebSocket, then connect to it.
+ // Is there a better way to do this?
+ if (chrome.runtime.getURL('/').startsWith('safari-web-extension://')) { // Safari-only
+ chrome.runtime.sendNativeMessage('com.rsnous.tabfs', {op: 'safari_did_connect'}, resp => {
+ console.log(resp);
+
+ let socket;
+ function connectSocket(checkAfterTime) {
+ socket = new WebSocket('ws://localhost:9991');
+ socket.addEventListener('message', event => {
+ onMessage(JSON.parse(event.data));
+ });
+
+ port = { postMessage(message) {
+ socket.send(JSON.stringify(message));
+ } };
+
+ setTimeout(() => {
+ if (socket.readyState !== 1) {
+ console.log('ws connection failed, retrying in', checkAfterTime);
+ connectSocket(checkAfterTime * 2);
+ }
+ }, checkAfterTime);
+ }
+ connectSocket(200);
+ });
+ return;
+ }
+
port = chrome.runtime.connectNative('com.rsnous.tabfs');
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(p => {console.log('disconnect', p)});