TabFS/extension/background.js
Omar Rizwan b948c235b8 fs,extension: Make truncate work on cached file content.
We now remember the path of each cached file, so when someone
truncates that path, we can truncate all open instances of it in
cache. Important when someone pipes to stomp a file / opens with
O_TRUNC (which FUSE disaggregates into an open() and then a truncate()
call). Gets rid of the need for FUSE_CAP_ATOMIC_O_TRUNC, which never
worked on macOS anyway.

Fixes #40.
2021-04-06 13:48:48 -07:00

749 lines
26 KiB
JavaScript

const unix = {
EPERM: 1,
ENOENT: 2,
ESRCH: 3,
EINTR: 4,
EIO: 5,
ENXIO: 6,
ENOTSUP: 45,
ETIMEDOUT: 110, // FIXME: not on macOS (?)
// Unix file types
S_IFMT: 0170000, // type of file mask
S_IFIFO: 010000, // named pipe (fifo)
S_IFCHR: 020000, // character special
S_IFDIR: 040000, // directory
S_IFBLK: 060000, // block special
S_IFREG: 0100000, // regular
S_IFLNK: 0120000, // symbolic link
S_IFSOCK: 0140000, // socket
}
class UnixError extends Error {
constructor(error) { super(); this.name = "UnixError"; this.error = error; }
}
const sanitize = (function() {
// from https://github.com/parshap/node-sanitize-filename/blob/209c39b914c8eb48ee27bcbde64b2c7822fdf3de/index.js
// I've added ' ' to the list of illegal characters. it's a
// decision whether we want to allow spaces in filenames... I think
// they're annoying, so I'm sanitizing them out for now.
var illegalRe = /[\/\?<>\\:\*\|" ]/g;
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
var reservedRe = /^\.+$/;
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
var windowsTrailingRe = /[\. ]+$/;
function sanitize(input, replacement) {
if (typeof input !== 'string') {
throw new Error('Input must be string');
}
var sanitized = input
.replace(illegalRe, replacement)
.replace(controlRe, replacement)
.replace(reservedRe, replacement)
.replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement);
return sanitized.slice(0, 200);
}
return input => sanitize(input, '_');
})();
const stringToUtf8Array = (function() {
const encoder = new TextEncoder("utf-8");
return str => encoder.encode(str);
})();
const utf8ArrayToString = (function() {
const decoder = new TextDecoder("utf-8");
return utf8 => decoder.decode(utf8);
})();
// Helper function: generates a full set of file operations that you
// can use as a route handler (so clients can read and write
// sections of the file, stat it to get its size and see it show up
// in ls, etc), given getData and setData functions that define the
// contents of the entire file.
const defineFile = (function() {
const Cache = {
// used when you open a file to cache the content we got from the
// browser until you close that file. (so we can respond to
// individual chunk read() and write() requests without doing a
// whole new conversation with the browser and regenerating the
// content -- important for taking a screenshot, for instance)
store: {}, nextHandle: 0,
storeObject(path, object) {
const handle = ++this.nextHandle;
this.store[handle] = {path, object};
return handle;
},
getObjectForHandle(handle) { return this.store[handle].object; },
setObjectForHandle(handle, object) { this.store[handle].object = object; },
removeObjectForHandle(handle) { delete this.store[handle]; },
setObjectForPath(path, object) {
for (let storedObject of Object.values(this.store)) {
if (storedObject.path === path) {
storedObject.object = object;
}
}
}
};
function toUtf8Array(stringOrArray) {
if (typeof stringOrArray == 'string') { return stringToUtf8Array(stringOrArray); }
else { return stringOrArray; }
}
return (getData, setData) => ({
// getData: (req: Request U Vars) -> Promise<contentsOfFile: String|Uint8Array>
// setData [optional]: (req: Request U Vars, newContentsOfFile: String) -> Promise<>
// You can override file operations (like `truncate` or `getattr`)
// in the returned set if you want different behavior from what's
// defined here.
async getattr(req) {
return {
st_mode: unix.S_IFREG | 0444 | (setData ? 0222 : 0),
st_nlink: 1,
// you'll want to override this if getData() is slow, because
// getattr() gets called a lot more cavalierly than open().
st_size: toUtf8Array(await getData(req)).length
};
},
// We call getData() once when the file is opened, then cache that
// data for all subsequent reads from that application.
async open(req) {
const data = await getData(req);
return { fh: Cache.storeObject(req.path, toUtf8Array(data)) };
},
async read({fh, size, offset}) {
return { buf: String.fromCharCode(...Cache.getObjectForHandle(fh).slice(offset, offset + size)) };
},
async write(req) {
const {fh, offset, buf} = req;
let arr = Cache.getObjectForHandle(fh);
const bufarr = stringToUtf8Array(buf);
if (offset + bufarr.length > arr.length) {
const newArr = new Uint8Array(offset + bufarr.length);
newArr.set(arr.slice(0, Math.min(offset, arr.length)));
arr = newArr;
Cache.setObjectForHandle(fh, arr);
}
arr.set(bufarr, offset);
// I guess caller should override write() if they want to actually
// patch and not just re-set the whole string (for example,
// if they want to hot-reload just one function the user modified)
await setData(req, utf8ArrayToString(arr)); return { size: bufarr.length };
},
async release({fh}) { Cache.removeObjectForHandle(fh); return {}; },
async truncate(req) {
let arr = toUtf8Array(await getData(req));
if (req.size !== arr.length) {
const newArr = new Uint8Array(req.size);
newArr.set(arr.slice(0, Math.min(req.size, arr.length)));
arr = newArr;
}
Cache.setObjectForPath(req.path, arr);
await setData(req, utf8ArrayToString(arr)); return {};
}
});
})();
const Routes = {};
Routes["/tabs/create"] = {
async write({buf}) {
const url = buf.trim();
await browser.tabs.create({url});
return {size: stringToUtf8Array(buf).length};
},
async truncate() { return {}; }
};
Routes["/tabs/by-id"] = {
async readdir() {
const tabs = await browser.tabs.query({});
return { entries: [".", "..", ...tabs.map(tab => String(tab.id))] };
}
};
(function() {
const withTab = (readHandler, writeHandler) => defineFile(async ({tabId}) => {
const tab = await browser.tabs.get(tabId);
return readHandler(tab);
}, writeHandler ? async ({tabId}, buf) => {
await browser.tabs.update(tabId, writeHandler(buf));
} : undefined);
const fromScript = code => defineFile(async ({tabId}) => {
return (await browser.tabs.executeScript(tabId, {code}))[0];
});
Routes["/tabs/by-id/#TAB_ID/url.txt"] = withTab(tab => tab.url + "\n", buf => ({ url: buf }));
Routes["/tabs/by-id/#TAB_ID/title.txt"] = withTab(tab => tab.title + "\n");
Routes["/tabs/by-id/#TAB_ID/text.txt"] = fromScript(`document.body.innerText`);
Routes["/tabs/by-id/#TAB_ID/body.html"] = fromScript(`document.body.innerHTML`);
// echo true > mnt/tabs/by-id/1644/active
// cat mnt/tabs/by-id/1644/active
Routes["/tabs/by-id/#TAB_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() {
const evals = {};
Routes["/tabs/by-id/#TAB_ID/evals"] = {
async readdir({path, tabId}) {
return { entries: [".", "..",
...Object.keys(evals[tabId] || {}),
...Object.keys(evals[tabId] || {}).map(f => f + '.result')] };
},
getattr() {
return {
st_mode: unix.S_IFDIR | 0777, // writable so you can create/rm evals
st_nlink: 3,
st_size: 0,
};
},
};
Routes["/tabs/by-id/#TAB_ID/evals/:FILENAME"] = {
// NOTE: eval runs in extension's content script, not in original page JS context
async mknod({tabId, filename, mode}) {
evals[tabId] = evals[tabId] || {};
evals[tabId][filename] = { code: '' };
return {};
},
async unlink({tabId, filename}) {
delete evals[tabId][filename]; // TODO: also delete evals[tabId] if empty
return {};
},
...defineFile(async ({tabId, filename}) => {
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 ({tabId, filename}, buf) => {
if (filename.endsWith('.result')) {
// FIXME: case where they try to write to .result file
} else {
const name = filename;
evals[tabId][name].code = buf;
evals[tabId][name].result = JSON.stringify((await browser.tabs.executeScript(tabId, {code: buf}))[0]) + '\n';
}
})
};
})();
(function() {
const watches = {};
Routes["/tabs/by-id/#TAB_ID/watches"] = {
async readdir({tabId}) {
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,
};
},
};
Routes["/tabs/by-id/#TAB_ID/watches/:EXPR"] = {
// NOTE: eval runs in extension's content script, not in original page JS context
async mknod({tabId, expr, mode}) {
watches[tabId] = watches[tabId] || {};
watches[tabId][expr] = async function() {
return (await browser.tabs.executeScript(tabId, {code: expr}))[0];
};
return {};
},
async unlink({tabId, expr}) {
delete watches[tabId][expr]; // TODO: also delete watches[tabId] if empty
return {};
},
...defineFile(async ({tabId, expr}) => {
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);
})
};
})();
Routes["/tabs/by-id/#TAB_ID/window"] = {
// a symbolic link to /windows/[id for this window]
async readlink({tabId}) {
const tab = await browser.tabs.get(tabId);
return { buf: "../../../windows/" + tab.windowId };
}
};
Routes["/tabs/by-id/#TAB_ID/control"] = {
// echo remove > mnt/tabs/by-id/1644/control
async write({tabId, buf}) {
const command = buf.trim();
// can use `discard`, `remove`, `reload`, `goForward`, `goBack`...
// see https://developer.chrome.com/extensions/tabs
await browser.tabs[command](tabId);
return {size: stringToUtf8Array(buf).length};
},
async truncate({size}) { return {}; }
};
// debugger/ : debugger-API-dependent (Chrome-only)
(function() {
if (!chrome.debugger) return;
async function attachDebugger(tabId) {
return new Promise((resolve, reject) => chrome.debugger.attach({tabId}, "1.3", () => {
if (chrome.runtime.lastError) { reject(chrome.runtime.lastError); }
else { resolve(); }
}));
}
async function detachDebugger(tabId) {
return new Promise((resolve, reject) => chrome.debugger.detach({tabId}, () => {
if (chrome.runtime.lastError) { reject(chrome.runtime.lastError); }
else { resolve(); }
}));
}
const TabManager = (function() {
if (chrome.debugger) chrome.debugger.onEvent.addListener((source, method, params) => {
console.log(source, method, params);
if (method === "Page.frameStartedLoading") {
// we're gonna assume we're always plugged into both Page and Debugger.
TabManager.scriptsForTab[source.tabId] = {};
} else if (method === "Debugger.scriptParsed") {
TabManager.scriptsForTab[source.tabId] = TabManager.scriptsForTab[source.tabId] || {};
TabManager.scriptsForTab[source.tabId][params.scriptId] = params;
}
});
return {
scriptsForTab: {},
debugTab: async function(tabId) {
// meant to be higher-level wrapper for raw attach/detach
// TODO: could we remember if we're already attached? idk if it's worth it
try { await attachDebugger(tabId); }
catch (e) {
if (e.message.indexOf('Another debugger is already attached') !== -1) {
await detachDebugger(tabId);
await attachDebugger(tabId);
}
}
// TODO: detach automatically? some kind of reference counting thing?
},
enableDomainForTab: async function(tabId, domain) {
// TODO: could we remember if we're already enabled? idk if it's worth it
if (domain === 'Debugger') { TabManager.scriptsForTab[tabId] = {}; }
await sendDebuggerCommand(tabId, `${domain}.enable`, {});
}
};
})();
function sendDebuggerCommand(tabId, method, commandParams) {
return new Promise((resolve, reject) =>
chrome.debugger.sendCommand({tabId}, method, commandParams, result => {
if (result) { resolve(result); } else { reject(chrome.runtime.lastError); }
})
);
}
// possible idea: console (using Log API instead of monkey-patching)
// resources/
// TODO: scripts/ TODO: allow creation, eval immediately
Routes["/tabs/by-id/#TAB_ID/debugger/resources"] = {
async readdir({tabId}) {
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
return { entries: [".", "..", ...frameTree.resources.map(r => sanitize(String(r.url)))] };
}
};
Routes["/tabs/by-id/#TAB_ID/debugger/resources/:SUFFIX"] = defineFile(async ({path, tabId, suffix}) => {
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
for (let resource of frameTree.resources) {
const resourceSuffix = sanitize(String(resource.url));
if (resourceSuffix === suffix) {
let {base64Encoded, content} = await sendDebuggerCommand(tabId, "Page.getResourceContent", {
frameId: frameTree.frame.id,
url: resource.url
});
if (base64Encoded) { return Uint8Array.from(atob(content), c => c.charCodeAt(0)); }
return content;
}
}
throw new UnixError(unix.ENOENT);
});
Routes["/tabs/by-id/#TAB_ID/debugger/scripts"] = {
async opendir({tabId}) {
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
return { fh: 0 };
},
async readdir({tabId}) {
// it's useful to put the ID first in the script filenames, so
// the .js extension stays on the end
const scriptFileNames = Object.values(TabManager.scriptsForTab[tabId])
.map(params => params.scriptId + "_" + sanitize(params.url));
return { entries: [".", "..", ...scriptFileNames] };
}
};
function pathScriptInfo(tabId, filename) {
const [scriptId, ...rest] = filename.split("_");
const scriptInfo = TabManager.scriptsForTab[tabId][scriptId];
if (!scriptInfo || sanitize(scriptInfo.url) !== rest.join("_")) {
throw new UnixError(unix.ENOENT);
}
return scriptInfo;
}
Routes["/tabs/by-id/#TAB_ID/debugger/scripts/:FILENAME"] = defineFile(async ({tabId, filename}) => {
await TabManager.debugTab(tabId);
await TabManager.enableDomainForTab(tabId, "Page");
await TabManager.enableDomainForTab(tabId, "Debugger");
const {scriptId} = pathScriptInfo(tabId, filename);
const {scriptSource} = await sendDebuggerCommand(tabId, "Debugger.getScriptSource", {scriptId});
return scriptSource;
}, async ({tabId, filename}, buf) => {
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
const {scriptId} = pathScriptInfo(tabId, filename);
await sendDebuggerCommand(tabId, "Debugger.setScriptSource", {scriptId, scriptSource: buf});
});
})();
Routes["/tabs/by-id/#TAB_ID/inputs"] = {
async readdir({tabId}) {
// 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`)] };
}
};
Routes["/tabs/by-id/#TAB_ID/inputs/:INPUT_ID.txt"] = defineFile(async ({tabId, inputId}) => {
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 ({tabId, inputId}, buf) => {
const code = `document.getElementById('${inputId}').value = unescape('${escape(buf)}')`;
await browser.tabs.executeScript(tabId, {code});
});
Routes["/tabs/by-title"] = {
getattr() {
return {
st_mode: unix.S_IFDIR | 0777, // writable so you can delete tabs
st_nlink: 3,
st_size: 0,
};
},
async readdir() {
const tabs = await browser.tabs.query({});
return { entries: [".", "..", ...tabs.map(tab => sanitize(String(tab.title)) + "." + String(tab.id))] };
}
};
Routes["/tabs/by-title/:TAB_TITLE.#TAB_ID"] = {
// TODO: date
async readlink({tabId}) { // a symbolic link to /tabs/by-id/[id for this tab]
return { buf: "../by-id/" + tabId };
},
async unlink({tabId}) { // you can delete a by-title/TAB to close that tab
await browser.tabs.remove(tabId);
return {};
}
};
Routes["/tabs/last-focused"] = {
// a symbolic link to /tabs/by-id/[id for this tab]
async readlink() {
const id = (await browser.tabs.query({ active: true, lastFocusedWindow: true }))[0].id;
return { buf: "by-id/" + id };
}
};
Routes["/windows"] = {
async readdir() {
const windows = await browser.windows.getAll();
return { entries: [".", "..", ...windows.map(window => String(window.id))] };
}
};
Routes["/windows/last-focused"] = {
// a symbolic link to /windows/[id for this window]
async readlink() {
const windowId = (await browser.windows.getLastFocused()).id;
return { buf: windowId };
}
};
(function() {
const withWindow = (readHandler, writeHandler) => defineFile(async ({windowId}) => {
const window = await browser.windows.get(windowId);
return readHandler(window);
}, writeHandler ? async ({windowId}, buf) => {
await browser.windows.update(windowId, writeHandler(buf));
} : undefined);
Routes["/windows/#WINDOW_ID/focused"] =
withWindow(window => JSON.stringify(window.focused) + '\n',
buf => ({ focused: buf.startsWith('true') }));
})();
Routes["/windows/#WINDOW_ID/visible-tab.png"] = { ...defineFile(async ({windowId}) => {
// 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 dataUrl = await browser.tabs.captureVisibleTab(windowId, {format: 'png'});
return Uint8Array.from(atob(dataUrl.substr(("data:image/png;base64,").length)),
c => c.charCodeAt(0));
}), async getattr() {
return {
st_mode: unix.S_IFREG | 0444,
st_nlink: 1,
st_size: 10000000 // hard-code to 10MB for now
};
} };
Routes["/extensions"] = {
async readdir() {
const infos = await browser.management.getAll();
return { entries: [".", "..", ...infos.map(info => `${sanitize(info.name)}.${info.id}`)] };
}
};
Routes["/extensions/:EXTENSION_TITLE.:EXTENSION_ID/enabled"] = { ...defineFile(async ({extensionId}) => {
const info = await browser.management.get(extensionId);
return String(info.enabled) + '\n';
}, async ({extensionId}, buf) => {
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 {}; } };
Routes["/runtime/reload"] = {
async write({buf}) {
await browser.runtime.reload();
return {size: stringToUtf8Array(buf).length};
},
truncate() { return {}; }
};
// Ensure that there are routes for all ancestors. This algorithm is
// probably not correct, but whatever. Basically, you need to start at
// the deepest level, fill in all the parents 1 level up that don't
// exist yet, then walk up one level at a time. It's important to go
// one level at a time so you know (for each parent) what all the
// children will be.
for (let i = 10; i >= 0; i--) {
for (let path of Object.keys(Routes).filter(key => key.split("/").length === i)) {
path = path.substr(0, path.lastIndexOf("/"));
if (path == '') path = '/';
if (!Routes[path]) {
function depth(p) { return p === '/' ? 0 : (p.match(/\//g) || []).length; }
// find all direct children
let entries = Object.keys(Routes)
.filter(k => k.startsWith(path) && depth(k) === depth(path) + 1)
.map(k => k.substr((path === '/' ? 0 : path.length) + 1).split('/')[0]);
entries = [".", "..", ...new Set(entries)];
Routes[path] = { readdir() { return { entries }; } };
}
}
// I also think it would be better to compute this stuff on the fly,
// so you could patch more routes in at runtime, but I need to think
// a bit about how to make that work with wildcards.
}
for (let key in Routes) {
// /tabs/by-id/#TAB_ID/url.txt -> RegExp \/tabs\/by-id\/(?<int$TAB_ID>[0-9]+)\/url.txt
Routes[key].__regex = new RegExp(
'^' + key
.split('/')
.map(keySegment => keySegment
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/([#:])([A-Z_]+)/g, (_, sigil, varName) => {
return `(?<${sigil === '#' ? 'int$' : 'string$'}${varName}>` +
(sigil === '#' ? '[0-9]+' : '[^/]+') + `)`;
}))
.join('/') + '$');
Routes[key].__match = function(path) {
const result = Routes[key].__regex.exec(path);
if (!result) { return; }
const vars = {};
for (let [typeAndVarName, value] of Object.entries(result.groups || {})) {
let [type_, varName] = typeAndVarName.split('$');
// TAB_ID -> tabId
varName = varName.toLowerCase();
varName = varName.replace(/_([a-z])/g, c => c[1].toUpperCase());
vars[varName] = type_ === 'int' ? parseInt(value) : value;
}
return vars;
};
// Fill in default implementations of fs ops:
// if readdir -> directory -> add getattr, opendir, releasedir
if (Routes[key].readdir) {
Routes[key] = {
getattr() {
return {
st_mode: unix.S_IFDIR | 0755,
st_nlink: 3,
st_size: 0,
};
},
opendir({path}) { return { fh: 0 }; },
releasedir({path}) { return {}; },
...Routes[key]
};
} else if (Routes[key].readlink) {
Routes[key] = {
async getattr(req) {
const st_size = (await this.readlink(req)).buf.length + 1;
return {
st_mode: unix.S_IFLNK | 0444,
st_nlink: 1,
// You _must_ return correct linkee path length from getattr!
st_size
};
},
...Routes[key]
};
} else if (Routes[key].read || Routes[key].write) {
Routes[key] = {
async getattr() {
return {
st_mode: unix.S_IFREG | ((Routes[key].read && 0444) | (Routes[key].write && 0222)),
st_nlink: 1,
st_size: 100 // FIXME
};
},
open() { return { fh: 0 }; },
release() { return {}; },
...Routes[key]
};
}
}
function tryMatchRoute(path) {
if (path.match(/\/\._[^\/]+$/)) {
// Apple Double ._whatever file for xattrs
throw new UnixError(unix.ENOTSUP);
}
for (let route of Object.values(Routes)) {
const vars = route.__match(path);
if (vars) { return [route, vars]; }
}
throw new UnixError(unix.ENOENT);
}
let port;
async function onMessage(req) {
if (req.buf) req.buf = atob(req.buf);
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({ id: req.id, op: req.op, error: unix.ETIMEDOUT });
}, 1000);
/* console.time(req.op + ':' + req.path);*/
try {
const [route, vars] = tryMatchRoute(req.path);
response = await route[req.op]({...req, ...vars});
response.op = req.op;
if (response.buf) { response.buf = btoa(response.buf); }
} catch (e) {
console.error(e);
response = {
op: req.op,
error: e instanceof UnixError ? e.error : unix.EIO
};
}
/* console.timeEnd(req.op + ':' + req.path);*/
if (!didTimeout) {
clearTimeout(timeout);
console.log('resp', response);
response.id = req.id;
port.postMessage(response);
}
};
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); });
}
if (typeof process === 'object') {
// we're running in node (as part of a test)
// return everything they might want to test
module.exports = {Routes, tryMatchRoute};
} else {
tryConnect();
}