chore: 🤖 wire up postMessage ipc proxy and Ember ipc service

This commit is contained in:
Randall Morey committed 2020-11-09 15:48:54 -05:00
1 parent 4264301222
commit cc975d221d
9 files changed
+196 -44

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'
});
}
}
+107
View File
@@ -0,0 +1,107 @@
import Service from '@ember/service';
import { Promise } from 'rsvp';
/**
* An IPC request is a promise-like object that invokes a method on the main
* process and resolves to its response.
*
* Since the renderer cannot call into main or access Node modules directly,
* it uses the window's `postMessage` interface. Messages posted in this manner
* are proxied by `preload.js` to the main process. This ensures that main
* and renderer remain isolated and frontend code does not rely on the presence
* of a main process.
*/
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 {?string} origin
*/
constructor(method, payload, origin='serve://boundary') {
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.
*/
export default class IpcService extends Service {
// =methods
/**
* Creates a new IPC request and returns it.
* @param {string} method
* @param {object} payload
* @return {IPCRequest}
*/
invoke(method, payload) {
return new IPCRequest(method, payload);
}
}
+1
View File
@@ -0,0 +1 @@
{{@model.message}}
@@ -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);
});
});
@@ -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,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | ipc', function(hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function(assert) {
let service = this.owner.lookup('service:ipc');
assert.ok(service);
});
});