Merge pull request #374 from hashicorp/chore-ipc

Chore ipc
This commit is contained in:
Susmitha Girumala authored and GitHub committed 2020-11-10 16:36:48 -05:00
commit e05d19f814
12 files changed
+1340 -876

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class IndexRoute extends Route {
// =services
@service ipc;
// =methods
/**
* Invokes an IPC method and returns the result.
* @return {Promise}
*/
model() {
return this.ipc.invoke('getFoobars', {
id: '1'
});
}
}
+150
View File
@@ -0,0 +1,150 @@
import Service from '@ember/service';
import { getOwner } from '@ember/application';
import { Promise } from 'rsvp';
import config from '../config/environment';
/**
* An IPC request is a promise-like object that issues a request via
* `window.postMessage` and resolves to a response received via a response
* port. IPC requests are used by an Electron renderer process to communicate
* with the main process when the renderer is fully isolated.
*
* Some of the design goals of this "safe IPC" are to respect the confines
* of process isolation and security, without using node integration, while
* retaining the ability to run the Ember application within a browser
* (sans main process) by using only APIs available in a standard
* (non-Electron) browser. This is achieved by posting messages to the
* renderer's `window.postMessage` interface. Thus, the renderer requires no
* knowledge of the main process or the Electron context. In fact a main
* process and Electron context are not even required.
*
* In an Electron context, the `preload.js` script sets up a `window.postMessge`
* listener which acts as proxy layer between the main and renderer processes.
* It waits for incoming requests from the renderer and forwards them to main.
* Responses from the main process are forwarded back via a response port
* (sent with the original request).
*
* Local development in a non-Electron browser context or test context should
* mock up IPC requests. See the ipc service test for an example of mocking.
*
* TODO: add utilities for quickly mocking IPC responses (like Mirage)
*
*/
export class IPCRequest {
// =attributes
/**
* @type {?MessageChannel}
* @private
*/
#channel = null;
/**
* @type {?Promise}
* @private
*/
#promise = null;
// =methods
/**
* Posts a message to the window object, along with a MessagePort
* with which to receive a response. An internal promise is generated which
* resolves to the response.
* @param {string} method
* @param {object} payload
* @param {Window} window - A reference to the window object
* @param {?string} origin
*/
constructor(method, payload, window, origin=config.ipc.defaultOrigin) {
this.method = method;
this.payload = payload;
this.origin = origin;
this.#channel = new MessageChannel();
this.#promise = new Promise((resolve/*, reject*/) => {
this.#channel.port1.onmessage = (event) => {
this.close();
resolve(event.data);
}
});
window.postMessage({
method: this.method,
payload: this.payload
}, origin, [ this.#channel.port2 ]);
}
/**
* Closes the message ports and drops the channel reference.
* This is called upon receiving a response for cleanup purposes.
*/
close() {
this.#channel.port1.close();
this.#channel.port2.close();
this.#channel = null;
}
/**
* Proxies to the underlying promise.
*/
then() {
return this.#promise.then(...arguments);
}
/**
* Proxies to the underlying promise.
*/
catch() {
return this.#promise.catch(...arguments);
}
/**
* Proxies to the underlying promise.
*/
finally() {
return this.#promise.finally(...arguments);
}
}
/**
* A simple factory for generating IPCRequests in Ember.
* @example
*
* class MyClass {
* @service ipc;
*
* makeRequest {
* return this.ipc.invoke('getFoobars', { hello: 'world' });
* }
* }
*/
export default class IpcService extends Service {
// =attributes
/**
* Looks up the window object indirectly.
* @type {Window}
*/
get window() {
// The Ember way of accessing globals...
const document = getOwner(this).lookup('service:-document').documentElement;
// defaultView === window, but without using globals directly
return document.parentNode.defaultView;
}
// =methods
/**
* Creates a new IPC request and returns it.
* @param {string} method
* @param {object} payload
* @param {object} origin
* @return {IPCRequest}
*/
invoke(method, payload, origin) {
return new IPCRequest(method, payload, this.window, origin);
}
}
+1
View File
@@ -0,0 +1 @@
{{@model.message}}
+6
View File
@@ -31,6 +31,12 @@ module.exports = function (environment) {
namespace: 'v1',
},
ipc: {
defaultOrigin: process.env.EMBER_CLI_ELECTRON
? 'serve://boundary'
: 'http://localhost:4200'
},
appName: APP_NAME,
};
@@ -1,42 +0,0 @@
const { fileURLToPath } = require('url');
const path = require('path');
const fs = require('fs');
const { promisify } = require('util');
const access = promisify(fs.access);
//
// Patch asset loading -- Ember apps use absolute paths to reference their
// assets, e.g. `<img src="/images/foo.jpg">`. When the current URL is a `file:`
// URL, that ends up resolving to the absolute filesystem path `/images/foo.jpg`
// rather than being relative to the root of the Ember app. So, we intercept
// `file:` URL request and look to see if they point to an asset when
// interpreted as being relative to the root of the Ember app. If so, we return
// that path, and if not we leave them as-is, as their absolute path.
//
async function getAssetPath(emberAppDir, url) {
let urlPath = fileURLToPath(url);
// Get the root of the path -- should be '/' on MacOS or something like
// 'C:\' on Windows
let { root } = path.parse(urlPath);
// Get the relative path from the root to the full path
let relPath = path.relative(root, urlPath);
// Join the relative path with the Ember app directory
let appPath = path.join(emberAppDir, relPath);
try {
await access(appPath);
return appPath;
} catch (e) {
return urlPath;
}
}
module.exports = function handleFileURLs(emberAppDir) {
const { protocol } = require('electron');
protocol.interceptFileProtocol('file', async ({ url }, callback) => {
callback(await getAssetPath(emberAppDir, url));
});
};
module.exports.getAssetPath = getAssetPath;
+17
View File
@@ -0,0 +1,17 @@
const { ipcMain } = require('electron');
const isDev = require('electron-is-dev');
const log = (method, request) => {
if (isDev) console.log(`[ipc-main](${method}): `, request);
}
/**
* A demonstration method `getFoobars` which returns an object with a message.
*/
ipcMain.handle('getFoobars', async (event, request) => {
log('getFoobars', request);
return {
message: 'this is a response from an ipcMain handler'
};
});
+2 -1
View File
@@ -1,7 +1,8 @@
/* eslint-disable no-console */
const path = require('path');
const { default: installExtension, EMBER_INSPECTOR } = require('electron-devtools-installer');
const { session, app, protocol, BrowserWindow } = require('electron');
const { session, app, protocol, BrowserWindow, ipcMain } = require('electron');
require('./handlers.js');
const isDev = require('electron-is-dev');
+24 -1
View File
@@ -1 +1,24 @@
// nothing here yet
const { ipcRenderer } = require('electron');
const emberAppOrigin = 'serve://boundary';
process.once('loaded', () => {
/**
* Ember-land has no access to the renderer or node modules, and thus
* cannot call into the main process or make invocations directly.
* In order to communicate with the main process, the Ember app uses
* the `postMessage` interface to send messages to its browser window.
* These messages include message ports with which to receive responses from
* the main process.
*
* This preload script simply wires up the forwarding of
* messages received on the window and responses received from main.
*/
window.addEventListener('message', async function (event) {
if (event.origin !== emberAppOrigin) return;
const response =
await ipcRenderer.invoke(event.data.method, event.data.payload);
event.ports[0].postMessage(response);
});
});
+2 -1
View File
@@ -74,7 +74,8 @@
"prettier": "^2.1.2",
"qunit-dom": "^1.2.0",
"rose": "*",
"sass": "^1.29.0"
"sass": "^1.29.0",
"sinon": "^9.2.1"
},
"engines": {
"node": "10.* || >= 12"
@@ -0,0 +1,11 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Route | index', function(hooks) {
setupTest(hooks);
test('it exists', function(assert) {
let route = this.owner.lookup('route:index');
assert.ok(route);
});
});
@@ -0,0 +1,57 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { IPCRequest } from 'desktop/services/ipc';
import sinon from 'sinon';
module('Unit | Service | ipc', function(hooks) {
setupTest(hooks);
hooks.beforeEach(() => {
// IPC requests talk to the main process indirectly via window.postMessage
// proxy layer (mediated by preload.js).
// But we don't want to send these for real in tests.
sinon.stub(window, 'postMessage');
});
hooks.afterEach(() => {
sinon.restore();
});
test('it creates new IPCRequest objects', function(assert) {
assert.expect(1);
const service = this.owner.lookup('service:ipc');
const request = service.invoke('hello', 'world', 'http://localhost:4200');
assert.equal(request.constructor, IPCRequest);
});
test('IPCRequest objects post messages on their window objects', function(assert) {
assert.expect(2);
const myWindow = {
postMessage(request) {
assert.equal(request.method, 'hello');
assert.equal(request.payload, 'world');
}
};
const request = new IPCRequest('hello', 'world', myWindow);
});
test('IPCRequest objects resolve to the value posted via a response message port', async function(assert) {
assert.expect(1);
const myWindow = {
postMessage(request, origin, [ port ]) {
// Post back a message on the response port
port.postMessage({
foo: 'bar',
test: 42
});
}
};
const request = new IPCRequest('hello', 'world', myWindow);
const response = await request;
assert.deepEqual(response, {
foo: 'bar',
test: 42
});
});
});
+1048 -831
View File
File diff suppressed because it is too large. Load diff