diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
index 8db9981..8c0d40b 100644
--- a/DOCUMENTATION.md
+++ b/DOCUMENTATION.md
@@ -320,6 +320,78 @@ It is also possible to supply additional parameters using the [optional] `option
> For more information refer to [Electron documentation](https://electronjs.org/docs/api/browser-window#winloadurlurl-options).
+## Writing a Plugin
+### Initializing
+Add a directory for your plugin's electron platform under src/electron and in it, initialize an NPM repository (`npm init`)
+### plugin.xml
+In your plugin.xml, add the following elements:
+```xml
+
+
+
+```
+
+### package.json
+In the repository's `package.json`, add the following:
+```json
+ "cordova": {
+ "serviceName": "YourService"
+ }
+```
+Where `YourService` is the service name that is being registered with cordova.
+
+### Writing the actual plugin
+In the `src/electron` directory, add a file `index.js`\
+This file could look something like this:
+```js
+module.exports = function(action, args, callbackContext) {
+ if(action === 'yourAction') {
+ console.log(args[0]); // will echo 'foo'
+ console.log(args[1]); // will echo 123
+ callbackContext.success('yourAction completed successfully');
+ return true;
+ }
+ return false;
+}
+```
+Returning `true` from the function indicates that the invocation has returned without problem, while returning `false` means the action was not found on the service.\
+The service can then be called via
+```js
+const success = success => console.log(success)
+const error = error => console.log(error)
+cordova.exec(success, error, 'YourService', 'yourAction', ['foo', 123]);
+```
+### Error handling
+To indicate an error in the execution and trigger the plugin caller's `error` callback, use `callbackContext.error`
+
+### Multiple plugin results
+If you want to return more than one plugin result, you need to keep the callback, this can be done by using `callbackContext.sendPluginResult` and `pluginResult.setKeepCallback`.\
+Here is an example of what this might look like:
+```js
+module.exports = function(action, args, callbackContext) {
+ if(action === 'yourAction') {
+ let i = 0;
+ const PluginResult = callbackContext.PluginResult;
+ const interval = setInterval(() => {
+ if (i++ < 3) {
+ const result = new PluginResult(PluginResult.STATUS_OK, i);
+ result.setKeepCallback(true);
+ callbackContext.sendPluginResult(result);
+ return;
+ }
+ clearInterval(interval)
+ callbackContext.success('Last result')
+ callbackContext.success('Already closed. This will not arrive')
+ }, 1000);
+ return true;
+ }
+ return false;
+}
+```
+In the example above, the `success` function will be called 4 times, with `0`, `1`, `2` and `'Last result'`.\
+After this, the listener for the callbackContext will be removed and subsequent calls will not be heard by the caller.
+
+
## Customizing the Electron's Main Process
If it is necessary to customize the Electron's main process beyond the scope of the Electron's configuration settings, changes can be added directly to the `cdv-electron-main.js` file located in `{PROJECT_ROOT_DIR}/platform/electron/platform_www/`. This is the application's main process.
diff --git a/bin/templates/platform_www/CallbackContext.js b/bin/templates/platform_www/CallbackContext.js
new file mode 100644
index 0000000..a354b85
--- /dev/null
+++ b/bin/templates/platform_www/CallbackContext.js
@@ -0,0 +1,41 @@
+class PluginResult {
+ constructor (status, data) {
+ this.status = status;
+ this.data = data !== undefined ? data : null;
+ this.keepCallback = false;
+ }
+
+ setKeepCallback (value) {
+ this.keepCallback = value;
+ }
+}
+PluginResult.STATUS_OK = 1;
+PluginResult.STATUS_ERROR = 2;
+PluginResult.ERROR_UNKNOWN_SERVICE = 4;
+PluginResult.ERROR_UNKNOWN_ACTION = 8;
+PluginResult.ERROR_UNEXPECTED_RESULT = 16;
+PluginResult.ERROR_INVOCATION_EXCEPTION_NODE = 32;
+PluginResult.ERROR_INVOCATION_EXCEPTION_CHROME = 64;
+
+class CallbackContext {
+ constructor (contextId, window) {
+ this.contextId = contextId;
+ this.window = window;
+ // add PluginResult as instance variable to be able to access it in plugins
+ this.PluginResult = PluginResult;
+ }
+
+ sendPluginResult (result) {
+ this.window.webContents.send(this.contextId, result);
+ }
+
+ success (data) {
+ this.sendPluginResult(new PluginResult(PluginResult.STATUS_OK, data));
+ }
+
+ error (data) {
+ this.sendPluginResult(new PluginResult(PluginResult.STATUS_ERROR, data));
+ }
+}
+
+module.exports = { CallbackContext, PluginResult };
diff --git a/bin/templates/platform_www/cdv-electron-main.js b/bin/templates/platform_www/cdv-electron-main.js
index 19b852c..beab443 100644
--- a/bin/templates/platform_www/cdv-electron-main.js
+++ b/bin/templates/platform_www/cdv-electron-main.js
@@ -91,9 +91,9 @@ function createWindow () {
// Emitted when the window is closed.
mainWindow.on('closed', () => {
- // Dereference the window object, usually you would store windows
- // in an array if your app supports multi windows, this is the time
- // when you should delete the corresponding element.
+ // Dereference the window object, usually you would store windows
+ // in an array if your app supports multi windows, this is the time
+ // when you should delete the corresponding element.
mainWindow = null;
});
}
@@ -146,17 +146,56 @@ app.on('activate', () => {
}
});
-ipcMain.handle('cdv-plugin-exec', async (_, serviceName, action, ...args) => {
- if (cordova && cordova.services && cordova.services[serviceName]) {
- const plugin = require(cordova.services[serviceName]);
+ipcMain.handle('cdv-plugin-exec', async (_, serviceName, action, args, callbackId) => {
+ // This function should never return a rejected promise or throw an exception, as otherwise ipcRenderer callback will convert the parameter to a string incapsulated in an Error. See https://github.com/electron/electron/issues/24427
- return plugin[action]
- ? plugin[action](args)
- : Promise.reject(new Error(`The action "${action}" for the requested plugin service "${serviceName}" does not exist.`));
- } else {
- return Promise.reject(new Error(`The requested plugin service "${serviceName}" does not exist have native support.`));
+ const { CallbackContext, PluginResult } = require('./CallbackContext.js');
+ const callbackContext = new CallbackContext(callbackId, mainWindow);
+
+ // this condition should never be met, exec.js already tests for it.
+ if (!(cordova && cordova.services && cordova.services[serviceName])) {
+ const message = `NODE: Invalid Service. Service '${serviceName}' does not have an electron implementation.`;
+ callbackContext.error(new PluginResult(PluginResult.ERROR | PluginResult.ERROR_UNKNOWN_SERVICE, message));
+ return;
}
-});
+ const plugin = require(cordova.services[serviceName]);
+
+ // API3 backwards compatible plugin call handling
+ const packageConfig = require(cordova.services[serviceName] + '/package.json');
+ if (packageConfig.cordova.API3 !== false) {
+ console.error('WARNING! Package ' + cordova.services[serviceName] + ' is using a deprecated API. Migrate to cordova-electron API 4.x ASAP. This API will be break in the next major version.');
+ try {
+ await plugin[action](args);
+ } catch (exception) {
+ const message = "NODE: Exception while invoking service action '" + serviceName + '.' + action + "'\r\n" + exception;
+ // print error to terminal
+ console.error(message, exception);
+ // trigger node side error callback
+ callbackContext.error(new PluginResult(PluginResult.ERROR | PluginResult.ERROR_INVOCATION_EXCEPTION_NODE, { message, exception }));
+ }
+ return;
+ }
+
+ // API 4.x handling
+ try {
+ const result = await plugin(action, args, callbackContext);
+ if (result === true) {
+ // successful invocation
+ } else if (result === false) {
+ const message = `NODE: Invalid action. Service '${serviceName}' does not have an electron implementation for action '${action}'.`;
+ callbackContext.error(new PluginResult(PluginResult.ERROR | PluginResult.ERROR_UNKNOWN_ACTION, message));
+ } else {
+ const message = 'NODE: Unexpected plugin exec result' + result;
+ callbackContext.error(new PluginResult(PluginResult.ERROR | PluginResult.ERROR_UNEXPECTED_RESULT, message));
+ }
+ } catch (exception) {
+ const message = "NODE: Exception while invoking service action '" + serviceName + '.' + action + "'\r\n" + exception;
+ // print error to terminal
+ console.error(message, exception);
+ // trigger node side error callback
+ callbackContext.error(new PluginResult(PluginResult.ERROR | PluginResult.ERROR_INVOCATION_EXCEPTION_NODE, { message, exception }));
+ }
+});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
diff --git a/bin/templates/platform_www/cdv-electron-preload.js b/bin/templates/platform_www/cdv-electron-preload.js
index ac00ab2..15427dd 100644
--- a/bin/templates/platform_www/cdv-electron-preload.js
+++ b/bin/templates/platform_www/cdv-electron-preload.js
@@ -20,16 +20,33 @@
const { contextBridge, ipcRenderer } = require('electron');
const { cordova } = require('./package.json');
+const { PluginResult } = require('./CallbackContext.js');
+
contextBridge.exposeInMainWorld('_cdvElectronIpc', {
- exec: (success, error, serviceName, action, args) => {
- return ipcRenderer.invoke('cdv-plugin-exec', serviceName, action, args)
- .then(
- success,
- error
- );
+ exec: async (success, error, serviceName, action, args, callbackId) => {
+ ipcRenderer.on(callbackId, (event, result) => {
+ if (result.status === PluginResult.STATUS_OK) {
+ success(result.data);
+ } else if (result.status & PluginResult.STATUS_ERROR) {
+ error(result.data);
+ } else {
+ error(new Error('Unexpected plugin result status code'));
+ }
+
+ if (!result.keepCallback) {
+ ipcRenderer.removeAllListeners(callbackId);
+ }
+ });
+ try {
+ await ipcRenderer.invoke('cdv-plugin-exec', serviceName, action, args, callbackId);
+ } catch (exception) {
+ const message = "CHROME: Exception while invoking service action '" + serviceName + '.' + action + "'";
+ console.error(message, exception);
+ error({ message, exception });
+ }
},
hasService: (serviceName) => cordova &&
- cordova.services &&
- cordova.services[serviceName]
+ cordova.services &&
+ cordova.services[serviceName]
});
diff --git a/cordova-js-src/exec.js b/cordova-js-src/exec.js
index 835a8c4..d60eda3 100644
--- a/cordova-js-src/exec.js
+++ b/cordova-js-src/exec.js
@@ -39,9 +39,10 @@ const execProxy = require('cordova/exec/proxy');
* @param {String[]} [args] Zero or more arguments to pass to the method
*/
module.exports = function (success, fail, service, action, args) {
+ const callbackId = service + cordova.callbackId++;
if (window._cdvElectronIpc.hasService(service)) {
// Electron based plugin support
- window._cdvElectronIpc.exec(success, fail, service, action, args);
+ window._cdvElectronIpc.exec(success, fail, service, action, args, callbackId);
} else {
// Fall back for browser based plugin support...
const proxy = execProxy.get(service, action);
@@ -49,8 +50,6 @@ module.exports = function (success, fail, service, action, args) {
args = args || [];
if (proxy) {
- const callbackId = service + cordova.callbackId++;
-
if (typeof success === 'function' || typeof fail === 'function') {
cordova.callbacks[callbackId] = { success: success, fail: fail };
}
@@ -96,13 +95,14 @@ module.exports = function (success, fail, service, action, args) {
};
proxy(onSuccess, onError, args);
} catch (e) {
- console.log('Exception calling native with command :: ' + service + ' :: ' + action + ' ::exception=' + e);
+ console.error(`Exception when calling fallback browser action '${action}' on service '${service}'`, e);
}
} else {
- console.log('Error: exec proxy not found for :: ' + service + ' :: ' + action);
+ const message = `Could not call electron action '${action}' on service '${service}'; Service is not registered`;
+ console.error(message);
if (typeof fail === 'function') {
- fail('Missing Command Error');
+ fail(message);
}
}
}
diff --git a/package.json b/package.json
index 48b1285..e01dafc 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"devDependencies": {
"@cordova/eslint-config": "^4.0.0",
"cordova-js": "^6.1.0",
+ "eslint": "^7.32.0",
"jasmine": "^4.1.0",
"nyc": "^15.1.0",
"rewire": "^6.0.0"
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/plugin.xml b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/plugin.xml
index 835911f..9d8cd3f 100644
--- a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/plugin.xml
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/plugin.xml
@@ -18,23 +18,28 @@
under the License.
-->
-
- Sanple
- Cordova Sample Plugin
- Apache 2.0
- cordova,sample
-
-
-
-
-
-
-
-
+
+ Sanple
+ Cordova Sample Plugin
+ Apache 2.0
+ cordova,sample
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/browser/sample.js b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/browser/sample.js
new file mode 100644
index 0000000..fb7d441
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/browser/sample.js
@@ -0,0 +1,8 @@
+module.exports = {
+ testLocation() {
+ return window.location;
+ },
+ testEchoBrowser(args) {
+ return "BROWSER: echo1 is: " + args[1];
+ }
+}
\ No newline at end of file
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/electron/index.js b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/electron/index.js
index 774baa5..9fe6107 100644
--- a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/electron/index.js
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/electron/index.js
@@ -17,22 +17,59 @@
under the License.
*/
-const { system, osInfo } = require('systeminformation');
-const electron = require
-
-module.exports = {
- getSampleInfo: async () => {
- try {
- const { model } = await system();
- const { platform } = await osInfo();
-
- return {
- model,
- platform: platform === 'darwin' ? codename : distro,
- electronVersion: process.versions.electron
- };
- } catch (e) {
- console.log(e);
+class Sample {
+ testProcess(args, callbackContext) {
+ const process = require('process')
+ callbackContext.success(process.cwd());
+ }
+ testMany(args, callbackContext) {
+ let i = 0;
+ const PluginResult = callbackContext.PluginResult;
+ const interval = setInterval(() => {
+ if (i++ < 3) {
+ const result = new PluginResult(PluginResult.STATUS_OK, i);
+ result.setKeepCallback(true);
+ callbackContext.sendPluginResult(result);
+ return;
+ }
+ callbackContext.success('ELECTRON: last many. This will arrive')
+ callbackContext.success('ELECTRON: > last many. This will not')
+ clearInterval(interval)
+ }, 1000);
+ }
+ testError(args, callbackContext) {
+ const PluginResult = callbackContext.PluginResult;
+ let result;
+ for (let i = 0; i < 3; i++) {
+ result = new PluginResult(PluginResult.STATUS_ERROR, "ELECTRON: multiple errors: " + i);
+ result.setKeepCallback(true)
+ callbackContext.sendPluginResult(result)
}
+ callbackContext.error('ELECTRON: last error. This will arrive')
+ callbackContext.error('ELECTRON: > last error. This will not')
+ }
+ testEchoElectron(args, callbackContext) {
+ const message = 'ELECTRON: echo1 is ' + args[1]
+ console.log(message)
+ callbackContext.success(message);
}
-};
+ testException(args, callbackContext) {
+ throw new Error('Exception in sample plugin')
+ }
+}
+
+module.exports = function (action, args, callbackContext) {
+ const sample = new Sample();
+ if (!sample[action]) {
+ return false;
+ }
+ console.log('ELECTRON/PLUGIN Sample: dispatching service action ' + action + '(' + args + ')')
+ sample[action](args, callbackContext)
+ return true;
+}
+
+// module.exports = {
+// testEchoElectron: function() {
+// console.log('ECHO ELECTRON', arguments)
+// }
+// }
\ No newline at end of file
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/electron/package.json b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/electron/package.json
index 1fdb535..968b12c 100644
--- a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/electron/package.json
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/src/electron/package.json
@@ -15,6 +15,7 @@
"systeminformation": "^4.27.9"
},
"cordova": {
- "serviceName": "Sample"
+ "serviceName": "Sample",
+ "API3": false
}
}
diff --git a/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/www/init.js b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/www/init.js
new file mode 100644
index 0000000..d229351
--- /dev/null
+++ b/tests/spec/fixtures/test-app-with-electron-plugin/plugins/cordova-plugin-sample/www/init.js
@@ -0,0 +1,56 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+document.addEventListener("deviceready", () => {
+ const tests = {
+ testProcess: null,
+ testLocation: null,
+ testMany: null,
+ testError: null,
+ testException: null,
+ testEchoElectron: ['echo', 'echoo', 'echooo'],
+ testEchoBrowser: ['echo', 'echoo', 'echooo'],
+ testUnknownAction: ['echo', 'echoo', 'echooo']
+ }
+ for(let test in tests) {
+ cordova.exec(
+ data => {
+ console.log(`${test} success callback`, data)
+ },
+ error => {
+ console.error(`${test} error callback`, error)
+ },
+ 'Sample',
+ test,
+ tests[test]
+ );
+ }
+
+ cordova.exec(
+ data => {
+ console.log(`UnkonwService success callback`, data)
+ },
+ error => {
+ console.error(`UnkonwService error callback`, error)
+ },
+ 'UnknownService',
+ 'irrelevant'
+ );
+});