mirror of
https://github.com/osnr/TabFS.git
synced 2024-05-01 21:42:45 +02:00
extension: Big refactor to use named variables in routes.
Also move debugger-specific stuff into the debugger block.
This commit is contained in:
parent
be519afe5f
commit
b6f44d95f4
|
@ -1,5 +1,3 @@
|
||||||
const TESTING = (typeof chrome === 'undefined');
|
|
||||||
|
|
||||||
const unix = {
|
const unix = {
|
||||||
EPERM: 1,
|
EPERM: 1,
|
||||||
ENOENT: 2,
|
ENOENT: 2,
|
||||||
|
@ -23,7 +21,6 @@ const unix = {
|
||||||
// Open flags
|
// Open flags
|
||||||
O_TRUNC: 01000,
|
O_TRUNC: 01000,
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnixError extends Error {
|
class UnixError extends Error {
|
||||||
constructor(error) { super(); this.name = "UnixError"; this.error = error; }
|
constructor(error) { super(); this.name = "UnixError"; this.error = error; }
|
||||||
}
|
}
|
||||||
|
@ -68,61 +65,6 @@ const utf8ArrayToString = (function() {
|
||||||
return utf8 => decoder.decode(utf8);
|
return utf8 => decoder.decode(utf8);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
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); }
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = {};
|
const router = {};
|
||||||
|
|
||||||
const Cache = {
|
const Cache = {
|
||||||
|
@ -151,33 +93,34 @@ const defineFile = (getData, setData) => ({
|
||||||
// show up in ls, etc), given getData and setData functions that
|
// show up in ls, etc), given getData and setData functions that
|
||||||
// define the contents of the entire file.
|
// define the contents of the entire file.
|
||||||
|
|
||||||
// getData: (path: String) -> Promise<contentsOfFile: String|Uint8Array>
|
// getData: (req: Request U Vars) -> Promise<contentsOfFile: String|Uint8Array>
|
||||||
// setData [optional]: (path: String, newContentsOfFile: String) -> Promise<>
|
// setData [optional]: (req: Request U Vars, newContentsOfFile: String) -> Promise<>
|
||||||
|
|
||||||
// You can override file operations (like `truncate` or `getattr`)
|
// You can override file operations (like `truncate` or `getattr`)
|
||||||
// in the returned set if you want different behavior from what's
|
// in the returned set if you want different behavior from what's
|
||||||
// defined here.
|
// defined here.
|
||||||
|
|
||||||
async getattr({path}) {
|
async getattr(req) {
|
||||||
return {
|
return {
|
||||||
st_mode: unix.S_IFREG | 0444 | (setData ? 0222 : 0),
|
st_mode: unix.S_IFREG | 0444 | (setData ? 0222 : 0),
|
||||||
st_nlink: 1,
|
st_nlink: 1,
|
||||||
// you'll want to override this if getData() is slow, because
|
// you'll want to override this if getData() is slow, because
|
||||||
// getattr() gets called a lot more cavalierly than open().
|
// getattr() gets called a lot more cavalierly than open().
|
||||||
st_size: toUtf8Array(await getData(path)).length
|
st_size: toUtf8Array(await getData(req)).length
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
// We call getData() once when the file is opened, then cache that
|
// We call getData() once when the file is opened, then cache that
|
||||||
// data for all subsequent reads from that application.
|
// data for all subsequent reads from that application.
|
||||||
async open({path, flags}) {
|
async open(req) {
|
||||||
const data = !(flags & unix.O_TRUNC) ? await getData(path) : "";
|
const data = !(req.flags & unix.O_TRUNC) ? await getData(req) : "";
|
||||||
return { fh: Cache.storeObject(toUtf8Array(data)) };
|
return { fh: Cache.storeObject(toUtf8Array(data)) };
|
||||||
},
|
},
|
||||||
async read({path, fh, size, offset}) {
|
async read({fh, size, offset}) {
|
||||||
return { buf: String.fromCharCode(...Cache.getObjectForHandle(fh).slice(offset, offset + size)) }
|
return { buf: String.fromCharCode(...Cache.getObjectForHandle(fh).slice(offset, offset + size)) }
|
||||||
},
|
},
|
||||||
async write({path, fh, offset, buf}) {
|
async write(req) {
|
||||||
|
const {fh, offset, buf} = req;
|
||||||
let arr = Cache.getObjectForHandle(fh);
|
let arr = Cache.getObjectForHandle(fh);
|
||||||
const bufarr = stringToUtf8Array(buf);
|
const bufarr = stringToUtf8Array(buf);
|
||||||
if (offset + bufarr.length > arr.length) {
|
if (offset + bufarr.length > arr.length) {
|
||||||
|
@ -190,7 +133,7 @@ const defineFile = (getData, setData) => ({
|
||||||
// I guess caller should override write() if they want to actually
|
// I guess caller should override write() if they want to actually
|
||||||
// patch and not just re-set the whole string (for example,
|
// patch and not just re-set the whole string (for example,
|
||||||
// if they want to hot-reload just one function the user modified)
|
// if they want to hot-reload just one function the user modified)
|
||||||
await setData(path, utf8ArrayToString(arr)); return { size: bufarr.length };
|
await setData(req, utf8ArrayToString(arr)); return { size: bufarr.length };
|
||||||
},
|
},
|
||||||
async release({fh}) { Cache.removeObjectForHandle(fh); return {}; },
|
async release({fh}) { Cache.removeObjectForHandle(fh); return {}; },
|
||||||
|
|
||||||
|
@ -209,13 +152,13 @@ const defineFile = (getData, setData) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
router["/tabs/create"] = {
|
router["/tabs/create"] = {
|
||||||
async write({path, buf}) {
|
async write({buf}) {
|
||||||
const url = buf.trim();
|
const url = buf.trim();
|
||||||
await browser.tabs.create({url});
|
await browser.tabs.create({url});
|
||||||
return {size: stringToUtf8Array(buf).length};
|
return {size: stringToUtf8Array(buf).length};
|
||||||
},
|
},
|
||||||
async truncate({path, size}) { return {}; }
|
async truncate() { return {}; }
|
||||||
}
|
};
|
||||||
|
|
||||||
router["/tabs/by-id"] = {
|
router["/tabs/by-id"] = {
|
||||||
async readdir() {
|
async readdir() {
|
||||||
|
@ -225,17 +168,15 @@ router["/tabs/by-id"] = {
|
||||||
};
|
};
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
const withTab = (readHandler, writeHandler) => defineFile(async path => {
|
const withTab = (readHandler, writeHandler) => defineFile(async ({tabId}) => {
|
||||||
const tabId = parseInt(pathComponent(path, -2));
|
|
||||||
const tab = await browser.tabs.get(tabId);
|
const tab = await browser.tabs.get(tabId);
|
||||||
return readHandler(tab);
|
return readHandler(tab);
|
||||||
|
|
||||||
}, writeHandler ? async (path, buf) => {
|
}, writeHandler ? async ({tabId}, buf) => {
|
||||||
const tabId = parseInt(pathComponent(path, -2));
|
|
||||||
await browser.tabs.update(tabId, writeHandler(buf));
|
await browser.tabs.update(tabId, writeHandler(buf));
|
||||||
} : undefined);
|
} : undefined);
|
||||||
const fromScript = code => defineFile(async path => {
|
|
||||||
const tabId = parseInt(pathComponent(path, -2));
|
const fromScript = code => defineFile(async ({tabId}) => {
|
||||||
return (await browser.tabs.executeScript(tabId, {code}))[0];
|
return (await browser.tabs.executeScript(tabId, {code}))[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -267,22 +208,19 @@ router["/tabs/by-id"] = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
router["/tabs/by-id/#TAB_ID/evals/*"] = {
|
router["/tabs/by-id/#TAB_ID/evals/:FILENAME"] = {
|
||||||
// NOTE: eval runs in extension's content script, not in original page JS context
|
// NOTE: eval runs in extension's content script, not in original page JS context
|
||||||
async mknod({path, mode}) {
|
async mknod({tabId, filename, mode}) {
|
||||||
const [tabId, name] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
|
|
||||||
evals[tabId] = evals[tabId] || {};
|
evals[tabId] = evals[tabId] || {};
|
||||||
evals[tabId][name] = { code: '' };
|
evals[tabId][filename] = { code: '' };
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
async unlink({path}) {
|
async unlink({tabId, filename}) {
|
||||||
const [tabId, name] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
|
delete evals[tabId][filename]; // TODO: also delete evals[tabId] if empty
|
||||||
delete evals[tabId][name]; // TODO: also delete evals[tabId] if empty
|
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
...defineFile(async path => {
|
...defineFile(async ({tabId, filename}) => {
|
||||||
const [tabId, filename] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
|
|
||||||
const name = filename.replace(/\.result$/, '');
|
const name = filename.replace(/\.result$/, '');
|
||||||
if (!evals[tabId] || !(name in evals[tabId])) { throw new UnixError(unix.ENOENT); }
|
if (!evals[tabId] || !(name in evals[tabId])) { throw new UnixError(unix.ENOENT); }
|
||||||
|
|
||||||
|
@ -291,12 +229,12 @@ router["/tabs/by-id"] = {
|
||||||
} else {
|
} else {
|
||||||
return evals[tabId][name].code;
|
return evals[tabId][name].code;
|
||||||
}
|
}
|
||||||
}, async (path, buf) => {
|
}, async ({tabId, filename}, buf) => {
|
||||||
const [tabId, name] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
|
if (filename.endsWith('.result')) {
|
||||||
if (name.endsWith('.result')) {
|
// FIXME: case where they try to write to .result file
|
||||||
// FIXME
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
const name = filename;
|
||||||
evals[tabId][name].code = buf;
|
evals[tabId][name].code = buf;
|
||||||
evals[tabId][name].result = JSON.stringify((await browser.tabs.executeScript(tabId, {code: buf}))[0]) + '\n';
|
evals[tabId][name].result = JSON.stringify((await browser.tabs.executeScript(tabId, {code: buf}))[0]) + '\n';
|
||||||
}
|
}
|
||||||
|
@ -306,8 +244,7 @@ router["/tabs/by-id"] = {
|
||||||
(function() {
|
(function() {
|
||||||
const watches = {};
|
const watches = {};
|
||||||
router["/tabs/by-id/#TAB_ID/watches"] = {
|
router["/tabs/by-id/#TAB_ID/watches"] = {
|
||||||
async readdir({path}) {
|
async readdir({tabId}) {
|
||||||
const tabId = parseInt(pathComponent(path, -2));
|
|
||||||
return { entries: [".", "..", ...Object.keys(watches[tabId] || [])] };
|
return { entries: [".", "..", ...Object.keys(watches[tabId] || [])] };
|
||||||
},
|
},
|
||||||
getattr() {
|
getattr() {
|
||||||
|
@ -318,26 +255,24 @@ router["/tabs/by-id"] = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
router["/tabs/by-id/#TAB_ID/watches/*"] = {
|
router["/tabs/by-id/#TAB_ID/watches/:EXPR"] = {
|
||||||
// NOTE: eval runs in extension's content script, not in original page JS context
|
// NOTE: eval runs in extension's content script, not in original page JS context
|
||||||
async mknod({path, mode}) {
|
async mknod({tabId, expr, mode}) {
|
||||||
const [tabId, expr] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
|
|
||||||
watches[tabId] = watches[tabId] || {};
|
watches[tabId] = watches[tabId] || {};
|
||||||
watches[tabId][expr] = async function() {
|
watches[tabId][expr] = async function() {
|
||||||
return (await browser.tabs.executeScript(tabId, {code: expr}))[0];
|
return (await browser.tabs.executeScript(tabId, {code: expr}))[0];
|
||||||
};
|
};
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
async unlink({path}) {
|
async unlink({tabId, expr}) {
|
||||||
const [tabId, expr] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
|
|
||||||
delete watches[tabId][expr]; // TODO: also delete watches[tabId] if empty
|
delete watches[tabId][expr]; // TODO: also delete watches[tabId] if empty
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
...defineFile(async path => {
|
...defineFile(async ({tabId, expr}) => {
|
||||||
const [tabId, expr] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1)];
|
|
||||||
if (!watches[tabId] || !(expr in watches[tabId])) { throw new UnixError(unix.ENOENT); }
|
if (!watches[tabId] || !(expr in watches[tabId])) { throw new UnixError(unix.ENOENT); }
|
||||||
return JSON.stringify(await watches[tabId][expr]()) + '\n';
|
return JSON.stringify(await watches[tabId][expr]()) + '\n';
|
||||||
|
|
||||||
}, () => {
|
}, () => {
|
||||||
// setData handler -- only providing this so that getattr reports
|
// setData handler -- only providing this so that getattr reports
|
||||||
// that the file is writable, so it can be deleted without annoying prompt.
|
// that the file is writable, so it can be deleted without annoying prompt.
|
||||||
|
@ -348,40 +283,92 @@ router["/tabs/by-id"] = {
|
||||||
|
|
||||||
router["/tabs/by-id/#TAB_ID/window"] = {
|
router["/tabs/by-id/#TAB_ID/window"] = {
|
||||||
// a symbolic link to /windows/[id for this window]
|
// a symbolic link to /windows/[id for this window]
|
||||||
async readlink({path}) {
|
async readlink({tabId}) {
|
||||||
const tabId = parseInt(pathComponent(path, -2)); const tab = await browser.tabs.get(tabId);
|
const tab = await browser.tabs.get(tabId);
|
||||||
return { buf: "../../../windows/" + tab.windowId };
|
return { buf: "../../../windows/" + tab.windowId };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
router["/tabs/by-id/#TAB_ID/control"] = {
|
router["/tabs/by-id/#TAB_ID/control"] = {
|
||||||
// echo remove > mnt/tabs/by-id/1644/control
|
// echo remove > mnt/tabs/by-id/1644/control
|
||||||
async write({path, buf}) {
|
async write({tabId, buf}) {
|
||||||
const tabId = parseInt(pathComponent(path, -2));
|
|
||||||
const command = buf.trim();
|
const command = buf.trim();
|
||||||
// can use `discard`, `remove`, `reload`, `goForward`, `goBack`...
|
// can use `discard`, `remove`, `reload`, `goForward`, `goBack`...
|
||||||
// see https://developer.chrome.com/extensions/tabs
|
// see https://developer.chrome.com/extensions/tabs
|
||||||
await browser.tabs[command](tabId);
|
await browser.tabs[command](tabId);
|
||||||
return {size: stringToUtf8Array(buf).length};
|
return {size: stringToUtf8Array(buf).length};
|
||||||
},
|
},
|
||||||
async truncate({path, size}) { return {}; }
|
async truncate({size}) { return {}; }
|
||||||
};
|
};
|
||||||
// debugger/ : debugger-API-dependent (Chrome-only)
|
// debugger/ : debugger-API-dependent (Chrome-only)
|
||||||
(function() {
|
(function() {
|
||||||
if (!chrome.debugger) return;
|
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)
|
// possible idea: console (using Log API instead of monkey-patching)
|
||||||
// resources/
|
// resources/
|
||||||
// TODO: scripts/ TODO: allow creation, eval immediately
|
// TODO: scripts/ TODO: allow creation, eval immediately
|
||||||
|
|
||||||
router["/tabs/by-id/#TAB_ID/debugger/resources"] = {
|
router["/tabs/by-id/#TAB_ID/debugger/resources"] = {
|
||||||
async readdir({path}) {
|
async readdir({tabId}) {
|
||||||
const tabId = parseInt(pathComponent(path, -3));
|
|
||||||
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
|
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
|
||||||
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
|
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
|
||||||
return { entries: [".", "..", ...frameTree.resources.map(r => sanitize(String(r.url)))] };
|
return { entries: [".", "..", ...frameTree.resources.map(r => sanitize(String(r.url)))] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
router["/tabs/by-id/#TAB_ID/debugger/resources/*"] = defineFile(async path => {
|
router["/tabs/by-id/#TAB_ID/debugger/resources/:SUFFIX"] = defineFile(async ({path, tabId}) => {
|
||||||
const [tabId, suffix] = [parseInt(pathComponent(path, -4)), pathComponent(path, -1)];
|
|
||||||
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
|
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Page");
|
||||||
|
|
||||||
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
|
const {frameTree} = await sendDebuggerCommand(tabId, "Page.getResourceTree", {});
|
||||||
|
@ -399,15 +386,13 @@ router["/tabs/by-id/#TAB_ID/control"] = {
|
||||||
throw new UnixError(unix.ENOENT);
|
throw new UnixError(unix.ENOENT);
|
||||||
});
|
});
|
||||||
router["/tabs/by-id/#TAB_ID/debugger/scripts"] = {
|
router["/tabs/by-id/#TAB_ID/debugger/scripts"] = {
|
||||||
async opendir({path}) {
|
async opendir({tabId}) {
|
||||||
const tabId = parseInt(pathComponent(path, -3));
|
|
||||||
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
|
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
|
||||||
return { fh: 0 };
|
return { fh: 0 };
|
||||||
},
|
},
|
||||||
async readdir({path}) {
|
async readdir({tabId}) {
|
||||||
const tabId = parseInt(pathComponent(path, -3));
|
// it's useful to put the ID first in the script filenames, so
|
||||||
// it's useful to put the ID first so the .js extension stays on
|
// the .js extension stays on the end
|
||||||
// the end
|
|
||||||
const scriptFileNames = Object.values(TabManager.scriptsForTab[tabId])
|
const scriptFileNames = Object.values(TabManager.scriptsForTab[tabId])
|
||||||
.map(params => params.scriptId + "_" + sanitize(params.url));
|
.map(params => params.scriptId + "_" + sanitize(params.url));
|
||||||
return { entries: [".", "..", ...scriptFileNames] };
|
return { entries: [".", "..", ...scriptFileNames] };
|
||||||
|
@ -421,8 +406,7 @@ router["/tabs/by-id/#TAB_ID/control"] = {
|
||||||
}
|
}
|
||||||
return scriptInfo;
|
return scriptInfo;
|
||||||
}
|
}
|
||||||
router["/tabs/by-id/#TAB_ID/debugger/scripts/*"] = defineFile(async path => {
|
router["/tabs/by-id/#TAB_ID/debugger/scripts/:SUFFIX"] = defineFile(async ({path, tabId, suffix}) => {
|
||||||
const [tabId, suffix] = [parseInt(pathComponent(path, -4)), pathComponent(path, -1)];
|
|
||||||
await TabManager.debugTab(tabId);
|
await TabManager.debugTab(tabId);
|
||||||
await TabManager.enableDomainForTab(tabId, "Page");
|
await TabManager.enableDomainForTab(tabId, "Page");
|
||||||
await TabManager.enableDomainForTab(tabId, "Debugger");
|
await TabManager.enableDomainForTab(tabId, "Debugger");
|
||||||
|
@ -431,8 +415,7 @@ router["/tabs/by-id/#TAB_ID/control"] = {
|
||||||
const {scriptSource} = await sendDebuggerCommand(tabId, "Debugger.getScriptSource", {scriptId});
|
const {scriptSource} = await sendDebuggerCommand(tabId, "Debugger.getScriptSource", {scriptId});
|
||||||
return scriptSource;
|
return scriptSource;
|
||||||
|
|
||||||
}, async (path, buf) => {
|
}, async ({path, tabId, suffix}, buf) => {
|
||||||
const [tabId, suffix] = [parseInt(pathComponent(path, -4)), pathComponent(path, -1)];
|
|
||||||
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
|
await TabManager.debugTab(tabId); await TabManager.enableDomainForTab(tabId, "Debugger");
|
||||||
|
|
||||||
const {scriptId} = pathScriptInfo(tabId, path);
|
const {scriptId} = pathScriptInfo(tabId, path);
|
||||||
|
@ -450,14 +433,13 @@ router["/tabs/by-id/#TAB_ID/inputs"] = {
|
||||||
return { entries: [".", "..", ...ids.map(id => `${id}.txt`)] };
|
return { entries: [".", "..", ...ids.map(id => `${id}.txt`)] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
router["/tabs/by-id/#TAB_ID/inputs/*"] = defineFile(async path => {
|
router["/tabs/by-id/#TAB_ID/inputs/:INPUT_ID.txt"] = defineFile(async ({path, tabId, inputId}) => {
|
||||||
const [tabId, inputId] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1).slice(0, -4)];
|
|
||||||
const code = `document.getElementById('${inputId}').value`;
|
const code = `document.getElementById('${inputId}').value`;
|
||||||
const inputValue = (await browser.tabs.executeScript(tabId, {code}))[0];
|
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 */
|
if (inputValue === null) { throw new UnixError(unix.ENOENT); } /* FIXME: hack to deal with if inputId isn't valid */
|
||||||
return inputValue;
|
return inputValue;
|
||||||
}, async (path, buf) => {
|
|
||||||
const [tabId, inputId] = [parseInt(pathComponent(path, -3)), pathComponent(path, -1).slice(0, -4)];
|
}, async ({path, tabId, inputId}, buf) => {
|
||||||
const code = `document.getElementById('${inputId}').value = unescape('${escape(buf)}')`;
|
const code = `document.getElementById('${inputId}').value = unescape('${escape(buf)}')`;
|
||||||
await browser.tabs.executeScript(tabId, {code});
|
await browser.tabs.executeScript(tabId, {code});
|
||||||
});
|
});
|
||||||
|
@ -472,24 +454,22 @@ router["/tabs/by-title"] = {
|
||||||
},
|
},
|
||||||
async readdir() {
|
async readdir() {
|
||||||
const tabs = await browser.tabs.query({});
|
const tabs = await browser.tabs.query({});
|
||||||
return { entries: [".", "..", ...tabs.map(tab => sanitize(String(tab.title)) + "_" + String(tab.id))] };
|
return { entries: [".", "..", ...tabs.map(tab => sanitize(String(tab.title)) + "." + String(tab.id))] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
router["/tabs/by-title/*"] = {
|
router["/tabs/by-title/:TAB_TITLE.#TAB_ID"] = {
|
||||||
// TODO: date
|
// TODO: date
|
||||||
async readlink({path}) { // a symbolic link to /tabs/by-id/[id for this tab]
|
async readlink({tabId}) { // a symbolic link to /tabs/by-id/[id for this tab]
|
||||||
const parts = path.split("_"); const tabId = parts[parts.length - 1];
|
|
||||||
return { buf: "../by-id/" + tabId };
|
return { buf: "../by-id/" + tabId };
|
||||||
},
|
},
|
||||||
async unlink({path}) { // you can delete a by-title/TAB to close that tab
|
async unlink({tabId}) { // 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);
|
await browser.tabs.remove(tabId);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
router["/tabs/last-focused"] = {
|
router["/tabs/last-focused"] = {
|
||||||
// a symbolic link to /tabs/by-id/[id for this tab]
|
// a symbolic link to /tabs/by-id/[id for this tab]
|
||||||
async readlink({path}) {
|
async readlink() {
|
||||||
const id = (await browser.tabs.query({ active: true, lastFocusedWindow: true }))[0].id;
|
const id = (await browser.tabs.query({ active: true, lastFocusedWindow: true }))[0].id;
|
||||||
return { buf: "by-id/" + id };
|
return { buf: "by-id/" + id };
|
||||||
}
|
}
|
||||||
|
@ -503,35 +483,33 @@ router["/windows"] = {
|
||||||
};
|
};
|
||||||
router["/windows/last-focused"] = {
|
router["/windows/last-focused"] = {
|
||||||
// a symbolic link to /windows/[id for this window]
|
// a symbolic link to /windows/[id for this window]
|
||||||
async readlink({path}) {
|
async readlink() {
|
||||||
const windowId = (await browser.windows.getLastFocused()).id;
|
const windowId = (await browser.windows.getLastFocused()).id;
|
||||||
return { buf: windowId };
|
return { buf: windowId };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
(function() {
|
(function() {
|
||||||
const withWindow = (readHandler, writeHandler) => defineFile(async path => {
|
const withWindow = (readHandler, writeHandler) => defineFile(async ({windowId}) => {
|
||||||
const windowId = parseInt(pathComponent(path, -2));
|
|
||||||
const window = await browser.windows.get(windowId);
|
const window = await browser.windows.get(windowId);
|
||||||
return readHandler(window);
|
return readHandler(window);
|
||||||
|
|
||||||
}, writeHandler ? async (path, buf) => {
|
}, writeHandler ? async ({windowId}, buf) => {
|
||||||
const windowId = parseInt(pathComponent(path, -2));
|
|
||||||
await browser.windows.update(windowId, writeHandler(buf));
|
await browser.windows.update(windowId, writeHandler(buf));
|
||||||
} : undefined);
|
} : undefined);
|
||||||
|
|
||||||
router["/windows/*/focused"] = withWindow(window => JSON.stringify(window.focused) + '\n',
|
router["/windows/#WINDOW_ID/focused"] =
|
||||||
buf => ({ focused: buf.startsWith('true') }));
|
withWindow(window => JSON.stringify(window.focused) + '\n',
|
||||||
|
buf => ({ focused: buf.startsWith('true') }));
|
||||||
})();
|
})();
|
||||||
router["/windows/*/visible-tab.png"] = { ...defineFile(async path => {
|
router["/windows/#WINDOW_ID/visible-tab.png"] = { ...defineFile(async ({windowId}) => {
|
||||||
// screen capture is a window thing and not a tab 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 only capture the visible tab for each window anyway; you
|
||||||
// can't take a screenshot of just any arbitrary tab
|
// can't take a screenshot of just any arbitrary tab
|
||||||
const windowId = parseInt(pathComponent(path, -2));
|
|
||||||
const dataUrl = await browser.tabs.captureVisibleTab(windowId, {format: 'png'});
|
const dataUrl = await browser.tabs.captureVisibleTab(windowId, {format: 'png'});
|
||||||
return Uint8Array.from(atob(dataUrl.substr(("data:image/png;base64,").length)),
|
return Uint8Array.from(atob(dataUrl.substr(("data:image/png;base64,").length)),
|
||||||
c => c.charCodeAt(0));
|
c => c.charCodeAt(0));
|
||||||
|
|
||||||
}), async getattr({path}) {
|
}), async getattr() {
|
||||||
return {
|
return {
|
||||||
st_mode: unix.S_IFREG | 0444,
|
st_mode: unix.S_IFREG | 0444,
|
||||||
st_nlink: 1,
|
st_nlink: 1,
|
||||||
|
@ -543,29 +521,27 @@ router["/windows/*/visible-tab.png"] = { ...defineFile(async path => {
|
||||||
router["/extensions"] = {
|
router["/extensions"] = {
|
||||||
async readdir() {
|
async readdir() {
|
||||||
const infos = await browser.management.getAll();
|
const infos = await browser.management.getAll();
|
||||||
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/:EXTENSION_TITLE.:EXTENSION_ID/enabled"] = { ...defineFile(async ({extensionId}) => {
|
||||||
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 ({extensionId}, buf) => {
|
||||||
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 >`
|
// suppress truncate so it doesn't accidentally flip the state when you do, e.g., `echo true >`
|
||||||
}), truncate() { return {}; } };
|
}), truncate() { return {}; } };
|
||||||
|
|
||||||
router["/runtime/reload"] = {
|
router["/runtime/reload"] = {
|
||||||
async write({path, buf}) {
|
async write({buf}) {
|
||||||
await browser.runtime.reload();
|
await browser.runtime.reload();
|
||||||
return {size: stringToUtf8Array(buf).length};
|
return {size: stringToUtf8Array(buf).length};
|
||||||
},
|
},
|
||||||
truncate() { return {}; }
|
truncate() { return {}; }
|
||||||
};
|
};
|
||||||
router["/runtime/background.js.html"] = defineFile(async path => {
|
router["/runtime/background.js.html"] = defineFile(async () => {
|
||||||
const js = await window.fetch(chrome.runtime.getURL('background.js'))
|
const js = await window.fetch(chrome.runtime.getURL('background.js'))
|
||||||
.then(r => r.text());
|
.then(r => r.text());
|
||||||
return `
|
return `
|
||||||
|
@ -613,14 +589,15 @@ for (let i = 10; i >= 0; i--) {
|
||||||
|
|
||||||
|
|
||||||
for (let key in router) {
|
for (let key in router) {
|
||||||
// /tabs/by-id/#TAB_ID/url.txt -> RegExp \/tabs\/by-id\/(?<int$TAB_ID>[^/]+)\/url.txt
|
// /tabs/by-id/#TAB_ID/url.txt -> RegExp \/tabs\/by-id\/(?<int$TAB_ID>[0-9]+)\/url.txt
|
||||||
router[key].__regex = new RegExp(
|
router[key].__regex = new RegExp(
|
||||||
'^' + key
|
'^' + key
|
||||||
.split('/')
|
.split('/')
|
||||||
.map(keySegment => keySegment
|
.map(keySegment => keySegment
|
||||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
.replace(/([#:])([A-Z_]+)/g, (_, sigil, varName) => {
|
.replace(/([#:])([A-Z_]+)/g, (_, sigil, varName) => {
|
||||||
return `(?<${sigil === '#' ? 'int$' : 'string$'}${varName}>[^/]+)`;
|
return `(?<${sigil === '#' ? 'int$' : 'string$'}${varName}>` +
|
||||||
|
(sigil === '#' ? '[0-9]+' : '[^/]+') + `)`;
|
||||||
}))
|
}))
|
||||||
.join('/') + '$');
|
.join('/') + '$');
|
||||||
|
|
||||||
|
@ -629,7 +606,7 @@ for (let key in router) {
|
||||||
if (!result) { return; }
|
if (!result) { return; }
|
||||||
|
|
||||||
const vars = {};
|
const vars = {};
|
||||||
for (let [typeAndVarName, value] of Object.entries(result.groups)) {
|
for (let [typeAndVarName, value] of Object.entries(result.groups || {})) {
|
||||||
let [type_, varName] = typeAndVarName.split('$');
|
let [type_, varName] = typeAndVarName.split('$');
|
||||||
// TAB_ID -> tabId
|
// TAB_ID -> tabId
|
||||||
varName = varName.toLowerCase();
|
varName = varName.toLowerCase();
|
||||||
|
@ -658,8 +635,8 @@ for (let key in router) {
|
||||||
|
|
||||||
} else if (router[key].readlink) {
|
} else if (router[key].readlink) {
|
||||||
router[key] = {
|
router[key] = {
|
||||||
async getattr({path}) {
|
async getattr(req) {
|
||||||
const st_size = (await this.readlink({path})).buf.length + 1;
|
const st_size = (await this.readlink(req)).buf.length + 1;
|
||||||
return {
|
return {
|
||||||
st_mode: unix.S_IFLNK | 0444,
|
st_mode: unix.S_IFLNK | 0444,
|
||||||
st_nlink: 1,
|
st_nlink: 1,
|
||||||
|
@ -686,13 +663,15 @@ for (let key in router) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findRoute(path) {
|
function tryMatchRoute(path) {
|
||||||
if (path.match(/\/\._[^\/]+$/)) {
|
if (path.match(/\/\._[^\/]+$/)) {
|
||||||
throw new UnixError(unix.ENOTSUP); // Apple Double file for xattrs
|
// Apple Double ._whatever file for xattrs
|
||||||
|
throw new UnixError(unix.ENOTSUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let route of Object.values(router)) {
|
for (let route of Object.values(router)) {
|
||||||
if (route.__match(path)) { return route; }
|
const vars = route.__match(path);
|
||||||
|
if (vars) { return [route, vars]; }
|
||||||
}
|
}
|
||||||
throw new UnixError(unix.ENOENT);
|
throw new UnixError(unix.ENOENT);
|
||||||
}
|
}
|
||||||
|
@ -712,7 +691,8 @@ async function onMessage(req) {
|
||||||
|
|
||||||
/* console.time(req.op + ':' + req.path);*/
|
/* console.time(req.op + ':' + req.path);*/
|
||||||
try {
|
try {
|
||||||
response = await findRoute(req.path)[req.op](req);
|
const [route, vars] = tryMatchRoute(req.path);
|
||||||
|
response = await route[req.op]({...req, ...vars});
|
||||||
response.op = req.op;
|
response.op = req.op;
|
||||||
if (response.buf) { response.buf = btoa(response.buf); }
|
if (response.buf) { response.buf = btoa(response.buf); }
|
||||||
|
|
||||||
|
@ -775,7 +755,7 @@ function tryConnect() {
|
||||||
if (typeof process === 'object') {
|
if (typeof process === 'object') {
|
||||||
// we're running in node (as part of a test)
|
// we're running in node (as part of a test)
|
||||||
// return everything they might want to test
|
// return everything they might want to test
|
||||||
module.exports = {router, findRoute};
|
module.exports = {router, tryMatchRoute};
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
tryConnect();
|
tryConnect();
|
||||||
|
|
10
test/test.js
10
test/test.js
|
@ -3,7 +3,7 @@ const assert = require('assert');
|
||||||
// mock chrome namespace
|
// mock chrome namespace
|
||||||
global.chrome = {};
|
global.chrome = {};
|
||||||
// run background.js
|
// run background.js
|
||||||
const {router, findRoute} = require('../extension/background');
|
const {router, tryMatchRoute} = require('../extension/background');
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const tabRoute = await router['/tabs/by-id/#TAB_ID'].readdir();
|
const tabRoute = await router['/tabs/by-id/#TAB_ID'].readdir();
|
||||||
|
@ -15,7 +15,9 @@ const {router, findRoute} = require('../extension/background');
|
||||||
assert.deepEqual(await router['/tabs'].readdir(),
|
assert.deepEqual(await router['/tabs'].readdir(),
|
||||||
{ entries: ['.', '..', 'create',
|
{ entries: ['.', '..', 'create',
|
||||||
'by-id', 'by-title', 'last-focused'] });
|
'by-id', 'by-title', 'last-focused'] });
|
||||||
|
|
||||||
assert.deepEqual(findRoute('/tabs/by-id/10/url.txt'),
|
assert.deepEqual(tryMatchRoute('/'), [router['/'], {}]);
|
||||||
router['/tabs/by-id/#TAB_ID/url.txt']);
|
|
||||||
|
assert.deepEqual(tryMatchRoute('/tabs/by-id/10/url.txt'),
|
||||||
|
[router['/tabs/by-id/#TAB_ID/url.txt'], {tabId: 10}]);
|
||||||
})();
|
})();
|
||||||
|
|
Loading…
Reference in a new issue