From 4ba0afaea8dfbc38142b9663f993c2f645152886 Mon Sep 17 00:00:00 2001 From: Vera Xia Date: Mon, 12 Feb 2024 10:52:44 -0800 Subject: [PATCH] Supports Electron 19+ (#398) * setup package to include dist files * sdk as dependency instead of devDependency * update missing dependency * WIP refactor for v20+ * wip update build script * update sample and readme * update read me * EOF * update electron readme * setup CI to run electron sample * setup electron run command * setup build script * fix electron command * fix arguments & readme * fix ci argument * add test log * add more logs for ci debug * log more details to debug in ci * set ci action in create window * set linux sample & test macos by directly run the electron sample * test xvfb * fix yaml * try catch subprocess exception * test run with check flag * test with timeout * improve docs * reduce timeout time * merge main * merge main * fix merge * update aws-crt * lock file * update electron version to address dependabot alerts * fix package-lock --- .github/workflows/ci.yml | 25 ++ .../workflows/ci_run_pubsub_electron_cfg.json | 13 ++ documents/FAQ.md | 7 + package-lock.json | 2 +- samples/node/pub_sub_electron_node/Main.ts | 220 +++++++++++++++++- samples/node/pub_sub_electron_node/README.md | 89 +++++-- .../node/pub_sub_electron_node/package.json | 16 +- .../pub_sub_electron_node/preload_pubsub5.ts | 206 +--------------- utils/run_in_ci.py | 145 ++++++------ 9 files changed, 426 insertions(+), 297 deletions(-) create mode 100644 .github/workflows/ci_run_pubsub_electron_cfg.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79a7ee58..a1e77264 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,11 @@ jobs: - name: run PubSub sample run: | python ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_cfg.json + # The electron app in CI would not perform any IoT operations. The CI only verify the app is launched correctly. + # This is because electron requires manually input for pwd while processing the credentials. Currently we don't have a good way to workaround it. + - name: run PubSub Electron sample + run: | + python ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_electron_cfg.json # TODO Temporarily disable this job. Enable it after the issue is properly fixes. # - name: run Windows Certificate Connect sample # run: | @@ -128,6 +133,12 @@ jobs: - name: run PubSub sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_cfg.json + + # The electron app in CI would not perform any IoT operations. The CI only verify the app is launched correctly. + # This is because electron requires manually input for pwd while processing the credentials. Currently we don't have a good way to workaround it. + - name: run PubSub Electron sample + run: | + python ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_electron_cfg.json - name: run PKCS12 sample run: | cert=$(aws secretsmanager get-secret-value --region us-east-1 --secret-id "ci/PubSub/cert" --query "SecretString" | cut -f2 -d":" | cut -f2 -d\") && echo -e "$cert" > /tmp/certificate.pem @@ -166,6 +177,13 @@ jobs: - name: run PubSub sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_cfg.json + + # Electron requires X11 on linux, while github actions does not have X11 support. We work around it using XVFB. + - name: run PubSub Electron sample + run: | + export DISPLAY=:99 + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + python ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_electron_cfg.json - name: configure AWS credentials (Device Advisor) uses: aws-actions/configure-aws-credentials@v1 with: @@ -272,6 +290,13 @@ jobs: - name: run PubSub sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_cfg.json + + # Electron requires X11 on linux, while github actions does not have X11 support. We work around it using XVFB. + - name: run PubSub Electron sample + run: | + export DISPLAY=:99 + sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & + python ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_electron_cfg.json - name: run PubSub JS sample run: | python3 ./aws-iot-device-sdk-js-v2/utils/run_in_ci.py --file ./aws-iot-device-sdk-js-v2/.github/workflows/ci_run_pubsub_js_cfg.json diff --git a/.github/workflows/ci_run_pubsub_electron_cfg.json b/.github/workflows/ci_run_pubsub_electron_cfg.json new file mode 100644 index 00000000..11d8c71a --- /dev/null +++ b/.github/workflows/ci_run_pubsub_electron_cfg.json @@ -0,0 +1,13 @@ +{ + "language": "Javascript", + "runnable_file": "./aws-iot-device-sdk-js-v2/samples/node/pub_sub_electron_node", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "data": "is_ci" + } + ], + "node_cmd": "npm start", + "timeout": 180 +} diff --git a/documents/FAQ.md b/documents/FAQ.md index 6ef413af..13c8cbf5 100644 --- a/documents/FAQ.md +++ b/documents/FAQ.md @@ -8,6 +8,7 @@ * [How do debug in VSCode?](#how-do-debug-in-vscode) * [What certificates do I need?](#what-certificates-do-i-need) * [I would like to build a browser application and got error "Property does not exist on type 'typeof import("\/node_modules/aws-crt/dist/**native**/*")](#browser-error) +* [Vercel/pkg Support](#vercel/pkg-support) * [I still have more questions about this sdk?](#i-still-have-more-questions-about-this-sdk) ### Where should I start? @@ -110,6 +111,12 @@ To set up the path in tsconfig.json, you can add a mapping for the library modul "aws-iot-device-sdk-v2": ["node_modules/aws-iot-device-sdk-v2/dist/browser"] }, ``` +### Vercel/pkg Support + +#### Uncaught Error: A dynamic link library (DLL) initialization routine failed. \\?\ +The vercel/pkg is a tool to package your Node.js project into an executable that can be run on devices without Node.js installed. You can find instructions at https://github.com/vercel/pkg. +If the DLL load failure issue happened on windows with Vercel/pkg, please try the latest version. The issue should be fixed in v1.19.1. +The library `aws-iot-device-sdk-v2` depends on the native modules `aws-crt`. When vercel/pkg package the node project, it would renamed node.exe into the generated single executable. In such case, the symbols needed by native modules `aws-crt` are exported by the renamed executable instead of node.exe. In order to load native modules on Windows, the library need to install a delay-load hook to redirect the reference to use the loading executable. A windows delay load is required here. ### I still have more questions about this sdk? diff --git a/package-lock.json b/package-lock.json index bff52b29..05929061 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4833,4 +4833,4 @@ } } } -} +} \ No newline at end of file diff --git a/samples/node/pub_sub_electron_node/Main.ts b/samples/node/pub_sub_electron_node/Main.ts index 7e23bb90..94c3fbbd 100644 --- a/samples/node/pub_sub_electron_node/Main.ts +++ b/samples/node/pub_sub_electron_node/Main.ts @@ -2,11 +2,23 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ -const { app, BrowserWindow} = require('electron') +const { app, BrowserWindow, ipcMain } = require('electron') const path = require("path") +import { mqtt5, iot } from "aws-iot-device-sdk-v2" +import { ICrtError } from "aws-crt" +import { once } from "events" +import { toUtf8 } from '@aws-sdk/util-utf8-browser' +import * as args from "./settings" -function createWindow () { - const win = new BrowserWindow({ +var win: Electron.BrowserWindow +var client: mqtt5.Mqtt5Client | null; +var qos0_topic = "test/topic/qos0"; +var qos1_topic = "test/topic/qos1"; +const cmdline_args = process.argv; + +async function createWindow() { + console.log("start creating windows"); + win = new BrowserWindow({ width: 800, height: 600, webPreferences: { @@ -15,9 +27,19 @@ function createWindow () { }) win.loadFile('./index.html'); + + // Launch and exit the app if we are testing in CI. + if (cmdline_args[2] == "is_ci") { + console.log("App launched in ci, exiting..."); + app.quit(); + } } app.whenReady().then(() => { + ipcMain.handle('PubSub5MtlsStart', PubSub5MtlsStart) + ipcMain.handle('PubSub5WebsocketsStart', PubSub5WebsocketsStart) + ipcMain.handle('PubSub5Stop', PubSub5Stop) + ipcMain.handle('PublishTestMessage', PublishTestQoS1Message) createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { @@ -32,3 +54,195 @@ app.on('window-all-closed', () => { } }) + + +app.on('will-quit', async () => { + await PubSub5Stop() +}); + +function console_render_log(msg: string) { + try { + win?.webContents?.send('log', msg) + console.log(msg); + } + catch (error) { + console.log("Failed to log the message: " + error) + } +} + +function createClientConfig(isWebsocket: boolean): mqtt5.Mqtt5ClientConfig { + let builder: iot.AwsIotMqtt5ClientConfigBuilder | undefined = undefined; + + if (!isWebsocket) { + console_render_log("Start to build client with Mtls... Please make sure setting up the credentials in \"Settings.ts\""); + builder = iot.AwsIotMqtt5ClientConfigBuilder.newDirectMqttBuilderWithMtlsFromPath( + args.endpoint, + args.cert_file_path, + args.key_file_path + ); + } else { + console_render_log("Start to build client with websocket configuration... Please make sure setup the endpoint and region in \"Settings.ts\""); + let wsOptions: iot.WebsocketSigv4Config | undefined = undefined; + if (args.region) { + wsOptions = { region: args.region }; + } + builder = iot.AwsIotMqtt5ClientConfigBuilder.newWebsocketMqttBuilderWithSigv4Auth( + args.endpoint, + wsOptions + ); + } + + builder.withConnectProperties({ + keepAliveIntervalSeconds: 1200, + clientId: "test-client" + }); + + return builder.build(); +} + + +function createClient(isWebsocket: boolean): mqtt5.Mqtt5Client { + + let config: mqtt5.Mqtt5ClientConfig = createClientConfig(isWebsocket); + + console_render_log("Creating client for " + config.hostName); + client = new mqtt5.Mqtt5Client(config); + + client.on('error', (error: ICrtError) => { + console_render_log("Error event: " + error.toString()); + }); + + client.on("messageReceived", (eventData: mqtt5.MessageReceivedEvent): void => { + console_render_log("Message Received event: " + JSON.stringify(eventData.message)); + if (eventData.message.payload) { + console_render_log(" with payload: " + toUtf8(new Uint8Array(eventData.message.payload as ArrayBuffer))); + } + }); + + client.on('attemptingConnect', (eventData: mqtt5.AttemptingConnectEvent) => { + console_render_log("Attempting Connect event"); + }); + + client.on('connectionSuccess', (eventData: mqtt5.ConnectionSuccessEvent) => { + console_render_log("Connection Success event"); + console_render_log("Connack: " + JSON.stringify(eventData.connack)); + console_render_log("Settings: " + JSON.stringify(eventData.settings)); + }); + + client.on('connectionFailure', (eventData: mqtt5.ConnectionFailureEvent) => { + console_render_log("Connection failure event: " + eventData.error.toString()); + if (eventData.connack) { + console_render_log("Connack: " + JSON.stringify(eventData.connack)); + } + }); + + client.on('disconnection', (eventData: mqtt5.DisconnectionEvent) => { + console_render_log("Disconnection event: " + eventData.error.toString()); + if (eventData.disconnect !== undefined) { + console_render_log('Disconnect packet: ' + JSON.stringify(eventData.disconnect)); + } + }); + + client.on('stopped', (eventData: mqtt5.StoppedEvent) => { + console_render_log("Stopped event"); + }); + + return client; +} + +async function createClientAndStartPubSub(isWebsocket: boolean) { + + try { + + client = createClient(isWebsocket); + + const connectionSuccess = once(client, "connectionSuccess"); + + client.start(); + await connectionSuccess; + + const suback = await client.subscribe({ + subscriptions: [ + { qos: mqtt5.QoS.AtLeastOnce, topicFilter: qos1_topic }, + { qos: mqtt5.QoS.AtMostOnce, topicFilter: qos0_topic } + ] + }); + console_render_log('Suback result: ' + JSON.stringify(suback)); + + const qos0PublishResult = await client.publish({ + qos: mqtt5.QoS.AtMostOnce, + topicName: qos0_topic, + payload: JSON.stringify("This is a qos 0 payload"), + userProperties: [ + { name: "test", value: "userproperty" } + ] + }); + console_render_log('QoS 0 Publish result: ' + JSON.stringify(qos0PublishResult)); + + const qos1PublishResult = await client.publish({ + qos: mqtt5.QoS.AtLeastOnce, + topicName: qos1_topic, + payload: JSON.stringify("This is a qos 1 payload") + }); + console_render_log('QoS 1 Publish result: ' + JSON.stringify(qos1PublishResult)); + } + catch (error) { + console_render_log("Client failed: " + error) + } + +} + +export const PubSub5MtlsStart = async () => { + if (client != null) { + console_render_log("Client is already started."); + return; + } + + await createClientAndStartPubSub(false) + +} + +export const PubSub5WebsocketsStart = async () => { + if (client != null) { + console_render_log("Client is already started, please stop the client first."); + return; + } + + await createClientAndStartPubSub(true); + +} + + +async function PubSub5Stop() { + if (client == null) { + console_render_log("Client is not started.") + return; + } + + try { + const stopped = once(client, "stopped"); + client.stop(); + await stopped; + client.close(); + client = null; + } + catch (error) { + console_render_log("Client is not started.") + } +} + + + +async function PublishTestQoS1Message() { + if (client == null) { + console_render_log("Client is not started.") + return; + } + + const qos1PublishResult = await client.publish({ + qos: mqtt5.QoS.AtLeastOnce, + topicName: qos1_topic, + payload: JSON.stringify("This is a qos 1 payload") + }); + console_render_log('QoS 1 Publish result: ' + JSON.stringify(qos1PublishResult)); +} diff --git a/samples/node/pub_sub_electron_node/README.md b/samples/node/pub_sub_electron_node/README.md index 7be33517..6800a461 100644 --- a/samples/node/pub_sub_electron_node/README.md +++ b/samples/node/pub_sub_electron_node/README.md @@ -13,9 +13,7 @@ MQTT5 introduces additional features and enhancements that improve the developme ## Requirements -The sample is built with typescript@5^ and Electron@19. Please note the SDK currently does not support Electron20+. -Node14 is recommended to run the sample. - +The sample is built with typescript@5^. Node14+ would be minimal Node version to run the sample. ## IoT Core Policy Your IoT Core Thing's [Policy](https://docs.aws.amazon.com/iot/latest/developerguide/iot-policies.html) must provide privileges for this sample to connect, subscribe, publish, and receive. Below is a sample policy that can be used on your IoT Core Thing that will allow this sample to run as intended. @@ -62,12 +60,11 @@ Replace with the following with the data from your AWS account: * ``: The AWS IoT Core region where you created your AWS IoT Core thing you wish to use with this sample. For example `us-east-1`. * ``: Your AWS IoT Core account ID. This is the set of numbers in the top right next to your AWS account name when using the AWS IoT Core website. -Note that in a real application, you may want to avoid the use of wildcards in your ClientID or use them selectively. Please follow best practices when working with AWS on production applications using the SDK. Also, for the purposes of this sample, please make sure your policy allows a client ID of `test-*` to connect or use `--client_id ` to send the client ID your policy supports. +Note that in a real application, you may want to avoid the use of wildcards in your ClientID or use them selectively. Please follow best practices when working with AWS on production applications using the SDK. ## How to run - ### Direct MQTT via mTLS To Run this sample using a direct MQTT5 connection with a key and certificate, go to the `node/pub_sub_electron_node` folder. @@ -75,51 +72,93 @@ To Run this sample using a direct MQTT5 connection with a key and certificate, g 2. Install node packages ``` sh -npm install +npm install . ``` -3. Build and Run +3. Start Sample ```sh -npm run build npm run start ``` -### Websockets +### MQTT5 over Websockets with TLS To Run this sample using Websockets, go to the `node/pub_sub_electron_node` folder. -1. Setup your credential. You will need to set your AWS credentials in your environment variables or local files. See the [authorizing direct AWS](https://docs.aws.amazon.com/iot/latest/developerguide/authorizing-direct-aws.html) page for documentation on how to get the AWS credentials, which then you can set to the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` environment variables. +1. Setup your AWS credentials in your environment variables or local files. See the [authorizing direct AWS](https://docs.aws.amazon.com/iot/latest/developerguide/authorizing-direct-aws.html) page for documentation on how to get the AWS credentials, which then you can set to the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` environment variables. -2. Setup `settings.ts`. You will need setup the `region` and `endpoint` in `settings.ts` to setup the e +2. Setup `node/pub_sub_electron_node/settings.ts`. You will need setup the `region` and `endpoint` in the setting file. 3. Install node packages ```sh -npm install +npm install . ``` 4. Build and Run ```sh -npm run build npm run start ``` -## Electron Q&A -### Warning: `objc[79765]: Class WebSwapCGLLayer is implemented in both ` ? +## Package +Please refer to (Electron-tutorial-packaging)[https://www.electronjs.org/docs/latest/tutorial/tutorial-packaging] for packaging details. +As our sample is using typescript, you will need include the compiled js files while packaging. You can config the `package.json` or `forge.config.js` to set the output folder for the compiled files. +Example `package.json`: +```js +"build": { + "files": [ + "dist/*" + ] + } +``` -This is an issue running Electron on MacOS. The API has a name duplication for "WebSwapCGLLayer". The warning should not affect your development. The issue is fixed by Electron in v22. Unfortunately, our SDK currently only supports Electron@19 and below. -More info: https://github.com/electron/electron/issues/33685 +## Electron FAQ +### Warning: `objc[79765]: Class WebSwapCGLLayer is implemented in both ... ` +This is an issue when running Electron on MacOS. The API has a name duplication for "WebSwapCGLLayer". This warning should not affect your development and has been fixed in Electron v22 -### How to debug with Dev Tools -You can open dev tool using the following API: -``` - win.webContents.openDevTools() -``` +More info: https://github.com/electron/electron/issues/33685 ### SyntaxError: Unexpected token '?' Please check your dependency and Node version. If the error is not from your code, it is most likely your dependency is using a different version of node. As the nullish coalescing operator (??) is introduced in Node14, using Node14+ would help. ### N-API call failed: napi_create_external_arraybuffer( env, data_buffer->buffer, data_buffer->len, s_finalize_external_binary_byte_buf, data_buffer, &napi_binary). -Electron removed support for `napi_create_external_arraybuffer` since Electron@20. You can find more information from the Electron community here: https://github.com/electron/electron/issues/35801. There is no solid solution for the issue right now. Our team is actively working on resolving it. +This issue has been fixed in aws-iot-device-sdk-js-v2 v1.19.1. +Electron removed support for `napi_create_external_arraybuffer` in Electron v20. You can find more information from the Electron community here: https://github.com/electron/electron/issues/35801. -### Why does the SDK not support Electron@20+ -Same as the above question. +### Electron Packager Instructions "Error: An unhandled rejection has occurred inside Forge: Error: ENAMETOOLONG: name too long, scandir" with recursive path copy +The Electron Forge has an issue when copying files with a relative library path. We could avoid it by removing the local path for the dependency. +As an example: +``` +"dependencies": { + "aws-iot-device-sdk-v2": "file:../../..", +} +``` +change it to: +``` +"dependencies": { + "aws-iot-device-sdk-v2": "^1.19.1", +} +``` +As a workaround, if you would like to package the sample with your local library, you can manually use electron-packager with `--ignore=electron-packager` to work around (Reference:https://github.com/electron/electron-packager/issues/396) + + +### Uncaught Error: A dynamic link library (DLL) initialization routine failed. \\?\ +*If you are on windows* + +The issue should be fixed in release v1.19.1. +The library `aws-iot-device-sdk-v2` depends on the native modules `aws-crt`. In Electron 4.x and higher, the symbols needed by native modules are exported by electron.exe instead of node.dll or node.exe. In order to load native modules on Windows, the library need to install a delay-load hook that triggers when the native module is loaded to redirect the reference to use the loading executable (electron.exe in this case). A windows delay load is required for the library. + +*If you still see this error after v1.19.1* +The issue usually indicates you are using a library distribution different from your development environment. When you run npm install, the node modules will pull the build files unique to your operating system, your architectures and the Node version. This usually happens when npm failed to pull the library with your development environment. Inspect the library distribution and make sure you are using the correct binary build. +Try +1. delete `node_modules` and `package-lock.json` +2. Make sure you are using the same node api version as the library distribution used. +3. Run `npm install` to reinstall the dependencies. + +### Error "GPU process launch failed: error_code=18" +Electron bug: https://github.com/electron/electron/issues/32074 +There is no valid workaround for now, could be disabled by `--no-sandbox`, while it might not be an option in prod. + +### How to debug with Dev Tools +You can open dev tool using the following API: +``` + win.webContents.openDevTools() +``` diff --git a/samples/node/pub_sub_electron_node/package.json b/samples/node/pub_sub_electron_node/package.json index d6bab8de..4bdd71c4 100644 --- a/samples/node/pub_sub_electron_node/package.json +++ b/samples/node/pub_sub_electron_node/package.json @@ -4,8 +4,8 @@ "description": "PubSub Electron sample", "main": "./dist/Main.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "./node_modules/.bin/tsc", + "tsc": "tsc", + "prepare": "npm run tsc", "start": "electron ." }, "repository": { @@ -19,9 +19,17 @@ }, "homepage": "https://github.com/aws/aws-iot-device-sdk-js-v2#readme", "devDependencies": { - "aws-iot-device-sdk-v2": "file:../../..", - "electron": "^19.1.9", + "electron": "^22.3.25", "events": "^3.3.0", "typescript": "^5.1.3" + }, + "dependencies": { + "aws-iot-device-sdk-v2": "file:../../../", + "@aws-sdk/util-utf8-browser": "^3.109.0" + }, + "build": { + "files": [ + "dist/*" + ] } } diff --git a/samples/node/pub_sub_electron_node/preload_pubsub5.ts b/samples/node/pub_sub_electron_node/preload_pubsub5.ts index 539b7554..ed345199 100644 --- a/samples/node/pub_sub_electron_node/preload_pubsub5.ts +++ b/samples/node/pub_sub_electron_node/preload_pubsub5.ts @@ -3,23 +3,20 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ -import {mqtt5, iot} from "aws-iot-device-sdk-v2"; -import {ICrtError} from "aws-crt"; -import {once} from "events"; -import { toUtf8 } from '@aws-sdk/util-utf8-browser'; -import * as args from "./settings" -import { contextBridge } from 'electron'; + +import { contextBridge, ipcRenderer} from 'electron'; contextBridge.exposeInMainWorld( 'electron', { - Mqtt5MtlsStart: () => PubSub5MtlsStart(), - Mqtt5WebsocketsStart: () => PubSub5WebsocketsStart(), - Mqtt5Stop: () => PubSub5Stop(), - Mqtt5PublishQoS1: () => PublishTestMessage() + Mqtt5MtlsStart: () => ipcRenderer.invoke('PubSub5MtlsStart'), + Mqtt5WebsocketsStart: () => ipcRenderer.invoke('PubSub5WebsocketsStart'), + Mqtt5Stop: () => ipcRenderer.invoke('PubSub5Stop'), + Mqtt5PublishQoS1: () => ipcRenderer.invoke('PublishTestMessage') } ) + function log (msg : string){ console.log(msg); let consoleDiv = document.getElementById("console") as HTMLInputElement; @@ -28,189 +25,6 @@ function log (msg : string){ consoleDiv?.appendChild(div); } -function creatClientConfig(isWebsocket: boolean) : mqtt5.Mqtt5ClientConfig { - let builder : iot.AwsIotMqtt5ClientConfigBuilder | undefined = undefined; - - if (!isWebsocket) { - log("Start to build client with Mtls... Please make sure setting up the credentials in \"Settings.ts\""); - builder = iot.AwsIotMqtt5ClientConfigBuilder.newDirectMqttBuilderWithMtlsFromPath( - args.endpoint, - args.cert_file_path, - args.key_file_path - ); - } else { - log("Start to build client with websocket configuration... Please make sure setup the endpoint and region in \"Settings.ts\""); - let wsOptions : iot.WebsocketSigv4Config | undefined = undefined; - if (args.region) { - wsOptions = { region: args.region }; - } - builder = iot.AwsIotMqtt5ClientConfigBuilder.newWebsocketMqttBuilderWithSigv4Auth( - args.endpoint, - wsOptions - ); - } - - builder.withConnectProperties({ - keepAliveIntervalSeconds: 1200, - clientId: "test-client" - }); - - return builder.build(); -} - -let client :mqtt5.Mqtt5Client | null; -let qos0_topic = "test/topic/qos0"; -let qos1_topic = "test/topic/qos1"; - - -function createClient(isWebsocket: boolean) : mqtt5.Mqtt5Client { - - let config : mqtt5.Mqtt5ClientConfig = creatClientConfig(isWebsocket); - - log("Creating client for " + config.hostName); - client = new mqtt5.Mqtt5Client(config); - - client.on('error', (error: ICrtError) => { - log("Error event: " + error.toString()); - }); - - client.on("messageReceived",(eventData: mqtt5.MessageReceivedEvent) : void => { - log("Message Received event: " + JSON.stringify(eventData.message)); - if (eventData.message.payload) { - log(" with payload: " + toUtf8(new Uint8Array(eventData.message.payload as ArrayBuffer))); - } - } ); - - client.on('attemptingConnect', (eventData: mqtt5.AttemptingConnectEvent) => { - log("Attempting Connect event"); - }); - - client.on('connectionSuccess', (eventData: mqtt5.ConnectionSuccessEvent) => { - log("Connection Success event"); - log("Connack: " + JSON.stringify(eventData.connack)); - log("Settings: " + JSON.stringify(eventData.settings)); - }); - - client.on('connectionFailure', (eventData: mqtt5.ConnectionFailureEvent) => { - log("Connection failure event: " + eventData.error.toString()); - if (eventData.connack) { - log("Connack: " + JSON.stringify(eventData.connack)); - } - }); - - client.on('disconnection', (eventData: mqtt5.DisconnectionEvent) => { - log("Disconnection event: " + eventData.error.toString()); - if (eventData.disconnect !== undefined) { - log('Disconnect packet: ' + JSON.stringify(eventData.disconnect)); - } - }); - - client.on('stopped', (eventData: mqtt5.StoppedEvent) => { - log("Stopped event"); - }); - - return client; -} - -async function createClientAndStartPubSub(isWebsocket : boolean) { - - try{ - - let client : mqtt5.Mqtt5Client = createClient(isWebsocket); - - const connectionSuccess = once(client, "connectionSuccess"); - - client.start(); - await connectionSuccess; - - const suback = await client.subscribe({ - subscriptions: [ - { qos: mqtt5.QoS.AtLeastOnce, topicFilter: qos1_topic }, - { qos: mqtt5.QoS.AtMostOnce, topicFilter: qos0_topic } - ] - }); - log('Suback result: ' + JSON.stringify(suback)); - - const qos0PublishResult = await client.publish({ - qos: mqtt5.QoS.AtMostOnce, - topicName: qos0_topic, - payload: JSON.stringify("This is a qos 0 payload"), - userProperties: [ - {name: "test", value: "userproperty"} - ] - }); - log('QoS 0 Publish result: ' + JSON.stringify(qos0PublishResult)); - - const qos1PublishResult = await client.publish({ - qos: mqtt5.QoS.AtLeastOnce, - topicName: qos1_topic, - payload: JSON.stringify("This is a qos 1 payload") - }); - log('QoS 1 Publish result: ' + JSON.stringify(qos1PublishResult)); - } - catch(error) - { - log("Client failed: " + error) - } - -} - -export const PubSub5MtlsStart= async () => { - if(client !=null) - { - log("Client is already started."); - return; - } - // make it wait as long as possible once the promise completes we'll turn it off. - const timer = setTimeout(() => {}, 2147483647); - - await createClientAndStartPubSub(false); - - clearTimeout(timer); -} - -export const PubSub5WebsocketsStart = async () => { - if(client !=null) - { - log("Client is already started, please stop the client first."); - return; - } - // make it wait as long as possible once the promise completes we'll turn it off. - const timer = setTimeout(() => {}, 2147483647); - - await createClientAndStartPubSub(true); - - clearTimeout(timer); -} - - -async function PubSub5Stop(){ - if(client == null) - { - log("Client is not started.") - return; - } - - const stopped = once(client, "stopped"); - client.stop(); - await stopped; - client.close(); - client = null; -} - - - -async function PublishTestMessage(){ - if(client == null) - { - log("Client is not started.") - return; - } - - const qos1PublishResult = await client.publish({ - qos: mqtt5.QoS.AtLeastOnce, - topicName: qos1_topic, - payload: JSON.stringify("This is a qos 1 payload") - }); - log('QoS 1 Publish result: ' + JSON.stringify(qos1PublishResult)); -} +ipcRenderer.on('log', (event, args) => { + log(args) +}) diff --git a/utils/run_in_ci.py b/utils/run_in_ci.py index 283c90a3..ab7bce82 100644 --- a/utils/run_in_ci.py +++ b/utils/run_in_ci.py @@ -33,7 +33,8 @@ def setup_json_arguments_list(file, input_uuid=None): for argument in config_json['arguments']: # Add the name of the argument - config_json_arguments_list.append(argument['name']) + if( 'name' in argument): + config_json_arguments_list.append(argument['name']) # Based on the data present, we need to process and add the data differently try: @@ -216,7 +217,8 @@ def cleanup_runnable(): global config_json_arguments_list for argument in config_json['arguments']: - config_json_arguments_list.append(argument['name']) + if( 'name' in argument): + config_json_arguments_list.append(argument['name']) # Based on the data present, we need to process and add the data differently try: @@ -252,77 +254,84 @@ def launch_runnable(): exit_code = 0 - print("Launching runnable...") - - # Java - if (config_json['language'] == "Java"): - - # Flatten arguments down into a single string - arguments_as_string = "" - for i in range(0, len(config_json_arguments_list)): - arguments_as_string += str(config_json_arguments_list[i]) - if (i+1 < len(config_json_arguments_list)): - arguments_as_string += " " - - arguments = ["mvn", "compile", "exec:java"] - arguments.append("-pl") - arguments.append(config_json['runnable_file']) - arguments.append("-Dexec.mainClass=" + config_json['runnable_main_class']) - arguments.append("-Daws.crt.ci=True") - - # We have to do this as a string, unfortunately, due to how -Dexec.args= works... - argument_string = subprocess.list2cmdline(arguments) + " -Dexec.args=\"" + arguments_as_string + "\"" - print(f"Running cmd: {argument_string}") - runnable_return = subprocess.run(argument_string, shell=True) - exit_code = runnable_return.returncode - - # C++ - elif (config_json['language'] == "CPP"): - runnable_return = subprocess.run( - args=config_json_arguments_list, executable=config_json['runnable_file']) - exit_code = runnable_return.returncode - - elif (config_json['language'] == "Python"): - config_json_arguments_list.append("--is_ci") - config_json_arguments_list.append("True") - - runnable_return = subprocess.run( - args=[sys.executable, config_json['runnable_file']] + config_json_arguments_list) - exit_code = runnable_return.returncode - - elif (config_json['language'] == "Javascript"): - os.chdir(config_json['runnable_file']) - - config_json_arguments_list.append("--is_ci") - config_json_arguments_list.append("true") - - runnable_return_one = None - if sys.platform == "win32" or sys.platform == "cygwin": - runnable_return_one = subprocess.run(args=["npm", "install"], shell=True) - else: - runnable_return_one = subprocess.run(args=["npm", "install"]) - - if (runnable_return_one == None or runnable_return_one.returncode != 0): - exit_code = runnable_return_one.returncode - else: - runnable_return_two = None - arguments = [] - if 'node_cmd' in config_json: - arguments = config_json['node_cmd'].split(" ") - else: - arguments = ["node", "dist/index.js"] + runable_timeout = None + if ('timeout' in config_json): + runable_timeout = config_json['timeout'] + print("Launching runnable...") + try: + # Java + if (config_json['language'] == "Java"): + + # Flatten arguments down into a single string + arguments_as_string = "" + for i in range(0, len(config_json_arguments_list)): + arguments_as_string += str(config_json_arguments_list[i]) + if (i+1 < len(config_json_arguments_list)): + arguments_as_string += " " + + arguments = ["mvn", "compile", "exec:java"] + arguments.append("-pl") + arguments.append(config_json['runnable_file']) + arguments.append("-Dexec.mainClass=" + config_json['runnable_main_class']) + arguments.append("-Daws.crt.ci=True") + + # We have to do this as a string, unfortunately, due to how -Dexec.args= works... + argument_string = subprocess.list2cmdline(arguments) + " -Dexec.args=\"" + arguments_as_string + "\"" + print(f"Running cmd: {argument_string}") + runnable_return = subprocess.run(argument_string, shell=True) + exit_code = runnable_return.returncode + + # C++ + elif (config_json['language'] == "CPP"): + runnable_return = subprocess.run( + args=config_json_arguments_list, executable=config_json['runnable_file'], timeout=runable_timeout) + exit_code = runnable_return.returncode + + elif (config_json['language'] == "Python"): + config_json_arguments_list.append("--is_ci") + config_json_arguments_list.append("True") + + runnable_return = subprocess.run( + args=[sys.executable, config_json['runnable_file']] + config_json_arguments_list, timeout=runable_timeout) + exit_code = runnable_return.returncode + + elif (config_json['language'] == "Javascript"): + os.chdir(config_json['runnable_file']) + + config_json_arguments_list.append("--is_ci") + config_json_arguments_list.append("true") + + runnable_return_one = None if sys.platform == "win32" or sys.platform == "cygwin": - runnable_return_two = subprocess.run( - args=arguments + config_json_arguments_list, shell=True) + runnable_return_one = subprocess.run(args=["npm", "install"], shell=True, timeout=runable_timeout) else: - runnable_return_two = subprocess.run( - args=arguments + config_json_arguments_list) + runnable_return_one = subprocess.run(args=["npm", "install"], timeout=runable_timeout) - if (runnable_return_two != None): - exit_code = runnable_return_two.returncode + if (runnable_return_one == None or runnable_return_one.returncode != 0): + exit_code = runnable_return_one.returncode else: - exit_code = 1 + runnable_return_two = None + arguments = [] + if 'node_cmd' in config_json: + arguments = config_json['node_cmd'].split(" ") + else: + arguments = ["node", "dist/index.js"] + + if sys.platform == "win32" or sys.platform == "cygwin": + runnable_return_two = subprocess.run( + args=arguments + config_json_arguments_list, shell=True, check=True, timeout=runable_timeout) + else: + runnable_return_two = subprocess.run( + args=arguments + config_json_arguments_list, timeout=runable_timeout) + + if (runnable_return_two != None): + exit_code = runnable_return_two.returncode + else: + exit_code = 1 + except subprocess.CalledProcessError as e: + print(e.output) + exit_code = 1 cleanup_runnable() return exit_code