Merge branch 'hw-diag' of github.com:balena-io/etcher into hw-diag

This commit is contained in:
Peter Makra 2022-04-27 13:18:19 +02:00
commit 37e2ef1fef
62 changed files with 20250 additions and 8149 deletions

View File

@ -7,7 +7,6 @@ indent_size = 2
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false

View File

@ -1,6 +1,11 @@
- **Etcher version:** - **Etcher version:**
- **Operating system and architecture:** - **Operating system and architecture:**
- **Image flashed:** - **Image flashed:**
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
- **What happened:**
- **Do you see any meaningful error information in the DevTools?** - **Do you see any meaningful error information in the DevTools?**
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. --> <!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->

View File

@ -15,7 +15,7 @@
}, },
"builder": { "builder": {
"appId": "io.balena.etcher", "appId": "io.balena.etcher",
"copyright": "Copyright 2016-2020 Balena Ltd", "copyright": "Copyright 2016-2021 Balena Ltd",
"productName": "balenaEtcher", "productName": "balenaEtcher",
"nodeGypRebuild": false, "nodeGypRebuild": false,
"afterPack": "./afterPack.js", "afterPack": "./afterPack.js",
@ -24,13 +24,13 @@
"generated", "generated",
"lib/shared/catalina-sudo/sudo-askpass.osascript.js" "lib/shared/catalina-sudo/sudo-askpass.osascript.js"
], ],
"beforeBuild": "./beforeBuild.js",
"afterSign": "./afterSignHook.js", "afterSign": "./afterSignHook.js",
"mac": { "mac": {
"category": "public.app-category.developer-tools", "category": "public.app-category.developer-tools",
"hardenedRuntime": true, "hardenedRuntime": true,
"entitlements": "entitlements.mac.plist", "entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist" "entitlementsInherit": "entitlements.mac.plist",
"artifactName": "${productName}-${version}.${ext}"
}, },
"dmg": { "dmg": {
"iconSize": 110, "iconSize": 110,

11635
.versionbot/CHANGELOG.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,543 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
# v1.7.8
## (2022-03-18)
* patch: complete suse uninstall readme [Peter Makra]
* patch: completed suse instructions [Peter Makra]
* patch: order rpm instrictions [Peter Makra]
* patch: enabled update notification for version 1.7.8 [Peter Makra]
* patch: updated title to balenaEtcher [Peter Makra]
* patch: cleanup and organize readme [Peter Makra]
* patch: extend cloudsmith attribution in readme [Peter Makra]
* Update macOS Icon to Big Sur Style [Logicer]
# v1.7.7
## (2022-02-22)
* patch: clarified update check [Peter Makra]
* patch: autoupdate stagingPercentage check, include default [Peter Makra]
# v1.7.6
## (2022-02-21)
* patch: version number notification [Peter Makra]
* patch: fixed typos in template [Peter Makra]
* patch: add requirements and help to issue template [mcraa]
* patch: add requirements and help to issue template [mcraa]
# v1.7.5
## (2022-02-21)
* patch: fix flashing from URL when using basic auth [Marco Füllemann]
# v1.7.4
## (2022-02-21)
* patch: set version update notification 1.7.3 [Peter Makra]
* patch: updated electron to 12.2.3 [Peter Makra]
* patch: updated electron to 12.2.3 [Peter Makra]
# v1.7.3
## (2021-12-29)
* patch: fix mesage of null [Peter Makra]
# v1.7.2
## (2021-12-21)
* patch: fixed open from browser on windows [Peter Makra]
# v1.7.1
## (2021-11-22)
* patch: Revert back to electron-rebuild [Lorenzo Alberto Maria Ambrosi]
* patch: Disallow TS in JS [Lorenzo Alberto Maria Ambrosi]
* patch: Remove esInterop TS flag [Lorenzo Alberto Maria Ambrosi]
* patch: Use @balena/sudo-prompt [Lorenzo Alberto Maria Ambrosi]
* patch: Update rpiboot guide link [Lorenzo Alberto Maria Ambrosi]
* patch: Improve webpack build time [Lorenzo Alberto Maria Ambrosi]
# v1.7.0
## (2021-11-09)
* patch: Add missing @types/react@16.8.5 [Lorenzo Alberto Maria Ambrosi]
* patch: Use npm ci in Makefile [Lorenzo Alberto Maria Ambrosi]
* patch: Add draft info boxes for system information [Lorenzo Alberto Maria Ambrosi]
* patch: Remove electron-rebuild package [Lorenzo Alberto Maria Ambrosi]
* patch: Make electron a dev. dependency [Lorenzo Alberto Maria Ambrosi]
* patch: Remove electron-rebuild package [Lorenzo Alberto Maria Ambrosi]
* patch: Use exact modules versions [Lorenzo Alberto Maria Ambrosi]
* patch: Update etcher-sdk from v6.2.5 to v6.3.0 [Lorenzo Alberto Maria Ambrosi]
* Fix write step for Http file process [JSReds]
* patch: Fix linting errors [Lorenzo Alberto Maria Ambrosi]
* minor: Refactor dependencies installation to avoid custom scripts [Lorenzo Alberto Maria Ambrosi]
* patch: Fix LEDs init error [Lorenzo Alberto Maria Ambrosi]
# v1.6.0
## (2021-09-20)
* Add support for basic auth when downloading images from URL. [Marco Füllemann]
* patch: Update etcher-sdk from v6.2.1 to v6.2.5 [Lorenzo Alberto Maria Ambrosi]
* Update Makefile to Apple M1 info [David Gaspar]
* Add LED settings for potentially different hardware [Lorenzo Alberto Maria Ambrosi]
# v1.5.122
## (2021-09-02)
* Restore image file selection LED-drive pathing [Lorenzo Alberto Maria Ambrosi]
* Update scripts submodule [Lorenzo Alberto Maria Ambrosi]
* Change LEDs colours [Lorenzo Alberto Maria Ambrosi]
* Windows images now show the proper warning again [Lorenzo Alberto Maria Ambrosi]
* Fix Update and install with DNF instructions [Mohamed Salah]
* Add possibile authorization as a query param [JSReds]
* update the windows part [Xtraim]
* Update SUPPORT.md [thambu1710]
* replace make webpack with npm run webpack [Seth Falco]
* Add loader on image select [JSReds]
* add pnp-webpack-plugin [Zane Hitchcox]
* Remove redundant codespell dependency/tests [Lorenzo Alberto Maria Ambrosi]
# v1.5.121
## (2021-07-05)
* patch: Delete Codeowners [Vipul Gupta]
* Add source maps for devtools [Lorenzo Alberto Maria Ambrosi]
* Clone submodules when initializing modules [Lorenzo Alberto Maria Ambrosi]
* patch: Select drive on list interaction rather than modal closing [Lorenzo Alberto Maria Ambrosi]
# v1.5.120
## (2021-05-11)
* Update README to reference Cloudsmith [Lorenzo Alberto Maria Ambrosi]
# v1.5.119
## (2021-04-30)
* Update readme for new PPA provider [Lorenzo Alberto Maria Ambrosi]
# v1.5.118
## (2021-04-27)
* patch: development environment [Zane Hitchcox]
* patch: watch files for electron [Zane Hitchcox]
# v1.5.117
## (2021-04-02)
* Rename mac releases (keep old naming) [Alexis Svinartchouk]
* Disable spectron tests on macOS [Alexis Svinartchouk]
* Update electron to v12.0.2 [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 6.1.1 to 6.2.1 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-6.2.1
> ### (2021-03-26)
>
>
> <details>
> <summary> Update node-raspberrypi-usbboot from 0.2.11 to 0.3.0 [Alexis Svinartchouk] </summary>
>
>> ### node-raspberrypi-usbboot-0.3.0
>> #### (2021-03-26)
>>
>> * Add support for compute module 4 [Alexis Svinartchouk]
>> * Fix size endianness of boot_message_t message [Alexis Svinartchouk]
>>
> </details>
>
>
> ## etcher-sdk-6.2.0
> ### (2021-02-18)
>
> * Added BeagleBone USB Boot example [Parthiban Gandhi]
> * Added BeagleBone USB Boot support [Parthiban Gandhi]
>
</details>
* Fix getAppPath() returning an asar file on macOS [Alexis Svinartchouk]
* Grammar fix [Andrew Scheller]
* (docs) update README.md [vlad doster]
* Update copyright year in electron-builder.yml [Andrew Scheller]
* Update copyright year in .resinci.json [Andrew Scheller]
* Separate the Yum and DNF instructions. [Dugan Chen]
* Set msvs_version to 2019 when rebuilding [Alexis Svinartchouk]
* Use moduleIds: 'natural' in webpack config to keep js files in arm64 and x64 mac builds identical [Alexis Svinartchouk]
* Update electron-builder to 22.10.5 [Alexis Svinartchouk]
* Update spectron to v13 [Alexis Svinartchouk]
* Update dependencies, use aws4-axios@2.2.1 to avoid adding more dependiencies [Alexis Svinartchouk]
* Update scripts to build universal mac dmgs on the ci [Alexis Svinartchouk]
* Fix beforeBuild.js script to also work on mac [Alexis Svinartchouk]
* Support building universal dmgs (x64 and arm64) for mac [Alexis Svinartchouk]
* Update electron-builder to 22.10.4 [Alexis Svinartchouk]
* Fix titlebar z-index [Alexis Svinartchouk]
* Explicitly set contextIsolation to false [Alexis Svinartchouk]
* Update electron from 9.4.1 to 11.2.3 [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 6.1.0 to 6.1.1 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-6.1.1
> ### (2021-02-10)
>
>
> <details>
> <summary> Update node-raspberrypi-usbboot from 0.2.10 to 0.2.11 [Alexis Svinartchouk] </summary>
>
>> ### node-raspberrypi-usbboot-0.2.11
>> #### (2021-02-10)
>>
>> * Update @balena.io/usb from 1.3.12 to 1.3.14 [Alexis Svinartchouk]
>>
> </details>
>
>
</details>
# v1.5.116
## (2021-02-03)
* Only cleanup temporary decompressed files in child-writer [Alexis Svinartchouk]
* Add .versionbot/CHANGELOG.yml [Alexis Svinartchouk]
* Stop using node-tmp, use withTmpFile from etcher-sdk instead [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 5.2.2 to 6.1.0 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-6.1.0
> ### (2021-02-03)
>
> * Prefix temporary decompressed images filenames [Alexis Svinartchouk]
>
> ## etcher-sdk-6.0.1
> ### (2021-02-02)
>
> * Ignore ENOENT errors on unlink in withTmpFile [Alexis Svinartchouk]
>
> ## etcher-sdk-6.0.0
> ### (2021-02-01)
>
> * Export tmp and add prefix and postfix options [Alexis Svinartchouk]
>
> ## etcher-sdk-5.2.3
> ### (2021-01-26)
>
> * upgrade lint [Zane Hitchcox]
>
</details>
* Revert "Change some border colors to have higher contrast" [Alexis Svinartchouk]
* Update electron to v9.4.1 [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 5.2.1 to 5.2.2 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.2.2
> ### (2021-01-19)
>
>
> <details>
> <summary> Update drivelist from 9.2.2 to 9.2.4 [Alexis Svinartchouk] </summary>
>
>> ### drivelist-9.2.4
>> #### (2021-01-19)
>>
>> * Pass strings between methods as std::string instead of char * [Floris Bos]
>>
>> ### drivelist-9.2.3
>> #### (2021-01-19)
>>
>> * Support lsblk versions that do no support the pttype column [Alexis Svinartchouk]
>>
> </details>
>
>
</details>
# v1.5.115
## (2021-01-18)
<details>
<summary> Update etcher-sdk from 5.1.12 to 5.2.1 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.2.1
> ### (2021-01-15)
>
> * Only run one diskpart at a time [Alexis Svinartchouk]
> * Ignore diskpart VDS_E_DISK_IS_OFFLINE errors [Alexis Svinartchouk]
>
> ## etcher-sdk-5.2.0
> ### (2021-01-06)
>
> * Store progress on usbboot devices [Alexis Svinartchouk]
>
</details>
# v1.5.114
## (2021-01-12)
* Remove libappindicator1 debian dependency [Alexis Svinartchouk]
<details>
<summary> Update etcher-sdk from 5.1.11 to 5.1.12 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.1.12
> ### (2021-01-06)
>
> * Remove BlockDevice.mountpoints incorrect typing [Alexis Svinartchouk]
> * Update axios to 0.21.1 and aws4-axios to 2.0.1 [Alexis Svinartchouk]
>
</details>
<details>
<summary> Update rendition from 18.8.3 to 19.2.0 [Alexis Svinartchouk] </summary>
> ## rendition-19.2.0
> ### (2020-12-29)
>
> * Add truncate property to Txt component [JSReds]
>
> ## rendition-19.1.0
> ### (2020-12-29)
>
> * Add fallback image source to Img component [Stevche Radevski]
>
> ## rendition-19.0.0
> ### (2020-12-21)
>
> * Remove Arcslider component [Stevche Radevski]
>
> ## rendition-18.20.4
> ### (2020-12-17)
>
> * Upgrade rehype-raw to latest version [Kakhaber]
>
> ## rendition-18.20.3
> ### (2020-12-17)
>
> * Fix disabled button tooltip [JSReds]
>
> ## rendition-18.20.2
> ### (2020-12-16)
>
> * Turn keydown handler into an arrow function [Stevche Radevski]
>
> ## rendition-18.20.1
> ### (2020-12-14)
>
> * Fix form not getting the Enter key event when nested in a modal [Stevche Radevski]
>
> ## rendition-18.20.0
> ### (2020-12-14)
>
> * feat: Add new StatsBar component [Graham McCulloch]
>
> ## rendition-18.19.2
> ### (2020-12-14)
>
> * Update snapshots [Graham McCulloch]
> * Removed out-of-date documentation and template text [Graham McCulloch]
>
> ## rendition-18.19.1
> ### (2020-12-04)
>
> * Markdown: Fix line breaks [Kakhaber]
>
> ## rendition-18.19.0
> ### (2020-12-02)
>
> * Make card size responsive [Stevche Radevski]
>
> ## rendition-18.18.0
> ### (2020-12-02)
>
> * Allow passing responsive values to datagrid width props [Stevche Radevski]
>
> ## rendition-18.17.2
> ### (2020-12-01)
>
> * Update snapshots due to a Card change [JSReds]
>
> ## rendition-18.17.1
> ### (2020-12-01)
>
> * Card: make body to be full height [JSReds]
>
> ## rendition-18.17.0
> ### (2020-12-01)
>
> * Add star rating component [Kakhaber]
>
> ## rendition-18.16.0
> ### (2020-11-23)
>
> * Completely revamp the development setup for rendition [Stevche Radevski]
>
> ## rendition-18.15.1
> ### (2020-11-16)
>
> * Modal: Change the button margins to use the predefined spacing palette [Thodoris Greasidis]
>
> ## rendition-18.15.0
> ### (2020-11-16)
>
> * Modal: Move the cancel button first for dangerous & warning actions [Thodoris Greasidis]
>
> ## rendition-18.14.0
> ### (2020-11-16)
>
> * Allow passing checked items as a prop to Table [Stevche Radevski]
>
> ## rendition-18.13.4
> ### (2020-11-16)
>
> * Fix accidental complete lodash import [Thodoris Greasidis]
>
> ## rendition-18.13.3
> ### (2020-11-16)
>
> * Form: Remove the flaky Captcha sceenshot test [Thodoris Greasidis]
> * Update react-simplemde-editor & snapshots for upstream versions [Thodoris Greasidis]
>
> ## rendition-18.13.2
> ### (2020-10-29)
>
> * Updated snapshots [Graham McCulloch]
> * Fix: Confirm only depends on the files it needs [Graham McCulloch]
>
> ## rendition-18.13.1
> ### (2020-10-23)
>
> * Button: Preserve event during confirmation [Kakhaber]
>
> ## rendition-18.13.0
> ### (2020-10-22)
>
> * Button: Add confirmation property [Kakhaber]
>
> ## rendition-18.12.2
> ### (2020-10-21)
>
> * Tabs: changed interfaces and props [JSReds]
>
> ## rendition-18.12.1
> ### (2020-10-20)
>
> * Fix Tabs typings [Stevche Radevski]
>
> ## rendition-18.12.0
> ### (2020-10-19)
>
> * Add a Grid component [Stevche Radevski]
>
> ## rendition-18.11.3
> ### (2020-10-14)
>
> * Added more documentation for JsonSchemaRenderer [Graham McCulloch]
>
> ## rendition-18.11.2
> ### (2020-10-14)
>
> * fix: UI schema for JsonSchemaRenderer DropDownButton and ButtonGroup widgets [Graham McCulloch]
>
> ## rendition-18.11.1
> ### (2020-10-13)
>
> * Add dark mode to storybook [Stevche Radevski]
>
> ## rendition-18.11.0
> ### (2020-10-08)
>
> * Allow passing widget to extraFormats field [Stevche Radevski]
>
> ## rendition-18.10.2
> ### (2020-09-30)
>
> * Resolve module path not relying on node_moules dir [Kakhaber]
>
> ## rendition-18.10.1
> ### (2020-09-29)
>
> * Set tabpanel height so it stretches to full height [StefKors]
> * Specify tabs width to fix layout problems [StefKors]
>
> ## rendition-18.10.0
> ### (2020-09-24)
>
> * feat: Add ColorWidget for JsonSchemaRenderer [Graham McCulloch]
>
> ## rendition-18.9.2
> ### (2020-09-22)
>
> * Markdown: Ignore decorators inside a code block [Kakhaber]
>
> ## rendition-18.9.1
> ### (2020-09-21)
>
> * Add compact variation to tabs [StefKors]
>
> ## rendition-18.9.0
> ### (2020-09-18)
>
> * Improve spacing for Modal and Select components [Stevche Radevski]
>
> ## rendition-18.8.4
> ### (2020-09-17)
>
> * fix: Use widget's display name to reference the widget [Graham McCulloch]
>
</details>
* Update dependencies [Alexis Svinartchouk]
* Update @balena/lint to 5.3.0 [Alexis Svinartchouk]
* Update webpack to v5 [Alexis Svinartchouk]
* Fix typo in webpack.config.ts comment [Alexis Svinartchouk]
* docs: fix quote marks [Aaron Shaw]
* Disable screensaver while flashing (on balena-electron-env) [Alexis Svinartchouk]
# v1.5.113
## (2020-12-16)
* Show the first error for each drive (not the last) [Alexis Svinartchouk]
* Fix red leds not showing for failed devices [Alexis Svinartchouk]
* docs: add documentation links [Aaron Shaw]
* docs: update macOS version [Aaron Shaw]
* Improve hover message when the drive is too small [Alexis Svinartchouk]
* Update electron to v9.4.0 [Alexis Svinartchouk]
* Update npm to v6.14.8 [Giovanni Garufi]
* Update rgb leds colors [Alexis Svinartchouk]
* Remove unmountOnSuccess setting [Alexis Svinartchouk]
* Only show auto-updates setting on supported targets [Alexis Svinartchouk]
* Remove dead code in settings modal [Alexis Svinartchouk]
* Fix effective flashing speed calculation for compressed images [Alexis Svinartchouk]
* Change some border colors to have higher contrast [Lorenzo Alberto Maria Ambrosi]
<details>
<summary> Update etcher-sdk from 5.1.10 to 5.1.11 [Alexis Svinartchouk] </summary>
> ## etcher-sdk-5.1.11
> ### (2020-12-07)
>
> * Don't use the O_SYNC flag for block devices, only O_DIRECT [Alexis Svinartchouk]
>
</details>
<details>
<summary> Update sys-class-rgb-led from 2.1.1 to 3.0.0 [Alexis Svinartchouk] </summary>
> ## sys-class-rgb-led-3.0.0
> ### (2020-12-03)
>
> * Add example etcher-pro rainbow animation [Alexis Svinartchouk]
> * Use one setInterval instead of a loop for each led, t in seconds [Alexis Svinartchouk]
>
</details>
# v1.5.112 # v1.5.112
## (2020-12-02) ## (2020-12-02)

View File

@ -1,2 +0,0 @@
* @thundron @zvin @jviotti
/scripts @nazrhom

4
FAQ.md
View File

@ -37,10 +37,10 @@ modules=xwayland.so
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state. Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems. To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
## I receive ”No polkit authentication agent found” error in GNU/Linux ## I receive "No polkit authentication agent found" error in GNU/Linux
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice. Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
## May I run Etcher in older macOS versions? ## May I run Etcher in older macOS versions?
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.9 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms). Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).

View File

@ -3,7 +3,7 @@
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
RESIN_SCRIPTS ?= ./scripts/resin RESIN_SCRIPTS ?= ./scripts/resin
export NPM_VERSION ?= 6.14.5 export NPM_VERSION ?= 6.14.8
S3_BUCKET = artifacts.ci.balena-cloud.com S3_BUCKET = artifacts.ci.balena-cloud.com
# This directory will be completely deleted by the `clean` rule # This directory will be completely deleted by the `clean` rule
@ -66,6 +66,9 @@ else
ifeq ($(shell uname -m),x86_64) ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64 HOST_ARCH = x64
endif endif
ifeq ($(shell uname -m),arm64)
HOST_ARCH = aarch64
endif
endif endif
endif endif
@ -86,11 +89,9 @@ TARGET_ARCH ?= $(HOST_ARCH)
# Electron # Electron
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
electron-develop: electron-develop:
$(RESIN_SCRIPTS)/electron/install.sh \ git submodule update --init && \
-b $(shell pwd) \ npm ci && \
-r $(TARGET_ARCH) \ npm run webpack
-s $(PLATFORM) \
-m $(NPM_VERSION)
electron-test: electron-test:
$(RESIN_SCRIPTS)/electron/test.sh \ $(RESIN_SCRIPTS)/electron/test.sh \

141
README.md
View File

@ -5,16 +5,15 @@
Etcher is a powerful OS image flasher built with web technologies to ensure Etcher is a powerful OS image flasher built with web technologies to ensure
flashing an SDCard or USB drive is a pleasant and safe experience. It protects flashing an SDCard or USB drive is a pleasant and safe experience. It protects
you from accidentally writing to your hard-drives, ensures every byte of data you from accidentally writing to your hard-drives, ensures every byte of data
was written correctly and much more. It can also flash directly Raspberry Pi devices that support the usbboot protocol was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bootmodes/device.md).
[![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher) [![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher)
[![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE) [![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE)
[![Dependency status](https://img.shields.io/david/balena-io/etcher.svg?style=flat-square)](https://david-dm.org/balena-io/etcher)
[![Balena.io Forums](https://img.shields.io/discourse/https/forums.balena.io/topics.svg?style=flat-square&label=balena.io%20forums)](https://forums.balena.io/c/etcher) [![Balena.io Forums](https://img.shields.io/discourse/https/forums.balena.io/topics.svg?style=flat-square&label=balena.io%20forums)](https://forums.balena.io/c/etcher)
*** ---
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones] [**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
## Supported Operating Systems ## Supported Operating Systems
@ -22,7 +21,7 @@ was written correctly and much more. It can also flash directly Raspberry Pi dev
- macOS 10.10 (Yosemite) and later - macOS 10.10 (Yosemite) and later
- Microsoft Windows 7 and later - Microsoft Windows 7 and later
Note that Etcher will run on any platform officially supported by **Note**: Etcher will run on any platform officially supported by
[Electron][electron]. Read more in their [Electron][electron]. Read more in their
[documentation][electron-supported-platforms]. [documentation][electron-supported-platforms].
@ -31,21 +30,27 @@ Note that Etcher will run on any platform officially supported by
Refer to the [downloads page][etcher] for the latest pre-made Refer to the [downloads page][etcher] for the latest pre-made
installers for all supported operating systems. installers for all supported operating systems.
## Packages
> [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](https://cloudsmith.com) \
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
enables your organization to create, store and share packages in any format, to any place, with total
confidence.
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64) #### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
1. Add Etcher debian repository: > Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
1. Add Etcher Debian repository:
```sh ```sh
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
| sudo -E bash
``` ```
2. Trust Bintray.com's GPG key: 2. Update and install:
```sh
sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys 379CE192D401AB61
```
3. Update and install:
```sh ```sh
sudo apt-get update sudo apt-get update
@ -56,30 +61,48 @@ installers for all supported operating systems.
```sh ```sh
sudo apt-get remove balena-etcher-electron sudo apt-get remove balena-etcher-electron
sudo rm /etc/apt/sources.list.d/balena-etcher.list rm /etc/apt/sources.list.d/balena-etcher.list
sudo apt-get update apt-get clean
rm -rf /var/lib/apt/lists/*
apt-get update
``` ```
##### OpenSUSE LEAP & Tumbleweed install #### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
```sh > Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
sudo zypper ar https://balena.io/etcher/static/etcher-rpm.repo
sudo zypper ref
sudo zypper in balena-etcher-electron
```
##### Uninstall
```sh ##### DNF
sudo zypper rm balena-etcher-electron
```
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
1. Add Etcher rpm repository: 1. Add Etcher rpm repository:
```sh ```sh
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install:
```sh
sudo dnf install -y balena-etcher-electron
```
###### Uninstall
```sh
rm /etc/yum.repos.d/balena-etcher.repo
rm /etc/yum.repos.d/balena-etcher-source.repo
```
##### Yum
1. Add Etcher rpm repository:
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
``` ```
2. Update and install: 2. Update and install:
@ -87,25 +110,38 @@ sudo zypper rm balena-etcher-electron
```sh ```sh
sudo yum install -y balena-etcher-electron sudo yum install -y balena-etcher-electron
``` ```
or
###### Uninstall
```sh
sudo yum remove -y balena-etcher-electron
rm /etc/yum.repos.d/balena-etcher.repo
rm /etc/yum.repos.d/balena-etcher-source.repo
```
#### OpenSUSE LEAP & Tumbleweed install (zypper)
1. Add the repo
```sh ```sh
sudo dnf install -y balena-etcher-electron curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install
```sh
sudo zypper up
sudo zypper install balena-etcher-electron
``` ```
##### Uninstall ##### Uninstall
```sh ```sh
sudo yum remove -y balena-etcher-electron sudo zypper rm balena-etcher-electron
sudo rm /etc/yum.repos.d/etcher-rpm.repo # remove the repo
sudo yum clean all sudo zypper rr balena-etcher
sudo yum makecache fast sudo zypper rr balena-etcher-source
```
or
```sh
sudo dnf remove -y balena-etcher-electron
sudo rm /etc/yum.repos.d/etcher-rpm.repo
sudo dnf clean all
sudo dnf makecache
``` ```
#### Solus (GNU/Linux x64) #### Solus (GNU/Linux x64)
@ -120,11 +156,10 @@ sudo eopkg it etcher
sudo eopkg rm etcher sudo eopkg rm etcher
``` ```
#### Arch Linux / Manjaro (GNU/Linux x64) #### Arch/Manjaro Linux (GNU/Linux x64)
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release: Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
```sh ```sh
yay -S balena-etcher yay -S balena-etcher
``` ```
@ -135,20 +170,20 @@ yay -S balena-etcher
yay -R balena-etcher yay -R balena-etcher
``` ```
#### Brew Cask (macOS) #### Brew (macOS)
Note that the Etcher Cask has to be updated manually to point to new versions, **Note**: Etcher has to be updated manually to point to new versions,
so it might not refer to the latest version immediately after an Etcher so it might not refer to the latest version immediately after an Etcher
release. release.
```sh ```sh
brew cask install balenaetcher brew install balenaetcher
``` ```
##### Uninstall ##### Uninstall
```sh ```sh
brew cask uninstall balenaetcher brew uninstall balenaetcher
``` ```
#### Chocolatey (Windows) #### Chocolatey (Windows)
@ -168,20 +203,20 @@ choco uninstall etcher
## Support ## Support
If you're having any problem, please [raise an issue][newissue] on GitHub and If you're having any problem, please [raise an issue][newissue] on GitHub, and
the balena.io team will be happy to help. the balena.io team will be happy to help.
## License ## License
Etcher is free software, and may be redistributed under the terms specified in Etcher is free software and may be redistributed under the terms specified in
the [license]. the [license].
[etcher]: https://balena.io/etcher [etcher]: https://balena.io/etcher
[electron]: https://electronjs.org/ [electron]: https://electronjs.org/
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms [electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md [support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
[CONTRIBUTING]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md [contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
[USER-DOCUMENTATION]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md [user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[milestones]: https://github.com/balena-io/etcher/milestones [milestones]: https://github.com/balena-io/etcher/milestones
[newissue]: https://github.com/balena-io/etcher/issues/new [newissue]: https://github.com/balena-io/etcher/issues/new
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE [license]: https://github.com/balena-io/etcher/blob/master/LICENSE

View File

@ -1,9 +1,16 @@
Getting help with Etcher Getting help with BalenaEtcher
======================== ===============================
There are various ways to get support for Etcher if you experience an issue or There are various ways to get support for Etcher if you experience an issue or
have an idea you'd like to share with us. have an idea you'd like to share with us.
Documentation
------
We have answers to a variety of frequently asked questions in the [user
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
Forums Forums
------ ------
@ -15,7 +22,7 @@ a look at the existing threads before opening a new one!
Make sure to mention the following information to help us provide better Make sure to mention the following information to help us provide better
support: support:
- The Etcher version you're running. - The BalenaEtcher version you're running.
- The operating system you're running Etcher in. - The operating system you're running Etcher in.
@ -25,10 +32,12 @@ support:
GitHub GitHub
------ ------
If you encounter an issue or have a suggestion, head on over to Etcher's [issue If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
tracker][issues] and if there isn't a ticket covering it, [create tracker][issues] and if there isn't a ticket covering it, [create
one][new-issue]. one][new-issue].
[discourse]: https://forums.balena.io/c/etcher [discourse]: https://forums.balena.io/c/etcher
[issues]: https://github.com/balena-io/etcher/issues [issues]: https://github.com/balena-io/etcher/issues
[new-issue]: https://github.com/balena-io/etcher/issues/new [new-issue]: https://github.com/balena-io/etcher/issues/new
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[faq]: https://etcher.io

Binary file not shown.

View File

@ -1,26 +0,0 @@
'use strict'
const cp = require('child_process');
const rimraf = require('rimraf');
const process = require('process');
// Rebuild native modules for ia32 and run webpack again for the ia32 part of windows packages
exports.default = function(context) {
if (context.platform.name === 'windows') {
cp.execFileSync(
'bash',
['./node_modules/.bin/electron-rebuild', '--types', 'dev', '--arch', context.arch],
);
rimraf.sync('generated');
cp.execFileSync(
'bash',
['./node_modules/.bin/webpack'],
{
env: {
...process.env,
npm_config_target_arch: context.arch,
},
},
);
}
}

View File

@ -91,7 +91,7 @@ make electron-develop
```sh ```sh
# Build the GUI # Build the GUI
make webpack npm run webpack
# Start Electron # Start Electron
npm start npm start
``` ```

View File

@ -160,6 +160,18 @@ pre-installed in all modern Windows versions.
- Run `clean`. This command will completely clean your drive by erasing any - Run `clean`. This command will completely clean your drive by erasing any
existent filesystem. existent filesystem.
- Run `create partition primary`. This command will create a new partition.
- Run `active`. This command will active the partition.
- Run `list partition`. This command will show available partition.
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
- Run `exit` to quit diskpart.
### OS X ### OS X
Run the following command in `Terminal.app`, replacing `N` by the corresponding Run the following command in `Terminal.app`, replacing `N` by the corresponding

View File

@ -1,10 +1,9 @@
appId: io.balena.etcher appId: io.balena.etcher
copyright: Copyright 2016-2020 Balena Ltd copyright: Copyright 2016-2021 Balena Ltd
productName: balenaEtcher productName: balenaEtcher
npmRebuild: true npmRebuild: true
nodeGypRebuild: false nodeGypRebuild: false
publish: null publish: null
beforeBuild: "./beforeBuild.js"
afterPack: "./afterPack.js" afterPack: "./afterPack.js"
asar: false asar: false
files: files:
@ -16,6 +15,7 @@ mac:
hardenedRuntime: true hardenedRuntime: true
entitlements: "entitlements.mac.plist" entitlements: "entitlements.mac.plist"
entitlementsInherit: "entitlements.mac.plist" entitlementsInherit: "entitlements.mac.plist"
artifactName: "${productName}-${version}.${ext}"
dmg: dmg:
background: assets/dmg/background.tiff background: assets/dmg/background.tiff
icon: assets/icon.icns icon: assets/icon.icns
@ -54,7 +54,6 @@ deb:
depends: depends:
- gconf2 - gconf2
- gconf-service - gconf-service
- libappindicator1
- libasound2 - libasound2
- libatk1.0-0 - libatk1.0-0
- libc6 - libc6

View File

@ -28,7 +28,6 @@ import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages'; import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives'; import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state'; import * as flashState from './models/flash-state';
import { init as ledsInit } from './models/leds';
import { deselectImage, getImage } from './models/selection-state'; import { deselectImage, getImage } from './models/selection-state';
import * as settings from './models/settings'; import * as settings from './models/settings';
import { Actions, observe, store } from './models/store'; import { Actions, observe, store } from './models/store';
@ -38,6 +37,7 @@ import * as exceptionReporter from './modules/exception-reporter';
import * as osDialog from './os/dialog'; import * as osDialog from './os/dialog';
import * as windowProgress from './os/window-progress'; import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage'; import MainPage from './pages/main/MainPage';
import './css/main.css';
window.addEventListener( window.addEventListener(
'unhandledrejection', 'unhandledrejection',
@ -216,8 +216,7 @@ function prepareDrive(drive: Drive) {
disabled: true, disabled: true,
icon: 'warning', icon: 'warning',
size: null, size: null,
link: link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
linkCTA: 'Install', linkCTA: 'Install',
linkTitle: 'Install missing drivers', linkTitle: 'Install missing drivers',
linkMessage: outdent` linkMessage: outdent`
@ -334,13 +333,19 @@ window.addEventListener('beforeunload', async (event) => {
flashingWorkflowUuid, flashingWorkflowUuid,
}); });
popupExists = false; popupExists = false;
} catch (error) { } catch (error: any) {
exceptionReporter.report(error); exceptionReporter.report(error);
} }
}); });
async function main() { export async function main() {
try {
const { init: ledsInit } = require('./models/leds');
await ledsInit(); await ledsInit();
} catch (error: any) {
exceptionReporter.report(error);
}
ReactDOM.render( ReactDOM.render(
React.createElement(MainPage), React.createElement(MainPage),
document.getElementById('main'), document.getElementById('main'),
@ -356,5 +361,3 @@ async function main() {
}, },
); );
} }
main();

View File

@ -43,6 +43,7 @@ import {
} from '../../styled-components'; } from '../../styled-components';
import { SourceMetadata } from '../source-selector/source-selector'; import { SourceMetadata } from '../source-selector/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface UsbbootDrive extends sourceDestination.UsbbootDrive { interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number; progress: number;
@ -136,17 +137,18 @@ const InitProgress = styled(
`; `;
export interface DriveSelectorProps export interface DriveSelectorProps
extends Omit<ModalProps, 'done' | 'cancel'> { extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
write: boolean; write: boolean;
multipleSelection: boolean; multipleSelection: boolean;
showWarnings?: boolean; showWarnings?: boolean;
cancel: () => void; cancel: (drives: DrivelistDrive[]) => void;
done: (drives: DrivelistDrive[]) => void; done: (drives: DrivelistDrive[]) => void;
titleLabel: string; titleLabel: string;
emptyListLabel: string; emptyListLabel: string;
emptyListIcon: JSX.Element; emptyListIcon: JSX.Element;
selectedList?: DrivelistDrive[]; selectedList?: DrivelistDrive[];
updateSelectedList?: () => DrivelistDrive[]; updateSelectedList?: () => DrivelistDrive[];
onSelect?: (drive: DrivelistDrive) => void;
} }
interface DriveSelectorState { interface DriveSelectorState {
@ -167,12 +169,14 @@ export class DriveSelector extends React.Component<
> { > {
private unsubscribe: (() => void) | undefined; private unsubscribe: (() => void) | undefined;
tableColumns: Array<TableColumn<Drive>>; tableColumns: Array<TableColumn<Drive>>;
originalList: DrivelistDrive[];
constructor(props: DriveSelectorProps) { constructor(props: DriveSelectorProps) {
super(props); super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {}; const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = this.props.selectedList || []; const selectedList = this.props.selectedList || [];
this.originalList = [...(this.props.selectedList || [])];
this.state = { this.state = {
drives: getDrives(), drives: getDrives(),
@ -199,7 +203,9 @@ export class DriveSelector extends React.Component<
fill={drive.isSystem ? '#fca321' : '#8f9297'} fill={drive.isSystem ? '#fca321' : '#8f9297'}
/> />
)} )}
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt> <Txt ml={(hasWarnings && 8) || 0}>
{middleEllipsis(description, 32)}
</Txt>
</Flex> </Flex>
); );
} }
@ -259,7 +265,7 @@ export class DriveSelector extends React.Component<
return ( return (
isUsbbootDrive(drive) || isUsbbootDrive(drive) ||
isDriverlessDrive(drive) || isDriverlessDrive(drive) ||
!isDriveValid(drive, image) || !isDriveValid(drive, image, this.props.write) ||
(this.props.write && drive.isReadOnly) (this.props.write && drive.isReadOnly)
); );
} }
@ -303,9 +309,9 @@ export class DriveSelector extends React.Component<
case compatibility.system(): case compatibility.system():
return warning.systemDrive(); return warning.systemDrive();
case compatibility.tooSmall(): case compatibility.tooSmall():
const recommendedDriveSize = const size =
this.state.image?.recommendedDriveSize || this.state.image?.size || 0; this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive); return warning.tooSmall({ size }, drive);
} }
} }
@ -348,16 +354,6 @@ export class DriveSelector extends React.Component<
} }
} }
private deselectingAll(rows: DrivelistDrive[]) {
return (
rows.length > 0 &&
rows.length === this.state.selectedList.length &&
this.state.selectedList.every(
(d) => rows.findIndex((r) => d.device === r.device) > -1,
)
);
}
componentDidMount() { componentDidMount() {
this.unsubscribe = store.subscribe(() => { this.unsubscribe = store.subscribe(() => {
const drives = getDrives(); const drives = getDrives();
@ -383,8 +379,8 @@ export class DriveSelector extends React.Component<
const displayedDrives = this.getDisplayedDrives(drives); const displayedDrives = this.getDisplayedDrives(drives);
const disabledDrives = this.getDisabledDrives(drives, image); const disabledDrives = this.getDisabledDrives(drives, image);
const numberOfSystemDrives = drives.filter(isSystemDrive).length; const numberOfSystemDrives = drives.filter(isSystemDrive).length;
const numberOfDisplayedSystemDrives = displayedDrives.filter(isSystemDrive) const numberOfDisplayedSystemDrives =
.length; displayedDrives.filter(isSystemDrive).length;
const numberOfHiddenSystemDrives = const numberOfHiddenSystemDrives =
numberOfSystemDrives - numberOfDisplayedSystemDrives; numberOfSystemDrives - numberOfDisplayedSystemDrives;
const hasSystemDrives = selectedList.filter(isSystemDrive).length; const hasSystemDrives = selectedList.filter(isSystemDrive).length;
@ -408,7 +404,7 @@ export class DriveSelector extends React.Component<
</Flex> </Flex>
} }
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>} titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={cancel} cancel={() => cancel(this.originalList)}
done={() => done(selectedList)} done={() => done(selectedList)}
action={`Select (${selectedList.length})`} action={`Select (${selectedList.length})`}
primaryButtonProps={{ primaryButtonProps={{
@ -448,14 +444,34 @@ export class DriveSelector extends React.Component<
onCheck={(rows: Drive[]) => { onCheck={(rows: Drive[]) => {
let newSelection = rows.filter(isDrivelistDrive); let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) { if (this.props.multipleSelection) {
if (this.deselectingAll(newSelection)) { if (rows.length === 0) {
newSelection = []; newSelection = [];
} }
const deselecting = selectedList.filter(
(selected) =>
newSelection.filter(
(row) => row.device === selected.device,
).length === 0,
);
const selecting = newSelection.filter(
(row) =>
selectedList.filter(
(selected) => row.device === selected.device,
).length === 0,
);
deselecting.concat(selecting).forEach((row) => {
if (this.props.onSelect) {
this.props.onSelect(row);
}
});
this.setState({ this.setState({
selectedList: newSelection, selectedList: newSelection,
}); });
return; return;
} }
if (this.props.onSelect) {
this.props.onSelect(newSelection[newSelection.length - 1]);
}
this.setState({ this.setState({
selectedList: newSelection.slice(newSelection.length - 1), selectedList: newSelection.slice(newSelection.length - 1),
}); });
@ -467,6 +483,9 @@ export class DriveSelector extends React.Component<
) { ) {
return; return;
} }
if (this.props.onSelect) {
this.props.onSelect(row);
}
const index = selectedList.findIndex( const index = selectedList.findIndex(
(d) => d.device === row.device, (d) => d.device === row.device,
); );
@ -515,7 +534,7 @@ export class DriveSelector extends React.Component<
if (missingDriversModal.drive !== undefined) { if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link); openExternal(missingDriversModal.drive.link);
} }
} catch (error) { } catch (error: any) {
logException(error); logException(error);
} finally { } finally {
this.setState({ missingDriversModal: {} }); this.setState({ missingDriversModal: {} });

View File

@ -59,13 +59,8 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
).map(([, error]: [string, FlashError]) => ({ ).map(([, error]: [string, FlashError]) => ({
...error, ...error,
})); }));
const { const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
averageSpeed, flashState.getFlashState();
blockmappedSize,
bytesWritten,
failed,
size,
} = flashState.getFlashState();
const { const {
skip, skip,
results = { results = {

View File

@ -17,7 +17,6 @@
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg'; import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg'; import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg'; import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
import * as _ from 'lodash';
import outdent from 'outdent'; import outdent from 'outdent';
import * as React from 'react'; import * as React from 'react';
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition'; import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
@ -104,6 +103,19 @@ const columns: Array<TableColumn<FlashError>> = [
}, },
]; ];
function getEffectiveSpeed(results: {
sourceMetadata: {
size: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
}) {
const flashedSize =
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
const timeSpent = flashedSize / results.averageFlashingSpeed;
return results.sourceMetadata.size / timeSpent;
}
export function FlashResults({ export function FlashResults({
goToMain, goToMain,
image = '', image = '',
@ -117,10 +129,9 @@ export function FlashResults({
errors: FlashError[]; errors: FlashError[];
skip: boolean; skip: boolean;
results: { results: {
bytesWritten: number;
sourceMetadata: { sourceMetadata: {
size: number; size: number;
blockmappedSize: number; blockmappedSize?: number;
}; };
averageFlashingSpeed: number; averageFlashingSpeed: number;
devices: { failed: number; successful: number }; devices: { failed: number; successful: number };
@ -129,11 +140,7 @@ export function FlashResults({
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false); const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = !skip && results.devices.successful === 0; const allFailed = !skip && results.devices.successful === 0;
const someFailed = results.devices.failed !== 0 || errors.length !== 0; const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const effectiveSpeed = _.round( const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
bytesToMegabytes(
results.sourceMetadata.size /
(results.sourceMetadata.blockmappedSize / results.averageFlashingSpeed),
),
1, 1,
); );
return ( return (

View File

@ -31,9 +31,7 @@ interface ReducedFlashingInfosProps {
style?: React.CSSProperties; style?: React.CSSProperties;
} }
export class ReducedFlashingInfos extends React.Component< export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
ReducedFlashingInfosProps
> {
constructor(props: ReducedFlashingInfosProps) { constructor(props: ReducedFlashingInfosProps) {
super(props); super(props);
this.state = {}; this.state = {};

View File

@ -16,12 +16,13 @@
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg'; import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as os from 'os';
import * as React from 'react'; import * as React from 'react';
import { Box, Button, Flex, Checkbox, Txt } from 'rendition';
import { Box, Button, Flex, Checkbox, TextWithCopy, Txt } from 'rendition';
import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { version, packageType } from '../../../../../package.json'; import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
@ -29,47 +30,41 @@ import { open as openExternal } from '../../os/open-external/services/open-exter
import { Modal } from '../../styled-components'; import { Modal } from '../../styled-components';
import { unlinkSync, readFileSync } from 'fs'; import { unlinkSync, readFileSync } from 'fs';
const platform = os.platform();
interface Setting { interface Setting {
name: string; name: string;
label: string | JSX.Element; label: string | JSX.Element;
options?: {
description: string;
confirmLabel: string;
};
hide?: boolean;
} }
async function getSettingsList(): Promise<Setting[]> { async function getSettingsList(): Promise<Setting[]> {
return [ const list: Setting[] = [
{ {
name: 'errorReporting', name: 'errorReporting',
label: 'Anonymously report errors and usage statistics to balena.io', label: 'Anonymously report errors and usage statistics to balena.io',
}, },
{ ];
name: 'unmountOnSuccess', if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
/** list.push({
* On Windows, "Unmounting" basically means "ejecting".
* On top of that, Windows users are usually not even
* familiar with the meaning of "unmount", which comes
* from the UNIX world.
*/
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
},
{
name: 'updatesEnabled', name: 'updatesEnabled',
label: 'Auto-updates enabled', label: 'Auto-updates enabled',
hide: ['rpm', 'deb'].includes(packageType), });
}, }
]; return list;
} }
interface SettingsModalProps { interface SettingsModalProps {
toggleModal: (value: boolean) => void; toggleModal: (value: boolean) => void;
} }
export function SettingsModal({ toggleModal }: SettingsModalProps): any { const UUID = process.env.BALENA_DEVICE_UUID;
const InfoBox = (props: any) => (
<Box fontSize={14}>
<Txt>{props.label}</Txt>
<TextWithCopy code text={props.value} copy={props.value} />
</Box>
);
export function SettingsModal({ toggleModal }: SettingsModalProps) {
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]); const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
const [showDiagScreen, setShowDiagScreen] = React.useState<boolean>(false); const [showDiagScreen, setShowDiagScreen] = React.useState<boolean>(false);
const [diagApiIsUp, setDiagApiIsUp] = React.useState<boolean>(false); const [diagApiIsUp, setDiagApiIsUp] = React.useState<boolean>(false);
@ -110,25 +105,16 @@ export function SettingsModal({ toggleModal }: SettingsModalProps): any {
})(); })();
}, []); }, []);
const toggleSetting = async (
setting: string, const toggleSetting = async (setting: string) => {
options?: Setting['options'],
) => {
const value = currentSettings[setting]; const value = currentSettings[setting];
const dangerous = options !== undefined; analytics.logEvent('Toggle setting', { setting, value });
analytics.logEvent('Toggle setting', {
setting,
value,
dangerous,
});
await settings.set(setting, !value); await settings.set(setting, !value);
setCurrentSettings({ setCurrentSettings({
...currentSettings, ...currentSettings,
[setting]: !value, [setting]: !value,
}); });
return;
}; };
const closeDiagFrame = () => { const closeDiagFrame = () => {
@ -227,18 +213,24 @@ export function SettingsModal({ toggleModal }: SettingsModalProps): any {
> >
<Flex flexDirection="column"> <Flex flexDirection="column">
{settingsList.map((setting: Setting, i: number) => { {settingsList.map((setting: Setting, i: number) => {
return setting.hide ? null : ( return (
<Flex key={setting.name} mb={14}> <Flex key={setting.name} mb={14}>
<Checkbox <Checkbox
toggle toggle
tabIndex={6 + i} tabIndex={6 + i}
label={setting.label} label={setting.label}
checked={currentSettings[setting.name]} checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)} onChange={() => toggleSetting(setting.name)}
/> />
</Flex> </Flex>
); );
})} })}
{UUID !== undefined && (
<Flex flexDirection="column">
<Txt fontSize={24}>System Information</Txt>
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
</Flex>
)}
<Flex <Flex
mt={18} mt={18}
alignItems="center" alignItems="center"

View File

@ -18,6 +18,8 @@ import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg'; import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg'; import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
import { sourceDestination } from 'etcher-sdk'; import { sourceDestination } from 'etcher-sdk';
import { ipcRenderer, IpcRendererEvent } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -33,6 +35,7 @@ import {
Card as BaseCard, Card as BaseCard,
Input, Input,
Spinner, Spinner,
Link,
} from 'rendition'; } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
@ -61,6 +64,8 @@ import ImageSvg from '../../../assets/image.svg';
import SrcSvg from '../../../assets/src.svg'; import SrcSvg from '../../../assets/src.svg';
import { DriveSelector } from '../drive-selector/drive-selector'; import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints'; import { DrivelistDrive } from '../../../../shared/drive-constraints';
import axios, { AxiosRequestConfig } from 'axios';
import { isJson } from '../../../../shared/utils';
const recentUrlImagesKey = 'recentUrlImages'; const recentUrlImagesKey = 'recentUrlImages';
@ -72,7 +77,7 @@ function normalizeRecentUrlImages(urls: any[]): URL[] {
.map((url) => { .map((url) => {
try { try {
return new URL(url); return new URL(url);
} catch (error) { } catch (error: any) {
// Invalid URL, skip // Invalid URL, skip
} }
}) })
@ -133,12 +138,15 @@ const URLSelector = ({
done, done,
cancel, cancel,
}: { }: {
done: (imageURL: string) => void; done: (imageURL: string, auth?: Authentication) => void;
cancel: () => void; cancel: () => void;
}) => { }) => {
const [imageURL, setImageURL] = React.useState(''); const [imageURL, setImageURL] = React.useState('');
const [recentImages, setRecentImages] = React.useState<URL[]>([]); const [recentImages, setRecentImages] = React.useState<URL[]>([]);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
React.useEffect(() => { React.useEffect(() => {
const fetchRecentUrlImages = async () => { const fetchRecentUrlImages = async () => {
const recentUrlImages: URL[] = await getRecentUrlImages(); const recentUrlImages: URL[] = await getRecentUrlImages();
@ -161,11 +169,12 @@ const URLSelector = ({
imageURL, imageURL,
]); ]);
setRecentUrlImages(normalizedRecentUrls); setRecentUrlImages(normalizedRecentUrls);
await done(imageURL); const auth = username ? { username, password } : undefined;
await done(imageURL, auth);
}} }}
> >
<Flex flexDirection="column"> <Flex flexDirection="column">
<Flex style={{ width: '100%' }} flexDirection="column"> <Flex mb={15} style={{ width: '100%' }} flexDirection="column">
<Txt mb="10px" fontSize="24px"> <Txt mb="10px" fontSize="24px">
Use Image URL Use Image URL
</Txt> </Txt>
@ -177,6 +186,49 @@ const URLSelector = ({
setImageURL(evt.target.value) setImageURL(evt.target.value)
} }
/> />
<Link
mt={15}
mb={15}
fontSize="14px"
onClick={() => {
if (showBasicAuth) {
setUsername('');
setPassword('');
}
setShowBasicAuth(!showBasicAuth);
}}
>
<Flex alignItems="center">
{showBasicAuth && (
<ChevronDownSvg height="1em" fill="currentColor" />
)}
{!showBasicAuth && (
<ChevronRightSvg height="1em" fill="currentColor" />
)}
<Txt ml={8}>Authentication</Txt>
</Flex>
</Link>
{showBasicAuth && (
<React.Fragment>
<Input
mb={15}
value={username}
placeholder="Enter username"
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setUsername(evt.target.value)
}
/>
<Input
value={password}
placeholder="Enter password"
type="password"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setPassword(evt.target.value)
}
/>
</React.Fragment>
)}
</Flex> </Flex>
{recentImages.length > 0 && ( {recentImages.length > 0 && (
<Flex flexDirection="column" height="78.6%"> <Flex flexDirection="column" height="78.6%">
@ -263,6 +315,7 @@ export interface SourceMetadata extends sourceDestination.Metadata {
drive?: DrivelistDrive; drive?: DrivelistDrive;
extension?: string; extension?: string;
archiveExtension?: string; archiveExtension?: string;
auth?: Authentication;
} }
interface SourceSelectorProps { interface SourceSelectorProps {
@ -279,6 +332,12 @@ interface SourceSelectorState {
showDriveSelector: boolean; showDriveSelector: boolean;
defaultFlowActive: boolean; defaultFlowActive: boolean;
imageSelectorOpen: boolean; imageSelectorOpen: boolean;
imageLoading: boolean;
}
interface Authentication {
username: string;
password: string;
} }
export class SourceSelector extends React.Component< export class SourceSelector extends React.Component<
@ -297,6 +356,7 @@ export class SourceSelector extends React.Component<
showDriveSelector: false, showDriveSelector: false,
defaultFlowActive: true, defaultFlowActive: true,
imageSelectorOpen: false, imageSelectorOpen: false,
imageLoading: false,
}; };
// Bind `this` since it's used in an event's callback // Bind `this` since it's used in an event's callback
@ -317,25 +377,52 @@ export class SourceSelector extends React.Component<
} }
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) { private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
this.setState({ imageLoading: true });
await this.selectSource( await this.selectSource(
imagePath, imagePath,
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File, isURL(this.normalizeImagePath(imagePath))
? sourceDestination.Http
: sourceDestination.File,
).promise; ).promise;
this.setState({ imageLoading: false });
} }
private async createSource(selected: string, SourceType: Source) { private async createSource(
selected: string,
SourceType: Source,
auth?: Authentication,
) {
try { try {
selected = await replaceWindowsNetworkDriveLetter(selected); selected = await replaceWindowsNetworkDriveLetter(selected);
} catch (error) { } catch (error: any) {
analytics.logException(error); analytics.logException(error);
} }
if (isJson(decodeURIComponent(selected))) {
const config: AxiosRequestConfig = JSON.parse(
decodeURIComponent(selected),
);
return new sourceDestination.Http({
url: config.url!,
axiosInstance: axios.create(_.omit(config, ['url'])),
});
}
if (SourceType === sourceDestination.File) { if (SourceType === sourceDestination.File) {
return new sourceDestination.File({ return new sourceDestination.File({
path: selected, path: selected,
}); });
} }
return new sourceDestination.Http({ url: selected });
return new sourceDestination.Http({ url: selected, auth });
}
public normalizeImagePath(imgPath: string) {
const decodedPath = decodeURIComponent(imgPath);
if (isJson(decodedPath)) {
return JSON.parse(decodedPath).url ?? decodedPath;
}
return decodedPath;
} }
private reselectSource() { private reselectSource() {
@ -349,6 +436,7 @@ export class SourceSelector extends React.Component<
private selectSource( private selectSource(
selected: string | DrivelistDrive, selected: string | DrivelistDrive,
SourceType: Source, SourceType: Source,
auth?: Authentication,
): { promise: Promise<void>; cancel: () => void } { ): { promise: Promise<void>; cancel: () => void } {
let cancelled = false; let cancelled = false;
return { return {
@ -360,7 +448,10 @@ export class SourceSelector extends React.Component<
let source; let source;
let metadata: SourceMetadata | undefined; let metadata: SourceMetadata | undefined;
if (isString(selected)) { if (isString(selected)) {
if (SourceType === sourceDestination.Http && !isURL(selected)) { if (
SourceType === sourceDestination.Http &&
!isURL(this.normalizeImagePath(selected))
) {
this.handleError( this.handleError(
'Unsupported protocol', 'Unsupported protocol',
selected, selected,
@ -378,7 +469,7 @@ export class SourceSelector extends React.Component<
}, },
}); });
} }
source = await this.createSource(selected, SourceType); source = await this.createSource(selected, SourceType, auth);
if (cancelled) { if (cancelled) {
return; return;
@ -395,7 +486,7 @@ export class SourceSelector extends React.Component<
} }
metadata.SourceType = SourceType; metadata.SourceType = SourceType;
if (!metadata.hasMBR) { if (!metadata.hasMBR && this.state.warning === null) {
analytics.logEvent('Missing partition table', { metadata }); analytics.logEvent('Missing partition table', { metadata });
this.setState({ this.setState({
warning: { warning: {
@ -404,7 +495,7 @@ export class SourceSelector extends React.Component<
}, },
}); });
} }
} catch (error) { } catch (error: any) {
this.handleError( this.handleError(
'Error opening source', 'Error opening source',
sourcePath, sourcePath,
@ -414,7 +505,7 @@ export class SourceSelector extends React.Component<
} finally { } finally {
try { try {
await source.close(); await source.close();
} catch (error) { } catch (error: any) {
// Noop // Noop
} }
} }
@ -439,6 +530,7 @@ export class SourceSelector extends React.Component<
} }
if (metadata !== undefined) { if (metadata !== undefined) {
metadata.auth = auth;
selectionState.selectSource(metadata); selectionState.selectSource(metadata);
analytics.logEvent('Select image', { analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of // An easy way so we can quickly identify if we're making use of
@ -504,7 +596,7 @@ export class SourceSelector extends React.Component<
return; return;
} }
await this.selectSource(imagePath, sourceDestination.File).promise; await this.selectSource(imagePath, sourceDestination.File).promise;
} catch (error) { } catch (error: any) {
exceptionReporter.report(error); exceptionReporter.report(error);
} finally { } finally {
this.setState({ imageSelectorOpen: false }); this.setState({ imageSelectorOpen: false });
@ -558,10 +650,21 @@ export class SourceSelector extends React.Component<
this.setState({ defaultFlowActive }); this.setState({ defaultFlowActive });
} }
private closeModal() {
this.setState({
showDriveSelector: false,
});
}
// TODO add a visual change when dragging a file over the selector // TODO add a visual change when dragging a file over the selector
public render() { public render() {
const { flashing } = this.props; const { flashing } = this.props;
const { showImageDetails, showURLSelector, showDriveSelector } = this.state; const {
showImageDetails,
showURLSelector,
showDriveSelector,
imageLoading,
} = this.state;
const selectionImage = selectionState.getImage(); const selectionImage = selectionState.getImage();
let image: SourceMetadata | DrivelistDrive = let image: SourceMetadata | DrivelistDrive =
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata); selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
@ -599,16 +702,18 @@ export class SourceSelector extends React.Component<
}} }}
/> />
{selectionImage !== undefined ? ( {selectionImage !== undefined || imageLoading ? (
<> <>
<StepNameButton <StepNameButton
plain plain
onClick={() => this.showSelectedImageDetails()} onClick={() => this.showSelectedImageDetails()}
tooltip={imageName || imageBasename} tooltip={imageName || imageBasename}
> >
<Spinner show={imageLoading}>
{middleEllipsis(imageName || imageBasename, 20)} {middleEllipsis(imageName || imageBasename, 20)}
</Spinner>
</StepNameButton> </StepNameButton>
{!flashing && ( {!flashing && !imageLoading && (
<ChangeButton <ChangeButton
plain plain
mb={14} mb={14}
@ -617,7 +722,7 @@ export class SourceSelector extends React.Component<
Remove Remove
</ChangeButton> </ChangeButton>
)} )}
{!_.isNil(imageSize) && ( {!_.isNil(imageSize) && !imageLoading && (
<DetailsText>{prettyBytes(imageSize)}</DetailsText> <DetailsText>{prettyBytes(imageSize)}</DetailsText>
)} )}
</> </>
@ -661,6 +766,9 @@ export class SourceSelector extends React.Component<
{this.state.warning != null && ( {this.state.warning != null && (
<SmallModal <SmallModal
style={{
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
}}
titleElement={ titleElement={
<span> <span>
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '} <ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
@ -709,7 +817,7 @@ export class SourceSelector extends React.Component<
showURLSelector: false, showURLSelector: false,
}); });
}} }}
done={async (imageURL: string) => { done={async (imageURL: string, auth?: Authentication) => {
// Avoid analytics and selection state changes // Avoid analytics and selection state changes
// if no file was resolved from the dialog. // if no file was resolved from the dialog.
if (!imageURL) { if (!imageURL) {
@ -719,6 +827,7 @@ export class SourceSelector extends React.Component<
({ promise, cancel: cancelURLSelection } = this.selectSource( ({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL, imageURL,
sourceDestination.Http, sourceDestination.Http,
auth,
)); ));
await promise; await promise;
} }
@ -736,21 +845,30 @@ export class SourceSelector extends React.Component<
titleLabel="Select source" titleLabel="Select source"
emptyListLabel="Plug a source drive" emptyListLabel="Plug a source drive"
emptyListIcon={<SrcSvg width="40px" />} emptyListIcon={<SrcSvg width="40px" />}
cancel={() => { cancel={(originalList) => {
this.setState({ if (originalList.length) {
showDriveSelector: false, const originalSource = originalList[0];
}); if (selectionImage?.drive?.device !== originalSource.device) {
}} this.selectSource(
done={async (drives: DrivelistDrive[]) => { originalSource,
if (drives.length) {
await this.selectSource(
drives[0],
sourceDestination.BlockDevice, sourceDestination.BlockDevice,
); );
} }
this.setState({ } else {
showDriveSelector: false, selectionState.deselectImage();
}); }
this.closeModal();
}}
done={() => this.closeModal()}
onSelect={(drive) => {
if (drive) {
if (
selectionState.getImage()?.drive?.device === drive?.device
) {
return selectionState.deselectImage();
}
this.selectSource(drive, sourceDestination.BlockDevice);
}
}} }}
/> />
)} )}

View File

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { scanner } from 'etcher-sdk';
import * as React from 'react'; import * as React from 'react';
import { Flex, Txt } from 'rendition'; import { Flex, Txt } from 'rendition';
@ -28,6 +27,7 @@ import {
getSelectedDrives, getSelectedDrives,
deselectDrive, deselectDrive,
selectDrive, selectDrive,
deselectAllDrives,
} from '../../models/selection-state'; } from '../../models/selection-state';
import { observe } from '../../models/store'; import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
@ -36,6 +36,7 @@ import { TargetSelectorButton } from './target-selector-button';
import TgtSvg from '../../../assets/tgt.svg'; import TgtSvg from '../../../assets/tgt.svg';
import DriveSvg from '../../../assets/drive.svg'; import DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages'; import { warning } from '../../../../shared/messages';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
export const getDriveListLabel = () => { export const getDriveListLabel = () => {
return getSelectedDrives() return getSelectedDrives()
@ -69,9 +70,7 @@ export const TargetSelectorModal = (
/> />
); );
export const selectAllTargets = ( export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
modalTargets: scanner.adapters.DrivelistDrive[],
) => {
const selectedDrivesFromState = getSelectedDrives(); const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter( const deselected = selectedDrivesFromState.filter(
(drive) => (drive) =>
@ -113,9 +112,8 @@ export const TargetSelector = ({
const [{ driveListLabel, targets }, setStateSlice] = React.useState( const [{ driveListLabel, targets }, setStateSlice] = React.useState(
getDriveSelectionStateSlice(), getDriveSelectionStateSlice(),
); );
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState( const [showTargetSelectorModal, setShowTargetSelectorModal] =
false, React.useState(false);
);
React.useEffect(() => { React.useEffect(() => {
return observe(() => { return observe(() => {
@ -164,11 +162,30 @@ export const TargetSelector = ({
{showTargetSelectorModal && ( {showTargetSelectorModal && (
<TargetSelectorModal <TargetSelectorModal
write={true} write={true}
cancel={() => setShowTargetSelectorModal(false)} cancel={(originalList) => {
done={(modalTargets) => { if (originalList.length) {
selectAllTargets(modalTargets); selectAllTargets(originalList);
} else {
deselectAllDrives();
}
setShowTargetSelectorModal(false); setShowTargetSelectorModal(false);
}} }}
done={(modalTargets) => {
if (modalTargets.length === 0) {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
onSelect={(drive) => {
if (
getSelectedDrives().find(
(selectedDrive) => selectedDrive.device === drive.device,
)
) {
return deselectDrive(drive.device);
}
selectDrive(drive.device);
}}
/> />
)} )}
</Flex> </Flex>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<main id="main"></main>
<script src="http://localhost:3030/gui.js"></script>
</body>
</html>

View File

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Etcher</title> <title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css"> <link rel="stylesheet" type="text/css" href="index.css">
</head> </head>
<body> <body>

View File

@ -14,8 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import * as electron from 'electron';
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { DrivelistDrive } from '../../../shared/drive-constraints';
import { bytesToMegabytes } from '../../../shared/units'; import { bytesToMegabytes } from '../../../shared/units';
import { Actions, store } from './store'; import { Actions, store } from './store';
@ -45,6 +47,8 @@ export function isFlashing(): boolean {
* start a flash process. * start a flash process.
*/ */
export function setFlashingFlag() { export function setFlashingFlag() {
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.send('disable-screensaver');
store.dispatch({ store.dispatch({
type: Actions.SET_FLASHING_FLAG, type: Actions.SET_FLASHING_FLAG,
data: {}, data: {},
@ -66,6 +70,8 @@ export function unsetFlashingFlag(results: {
type: Actions.UNSET_FLASHING_FLAG, type: Actions.UNSET_FLASHING_FLAG,
data: results, data: results,
}); });
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.send('enable-screensaver');
} }
export function setDevicePaths(devicePaths: string[]) { export function setDevicePaths(devicePaths: string[]) {
@ -79,12 +85,16 @@ export function addFailedDeviceError({
device, device,
error, error,
}: { }: {
device: sdk.scanner.adapters.DrivelistDrive; device: DrivelistDrive;
error: Error; error: Error;
}) { }) {
const failedDeviceErrorsMap = new Map( const failedDeviceErrorsMap = new Map(
store.getState().toJS().failedDeviceErrors, store.getState().toJS().failedDeviceErrors,
); );
if (failedDeviceErrorsMap.has(device.device)) {
// Only store the first error
return;
}
failedDeviceErrorsMap.set(device.device, { failedDeviceErrorsMap.set(device.device, {
description: device.description, description: device.description,
device: device.device, device: device.device,

View File

@ -15,40 +15,19 @@
*/ */
import * as _ from 'lodash'; import * as _ from 'lodash';
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led'; import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
import { import {
isSourceDrive,
DrivelistDrive, DrivelistDrive,
isSourceDrive,
} from '../../../shared/drive-constraints'; } from '../../../shared/drive-constraints';
import { getDrives } from './available-drives';
import { getSelectedDrives } from './selection-state';
import * as settings from './settings'; import * as settings from './settings';
import { DEFAULT_STATE, observe } from './store'; import { observe, store } from './store';
const leds: Map<string, RGBLed> = new Map(); const leds: Map<string, RGBLed> = new Map();
const animator = new Animator([], 10);
function setLeds(
drivesPaths: Set<string>,
colorOrAnimation: Color | AnimationFunction,
frequency?: number,
) {
for (const path of drivesPaths) {
const led = leds.get(path);
if (led) {
if (Array.isArray(colorOrAnimation)) {
led.setStaticColor(colorOrAnimation);
} else {
led.setAnimation(colorOrAnimation, frequency);
}
}
}
}
const red: Color = [1, 0, 0];
const green: Color = [0, 1, 0];
const blue: Color = [0, 0, 1];
const white: Color = [1, 1, 1];
const black: Color = [0, 0, 0];
const purple: Color = [0.5, 0, 0.5];
function createAnimationFunction( function createAnimationFunction(
intensityFunction: (t: number) => number, intensityFunction: (t: number) => number,
@ -56,21 +35,39 @@ function createAnimationFunction(
): AnimationFunction { ): AnimationFunction {
return (t: number): Color => { return (t: number): Color => {
const intensity = intensityFunction(t); const intensity = intensityFunction(t);
return color.map((v) => v * intensity) as Color; return color.map((v: number) => v * intensity) as Color;
}; };
} }
function blink(t: number) { function blink(t: number) {
return Math.floor(t / 1000) % 2; return Math.floor(t) % 2;
} }
function breathe(t: number) { function one(_t: number) {
return (1 + Math.sin(t / 1000)) / 2; return 1;
} }
const breatheBlue = createAnimationFunction(breathe, blue); type LEDColors = {
const blinkGreen = createAnimationFunction(blink, green); green: Color;
const blinkPurple = createAnimationFunction(blink, purple); purple: Color;
red: Color;
blue: Color;
white: Color;
black: Color;
};
type LEDAnimationFunctions = {
blinkGreen: AnimationFunction;
blinkPurple: AnimationFunction;
staticRed: AnimationFunction;
staticGreen: AnimationFunction;
staticBlue: AnimationFunction;
staticWhite: AnimationFunction;
staticBlack: AnimationFunction;
};
let ledColors: LEDColors;
let ledAnimationFunctions: LEDAnimationFunctions;
interface LedsState { interface LedsState {
step: 'main' | 'flashing' | 'verifying' | 'finish'; step: 'main' | 'flashing' | 'verifying' | 'finish';
@ -80,6 +77,17 @@ interface LedsState {
failedDrives: string[]; failedDrives: string[];
} }
function setLeds(animation: AnimationFunction, drivesPaths: Set<string>) {
const rgbLeds: RGBLed[] = [];
for (const path of drivesPaths) {
const led = leds.get(path);
if (led) {
rgbLeds.push(led);
}
}
return { animation, rgbLeds };
}
// Source slot (1st slot): behaves as a target unless it is chosen as source // Source slot (1st slot): behaves as a target unless it is chosen as source
// No drive: black // No drive: black
// Drive plugged: blue - on // Drive plugged: blue - on
@ -110,6 +118,7 @@ export function updateLeds({
// Remove selected devices from plugged set // Remove selected devices from plugged set
for (const d of selectedOk) { for (const d of selectedOk) {
plugged.delete(d); plugged.delete(d);
unplugged.delete(d);
} }
// Remove plugged devices from unplugged set // Remove plugged devices from unplugged set
@ -122,74 +131,90 @@ export function updateLeds({
selectedOk.delete(d); selectedOk.delete(d);
} }
const mapping: Array<{
animation: AnimationFunction;
rgbLeds: RGBLed[];
}> = [];
// Handle source slot // Handle source slot
if (sourceDrive !== undefined) { if (sourceDrive !== undefined) {
if (unplugged.has(sourceDrive)) { if (plugged.has(sourceDrive)) {
unplugged.delete(sourceDrive);
// TODO
setLeds(new Set([sourceDrive]), breatheBlue, 2);
} else if (plugged.has(sourceDrive)) {
plugged.delete(sourceDrive); plugged.delete(sourceDrive);
setLeds(new Set([sourceDrive]), blue); mapping.push(
setLeds(ledAnimationFunctions.staticBlue, new Set([sourceDrive])),
);
} }
} }
if (step === 'main') { if (step === 'main') {
setLeds(unplugged, black); mapping.push(
setLeds(plugged, black); setLeds(
setLeds(selectedOk, white); ledAnimationFunctions.staticBlack,
setLeds(selectedFailed, white); new Set([...unplugged, ...plugged]),
),
setLeds(
ledAnimationFunctions.staticWhite,
new Set([...selectedOk, ...selectedFailed]),
),
);
} else if (step === 'flashing') { } else if (step === 'flashing') {
setLeds(unplugged, black); mapping.push(
setLeds(plugged, black); setLeds(
setLeds(selectedOk, blinkPurple, 2); ledAnimationFunctions.staticBlack,
setLeds(selectedFailed, red); new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.blinkPurple, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
} else if (step === 'verifying') { } else if (step === 'verifying') {
setLeds(unplugged, black); mapping.push(
setLeds(plugged, black); setLeds(
setLeds(selectedOk, blinkGreen, 2); ledAnimationFunctions.staticBlack,
setLeds(selectedFailed, red); new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.blinkGreen, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
} else if (step === 'finish') { } else if (step === 'finish') {
setLeds(unplugged, black); mapping.push(
setLeds(plugged, black); setLeds(
setLeds(selectedOk, green); ledAnimationFunctions.staticBlack,
setLeds(selectedFailed, red); new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.staticGreen, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
} }
} animator.mapping = mapping;
interface DeviceFromState {
devicePath?: string;
device: string;
} }
let ledsState: LedsState | undefined; let ledsState: LedsState | undefined;
function stateObserver(state: typeof DEFAULT_STATE) { function stateObserver() {
const s = state.toJS(); const s = store.getState().toJS();
let step: 'main' | 'flashing' | 'verifying' | 'finish'; let step: 'main' | 'flashing' | 'verifying' | 'finish';
if (s.isFlashing) { if (s.isFlashing) {
step = s.flashState.type; step = s.flashState.type;
} else { } else {
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish'; step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
} }
const availableDrives = s.availableDrives.filter( const availableDrives = getDrives().filter(
(d: DeviceFromState) => d.devicePath, (d: DrivelistDrive) => d.devicePath,
); );
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) => const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
isSourceDrive(d, s.selection.image), isSourceDrive(d, s.selection.image),
)[0]?.devicePath; )[0]?.devicePath;
const availableDrivesPaths = availableDrives.map( const availableDrivesPaths = availableDrives.map(
(d: DeviceFromState) => d.devicePath, (d: DrivelistDrive) => d.devicePath,
); );
let selectedDrivesPaths: string[]; let selectedDrivesPaths: string[];
if (step === 'main') { if (step === 'main') {
selectedDrivesPaths = availableDrives selectedDrivesPaths = getSelectedDrives()
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device)) .filter((drive) => drive.devicePath !== null)
.map((d: DrivelistDrive) => d.devicePath); .map((drive) => drive.devicePath) as string[];
} else { } else {
selectedDrivesPaths = s.devicePaths; selectedDrivesPaths = s.devicePaths;
} }
const failedDevicePaths = s.failedDeviceErrors.map( const failedDevicePaths = s.failedDeviceErrors.map(
([devicePath]: [string]) => devicePath, ([, { devicePath }]: [string, { devicePath: string }]) => devicePath,
); );
const newLedsState = { const newLedsState = {
step, step,
@ -197,7 +222,7 @@ function stateObserver(state: typeof DEFAULT_STATE) {
availableDrives: availableDrivesPaths, availableDrives: availableDrivesPaths,
selectedDrives: selectedDrivesPaths, selectedDrives: selectedDrivesPaths,
failedDrives: failedDevicePaths, failedDrives: failedDevicePaths,
}; } as LedsState;
if (!_.isEqual(newLedsState, ledsState)) { if (!_.isEqual(newLedsState, ledsState)) {
updateLeds(newLedsState); updateLeds(newLedsState);
ledsState = newLedsState; ledsState = newLedsState;
@ -220,6 +245,16 @@ export async function init(): Promise<void> {
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) { for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames)); leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
} }
ledColors = (await settings.get('ledColors')) || {};
ledAnimationFunctions = {
blinkGreen: createAnimationFunction(blink, ledColors['green']),
blinkPurple: createAnimationFunction(blink, ledColors['purple']),
staticRed: createAnimationFunction(one, ledColors['red']),
staticGreen: createAnimationFunction(one, ledColors['green']),
staticBlue: createAnimationFunction(one, ledColors['blue']),
staticWhite: createAnimationFunction(one, ledColors['white']),
staticBlack: createAnimationFunction(one, ledColors['black']),
};
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 })); observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
} }
} }

View File

@ -51,7 +51,7 @@ async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
let contents = '{}'; let contents = '{}';
try { try {
contents = await fs.readFile(filename, { encoding: 'utf8' }); contents = await fs.readFile(filename, { encoding: 'utf8' });
} catch (error) { } catch (error: any) {
// noop // noop
} }
try { try {
@ -77,8 +77,7 @@ export async function writeConfigFile(
const DEFAULT_SETTINGS: _.Dictionary<any> = { const DEFAULT_SETTINGS: _.Dictionary<any> = {
errorReporting: true, errorReporting: true,
unmountOnSuccess: true, updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
desktopNotifications: true, desktopNotifications: true,
autoBlockmapping: true, autoBlockmapping: true,
decompressFirst: true, decompressFirst: true,
@ -105,7 +104,7 @@ export async function set(
settings[key] = value; settings[key] = value;
try { try {
await writeConfigFileFn(CONFIG_PATH, settings); await writeConfigFileFn(CONFIG_PATH, settings);
} catch (error) { } catch (error: any) {
// Revert to previous value if persisting settings failed // Revert to previous value if persisting settings failed
settings[key] = previousValue; settings[key] = previousValue;
throw error; throw error;

View File

@ -102,10 +102,9 @@ function validateMixpanelConfig(config: {
* This function sends the debug message to product analytics services. * This function sends the debug message to product analytics services.
*/ */
export function logEvent(message: string, data: _.Dictionary<any> = {}) { export function logEvent(message: string, data: _.Dictionary<any> = {}) {
const { const { applicationSessionUuid, flashingWorkflowUuid } = store
applicationSessionUuid, .getState()
flashingWorkflowUuid, .toJS();
} = store.getState().toJS();
resinCorvus.logEvent(message, { resinCorvus.logEvent(message, {
...data, ...data,
sample: mixpanelSample, sample: mixpanelSample,

View File

@ -15,10 +15,15 @@
*/ */
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import {
Adapter,
BlockDeviceAdapter,
UsbbootDeviceAdapter,
} from 'etcher-sdk/build/scanner/adapters';
import { geteuid, platform } from 'process'; import { geteuid, platform } from 'process';
const adapters: sdk.scanner.adapters.Adapter[] = [ const adapters: Adapter[] = [
new sdk.scanner.adapters.BlockDeviceAdapter({ new BlockDeviceAdapter({
includeSystemDrives: () => true, includeSystemDrives: () => true,
}), }),
]; ];
@ -26,14 +31,15 @@ const adapters: sdk.scanner.adapters.Adapter[] = [
// Can't use permissions.isElevated() here as it returns a promise and we need to set // Can't use permissions.isElevated() here as it returns a promise and we need to set
// module.exports = scanner right now. // module.exports = scanner right now.
if (platform !== 'linux' || geteuid() === 0) { if (platform !== 'linux' || geteuid() === 0) {
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter()); adapters.push(new UsbbootDeviceAdapter());
} }
if ( if (platform === 'win32') {
platform === 'win32' && const {
sdk.scanner.adapters.DriverlessDeviceAdapter !== undefined DriverlessDeviceAdapter: driverless,
) { // tslint:disable-next-line:no-var-requires
adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter()); } = require('etcher-sdk/build/scanner/adapters/driverless');
adapters.push(new driverless());
} }
export const scanner = new sdk.scanner.Scanner(adapters); export const scanner = new sdk.scanner.Scanner(adapters);

View File

@ -15,7 +15,6 @@
*/ */
import { Drive as DrivelistDrive } from 'drivelist'; import { Drive as DrivelistDrive } from 'drivelist';
import * as electron from 'electron';
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import * as ipc from 'node-ipc'; import * as ipc from 'node-ipc';
@ -25,6 +24,7 @@ import * as path from 'path';
import * as packageJSON from '../../../../package.json'; import * as packageJSON from '../../../../package.json';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
import * as permissions from '../../../shared/permissions'; import * as permissions from '../../../shared/permissions';
import { getAppPath } from '../../../shared/utils';
import { SourceMetadata } from '../components/source-selector/source-selector'; import { SourceMetadata } from '../components/source-selector/source-selector';
import * as flashState from '../models/flash-state'; import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state'; import * as selectionState from '../models/selection-state';
@ -93,11 +93,7 @@ function terminateServer() {
} }
function writerArgv(): string[] { function writerArgv(): string[] {
let entryPoint = path.join( let entryPoint = path.join(getAppPath(), 'generated', 'child-writer.js');
electron.remote.app.getAppPath(),
'generated',
'child-writer.js',
);
// AppImages run over FUSE, so the files inside the mount point // AppImages run over FUSE, so the files inside the mount point
// can only be accessed by the user that mounted the AppImage. // can only be accessed by the user that mounted the AppImage.
// This means we can't re-spawn Etcher as root from the same // This means we can't re-spawn Etcher as root from the same
@ -151,11 +147,7 @@ async function performWrite(
let cancelled = false; let cancelled = false;
let skip = false; let skip = false;
ipc.serve(); ipc.serve();
const { const { autoBlockmapping, decompressFirst } = await settings.getAll();
unmountOnSuccess,
autoBlockmapping,
decompressFirst,
} = await settings.getAll();
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => { ipc.server.on('error', (error) => {
terminateServer(); terminateServer();
@ -174,7 +166,6 @@ async function performWrite(
driveCount: drives.length, driveCount: drives.length,
uuid: flashState.getFlashUuid(), uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess,
}; };
ipc.server.on('fail', ({ device, error }) => { ipc.server.on('fail', ({ device, error }) => {
@ -211,7 +202,6 @@ async function performWrite(
destinations: drives, destinations: drives,
SourceType: image.SourceType.name, SourceType: image.SourceType.name,
autoBlockmapping, autoBlockmapping,
unmountOnSuccess,
decompressFirst, decompressFirst,
}); });
}); });
@ -228,7 +218,7 @@ async function performWrite(
}); });
flashResults.cancelled = cancelled || results.cancelled; flashResults.cancelled = cancelled || results.cancelled;
flashResults.skip = skip; flashResults.skip = skip;
} catch (error) { } catch (error: any) {
// This happens when the child is killed using SIGKILL // This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137; const SIGKILL_EXIT_CODE = 137;
if (error.code === SIGKILL_EXIT_CODE) { if (error.code === SIGKILL_EXIT_CODE) {
@ -278,7 +268,7 @@ export async function flash(
throw new Error('There is already a flash in progress'); throw new Error('There is already a flash in progress');
} }
flashState.setFlashingFlag(); await flashState.setFlashingFlag();
flashState.setDevicePaths( flashState.setDevicePaths(
drives.map((d) => d.devicePath).filter((p) => p != null) as string[], drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
); );
@ -290,16 +280,18 @@ export async function flash(
uuid: flashState.getFlashUuid(), uuid: flashState.getFlashUuid(),
status: 'started', status: 'started',
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
}; };
analytics.logEvent('Flash', analyticsData); analytics.logEvent('Flash', analyticsData);
try { try {
const result = await write(image, drives, flashState.setProgressState); const result = await write(image, drives, flashState.setProgressState);
flashState.unsetFlashingFlag(result); await flashState.unsetFlashingFlag(result);
} catch (error) { } catch (error: any) {
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); await flashState.unsetFlashingFlag({
cancelled: false,
errorCode: error.code,
});
windowProgress.clear(); windowProgress.clear();
const { results = {} } = flashState.getFlashResults(); const { results = {} } = flashState.getFlashResults();
const eventData = { const eventData = {
@ -345,7 +337,6 @@ export async function cancel(type: string) {
driveCount: drives.length, driveCount: drives.length,
uuid: flashState.getFlashUuid(), uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
status, status,
}; };
analytics.logEvent('Cancel', analyticsData); analytics.logEvent('Cancel', analyticsData);
@ -358,7 +349,7 @@ export async function cancel(type: string) {
if (socket !== undefined) { if (socket !== undefined) {
ipc.server.emit(socket, status); ipc.server.emit(socket, status);
} }
} catch (error) { } catch (error: any) {
analytics.logException(error); analytics.logException(error);
} }
} }

View File

@ -27,7 +27,7 @@ async function mountSourceDrive() {
if (sourceDrivePath) { if (sourceDrivePath) {
try { try {
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath); await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
} catch (error) { } catch (error: any) {
// noop // noop
} }
} }

View File

@ -15,6 +15,7 @@
*/ */
import { exec } from 'child_process'; import { exec } from 'child_process';
import { withTmpFile } from 'etcher-sdk/build/tmp';
import { readFile } from 'fs'; import { readFile } from 'fs';
import { chain, trim } from 'lodash'; import { chain, trim } from 'lodash';
import { platform } from 'os'; import { platform } from 'os';
@ -22,8 +23,6 @@ import { join } from 'path';
import { env } from 'process'; import { env } from 'process';
import { promisify } from 'util'; import { promisify } from 'util';
import { withTmpFile } from '../../../shared/tmp';
const readFileAsync = promisify(readFile); const readFileAsync = promisify(readFile);
const execAsync = promisify(exec); const execAsync = promisify(exec);
@ -41,11 +40,11 @@ async function getWmicNetworkDrivesOutput(): Promise<string> {
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded. // So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
const options = { const options = {
// Close the file once it's created // Close the file once it's created
discardDescriptor: true, keepOpen: false,
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-") // Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
prefix: 'tmp', prefix: 'tmp',
}; };
return withTmpFile(options, async (path) => { return withTmpFile(options, async ({ path }) => {
const command = [ const command = [
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'), join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
'path', 'path',

View File

@ -117,7 +117,7 @@ async function flashImageToDrive(
} }
goToSuccess(); goToSuccess();
} }
} catch (error) { } catch (error: any) {
notifyFailure(iconPath, basename, drives); notifyFailure(iconPath, basename, drives);
let errorMessage = getErrorMessageFromCode(error.code); let errorMessage = getErrorMessageFromCode(error.code);
if (!errorMessage) { if (!errorMessage) {

View File

@ -276,9 +276,9 @@ export class MainPage extends React.Component<
style={{ style={{
// Allow window to be dragged from header // Allow window to be dragged from header
// @ts-ignore // @ts-ignore
'-webkit-app-region': 'drag', WebkitAppRegion: 'drag',
position: 'relative', position: 'relative',
zIndex: 1, zIndex: 2,
}} }}
> >
<Flex width="100%" /> <Flex width="100%" />
@ -304,7 +304,7 @@ export class MainPage extends React.Component<
onClick={() => this.setState({ hideSettings: false })} onClick={() => this.setState({ hideSettings: false })}
style={{ style={{
// Make touch events click instead of dragging // Make touch events click instead of dragging
'-webkit-app-region': 'no-drag', WebkitAppRegion: 'no-drag',
}} }}
/> />
{!settings.getSync('disableExternalLinks') && ( {!settings.getSync('disableExternalLinks') && (
@ -319,7 +319,7 @@ export class MainPage extends React.Component<
tabIndex={6} tabIndex={6}
style={{ style={{
// Make touch events click instead of dragging // Make touch events click instead of dragging
'-webkit-app-region': 'no-drag', WebkitAppRegion: 'no-drag',
}} }}
/> />
)} )}

10
lib/gui/app/renderer.ts Normal file
View File

@ -0,0 +1,10 @@
// @ts-nocheck
import { main } from './app';
if (module.hot) {
module.hot.accept('./app', () => {
main();
});
}
main();

View File

@ -43,7 +43,7 @@ async function checkForUpdates(interval: number) {
const release = await autoUpdater.checkForUpdates(); const release = await autoUpdater.checkForUpdates();
const isOutdated = const isOutdated =
semver.compare(release.updateInfo.version, version) > 0; semver.compare(release.updateInfo.version, version) > 0;
const shouldUpdate = release.updateInfo.stagingPercentage || 0 > 0; const shouldUpdate = release.updateInfo.stagingPercentage !== 0; // undefinded (default) means 100%
if (shouldUpdate && isOutdated) { if (shouldUpdate && isOutdated) {
await autoUpdater.downloadUpdate(); await autoUpdater.downloadUpdate();
packageUpdated = true; packageUpdated = true;
@ -97,6 +97,7 @@ const sourceSelectorReady = new Promise((resolve) => {
async function selectImageURL(url?: string) { async function selectImageURL(url?: string) {
// 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests // 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests
if (url !== undefined && url !== 'data:,') { if (url !== undefined && url !== 'data:,') {
url = url.replace(/\/$/, ''); // on windows the url ends with an extra slash
url = url.startsWith(scheme) ? url.slice(scheme.length) : url; url = url.startsWith(scheme) ? url.slice(scheme.length) : url;
await sourceSelectorReady; await sourceSelectorReady;
electron.BrowserWindow.getAllWindows().forEach((window) => { electron.BrowserWindow.getAllWindows().forEach((window) => {
@ -147,6 +148,7 @@ async function createMainWindow() {
webPreferences: { webPreferences: {
backgroundThrottling: false, backgroundThrottling: false,
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false,
webviewTag: true, webviewTag: true,
zoomFactor: width / defaultWidth, zoomFactor: width / defaultWidth,
enableRemoteModule: true, enableRemoteModule: true,

View File

@ -27,6 +27,7 @@ import {
OnProgressFunction, OnProgressFunction,
OnFailFunction, OnFailFunction,
decompressThenFlash, decompressThenFlash,
DECOMPRESSED_IMAGE_PREFIX,
} from 'etcher-sdk/build/multi-write'; } from 'etcher-sdk/build/multi-write';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp'; import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import * as ipc from 'node-ipc'; import * as ipc from 'node-ipc';
@ -34,8 +35,10 @@ import { totalmem } from 'os';
import { toJSON } from '../../shared/errors'; import { toJSON } from '../../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes'; import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
import { delay } from '../../shared/utils'; import { delay, isJson } from '../../shared/utils';
import { SourceMetadata } from '../app/components/source-selector/source-selector'; import { SourceMetadata } from '../app/components/source-selector/source-selector';
import axios from 'axios';
import * as _ from 'lodash';
ipc.config.id = process.env.IPC_CLIENT_ID as string; ipc.config.id = process.env.IPC_CLIENT_ID as string;
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string; ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
@ -68,7 +71,7 @@ function log(message: string) {
*/ */
async function terminate(exitCode: number) { async function terminate(exitCode: number) {
ipc.disconnect(IPC_SERVER_ID); ipc.disconnect(IPC_SERVER_ID);
await cleanupTmpFiles(Date.now()); await cleanupTmpFiles(Date.now(), DECOMPRESSED_IMAGE_PREFIX);
process.nextTick(() => { process.nextTick(() => {
process.exit(exitCode || SUCCESS); process.exit(exitCode || SUCCESS);
}); });
@ -167,10 +170,10 @@ async function writeAndValidate({
interface WriteOptions { interface WriteOptions {
image: SourceMetadata; image: SourceMetadata;
destinations: DrivelistDrive[]; destinations: DrivelistDrive[];
unmountOnSuccess: boolean;
autoBlockmapping: boolean; autoBlockmapping: boolean;
decompressFirst: boolean; decompressFirst: boolean;
SourceType: string; SourceType: string;
httpRequest?: any;
} }
ipc.connectTo(IPC_SERVER_ID, () => { ipc.connectTo(IPC_SERVER_ID, () => {
@ -257,13 +260,12 @@ ipc.connectTo(IPC_SERVER_ID, () => {
const imagePath = options.image.path; const imagePath = options.image.path;
log(`Image: ${imagePath}`); log(`Image: ${imagePath}`);
log(`Devices: ${destinations.join(', ')}`); log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Auto blockmapping: ${options.autoBlockmapping}`); log(`Auto blockmapping: ${options.autoBlockmapping}`);
log(`Decompress first: ${options.decompressFirst}`); log(`Decompress first: ${options.decompressFirst}`);
const dests = options.destinations.map((destination) => { const dests = options.destinations.map((destination) => {
return new BlockDevice({ return new BlockDevice({
drive: destination, drive: destination,
unmountOnSuccess: options.unmountOnSuccess, unmountOnSuccess: true,
write: true, write: true,
direct: true, direct: true,
}); });
@ -282,7 +284,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
path: imagePath, path: imagePath,
}); });
} else { } else {
source = new Http({ url: imagePath, avoidRandomAccess: true }); const decodedImagePath = decodeURIComponent(imagePath);
if (isJson(decodedImagePath)) {
const imagePathObject = JSON.parse(decodedImagePath);
source = new Http({
url: imagePathObject.url,
avoidRandomAccess: true,
axiosInstance: axios.create(_.omit(imagePathObject, ['url'])),
auth: options.image.auth,
});
} else {
source = new Http({
url: imagePath,
avoidRandomAccess: true,
auth: options.image.auth,
});
}
} }
} }
const results = await writeAndValidate({ const results = await writeAndValidate({
@ -301,7 +318,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
ipc.of[IPC_SERVER_ID].emit('done', { results }); ipc.of[IPC_SERVER_ID].emit('done', { results });
await delay(DISCONNECT_DELAY); await delay(DISCONNECT_DELAY);
await terminate(exitCode); await terminate(exitCode);
} catch (error) { } catch (error: any) {
exitCode = GENERAL_ERROR; exitCode = GENERAL_ERROR;
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
} }

View File

@ -15,11 +15,12 @@
*/ */
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import { app, remote } from 'electron';
import { join } from 'path'; import { join } from 'path';
import { env } from 'process'; import { env } from 'process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getAppPath } from '../utils';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED'; const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
@ -37,7 +38,7 @@ export async function sudo(
env: { env: {
PATH: env.PATH, PATH: env.PATH,
SUDO_ASKPASS: join( SUDO_ASKPASS: join(
(app || remote.app).getAppPath(), getAppPath(),
__dirname, __dirname,
'sudo-askpass.osascript.js', 'sudo-askpass.osascript.js',
), ),
@ -49,7 +50,7 @@ export async function sudo(
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length), stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
stderr, stderr,
}; };
} catch (error) { } catch (error: any) {
if (error.code === 1) { if (error.code === 1) {
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) { if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
return { cancelled: true }; return { cancelled: true };

View File

@ -104,24 +104,19 @@ export function isDriveLargeEnough(
return driveSize >= (image.size || UNKNOWN_SIZE); return driveSize >= (image.size || UNKNOWN_SIZE);
} }
/**
* @summary Check if a drive is disabled (i.e. not ready for selection)
*/
export function isDriveDisabled(drive: DrivelistDrive): boolean {
return drive.disabled || false;
}
/** /**
* @summary Check if a drive is valid, i.e. large enough for an image * @summary Check if a drive is valid, i.e. large enough for an image
*/ */
export function isDriveValid( export function isDriveValid(
drive: DrivelistDrive, drive: DrivelistDrive,
image?: SourceMetadata, image?: SourceMetadata,
write: boolean = true,
): boolean { ): boolean {
return ( return (
!write ||
(!drive.disabled &&
isDriveLargeEnough(drive, image) && isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata) && !isSourceDrive(drive, image as SourceMetadata))
!isDriveDisabled(drive)
); );
} }

View File

@ -81,13 +81,10 @@ export const compatibility = {
} as const; } as const;
export const warning = { export const warning = {
unrecommendedDriveSize: ( tooSmall: (source: { size: number }, target: { size: number }) => {
image: { recommendedDriveSize: number },
drive: { device: string; size: number },
) => {
return outdent({ newline: ' ' })` return outdent({ newline: ' ' })`
This image recommends a ${prettyBytes(image.recommendedDriveSize)} The selected source is ${prettyBytes(source.size - target.size)}
drive, however ${drive.device} is only ${prettyBytes(drive.size)}. larger than this drive.
`; `;
}, },
@ -126,7 +123,7 @@ export const warning = {
}, },
largeDriveSize: () => { largeDriveSize: () => {
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.'; return "This is a large drive! Make sure it doesn't contain files that you want to keep.";
}, },
systemDrive: () => { systemDrive: () => {

View File

@ -15,30 +15,32 @@
*/ */
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import { withTmpFile } from 'etcher-sdk/build/tmp';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as os from 'os'; import * as os from 'os';
import * as semver from 'semver'; import * as semver from 'semver';
import * as sudoPrompt from 'sudo-prompt'; import * as sudoPrompt from '@balena/sudo-prompt';
import { promisify } from 'util'; import { promisify } from 'util';
import { sudo as catalinaSudo } from './catalina-sudo/sudo'; import { sudo as catalinaSudo } from './catalina-sudo/sudo';
import * as errors from './errors'; import * as errors from './errors';
import { withTmpFile } from './tmp';
const execAsync = promisify(childProcess.exec); const execAsync = promisify(childProcess.exec);
const execFileAsync = promisify(childProcess.execFile); const execFileAsync = promisify(childProcess.execFile);
type Std = string | Buffer | undefined;
function sudoExecAsync( function sudoExecAsync(
cmd: string, cmd: string,
options: { name: string }, options: { name: string },
): Promise<{ stdout: string; stderr: string }> { ): Promise<{ stdout: Std; stderr: Std }> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
sudoPrompt.exec( sudoPrompt.exec(
cmd, cmd,
options, options,
(error: Error | null, stdout: string, stderr: string) => { (error: Error | undefined, stdout: Std, stderr: Std) => {
if (error != null) { if (error) {
reject(error); reject(error);
} else { } else {
resolve({ stdout, stderr }); resolve({ stdout, stderr });
@ -60,7 +62,7 @@ export async function isElevated(): Promise<boolean> {
// See http://stackoverflow.com/a/28268802 // See http://stackoverflow.com/a/28268802
try { try {
await execAsync('fltmc'); await execAsync('fltmc');
} catch (error) { } catch (error: any) {
if (error.code === os.constants.errno.EPERM) { if (error.code === os.constants.errno.EPERM) {
return false; return false;
} }
@ -146,7 +148,7 @@ async function elevateScriptCatalina(
try { try {
const { cancelled } = await catalinaSudo(cmd); const { cancelled } = await catalinaSudo(cmd);
return { cancelled }; return { cancelled };
} catch (error) { } catch (error: any) {
throw errors.createError({ title: error.stderr }); throw errors.createError({ title: error.stderr });
} }
} }
@ -172,10 +174,11 @@ export async function elevateCommand(
); );
return await withTmpFile( return await withTmpFile(
{ {
keepOpen: false,
prefix: 'balena-etcher-electron-', prefix: 'balena-etcher-electron-',
postfix: '.cmd', postfix: '.cmd',
}, },
async (path) => { async ({ path }) => {
await fs.writeFile(path, launchScript); await fs.writeFile(path, launchScript);
if (isWindows) { if (isWindows) {
return elevateScriptWindows(path, options.applicationName); return elevateScriptWindows(path, options.applicationName);
@ -189,7 +192,7 @@ export async function elevateCommand(
} }
try { try {
return await elevateScriptUnix(path, options.applicationName); return await elevateScriptUnix(path, options.applicationName);
} catch (error) { } catch (error: any) {
// We're hardcoding internal error messages declared by `sudo-prompt`. // We're hardcoding internal error messages declared by `sudo-prompt`.
// There doesn't seem to be a better way to handle these errors, so // There doesn't seem to be a better way to handle these errors, so
// for now, we should make sure we double check if the error messages // for now, we should make sure we double check if the error messages

View File

@ -1,27 +0,0 @@
import * as tmp from 'tmp';
function tmpFileAsync(
options: tmp.FileOptions,
): Promise<{ path: string; cleanup: () => void }> {
return new Promise((resolve, reject) => {
tmp.file(options, (error, path, _fd, cleanup) => {
if (error) {
reject(error);
} else {
resolve({ path, cleanup });
}
});
});
}
export async function withTmpFile<T>(
options: tmp.FileOptions,
fn: (path: string) => Promise<T>,
): Promise<T> {
const { path, cleanup } = await tmpFileAsync(options);
try {
return await fn(path);
} finally {
cleanup();
}
}

View File

@ -15,6 +15,7 @@
*/ */
import axios from 'axios'; import axios from 'axios';
import { app, remote } from 'electron';
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import * as errors from './errors'; import * as errors from './errors';
@ -47,3 +48,25 @@ export async function delay(duration: number): Promise<void> {
setTimeout(resolve, duration); setTimeout(resolve, duration);
}); });
} }
export function getAppPath(): string {
return (
(app || remote.app)
.getAppPath()
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
// include the app-x64 or app-arm64 folder depending on the arch.
// We don't care about the app.asar file, we want the actual folder.
.replace(/\.asar$/, () =>
process.platform === 'darwin' ? '-' + process.arch : '',
)
);
}
export function isJson(jsonString: string) {
try {
JSON.parse(jsonString);
} catch (e) {
return false;
}
return true;
}

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "balena-etcher", "name": "balena-etcher",
"private": true, "private": true,
"displayName": "balenaEtcher", "displayName": "balenaEtcher",
"version": "1.5.112", "version": "1.7.8",
"packageType": "local", "packageType": "local",
"main": "generated/etcher.js", "main": "generated/etcher.js",
"description": "Flash OS images to SD cards and USB drives, safely and easily.", "description": "Flash OS images to SD cards and USB drives, safely and easily.",
@ -15,20 +15,19 @@
"scripts": { "scripts": {
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts", "lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
"lint-css": "prettier --write lib/**/*.css", "lint-css": "prettier --write lib/**/*.css",
"lint-spell": "codespell --dictionary - --dictionary dictionary.txt --skip *.ttf *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension lib tests docs Makefile *.md LICENSE", "lint": "npm run lint-ts && npm run lint-css",
"lint": "npm run lint-ts && npm run lint-css && npm run lint-spell", "test-spectron": "mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts",
"test-spectron": "mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts", "test-gui": "electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
"test-gui": "electron-mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts", "test-shared": "electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
"test-shared": "electron-mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
"test": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks", "test": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks",
"sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh", "sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh",
"start": "./node_modules/.bin/electron .", "start": "./node_modules/.bin/electron .",
"postshrinkwrap": "ts-node ./scripts/clean-shrinkwrap.ts", "postinstall": "electron-rebuild -t prod,dev,optional",
"webpack": "webpack", "webpack": "webpack",
"watch": "webpack --watch", "watch": "webpack serve --no-optimization-minimize --config ./webpack.dev.config.ts",
"concourse-build-electron": "npm run webpack", "concourse-build-electron": "npm run webpack",
"concourse-test": "npx npm@6.14.5 test", "concourse-test": "npx npm@6.14.8 test",
"concourse-test-electron": "npx npm@6.14.5 test" "concourse-test-electron": "npx npm@6.14.8 test"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -45,72 +44,78 @@
}, },
"author": "Balena Inc. <hello@etcher.io>", "author": "Balena Inc. <hello@etcher.io>",
"license": "Apache-2.0", "license": "Apache-2.0",
"platformSpecificDependencies": [
"fsevents",
"winusb-driver-generator"
],
"devDependencies": { "devDependencies": {
"@balena/lint": "^5.0.4", "@balena/lint": "5.3.0",
"@fortawesome/fontawesome-free": "^5.13.1", "@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
"@svgr/webpack": "^5.4.0", "@fortawesome/fontawesome-free": "5.13.1",
"@types/chai": "^4.2.7", "@svgr/webpack": "5.5.0",
"@types/copy-webpack-plugin": "^6.0.0", "@types/chai": "4.2.7",
"@types/mime-types": "^2.1.0", "@types/copy-webpack-plugin": "6.0.0",
"@types/mini-css-extract-plugin": "^0.9.1", "@types/mime-types": "2.1.0",
"@types/mocha": "^8.0.3", "@types/mini-css-extract-plugin": "1.2.2",
"@types/node": "^12.12.39", "@types/mocha": "8.0.3",
"@types/node-ipc": "^9.1.2", "@types/node": "14.14.41",
"@types/react-dom": "^16.8.4", "@types/node-ipc": "9.1.2",
"@types/semver": "^7.1.0", "@types/react": "16.8.5",
"@types/sinon": "^9.0.0", "@types/react-dom": "16.8.4",
"@types/terser-webpack-plugin": "^4.1.0", "@types/semver": "7.1.0",
"@types/tmp": "^0.2.0", "@types/sinon": "9.0.0",
"@types/webpack-node-externals": "^2.5.0", "@types/terser-webpack-plugin": "5.0.2",
"chai": "^4.2.0", "@types/tmp": "0.2.0",
"copy-webpack-plugin": "^6.0.1", "@types/webpack-node-externals": "2.5.0",
"css-loader": "^4.2.1", "aws4-axios": "2.2.1",
"d3": "^4.13.0", "chai": "4.2.0",
"debug": "^4.2.0", "copy-webpack-plugin": "7.0.0",
"electron": "9.3.3", "css-loader": "5.0.1",
"electron-builder": "^22.9.1", "d3": "4.13.0",
"electron-mocha": "^9.3.2", "debug": "4.2.0",
"electron-notarize": "^1.0.0", "electron": "12.2.3",
"electron-rebuild": "^2.3.2", "electron-builder": "22.10.5",
"electron-updater": "^4.3.5", "electron-mocha": "9.3.2",
"etcher-sdk": "^5.1.10", "electron-notarize": "1.0.0",
"file-loader": "^6.0.0", "electron-rebuild": "3.2.5",
"husky": "^4.2.5", "electron-updater": "4.3.5",
"immutable": "^3.8.1", "esbuild-loader": "2.16.0",
"lint-staged": "^10.2.2", "etcher-sdk": "6.3.0",
"lodash": "^4.17.10", "file-loader": "6.2.0",
"mini-css-extract-plugin": "^0.10.0", "husky": "4.2.5",
"mocha": "^8.0.1", "immutable": "3.8.1",
"native-addon-loader": "^2.0.1", "lint-staged": "10.2.2",
"node-ipc": "^9.1.1", "lodash": "4.17.10",
"mini-css-extract-plugin": "1.3.3",
"mocha": "8.0.1",
"native-addon-loader": "2.0.1",
"node-ipc": "9.1.1",
"omit-deep-lodash": "1.1.4", "omit-deep-lodash": "1.1.4",
"outdent": "^0.7.1", "outdent": "0.7.1",
"path-is-inside": "^1.0.2", "path-is-inside": "1.0.2",
"pretty-bytes": "^5.3.0", "pnp-webpack-plugin": "1.6.4",
"react": "^16.8.5", "pretty-bytes": "5.3.0",
"react-dom": "^16.8.5", "react": "16.8.5",
"redux": "^4.0.5", "react-dom": "16.8.5",
"rendition": "^18.8.3", "redux": "4.0.5",
"resin-corvus": "^2.0.5", "rendition": "19.2.0",
"semver": "^7.3.2", "resin-corvus": "2.0.5",
"simple-progress-webpack-plugin": "^1.1.2", "semver": "7.3.2",
"sinon": "^9.0.2", "simple-progress-webpack-plugin": "1.1.2",
"spectron": "^11.0.0", "sinon": "9.0.2",
"string-replace-loader": "^2.3.0", "spectron": "14.0.0",
"styled-components": "^5.1.0", "string-replace-loader": "3.0.1",
"sudo-prompt": "github:zvin/sudo-prompt#7cdede2f0da28fbcc2db48402d7d935f3a825c91", "style-loader": "2.0.0",
"sys-class-rgb-led": "^2.1.1", "styled-components": "5.1.0",
"tmp": "^0.2.1", "sys-class-rgb-led": "3.0.0",
"ts-loader": "^8.0.0", "terser-webpack-plugin": "5.2.5",
"ts-node": "^9.0.0", "ts-loader": "8.0.12",
"tslib": "^2.0.0", "ts-node": "9.1.1",
"typescript": "^4.1.2", "tslib": "2.0.0",
"uuid": "^8.1.0", "typescript": "4.4.4",
"webpack": "^4.40.2", "url-loader": "4.1.1",
"webpack-cli": "^3.3.9" "uuid": "8.1.0",
"webpack": "5.11.0",
"webpack-cli": "4.2.0",
"webpack-dev-server": "4.5.0"
},
"versionist": {
"publishedAt": "2022-03-18T10:39:52.682Z"
} }
} }

View File

@ -6,7 +6,7 @@ sentry:
team: resinio team: resinio
type: electron type: electron
triggerNotification: triggerNotification:
version: 1.5.81 version: 1.7.8
stagingPercentage: 100 stagingPercentage: 100
upstream: upstream:
- repo: etcher-sdk - repo: etcher-sdk

View File

@ -1,3 +1,2 @@
codespell==1.12.0
awscli==1.11.87 awscli==1.11.87
shyaml==0.5.0 shyaml==0.5.0

View File

@ -29,11 +29,15 @@ const SHRINKWRAP_FILENAME = path.join(__dirname, '..', 'npm-shrinkwrap.json');
async function main() { async function main() {
try { try {
const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies); const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies);
for (const item of Object.values(cleaned.dependencies)) {
// @ts-ignore
item.dev = true;
}
await writeFileAsync( await writeFileAsync(
SHRINKWRAP_FILENAME, SHRINKWRAP_FILENAME,
JSON.stringify(cleaned, null, JSON_INDENT), JSON.stringify(cleaned, null, JSON_INDENT),
); );
} catch (error) { } catch (error: any) {
console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`); console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`);
process.exitCode = 1; process.exitCode = 1;
} }

View File

@ -573,7 +573,8 @@ describe('Model: flashState', function () {
}); });
describe('.getFlashUuid()', function () { describe('.getFlashUuid()', function () {
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const UUID_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
it('should be initially undefined', function () { it('should be initially undefined', function () {
expect(flashState.getFlashUuid()).to.be.undefined; expect(flashState.getFlashUuid()).to.be.undefined;

View File

@ -23,7 +23,7 @@ import * as settings from '../../../lib/gui/app/models/settings';
async function checkError(promise: Promise<any>, fn: (err: Error) => any) { async function checkError(promise: Promise<any>, fn: (err: Error) => any) {
try { try {
await promise; await promise;
} catch (error) { } catch (error: any) {
await fn(error); await fn(error);
return; return;
} }

View File

@ -83,7 +83,7 @@ describe('Browser: imageWriter', () => {
imageWriter.flash(image, [fakeDrive], performWriteStub), imageWriter.flash(image, [fakeDrive], performWriteStub),
]); ]);
assert.fail('Writing twice should fail'); assert.fail('Writing twice should fail');
} catch (error) { } catch (error: any) {
expect(error.message).to.equal( expect(error.message).to.equal(
'There is already a flash in progress', 'There is already a flash in progress',
); );
@ -133,7 +133,7 @@ describe('Browser: imageWriter', () => {
}); });
try { try {
await imageWriter.flash(image, [fakeDrive], performWriteStub); await imageWriter.flash(image, [fakeDrive], performWriteStub);
} catch (error) { } catch (error: any) {
expect(error).to.be.an.instanceof(Error); expect(error).to.be.an.instanceof(Error);
expect(error.message).to.equal('write error'); expect(error.message).to.equal('write error');
} }

View File

@ -16,7 +16,6 @@
import { expect } from 'chai'; import { expect } from 'chai';
import * as settings from '../../../lib/gui/app/models/settings';
import * as progressStatus from '../../../lib/gui/app/modules/progress-status'; import * as progressStatus from '../../../lib/gui/app/modules/progress-status';
describe('Browser: progressStatus', function () { describe('Browser: progressStatus', function () {
@ -30,8 +29,6 @@ describe('Browser: progressStatus', function () {
eta: 15, eta: 15,
speed: 100000000000000, speed: 100000000000000,
}; };
settings.set('unmountOnSuccess', true);
}); });
it('should report 0% if percentage == 0 but speed != 0', function () { it('should report 0% if percentage == 0 but speed != 0', function () {
@ -40,22 +37,14 @@ describe('Browser: progressStatus', function () {
); );
}); });
it('should handle percentage == 0, flashing, unmountOnSuccess', function () { it('should handle percentage == 0, flashing', function () {
this.state.speed = 0; this.state.speed = 0;
expect(progressStatus.titleFromFlashState(this.state)).to.equal( expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'0% Flashing...', '0% Flashing...',
); );
}); });
it('should handle percentage == 0, flashing, !unmountOnSuccess', function () { it('should handle percentage == 0, verifying', function () {
this.state.speed = 0;
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'0% Flashing...',
);
});
it('should handle percentage == 0, verifying, unmountOnSuccess', function () {
this.state.speed = 0; this.state.speed = 0;
this.state.type = 'verifying'; this.state.type = 'verifying';
expect(progressStatus.titleFromFlashState(this.state)).to.equal( expect(progressStatus.titleFromFlashState(this.state)).to.equal(
@ -63,31 +52,14 @@ describe('Browser: progressStatus', function () {
); );
}); });
it('should handle percentage == 0, verifying, !unmountOnSuccess', function () { it('should handle percentage == 50, flashing', function () {
this.state.speed = 0;
this.state.type = 'verifying';
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'0% Validating...',
);
});
it('should handle percentage == 50, flashing, unmountOnSuccess', function () {
this.state.percentage = 50; this.state.percentage = 50;
expect(progressStatus.titleFromFlashState(this.state)).to.equal( expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'50% Flashing...', '50% Flashing...',
); );
}); });
it('should handle percentage == 50, flashing, !unmountOnSuccess', function () { it('should handle percentage == 50, verifying', function () {
this.state.percentage = 50;
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'50% Flashing...',
);
});
it('should handle percentage == 50, verifying, unmountOnSuccess', function () {
this.state.percentage = 50; this.state.percentage = 50;
this.state.type = 'verifying'; this.state.type = 'verifying';
expect(progressStatus.titleFromFlashState(this.state)).to.equal( expect(progressStatus.titleFromFlashState(this.state)).to.equal(
@ -95,31 +67,14 @@ describe('Browser: progressStatus', function () {
); );
}); });
it('should handle percentage == 50, verifying, !unmountOnSuccess', function () { it('should handle percentage == 100, flashing', function () {
this.state.percentage = 50;
this.state.type = 'verifying';
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'50% Validating...',
);
});
it('should handle percentage == 100, flashing, unmountOnSuccess', function () {
this.state.percentage = 100; this.state.percentage = 100;
expect(progressStatus.titleFromFlashState(this.state)).to.equal( expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...', 'Finishing...',
); );
}); });
it('should handle percentage == 100, flashing, !unmountOnSuccess', function () { it('should handle percentage == 100, verifying', function () {
this.state.percentage = 100;
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...',
);
});
it('should handle percentage == 100, verifying, unmountOnSuccess', function () {
this.state.percentage = 100; this.state.percentage = 100;
this.state.type = 'verifying'; this.state.type = 'verifying';
expect(progressStatus.titleFromFlashState(this.state)).to.equal( expect(progressStatus.titleFromFlashState(this.state)).to.equal(
@ -127,9 +82,8 @@ describe('Browser: progressStatus', function () {
); );
}); });
it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () { it('should handle percentage == 100, validating', function () {
this.state.percentage = 100; this.state.percentage = 100;
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal( expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...', 'Finishing...',
); );

View File

@ -514,40 +514,6 @@ describe('Shared: DriveConstraints', function () {
}); });
}); });
describe('.isDriveDisabled()', function () {
it('should return true if the drive is disabled', function () {
const result = constraints.isDriveDisabled(({
device: '/dev/disk1',
size: 1000000000,
isReadOnly: false,
disabled: true,
} as unknown) as constraints.DrivelistDrive);
expect(result).to.be.true;
});
it('should return false if the drive is not disabled', function () {
const result = constraints.isDriveDisabled(({
device: '/dev/disk1',
size: 1000000000,
isReadOnly: false,
disabled: false,
} as unknown) as constraints.DrivelistDrive);
expect(result).to.be.false;
});
it('should return false if "disabled" is undefined', function () {
const result = constraints.isDriveDisabled({
device: '/dev/disk1',
size: 1000000000,
isReadOnly: false,
} as constraints.DrivelistDrive);
expect(result).to.be.false;
});
});
describe('.isDriveSizeRecommended()', function () { describe('.isDriveSizeRecommended()', function () {
const image: SourceMetadata = { const image: SourceMetadata = {
description: 'rpi.img', description: 'rpi.img',
@ -1192,7 +1158,7 @@ describe('Shared: DriveConstraints', function () {
'/dev/disk6', '/dev/disk6',
]; ];
const drives = [ const drives = [
({ {
device: drivePaths[0], device: drivePaths[0],
description: 'My Drive', description: 'My Drive',
size: 123456789, size: 123456789,
@ -1200,8 +1166,8 @@ describe('Shared: DriveConstraints', function () {
mountpoints: [{ path: __dirname }], mountpoints: [{ path: __dirname }],
isSystem: false, isSystem: false,
isReadOnly: false, isReadOnly: false,
} as unknown) as constraints.DrivelistDrive, } as unknown as constraints.DrivelistDrive,
({ {
device: drivePaths[1], device: drivePaths[1],
description: 'My Other Drive', description: 'My Other Drive',
size: 123456789, size: 123456789,
@ -1209,8 +1175,8 @@ describe('Shared: DriveConstraints', function () {
mountpoints: [], mountpoints: [],
isSystem: false, isSystem: false,
isReadOnly: true, isReadOnly: true,
} as unknown) as constraints.DrivelistDrive, } as unknown as constraints.DrivelistDrive,
({ {
device: drivePaths[2], device: drivePaths[2],
description: 'My Drive', description: 'My Drive',
size: 1234567, size: 1234567,
@ -1218,8 +1184,8 @@ describe('Shared: DriveConstraints', function () {
mountpoints: [], mountpoints: [],
isSystem: false, isSystem: false,
isReadOnly: false, isReadOnly: false,
} as unknown) as constraints.DrivelistDrive, } as unknown as constraints.DrivelistDrive,
({ {
device: drivePaths[3], device: drivePaths[3],
description: 'My Drive', description: 'My Drive',
size: 123456789, size: 123456789,
@ -1227,8 +1193,8 @@ describe('Shared: DriveConstraints', function () {
mountpoints: [], mountpoints: [],
isSystem: true, isSystem: true,
isReadOnly: false, isReadOnly: false,
} as unknown) as constraints.DrivelistDrive, } as unknown as constraints.DrivelistDrive,
({ {
device: drivePaths[4], device: drivePaths[4],
description: 'My Drive', description: 'My Drive',
size: 128000000001, size: 128000000001,
@ -1236,8 +1202,8 @@ describe('Shared: DriveConstraints', function () {
mountpoints: [], mountpoints: [],
isSystem: false, isSystem: false,
isReadOnly: false, isReadOnly: false,
} as unknown) as constraints.DrivelistDrive, } as unknown as constraints.DrivelistDrive,
({ {
device: drivePaths[5], device: drivePaths[5],
description: 'My Drive', description: 'My Drive',
size: 12345678, size: 12345678,
@ -1245,8 +1211,8 @@ describe('Shared: DriveConstraints', function () {
mountpoints: [], mountpoints: [],
isSystem: false, isSystem: false,
isReadOnly: false, isReadOnly: false,
} as unknown) as constraints.DrivelistDrive, } as unknown as constraints.DrivelistDrive,
({ {
device: drivePaths[6], device: drivePaths[6],
description: 'My Drive', description: 'My Drive',
size: 123456789, size: 123456789,
@ -1254,7 +1220,7 @@ describe('Shared: DriveConstraints', function () {
mountpoints: [], mountpoints: [],
isSystem: false, isSystem: false,
isReadOnly: false, isReadOnly: false,
} as unknown) as constraints.DrivelistDrive, } as unknown as constraints.DrivelistDrive,
]; ];
const image: SourceMetadata = { const image: SourceMetadata = {

View File

@ -30,9 +30,8 @@ describe('Shared: SupportedFormats', function () {
], ],
(imagePath) => { (imagePath) => {
it(`should return true if filename is ${imagePath}`, function () { it(`should return true if filename is ${imagePath}`, function () {
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage( const looksLikeWindowsImage =
imagePath, supportedFormats.looksLikeWindowsImage(imagePath);
);
expect(looksLikeWindowsImage).to.be.true; expect(looksLikeWindowsImage).to.be.true;
}); });
}, },
@ -45,9 +44,8 @@ describe('Shared: SupportedFormats', function () {
], ],
(imagePath) => { (imagePath) => {
it(`should return false if filename is ${imagePath}`, function () { it(`should return false if filename is ${imagePath}`, function () {
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage( const looksLikeWindowsImage =
imagePath, supportedFormats.looksLikeWindowsImage(imagePath);
);
expect(looksLikeWindowsImage).to.be.false; expect(looksLikeWindowsImage).to.be.false;
}); });
}, },

View File

@ -15,15 +15,20 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { platform } from 'os';
import { Application } from 'spectron'; import { Application } from 'spectron';
import * as electronPath from 'electron'; import * as electronPath from 'electron';
describe('Spectron', function () { // TODO: spectron fails to start on the CI with:
// Error: Failed to create session.
// unknown error: Chrome failed to start: exited abnormally
if (platform() !== 'darwin') {
describe('Spectron', function () {
// Mainly for CI jobs // Mainly for CI jobs
this.timeout(40000); this.timeout(40000);
const app = new Application({ const app = new Application({
path: (electronPath as unknown) as string, path: electronPath as unknown as string,
args: ['--no-sandbox', '.'], args: ['--no-sandbox', '.'],
}); });
@ -54,7 +59,8 @@ describe('Spectron', function () {
it('should set a proper title', async () => { it('should set a proper title', async () => {
// @ts-ignore (SpectronClient.getTitle exists) // @ts-ignore (SpectronClient.getTitle exists)
return expect(await app.client.getTitle()).to.equal('Etcher'); return expect(await app.client.getTitle()).to.equal('balenaEtcher');
}); });
}); });
}); });
}

View File

@ -1,12 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"target": "es2019",
"typeRoots": ["./node_modules/@types", "./typings"],
"module": "commonjs",
"lib": ["dom", "esnext"],
"declaration": true,
"declarationMap": true,
"jsx": "react",
"pretty": true,
"sourceMap": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"resolveJsonModule": true, "noImplicitReturns": true,
"target": "es2019", "noFallthroughCasesInSwitch": true,
"moduleResolution": "node", "moduleResolution": "node",
"jsx": "react", "allowSyntheticDefaultImports": true,
"typeRoots": ["./node_modules/@types", "./typings"] "resolveJsonModule": true,
} }
} }

View File

@ -10,7 +10,16 @@
"jsx": "react", "jsx": "react",
"typeRoots": ["./node_modules/@types", "./typings"], "typeRoots": ["./node_modules/@types", "./typings"],
"importHelpers": true, "importHelpers": true,
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
"lib": ["dom", "esnext"],
"declaration": true,
"declarationMap": true,
"pretty": true,
"sourceMap": true,
"baseUrl": "./src",
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowJs": true
}, },
"include": [ "include": [
"lib/**/*.ts", "lib/**/*.ts",

1
typings/pnp-webpack-plugin/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'pnp-webpack-plugin';

View File

@ -1 +1 @@
declare module 'sudo-prompt'; declare module '@balena/sudo-prompt';

View File

@ -17,7 +17,6 @@
import * as CopyPlugin from 'copy-webpack-plugin'; import * as CopyPlugin from 'copy-webpack-plugin';
import { readdirSync } from 'fs'; import { readdirSync } from 'fs';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
import * as os from 'os'; import * as os from 'os';
import outdent from 'outdent'; import outdent from 'outdent';
import * as path from 'path'; import * as path from 'path';
@ -25,6 +24,9 @@ import { env } from 'process';
import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin'; import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin';
import * as TerserPlugin from 'terser-webpack-plugin'; import * as TerserPlugin from 'terser-webpack-plugin';
import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack'; import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
import * as PnpWebpackPlugin from 'pnp-webpack-plugin';
import * as tsconfigRaw from './tsconfig.webpack.json';
/** /**
* Don't webpack package.json as mixpanel & sentry tokens * Don't webpack package.json as mixpanel & sentry tokens
@ -32,8 +34,7 @@ import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
*/ */
function externalPackageJson(packageJsonPath: string) { function externalPackageJson(packageJsonPath: string) {
return ( return (
_context: string, { request }: { context: string; request: string },
request: string,
callback: (error?: Error | null, result?: string) => void, callback: (error?: Error | null, result?: string) => void,
) => { ) => {
if (_.endsWith(request, 'package.json')) { if (_.endsWith(request, 'package.json')) {
@ -50,8 +51,7 @@ function platformSpecificModule(
) { ) {
// Resolves module on platform, otherwise resolves the replacement // Resolves module on platform, otherwise resolves the replacement
return ( return (
_context: string, { request }: { context: string; request: string },
request: string,
callback: (error?: Error, result?: string, type?: string) => void, callback: (error?: Error, result?: string, type?: string) => void,
) => { ) => {
if (request === module && os.platform() !== platform) { if (request === module && os.platform() !== platform) {
@ -70,6 +70,8 @@ function renameNodeModules(resourcePath: string) {
path path
.relative(__dirname, resourcePath) .relative(__dirname, resourcePath)
.replace('node_modules', 'modules') .replace('node_modules', 'modules')
// use the same name on all architectures so electron-builder can build a universal dmg on mac
.replace(LZMA_BINDINGS_FOLDER, LZMA_BINDINGS_FOLDER_RENAMED)
// file-loader expects posix paths, even on Windows // file-loader expects posix paths, even on Windows
.replace(/\\/g, '/') .replace(/\\/g, '/')
); );
@ -89,6 +91,7 @@ function findLzmaNativeBindingsFolder(): string {
} }
const LZMA_BINDINGS_FOLDER = findLzmaNativeBindingsFolder(); const LZMA_BINDINGS_FOLDER = findLzmaNativeBindingsFolder();
const LZMA_BINDINGS_FOLDER_RENAMED = 'binding';
interface ReplacementRule { interface ReplacementRule {
search: string; search: string;
@ -119,7 +122,15 @@ function fetchWasm(...where: string[]) {
} catch { } catch {
} }
function appPath() { function appPath() {
return Path.isAbsolute(__dirname) ? __dirname : Path.join(electron.remote.app.getAppPath(), 'generated'); return Path.isAbsolute(__dirname) ?
__dirname :
Path.join(
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
// include the app-x64 or app-arm64 folder depending on the arch.
// We don't care about the app.asar file, we want the actual folder.
electron.remote.app.getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
'generated'
);
} }
scriptDirectory = Path.join(appPath(), 'modules', ${whereStr}, '/'); scriptDirectory = Path.join(appPath(), 'modules', ${whereStr}, '/');
`; `;
@ -128,16 +139,17 @@ function fetchWasm(...where: string[]) {
const commonConfig = { const commonConfig = {
mode: 'production', mode: 'production',
optimization: { optimization: {
moduleIds: 'natural',
minimize: true, minimize: true,
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({
parallel: true,
terserOptions: { terserOptions: {
compress: false, compress: false,
mangle: false, mangle: false,
output: { format: {
beautify: true,
comments: false, comments: false,
ecma: 2018, ecma: 2020,
}, },
}, },
extractComments: false, extractComments: false,
@ -148,7 +160,12 @@ const commonConfig = {
rules: [ rules: [
{ {
test: /\.css$/, test: /\.css$/,
use: 'css-loader', use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
loader: 'file-loader',
options: { name: renameNodeModules },
}, },
{ {
test: /\.svg$/, test: /\.svg$/,
@ -158,9 +175,11 @@ const commonConfig = {
test: /\.tsx?$/, test: /\.tsx?$/,
use: [ use: [
{ {
loader: 'ts-loader', loader: 'esbuild-loader',
options: { options: {
configFile: 'tsconfig.webpack.json', loader: 'tsx',
target: 'es2021',
tsconfigRaw,
}, },
}, },
], ],
@ -192,12 +211,7 @@ const commonConfig = {
// remove node-pre-gyp magic from lzma-native // remove node-pre-gyp magic from lzma-native
{ {
search: 'require(binding_path)', search: 'require(binding_path)',
replace: () => { replace: `require('./${LZMA_BINDINGS_FOLDER}/lzma_native.node')`,
return `require('./${path.posix.join(
LZMA_BINDINGS_FOLDER,
'lzma_native.node',
)}')`;
},
}, },
// use regular stream module instead of readable-stream // use regular stream module instead of readable-stream
{ {
@ -241,7 +255,19 @@ const commonConfig = {
"return await readFile(Path.join(__dirname, '..', 'blobs', filename));", "return await readFile(Path.join(__dirname, '..', 'blobs', filename));",
replace: outdent` replace: outdent`
const { app, remote } = require('electron'); const { app, remote } = require('electron');
return await readFile(Path.join((app || remote.app).getAppPath(), 'generated', __dirname.replace('node_modules', 'modules'), '..', 'blobs', filename)); return await readFile(
Path.join(
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
// include the app-x64 or app-arm64 folder depending on the arch.
// We don't care about the app.asar file, we want the actual folder.
(app || remote.app).getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
'generated',
__dirname.replace('node_modules', 'modules'),
'..',
'blobs',
filename
)
);
`, `,
}), }),
// Use the libext2fs.wasm file in the generated folder // Use the libext2fs.wasm file in the generated folder
@ -272,16 +298,20 @@ const commonConfig = {
extensions: ['.node', '.js', '.json', '.ts', '.tsx'], extensions: ['.node', '.js', '.json', '.ts', '.tsx'],
}, },
plugins: [ plugins: [
PnpWebpackPlugin,
new SimpleProgressWebpackPlugin({ new SimpleProgressWebpackPlugin({
format: process.env.WEBPACK_PROGRESS || 'verbose', format: process.env.WEBPACK_PROGRESS || 'verbose',
}), }),
// Force axios to use http.js, not xhr.js as we need stream support // Force axios to use http.js, not xhr.js as we need stream support
// (it's package.json file replaces http with xhr for browser targets). // (its package.json file replaces http with xhr for browser targets).
new NormalModuleReplacementPlugin( new NormalModuleReplacementPlugin(
slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/), slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/),
'./http.js', './http.js',
), ),
], ],
resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)],
},
output: { output: {
path: path.join(__dirname, 'generated'), path: path.join(__dirname, 'generated'),
filename: '[name].js', filename: '[name].js',
@ -319,7 +349,7 @@ if (os.platform() === 'win32') {
// liblzma.dll is required on Windows for lzma-native // liblzma.dll is required on Windows for lzma-native
guiConfigCopyPatterns.push({ guiConfigCopyPatterns.push({
from: `node_modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`, from: `node_modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
to: `modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`, to: `modules/lzma-native/${LZMA_BINDINGS_FOLDER_RENAMED}/liblzma.dll`,
}); });
} }
@ -331,10 +361,19 @@ const guiConfig = {
__filename: true, __filename: true,
}, },
entry: { entry: {
gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.ts'), gui: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
}, },
// entry: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
plugins: [ plugins: [
...commonConfig.plugins, ...commonConfig.plugins,
new CopyPlugin({
patterns: [
{ from: 'lib/gui/app/index.html', to: 'index.html' },
// electron-builder doesn't bundle folders named "assets"
// See https://github.com/electron-userland/electron-builder/issues/4545
{ from: 'assets/icon.png', to: 'media/icon.png' },
],
}),
// Remove "Download the React DevTools for a better development experience" message // Remove "Download the React DevTools for a better development experience" message
new BannerPlugin({ new BannerPlugin({
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };', banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
@ -373,41 +412,4 @@ const childWriterConfig = {
}, },
}; };
const cssConfig = { export default [guiConfig, etcherConfig, childWriterConfig];
mode: 'production',
optimization: {
minimize: false,
},
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
loader: 'file-loader',
options: { name: renameNodeModules },
},
],
},
plugins: [
new MiniCssExtractPlugin({ filename: '[name].css' }),
new CopyPlugin({
patterns: [
{ from: 'lib/gui/app/index.html', to: 'index.html' },
// electron-builder doesn't bundle folders named "assets"
// See https://github.com/electron-userland/electron-builder/issues/4545
{ from: 'assets/icon.png', to: 'media/icon.png' },
],
}),
],
entry: {
index: path.join(__dirname, 'lib', 'gui', 'app', 'css', 'main.css'),
},
output: {
path: path.join(__dirname, 'generated'),
},
};
module.exports = [cssConfig, guiConfig, etcherConfig, childWriterConfig];

24
webpack.dev.config.ts Normal file
View File

@ -0,0 +1,24 @@
import configs from './webpack.config';
import { WebpackOptionsNormalized } from 'webpack';
import * as fs from 'fs';
const [
guiConfig,
etcherConfig,
childWriterConfig,
] = (configs as unknown) as WebpackOptionsNormalized[];
configs.forEach((config) => {
config.mode = 'development';
// @ts-ignore
config.devtool = 'source-map';
});
guiConfig.devServer = {
hot: true,
port: 3030,
};
fs.copyFileSync('./lib/gui/app/index.dev.html', './generated/index.html');
export default [guiConfig, etcherConfig, childWriterConfig];