12 files changed
+1340
-876
No files matched your search
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{{@model.message}}
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
};
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Reference in new issue
Block a user