Skip to content

Commit

Permalink
automatically detect --chrome-data-dir, load ffi only on demand,
Browse files Browse the repository at this point in the history
  • Loading branch information
NiklasGollenstede committed Oct 12, 2017
1 parent 0e59197 commit 4ebb8ff
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 41 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ NativeExt can automatically update its manifest, but the ids of the allowed exte
```
The name of your file should be your extensions name/id/domain or that of its developing organization and should probably not contain spaces.


### Building

Building NativeExt requires node.js v8+ and npm. After cloning or downloading the sources, install the dependencies with:
Expand Down
56 changes: 43 additions & 13 deletions browser.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use strict'; module.exports = version => { const browser = { };
'use strict'; module.exports = async ({ version, port, }) => { const browser = { };

/**
* This module collects and exposes information about the connecting browser and extension.
* It is available as 'browser' for the extension modules.
*/

const FS = require('fs'), Path = require('path');
const FS = require('fs'), Path = require('path'), { promisify, } = require('util');
const realpath = promisify(FS.realpath);

void version; // nothing version specific so far

Expand Down Expand Up @@ -65,20 +66,49 @@ function getBrowserPid() { switch (process.platform) {
switch (browser.name) {
case 'chromium': case 'chrome': {
const extId = browser.extId = (/^chrome-extension:\/\/(.*)\/?$/).exec(process.argv[4])[1];
const pid = browser.pid;
console.info({ extId, pid, }, getBrowserArgs());
throw new Error(`Not implemented`);
// defaults: https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md
// besides `--user-data-dir=./test_dir` the cli
// can contain `--user-data-dir=".\test dir"` or `"--user-data-dir=.\test dir"` on windows
// and (probably) also `--user-data-dir=./test\ dir` on mac/linux
}
const { cwd, args, } = getBrowserArgs();
console.info({ cwd, args, });
const arg = args.find(arg => (/^"?--user-data-dir=/).test(arg)); // TODO: does /user-data-dir= work as well (on windows?)
let cdd = arg && arg.replace(/^--user-data-dir=/, '').replace(/"/g, '').replace(/\\ /g, ' ');
if (cdd && !Path.isAbsolute(cdd)) { throw new Error(`Chrome was started with the --user-data-dir argument, but the path (${cdd}) was not absolute. To use NativeExt, please supply an absolute path!`); }
if (!cdd) { switch (process.platform) { // use default, currently ignores Chrome Beta/Dev/Canary
case 'win32': {
cdd = Path.join(process.env.LOCALAPPDATA, String.raw`Google\Chrome\User Data`);
} break;
case 'linux': {
if (process.env.CHROME_USER_DATA_DIR) { return Path.resolve(process.env.CHROME_USER_DATA_DIR); }
const config = (process.env.CHROME_CONFIG_HOME || process.env.XDG_CONFIG_HOME || '~/.config').replace(/^~(?=[\\\/])/, () => require('os').homedir());
cdd = Path.join(config, browser.name === 'chromium' ? 'chromium' : 'google-chrome');
} break;
case 'darwin': {
cdd = Path.join(require('os').homedir(), 'Library/Application Support', browser.name === 'chromium' ? 'Chromium' : 'Google/Chrome');
} break;
default: throw new Error(`Unknown OS ${ process.platform }`);
} }
try { FS.statSync(cdd); } catch (error) { throw new Error(`Failed to locate the chrome data dir, deducted "${cdd}" but that doesn't exist`); }

try { browser.extDir = FS.realpathSync(Path.join(cdd, 'Default', 'Extensions', extId)); browser.profileDir = Path.join(cdd, 'Default'); }
catch (error) { // the extension is not installed in the default profile.
// This can have two causes: (1) the extension is installed as a temporary extension; (2) the current profile is not 'Default'
// The former should only happen to developers (who should have read the docs), so this only handles the second case:
const profile = (await port.request('init.getChromeProfileDirName'));
try { FS.accessSync(Path.join(cdd, profile)); } catch (error) { throw new Error(`The Profile "${profile}" does not exist in "${cdd}"`); }
try { browser.extDir = FS.realpathSync(Path.join(cdd, profile, 'Extensions', extId)); browser.profileDir = Path.join(cdd, profile); }
catch (error) { throw new Error(`The extension ${extId} is not installed in ${ Path.join(cdd, profile) }. (Read the docs for unpacked extensions)`); }
}
} break;
case 'firefox': {
const extId = browser.extId = process.argv[5];
if (process.env.MOZ_CRASHREPORTER_EVENTS_DIRECTORY) {
const extPath = FS.realpathSync(Path.resolve(process.env.MOZ_CRASHREPORTER_EVENTS_DIRECTORY, '../../extensions', extId +'.xpi'));
let stat; try { stat = FS.statSync(extPath); } catch (error) { throw new Error(`Can't access extension at ${ extPath }`); }
browser[stat.isDirectory() ? 'extDir' : 'extFile'] = extPath;
browser.profileDir = Path.resolve(process.env.MOZ_CRASHREPORTER_EVENTS_DIRECTORY, '../..');
const extPath = Path.join(browser.profileDir, 'extensions', extId);
const [ extDir, extFile, ] = (await Promise.all([
realpath(extPath).catch(() => null),
realpath(extPath +'.xpi').catch(() => null),
]));
if (extDir && FS.statSync(extDir).isDirectory()) { browser.extDir = extDir; }
else if (extFile && FS.statSync(extFile).isFile()) { browser.extFile = extFile; }
else { throw new Error(`Can't locate extension at ${ extPath }(.xpi)`); }
} else {
throw new Error(`MOZ_CRASHREPORTER_EVENTS_DIRECTORY environment variable not set by Firefox`);
// const args = getBrowserArgs();
Expand Down
69 changes: 51 additions & 18 deletions connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ const FS = Object.assign({ }, require('fs')), Path = require('path'), _package =


// set up communication
const Port = require('multiport'), port = new Port(
const Port = require('./node_modules/multiport/index.js'), port = new Port(
new (require('./runtime-port.js'))(process.stdin, process.stdout),
Port.web_ext_Port,
);
// { const { stdin, } = process; stdin.pause(); ready.then(() => stdin.resume()); }


{ // can't log to stdio if started by the browser ==> forward to browser console
Expand Down Expand Up @@ -46,32 +45,66 @@ let protocol; { // protocol negotiation
}


const modules = { __proto__: null, }; { // extend require
const modules = { __proto__: null, }; let originalRequireResolve; { // extend require
const Module = require('module'), { _resolveFilename, _load, } = Module;
Module._resolveFilename = function(path, mod) {
originalRequireResolve = _resolveFilename;
Module._resolveFilename = function(path) {
if (path in modules) { return path; }
return _resolveFilename(path, mod);
return _resolveFilename.apply(Module, arguments);
};
Module._load = function(path) {
if (path in modules) { return modules[path]; }
return _load.apply(null, arguments);
return _load.apply(Module, arguments);
};
} function exposeLazy(name, getter) {
Object.defineProperty(modules, name, { configurable: true, enumerable: true, get() {
const value = getter();
Object.defineProperty(modules, name, { value, });
return value;
}, });
}


{ // load and expose 'ffi' and 'ref'
require('bindings'); const module = require.cache[require.resolve('bindings')], { exports, } = module;
[ 'ref', 'ffi', ].forEach(name => {
module.exports = () => module.require(Path.join(process.cwd(), `res/${name}.node`));
modules[name] = module.require(name);
});
module.exports = exports;
modules['ref-array'] = require('ref-array'); modules['ref-struct'] = require('ref-struct');
{ // expose and lazy load 'ffi' and 'ref'
const cwd = process.cwd(), bindingsPath = require.resolve('bindings'); let bindingsModule;

exposeLazy('ffi', makeLazyLoader('ffi', () => void modules.ref));
exposeLazy('ref', makeLazyLoader('ref'));
exposeLazy('ref-array', () => requireClean('ref-array'));
exposeLazy('ref-struct', () => requireClean('ref-struct'));

function makeLazyLoader(name, precond) { return () => {
precond && precond();
if (!bindingsModule) { require(bindingsPath); bindingsModule = require.cache[bindingsPath]; }
const bindingsExports = bindingsModule.exports;
const nodePath = Path.join(cwd, `res/${name}.node`);
bindingsModule.exports = () => module.require(nodePath);
let exports; try {
exports = requireClean(name);
} finally {
bindingsModule.exports = bindingsExports;
delete require.cache[nodePath];
delete require.cache[bindingsPath];
} return exports;
}; }

function requireClean(id) { // use local require and original cwd, restore and cleanup afterwards
let exports, currentCwd; try {
currentCwd = process.cwd(); process.chdir(cwd);
const fullPath = originalRequireResolve(id, module);
exports = require(fullPath);
(function clear(module) {
delete require.cache[module.filename] && module.children.forEach(clear);
})(require.cache[fullPath]);
} finally {
process.chdir(currentCwd);
} return exports;
}
}


// (lazily) load and expose 'browser' infos
const browser = modules.browser = require('./browser.js')(protocol);
// load and expose 'browser' infos
const browser = modules.browser = (await require('./browser.js')({ versions: protocol, port, }));


// set up file system
Expand Down Expand Up @@ -122,7 +155,7 @@ const extRoot = Path.resolve('/webext/'); let extDir; {
{
const Module = require('module'), { _findPath, } = Module;
Module._findPath = function(path) {
path = _findPath(path);
path = _findPath.apply(Module, arguments);
if (path && path.startsWith(extDir) && (path.length === extDir.length || path[extDir.length] === '\\' || path[extDir.length] === '/'))
{ path = extRoot + path.slice(extDir.length); }
return path;
Expand All @@ -135,7 +168,7 @@ const extRoot = Path.resolve('/webext/'); let extDir; {
process.versions.native_ext = _package.version;
process.argv.splice(0, Infinity);
process.mainModule.filename = extRoot + Path.sep +'.';
process.mainModule.paths = module.constructor._nodeModulePaths(extRoot + Path.sep +'.');
process.mainModule.paths = module.constructor._nodeModulePaths(process.mainModule.filename);
process.mainModule.exports = null;
process.mainModule.children.splice(0, Infinity);
process.chdir(extDir);
Expand Down
33 changes: 24 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/* eslint-disable strict */ (function(global) { 'use strict'; define(async ({ /* global define, */ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
'node_modules/web-ext-utils/browser/': { runtime, rootUrl, },
'node_modules/web-ext-utils/browser/': { runtime, rootUrl, manifest, Storage: { local: Storage, }, },
'node_modules/web-ext-utils/lib/multiport/': Port,
'node_modules/web-ext-utils/utils/event': { setEvent, },
exports,
}) => {
}) => { const Native = { };

let channel = null; const refs = new WeakMap, cache = { __proto__: null, };
let channel = null; const refs = new Map, cache = { __proto__: null, };

const fireError = setEvent(exports, 'onUncaughtException', { lazy: false, });
const fireReject = setEvent(exports, 'onUnhandledRejection', { lazy: false, });
Object.defineProperty(Native, 'options', { value: { __proto__: null, }, enumerable: true, });
const fireError = setEvent(Native, 'onUncaughtException', { lazy: false, });
const fireReject = setEvent(Native, 'onUnhandledRejection', { lazy: false, });

function connect() {
if (channel) { return channel.port; }
Expand All @@ -22,7 +22,7 @@ function connect() {
}); port.ended.then(disconnect);

// setup
port.addHandlers('init.', { }); // for future requests
port.addHandlers('init.', initHandlers);
port.inited = port.request('init', { versions: [ '0.2', ], }).then(version => (port.version = version));

// handle messages
Expand All @@ -38,7 +38,7 @@ function connect() {

function disconnect() {
if (!channel) { return; } const { port, } = channel, { error, } = port; channel = null; port.destroy();
refs.forEach(obj => { try { refs.delete(obj); obj.doClose(); obj.onDisconnect && obj.onDisconnect(error); } catch (error) { console.error(error); } });
refs.forEach(obj => { if (obj) { try { obj.doClose(); obj.onDisconnect && obj.onDisconnect(error); } catch (error) { console.error(error); } } }); refs.clear();
Object.keys(cache).forEach(key => delete cache[key]);
}

Expand Down Expand Up @@ -86,7 +86,22 @@ function unref(ref) {
return true;
}

Object.assign(exports, {
const initHandlers = {
async getChromeProfileDirName() {
if (typeof Native.options.getChromeProfileDirName === 'function') { return Native.options.getChromeProfileDirName(); }
const key = '__native-ext__.chromeProfileDirName';
const { [key]: stored, } = (await Storage.get(key)); if (stored) { return stored; }
const maybe = global.prompt( // prompt doesn't really work for this because it blocks the browser
`To communicate with your OS to enable some advanced features, ${manifest.name} needs to know the name of your Chrome profile directory.
Please paste the path left of "Profile Path" on "chrome://version/" below:`.replace(/^\s+/gm, ''),
'',
).replace(/^.*[\\\/]/, '');
channel.port.inited.then(() => { console.log('saving profile'); Storage.set({ [key]: maybe, }); }); // save if connection succeeds
return maybe;
},
};

return Object.assign(Native, {
require, unref, nuke: disconnect,
get version() { return channel && channel.port.version; },
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"eslintrc": "NiklasGollenstede/eslintrc",
"extract-zip": "1.6.5",
"ffi": "2.2.0",
"multiport": "0.2.1",
"multiport": "0.2.2",
"pkg": "4.2.3",
"ref-array": "^1.2.0",
"ref-struct": "^1.1.0",
Expand Down

0 comments on commit 4ebb8ff

Please sign in to comment.