mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-26 20:56:34 +00:00
Merge branch 'hw-diag' of github.com:balena-io/etcher into hw-diag
This commit is contained in:
commit
37e2ef1fef
@ -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
|
||||||
|
7
.github/ISSUE_TEMPLATE.md
vendored
7
.github/ISSUE_TEMPLATE.md
vendored
@ -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 -->
|
||||||
|
@ -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
11635
.versionbot/CHANGELOG.yml
Normal file
File diff suppressed because it is too large
Load Diff
537
CHANGELOG.md
537
CHANGELOG.md
@ -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)
|
||||||
|
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
* @thundron @zvin @jviotti
|
|
||||||
/scripts @nazrhom
|
|
4
FAQ.md
4
FAQ.md
@ -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).
|
||||||
|
13
Makefile
13
Makefile
@ -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
141
README.md
@ -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).
|
||||||
|
|
||||||
[](https://balena.io/etcher)
|
[](https://balena.io/etcher)
|
||||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||||
[](https://david-dm.org/balena-io/etcher)
|
|
||||||
[](https://forums.balena.io/c/etcher)
|
[](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
|
||||||
|
|
||||||
|
> [](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
|
||||||
|
17
SUPPORT.md
17
SUPPORT.md
@ -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
|
||||||
|
BIN
assets/icon.icns
BIN
assets/icon.icns
Binary file not shown.
@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
|
||||||
|
@ -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: {} });
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 (
|
||||||
|
@ -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 = {};
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
|
12
lib/gui/app/index.dev.html
Normal file
12
lib/gui/app/index.dev.html
Normal 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>
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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 }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
|
@ -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) {
|
||||||
|
@ -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
10
lib/gui/app/renderer.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { main } from './app';
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
module.hot.accept('./app', () => {
|
||||||
|
main();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
@ -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,
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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 };
|
||||||
|
@ -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)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: () => {
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
14522
npm-shrinkwrap.json → package-lock.json
generated
14522
npm-shrinkwrap.json → package-lock.json
generated
File diff suppressed because it is too large
Load Diff
155
package.json
155
package.json
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
repo.yml
2
repo.yml
@ -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
|
||||||
|
@ -1,3 +1,2 @@
|
|||||||
codespell==1.12.0
|
|
||||||
awscli==1.11.87
|
awscli==1.11.87
|
||||||
shyaml==0.5.0
|
shyaml==0.5.0
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
}
|
}
|
||||||
|
@ -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...',
|
||||||
);
|
);
|
||||||
|
@ -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 = {
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
1
typings/pnp-webpack-plugin/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'pnp-webpack-plugin';
|
2
typings/sudo-prompt/index.d.ts
vendored
2
typings/sudo-prompt/index.d.ts
vendored
@ -1 +1 @@
|
|||||||
declare module 'sudo-prompt';
|
declare module '@balena/sudo-prompt';
|
||||||
|
@ -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
24
webpack.dev.config.ts
Normal 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];
|
Loading…
x
Reference in New Issue
Block a user