Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added possibility for multiple plugin results #247

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<platform name="electron">
<framework src="src/electron" />
</platform>
```

### 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.
Expand Down
41 changes: 41 additions & 0 deletions bin/templates/platform_www/CallbackContext.js
Original file line number Diff line number Diff line change
@@ -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 };
63 changes: 51 additions & 12 deletions bin/templates/platform_www/cdv-electron-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
}
Expand Down Expand Up @@ -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.
33 changes: 25 additions & 8 deletions bin/templates/platform_www/cdv-electron-preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
});
12 changes: 6 additions & 6 deletions cordova-js-src/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,17 @@ 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);

args = args || [];

if (proxy) {
const callbackId = service + cordova.callbackId++;

if (typeof success === 'function' || typeof fail === 'function') {
cordova.callbacks[callbackId] = { success: success, fail: fail };
}
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,28 @@
under the License.
-->

<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
id="cordova-plugin-sample"
version="1.0.0">
<name>Sanple</name>
<description>Cordova Sample Plugin</description>
<license>Apache 2.0</license>
<keywords>cordova,sample</keywords>

<engines>
<engine name="cordova-electron" version=">=3.0.0" />
</engines>

<js-module src="www/sample.js" name="sample">
<clobbers target="sample" />
</js-module>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" id="cordova-plugin-sample" version="1.0.0">
<name>Sanple</name>
<description>Cordova Sample Plugin</description>
<license>Apache 2.0</license>
<keywords>cordova,sample</keywords>

<engines>
<engine name="cordova-electron" version=">=3.0.0" />
</engines>

<js-module src="www/init.js" name="initializer">
<runs/>
</js-module>

<platform name="electron">
<framework src="src/electron" />
</platform>

<platform name="electron">
<framework src="src/electron" />
</platform>

<platform name="browser">
<js-module src="src/browser/sample.js" name="bsample">
<runs />
</js-module>
</platform>
</plugin>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
testLocation() {
return window.location;
},
testEchoBrowser(args) {
return "BROWSER: echo1 is: " + args[1];
}
}
Loading