diff options
Diffstat (limited to 'extension/background.js')
-rw-r--r-- | extension/background.js | 260 |
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)}); |