Compare commits

..

579 Commits

Author SHA1 Message Date
Kyle Harding
2b63fbed03 Remove repo config from flowzone.yml
This functionality is being deprecated in Flowzone.

See: https://github.com/product-os/flowzone/pull/833

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2023-12-19 18:15:38 -05:00
flowzone-app[bot]
e969735955 v1.18.13 2023-10-16 13:32:31 +00:00
flowzone-app[bot]
45bb29a393 Merge pull request #4102 from balena-io/aethernet/childwriter-standalone
patch: compile child-writer.ts as a standalone cli
2023-10-16 13:31:21 +00:00
Edwin Joassart
f38bca290f patch: upgrade to electron 25 2023-10-16 14:49:06 +02:00
Edwin Joassart
fb8ed5b529 patch: refactor scanner, loader and flasher out of gui + upgrade to electron 25 2023-10-16 14:49:06 +02:00
flowzone-app[bot]
09e13e9b43 v1.18.12 2023-07-19 10:24:26 +00:00
Edwin Joassart
13e1e8e504 Merge pull request #4060 from jcapona/master
patch: update instructions for installing deb file
2023-07-19 12:23:20 +02:00
Jorge Capona
acab03ad77 Update instructions for installing deb file
Change-type: patch
2023-07-14 15:27:38 -05:00
flowzone-app[bot]
0a6c15f702 v1.18.11 2023-07-13 14:31:44 +00:00
dfunckt
589ce9c28e Merge pull request #4075 from leadpogrommer/fix_focus_stealing
Prevent stealing window focus from auth dialog
2023-07-13 17:30:44 +03:00
leadpogrommer
f716c74ef7 fix: prevent stealing window focus from auth dialog
Change-type: patch
2023-07-12 22:06:04 +07:00
flowzone-app[bot]
2d7a6220cd v1.18.10 2023-07-12 11:22:02 +00:00
dfunckt
e0b26d455c Merge pull request #3765 from jsoref/spelling
Spelling
2023-07-12 14:21:07 +03:00
Josh Soref
06d246e3fd spelling: validates
Change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2023-07-12 13:40:47 +03:00
Josh Soref
67b26a5b69 spelling: undefined
Change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2023-07-12 13:40:47 +03:00
Josh Soref
b4b9db7ffa spelling: except if
Change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2023-07-12 13:40:47 +03:00
flowzone-app[bot]
cc037d23c4 v1.18.9 2023-07-12 09:07:22 +00:00
dfunckt
9c9c036956 Merge pull request #4086 from balena-io/fix-opening-links-in-safe-webview
Fix opening links from within SafeWebView
2023-07-12 12:06:25 +03:00
Akis Kesoglou
7fdbc439f7 Fix Publish action on Windows 2023-07-12 11:34:22 +03:00
Akis Kesoglou
9410669294 Fix lint issues 2023-07-06 21:50:29 +03:00
Akis Kesoglou
497bb0e2cb Fix opening links from within SafeWebView
Change-type: patch
2023-07-06 21:41:59 +03:00
balenaCI
a42be8ee74 v1.18.8 2023-04-26 09:57:47 +00:00
flowzone-app[bot]
16b50d2a71 Merge pull request #4056 from balena-io/update-support
Patch: Fix support link
2023-04-26 09:57:05 +00:00
Oliver Plummer
882b385c88 Patch: Fix Support link 2023-04-26 11:25:55 +02:00
balenaCI
059a36659e v1.18.7 2023-04-25 15:25:37 +00:00
flowzone-app[bot]
cd9cf09422 Merge pull request #4055 from balena-io/update-docs
Update docs
2023-04-25 15:24:48 +00:00
Edwin Joassart
02a4067118 patch: update docs to remove cloudsmith install instructions for linux 2023-04-25 16:55:30 +02:00
balenaCI
6fae328f1f v1.18.6 2023-03-21 13:24:20 +00:00
Lizzie Epton
81b0eed4d4 Merge pull request #4036 from balena-io/add-flash-with-etcher-to-docs
add-flash-with-etcher-to-docs
2023-03-21 13:23:29 +00:00
Lizzie Epton
b786c8bc10 Update docs/FAQ.md
Co-authored-by: Chris Crocker-White <chriscw@balena.io>
2023-03-21 12:49:04 +00:00
Lizzie Epton
856b426dc9 add-flash-with-etcher-to-docs
Change-type: patch
2023-03-21 10:17:14 +00:00
balenaCI
197a8f9c57 v1.18.5 2023-03-09 11:30:37 +00:00
Edwin Joassart
bc4ee48c1b Merge pull request #4025 from balena-io/aethernet-apt-update
patch: add apt-get update in flowzone preinstall
2023-03-09 12:29:40 +01:00
Edwin Joassart
0d9ac71088 patch: add apt-get update in flowzone preinstall
libudev package has changed and cannot be installed if we not update apt cache
2023-03-09 10:51:34 +01:00
balenaCI
a0fc9bbd68 v1.18.4 2023-03-02 17:31:34 +00:00
Edwin Joassart
7e0519df9a Merge pull request #4019 from balena-io/fix-accept-encoding-gzip
patch: bump etcher-sdk to 8.3.1
2023-03-02 18:30:30 +01:00
JOASSART Edwin
bf0360e7f4 patch: bump etcher-sdk to 8.3.1 2023-03-02 10:14:22 +01:00
balenaCI
62bae7c52e v1.18.3 2023-02-22 12:12:41 +00:00
Balena CI
802f5b2980 Merge pull request #4005 from balena-io/etcher-efp-ref-in-doc
Add reference to etcher-efp in publishing.md
2023-02-22 14:11:57 +02:00
Lizzie Epton
496f131c4b fix-typo
Change-type: patch
2023-02-22 11:42:12 +00:00
Lizzie Epton
f582b0215c edits-to-info-about-efp
Change-type: patch
2023-02-22 11:31:21 +00:00
Edwin Joassart
4c3c4babea Add reference to etcher-efp in publishing.md
Add reference to etcher-efp in publishing.md

Change-type: patch
2023-02-22 11:31:21 +00:00
balenaCI
6ec0550b4c v1.18.2 2023-02-21 13:17:11 +00:00
mcraa
4e9039c244 Merge pull request #4009 from balena-io/migrate-docs
Docs to Docusaurus
2023-02-21 13:16:15 +00:00
mcraa
e479b95d72 patch: organize docs 2023-02-21 13:39:28 +01:00
mcraa
926ff2b754 patch: actualized develop guide 2023-02-21 12:59:44 +01:00
mcraa
394b64319d patch: updated commit message guide 2023-02-21 12:58:13 +01:00
Lizzie Epton
96fa53b6ee add-item-from-FAQs
Change-type: patch
2023-02-20 11:07:03 +00:00
mcraa
9b54e2af0b patch: removed gt characters from contributing guide 2023-02-15 15:59:46 +01:00
mcraa
b01cf3c2e1 patch: added docosaurus site name 2023-02-15 15:59:46 +01:00
balenaCI
46307d85d8 v1.18.1 2023-02-15 14:54:47 +00:00
mcraa
772df8f5e7 Merge pull request #4012 from balena-io/electron-remote-rpiboot
patch: use @electron/remote for locating rpiboot files
2023-02-15 14:53:53 +00:00
mcraa
04fa3dcd8c patch: use @electron/remote for locating rpiboot files 2023-02-15 15:01:07 +01:00
balenaCI
6538864de4 v1.18.0 2023-02-14 18:07:07 +00:00
dfunckt
480adc3426 Merge pull request #4011 from balena-io/update-electron-19
Drop Spectron and update to Electron 19
2023-02-14 20:06:09 +02:00
Akis Kesoglou
c11db0a279 Update to Electron 19
Change-type: minor
2023-02-14 18:35:01 +02:00
Akis Kesoglou
6f7570d265 Remove Spectron and related (low-value) tests
Spectron is long deprecated and abandoned and the browser tests are so rudimentary that it’s no longer worth having them around. We will introduce a proper browser-based test suite in the short term — it’s a project in progress.

Change-type: minor
2023-02-14 18:34:56 +02:00
balenaCI
ae976894a3 v1.17.0 2023-02-14 16:18:57 +00:00
dfunckt
cd00f78c05 Merge pull request #4010 from balena-io/update-electron-17
Update to Electron 17 and Node 16
2023-02-14 18:17:55 +02:00
Akis Kesoglou
3c1dd6ce29 Update to Electron 17 and Node 16
This is the latest Electron version officially supported by Spectron.

Change-type: minor
2023-02-14 17:46:49 +02:00
balenaCI
5099c6ff21 v1.16.0 2023-02-14 12:40:43 +00:00
dfunckt
c63c98b80a Merge pull request #4003 from balena-io/update-electron
Update to Electron 14
2023-02-14 14:39:56 +02:00
builder555
6834dae281 this is no longer necessary, and breaks with new electron 2023-02-14 13:42:30 +02:00
Akis Kesoglou
df7854111a Update to Electron 14
Change-type: minor
2023-02-14 13:42:29 +02:00
balenaCI
c0404597c0 v1.15.6 2023-02-13 11:23:14 +00:00
mcraa
64eafdc6f0 Merge pull request #4004 from l10n-tw/master
patch: app: i18n: Translation: Update zh-TW strings
2023-02-13 11:22:27 +00:00
Edward Wu
b51418814f patch: app: i18n: Translation: Update zh-TW strings
* Improve translate.
* Sync layout with English strings ts file.

Signed-off-by: Edward Wu <bluehome.wu@gmail.com>
2023-02-12 09:31:06 +08:00
balenaCI
748f9d9147 v1.15.5 2023-02-03 14:25:21 +00:00
Edwin Joassart
5c8c4ea412 Merge pull request #4006 from balena-io/aethernet/restore-update
revert auto-update feature
2023-02-03 15:24:32 +01:00
JOASSART Edwin
e6d33eda2b revert auto-update feature
Change-type: patch
2023-02-03 11:31:56 +01:00
balenaCI
324102bc73 v1.15.4 2023-02-02 18:26:47 +00:00
Balena CI
e6182ff807 Merge pull request #4000 from balena-io/switch-to-electron-remote
Switch to `@electron/remote`
2023-02-02 20:26:02 +02:00
Akis Kesoglou
7ee174edce Switch to @electron/remote
Electron 12 deprecated `electron.remote` and the functionality was removed in Electron 14, but became available as a separate `@electron/remote` module. This commit makes the transition to the external module as an intermediary step to enable updating to a newer Electron version.

Change-type: patch
2023-02-02 19:50:04 +02:00
balenaCI
cbb4810260 v1.15.3 2023-02-02 17:23:19 +00:00
Balena CI
c3257216c2 Merge pull request #4001 from balena-io/aethernet/standalone-efp
move EFP & success-banner to efp.balena.io
2023-02-02 19:22:27 +02:00
Edwin Joassart
a140faaebe move EFP & success-banner to efp.balena.io
Change-type: patch
2023-02-02 14:06:29 +01:00
balenaCI
79200d1f79 v1.15.2 2023-02-02 13:05:03 +00:00
Balena CI
10e2da2c00 Merge pull request #4002 from balena-io/aethernet/remove-getconfig
Remove configuration remote update
2023-02-02 15:04:10 +02:00
Edwin Joassart
85a49a221f Remove configuration remote update
Change-type: patch
2023-02-01 15:09:04 +01:00
balenaCI
1bc64bbaf8 v1.15.1 2023-02-01 12:18:57 +00:00
dfunckt
180bd29afa Merge pull request #3995 from balena-io/fix-build
Fix build
2023-02-01 14:17:33 +02:00
Akis Kesoglou
48ddafd120 Remove redundant resinci-deploy build step
Change-type: patch
2023-02-01 12:50:00 +02:00
Akis Kesoglou
851219f835 Lazily import Electron from child-writer process
No idea how this *used* to work, but it doesn’t since 887ec428 and this is fixing it properly.

Change-type: patch
2023-02-01 11:44:39 +02:00
balenaCI
286c74b41b v1.15.0 2023-01-27 11:36:34 +00:00
dfunckt
8a0711e2a6 Merge pull request #3987 from balena-io/support-node-18
Add support for Node 18
2023-01-27 13:35:09 +02:00
Akis Kesoglou
887ec42847 Add support for Node 18
The Electron version we’re currently using is on Node 14 but this is a step forward to upgrading to a newer Electron and Node version.

Updates etcher-sdk and switches the redundant aws4-axios dependency to just axios.

Also changed bundler to stop trying to bundle wasm files — they must be included inline with JS code as data — and removed some now redundant code.

The crucial changes that enable support are:

1. The update to etcher-sdk@8 where some dependency fixes and updates took place
2. The downgrade and pinning of "electron-rebuild" to v3.2.3 until we’re able to update to Electron >= 14.2. The patch we need to avoid is https://github.com/electron/rebuild/pull/907. Also see: https://github.com/nodejs/node-gyp/issues/2673 and https://github.com/electron/rebuild/issues/913
3. A rule in webpack.config to ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used by etcher-sdk and does a runtime check to its availability. We’re not currently using the “assume role” functionality (AFAIU) of aws4-axios and we don’t care that it’s not found, so force webpack to ignore the import. See https://github.com/aws/aws-sdk-js-v3/issues/3025

Change-type: minor
2023-01-27 12:12:11 +02:00
balenaCI
62c3c35526 v1.14.3 2023-01-19 12:21:04 +00:00
Balena CI
1a368f55fa Merge pull request #3982 from balena-io/i18n-sudo-en-fallback
patch: fixed mac sudo on other languages
2023-01-19 14:19:34 +02:00
Peter Makra
19d1e093fc patch: fixed mac sudo on other languages 2023-01-19 11:56:44 +01:00
balenaCI
407138c999 v1.14.2 2023-01-17 14:37:43 +00:00
Balena CI
b5536bfc7f Merge pull request #3980 from balena-io/update-sdk-for-cm4v5
patch: update etcher-sdk for cm4v5
2023-01-17 16:36:07 +02:00
Peter Makra
72af77860b patch: revert to lockfile v1 2023-01-17 14:57:15 +01:00
builder555
8e63be2efe patch: update etcher-sdk for cm4v5
Change-type: patch
2023-01-16 16:34:02 -05:00
balenaCI
5f014e163e v1.14.1 2023-01-16 13:22:38 +00:00
Balena CI
bd88e5a1ca Merge pull request #3978 from balena-io/aethernet/fix-screensaver
fix disabled-screensaver unhandled exception outside balena-electron env
2023-01-16 15:21:13 +02:00
Edwin Joassart
5bd4e06cb9 send exeption to console even when error reporting is off 2023-01-16 13:24:12 +01:00
Edwin Joassart
46c406e8c1 fix disabled-screensaver unhandled exception outside balena-electron env
Change-type: patch
2023-01-16 12:48:56 +01:00
balenaCI
615e035a5d v1.14.0 2023-01-16 11:23:56 +00:00
Balena CI
7616c41564 Merge pull request #3891 from balena-io/removes-corvus
Removes corvus in favor of sentry and analytics client
2023-01-16 13:22:29 +02:00
balenaCI
d5ba1ea5e1 v1.13.4 2023-01-12 15:10:51 +00:00
Balena CI
54d3636a22 Merge pull request #3890 from balena-io/wolvi-lataniere/adding-serial-number-etcher-pro
Adding EtcherPro device serial number to the Settings modal
2023-01-12 17:09:11 +02:00
Aurelien VALADE
45f6ee667d Cleaning-up EtcherPro specific code 2023-01-12 14:52:08 +01:00
Aurelien VALADE
d25eda9a7d Adding EtcherPro device serial number to the Settings modal
Change-type: patch
2023-01-12 12:12:10 +01:00
Otávio Jacobi
86d43a536f Anonymizes all paths before sending
Change-type: patch
2023-01-12 11:11:52 +00:00
Edwin Joassart
6c417e35a1 patch: Sentry fix path 2023-01-12 11:11:52 +00:00
Otávio Jacobi
2b728d3c52 Remove personal path on etcher
Change-type: minor
2023-01-12 11:11:52 +00:00
Edwin Joassart
f3f7ecb852 Unifying sentry reports in a single project
Change-type: patch
2023-01-12 11:11:52 +00:00
Otávio Jacobi
41fca03c98 Removes corvus in favor of sentry and analytics client
Change-type: patch
Signed-off-by: Otavio Jacobi
2023-01-12 11:11:52 +00:00
Otávio Jacobi
10caf8f1b6 Removes corvus in favor of sentry and analytics client
Change-type: patch
Signed-off-by: Otavio Jacobi
2023-01-12 11:11:52 +00:00
balenaCI
7420283249 v1.13.3 2023-01-11 14:30:46 +00:00
Balena CI
453952440f Merge pull request #3971 from balena-io/mcraa/win-cm4
patch: progress cm4 to second stage
2023-01-11 16:28:49 +02:00
Peter Makra
2475d576c7 patch: progress cm4 to second stage 2023-01-11 13:36:11 +01:00
balenaCI
8cd6da1260 v1.13.2 2023-01-02 20:55:59 +00:00
Balena CI
82dd4fc1d1 Merge pull request #3964 from balena-io/fix-winget-releaser
patch: fixed winget parameter name
2023-01-02 15:54:20 -05:00
mcraa
33fe4b2c1a patch: fixed winget parameter name 2023-01-02 21:17:55 +01:00
balenaCI
b1c1188107 v1.13.1 2023-01-02 17:26:57 +00:00
Balena CI
63b45aefae Merge pull request #3959 from balena-io/update-copyright
patch: update copyright in electron-builder
2023-01-02 12:25:24 -05:00
Peter Makra
f79cb0fac5 patch: updated sdk to fix bz2 issue 2023-01-02 17:44:42 +01:00
JOASSART Edwin
ec42892c7c patch: update copyright in electron-builder 2023-01-02 12:45:42 +01:00
balenaCI
371716fe6a v1.13.0 2022-12-28 16:48:14 +00:00
Balena CI
d5fb6bec15 Merge pull request #3945 from balena-io/update-sdk-for-cm4
Patch: update etcher-sdk version to fix CM4 issues
2022-12-28 11:46:52 -05:00
Peter Makra
c5e7bf26d7 bump electron deps 2022-12-23 21:32:30 +01:00
Peter Makra
e3072ac416 minor: electron version bump 2022-12-23 21:32:30 +01:00
Peter Makra
dfaf06e4cf sdk version bump 2022-12-23 21:32:29 +01:00
Peter Makra
6e24d25576 fixed ext2fs regex 2022-12-23 21:32:29 +01:00
Peter Makra
b59b171e43 patch: handle ext2fs with webpack 2022-12-23 21:32:29 +01:00
Peter Makra
28726584c2 prerelease etcher-compat etcher-sdk 2022-12-23 21:32:29 +01:00
Peter Makra
00b151311a alignerd webpack to ext2fs 2022-12-23 21:32:28 +01:00
builder555
36c813714b Patch: update etcher-sdk version to fix CM4 issues
Change-type: patch
2022-12-23 21:32:28 +01:00
balenaCI
2ae6764dd9 v1.12.7 2022-12-20 19:35:13 +00:00
Balena CI
debefc9652 Merge pull request #3954 from balena-io/renovate/i18next-21.x
Update dependency i18next to 21.10.0
2022-12-20 14:33:35 -05:00
Renovate Bot
b068b847c7 Update dependency i18next to 21.10.0
Update i18next to 21.10.0

Update i18next from 21.8.14 to 21.10.0

Change-type: patch
2022-12-20 18:56:45 +00:00
balenaCI
6c410c07ce v1.12.6 2022-12-20 18:00:05 +00:00
Balena CI
c01206c1f3 Merge pull request #3953 from balena-io/renovate/react-i18next-11.x
Update dependency react-i18next to 11.18.6
2022-12-20 12:58:30 -05:00
Renovate Bot
2e85fb45de Update dependency react-i18next to 11.18.6
Update react-i18next to 11.18.6

Update react-i18next from 11.18.1 to 11.18.6

Change-type: patch
2022-12-20 17:02:05 +00:00
balenaCI
67513e384d v1.12.5 2022-12-20 12:27:35 +00:00
Balena CI
828dafa493 Merge pull request #3950 from balena-io/easier-text-settings
Patch: made trim setting more readable
2022-12-20 07:26:13 -05:00
builder555
5c5a761222 Patch: made trim setting more readable
Change-type: patch
2022-12-20 06:52:06 -05:00
balenaCI
fab10e5fc5 v1.12.4 2022-12-19 19:42:01 +00:00
Balena CI
797345fc1c Merge pull request #3780 from balena-io/actions
patch: introducing github actions (WinGet)
2022-12-19 14:40:29 -05:00
Anton Belodedenko
a0388a43c3 Update winget.yml 2022-12-19 11:05:16 -08:00
mcraa
f5b0a3023b Update winget.yml 2022-12-19 11:05:16 -08:00
mcraa
dc1d7bd1fd fixed version of action to v1 2022-12-19 11:05:16 -08:00
Vedant
9d674321b6 Update winget.yml 2022-12-19 11:05:16 -08:00
Begula
f9c8378d6a patch: publish to winget with gh action 2022-12-19 11:05:16 -08:00
balenaCI
65da751a52 v1.12.3 2022-12-19 09:51:53 +00:00
Balena CI
72142be0de Merge pull request #3948 from balena-io/fix-i18n-settings
Patch: replaced plain text with i18n in settings
2022-12-19 04:50:25 -05:00
builder555
11cea7c926 Patch: replaced plain text with i18n in settings
Change-type: patch
2022-12-16 14:42:28 -05:00
balenaCI
8d46ee4c22 v1.12.2 2022-12-16 16:59:05 +00:00
Balena CI
d63c09e2c2 Merge pull request #3944 from balena-io/renovate/webpack-dev-server-4.x
Update dependency webpack-dev-server to 4.11.1
2022-12-16 11:56:45 -05:00
Renovate Bot
c9e9d7d109 Update dependency webpack-dev-server to 4.11.1
Update webpack-dev-server to 4.11.1

Update webpack-dev-server from 4.5.0 to 4.11.1

Change-type: patch
2022-12-16 15:57:46 +00:00
balenaCI
2412d20eb4 v1.12.1 2022-12-16 15:02:36 +00:00
Balena CI
7f90d23a12 Merge pull request #3947 from balena-io/expose-trim-setting
Patch: expose trim ext{2,3,4} setting
2022-12-16 09:59:32 -05:00
builder555
b9a82be29b Patch: expose trim ext{2,3,4} setting
Change-type: patch
2022-12-16 09:24:49 -05:00
balenaCI
638673ba5e v1.12.0 2022-12-14 16:17:32 +00:00
Balena CI
898fe4f216 Merge pull request #3936 from balena-io/i18n-conflict-resolve
I18n conflict resolve
2022-12-14 11:15:48 -05:00
Peter Makra
7e805662d1 check if modal children is aray 2022-12-14 15:48:48 +01:00
Peter Makra
baf59c73ac populated lockfile 2022-12-14 12:24:40 +01:00
mcraa
38ad9c97c6 added i18next to devDependencies 2022-12-14 12:03:30 +01:00
ab77
8fc574f059 i18n support and Chinese translation
Change-type: minor
2022-12-12 18:36:32 -08:00
r-q
78b0f00e88 chore: bind some translations
according to a suggestion of @lurch
2022-12-12 18:36:32 -08:00
r-q
0f10f2d483 fix: suit i18n with mocha and optimize translation
- use `import * as i18next from 'i18next';` instead of `import i18next from 'i18next';` and add an specific env to bypass mocha test
- optimized several translations
2022-12-12 18:36:32 -08:00
r-q
eb5f5bbb9e fix: optimize translations
more direct string-concatenation, thanks to @lurch
2022-12-12 18:36:32 -08:00
r-q
67d26ff790 minor: optimize i18n
Optimized several translations.
This commit itself is only a patch, but as a pull request must have at least one commit with a change-type.

Change-Type: minor
2022-12-12 18:36:32 -08:00
r-q
17f2008d88 refactor: split translations to files
- split translations from i18n.ts to several .ts files in lib/gui/app/i18n
- make a README for new language changes
- add zh-TW instead of only zh-CN
2022-12-12 18:36:32 -08:00
r-q
db1bf7e488 feat: make i18n and add Chinese support
- make i18n using i18next
- add Chinese (Simplified) support
2022-12-12 18:36:32 -08:00
balenaCI
4b786b8a9f v1.11.10 2022-12-13 02:27:43 +00:00
Balena CI
fdfa0d3258 Merge pull request #3943 from balena-io/renovate/webpack-cli-4.x
Update dependency webpack-cli to 4.10.0
2022-12-12 21:26:03 -05:00
Renovate Bot
757aa77d89 Update dependency webpack-cli to 4.10.0
Update webpack-cli to 4.10.0

Update webpack-cli from 4.2.0 to 4.10.0

Change-type: patch
2022-12-13 01:18:03 +00:00
balenaCI
d70ea06565 v1.11.9 2022-12-12 23:58:11 +00:00
Balena CI
f2ebd10053 Merge pull request #3941 from balena-io/renovate/webpack-5.x
Update dependency webpack to 5.75.0
2022-12-12 18:56:06 -05:00
Renovate Bot
cd67b442c9 Update dependency webpack to 5.75.0
Update webpack to 5.75.0

Update webpack from 5.11.0 to 5.75.0

Change-type: patch
2022-12-12 22:55:21 +00:00
balenaCI
852c83c4fb v1.11.8 2022-12-12 21:55:30 +00:00
Balena CI
e3b2ee3b83 Merge pull request #3940 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.28
2022-12-12 16:54:15 -05:00
Renovate Bot
927a026b86 Update dependency awscli to 1.27.28
Update awscli to 1.27.28

Update awscli from 1.27.27 to 1.27.28

Change-type: patch
2022-12-12 20:57:48 +00:00
balenaCI
c851e1d54f v1.11.7 2022-12-12 19:57:33 +00:00
Balena CI
e6fdca171f Merge pull request #3939 from balena-io/renovate/uuid-8.x
Update dependency uuid to 8.3.2
2022-12-12 14:56:00 -05:00
Renovate Bot
c9cfb87733 Update dependency uuid to 8.3.2
Update uuid to 8.3.2

Update uuid from 8.1.0 to 8.3.2

Change-type: patch
2022-12-12 18:56:41 +00:00
balenaCI
b0b7c53294 v1.11.6 2022-12-12 17:59:45 +00:00
Balena CI
e8dc6579fe Merge pull request #3933 from balena-io/renovate/tslib-2.x
Update dependency tslib to 2.4.1
2022-12-12 12:57:52 -05:00
Renovate Bot
f0747abe3f Update dependency tslib to 2.4.1
Update tslib to 2.4.1

Update tslib from 2.0.0 to 2.4.1

Change-type: patch
2022-12-12 16:59:52 +00:00
Balena CI
32fab87340 Merge pull request #3935 from balena-io/aethernet-buildUbuntu20
Patch: run linux build on ubuntu-20.04
2022-12-12 11:10:38 -05:00
Edwin Joassart
adcd8e0325 Patch: run linux build on ubuntu-20.04
as [`18.04` has been removed](https://github.blog/changelog/2022-08-09-github-actions-the-ubuntu-18-04-actions-runner-image-is-being-deprecated-and-will-be-removed-by-12-1-22/)

We cannot use `latest` as the glibc version will cause issue with older ubuntu version.
2022-12-12 12:09:01 +01:00
balenaCI
7b5808eb2b v1.11.5 2022-12-10 12:28:20 +00:00
Balena CI
a8f7422cf5 Merge pull request #3932 from balena-io/renovate/ts-loader-8.x
Update dependency ts-loader to 8.4.0
2022-12-10 07:26:55 -05:00
Renovate Bot
5ae9a26361 Update dependency ts-loader to 8.4.0
Update ts-loader to 8.4.0

Update ts-loader from 8.0.12 to 8.4.0

Change-type: patch
2022-12-10 11:57:20 +00:00
balenaCI
cf1fdb8c5f v1.11.4 2022-12-10 10:57:12 +00:00
Balena CI
bf7ebde100 Merge pull request #3930 from balena-io/renovate/styled-components-5.x
Update dependency styled-components to 5.3.6
2022-12-10 05:55:52 -05:00
Renovate Bot
88c5fa5035 Update dependency styled-components to 5.3.6
Update styled-components to 5.3.6

Update styled-components from 5.1.0 to 5.3.6

Change-type: patch
2022-12-10 09:57:26 +00:00
balenaCI
887b0dd538 v1.11.3 2022-12-10 08:54:54 +00:00
Balena CI
364d1db56a Merge pull request #3931 from balena-io/renovate/terser-webpack-plugin-5.x
Update dependency terser-webpack-plugin to 5.3.6
2022-12-10 03:53:34 -05:00
Renovate Bot
c431222909 Update dependency terser-webpack-plugin to 5.3.6
Update terser-webpack-plugin to 5.3.6

Update terser-webpack-plugin from 5.2.5 to 5.3.6

Change-type: patch
2022-12-10 07:54:55 +00:00
balenaCI
55a0f68b97 v1.11.2 2022-12-10 07:32:27 +00:00
Balena CI
af2563dfc2 Merge pull request #3929 from balena-io/renovate/string-replace-loader-3.x
Update dependency string-replace-loader to 3.1.0
2022-12-10 02:31:00 -05:00
Renovate Bot
33f8851083 Update dependency string-replace-loader to 3.1.0
Update string-replace-loader to 3.1.0

Update string-replace-loader from 3.0.1 to 3.1.0

Change-type: patch
2022-12-10 06:58:36 +00:00
balenaCI
fe1f19b9fa v1.11.1 2022-12-10 06:19:32 +00:00
Balena CI
871cf3ec0a Merge pull request #3928 from balena-io/renovate/sinon-9.x
Update dependency sinon to 9.2.4
2022-12-10 01:17:59 -05:00
Renovate Bot
686a5837b6 Update dependency sinon to 9.2.4
Update sinon to 9.2.4

Update sinon from 9.0.2 to 9.2.4

Change-type: patch
2022-12-10 04:58:08 +00:00
balenaCI
23f2dd5ce5 v1.11.0 2022-12-10 04:27:20 +00:00
Balena CI
d5d39b395b Merge pull request #3927 from balena-io/renovate/shyaml-0.x
Update dependency shyaml to 0.6.2
2022-12-09 23:25:51 -05:00
Renovate Bot
2acad790d3 Update dependency shyaml to 0.6.2
Update shyaml to 0.6.2

Update shyaml from 0.5.0 to 0.6.2

Change-type: minor
2022-12-10 03:56:23 +00:00
balenaCI
30133306d6 v1.10.29 2022-12-10 03:05:04 +00:00
Balena CI
04a62f2ad8 Merge pull request #3925 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.27
2022-12-09 22:03:25 -05:00
Renovate Bot
17858a7d72 Update dependency awscli to 1.27.27
Update awscli to 1.27.27

Update awscli from 1.27.26 to 1.27.27

Change-type: patch
2022-12-10 02:19:49 +00:00
balenaCI
620307568f v1.10.28 2022-12-10 02:10:44 +00:00
Balena CI
a349c5d9ac Merge pull request #3926 from balena-io/renovate/rendition-19.x
Update dependency rendition to 19.3.2
2022-12-09 21:08:52 -05:00
Renovate Bot
0d740ad12d Update dependency rendition to 19.3.2
Update rendition to 19.3.2

Update rendition from 19.2.0 to 19.3.2

Change-type: patch
2022-12-09 23:58:04 +00:00
balenaCI
85a3f28869 v1.10.27 2022-12-09 20:59:25 +00:00
Balena CI
dbd5397405 Merge pull request #3924 from balena-io/renovate/redux-4.x
Update dependency redux to 4.2.0
2022-12-09 15:57:56 -05:00
Renovate Bot
85c183b9ef Update dependency redux to 4.2.0
Update redux to 4.2.0

Update redux from 4.0.5 to 4.2.0

Change-type: patch
2022-12-09 19:58:31 +00:00
balenaCI
0d0af1d1dd v1.10.26 2022-12-09 18:58:23 +00:00
Balena CI
ad423fc187 Merge pull request #3923 from balena-io/renovate/pretty-bytes-5.x
Update dependency pretty-bytes to 5.6.0
2022-12-09 13:56:50 -05:00
Renovate Bot
d8b2a7a236 Update dependency pretty-bytes to 5.6.0
Update pretty-bytes to 5.6.0

Update pretty-bytes from 5.3.0 to 5.6.0

Change-type: patch
2022-12-09 18:00:18 +00:00
balenaCI
13ec8cbe98 v1.10.25 2022-12-09 17:01:54 +00:00
Balena CI
a7cae23612 Merge pull request #3922 from balena-io/renovate/pnp-webpack-plugin-1.x
Update dependency pnp-webpack-plugin to 1.7.0
2022-12-09 12:00:09 -05:00
Renovate Bot
86bb093f3d Update dependency pnp-webpack-plugin to 1.7.0
Update pnp-webpack-plugin to 1.7.0

Update pnp-webpack-plugin from 1.6.4 to 1.7.0

Change-type: patch
2022-12-09 16:01:26 +00:00
balenaCI
997e1eb2f2 v1.10.24 2022-12-09 14:59:56 +00:00
Balena CI
34cc8b8933 Merge pull request #3921 from balena-io/renovate/node-ipc-9.x
Update dependency node-ipc to 9.2.1
2022-12-09 09:58:26 -05:00
Renovate Bot
f26b074811 Update dependency node-ipc to 9.2.1
Update node-ipc to 9.2.1

Update node-ipc from 9.1.1 to 9.2.1

Change-type: patch
2022-12-09 13:56:52 +00:00
balenaCI
adaa07b4b0 v1.10.23 2022-12-09 13:05:54 +00:00
Balena CI
96f4569342 Merge pull request #3919 from balena-io/renovate/mocha-8.x
Update dependency mocha to 8.4.0
2022-12-09 08:04:01 -05:00
Renovate Bot
be190c6c80 Update dependency mocha to 8.4.0
Update mocha to 8.4.0

Update mocha from 8.0.1 to 8.4.0

Change-type: patch
2022-12-09 11:59:55 +00:00
balenaCI
809617a82d v1.10.22 2022-12-09 10:57:45 +00:00
Balena CI
df02732002 Merge pull request #3918 from balena-io/renovate/mini-css-extract-plugin-1.x
Update dependency mini-css-extract-plugin to 1.6.2
2022-12-09 05:56:17 -05:00
Renovate Bot
d35f3c3049 Update dependency mini-css-extract-plugin to 1.6.2
Update mini-css-extract-plugin to 1.6.2

Update mini-css-extract-plugin from 1.3.3 to 1.6.2

Change-type: patch
2022-12-09 09:59:39 +00:00
balenaCI
8b047e3b14 v1.10.21 2022-12-09 08:58:01 +00:00
Balena CI
fa41d21e27 Merge pull request #3917 from balena-io/renovate/lint-staged-10.x
Update dependency lint-staged to 10.5.4
2022-12-09 03:56:44 -05:00
Renovate Bot
54e6c5e2c1 Update dependency lint-staged to 10.5.4
Update lint-staged to 10.5.4

Update lint-staged from 10.2.2 to 10.5.4

Change-type: patch
2022-12-09 07:57:13 +00:00
balenaCI
43fc3dd7eb v1.10.20 2022-12-09 06:59:32 +00:00
Balena CI
12a1340c8e Merge pull request #3916 from balena-io/renovate/husky-4.x
Update dependency husky to 4.3.8
2022-12-09 01:58:11 -05:00
Renovate Bot
cf8b5790a1 Update dependency husky to 4.3.8
Update husky to 4.3.8

Update husky from 4.2.5 to 4.3.8

Change-type: patch
2022-12-09 05:56:33 +00:00
balenaCI
659d85a833 v1.10.19 2022-12-09 04:58:50 +00:00
Balena CI
96c44d31c9 Merge pull request #3915 from balena-io/renovate/esbuild-loader-2.x
Update dependency esbuild-loader to 2.20.0
2022-12-08 23:56:57 -05:00
Renovate Bot
ba812b4f64 Update dependency esbuild-loader to 2.20.0
Update esbuild-loader to 2.20.0

Update esbuild-loader from 2.16.0 to 2.20.0

Change-type: patch
2022-12-09 03:59:40 +00:00
balenaCI
4087258fbd v1.10.18 2022-12-09 03:07:59 +00:00
Balena CI
955be13129 Merge pull request #3914 from balena-io/renovate/electron-updater-4.x
Update dependency electron-updater to 4.6.5
2022-12-08 22:06:19 -05:00
Renovate Bot
32011c0dea Update dependency electron-updater to 4.6.5
Update electron-updater to 4.6.5

Update electron-updater from 4.3.5 to 4.6.5

Change-type: patch
2022-12-09 02:21:28 +00:00
balenaCI
b68325c71c v1.10.17 2022-12-09 01:21:27 +00:00
Balena CI
84bce86fce Merge pull request #3913 from balena-io/renovate/electron-notarize-1.x
Update dependency electron-notarize to 1.2.2
2022-12-08 20:19:51 -05:00
Renovate Bot
d68eab1dda Update dependency electron-notarize to 1.2.2
Update electron-notarize to 1.2.2

Update electron-notarize from 1.0.0 to 1.2.2

Change-type: patch
2022-12-08 23:57:37 +00:00
balenaCI
09cf014d14 v1.10.16 2022-12-08 22:58:43 +00:00
Balena CI
d5bab5805f Merge pull request #3912 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.26
2022-12-08 17:57:07 -05:00
Renovate Bot
b5ab500a14 Update dependency awscli to 1.27.26
Update awscli to 1.27.26

Update awscli from 1.27.25 to 1.27.26

Change-type: patch
2022-12-08 21:59:13 +00:00
balenaCI
49253d37c9 v1.10.15 2022-12-08 21:37:02 +00:00
Balena CI
97cf3b25ad Merge pull request #3911 from balena-io/renovate/electron-builder-22.x
Update dependency electron-builder to 22.14.13
2022-12-08 16:35:33 -05:00
Renovate Bot
99862b95a5 Update dependency electron-builder to 22.14.13
Update electron-builder to 22.14.13

Update electron-builder from 22.10.5 to 22.14.13

Change-type: patch
2022-12-08 20:56:08 +00:00
balenaCI
8b765d58e5 v1.10.14 2022-12-08 19:55:07 +00:00
Balena CI
8f566e45b8 Merge pull request #3910 from balena-io/renovate/debug-4.x
Update dependency debug to 4.3.4
2022-12-08 14:53:44 -05:00
Renovate Bot
b8af86e30c Update dependency debug to 4.3.4
Update debug to 4.3.4

Update debug from 4.2.0 to 4.3.4

Change-type: patch
2022-12-08 18:58:28 +00:00
balenaCI
784f193b6d v1.10.13 2022-12-08 17:57:08 +00:00
Balena CI
3967adb1b5 Merge pull request #3909 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.25
2022-12-08 12:55:28 -05:00
Renovate Bot
0667d1110f Update dependency awscli to 1.27.25
Update awscli to 1.27.25

Update awscli from 1.27.24 to 1.27.25

Change-type: patch
2022-12-08 16:59:48 +00:00
balenaCI
61dd22bdf3 v1.10.12 2022-12-08 15:56:15 +00:00
Anton Belodedenko
24eb8b05b0 Merge pull request #3907 from balena-io/renovate/css-loader-5.x
Update dependency css-loader to 5.2.7
2022-12-08 07:54:36 -08:00
Renovate Bot
6991a4950b Update dependency css-loader to 5.2.7
Update css-loader to 5.2.7

Update css-loader from 5.0.1 to 5.2.7

Change-type: patch
2022-12-07 03:55:56 +00:00
balenaCI
bb169cf674 v1.10.11 2022-12-07 03:22:00 +00:00
Anton Belodedenko
e5d0d2e262 Merge pull request #3906 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.24
2022-12-06 19:20:29 -08:00
Renovate Bot
72b4d4f4fa Update dependency awscli to 1.27.24
Update awscli to 1.27.24

Update awscli from 1.27.5 to 1.27.24

Change-type: patch
2022-12-07 02:27:18 +00:00
balenaCI
9b2f2eb4c3 v1.10.10 2022-12-07 02:19:19 +00:00
Anton Belodedenko
ce52ef95a9 Merge pull request #3905 from balena-io/renovate/node-14.x
Update dependency @types/node to 14.18.34
2022-12-06 18:17:44 -08:00
Renovate Bot
aa3756ad17 Update dependency @types/node to 14.18.34
Update @types/node to 14.18.34

Update @types/node from 14.18.33 to 14.18.34

Change-type: patch
2022-12-07 01:17:15 +00:00
balenaCI
73081e726d v1.10.9 2022-12-06 23:59:23 +00:00
Anton Belodedenko
d53dc4149b Merge pull request #3903 from balena-io/ab77/operational
Enable repository configuration
2022-12-06 15:57:58 -08:00
ab77
0d5bb4935f Enable repository configuration
Change-type: patch
2022-12-06 14:59:31 -08:00
balenaCI
14aeb0060b v1.10.8 2022-12-05 21:38:43 +00:00
Anton Belodedenko
239726f3ce Merge pull request #3864 from balena-io/renovate/chai-4.x
Update dependency chai to 4.3.7
2022-12-05 13:37:20 -08:00
Renovate Bot
4ed3002716 Update dependency chai to 4.3.7
Update chai to 4.3.7

Update chai from 4.2.0 to 4.3.7

Change-type: patch
2022-12-05 13:04:10 -08:00
balenaCI
7286fba240 v1.10.7 2022-12-05 19:39:09 +00:00
Anton Belodedenko
895c306fb7 Merge pull request #3868 from balena-io/ab77/operational
Use core workflow for GitHub publish
2022-12-05 11:37:16 -08:00
ab77
f3844d56e2 Use core workflow for GitHub publish
Change-type: patch
2022-12-05 10:51:35 -08:00
balenaCI
540dc3150a v1.10.6 2022-12-02 14:05:00 +00:00
Edwin Joassart
035c8dfec3 Merge pull request #3897 from balena-io/aethernet-assetv
Dummy update to fix asset version issue
2022-12-02 15:03:34 +01:00
Edwin Joassart
03d6a011db Dummy update to fix asset version issue
Due to a race between two patch, 1.10.5 assets are labelled 1.10.3.
This dummy PR should fix this.

Change-type: patch
2022-12-02 14:17:31 +01:00
balenaCI
27f64650f9 v1.10.5 2022-12-02 12:41:21 +00:00
Edwin Joassart
ccca009972 Merge pull request #3893 from balena-io/aethernet-fix-ubuntu
Patch: run linux build on ubuntu-18.04
2022-12-02 13:39:56 +01:00
Edwin Joassart
57a6ceff0e Patch: run linux build on ubuntu-18.04
Running on ubuntu-latest means you need a more recent version of glibc which breaks on older ubuntu.

Thanks to @theofficialgman for suggesting the fix.
2022-12-02 13:00:01 +01:00
balenaCI
30c4baa58b v1.10.4 2022-12-01 23:27:57 +00:00
Anton Belodedenko
a930d77064 Merge pull request #3875 from p-linnane/brew-remove
Remove Homebrew instructions
2022-12-01 15:26:24 -08:00
Patrick Linnane
0d1cfffa5c patch: remove Homebrew instructions in README
Homebrew no longer supports etcher, so removing install instructions.

Change-type: patch
2022-12-01 14:35:59 -08:00
balenaCI
3c7422764c v1.10.3 2022-12-01 22:31:26 +00:00
Anton Belodedenko
55176b9f8f Merge pull request #3895 from balena-io/ab77/external-contributors
Allow external contributors
2022-12-01 14:29:59 -08:00
ab77
156b9314b5 Allow external contributors
Change-type: patch
2022-12-01 13:50:27 -08:00
balenaCI
76d22280dc v1.10.2 2022-11-25 19:22:51 +00:00
bulldozer-balena[bot]
e4251a3862 Merge pull request #3886 from balena-io/aethernet-patch-analytics
Fix missing analytics token
2022-11-25 19:21:35 +00:00
Edwin Joassart
831339bd2c Fix missing analytics token
Change-type: patch
Signed-off-by: Edwin Joassart edwin.joassart@balena.io
2022-11-25 19:14:58 +01:00
balenaCI
952ea80e15 v1.10.1 2022-11-21 16:50:16 +00:00
bulldozer-balena[bot]
813c497e4b Merge pull request #3882 from balena-io/wolvi-lataniere/fix-screensaver-methods-calls
Fixing call to electron block screensaver methods invocation
2022-11-21 16:48:50 +00:00
Aurelien VALADE
1b5b647135 Fixing call to electron block screensaver methods invocation
Replacing `send` calls to `invoke` for `enable/disable-screensaver` calls.

Change-type: patch
Signed-off-by: Aurelien VALADE <aurelien.valade@balena.io>
2022-11-21 16:26:15 +01:00
balenaCI
7de99003ca v1.10.0 2022-11-10 20:54:14 +00:00
bulldozer-balena[bot]
e09bdd734b Merge pull request #3871 from balena-io/test
testing renovate
2022-11-10 20:52:37 +00:00
builder555
306e087ec6 testing renovate
Change-Type: minor
2022-11-10 15:12:39 -05:00
balenaCI
c6b0178a87 v1.9.0 2022-11-08 21:37:26 +00:00
bulldozer-balena[bot]
4e581ea1ce Merge pull request #3861 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.5
2022-11-08 21:36:02 +00:00
Renovate Bot
26dc2d19e5 Update dependency awscli to 1.27.5
Update awscli to 1.27.5

Update awscli from 1.11.87 to 1.27.5

Change-type: minor
2022-11-08 20:50:05 +00:00
balenaCI
b99282acfb v1.8.17 2022-11-08 20:38:29 +00:00
bulldozer-balena[bot]
4e48724d0c Merge pull request #3860 from balena-io/renovate/react-dom-16.x
Update dependency @types/react-dom to 16.9.17
2022-11-08 20:36:50 +00:00
Renovate Bot
448ce141d5 Update dependency @types/react-dom to 16.9.17
Update @types/react-dom to 16.9.17

Update @types/react-dom from 16.8.4 to 16.9.17

Change-type: patch
2022-11-08 19:50:12 +00:00
balenaCI
695f287190 v1.8.16 2022-11-08 19:39:44 +00:00
bulldozer-balena[bot]
4de3271e15 Merge pull request #3858 from balena-io/renovate/react-16.x
Update dependency @types/react to 16.14.34
2022-11-08 19:38:16 +00:00
Renovate Bot
77b33b127d Update dependency @types/react to 16.14.34
Update @types/react to 16.14.34

Update @types/react from 16.8.5 to 16.14.34

Change-type: patch
2022-11-08 18:37:18 +00:00
balenaCI
9cd13ba381 v1.8.15 2022-11-08 18:21:06 +00:00
bulldozer-balena[bot]
9df23c8a3f Merge pull request #3859 from balena-io/ab77/operational
CI: generalise artefact handling
2022-11-08 18:19:29 +00:00
ab77
e3618b939e CI: generalise artefact handling
* on PR syncs, delete draft releases on Linux runners only
* delete draft releases when unmerged PRs are closed

Change-type: patch
2022-11-08 09:41:03 -08:00
balenaCI
6a39f5869a v1.8.14 2022-11-08 13:57:20 +00:00
bulldozer-balena[bot]
fd472efadc Merge pull request #3857 from balena-io/renovate/node-14.x
Update dependency @types/node to 14.18.33
2022-11-08 13:56:11 +00:00
Renovate Bot
7e2c2eae63 Update dependency @types/node to 14.18.33
Update @types/node to 14.18.33

Update @types/node from 14.14.41 to 14.18.33

Change-type: patch
2022-11-08 13:01:36 +00:00
balenaCI
5266571ca4 v1.8.13 2022-11-08 12:39:29 +00:00
bulldozer-balena[bot]
797868c474 Merge pull request #3856 from balena-io/renovate/copy-webpack-plugin-6.x
Update dependency @types/copy-webpack-plugin to 6.4.3
2022-11-08 12:38:11 +00:00
Renovate Bot
2c2a5c7c2b Update dependency @types/copy-webpack-plugin to 6.4.3
Update @types/copy-webpack-plugin to 6.4.3

Update @types/copy-webpack-plugin from 6.0.0 to 6.4.3

Change-type: patch
2022-11-08 11:47:28 +00:00
balenaCI
9e536d5337 v1.8.12 2022-11-08 11:33:03 +00:00
bulldozer-balena[bot]
860e680dd9 Merge pull request #3855 from balena-io/renovate/font-awesome
Update dependency @fortawesome/fontawesome-free to 5.15.4
2022-11-08 11:31:46 +00:00
Renovate Bot
7bb52aa170 Update dependency @fortawesome/fontawesome-free to 5.15.4
Update @fortawesome/fontawesome-free to 5.15.4

Update @fortawesome/fontawesome-free from 5.13.1 to 5.15.4

Change-type: patch
2022-11-08 10:50:37 +00:00
balenaCI
1c370f9100 v1.8.11 2022-11-08 10:38:49 +00:00
bulldozer-balena[bot]
ec7c772d0b Merge pull request #3854 from balena-io/renovate/balena-lint-5.x
Update dependency @balena/lint to 5.4.2
2022-11-08 10:37:18 +00:00
Renovate Bot
cc0285a77d Update dependency @balena/lint to 5.4.2
Update @balena/lint to 5.4.2

Update @balena/lint from 5.3.0 to 5.4.2

Change-type: patch
2022-11-08 09:46:20 +00:00
balenaCI
256d3550d1 v1.8.10 2022-11-08 09:35:05 +00:00
bulldozer-balena[bot]
db3a5f3b0a Merge pull request #3852 from balena-io/renovate/sys-class-rgb-led-3.x
Update dependency sys-class-rgb-led to 3.0.1
2022-11-08 09:33:38 +00:00
Renovate Bot
0e58edf113 Update dependency sys-class-rgb-led to 3.0.1
Update sys-class-rgb-led to 3.0.1

Update sys-class-rgb-led from 3.0.0 to 3.0.1

Change-type: patch
2022-11-08 08:54:54 +00:00
balenaCI
db136926a9 v1.8.9 2022-11-08 08:40:23 +00:00
bulldozer-balena[bot]
d84e7211be Merge pull request #3851 from balena-io/renovate/semver-7.x
Update dependency semver to 7.3.8
2022-11-08 08:38:44 +00:00
Renovate Bot
8357cc19d2 Update dependency semver to 7.3.8
Update semver to 7.3.8

Update semver from 7.3.2 to 7.3.8

Change-type: patch
2022-11-08 07:45:37 +00:00
balenaCI
2752b9fa95 v1.8.8 2022-11-08 07:33:39 +00:00
bulldozer-balena[bot]
0214be4953 Merge pull request #3850 from balena-io/renovate/omit-deep-lodash-1.x
Update dependency omit-deep-lodash to 1.1.7
2022-11-08 07:32:29 +00:00
Renovate Bot
a4f944e795 Update dependency omit-deep-lodash to 1.1.7
Update omit-deep-lodash to 1.1.7

Update omit-deep-lodash from 1.1.4 to 1.1.7

Change-type: patch
2022-11-08 06:35:41 +00:00
balenaCI
cd2ebf15fc v1.8.7 2022-11-08 06:19:14 +00:00
bulldozer-balena[bot]
7a7ea374e9 Merge pull request #3849 from balena-io/renovate/immutable-3.x
Update dependency immutable to 3.8.2
2022-11-08 06:17:03 +00:00
Renovate Bot
330df325f9 Update dependency immutable to 3.8.2
Update immutable to 3.8.2

Update immutable from 3.8.1 to 3.8.2

Change-type: patch
2022-11-08 05:22:49 +00:00
balenaCI
2fc0882b2e v1.8.6 2022-11-08 05:18:30 +00:00
bulldozer-balena[bot]
4dd779e010 Merge pull request #3847 from balena-io/renovate/electron-rebuild-3.x
Update dependency electron-rebuild to 3.2.9
2022-11-08 05:17:09 +00:00
Renovate Bot
3dc54405fe Update dependency electron-rebuild to 3.2.9
Update electron-rebuild to 3.2.9

Update electron-rebuild from 3.2.5 to 3.2.9

Change-type: patch
2022-11-08 04:39:11 +00:00
balenaCI
3f1aa5bac3 v1.8.5 2022-11-08 04:22:24 +00:00
bulldozer-balena[bot]
8f52fdb900 Merge pull request #3846 from balena-io/renovate/electron-mocha-9.x
Update dependency electron-mocha to 9.3.3
2022-11-08 04:21:09 +00:00
Renovate Bot
1b93891ed8 Update dependency electron-mocha to 9.3.3
Update electron-mocha to 9.3.3

Update electron-mocha from 9.3.2 to 9.3.3

Change-type: patch
2022-11-08 03:23:58 +00:00
balenaCI
33adc8ecf8 v1.8.4 2022-11-08 02:41:38 +00:00
bulldozer-balena[bot]
0455f7ea58 Merge pull request #3845 from balena-io/renovate/webpack-node-externals-2.x
Update dependency @types/webpack-node-externals to 2.5.3
2022-11-08 02:39:47 +00:00
Renovate Bot
ea5a167f4f Update dependency @types/webpack-node-externals to 2.5.3
Update @types/webpack-node-externals to 2.5.3

Update @types/webpack-node-externals from 2.5.0 to 2.5.3

Change-type: patch
2022-11-08 01:45:44 +00:00
balenaCI
8a1c4a4cc8 v1.8.3 2022-11-08 01:36:35 +00:00
bulldozer-balena[bot]
bd8bc81713 Merge pull request #3844 from balena-io/renovate/tmp-0.x
Update dependency @types/tmp to 0.2.3
2022-11-08 01:35:19 +00:00
Renovate Bot
98a5ddf58a Update dependency @types/tmp to 0.2.3
Update @types/tmp to 0.2.3

Update @types/tmp from 0.2.0 to 0.2.3

Change-type: patch
2022-11-08 00:34:31 +00:00
balenaCI
6223dbc541 v1.8.2 2022-11-08 00:19:28 +00:00
bulldozer-balena[bot]
7c56621c57 Merge pull request #3843 from balena-io/ab77/operational
Generate release notes with git
2022-11-08 00:18:08 +00:00
ab77
a61aa8e2be Generate release notes with git
Change-type: patch
2022-11-07 15:39:26 -08:00
balenaCI
7df4f9615b v1.8.1 2022-11-07 23:35:28 +00:00
bulldozer-balena[bot]
5742452fdf Merge pull request #3842 from balena-io/renovate/mime-types-2.x
Update dependency @types/mime-types to 2.1.1
2022-11-07 23:34:08 +00:00
Renovate Bot
fe09f9f862 Update dependency @types/mime-types to 2.1.1
Update @types/mime-types to 2.1.1

Update @types/mime-types from 2.1.0 to 2.1.1

Change-type: patch
2022-11-07 22:36:54 +00:00
balenaCI
3a4687ea0f v1.8.0 2022-11-07 22:27:05 +00:00
bulldozer-balena[bot]
db6490fb1b Merge pull request #3840 from balena-io/renovate/scripts-resin-digest
Update scripts/resin digest to 652fdd4
2022-11-07 22:25:48 +00:00
Renovate Bot
1642297101 Update scripts/resin digest to 652fdd4
Update scripts/resin to

Update scripts/resin from  to

Change-type: minor
2022-11-07 21:46:35 +00:00
balenaCI
5ecd223cfc v1.7.15 2022-11-07 21:36:38 +00:00
bulldozer-balena[bot]
306e40fd7b Merge pull request #3838 from balena-io/ab77/operational
Build targets individually
2022-11-07 21:34:46 +00:00
ab77
b58249b9c8 Build targets individually
Change-type: patch
2022-11-07 12:57:51 -08:00
balenaCI
b23b4b34d0 v1.7.14 2022-11-07 20:17:55 +00:00
bulldozer-balena[bot]
73bc921713 Merge pull request #3833 from balena-io/renovate/npm-lodash-vulnerability
Update dependency lodash to 4.17.21 [SECURITY]
2022-11-07 20:16:20 +00:00
Renovate Bot
f356e4c303 Update dependency lodash to 4.17.21 [SECURITY]
Update lodash to 4.17.21

Update lodash from 4.17.10 to 4.17.21

Change-type: patch
2022-11-07 19:31:58 +00:00
balenaCI
9888167f2e v1.7.13 2022-11-07 19:17:08 +00:00
bulldozer-balena[bot]
4561690478 Merge pull request #3837 from balena-io/ab77/operational
Update release notes on finalize
2022-11-07 19:15:38 +00:00
ab77
576113febf Update release notes on finalize
Change-type: patch
2022-11-07 10:33:38 -08:00
balenaCI
cc139bf750 v1.7.12 2022-11-07 18:32:20 +00:00
bulldozer-balena[bot]
ae91958c06 Merge pull request #3836 from balena-io/ab77/operational
Avoid duplicate releases
2022-11-07 18:30:56 +00:00
ab77
33dea6267f Avoid duplicate releases
Change-type: patch
2022-11-07 09:47:19 -08:00
balenaCI
c9a8bca96f v1.7.11 2022-11-07 17:32:27 +00:00
bulldozer-balena[bot]
8af376e608 Merge pull request #3832 from balena-io/ab77/operational
Only run finalize on Linux runners
2022-11-07 17:30:49 +00:00
ab77
9ab307df4f Only run finalize on Linux runners
Change-type: patch
2022-11-07 08:39:52 -08:00
balenaCI
e8a716f8bb v1.7.10 2022-11-07 15:59:42 +00:00
bulldozer-balena[bot]
a40e64f6cd Merge pull request #3819 from balena-io/ab77/operational
Switch to Flowzone
2022-11-07 15:58:11 +00:00
ab77
2e53feb38c Switch to Flowzone
Change-type: patch
2022-11-07 07:11:18 -08:00
Balena CI
5945ab1f50 v1.7.9 2022-04-22 16:47:29 +03:00
bulldozer-balena[bot]
59d67220d4 Merge pull request #3747 from balena-io/next-release
patch: deb afterinstall and readme updates
2022-04-22 13:45:50 +00:00
mcraa
61610ded84 patch: update allowed extensions to include deb afterinstall in build 2022-04-22 15:08:04 +02:00
Peter Makra
c87a132f40 patch: add update notification 2022-04-22 14:44:36 +02:00
Andrew Scheller
350d4de32b patch: fix usb-device-boot link in README
Change-type: patch
2022-03-31 17:46:08 +02:00
Ken Bannister
f5f9025d6d Fix application directory for Debian postinst script
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2022-03-31 17:45:10 +02:00
Balena CI
549d744d04 v1.7.8 2022-03-18 19:22:12 +02:00
bulldozer-balena[bot]
6194460dc2 Merge pull request #3723 from balena-io/v1.7.8-draft
small ui updates
2022-03-18 17:20:11 +00:00
Peter Makra
8370f638b4 patch: complete suse uninstall readme
Change-Type: patch
2022-03-11 00:40:20 +01:00
Peter Makra
ac34c51125 patch: completed suse instructions
Change-Type: patch
2022-03-11 00:33:29 +01:00
Peter Makra
b241470fe1 patch: order rpm instrictions
Change-Type: patch
2022-03-10 17:00:43 +01:00
Peter Makra
179697040c Merge branch 'v1.7.8-draft' of github.com:balena-io/etcher into v1.7.8-draft 2022-03-10 16:37:01 +01:00
Peter Makra
335766ed12 patch: enabled update notification for version 1.7.8
Change-Type: patch
2022-03-10 16:36:24 +01:00
Peter Makra
4c5d052a71 patch: updated title to balenaEtcher
fixes #3592

Change-Type: patch
2022-03-10 16:35:03 +01:00
Peter Makra
86423342a8 patch: cleanup and organize readme
Change-Type: patch
2022-03-10 16:27:38 +01:00
Peter Makra
d8b41552e3 patch: extend cloudsmith attribution in readme
Change-Type: patch
2022-03-10 16:08:43 +01:00
Logicer
11c65fb392 Update macOS Icon to Big Sur Style
Change-type: patch
2022-03-10 15:55:27 +01:00
Balena CI
bed126506f v1.7.7 2022-02-22 11:25:43 +02:00
bulldozer-balena[bot]
f6aeb52b16 Merge pull request #3711 from balena-io/fix-auto-update
patch: Fix auto update
2022-02-22 09:23:16 +00:00
Peter Makra
a5201942b8 patch: clarified update check 2022-02-22 09:50:34 +01:00
Peter Makra
c1f7164273 patch: autoupdate stagingPercentage check, include default
Change-Type: patch
2022-02-21 21:09:49 +01:00
Balena CI
6774bf784c v1.7.6 2022-02-21 18:00:46 +02:00
bulldozer-balena[bot]
56ec8b4eac Merge pull request #3699 from balena-io/update-issue-template
patch: add requirements and help to issue template
2022-02-21 15:59:05 +00:00
Peter Makra
35868509af patch: version number notification 2022-02-21 15:21:58 +01:00
Peter Makra
3ab6749f49 Merge branch 'master' into update-issue-template 2022-02-21 14:33:16 +01:00
Balena CI
7a012a92bc v1.7.5 2022-02-21 15:19:52 +02:00
bulldozer-balena[bot]
aba01825a0 Merge pull request #3700 from flec/fix-basic-auth
patch: fix flashing from URL when using basic auth
2022-02-21 13:17:31 +00:00
Peter Makra
907a3308de updated branch with 1.7.4 2022-02-21 13:53:16 +01:00
Peter Makra
4366bb372f patch: fixed typos in template 2022-02-21 13:49:18 +01:00
Marco Füllemann
a6f6cd4a19 patch: fix flashing from URL when using basic auth 2022-02-21 13:36:40 +01:00
Balena CI
03ee428039 v1.7.4 2022-02-21 13:44:44 +02:00
bulldozer-balena[bot]
8d652d064d Merge pull request #3704 from balena-io/update-electron-12.2.3
patch: updated electron to 12.2.3
2022-02-21 11:42:46 +00:00
Peter Makra
28adc34239 patch: set version update notification 1.7.3 2022-02-17 14:15:46 +01:00
Peter Makra
120e9bf42f Merge branch 'update-electron-12.2.3' of github.com:balena-io/etcher into update-electron-12.2.3 2022-02-17 13:47:02 +01:00
Peter Makra
59f54e194b patch: updated electron to 12.2.3
Change-Type: patch
2022-02-17 13:46:35 +01:00
Peter Makra
c4834e61a7 patch: updated electron to 12.2.3
Change-Type: patch
2022-02-17 13:37:17 +01:00
mcraa
e4d02bc561 patch: add requirements and help to issue template 2022-02-08 08:19:55 +00:00
mcraa
b9e54e39f7 patch: add requirements and help to issue template 2022-02-08 09:19:55 +01:00
Balena CI
f3c32eac65 v1.7.3 2021-12-29 17:09:42 +02:00
bulldozer-balena[bot]
9a303ab344 Merge pull request #3667 from balena-io/fix-message-of.null
patch: fix mesage of null
2021-12-29 15:06:59 +00:00
Peter Makra
9c1b55bebc patch: fix mesage of null
Change-Type: patch
2021-12-29 15:23:21 +01:00
Balena CI
30ae4bbd86 v1.7.2 2021-12-21 20:38:54 +02:00
bulldozer-balena[bot]
c6126a980a Merge pull request #3661 from balena-io/fix-windows-open-from-web
patch: fixed open from browser on windows
2021-12-21 18:36:22 +00:00
Peter Makra
ef90d048ca patch: fixed open from browser on windows
Change-type: patch
Signed-off-by: Peter Makra <peter@balena.io>
2021-12-21 17:47:10 +01:00
Balena CI
b938132038 v1.7.1 2021-11-29 11:05:10 +02:00
bulldozer-balena[bot]
3cb2e78fe7 Merge pull request #3630 from balena-io/improve-webpack-build
patch: Improve webpack build time
2021-11-29 09:02:29 +00:00
Lorenzo Alberto Maria Ambrosi
ea9875ddf0 patch: Revert back to electron-rebuild
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-22 12:24:26 +01:00
Lorenzo Alberto Maria Ambrosi
65dacd2ff2 patch: Disallow TS in JS
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-16 20:08:11 +01:00
Lorenzo Alberto Maria Ambrosi
a190818827 patch: Remove esInterop TS flag
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 14:39:33 +01:00
Lorenzo Alberto Maria Ambrosi
98e33b619b patch: Use @balena/sudo-prompt
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 14:39:07 +01:00
Lorenzo Alberto Maria Ambrosi
685ed715ac patch: Update rpiboot guide link
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 11:47:11 +01:00
Lorenzo Alberto Maria Ambrosi
3cf3c4b398 patch: Improve webpack build time
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 10:45:00 +01:00
Balena CI
1c2ef4b1d4 v1.7.0 2021-11-09 18:09:26 +02:00
bulldozer-balena[bot]
d22fc91585 Merge pull request #3628 from balena-io/device-info-draft
Device info draft
2021-11-09 16:07:20 +00:00
Lorenzo Alberto Maria Ambrosi
0a28af5c35 patch: Add missing @types/react@16.8.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-09 14:10:18 +01:00
Lorenzo Alberto Maria Ambrosi
0c1e5b88ef patch: Use npm ci in Makefile
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-09 13:53:04 +01:00
Lorenzo Alberto Maria Ambrosi
790201be90 patch: Add draft info boxes for system information
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-27 10:41:56 +02:00
Lorenzo Alberto Maria Ambrosi
d8d379f05e patch: Remove electron-rebuild package
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:42:46 +02:00
Lorenzo Alberto Maria Ambrosi
b5e9701048 patch: Make electron a dev. dependency
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:40:37 +02:00
Lorenzo Alberto Maria Ambrosi
292f86d6f5 patch: Remove electron-rebuild package
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:40:15 +02:00
Lorenzo Alberto Maria Ambrosi
76ca9934c8 patch: Use exact modules versions
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 11:24:32 +02:00
Lorenzo Alberto Maria Ambrosi
37b826ee4e patch: Update etcher-sdk from v6.2.5 to v6.3.0
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:27:32 +02:00
JSReds
1e1bd3c508 Fix write step for Http file process
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-10-13 17:22:57 +02:00
Lorenzo Alberto Maria Ambrosi
00e8f11913 patch: Fix linting errors
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:21:48 +02:00
Lorenzo Alberto Maria Ambrosi
a3c24a26a0 minor: Refactor dependencies installation to avoid custom scripts
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:16:11 +02:00
Lorenzo Alberto Maria Ambrosi
4232928ad8 patch: Fix LEDs init error
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-30 13:05:47 +02:00
Balena CI
b165fb78da v1.6.0 2021-09-24 20:12:18 +03:00
bulldozer-balena[bot]
e9f6c5ead9 Merge pull request #3599 from balena-io/led-color-settings
Led color settings
2021-09-24 17:10:00 +00:00
Marco Füllemann
b2d0c1c9dd add support for basic auth when downloading images from URL
When selecting "Flash from URL" the user can optionally provide a username and password for basic authentication. The authentication input fields are collapsed by default. When the authentication input fields are collapsed after entering values the values are cleared to ensure that the user sees all parameter passed to the server.

Change-Type: minor
Changelog-Entry: Add support for basic auth when downloading images from URL.
2021-09-17 11:16:29 +02:00
Lorenzo Alberto Maria Ambrosi
14d91400a4 patch: Update etcher-sdk from v6.2.1 to v6.2.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-17 11:11:36 +02:00
David Gaspar
d0114aece7 Update Makefile to Apple M1 info
Expanding host architecture detection.

Change-type: patch
2021-09-07 16:56:10 +02:00
Lorenzo Alberto Maria Ambrosi
dff2df4aab Add LED settings for potentially different hardware
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-07 15:20:56 +02:00
Balena CI
13159f93ee v1.5.122 2021-09-02 17:48:21 +03:00
bulldozer-balena[bot]
3ece1fd841 Merge pull request #3590 from balena-io/various-fixes
Various fixes
2021-09-02 14:46:26 +00:00
Lorenzo Alberto Maria Ambrosi
f46963b6b3 Restore image file selection LED-drive pathing
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-02 14:16:18 +02:00
Lorenzo Alberto Maria Ambrosi
b97f4e0031 Update scripts submodule
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-31 16:40:48 +02:00
Lorenzo Alberto Maria Ambrosi
e2d233d74b Change LEDs colours
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-26 20:15:49 +02:00
Lorenzo Alberto Maria Ambrosi
a7ca2e527b Restore windows images warning
Change-type: patch
Changelog-entry: Windows images now show the proper warning again
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-26 20:15:48 +02:00
Mohamed Salah
396a053c0a Fix Update and install with DNF instructions
Change-type: patch
2021-08-26 20:15:48 +02:00
JSReds
d1a3f1cb88 Add possibile authorization as a query param
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-08-26 20:15:48 +02:00
Xtraim
9f96558cdd update the windows part
I choose to add this part because, after the clean the usb stick
could stay in a raw state without creating the new partions,
activating and formatting.
Thanks

Change-type: patch
2021-08-20 08:31:49 +02:00
thambu1710
b3bc589d70 Update SUPPORT.md
Change-type: patch
2021-08-20 08:31:45 +02:00
Seth Falco
18d2c28110 replace make webpack with npm run webpack
Change-type: patch
2021-08-20 08:31:38 +02:00
JSReds
b272ef296d Add loader on image select
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-08-20 08:31:27 +02:00
Zane Hitchcox
32ca28a3a9 add pnp-webpack-plugin
Change-type: patch
2021-08-20 08:31:15 +02:00
Lorenzo Alberto Maria Ambrosi
4d5e5a3b0b Remove redundant codespell dependency/tests
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-20 08:28:48 +02:00
Balena CI
8b3f37102d v1.5.121 2021-07-14 19:55:16 +03:00
bulldozer-balena[bot]
4b74253631 Merge pull request #3489 from balena-io/direct-select-drive
patch: Select drive on list interaction rather than modal closing
2021-07-14 16:52:42 +00:00
Vipul Gupta
a81b552b95 patch: Delete Codeowners 2021-07-02 13:59:11 +02:00
Lorenzo Alberto Maria Ambrosi
53f53c0f75 Add source maps for devtools
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-25 17:28:17 +02:00
Lorenzo Alberto Maria Ambrosi
fdaf5c69d6 Clone submodules when initializing modules
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-24 14:52:22 +02:00
Lorenzo Alberto Maria Ambrosi
061afca5d3 patch: Select drive on list interaction rather than modal closing
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-24 14:52:22 +02:00
Balena CI
ccb08a48f1 v1.5.120 2021-05-11 19:56:49 +03:00
bulldozer-balena[bot]
a8f3d45b12 Merge pull request #3514 from balena-io/add-cloudsmith-reference
Update README to reference Cloudsmith
2021-05-11 16:49:15 +00:00
Lorenzo Alberto Maria Ambrosi
7e333caaf9 Update README to reference Cloudsmith
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-11 17:59:17 +02:00
Balena CI
70229e8684 v1.5.119 2021-05-01 11:45:32 +03:00
bulldozer-balena[bot]
261700389b Merge pull request #3500 from balena-io/new-deb-rpm-ppa
Update readme for new PPA provider
2021-05-01 08:43:36 +00:00
Lorenzo Alberto Maria Ambrosi
250aed2eb1 Update readme for new PPA provider
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-04-30 23:30:08 +02:00
Balena CI
ed1f008fe2 v1.5.118 2021-04-29 11:47:01 +03:00
bulldozer-balena[bot]
e9ce270dab Merge pull request #3495 from balena-io/etcher-dev-server
Etcher dev server
2021-04-29 08:45:03 +00:00
Zane Hitchcox
1ee110bc95 patch: development environment
Add webpack dev server and hot module reloading to get live changes and reloads without reloading the whole electron app.

This patch also runs the development environment in development mode, which is much, much faster on builds and rebuilds.
2021-04-26 21:17:32 -04:00
Zane Hitchcox
33dd07c675 patch: watch files for electron 2021-04-20 22:30:05 -04:00
Balena CI
39ccbbeeda v1.5.117 2021-04-06 14:44:31 +03:00
bulldozer-balena[bot]
55d2400ac7 Merge pull request #3432 from balena-io/electron-11
Electron 11
2021-04-06 11:42:19 +00:00
Alexis Svinartchouk
0bdea5c54c Rename mac releases (keep old naming)
Change-type: patch
2021-04-02 15:52:33 +02:00
Alexis Svinartchouk
3be372d49f Disable spectron tests on macOS
Change-type: patch
2021-04-01 15:48:39 +02:00
Alexis Svinartchouk
d0c66b2c48 Update electron to v12.0.2
Change-type: patch
2021-04-01 12:13:34 +02:00
Alexis Svinartchouk
65082c4790 Update etcher-sdk from 6.1.1 to 6.2.1
Update etcher-sdk from 6.1.1 to 6.2.1

Change-type: patch
2021-03-29 14:11:44 +02:00
Alexis Svinartchouk
e87ed9beed Fix getAppPath() returning an asar file on macOS
Change-type: patch
2021-03-23 17:53:54 +01:00
Andrew Scheller
bc5563d9c2 Grammar fix
"flash directly" sounds odd

Change-type: patch
2021-03-23 14:32:30 +01:00
vlad doster
ad83ab5dcc (docs) update README.md
- fix spelling
- emphasize notes
- add link
- fix macOS to account for new homebrew API

Change-type: patch
2021-03-23 12:00:40 +01:00
Andrew Scheller
0dc1cf9701 Update copyright year in electron-builder.yml
Change-type: patch
2021-03-23 11:55:43 +01:00
Andrew Scheller
11489c6538 Update copyright year in .resinci.json
Change-type: patch
2021-03-23 11:55:29 +01:00
Dugan Chen
2619d4bc86 Separate the Yum and DNF instructions.
Change-type: patch
2021-03-23 11:55:04 +01:00
Alexis Svinartchouk
3730efd350 Set msvs_version to 2019 when rebuilding
Change-type: patch
2021-03-22 17:23:43 +01:00
Alexis Svinartchouk
6ece32c546 Use moduleIds: 'natural' in webpack config to keep js files in arm64 and x64 mac builds identical
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
fd9996a3cc Update electron-builder to 22.10.5
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
f06cc89152 Update spectron to v13
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
c1d7ab3fa9 Update dependencies, use aws4-axios@2.2.1 to avoid adding more dependiencies
Also filter out dmg-license dependencies from the shrinkwrap file
aws4-axios@2.3.0 brings in react-native, see aws/aws-sdk-js-v3#1797

Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
b206483c7c Update scripts to build universal mac dmgs on the ci
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
c3eb8c7b56 Fix beforeBuild.js script to also work on mac
Change-type: patch
2021-03-15 19:26:49 +01:00
Alexis Svinartchouk
0849d4f435 Support building universal dmgs (x64 and arm64) for mac
Change-type: patch
2021-03-15 19:26:49 +01:00
Alexis Svinartchouk
1dba3ae19b Update electron-builder to 22.10.4
Change-type: patch
2021-02-16 15:49:18 +01:00
Alexis Svinartchouk
f33f2e3771 Fix titlebar z-index
Change-type: patch
2021-02-16 15:49:18 +01:00
Alexis Svinartchouk
e56aaed973 Explicitly set contextIsolation to false
Change-type: patch
2021-02-16 15:49:12 +01:00
Alexis Svinartchouk
a4659f038e Update electron from 9.4.1 to 11.2.3
Change-type: patch
2021-02-10 17:51:56 +01:00
Alexis Svinartchouk
cd462818da Update etcher-sdk from 6.1.0 to 6.1.1
Update etcher-sdk from 6.1.0 to 6.1.1

Change-type: patch
2021-02-10 17:50:47 +01:00
Balena CI
37769efbed v1.5.116 2021-02-03 17:56:40 +02:00
bulldozer-balena[bot]
0f70c4bbce Merge pull request #3414 from balena-io/116
116
2021-02-03 15:54:29 +00:00
Alexis Svinartchouk
48b5e8b9d9 Only cleanup temporary decompressed files in child-writer
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
1f138f0ecc Add .versionbot/CHANGELOG.yml
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
73f67e99ca Stop using node-tmp, use withTmpFile from etcher-sdk instead
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
9114da2445 Update etcher-sdk from 5.2.2 to 6.1.0
Update etcher-sdk from 5.2.2 to 6.1.0

Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
554bbcc780 Revert "Change some border colors to have higher contrast"
This reverts commit 8c4edaabba.

Change-type: patch
2021-02-01 19:44:37 +01:00
Alexis Svinartchouk
4db2289cfd Update electron to v9.4.1
Change-type: patch
2021-02-01 19:44:37 +01:00
Alexis Svinartchouk
c15b56bc23 Update etcher-sdk from 5.2.1 to 5.2.2
Update etcher-sdk from 5.2.1 to 5.2.2

Change-type: patch
2021-01-19 18:44:19 +01:00
Balena CI
9f52dda6ae v1.5.115 2021-01-18 14:09:10 +02:00
bulldozer-balena[bot]
fadcefb11a Merge pull request #3413 from balena-io/115
Update etcher-sdk from 5.1.12 to 5.2.1
2021-01-18 12:07:11 +00:00
Alexis Svinartchouk
361c32913c Update etcher-sdk from 5.1.12 to 5.2.1
Update etcher-sdk from 5.1.12 to 5.2.1

Change-type: patch
2021-01-18 10:46:59 +01:00
Balena CI
5c2042198e v1.5.114 2021-01-15 14:30:49 +02:00
bulldozer-balena[bot]
99df53098c Merge pull request #3394 from balena-io/114
114
2021-01-15 12:28:31 +00:00
Alexis Svinartchouk
aa563c87bd Remove libappindicator1 debian dependency
Changelog-entry: Remove libappindicator1 debian dependency
Change-type: patch
2021-01-12 15:22:43 +01:00
Alexis Svinartchouk
1188888956 Update etcher-sdk from 5.1.11 to 5.1.12
Update etcher-sdk from 5.1.11 to 5.1.12

Change-type: patch
2021-01-12 15:22:43 +01:00
Alexis Svinartchouk
f9d7991dc8 Update rendition from 18.8.3 to 19.2.0
Update rendition from 18.8.3 to 19.2.0

Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
53954e81fd Update dependencies
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
f82996bfd1 Update @balena/lint to 5.3.0
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
b74069eb41 Update webpack to v5
Changelog-entry: Update webpack to v5
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
e8c7591751 Fix typo in webpack.config.ts comment
Change-type: patch
2021-01-12 15:22:42 +01:00
Aaron Shaw
3521b61a81 docs: fix quote marks
Fix quote mark styling

Change-type: patch
Signed-off-by: Aaron Shaw <aaron@balena.io>
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
93db90c725 Disable screensaver while flashing (on balena-electron-env)
Change-type: patch
2021-01-12 15:22:42 +01:00
Balena CI
1dc56aed14 v1.5.113 2020-12-17 16:23:23 +02:00
bulldozer-balena[bot]
d814202424 Merge pull request #3377 from balena-io/113
113
2020-12-17 14:20:57 +00:00
Alexis Svinartchouk
c54856a616 Only store the first error for each target
Changelog-entry: Show the first error for each drive (not the last)
Change-type: patch
2020-12-16 12:33:17 +01:00
Alexis Svinartchouk
fc45df270a Fix red leds not showing for failed devices
Change-type: patch
2020-12-14 18:59:40 +01:00
Aaron Shaw
3cde2faed0 docs: add documentation links
add documentation and faq links

Change-Type: patch
Closes: https://github.com/balena-io/etcher/issues/3191
Signed-off-by: Aaron Shaw <aaron@balena.io>
2020-12-14 18:59:40 +01:00
Aaron Shaw
b4b8c89aad docs: update macOS version
Update macOS version as latest version of Electron is 10.10 compatible only (Yosemite)

Change-Type: patch
Signed-off-by: Aaron Shaw <aaron@balena.io>
2020-12-14 18:59:40 +01:00
Alexis Svinartchouk
36d05724c0 Improve hover message when the drive is too small
Changelog-entry: Improve hover message when the drive is too small
Change-type: patch
2020-12-14 18:59:40 +01:00
Alexis Svinartchouk
b1e4e681d1 Update electron to v9.4.0
Changelog-entry: Update electron to v9.4.0
Change-type: patch
2020-12-14 18:59:40 +01:00
Giovanni Garufi
3987078c11 Update npm to v6.14.8
Change-type: patch
2020-12-11 17:51:50 +01:00
Alexis Svinartchouk
de0010eb72 Update rgb leds colors
Change-type: patch
2020-12-10 17:18:54 +01:00
Alexis Svinartchouk
1f94f44b18 Remove unmountOnSuccess setting
Changelog-entry: Remove unmountOnSuccess setting
Change-type: patch
2020-12-10 15:36:19 +01:00
Alexis Svinartchouk
fe0b45cae6 Only show auto-updates setting on supported targets
Change-type: patch
2020-12-10 15:35:37 +01:00
Alexis Svinartchouk
c32e485f27 Remove dead code in settings modal
Change-type: patch
2020-12-10 14:05:08 +01:00
Alexis Svinartchouk
409b78fc21 Fix effective flashing speed calculation for compressed images
Changelog-entry: Fix effective flashing speed calculation for compressed images
Change-type: patch
2020-12-08 17:14:49 +01:00
bulldozer-balena[bot]
2f08142f5a Merge pull request #3379 from balena-io/high-contrast-lines
Change some border colors to have higher contrast
2020-12-08 13:54:20 +00:00
Lorenzo Alberto Maria Ambrosi
8c4edaabba Change some border colors to have higher contrast
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-12-08 14:37:24 +01:00
Alexis Svinartchouk
05497ce85c Update etcher-sdk from 5.1.10 to 5.1.11
Update etcher-sdk from 5.1.10 to 5.1.11

Changelog-entry: Update etcher-sdk from 5.1.10 to 5.1.11
Change-type: patch
2020-12-07 19:31:41 +01:00
Alexis Svinartchouk
d3df2fe57e Update sys-class-rgb-led from 2.1.1 to 3.0.0
Update sys-class-rgb-led from 2.1.1 to 3.0.0

Changelog-entry: Update sys-class-rgb-led from 2.1.1 to 3.0.0
Change-type: patch
2020-12-04 14:11:51 +01:00
Balena CI
a0f07082f2 v1.5.112 2020-12-03 17:19:22 +02:00
bulldozer-balena[bot]
b7efa8e1f0 Merge pull request #3362 from balena-io/112
112
2020-12-03 15:17:28 +00:00
Alexis Svinartchouk
3647457bb5 Add rendition and sys-class-rgb-led to repo.yml
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
2e5a39dcd8 Update sys-class-rgb-led from 2.1.0 to 2.1.1
Update sys-class-rgb-led from 2.1.0 to 2.1.1

Changelog-entry: Update sys-class-rgb-led from 2.1.0 to 2.1.1
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
edabacfb3a Fix spectron test to work on Windows in all cases
Change-type: none
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
f46176fd10 Fix layout when the featured project is not showing
Changelog-entry: Fix layout when the featured project is not showing
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
2158e20380 Improve flashing error handling
Changelog-entry: Improve flashing error handling
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
fa593e33d1 Update repo.yml to enable nested changelogs
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
50730bd3df Fix imports in child-writer.ts
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
4e68955981 Target commit instead of branch name for sudo-prompt
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
3c0084d012 Fix modal content height on Windows
Change-type: patch
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
8bd11a01ae Update etcher-sdk from 5.1.5 to 5.1.10
Update etcher-sdk from 5.1.5 to 5.1.10

Changelog-entry: Update etcher-sdk from 5.1.5 to 5.1.10
Change-type: patch
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
da3a22d0f6 Set useContentSize to true so the size is the same on all platforms
Changelog-entry: Set useContentSize to true so the size is the same on all platforms
Change-type: patch
2020-11-24 17:10:17 +01:00
Balena CI
e708212d41 v1.5.111 2020-11-23 19:54:50 +02:00
bulldozer-balena[bot]
a5ceba8435 Merge pull request #3345 from balena-io/111
111
2020-11-23 17:52:38 +00:00
Alexis Svinartchouk
446e8e1253 Update bl
Change-type: patch
2020-11-20 20:10:35 +01:00
Alexis Svinartchouk
c69b2fa053 Warn when the source drive has no partition table
Changelog-entry: Warn when the source drive has no partition table
Change-type: patch
2020-11-20 15:29:03 +01:00
Alexis Svinartchouk
0597c0e908 Update etcher-sdk to 5.1.5
Change-type: patch
2020-11-20 14:49:41 +01:00
Alexis Svinartchouk
af2b6bc8ca Update typescript to 4.1.2
Change-type: patch
2020-11-20 14:45:44 +01:00
Alexis Svinartchouk
a2c7a542df Use a different icon when no source drive is available
Changelog-entry: Use a different icon when no source drive is available
Change-type: patch
2020-11-20 14:45:18 +01:00
Alexis Svinartchouk
e37ae2743f Update etcher-sdk to 5.1.3
Change-type: patch
2020-11-17 11:33:36 +01:00
Alexis Svinartchouk
644d955f08 Prevent opening more than one file selector
Change-type: patch
2020-11-16 16:14:36 +01:00
Alexis Svinartchouk
e7b4f09021 Allow selecting a locked SD card as the source drive
Changelog-entry: Allow selecting a locked SD card as the source drive
Change-type: patch
2020-11-16 14:16:38 +01:00
Alexis Svinartchouk
1e0a6a3129 Removed disableExplicitDriveSelection setting, use autoSelectAllDrives instead
Change-type: patch
2020-11-13 20:23:07 +01:00
Alexis Svinartchouk
ef3b8915d8 Update etcher-sdk to 5.1.2
Change-type: patch
2020-11-13 18:30:26 +01:00
Alexis Svinartchouk
e58cfd89c5 Add successBannerURL setting
Change-type: patch
2020-11-11 13:31:04 +01:00
Alexis Svinartchouk
1c52379ee3 Add drivesOrder setting
Change-type: patch
2020-11-11 13:30:54 +01:00
Alexis Svinartchouk
e2c2b40690 Remove "Validate write on success" setting
Validation is always enabled, press the "skip" button to skip it.

Changelog-entry: Remove "Validate write on success" setting. Validation is always enabled, press the "skip" button to skip it.
Change-type: patch
2020-11-11 13:30:26 +01:00
Alexis Svinartchouk
bddb89e4a1 Update electron to v9.3.3
Changelog-entry: Update electron to v9.3.3
Change-type: patch
2020-11-11 13:30:18 +01:00
Alexis Svinartchouk
560ed91e2e Update etcher-sdk to 5.1.1, use WASM ext2fs module
Changelog-entry: Update etcher-sdk to 5.1.1, use WASM ext2fs module
Change-type: patch
2020-11-11 13:29:44 +01:00
Balena CI
1f8f7ad7f8 v1.5.110 2020-11-05 13:56:29 +02:00
bulldozer-balena[bot]
a2a0f2ef41 Merge pull request #3325 from balena-io/new-success-screen-2
New success screen 2
2020-11-05 11:54:37 +00:00
Lorenzo Alberto Maria Ambrosi
40e5fb2287 Add primary colors to default flow
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-04 14:01:56 +01:00
Lorenzo Alberto Maria Ambrosi
6c49c71b3f Remove console.log in tests
Change-type: patch
Changelog-entry: Remove console.log in tests
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
deb3db0fff Add more typings & refactor code accordingly
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
4872fa3d6e Fix URL not being selected with custom protocol
Change-type: patch
Changelog-entry: Fix URL not being selected with custom protocol
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
640a7409ee Add dash on table when selecting only some rows
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
a7637ad8d4 Fix settings spacing
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:51 +01:00
Lorenzo Alberto Maria Ambrosi
31409c61ca Use drive-selector's table for flash errors table
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:02:46 +01:00
Lorenzo Alberto Maria Ambrosi
e74dc9eb60 Update rendition to v18.8.3
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:11 +01:00
Lorenzo Alberto Maria Ambrosi
06997fdf29 Fix zoomFactor in webviews
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:11 +01:00
Lorenzo Alberto Maria Ambrosi
611e659626 Add retry button to the errors modal in success screen
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-11-03 15:01:07 +01:00
Lorenzo Alberto Maria Ambrosi
e484ae9837 Cleanup after child-process is terminated
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:08:44 +02:00
Lorenzo Alberto Maria Ambrosi
7e7ca9524e Add skip function to validation
Change-type: patch
Changelog-entry: Add skip function to validation
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:08:44 +02:00
Lorenzo Alberto Maria Ambrosi
db09b7440d Rework success screen
Change-type: patch
Changelog-entry: Rework success screen
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-22 14:07:20 +02:00
125 changed files with 47163 additions and 15315 deletions

View File

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

8
.gitattributes vendored
View File

@@ -1,3 +1,6 @@
# default
* text
# Javascript files must retain LF line-endings (to keep eslint happy)
*.js text eol=lf
*.jsx text eol=lf
@@ -27,6 +30,7 @@ Makefile text
*.yml text
*.patch text
*.txt text
*.tpl text
CODEOWNERS text
*.plist text
@@ -58,3 +62,7 @@ CODEOWNERS text
*.ttf binary diff=hex
xz-without-extension binary diff=hex
wmic-output.txt binary diff=hex
# gitsecret
*.secret binary
.gitsecret/** binary

View File

@@ -1,6 +1,11 @@
- **Etcher version:**
- **Operating system and architecture:**
- **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?**
<!-- 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 -->

169
.github/actions/publish/action.yml vendored Normal file
View File

@@ -0,0 +1,169 @@
---
name: package and publish GitHub (draft) release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
# --- custom environment
XCODE_APP_LOADER_EMAIL:
type: string
default: "accounts+apple@balena.io"
NODE_VERSION:
type: string
default: "18.x"
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }}
- name: Extract custom source artifact
if: runner.os != 'Windows'
shell: pwsh
working-directory: .
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Extract custom source artifact
if: runner.os == 'Windows'
shell: pwsh
working-directory: .
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -xf ${{ runner.temp }}\custom.tgz
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Install yq
shell: bash --noprofile --norc -eo pipefail -x {0}
run: choco install yq
if: runner.os == 'Windows'
# https://www.electron.build/code-signing.html
# https://github.com/Apple-Actions/import-codesign-certs
- name: Import Apple code signing certificate
if: runner.os == 'macOS'
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
- name: Import Windows code signing certificate
if: runner.os == 'Windows'
shell: powershell
run: |
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
Remove-Item -path ${{ runner.temp }} -include certificate.base64
Import-PfxCertificate `
-FilePath ${{ runner.temp }}/certificate.pfx `
-CertStoreLocation Cert:\CurrentUser\My `
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
Remove-Item -path ${{ runner.temp }} -include certificate.pfx
env:
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
# ... or refactor (e.g.) https://github.com/samuelmeuli/action-electron-builder
# https://github.com/product-os/scripts/tree/master/electron
# https://github.com/product-os/scripts/tree/master/shared
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
- name: Package release
id: package_release
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
ELECTRON_BUILDER_ARCHITECTURE="${runner_arch}"
APPLICATION_VERSION="$(jq -r '.version' package.json)"
ARCHITECTURE_FLAGS="--${ELECTRON_BUILDER_ARCHITECTURE}"
if [[ $runner_os =~ linux ]]; then
ELECTRON_BUILDER_OS='--linux'
TARGETS="$(yq e .linux.target[] electron-builder.yml)"
elif [[ $runner_os =~ darwin|macos|osx ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
ELECTRON_BUILDER_OS='--mac'
TARGETS="$(yq e .mac.target[] electron-builder.yml)"
elif [[ $runner_os =~ windows|win ]]; then
ARCHITECTURE_FLAGS="--ia32 ${ARCHITECTURE_FLAGS}"
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
CSC_LINK=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
ELECTRON_BUILDER_OS='--win'
TARGETS="$(yq e .win.target[] electron-builder.yml)"
else
exit 1
fi
npm link electron-builder
for target in ${TARGETS}; do
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
--c.extraMetadata.analytics.sentry.token='https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632' \
--c.extraMetadata.analytics.amplitude.token='balena-etcher' \
--c.extraMetadata.packageType="${target}"
find dist -type f -maxdepth 1
done
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
env:
# Apple notarization (afterSignHook.js)
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
CSC_FOR_PULL_REQUEST: true
# https://www.electron.build/auto-update.html#staged-rollouts
- name: Configure staged rollout(s)
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
percentage="$(cat < repo.yml | yq e .triggerNotification.stagingPercentage)"
find dist -type f -maxdepth 1 \
-name "latest*.yml" \
-exec yq -i e .version=\"${{ steps.package_release.outputs.version }}\" {} \;
find dist -type f -maxdepth 1 \
-name "latest*.yml" \
-exec yq -i e .stagingPercentage=\"$percentage\" {} \;
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
path: dist
retention-days: 1

64
.github/actions/test/action.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
---
name: test release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
# --- custom environment
NODE_VERSION:
type: string
default: "16.x"
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Test release
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
npm run flowzone-preinstall-${runner_os}
npm ci
npm run build
npm run test-${runner_os}
env:
# https://www.electronjs.org/docs/latest/api/environment-variables
ELECTRON_NO_ATTACH_CONSOLE: true
- name: Compress custom source
if: runner.os != 'Windows'
shell: pwsh
run: tar -acf ${{ runner.temp }}/custom.tgz .
- name: Compress custom source
if: runner.os == 'Windows'
shell: pwsh
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -acf ${{ runner.temp }}\custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

24
.github/workflows/flowzone.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
branches: [main, master]
# allow external contributions to use secrets within trusted code
pull_request_target:
types: [opened, synchronize, closed]
branches: [main, master]
jobs:
flowzone:
name: Flowzone
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
# prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for
# internal or external contributions respectively
if: |
(github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') ||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
secrets: inherit
with:
tests_run_on: '["ubuntu-20.04","macos-latest","windows-2019"]'
restrict_custom_actions: false
github_prerelease: true
cloudflare_website: "etcher"

13
.github/workflows/winget.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Publish to WinGet
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest # action can only be run on windows
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
with:
identifier: Balena.Etcher
installers-regex: 'balenaEtcher-Setup.*.exe$'
token: ${{ secrets.WINGET_PAT }}

7
.gitignore vendored
View File

@@ -28,6 +28,7 @@ pids
# Generated files
/generated
/binaries
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
@@ -51,3 +52,9 @@ node_modules
# VSCode files
.vscode
.gitsecret/keys/random_seed
!*.secret
secrets/APPLE_SIGNING_PASSWORD.txt
secrets/WINDOWS_SIGNING_PASSWORD.txt
secrets/XCODE_APP_LOADER_PASSWORD.txt
secrets/WINDOWS_SIGNING.pfx

BIN
.gitsecret/keys/pubring.kbx Normal file

Binary file not shown.

Binary file not shown.

BIN
.gitsecret/keys/trustdb.gpg Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d
secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568
secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1
secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714
secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
16

View File

@@ -1,74 +0,0 @@
{
"electron": {
"npm_version": "6.14.5",
"dependencies": {
"linux": [
"libudev-dev",
"libusb-1.0-0-dev",
"libyaml-dev",
"libgtk-3-0",
"libatk-bridge2.0-0",
"libdbus-1-3",
"libgbm1",
"libc6"
]
},
"builder": {
"appId": "io.balena.etcher",
"copyright": "Copyright 2016-2020 Balena Ltd",
"productName": "balenaEtcher",
"nodeGypRebuild": false,
"afterPack": "./afterPack.js",
"asar": false,
"files": [
"generated",
"lib/shared/catalina-sudo/sudo-askpass.osascript.js"
],
"beforeBuild": "./beforeBuild.js",
"afterSign": "./afterSignHook.js",
"mac": {
"category": "public.app-category.developer-tools",
"hardenedRuntime": true,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
},
"dmg": {
"iconSize": 110,
"contents": [
{
"x": 140,
"y": 245
},
{
"x": 415,
"y": 245,
"type": "link",
"path": "/Applications"
}
],
"window": {
"width": 544,
"height": 407
}
},
"linux": {
"category": "Utility",
"packageCategory": "utils",
"synopsis": "balenaEtcher 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 you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more."
},
"deb": {
"compression": "bzip2",
"priority": "optional",
"depends": [
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
]
},
"protocols": {
"name": "etcher",
"schemes": [
"etcher"
]
}
}
}
}

13351
.versionbot/CHANGELOG.yml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

127
README.md
View File

@@ -5,16 +5,15 @@
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
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.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
[![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher)
[![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE)
[![Dependency status](https://img.shields.io/david/balena-io/etcher.svg?style=flat-square)](https://david-dm.org/balena-io/etcher)
[![Balena.io Forums](https://img.shields.io/discourse/https/forums.balena.io/topics.svg?style=flat-square&label=balena.io%20forums)](https://forums.balena.io/c/etcher)
***
---
[**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
@@ -22,7 +21,7 @@ was written correctly and much more. It can also flash directly Raspberry Pi dev
- macOS 10.10 (Yosemite) 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
[documentation][electron-supported-platforms].
@@ -31,100 +30,40 @@ Note that Etcher will run on any platform officially supported by
Refer to the [downloads page][etcher] for the latest pre-made
installers for all supported operating systems.
## Packages
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
1. Add Etcher debian repository:
Package for Debian and Ubuntu can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
```sh
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
```
##### Install .deb file using apt
2. Trust Bintray.com's GPG key:
```sh
sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys 379CE192D401AB61
```
3. Update and install:
```sh
sudo apt-get update
sudo apt-get install balena-etcher-electron
```
```sh
sudo apt install ./balena-etcher_******_amd64.deb
```
##### Uninstall
```sh
sudo apt-get remove balena-etcher-electron
sudo rm /etc/apt/sources.list.d/balena-etcher.list
sudo apt-get update
```
```sh
sudo apt remove balena-etcher
```
##### OpenSUSE LEAP & Tumbleweed install
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
##### Yum
Package for Fedora-based and Redhat can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
1. Install using yum
```sh
sudo zypper ar https://balena.io/etcher/static/etcher-rpm.repo
sudo zypper ref
sudo zypper in balena-etcher-electron
sudo yum localinstall balena-etcher-***.x86_64.rpm
```
##### Uninstall
```sh
sudo zypper rm balena-etcher-electron
```
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
1. Add Etcher rpm repository:
```sh
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
```
2. Update and install:
```sh
sudo yum install -y balena-etcher-electron
```
or
```sh
sudo dnf install -y balena-etcher-electron
```
##### Uninstall
```sh
sudo yum remove -y balena-etcher-electron
sudo rm /etc/yum.repos.d/etcher-rpm.repo
sudo yum clean all
sudo yum makecache fast
```
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)
```sh
sudo eopkg it etcher
```
##### Uninstall
```sh
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:
```sh
yay -S balena-etcher
```
@@ -135,20 +74,18 @@ yay -S balena-etcher
yay -R balena-etcher
```
#### Brew Cask (macOS)
#### WinGet (Windows)
Note that the Etcher Cask has to be updated manually to point to new versions,
so it might not refer to the latest version immediately after an Etcher
release.
This package is updated by [gh-action](https://github.com/vedantmgoyal2009/winget-releaser), and is kept up to date automatically.
```sh
brew cask install balenaetcher
winget install balenaEtcher #or Balena.Etcher
```
##### Uninstall
```sh
brew cask uninstall balenaetcher
winget uninstall balenaEtcher
```
#### Chocolatey (Windows)
@@ -168,20 +105,20 @@ choco uninstall etcher
## 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.
## 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].
[etcher]: https://balena.io/etcher
[electron]: https://electronjs.org/
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.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
[support]: https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.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
[milestones]: https://github.com/balena-io/etcher/milestones
[newissue]: https://github.com/balena-io/etcher/issues/new
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE

11
after-install.tpl Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# Link to the binary
# Must hardcode balenaEtcher directory; no variable available
ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}'
# SUID chrome-sandbox for Electron 5+
chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true
update-mime-database /usr/share/mime || true
update-desktop-database /usr/share/applications || true

View File

@@ -10,13 +10,15 @@ async function main(context) {
}
const appName = context.packager.appInfo.productFilename
const appleId = 'accounts+apple@balena.io'
const appleId = process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io'
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD
// https://github.com/electron/notarize/blob/main/README.md
await notarize({
appBundleId: 'io.balena.etcher',
appPath: `${appOutDir}/${appName}.app`,
appleId,
appleIdPassword: `@keychain:Application Loader: ${appleId}`
appleIdPassword
})
}

Binary file not shown.

View File

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

View File

@@ -12,67 +12,29 @@ over the commit history.
- Be able to automatically reference relevant changes from a dependency
upgrade.
The guidelines are inspired by the [AngularJS git commit
guidelines][angular-commit-guidelines].
Commit structure
----------------
Each commit message consists of a header, a body and a footer. The header has a
special format that includes a type, a scope and a subject.
Each commit message needs to specify the semver-type. Which can be `patch|minor|major`.
See the [Semantic Versioning][semver] specification for a more detailed explanation of the meaning of these types.
See balena commit guidelines for more info about the whole commit structure.
```
<type>(<scope>): <subject>
<semver-type>: <subject>
```
or
```
<subject>
<BLANK LINE>
<body>
<details>
<BLANK LINE>
<footer>
Change-Type: <semver-type>
```
The subject should not contain more than 70 characters, including the type and
scope, and the body should be wrapped at 72 characters.
Type
----
Must be one of the following:
- `feat`: A new feature.
- `fix`: A bug fix.
- `minifix`: A minimal fix that doesn't warrant an entry in the CHANGELOG.
- `docs`: Documentation only changes.
- `style`: Changes that do not affect the meaning of the code (white-space,
formatting, missing semi-colons, JSDoc annotations, comments, etc).
- `refactor`: A code change that neither fixes a bug nor adds a feature.
- `perf`: A code change that improves performance.
- `test`: Adding missing tests.
- `chore`: Changes to the build process or auxiliary tools and libraries.
- `upgrade`: A version upgrade of a project dependency.
Scope
-----
The scope is required for types that make sense, such as `feat`, `fix`,
`test`, etc. Certain commit types, such as `chore` might not have a clearly
defined scope, in which case its better to omit it.
Subject
-------
The subject should contain a short description of the change:
- Use the imperative, present tense.
- Don't capitalize the first letter.
- No dot (.) at the end.
Footer
------
The footer contains extra information about the commit, such as tags.
**Breaking Changes** should start with the word BREAKING CHANGE: with a space
or two newlines. The rest of the commit message is then used for this.
Tags
----
@@ -121,125 +83,4 @@ Closes: https://github.com/balena-io/etcher/issues/XXX
Fixes: https://github.com/balena-io/etcher/issues/XXX
```
### `Change-Type: <type>`
This tag is used to determine the change type that a commit introduces. The
following types are supported:
- `major`
- `minor`
- `patch`
This tag can be omitted for commits that don't change the application from the
user's point of view, such as for refactoring commits.
Examples:
```
Change-Type: major
Change-Type: minor
Change-Type: patch
```
See the [Semantic Versioning][semver] specification for a more detailed
explanation of the meaning of these types.
### `Changelog-Entry: <message>`
This tag is used to describe the changes introduced by the commit in a more
human style that would fit the `CHANGELOG.md` better.
If the commit type is either `fix` or `feat`, the commit will take part in the
CHANGELOG. If this tag is not defined, then the commit subject will be used
instead.
You explicitly can use this tag to make a commit whose type is not `fix` nor
`feat` appear in the `CHANGELOG.md`.
Since whatever your write here will be shown *as it is* in the `CHANGELOG.md`,
take some time to write a decent entry. Consider the following guidelines:
- Use the imperative, present tense.
- Capitalize the first letter.
There is no fixed length limit for the contents of this tag, but always strive
to make as short as possible without compromising its quality.
Examples:
```
Changelog-Entry: Fix EPERM errors when flashing to a GPT drive.
```
Complete examples
-----------------
```
fix(GUI): ignore extensions before the first non-compressed extension
Currently, we extract all the extensions from an image path and report back
that the image is invalid if *any* of the extensions is not valid , however
this can cause trouble with images including information between dots that are
not strictly extensions, for example:
elementaryos-0.3.2-stable-i386.20151209.iso
Etcher will consider `20151209` to be an invalid extension and therefore
will prevent such image from being selected at all.
As a way to allow these corner cases but still make use of our enforced check
controls, the validation routine now only consider extensions starting from the
first non compressed extension.
Change-Type: patch
Changelog-Entry: Don't interpret image file name information between dots as image extensions.
Fixes: https://github.com/balena-io/etcher/issues/492
```
***
```
upgrade: etcher-image-write to v5.0.2
This version contains a fix to an `EPERM` issue happening to some Windows user,
triggered by the `write` system call during the first ~5% of a flash given that
the operating system still thinks the drive has a file system.
Change-Type: patch
Changelog-Entry: Upgrade `etcher-image-write` to v5.0.2.
Link: https://github.com/balena-io-modules/etcher-image-write/blob/master/CHANGELOG.md#502---2016-06-27
Fixes: https://github.com/balena-io/etcher/issues/531
```
***
```
feat(GUI): implement update notifier functionality
Auto-update functionality is not ready for usage. As a workaround to
prevent users staying with older versions, we now check for updates at
startup, and if the user is not running the latest version, we present a
modal informing the user of the availiblity of a new version, and
provide a call to action to open the Etcher website in his web browser.
Extra features:
- The user can skip the update, and tell the program to delay the
notification for 7 days.
Misc changes:
- Center modal with flexbox, to allow more flexibility on the modal height.
interacting with the S3 server.
- Implement `ManifestBindService`, which now serves as a backend for the
`manifest-bind` directive to allow the directive's functionality to be
re-used by other services.
- Namespace checkbox styles that are specific to the settings page.
Change-Type: minor
Changelog-Entry: Check for updates and show a modal prompting the user to download the latest version.
Closes: https://github.com/balena-io/etcher/issues/396
```
[angular-commit-guidelines]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit
[semver]: http://semver.org

View File

@@ -17,11 +17,11 @@ Developing
#### Common
- [NodeJS](https://nodejs.org) (at least v6.11)
- [Python 2.7](https://www.python.org)
- [NodeJS](https://nodejs.org) (at least v16.11)
- [Python 3](https://www.python.org)
- [jq](https://stedolan.github.io/jq/)
- [curl](https://curl.haxx.se/)
- [npm](https://www.npmjs.com/) (version 6.7)
- [npm](https://www.npmjs.com/)
```sh
pip install -r requirements.txt
@@ -33,16 +33,16 @@ You might need to run this with `sudo` or administrator permissions.
- [NSIS v2.51](http://nsis.sourceforge.net/Main_Page) (v3.x won't work)
- Either one of the following:
- [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools) containing standalone compilers, libraries and scripts
- Install the [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) via npm with `npm install --global windows-build-tools`
- [Visual Studio Community 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48146) (free) (other editions, like Professional and Enterprise, should work too)
**NOTE:** Visual Studio 2015 doesn't install C++ by default. You have to rerun the
- [Visual C++ 2019 Build Tools](https://visualstudio.microsoft.com/vs/features/cplusplus/) containing standalone compilers, libraries and scripts
- The [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools#windows-build-tools) should be installed along with NodeJS
- [Visual Studio Community 2019](https://visualstudio.microsoft.com/vs/) (free) (other editions, like Professional and Enterprise, should work too)
**NOTE:** Visual Studio doesn't install C++ by default. You have to rerun the
setup, select "Modify" and then check `Visual C++ -> Common Tools for Visual
C++ 2015` (see http://stackoverflow.com/a/31955339)
C++` (see http://stackoverflow.com/a/31955339)
- [MinGW](http://www.mingw.org)
You might need to `npm config set msvs_version 2015` for node-gyp to correctly detect
the version of Visual Studio you're using (in this example VS2015).
You might need to `npm config set msvs_version 2019` for node-gyp to correctly detect
the version of Visual Studio you're using (in this example VS2019).
The following MinGW packages are required:
@@ -61,7 +61,7 @@ as well.
#### Linux
- `libudev-dev` for libusb (install with `sudo apt install libudev-dev` for example)
- `libudev-dev` for libusb (for example install with `sudo apt install libudev-dev`, or on fedora `systemd-devel` contains the required package)
### Cloning the project
@@ -70,28 +70,13 @@ git clone --recursive https://github.com/balena-io/etcher
cd etcher
```
### Installing npm dependencies
**NOTE:** Please make use of the following command to install npm dependencies rather
than simply running `npm install` given that we need to do extra configuration
to make sure native dependencies are correctly compiled for Electron, otherwise
the application might not run successfully.
If you're on Windows, **run the command from the _Developer Command Prompt for
VS2015_**, to ensure all Visual Studio command utilities are available in the
`%PATH%`.
```sh
make electron-develop
```
### Running the application
#### GUI
```sh
# Build the GUI
make webpack
npm run webpack #or npm run build
# Start Electron
npm start
```
@@ -119,10 +104,6 @@ systems as they can before sending a pull request.
*The test suite is run automatically by CI servers when you send a pull
request.*
We also rely on various `make` targets to perform some common tasks:
- `make lint`: Run the linter.
- `make sass`: Compile SCSS files.
We make use of [EditorConfig] to communicate indentation, line endings and
other text editing default. We encourage you to install the relevant plugin in
@@ -132,20 +113,7 @@ process.
Updating a dependency
---------------------
Given we use [npm shrinkwrap][shrinkwrap], we have to take extra steps to make
sure the `npm-shrinkwrap.json` file gets updated correctly when we update a
dependency.
Use the following steps to ensure everything goes flawlessly:
- Run `make electron-develop` to ensure you don't have extraneous dependencies
you might have brought during development, or you are running older
dependencies because you come from another branch or reference.
- Install the new version of the dependency. For example: `npm install --save
<package>@<version>`. This will update the `npm-shrinkwrap.json` file.
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
- Commit *both* `package.json` and `package-lock.json`.
Diffing Binaries
----------------

View File

@@ -37,10 +37,16 @@ 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.
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.
## 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).
## Can I use the Flash With Etcher button on my site?
You can use the Flash with Etcher button on your site or blog, if you have an OS that you want your users to be able to easily flash using Etcher, add the following code where you want to button to be:
`<a href="https://efp.balena.io/open-image-url?imageUrl=<your image URL>"><img src="http://balena.io/flash-with-etcher.png" /></a>`

View File

@@ -8,10 +8,14 @@ Releasing
### Release Types
- **snapshot** (default): A continues snapshot of current master, made by the CI services
- **production**: Full releases
- **draft**: A continues snapshot of current master, made by the CI services
- **pre-release** (default): A continues snapshot of current master, made by the CI services
- **release**: Full releases
Draft release is created from each PR, tagged with the branch name.
All merged PR will generate a new tag/version as a *pre-release*.
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
### Flight Plan
#### Preparation
@@ -31,11 +35,10 @@ Releasing
- [Post release note to forums](https://forums.balena.io/c/etcher)
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
- [Update the website](https://github.com/balena-io/etcher-homepage)
- Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions
- If regressions arise; pull the release, and release a patched version, else:
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
- [Upload build artifacts to Amazon S3](#uploading-binaries-to-amazon-s3)
- Post changelog with `#release-notes` tag on Flowdock
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
- Post changelog with `#release-notes` tag on internal chat
- If this release packs noteworthy major changes:
- Write a blog post about it, and / or
- Write about it to the Etcher mailing list
@@ -48,7 +51,7 @@ Make sure to set the analytics tokens when generating production release binarie
```bash
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
```
#### Linux
@@ -57,46 +60,16 @@ export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
**NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
```bash
./scripts/build/docker/run-command.sh -r x64 -s . -c "make distclean"
```
##### Generating artifacts
```bash
# x64
# Build Debian packages
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
# Build RPM packages
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
# Build AppImages
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
# x86
# Build Debian packages
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
# Build RPM packages
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
# Build AppImages
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
```
The artifacts are generated by the CI and published as draft-release or pre-release.
`electron-builder` is used to create the packaged application.
#### Mac OS
**ATTENTION:** For production releases you'll need the code-signing key,
and set `CSC_NAME` to generate signed binaries on Mac OS.
```bash
make electron-develop
# Build the zip
make RELEASE_TYPE=production electron-installer-app-zip
# Build the dmg
make RELEASE_TYPE=production electron-installer-dmg
```
#### Windows
**ATTENTION:** For production releases you'll need the code-signing key,
@@ -105,38 +78,10 @@ and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Window
**NOTE:**
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
```bash
make electron-develop
# Build the Portable version
make RELEASE_TYPE=production electron-installer-portable
# Build the Installer
make RELEASE_TYPE=production electron-installer-nsis
```
### Uploading packages to Cloudfront
### Uploading packages to Bintray
```bash
export BINTRAY_USER="username@account"
export BINTRAY_API_KEY="youruserapikey"
```
```bash
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "debian" -y "debian" -r "x64" -f "dist/etcher-electron_1.2.1_amd64.deb"
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "debian" -y "debian" -r "x86" -f "dist/etcher-electron_1.2.1_i386.deb"
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "redhat" -y "redhat" -r "x64" -f "dist/etcher-electron-1.2.1.x86_64.rpm"
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "redhat" -y "redhat" -r "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
```
### Uploading binaries to Amazon S3
```bash
export S3_KEY="..."
```
```bash
./scripts/publish/aws-s3.sh -b "balena-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
```
Log in to cloudfront and upload the `rpm` and `deb` files.
### Dealing with a Problematic Release

View File

@@ -112,4 +112,4 @@ Analytics
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
check that no request is sent
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
F5), and check that initial events are not sent to Mixpanel**
F5), and check that initial events are not sent to Amplitude**

View File

@@ -7,44 +7,9 @@ systems.
Release Types
-------------
Etcher supports **production** and **snapshot** release types. Each is
published to a different S3 bucket, and production release types are code
signed, while snapshot release types aren't and include a short git commit-hash
as a build number. For example, `1.0.0-beta.19` is a production release type,
while `1.0.0-beta.19+531ab82` is a snapshot release type.
In terms of comparison: `1.0.0-beta.19` (production) < `1.0.0-beta.19+531ab82`
(snapshot) < `1.0.0-rc.1` (production) < `1.0.0-rc.1+7fde24a` (snapshot) <
`1.0.0` (production) < `1.0.0+2201e5f` (snapshot). Keep in mind that if you're
running a production release type, you'll only be prompted to update to
production release types, and if you're running a snapshot release type, you'll
only be prompted to update to other snapshot release types.
The build system creates (and publishes) snapshot release types by default, but
you can build a specific release type by setting the `RELEASE_TYPE` make
variable. For example:
```sh
make <target> RELEASE_TYPE=snapshot
make <target> RELEASE_TYPE=production
```
We can control the version range a specific Etcher version will consider when
showing the update notification dialog by tweaking the `updates.semverRange`
property of `package.json`.
Update Channels
---------------
Etcher has a setting to include the unstable update channel. If this option is
set, Etcher will consider both stable and unstable versions when showing the
update notifier dialog. Unstable versions are the ones that contain a `beta`
pre-release tag. For example:
- Production unstable version: `1.4.0-beta.1`
- Snapshot unstable version: `1.4.0-beta.1+7fde24a`
- Production stable version: `1.4.0`
- Snapshot stable version: `1.4.0+7fde24a`
Etcher supports **pre-release** and **final** release types as does Github. Each is
published to Github releases.
The release version is generated automatically from the commit messasges.
Signing
-------
@@ -73,63 +38,19 @@ Packaging
The resulting installers will be saved to `dist/out`.
Run the following commands:
### OS X
Run the following commands on all platforms with the right arguments:
```sh
make electron-installer-dmg
make electron-installer-app-zip
./node_modules/electron-builder build <...>
```
### GNU/Linux
```sh
make electron-installer-appimage
make electron-installer-debian
```
### Windows
```sh
make electron-installer-zip
make electron-installer-nsis
```
Publishing to Bintray
Publishing to Cloudfront
---------------------
We publish GNU/Linux Debian packages to [Bintray][bintray].
We publish GNU/Linux Debian packages to [Cloudfront][cloudfront].
Make sure you set the following environment variables:
- `BINTRAY_USER`
- `BINTRAY_API_KEY`
Run the following command:
```sh
make publish-bintray-debian
```
Publishing to S3
----------------
- [AWS CLI][aws-cli]
Make sure you have the [AWS CLI tool][aws-cli] installed and configured to
access balena.io's production or snapshot S3 bucket.
Run the following command to publish all files for the current combination of
_platform_ and _arch_ (building them if necessary):
```sh
make publish-aws-s3
```
Also add links to each AWS S3 file in [GitHub Releases][github-releases]. See
[`v1.0.0-beta.17`](https://github.com/balena-io/etcher/releases/tag/v1.0.0-beta.17)
as an example.
Log in to cloudfront and upload the `rpm` and `deb` files.
Publishing to Homebrew Cask
---------------------------
@@ -147,8 +68,12 @@ Post messages to the [Etcher forum][balena-forum-etcher] announcing the new vers
of Etcher, and including the relevant section of the Changelog.
[aws-cli]: https://aws.amazon.com/cli
[bintray]: https://bintray.com
[cloudfront]: https://cloudfront.com
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb
[homebrew-cask]: https://github.com/caskroom/homebrew-cask
[balena-forum-etcher]: https://forums.balena.io/c/etcher
[github-releases]: https://github.com/balena-io/etcher/releases
Updating EFP / Success-Banner
-----------------------------
Etcher Featured Project is automatically run based on an algorithm which promoted projects from the balena marketplace which have been contributed by the community, the algorithm prioritises projects which give uses the best experience. Editing both EFP and the Etcher Success-Banner can only be done by someone from balena, instruction are on the [Etcher-EFP repo (private)](https://github.com/balena-io/etcher-efp)

View File

@@ -1,9 +1,16 @@
Getting help with Etcher
========================
Getting help with BalenaEtcher
===============================
There are various ways to get support for Etcher if you experience an issue or
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
------
@@ -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
support:
- The Etcher version you're running.
- The BalenaEtcher version you're running.
- The operating system you're running Etcher in.
@@ -25,10 +32,12 @@ support:
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
one][new-issue].
[discourse]: https://forums.balena.io/c/etcher
[issues]: https://github.com/balena-io/etcher/issues
[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

View File

@@ -3,6 +3,11 @@ Etcher User Documentation
This document contains how-tos and FAQs oriented to Etcher users.
Config
------
Etcher's configuration is saved to the `config.json` file in the apps folder.
Not all the options are surfaced to the UI. You may edit this file to tweak settings even before launching the app.
Why is my drive not bootable?
-----------------------------
@@ -159,6 +164,18 @@ pre-installed in all modern Windows versions.
- Run `clean`. This command will completely clean your drive by erasing any
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
@@ -206,3 +223,5 @@ macOS 10.10 (Yosemite) and newer versions][electron-supported-platforms].
[unetbootin]: https://unetbootin.github.io
[windows-iot-dashboard]: https://developer.microsoft.com/en-us/windows/iot/downloads
[woeusb]: https://github.com/slacka/WoeUSB
See [PUBLISHING](/docs/PUBLISHING.md) for more details about release types.

View File

@@ -1,21 +1,23 @@
# https://www.electron.build/configuration/configuration
appId: io.balena.etcher
copyright: Copyright 2016-2020 Balena Ltd
copyright: Copyright 2016-2023 Balena Ltd
productName: balenaEtcher
npmRebuild: true
nodeGypRebuild: false
publish: null
beforeBuild: "./beforeBuild.js"
afterPack: "./afterPack.js"
afterPack: ./afterPack.js
afterSign: ./afterSignHook.js
asar: false
files:
- generated
- lib/shared/catalina-sudo/sudo-askpass.osascript.js
- lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
- lib/shared/catalina-sudo/sudo-askpass.osascript-en.js
mac:
icon: assets/icon.icns
category: public.app-category.developer-tools
hardenedRuntime: true
entitlements: "entitlements.mac.plist"
entitlementsInherit: "entitlements.mac.plist"
artifactName: "${productName}-${version}.${ext}"
target:
- dmg
dmg:
background: assets/dmg/background.tiff
icon: assets/icon.icns
@@ -32,6 +34,10 @@ dmg:
height: 405
win:
icon: assets/icon.ico
target:
- zip
- nsis
- portable
nsis:
oneClick: true
runAfterFinish: true
@@ -44,17 +50,23 @@ portable:
artifactName: "${productName}-Portable-${version}.${ext}"
requestExecutionLevel: user
linux:
icon: assets/iconset
target:
- AppImage
- rpm
- deb
category: Utility
packageCategory: utils
executableName: balena-etcher-electron
executableName: balena-etcher
synopsis: balenaEtcher 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 you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.
icon: assets/iconset
appImage:
artifactName: ${productName}-${version}-${env.ELECTRON_BUILDER_ARCHITECTURE}.${ext}
deb:
priority: optional
compression: bzip2
depends:
- gconf2
- gconf-service
- libappindicator1
- gconf2
- libasound2
- libatk1.0-0
- libc6
@@ -88,6 +100,7 @@ deb:
- libxss1
- libxtst6
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
afterInstall: "./after-install.tpl"
rpm:
depends:
- util-linux

View File

@@ -14,5 +14,11 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
</dict>
</plist>

View File

@@ -15,33 +15,31 @@
*/
import * as electron from 'electron';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import * as remote from '@electron/remote';
import { debounce, capitalize, Dictionary, values } from 'lodash';
import outdent from 'outdent';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json';
import {
DrivelistDrive,
isDriveValid,
isSourceDrive,
} from '../../shared/drive-constraints';
import { DrivelistDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state';
import { init as ledsInit } from './models/leds';
import { deselectImage, getImage, selectDrive } from './models/selection-state';
import * as settings from './models/settings';
import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics';
import { scanner as driveScanner } from './modules/drive-scanner';
import { startApiAndSpawnChild } from './modules/api';
import * as exceptionReporter from './modules/exception-reporter';
import * as osDialog from './os/dialog';
import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage';
import './css/main.css';
import * as i18next from 'i18next';
import { promises } from 'dns';
import { SourceMetadata } from '../../shared/typings/source-selector';
window.addEventListener(
'unhandledrejection',
@@ -91,7 +89,7 @@ analytics.logEvent('Application start', {
version: currentVersion,
});
const debouncedLog = _.debounce(console.log, 1000, { maxWait: 1000 });
const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 });
function pluralize(word: string, quantity: number) {
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
@@ -117,7 +115,7 @@ observe(() => {
// might cause some non-sense flashing state logs including
// `undefined` values.
debouncedLog(outdent({ newline: ' ' })`
${_.capitalize(currentFlashState.type)}
${capitalize(currentFlashState.type)}
${active},
${currentFlashState.percentage}%
at
@@ -130,184 +128,44 @@ observe(() => {
`);
});
/**
* @summary The radix used by USB ID numbers
*/
const USB_ID_RADIX = 16;
/**
* @summary The expected length of a USB ID number
*/
const USB_ID_LENGTH = 4;
/**
* @summary Convert a USB id (e.g. product/vendor) to a string
*
* @example
* console.log(usbIdToString(2652))
* > '0x0a5c'
*/
function usbIdToString(id: number): string {
return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`;
}
/**
* @summary Product ID of BCM2708
*/
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763;
/**
* @summary Product ID of BCM2710
*/
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764;
/**
* @summary Compute module descriptions
*/
const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary<string> = {
[USB_PRODUCT_ID_BCM2708_BOOT]: 'Compute Module 1',
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3',
};
async function driveIsAllowed(drive: {
devicePath: string;
device: string;
raw: string;
}) {
const driveBlacklist = (await settings.get('driveBlacklist')) || [];
return !(
driveBlacklist.includes(drive.devicePath) ||
driveBlacklist.includes(drive.device) ||
driveBlacklist.includes(drive.raw)
);
}
type Drive =
| sdk.sourceDestination.BlockDevice
| sdk.sourceDestination.UsbbootDrive
| sdk.sourceDestination.DriverlessDevice;
function prepareDrive(drive: Drive) {
if (drive instanceof sdk.sourceDestination.BlockDevice) {
// @ts-ignore (BlockDevice.drive is private)
return drive.drive;
} else if (drive instanceof sdk.sourceDestination.UsbbootDrive) {
// This is a workaround etcher expecting a device string and a size
// @ts-ignore
drive.device = drive.usbDevice.portId;
drive.size = null;
// @ts-ignore
drive.progress = 0;
drive.disabled = true;
drive.on('progress', (progress) => {
updateDriveProgress(drive, progress);
});
return drive;
} else if (drive instanceof sdk.sourceDestination.DriverlessDevice) {
const description =
COMPUTE_MODULE_DESCRIPTIONS[
drive.deviceDescriptor.idProduct.toString()
] || 'Compute Module';
return {
device: `${usbIdToString(
drive.deviceDescriptor.idVendor,
)}:${usbIdToString(drive.deviceDescriptor.idProduct)}`,
displayName: 'Missing drivers',
description,
mountpoints: [],
isReadOnly: false,
isSystem: false,
disabled: true,
icon: 'warning',
size: null,
link:
'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
linkCTA: 'Install',
linkTitle: 'Install missing drivers',
linkMessage: outdent`
Would you like to download the necessary drivers from the Raspberry Pi Foundation?
This will open your browser.
Once opened, download and run the installer from the "Windows Installer" section to install the drivers
`,
};
function setDrives(drives: Dictionary<DrivelistDrive>) {
// prevent setting drives while flashing otherwise we might lose some while we unmount them
if (!flashState.isFlashing()) {
availableDrives.setDrives(values(drives));
}
}
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
availableDrives.setDrives(_.values(drives));
}
// Spwaning the child process without privileges to get the drives list
// TODO: clean up this mess of exports
export let requestMetadata: any;
function getDrives() {
return _.keyBy(availableDrives.getDrives(), 'device');
}
// start the api and spawn the child process
startApiAndSpawnChild({
withPrivileges: false,
}).then(({ emit, registerHandler }) => {
// start scanning
emit('scan');
async function addDrive(drive: Drive) {
const preparedDrive = prepareDrive(drive);
if (!(await driveIsAllowed(preparedDrive))) {
return;
}
const drives = getDrives();
drives[preparedDrive.device] = preparedDrive;
setDrives(drives);
if (
(await settings.get('autoSelectAllDrives')) &&
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isDriveValid(drive.drive, getImage())
) {
selectDrive(drive.device);
}
}
// make the sourceMetada awaitable to be used on source selection
requestMetadata = async (params: any): Promise<SourceMetadata> => {
emit('sourceMetadata', JSON.stringify(params));
function removeDrive(drive: Drive) {
if (
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isSourceDrive(drive.drive, getImage())
) {
// Deselect the image if it was on the drive that was removed.
// This will also deselect the image if the drive mountpoints change.
deselectImage();
}
const preparedDrive = prepareDrive(drive);
const drives = getDrives();
delete drives[preparedDrive.device];
setDrives(drives);
}
return new Promise((resolve) =>
registerHandler('sourceMetadata', (data: any) => {
resolve(JSON.parse(data));
}),
);
};
function updateDriveProgress(
drive: sdk.sourceDestination.UsbbootDrive,
progress: number,
) {
const drives = getDrives();
// @ts-ignore
const driveInMap = drives[drive.device];
if (driveInMap) {
// @ts-ignore
drives[drive.device] = { ...driveInMap, progress };
setDrives(drives);
}
}
driveScanner.on('attach', addDrive);
driveScanner.on('detach', removeDrive);
driveScanner.on('error', (error) => {
// Stop the drive scanning loop in case of errors,
// otherwise we risk presenting the same error over
// and over again to the user, while also heavily
// spamming our error reporting service.
driveScanner.stop();
return exceptionReporter.report(error);
registerHandler('drives', (data: any) => {
setDrives(JSON.parse(data));
});
});
driveScanner.start();
let popupExists = false;
analytics.initAnalytics();
window.addEventListener('beforeunload', async (event) => {
if (!flashState.isFlashing() || popupExists) {
analytics.logEvent('Close application', {
@@ -326,9 +184,9 @@ window.addEventListener('beforeunload', async (event) => {
try {
const confirmed = await osDialog.showWarning({
confirmationLabel: 'Yes, quit',
rejectionLabel: 'Cancel',
title: 'Are you sure you want to close Etcher?',
confirmationLabel: i18next.t('yesExit'),
rejectionLabel: i18next.t('cancel'),
title: i18next.t('reallyExit'),
description: messages.warning.exitWhileFlashing(),
});
if (confirmed) {
@@ -337,8 +195,8 @@ window.addEventListener('beforeunload', async (event) => {
});
// This circumvents the 'beforeunload' event unlike
// electron.remote.app.quit() which does not.
electron.remote.process.exit(EXIT_CODES.SUCCESS);
// remote.app.quit() which does not.
remote.process.exit(EXIT_CODES.SUCCESS);
}
analytics.logEvent('Close rejected while flashing', {
@@ -346,13 +204,19 @@ window.addEventListener('beforeunload', async (event) => {
flashingWorkflowUuid,
});
popupExists = false;
} catch (error) {
} catch (error: any) {
exceptionReporter.report(error);
}
});
async function main() {
await ledsInit();
export async function main() {
try {
const { init: ledsInit } = require('./models/leds');
await ledsInit();
} catch (error: any) {
exceptionReporter.report(error);
}
ReactDOM.render(
React.createElement(MainPage),
document.getElementById('main'),
@@ -368,5 +232,3 @@ async function main() {
},
);
}
main();

View File

@@ -42,8 +42,9 @@ import {
Table,
} from '../../styled-components';
import DriveSVGIcon from '../../../assets/tgt.svg';
import { SourceMetadata } from '../source-selector/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next';
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number;
@@ -137,15 +138,18 @@ const InitProgress = styled(
`;
export interface DriveSelectorProps
extends Omit<ModalProps, 'done' | 'cancel'> {
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
write: boolean;
multipleSelection: boolean;
showWarnings?: boolean;
cancel: () => void;
cancel: (drives: DrivelistDrive[]) => void;
done: (drives: DrivelistDrive[]) => void;
titleLabel: string;
emptyListLabel: string;
emptyListIcon: JSX.Element;
selectedList?: DrivelistDrive[];
updateSelectedList?: () => DrivelistDrive[];
onSelect?: (drive: DrivelistDrive) => void;
}
interface DriveSelectorState {
@@ -166,12 +170,14 @@ export class DriveSelector extends React.Component<
> {
private unsubscribe: (() => void) | undefined;
tableColumns: Array<TableColumn<Drive>>;
originalList: DrivelistDrive[];
constructor(props: DriveSelectorProps) {
super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = this.props.selectedList || [];
this.originalList = [...(this.props.selectedList || [])];
this.state = {
drives: getDrives(),
@@ -184,7 +190,7 @@ export class DriveSelector extends React.Component<
this.tableColumns = [
{
field: 'description',
label: 'Name',
label: i18next.t('drives.name'),
render: (description: string, drive: Drive) => {
if (isDrivelistDrive(drive)) {
const isLargeDrive = isDriveSizeLarge(drive);
@@ -198,7 +204,9 @@ export class DriveSelector extends React.Component<
fill={drive.isSystem ? '#fca321' : '#8f9297'}
/>
)}
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt>
<Txt ml={(hasWarnings && 8) || 0}>
{middleEllipsis(description, 32)}
</Txt>
</Flex>
);
}
@@ -208,7 +216,7 @@ export class DriveSelector extends React.Component<
{
field: 'description',
key: 'size',
label: 'Size',
label: i18next.t('drives.size'),
render: (_description: string, drive: Drive) => {
if (isDrivelistDrive(drive) && drive.size !== null) {
return prettyBytes(drive.size);
@@ -218,7 +226,7 @@ export class DriveSelector extends React.Component<
{
field: 'description',
key: 'link',
label: 'Location',
label: i18next.t('drives.location'),
render: (_description: string, drive: Drive) => {
return (
<Txt>
@@ -258,7 +266,8 @@ export class DriveSelector extends React.Component<
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
!isDriveValid(drive, image)
!isDriveValid(drive, image, this.props.write) ||
(this.props.write && drive.isReadOnly)
);
}
@@ -301,9 +310,9 @@ export class DriveSelector extends React.Component<
case compatibility.system():
return warning.systemDrive();
case compatibility.tooSmall():
const recommendedDriveSize =
const size =
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive);
return warning.tooSmall({ size }, drive);
}
}
@@ -311,6 +320,7 @@ export class DriveSelector extends React.Component<
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
drive,
this.state.image,
this.props.write,
).slice(0, 2);
return (
// the column render fn expects a single Element
@@ -345,16 +355,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() {
this.unsubscribe = store.subscribe(() => {
const drives = getDrives();
@@ -380,8 +380,8 @@ export class DriveSelector extends React.Component<
const displayedDrives = this.getDisplayedDrives(drives);
const disabledDrives = this.getDisabledDrives(drives, image);
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
const numberOfDisplayedSystemDrives = displayedDrives.filter(isSystemDrive)
.length;
const numberOfDisplayedSystemDrives =
displayedDrives.filter(isSystemDrive).length;
const numberOfHiddenSystemDrives =
numberOfSystemDrives - numberOfDisplayedSystemDrives;
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
@@ -400,14 +400,14 @@ export class DriveSelector extends React.Component<
color="#5b82a7"
style={{ fontWeight: 600 }}
>
{drives.length} found
{i18next.t('drives.find', { length: drives.length })}
</Txt>
</Flex>
}
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={cancel}
cancel={() => cancel(this.originalList)}
done={() => done(selectedList)}
action={`Select (${selectedList.length})`}
action={i18next.t('drives.select', { select: selectedList.length })}
primaryButtonProps={{
primary: !showWarnings,
warning: showWarnings,
@@ -422,7 +422,7 @@ export class DriveSelector extends React.Component<
alignItems="center"
width="100%"
>
<DriveSVGIcon width="40px" height="90px" />
{this.props.emptyListIcon}
<b>{this.props.emptyListLabel}</b>
</Flex>
) : (
@@ -445,14 +445,34 @@ export class DriveSelector extends React.Component<
onCheck={(rows: Drive[]) => {
let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) {
if (this.deselectingAll(newSelection)) {
if (rows.length === 0) {
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({
selectedList: newSelection,
});
return;
}
if (this.props.onSelect) {
this.props.onSelect(newSelection[newSelection.length - 1]);
}
this.setState({
selectedList: newSelection.slice(newSelection.length - 1),
});
@@ -464,6 +484,9 @@ export class DriveSelector extends React.Component<
) {
return;
}
if (this.props.onSelect) {
this.props.onSelect(row);
}
const index = selectedList.findIndex(
(d) => d.device === row.device,
);
@@ -490,7 +513,11 @@ export class DriveSelector extends React.Component<
>
<Flex alignItems="center">
<ChevronDownSvg height="1em" fill="currentColor" />
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
<Txt ml={8}>
{i18next.t('drives.showHidden', {
num: numberOfHiddenSystemDrives,
})}
</Txt>
</Flex>
</Link>
)}
@@ -498,7 +525,7 @@ export class DriveSelector extends React.Component<
)}
{this.props.showWarnings && hasSystemDrives ? (
<Alert className="system-drive-alert" style={{ width: '67%' }}>
Selecting your system drive is dangerous and will erase your drive!
{i18next.t('drives.systemDriveDanger')}
</Alert>
) : null}
@@ -512,19 +539,21 @@ export class DriveSelector extends React.Component<
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error) {
} catch (error: any) {
logException(error);
} finally {
this.setState({ missingDriversModal: {} });
}
}}
action="Yes, continue"
action={i18next.t('yesContinue')}
cancelButtonProps={{
children: 'Cancel',
children: i18next.t('cancel'),
}}
children={
missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
i18next.t('drives.openInBrowser', {
link: missingDriversModal.drive.link,
})
}
/>
)}

View File

@@ -7,6 +7,7 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as prettyBytes from 'pretty-bytes';
import { DriveWithWarnings } from '../../pages/main/Flash';
import * as i18next from 'i18next';
const DriveStatusWarningModal = ({
done,
@@ -17,12 +18,12 @@ const DriveStatusWarningModal = ({
isSystem: boolean;
drivesWithWarnings: DriveWithWarnings[];
}) => {
let warningSubtitle = 'You are about to erase an unusually large drive';
let warningCta = 'Are you sure the selected drive is not a storage drive?';
let warningSubtitle = i18next.t('drives.largeDriveWarning');
let warningCta = i18next.t('drives.largeDriveWarningMsg');
if (isSystem) {
warningSubtitle = "You are about to erase your computer's drives";
warningCta = 'Are you sure you want to flash your system drive?';
warningSubtitle = i18next.t('drives.systemDriveWarning');
warningCta = i18next.t('drives.systemDriveWarningMsg');
}
return (
<Modal
@@ -33,9 +34,9 @@ const DriveStatusWarningModal = ({
cancelButtonProps={{
primary: false,
warning: true,
children: 'Change target',
children: i18next.t('drives.changeTarget'),
}}
action={"Yes, I'm sure"}
action={i18next.t('sure')}
primaryButtonProps={{
primary: false,
outline: true,
@@ -50,7 +51,7 @@ const DriveStatusWarningModal = ({
<Flex flexDirection="column">
<ExclamationTriangleSvg height="2em" fill="#fca321" />
<Txt fontSize="24px" color="#fca321">
WARNING!
{i18next.t('warning')}
</Txt>
</Flex>
<Txt fontSize="24px">{warningSubtitle}</Txt>

View File

@@ -20,6 +20,7 @@ import { v4 as uuidV4 } from 'uuid';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { FlashAnother } from '../flash-another/flash-another';
@@ -39,24 +40,27 @@ function restart(goToMain: () => void) {
goToMain();
}
async function getSuccessBannerURL() {
return (
(await settings.get('successBannerURL')) ??
'https://efp.balena.io/success-banner?borderTop=false&darkBackground=true'
);
}
function FinishPage({ goToMain }: { goToMain: () => void }) {
const [webviewShowing, setWebviewShowing] = React.useState(false);
const [successBannerURL, setSuccessBannerURL] = React.useState('');
(async () => {
setSuccessBannerURL(await getSuccessBannerURL());
})();
const flashResults = flashState.getFlashResults();
let errors: FlashError[] = flashResults.results?.errors;
if (errors === undefined) {
errors = (store.getState().toJS().failedDevicePaths || []).map(
([, error]: [string, FlashError]) => ({
...error,
}),
);
}
const {
averageSpeed,
blockmappedSize,
bytesWritten,
failed,
size,
} = flashState.getFlashState();
const errors: FlashError[] = (
store.getState().toJS().failedDeviceErrors || []
).map(([, error]: [string, FlashError]) => ({
...error,
}));
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
flashState.getFlashState();
const {
skip,
results = {
@@ -85,7 +89,7 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
}}
>
<FlashResults
image={selectionState.getImageName()}
image={selectionState.getImage()?.name}
results={results}
skip={skip}
errors={errors}
@@ -99,18 +103,20 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
}}
/>
</Flex>
<SafeWebview
src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true"
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
{successBannerURL.length && (
<SafeWebview
src={successBannerURL}
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
</Flex>
);
}

View File

@@ -17,6 +17,7 @@
import * as React from 'react';
import { BaseButton } from '../../styled-components';
import * as i18next from 'i18next';
export interface FlashAnotherProps {
onClick: () => void;
@@ -25,7 +26,7 @@ export interface FlashAnotherProps {
export const FlashAnother = (props: FlashAnotherProps) => {
return (
<BaseButton primary onClick={props.onClick}>
Flash another
{i18next.t('flash.another')}
</BaseButton>
);
};

View File

@@ -17,7 +17,6 @@
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/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 * as _ from 'lodash';
import outdent from 'outdent';
import * as React from 'react';
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
@@ -27,49 +26,48 @@ import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units';
import FlashSvg from '../../../assets/flash.svg';
import { getDrives } from '../../models/available-drives';
import { resetState } from '../../models/flash-state';
import * as selection from '../../models/selection-state';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { Modal, Table } from '../../styled-components';
import * as i18next from 'i18next';
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
[data-display='table-head'],
[data-display='table-body'] {
[data-display='table-cell'] {
&:first-child {
width: 30%;
}
&&& [data-display='table-head'],
&&& [data-display='table-body'] {
> [data-display='table-row'] {
> [data-display='table-cell'] {
&:first-child {
width: 30%;
}
&:nth-child(2) {
width: 20%;
}
&:nth-child(2) {
width: 20%;
}
&:last-child {
width: 50%;
&:last-child {
width: 50%;
}
}
}
}
`;
const DoneIcon = (props: {
skipped: boolean;
color: string;
allFailed: boolean;
someFailed: boolean;
}) => {
const { allFailed, someFailed } = props;
const someOrAllFailed = allFailed || someFailed;
const svgProps = {
width: '24px',
fill: someOrAllFailed ? '#c6c8c9' : '#1ac135',
width: '28px',
fill: props.color,
style: {
width: '28px',
height: '28px',
marginTop: '-25px',
marginLeft: '13px',
zIndex: 1,
color: someOrAllFailed ? '#c6c8c9' : '#1ac135',
},
};
return allFailed && !props.skipped ? (
return props.allFailed && !props.skipped ? (
<TimesCircleSvg {...svgProps} />
) : (
<CheckCircleSvg {...svgProps} />
@@ -91,21 +89,34 @@ function formattedErrors(errors: FlashError[]) {
const columns: Array<TableColumn<FlashError>> = [
{
field: 'description',
label: 'Target',
label: i18next.t('flash.target'),
},
{
field: 'device',
label: 'Location',
label: i18next.t('flash.location'),
},
{
field: 'message',
label: 'Error',
label: i18next.t('flash.error'),
render: (message: string, { code }: FlashError) => {
return message ?? code;
},
},
];
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({
goToMain,
image = '',
@@ -119,22 +130,19 @@ export function FlashResults({
errors: FlashError[];
skip: boolean;
results: {
bytesWritten: number;
sourceMetadata: {
size: number;
blockmappedSize: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
devices: { failed: number; successful: number };
};
} & FlexProps) {
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = results.devices.successful === 0;
const effectiveSpeed = _.round(
bytesToMegabytes(
results.sourceMetadata.size /
(results.bytesWritten / results.averageFlashingSpeed),
),
const allFailed = !skip && results?.devices?.successful === 0;
const someFailed = results?.devices?.failed !== 0 || errors?.length !== 0;
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
1,
);
return (
@@ -151,42 +159,43 @@ export function FlashResults({
<DoneIcon
skipped={skip}
allFailed={allFailed}
someFailed={results.devices.failed !== 0}
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
/>
<Txt>{middleEllipsis(image, 24)}</Txt>
</Flex>
<Txt fontSize={24} color="#fff" mb="17px">
Flash Complete!
{allFailed
? i18next.t('flash.flashFailed')
: i18next.t('flash.flashCompleted')}
</Txt>
{skip ? <Flex color="#7e8085">Validation has been skipped</Flex> : null}
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
</Flex>
<Flex flexDirection="column" color="#7e8085">
{Object.entries(results.devices).map(([type, quantity]) => {
const failedTargets = type === 'failed';
return quantity ? (
<Flex alignItems="center">
<CircleSvg
width="14px"
fill={type === 'failed' ? '#ff4444' : '#1ac135'}
color={failedTargets ? '#ff4444' : '#1ac135'}
/>
<Txt ml="10px" color="#fff">
{quantity}
</Txt>
<Txt
ml="10px"
tooltip={failedTargets ? formattedErrors(errors) : undefined}
>
{progress[type](quantity)}
</Txt>
{failedTargets && (
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
more info
</Link>
)}
</Flex>
) : null;
})}
{results.devices.successful !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#1ac135" />
<Txt ml="10px" color="#fff">
{results.devices.successful}
</Txt>
<Txt ml="10px">
{progress.successful(results.devices.successful)}
</Txt>
</Flex>
) : null}
{errors.length !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#ff4444" />
<Txt ml="10px" color="#fff">
{errors.length}
</Txt>
<Txt ml="10px" tooltip={formattedErrors(errors)}>
{progress.failed(errors.length)}
</Txt>
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
{i18next.t('flash.moreInfo')}
</Link>
</Flex>
) : null}
{!allFailed && (
<Txt
fontSize="10px"
@@ -194,12 +203,9 @@ export function FlashResults({
fontWeight: 500,
textAlign: 'center',
}}
tooltip={outdent({ newline: ' ' })`
The speed is calculated by dividing the image size by the flashing time.
Disk images with ext partitions flash faster as we are able to skip unused parts.
`}
tooltip={i18next.t('flash.speedTip')}
>
Effective speed: {effectiveSpeed} MB/s
{i18next.t('flash.speed', { speed: effectiveSpeed })}
</Txt>
)}
</Flex>
@@ -209,21 +215,24 @@ export function FlashResults({
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
Failed targets
{i18next.t('failedTarget')}
</Txt>
</Flex>
}
action="Retry failed targets"
action={i18next.t('failedRetry')}
cancel={() => setShowErrorsInfo(false)}
done={() => {
setShowErrorsInfo(false);
resetState();
selection
.getSelectedDrives()
.filter((drive) =>
errors.every((error) => error.device !== drive.device),
getDrives()
.map((drive) => {
selection.deselectDrive(drive.device);
return drive.device;
})
.filter((driveDevice) =>
errors.some((error) => error.device === driveDevice),
)
.forEach((drive) => selection.deselectDrive(drive.device));
.forEach((driveDevice) => selection.selectDrive(driveDevice));
goToMain();
}}
>

View File

@@ -18,22 +18,24 @@ import * as React from 'react';
import { Flex, Button, ProgressBar, Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { fromFlashState, FlashState } from '../../modules/progress-status';
import { fromFlashState } from '../../modules/progress-status';
import { StepButton } from '../../styled-components';
import * as i18next from 'i18next';
const FlashProgressBar = styled(ProgressBar)`
> div {
width: 220px;
width: 100%;
height: 12px;
color: white !important;
text-shadow: none !important;
transition-duration: 0s;
> div {
transition-duration: 0s;
}
}
width: 220px;
width: 100%;
height: 12px;
margin-bottom: 6px;
border-radius: 14px;
@@ -44,7 +46,7 @@ const FlashProgressBar = styled(ProgressBar)`
`;
interface ProgressButtonProps {
type: FlashState['type'];
type: 'decompressing' | 'flashing' | 'verifying';
active: boolean;
percentage: number;
position: number;
@@ -58,12 +60,10 @@ const colors = {
decompressing: '#00aeef',
flashing: '#da60ff',
verifying: '#1ac135',
downloading: '#00aeef',
default: '#00aeef',
} as const;
const CancelButton = styled(({ type, onClick, ...props }) => {
const status = type === 'verifying' ? 'Skip' : 'Cancel';
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
return (
<Button plain onClick={() => onClick(status)} {...props}>
{status}
@@ -71,6 +71,7 @@ const CancelButton = styled(({ type, onClick, ...props }) => {
);
})`
font-weight: 600;
&&& {
width: auto;
height: auto;
@@ -80,7 +81,6 @@ const CancelButton = styled(({ type, onClick, ...props }) => {
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
public render() {
const type = this.props.type || 'default';
const percentage = this.props.percentage;
const warning = this.props.warning;
const { status, position } = fromFlashState({
@@ -88,6 +88,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
percentage,
position: this.props.position,
});
const type = this.props.type || 'default';
if (this.props.active) {
return (
<>
@@ -128,7 +129,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
marginTop: 30,
}}
>
Flash!
{i18next.t('flash.flashNow')}
</StepButton>
);
}

View File

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

View File

@@ -15,6 +15,8 @@
*/
import * as electron from 'electron';
import * as remote from '@electron/remote';
import * as _ from 'lodash';
import * as React from 'react';
import * as packageJSON from '../../../../../package.json';
@@ -93,10 +95,11 @@ export class SafeWebview extends React.PureComponent<
);
this.entryHref = url.href;
// Events steal 'this'
this.didFailLoad = this.didFailLoad.bind(this);
this.didGetResponseDetails = this.didGetResponseDetails.bind(this);
this.handleDomReady = _.bind(this.handleDomReady, this);
this.didFailLoad = _.bind(this.didFailLoad, this);
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
// Make a persistent electron session for the webview
this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, {
this.session = remote.session.fromPartition(ELECTRON_SESSION, {
// Disable the cache for the session such that new content shows up when refreshing
cache: false,
});
@@ -119,6 +122,8 @@ export class SafeWebview extends React.PureComponent<
ref={this.webviewRef}
partition={ELECTRON_SESSION}
style={style}
// @ts-ignore
allowpopups="true"
/>
);
}
@@ -132,8 +137,8 @@ export class SafeWebview extends React.PureComponent<
this.didFailLoad,
);
this.webviewRef.current.addEventListener(
'new-window',
SafeWebview.newWindow,
'dom-ready',
this.handleDomReady,
);
this.webviewRef.current.addEventListener(
'console-message',
@@ -155,8 +160,8 @@ export class SafeWebview extends React.PureComponent<
this.didFailLoad,
);
this.webviewRef.current.removeEventListener(
'new-window',
SafeWebview.newWindow,
'dom-ready',
this.handleDomReady,
);
this.webviewRef.current.removeEventListener(
'console-message',
@@ -166,6 +171,15 @@ export class SafeWebview extends React.PureComponent<
this.session.webRequest.onCompleted(null);
}
handleDomReady() {
const webview = this.webviewRef.current;
if (webview == null) {
return;
}
const id = webview.getWebContentsId();
electron.ipcRenderer.send('webview-dom-ready', id);
}
// Set the element state to hidden
public didFailLoad() {
this.setState({
@@ -182,7 +196,10 @@ export class SafeWebview extends React.PureComponent<
// only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') {
const HTTP_OK = 200;
analytics.logEvent('SafeWebview loaded', { event });
const { webContents, ...webviewEvent } = event;
analytics.logEvent('SafeWebview loaded', {
...webviewEvent,
});
this.setState({
shouldShow: event.statusCode === HTTP_OK,
});
@@ -191,17 +208,4 @@ export class SafeWebview extends React.PureComponent<
}
}
}
// Open link in browser if it's opened as a 'foreground-tab'
public static async newWindow(event: electron.NewWindowEvent) {
const url = new window.URL(event.url);
if (
(url.protocol === 'http:' || url.protocol === 'https:') &&
event.disposition === 'foreground-tab' &&
// Don't open links if they're disabled by the env var
!(await settings.get('disableExternalLinks'))
) {
electron.shell.openExternal(url.href);
}
}
}

View File

@@ -16,60 +16,55 @@
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import * as _ from 'lodash';
import * as os from 'os';
import * as React from 'react';
import { Flex, Checkbox, Txt } from 'rendition';
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components';
const platform = os.platform();
import * as i18next from 'i18next';
import { etcherProInfo } from '../../utils/etcher-pro-specific';
interface Setting {
name: string;
label: string | JSX.Element;
options?: {
description: string;
confirmLabel: string;
};
hide?: boolean;
}
async function getSettingsList(): Promise<Setting[]> {
return [
const list: Setting[] = [
{
name: 'errorReporting',
label: 'Anonymously report errors and usage statistics to balena.io',
label: i18next.t('settings.errorReporting'),
},
{
name: 'unmountOnSuccess',
/**
* 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: 'validateWriteOnSuccess',
label: 'Validate write on success',
},
{
name: 'updatesEnabled',
label: 'Auto-updates enabled',
hide: ['rpm', 'deb'].includes(packageType),
name: 'autoBlockmapping',
label: i18next.t('settings.trimExtPartitions'),
},
];
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
list.push({
name: 'updatesEnabled',
label: i18next.t('settings.autoUpdate'),
});
}
return list;
}
interface SettingsModalProps {
toggleModal: (value: boolean) => void;
}
const EPInfo = etcherProInfo();
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[]>([]);
React.useEffect(() => {
@@ -90,50 +85,49 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
})();
});
const toggleSetting = async (
setting: string,
options?: Setting['options'],
) => {
const toggleSetting = async (setting: string) => {
const value = currentSettings[setting];
const dangerous = options !== undefined;
analytics.logEvent('Toggle setting', {
setting,
value,
dangerous,
});
analytics.logEvent('Toggle setting', { setting, value });
await settings.set(setting, !value);
setCurrentSettings({
...currentSettings,
[setting]: !value,
});
return;
};
return (
<Modal
titleElement={
<Txt fontSize={24} mb={24}>
Settings
{i18next.t('settings.settings')}
</Txt>
}
done={() => toggleModal(false)}
>
<Flex flexDirection="column">
{settingsList.map((setting: Setting, i: number) => {
return setting.hide ? null : (
return (
<Flex key={setting.name} mb={14}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)}
onChange={() => toggleSetting(setting.name)}
/>
</Flex>
);
})}
{EPInfo !== undefined && (
<Flex flexDirection="column">
<Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
{EPInfo.get_serial() === undefined ? (
<InfoBox label="UUID" value={EPInfo.uuid} />
) : (
<InfoBox label="Serial" value={EPInfo.get_serial()} />
)}
</Flex>
)}
<Flex
mt={18}
alignItems="center"

View File

@@ -18,14 +18,25 @@ import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import { sourceDestination } from 'etcher-sdk';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as _ from 'lodash';
import { GPTPartition, MBRPartition } from 'partitioninfo';
import { uniqBy, isNil } from 'lodash';
import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react';
import { Flex, ButtonProps, Modal as SmallModal, Txt } from 'rendition';
import { requestMetadata } from '../../app';
import {
Flex,
ButtonProps,
Modal as SmallModal,
Txt,
Card as BaseCard,
Input,
Spinner,
Link,
} from 'rendition';
import styled from 'styled-components';
import * as errors from '../../../../shared/errors';
@@ -36,25 +47,74 @@ import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import * as exceptionReporter from '../../modules/exception-reporter';
import * as osDialog from '../../os/dialog';
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';
import {
ChangeButton,
DetailsText,
Modal,
StepButton,
StepNameButton,
ScrollableFlex,
} from '../../styled-components';
import { colors } from '../../theme';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import URLSelector from '../url-selector/url-selector';
import { SVGIcon } from '../svg-icon/svg-icon';
import ImageSvg from '../../../assets/image.svg';
import SrcSvg from '../../../assets/src.svg';
import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
import { isJson } from '../../../../shared/utils';
import {
SourceMetadata,
Authentication,
Source,
} from '../../../../shared/typings/source-selector';
import * as i18next from 'i18next';
const recentUrlImagesKey = 'recentUrlImages';
function normalizeRecentUrlImages(urls: any[]): URL[] {
if (!Array.isArray(urls)) {
urls = [];
}
urls = urls
.map((url) => {
try {
return new URL(url);
} catch (error: any) {
// Invalid URL, skip
}
})
.filter((url) => url !== undefined);
urls = uniqBy(urls, (url) => url.href);
return urls.slice(urls.length - 5);
}
function getRecentUrlImages(): URL[] {
let urls = [];
try {
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
} catch {
// noop
}
return normalizeRecentUrlImages(urls);
}
function setRecentUrlImages(urls: URL[]) {
const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href));
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
}
const isURL = (imagePath: string) =>
imagePath.startsWith('https://') || imagePath.startsWith('http://');
const Card = styled(BaseCard)`
hr {
margin: 5px 0;
}
`;
// TODO move these styles to rendition
const ModalText = styled.p`
a {
@@ -67,10 +127,11 @@ const ModalText = styled.p`
`;
function getState() {
const image = selectionState.getImage();
return {
hasImage: selectionState.hasImage(),
imageName: selectionState.getImageName(),
imageSize: selectionState.getImageSize(),
imageName: image?.name,
imageSize: image?.size,
};
}
@@ -78,6 +139,132 @@ function isString(value: any): value is string {
return typeof value === 'string';
}
const URLSelector = ({
done,
cancel,
}: {
done: (imageURL: string, auth?: Authentication) => void;
cancel: () => void;
}) => {
const [imageURL, setImageURL] = React.useState('');
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
const [loading, setLoading] = React.useState(false);
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
React.useEffect(() => {
const fetchRecentUrlImages = async () => {
const recentUrlImages: URL[] = await getRecentUrlImages();
setRecentImages(recentUrlImages);
};
fetchRecentUrlImages();
}, []);
return (
<Modal
cancel={cancel}
primaryButtonProps={{
disabled: loading || !imageURL,
}}
action={loading ? <Spinner /> : i18next.t('ok')}
done={async () => {
setLoading(true);
const urlStrings = recentImages.map((url: URL) => url.href);
const normalizedRecentUrls = normalizeRecentUrlImages([
...urlStrings,
imageURL,
]);
setRecentUrlImages(normalizedRecentUrls);
const auth = username ? { username, password } : undefined;
await done(imageURL, auth);
}}
>
<Flex flexDirection="column">
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
<Txt mb="10px" fontSize="24px">
{i18next.t('source.useSourceURL')}
</Txt>
<Input
value={imageURL}
placeholder={i18next.t('source.enterValidURL')}
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
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}>{i18next.t('source.auth')}</Txt>
</Flex>
</Link>
{showBasicAuth && (
<React.Fragment>
<Input
mb={15}
value={username}
placeholder={i18next.t('source.username')}
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setUsername(evt.target.value)
}
/>
<Input
value={password}
placeholder={i18next.t('source.password')}
type="password"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setPassword(evt.target.value)
}
/>
</React.Fragment>
)}
</Flex>
{recentImages.length > 0 && (
<Flex flexDirection="column" height="78.6%">
<Txt fontSize={18}>Recent</Txt>
<ScrollableFlex flexDirection="column">
<Card
p="10px 15px"
rows={recentImages
.map((recent) => (
<Txt
key={recent.href}
onClick={() => {
setImageURL(recent.href);
}}
style={{
overflowWrap: 'break-word',
}}
>
{recent.pathname.split('/').pop()} - {recent.href}
</Txt>
))
.reverse()}
/>
</ScrollableFlex>
</Flex>
)}
</Flex>
</Modal>
);
};
interface Flow {
icon?: JSX.Element;
onClick: (evt: React.MouseEvent) => void;
@@ -113,28 +300,11 @@ const FlowSelector = styled(
font-weight: 600;
svg {
color: ${colors.primary.foreground}!important;
color: ${colors.primary.foreground} !important;
}
}
`;
export type Source =
| typeof sourceDestination.File
| typeof sourceDestination.BlockDevice
| typeof sourceDestination.Http;
export interface SourceMetadata extends sourceDestination.Metadata {
hasMBR?: boolean;
partitions?: MBRPartition[] | GPTPartition[];
path: string;
displayName: string;
description: string;
SourceType: Source;
drive?: DrivelistDrive;
extension?: string;
archiveExtension?: string;
}
interface SourceSelectorProps {
flashing: boolean;
}
@@ -148,6 +318,8 @@ interface SourceSelectorState {
showURLSelector: boolean;
showDriveSelector: boolean;
defaultFlowActive: boolean;
imageSelectorOpen: boolean;
imageLoading: boolean;
}
export class SourceSelector extends React.Component<
@@ -165,6 +337,8 @@ export class SourceSelector extends React.Component<
showURLSelector: false,
showDriveSelector: false,
defaultFlowActive: true,
imageSelectorOpen: false,
imageLoading: false,
};
// Bind `this` since it's used in an event's callback
@@ -185,25 +359,20 @@ export class SourceSelector extends React.Component<
}
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
this.setState({ imageLoading: true });
await this.selectSource(
imagePath,
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
isURL(this.normalizeImagePath(imagePath)) ? 'Http' : 'File',
).promise;
this.setState({ imageLoading: false });
}
private async createSource(selected: string, SourceType: Source) {
try {
selected = await replaceWindowsNetworkDriveLetter(selected);
} catch (error) {
analytics.logException(error);
public normalizeImagePath(imgPath: string) {
const decodedPath = decodeURIComponent(imgPath);
if (isJson(decodedPath)) {
return JSON.parse(decodedPath).url ?? decodedPath;
}
if (SourceType === sourceDestination.File) {
return new sourceDestination.File({
path: selected,
});
}
return new sourceDestination.Http({ url: selected });
return decodedPath;
}
private reselectSource() {
@@ -217,6 +386,7 @@ export class SourceSelector extends React.Component<
private selectSource(
selected: string | DrivelistDrive,
SourceType: Source,
auth?: Authentication,
): { promise: Promise<void>; cancel: () => void } {
let cancelled = false;
return {
@@ -225,12 +395,14 @@ export class SourceSelector extends React.Component<
},
promise: (async () => {
const sourcePath = isString(selected) ? selected : selected.device;
let source;
let metadata: SourceMetadata | undefined;
if (isString(selected)) {
if (SourceType === sourceDestination.Http && !isURL(selected)) {
if (
SourceType === 'Http' &&
!isURL(this.normalizeImagePath(selected))
) {
this.handleError(
'Unsupported protocol',
i18next.t('source.unsupportedProtocol'),
selected,
messages.error.unsupportedProtocol(),
);
@@ -242,62 +414,57 @@ export class SourceSelector extends React.Component<
this.setState({
warning: {
message: messages.warning.looksLikeWindowsImage(),
title: 'Possible Windows image detected',
title: i18next.t('source.windowsImage'),
},
});
}
source = await this.createSource(selected, SourceType);
if (cancelled) {
return;
}
try {
const innerSource = await source.getInnerSource();
if (cancelled) {
return;
}
metadata = await this.getMetadata(innerSource, selected);
if (cancelled) {
return;
}
metadata.SourceType = SourceType;
// this will send an event down the ipcMain asking for metadata
// we'll get the response through an event
if (!metadata.hasMBR) {
metadata = await requestMetadata({ selected, SourceType, auth });
if (!metadata?.hasMBR && this.state.warning === null) {
analytics.logEvent('Missing partition table', { metadata });
this.setState({
warning: {
message: messages.warning.missingPartitionTable(),
title: 'Missing partition table',
title: i18next.t('source.partitionTable'),
},
});
}
} catch (error) {
} catch (error: any) {
this.handleError(
'Error opening source',
i18next.t('source.errorOpen'),
sourcePath,
messages.error.openSource(sourcePath, error.message),
error,
);
} finally {
try {
await source.close();
} catch (error) {
// Noop
}
}
} else {
if (selected.partitionTableType === null) {
analytics.logEvent('Missing partition table', { selected });
this.setState({
warning: {
message: messages.warning.driveMissingPartitionTable(),
title: i18next.t('source.partitionTable'),
},
});
}
metadata = {
path: selected.device,
displayName: selected.displayName,
description: selected.displayName,
size: selected.size as SourceMetadata['size'],
SourceType: sourceDestination.BlockDevice,
SourceType: 'BlockDevice',
drive: selected,
};
}
if (metadata !== undefined) {
metadata.auth = auth;
metadata.SourceType = SourceType;
selectionState.selectSource(metadata);
analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of
@@ -331,27 +498,9 @@ export class SourceSelector extends React.Component<
analytics.logEvent(title, { path: sourcePath });
}
private async getMetadata(
source: sourceDestination.SourceDestination,
selected: string | DrivelistDrive,
) {
const metadata = (await source.getMetadata()) as SourceMetadata;
const partitionTable = await source.getPartitionTable();
if (partitionTable) {
metadata.hasMBR = true;
metadata.partitions = partitionTable.partitions;
} else {
metadata.hasMBR = false;
}
if (isString(selected)) {
metadata.extension = path.extname(selected).slice(1);
metadata.path = selected;
}
return metadata;
}
private async openImageSelector() {
analytics.logEvent('Open image selector');
this.setState({ imageSelectorOpen: true });
try {
const imagePath = await osDialog.selectImage();
@@ -361,16 +510,18 @@ export class SourceSelector extends React.Component<
analytics.logEvent('Image selector closed');
return;
}
await this.selectSource(imagePath, sourceDestination.File).promise;
} catch (error) {
await this.selectSource(imagePath, 'File').promise;
} catch (error: any) {
exceptionReporter.report(error);
} finally {
this.setState({ imageSelectorOpen: false });
}
}
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
const [file] = event.dataTransfer.files;
if (file) {
await this.selectSource(file.path, sourceDestination.File).promise;
await this.selectSource(file.path, 'File').promise;
}
}
@@ -402,7 +553,7 @@ export class SourceSelector extends React.Component<
private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', {
imagePath: selectionState.getImagePath(),
imagePath: selectionState.getImage()?.path,
});
this.setState({
@@ -414,10 +565,21 @@ export class SourceSelector extends React.Component<
this.setState({ defaultFlowActive });
}
private closeModal() {
this.setState({
showDriveSelector: false,
});
}
// TODO add a visual change when dragging a file over the selector
public render() {
const { flashing } = this.props;
const { showImageDetails, showURLSelector, showDriveSelector } = this.state;
const {
showImageDetails,
showURLSelector,
showDriveSelector,
imageLoading,
} = this.state;
const selectionImage = selectionState.getImage();
let image: SourceMetadata | DrivelistDrive =
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
@@ -455,36 +617,39 @@ export class SourceSelector extends React.Component<
}}
/>
{selectionImage !== undefined ? (
{selectionImage !== undefined || imageLoading ? (
<>
<StepNameButton
plain
onClick={() => this.showSelectedImageDetails()}
tooltip={imageName || imageBasename}
>
{middleEllipsis(imageName || imageBasename, 20)}
<Spinner show={imageLoading}>
{middleEllipsis(imageName || imageBasename, 20)}
</Spinner>
</StepNameButton>
{!flashing && (
{!flashing && !imageLoading && (
<ChangeButton
plain
mb={14}
onClick={() => this.reselectSource()}
>
Remove
{i18next.t('cancel')}
</ChangeButton>
)}
{!_.isNil(imageSize) && (
{!isNil(imageSize) && !imageLoading && (
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
)}
</>
) : (
<>
<FlowSelector
disabled={this.state.imageSelectorOpen}
primary={this.state.defaultFlowActive}
key="Flash from file"
flow={{
onClick: () => this.openImageSelector(),
label: 'Flash from file',
label: i18next.t('source.fromFile'),
icon: <FileSvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
@@ -494,7 +659,7 @@ export class SourceSelector extends React.Component<
key="Flash from URL"
flow={{
onClick: () => this.openURLSelector(),
label: 'Flash from URL',
label: i18next.t('source.fromURL'),
icon: <LinkSvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
@@ -504,7 +669,7 @@ export class SourceSelector extends React.Component<
key="Clone drive"
flow={{
onClick: () => this.openDriveSelector(),
label: 'Clone drive',
label: i18next.t('source.clone'),
icon: <CopySvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
@@ -516,13 +681,16 @@ export class SourceSelector extends React.Component<
{this.state.warning != null && (
<SmallModal
style={{
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
}}
titleElement={
<span>
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
<span>{this.state.warning.title}</span>
</span>
}
action="Continue"
action={i18next.t('continue')}
cancel={() => {
this.setState({ warning: null });
this.reselectSource();
@@ -540,17 +708,17 @@ export class SourceSelector extends React.Component<
{showImageDetails && (
<SmallModal
title="Image"
title={i18next.t('source.image')}
done={() => {
this.setState({ showImageDetails: false });
}}
>
<Txt.p>
<Txt.span bold>Name: </Txt.span>
<Txt.span bold>{i18next.t('source.name')}</Txt.span>
<Txt.span>{imageName || imageBasename}</Txt.span>
</Txt.p>
<Txt.p>
<Txt.span bold>Path: </Txt.span>
<Txt.span bold>{i18next.t('source.path')}</Txt.span>
<Txt.span>{imagePath}</Txt.span>
</Txt.p>
</SmallModal>
@@ -564,7 +732,7 @@ export class SourceSelector extends React.Component<
showURLSelector: false,
});
}}
done={async (imageURL: string) => {
done={async (imageURL: string, auth?: Authentication) => {
// Avoid analytics and selection state changes
// if no file was resolved from the dialog.
if (!imageURL) {
@@ -573,7 +741,8 @@ export class SourceSelector extends React.Component<
let promise;
({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL,
sourceDestination.Http,
'Http',
auth,
));
await promise;
}
@@ -586,24 +755,32 @@ export class SourceSelector extends React.Component<
{showDriveSelector && (
<DriveSelector
write={false}
multipleSelection={false}
titleLabel="Select source"
emptyListLabel="Plug a source"
cancel={() => {
this.setState({
showDriveSelector: false,
});
}}
done={async (drives: DrivelistDrive[]) => {
if (drives.length) {
await this.selectSource(
drives[0],
sourceDestination.BlockDevice,
);
titleLabel={i18next.t('source.selectSource')}
emptyListLabel={i18next.t('source.plugSource')}
emptyListIcon={<SrcSvg width="40px" />}
cancel={(originalList) => {
if (originalList.length) {
const originalSource = originalList[0];
if (selectionImage?.drive?.device !== originalSource.device) {
this.selectSource(originalSource, 'BlockDevice');
}
} else {
selectionState.deselectImage();
}
this.closeModal();
}}
done={() => this.closeModal()}
onSelect={(drive) => {
if (drive) {
if (
selectionState.getImage()?.drive?.device === drive?.device
) {
return selectionState.deselectImage();
}
this.selectSource(drive, 'BlockDevice');
}
this.setState({
showDriveSelector: false,
});
}}
/>
)}

View File

@@ -24,7 +24,7 @@ import {
} from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes';
import { getSelectedDrives } from '../../models/selection-state';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import {
ChangeButton,
DetailsText,
@@ -32,6 +32,7 @@ import {
StepNameButton,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next';
interface TargetSelectorProps {
targets: any[];
@@ -80,9 +81,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
if (targets.length === 1) {
const target = targets[0];
const warnings = getDriveImageCompatibilityStatuses(target).map(
getDriveWarning,
);
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
@@ -93,7 +96,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
</StepNameButton>
{!props.flashing && (
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
Change
{i18next.t('target.change')}
</ChangeButton>
)}
{target.size != null && (
@@ -106,9 +109,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
if (targets.length > 1) {
const targetsTemplate = [];
for (const target of targets) {
const warnings = getDriveImageCompatibilityStatuses(target).map(
getDriveWarning,
);
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
targetsTemplate.push(
<DetailsText
key={target.device}
@@ -128,11 +133,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
{targets.length} Targets
{targets.length} {i18next.t('target.targets')}
</StepNameButton>
{!props.flashing && (
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
Change
{i18next.t('target.change')}
</ChangeButton>
)}
{targetsTemplate}
@@ -147,7 +152,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
disabled={props.disabled}
onClick={props.openDriveSelector}
>
Select target
{i18next.t('target.selectTarget')}
</StepButton>
);
}

View File

@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { scanner } from 'etcher-sdk';
import * as React from 'react';
import { Flex, Txt } from 'rendition';
@@ -28,14 +27,17 @@ import {
getSelectedDrives,
deselectDrive,
selectDrive,
deselectAllDrives,
} from '../../models/selection-state';
import * as settings from '../../models/settings';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { TargetSelectorButton } from './target-selector-button';
import TgtSvg from '../../../assets/tgt.svg';
import DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
import * as i18next from 'i18next';
export const getDriveListLabel = () => {
return getSelectedDrives()
@@ -45,12 +47,7 @@ export const getDriveListLabel = () => {
.join('\n');
};
const shouldShowDrivesButton = () => {
return !settings.getSync('disableExplicitDriveSelection');
};
const getDriveSelectionStateSlice = () => ({
showDrivesButton: shouldShowDrivesButton(),
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
@@ -59,13 +56,14 @@ const getDriveSelectionStateSlice = () => ({
export const TargetSelectorModal = (
props: Omit<
DriveSelectorProps,
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
>,
) => (
<DriveSelector
multipleSelection={true}
titleLabel="Select target"
emptyListLabel="Plug a target drive"
titleLabel={i18next.t('target.selectTarget')}
emptyListLabel={i18next.t('target.plugTarget')}
emptyListIcon={<TgtSvg width="40px" />}
showWarnings={true}
selectedList={getSelectedDrives()}
updateSelectedList={getSelectedDrives}
@@ -73,9 +71,7 @@ export const TargetSelectorModal = (
/>
);
export const selectAllTargets = (
modalTargets: scanner.adapters.DrivelistDrive[],
) => {
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter(
(drive) =>
@@ -114,13 +110,11 @@ export const TargetSelector = ({
flashing,
}: TargetSelectorProps) => {
// TODO: inject these from redux-connector
const [
{ showDrivesButton, driveListLabel, targets },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
false,
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
getDriveSelectionStateSlice(),
);
const [showTargetSelectorModal, setShowTargetSelectorModal] =
React.useState(false);
React.useEffect(() => {
return observe(() => {
@@ -141,7 +135,7 @@ export const TargetSelector = ({
<TargetSelectorButton
disabled={disabled}
show={!hasDrive && showDrivesButton}
show={!hasDrive}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
@@ -168,11 +162,31 @@ export const TargetSelector = ({
{showTargetSelectorModal && (
<TargetSelectorModal
cancel={() => setShowTargetSelectorModal(false)}
done={(modalTargets) => {
selectAllTargets(modalTargets);
write={true}
cancel={(originalList) => {
if (originalList.length) {
selectAllTargets(originalList);
} else {
deselectAllDrives();
}
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>

View File

@@ -1,167 +0,0 @@
import { uniqBy } from 'lodash';
import * as React from 'react';
import Checkbox from 'rendition/dist_esm5/components/Checkbox';
import { Flex } from 'rendition/dist_esm5/components/Flex';
import Input from 'rendition/dist_esm5/components/Input';
import Link from 'rendition/dist_esm5/components/Link';
import RadioButton from 'rendition/dist_esm5/components/RadioButton';
import Txt from 'rendition/dist_esm5/components/Txt';
import * as settings from '../../models/settings';
import { Modal, ScrollableFlex } from '../../styled-components';
import { openDialog } from '../../os/dialog';
import { startEllipsis } from '../../utils/start-ellipsis';
const RECENT_URL_IMAGES_KEY = 'recentUrlImages';
const SAVE_IMAGE_AFTER_FLASH_KEY = 'saveUrlImage';
const SAVE_IMAGE_AFTER_FLASH_PATH_KEY = 'saveUrlImageTo';
function normalizeRecentUrlImages(urls: any[]): URL[] {
if (!Array.isArray(urls)) {
urls = [];
}
urls = urls
.map((url) => {
try {
return new URL(url);
} catch (error) {
// Invalid URL, skip
}
})
.filter((url) => url !== undefined);
urls = uniqBy(urls, (url) => url.href);
return urls.slice(-5);
}
function getRecentUrlImages(): URL[] {
let urls = [];
try {
urls = JSON.parse(localStorage.getItem(RECENT_URL_IMAGES_KEY) || '[]');
} catch {
// noop
}
return normalizeRecentUrlImages(urls);
}
function setRecentUrlImages(urls: string[]) {
localStorage.setItem(RECENT_URL_IMAGES_KEY, JSON.stringify(urls));
}
export const URLSelector = ({
done,
cancel,
}: {
done: (imageURL: string) => void;
cancel: () => void;
}) => {
const [imageURL, setImageURL] = React.useState('');
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
const [loading, setLoading] = React.useState(false);
const [saveImage, setSaveImage] = React.useState(false);
const [saveImagePath, setSaveImagePath] = React.useState('');
React.useEffect(() => {
const fetchRecentUrlImages = async () => {
const recentUrlImages: URL[] = await getRecentUrlImages();
setRecentImages(recentUrlImages);
};
const getSaveImageSettings = async () => {
const saveUrlImage: boolean = await settings.get(
SAVE_IMAGE_AFTER_FLASH_KEY,
);
const saveUrlImageToPath: string = await settings.get(
SAVE_IMAGE_AFTER_FLASH_PATH_KEY,
);
setSaveImage(saveUrlImage);
setSaveImagePath(saveUrlImageToPath);
};
fetchRecentUrlImages();
getSaveImageSettings();
}, []);
return (
<Modal
title="Use Image URL"
cancel={cancel}
primaryButtonProps={{
className: loading || !imageURL ? 'disabled' : '',
}}
done={async () => {
setLoading(true);
const urlStrings = recentImages
.map((url: URL) => url.href)
.concat(imageURL);
setRecentUrlImages(urlStrings);
await done(imageURL);
}}
>
<Flex flexDirection="column">
<Flex mb="16px" width="100%" height="auto" flexDirection="column">
<Input
value={imageURL}
placeholder="Enter a valid URL"
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setImageURL(evt.target.value)
}
/>
<Flex alignItems="flex-end">
<Checkbox
mt="16px"
checked={saveImage}
onChange={(evt) => {
const value = evt.target.checked;
setSaveImage(value);
settings
.set(SAVE_IMAGE_AFTER_FLASH_KEY, value)
.then(() => setSaveImage(value));
}}
label={<>Save file to:&nbsp;</>}
/>
<Link
disabled={!saveImage}
onClick={async () => {
if (saveImage) {
const folder = await openDialog('openDirectory');
if (folder) {
await settings.set(SAVE_IMAGE_AFTER_FLASH_PATH_KEY, folder);
setSaveImagePath(folder);
}
}
}}
>
{startEllipsis(saveImagePath, 20)}
</Link>
</Flex>
</Flex>
{recentImages.length > 0 && (
<Flex flexDirection="column" height="58%">
<Txt fontSize={18} mb="10px">
Recent
</Txt>
<ScrollableFlex flexDirection="column" p="0">
{recentImages
.map((recent, i) => (
<RadioButton
mb={i !== 0 ? '6px' : '0'}
key={recent.href}
checked={imageURL === recent.href}
label={`${recent.pathname.split('/').pop()} - ${
recent.href
}`}
onChange={() => {
setImageURL(recent.href);
}}
style={{
overflowWrap: 'break-word',
}}
/>
))
.reverse()}
</ScrollableFlex>
</Flex>
)}
</Flex>
</Modal>
);
};
export default URLSelector;

44
lib/gui/app/i18n.ts Normal file
View File

@@ -0,0 +1,44 @@
import * as i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import zh_CN_translation from './i18n/zh-CN';
import zh_TW_translation from './i18n/zh-TW';
import en_translation from './i18n/en';
export function langParser() {
if (process.env.LANG !== undefined) {
// Bypass mocha, where lang-detect don't works
return 'en';
}
const lang = Intl.DateTimeFormat().resolvedOptions().locale;
switch (lang.substr(0, 2)) {
case 'zh':
if (lang === 'zh-CN' || lang === 'zh-SG') {
return 'zh-CN';
} // Simplified Chinese
else {
return 'zh-TW';
} // Traditional Chinese
default:
return lang.substr(0, 2);
}
}
i18next.use(initReactI18next).init({
lng: langParser(),
fallbackLng: 'en',
nonExplicitSupportedLngs: true,
interpolation: {
escapeValue: false,
},
resources: {
'zh-CN': zh_CN_translation,
'zh-TW': zh_TW_translation,
en: en_translation,
},
});
export const supportedLocales = ['en', 'zh'];
export default i18next;

View File

@@ -0,0 +1,23 @@
# i18n
## How it was done
Using the open-source lib [i18next](https://www.i18next.com/).
## How to add your own language
1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned
in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES`
and `pt-BR`)
.
2. Copy the content from an existing translation and start to translate.
3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the
already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function.
4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to
be `sudo-askpass.osascript-xx.js` and edit
the `'balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and
those `Ok`s and `Cancel`s to your own language.
5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW`
, or `pt-BR` and `pt-PT`, edit
the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need.
6. Make a commit, and then a pull request on GitHub.

162
lib/gui/app/i18n/en.ts Normal file
View File

@@ -0,0 +1,162 @@
const translation = {
translation: {
continue: 'Continue',
ok: 'OK',
cancel: 'Cancel',
skip: 'Skip',
sure: "Yes, I'm sure",
warning: 'WARNING! ',
attention: 'Attention',
failed: 'Failed',
completed: 'Completed',
yesContinue: 'Yes, continue',
reallyExit: 'Are you sure you want to close Etcher?',
yesExit: 'Yes, quit',
progress: {
starting: 'Starting...',
decompressing: 'Decompressing...',
flashing: 'Flashing...',
finishing: 'Finishing...',
verifying: 'Validating...',
failing: 'Failed',
},
message: {
sizeNotRecommended: 'Not recommended',
tooSmall: 'Too small',
locked: 'Locked',
system: 'System drive',
containsImage: 'Source drive',
largeDrive: 'Large drive',
sourceLarger: 'The selected source is {{byte}} larger than this drive.',
flashSucceed_one: 'Successful target',
flashSucceed_other: 'Successful targets',
flashFail_one: 'Failed target',
flashFail_other: 'Failed targets',
toDrive: 'to {{description}} ({{name}})',
toTarget_one: 'to {{num}} target',
toTarget_other: 'to {{num}} targets',
andFailTarget_one: 'and failed to be flashed to {{num}} target',
andFailTarget_other: 'and failed to be flashed to {{num}} targets',
succeedTo: '{{name}} was successfully flashed {{target}}',
exitWhileFlashing:
'You are currently flashing a drive. Closing Etcher may leave your drive in an unusable state.',
looksLikeWindowsImage:
'It looks like you are trying to burn a Windows image.\n\nUnlike other images, Windows images require special processing to be made bootable. We suggest you use a tool specially designed for this purpose, such as <a href="https://rufus.akeo.ie">Rufus</a> (Windows), <a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux), or Boot Camp Assistant (macOS).',
image: 'image',
drive: 'drive',
missingPartitionTable:
'It looks like this is not a bootable {{type}}.\n\nThe {{type}} does not appear to contain a partition table, and might not be recognized or bootable by your device.',
largeDriveSize:
"This is a large drive! Make sure it doesn't contain files that you want to keep.",
systemDrive:
'Selecting your system drive is dangerous and will erase your drive!',
sourceDrive: 'Contains the image you chose to flash',
noSpace:
'Not enough space on the drive. Please insert larger one and try again.',
genericFlashError:
'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n{{error}}',
validation:
'The write has been completed successfully but Etcher detected potential corruption issues when reading the image back from the drive. \n\nPlease consider writing the image to a different drive.',
openError:
'Something went wrong while opening {{source}}.\n\nError: {{error}}',
flashError: 'Something went wrong while writing {{image}} {{targets}}.',
unplug:
"Looks like Etcher lost access to the drive. Did it get unplugged accidentally?\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
cannotWrite:
'Looks like Etcher is not able to write to this location of the drive. This error is usually caused by a faulty drive, reader, or port. \n\nPlease try again with another drive, reader, or port.',
childWriterDied:
'The writer process ended unexpectedly. Please try again, and contact the Etcher team if the problem persists.',
badProtocol: 'Only http:// and https:// URLs are supported.',
},
target: {
selectTarget: 'Select target',
plugTarget: 'Plug a target drive',
targets: 'Targets',
change: 'Change',
},
source: {
useSourceURL: 'Use Image URL',
auth: 'Authentication',
username: 'Enter username',
password: 'Enter password',
unsupportedProtocol: 'Unsupported protocol',
windowsImage: 'Possible Windows image detected',
partitionTable: 'Missing partition table',
errorOpen: 'Error opening source',
fromFile: 'Flash from file',
fromURL: 'Flash from URL',
clone: 'Clone drive',
image: 'Image',
name: 'Name: ',
path: 'Path: ',
selectSource: 'Select source',
plugSource: 'Plug a source drive',
osImages: 'OS Images',
allFiles: 'All',
enterValidURL: 'Enter a valid URL',
},
drives: {
name: 'Name',
size: 'Size',
location: 'Location',
find: '{{length}} found',
select: 'Select {{select}}',
showHidden: 'Show {{num}} hidden',
systemDriveDanger:
'Selecting your system drive is dangerous and will erase your drive!',
openInBrowser: '`Etcher will open {{link}} in your browser`',
changeTarget: 'Change target',
largeDriveWarning: 'You are about to erase an unusually large drive',
largeDriveWarningMsg:
'Are you sure the selected drive is not a storage drive?',
systemDriveWarning: "You are about to erase your computer's drives",
systemDriveWarningMsg:
'Are you sure you want to flash your system drive?',
},
flash: {
another: 'Flash another',
target: 'Target',
location: 'Location',
error: 'Error',
flash: 'Flash',
flashNow: 'Flash!',
skip: 'Validation has been skipped',
moreInfo: 'more info',
speedTip:
'The speed is calculated by dividing the image size by the flashing time.\nDisk images with ext partitions flash faster as we are able to skip unused parts.',
speed: 'Effective speed: {{speed}} MB/s',
speedShort: '{{speed}} MB/s',
eta: 'ETA: {{eta}}',
failedTarget: 'Failed targets',
failedRetry: 'Retry failed targets',
flashFailed: 'Flash Failed.',
flashCompleted: 'Flash Completed!',
},
settings: {
errorReporting:
'Anonymously report errors and usage statistics to balena.io',
autoUpdate: 'Auto-updates enabled',
settings: 'Settings',
systemInformation: 'System Information',
trimExtPartitions:
'Trim unallocated space on raw images (in ext-type partitions)',
},
menu: {
edit: 'Edit',
view: 'View',
devTool: 'Toggle Developer Tools',
window: 'Window',
help: 'Help',
pro: 'Etcher Pro',
website: 'Etcher Website',
issue: 'Report an issue',
about: 'About Etcher',
hide: 'Hide Etcher',
hideOthers: 'Hide Others',
unhide: 'Unhide All',
quit: 'Quit Etcher',
},
},
};
export default translation;

152
lib/gui/app/i18n/zh-CN.ts Normal file
View File

@@ -0,0 +1,152 @@
const translation = {
translation: {
ok: '好',
cancel: '取消',
continue: '继续',
skip: '跳过',
sure: '我确定',
warning: '请注意!',
attention: '请注意',
failed: '失败',
completed: '完毕',
yesExit: '是的,可以退出',
reallyExit: '真的要现在退出 Etcher 吗?',
yesContinue: '是的,继续',
progress: {
starting: '正在启动……',
decompressing: '正在解压……',
flashing: '正在烧录……',
finishing: '正在结束……',
verifying: '正在验证……',
failing: '失败……',
},
message: {
sizeNotRecommended: '大小不推荐',
tooSmall: '空间太小',
locked: '被锁定',
system: '系统盘',
containsImage: '存放源镜像',
largeDrive: '很大的磁盘',
sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。',
flashSucceed_one: '烧录成功',
flashSucceed_other: '烧录成功',
flashFail_one: '烧录失败',
flashFail_other: '烧录失败',
toDrive: '到 {{description}} ({{name}})',
toTarget_one: '到 {{num}} 个目标',
toTarget_other: '到 {{num}} 个目标',
andFailTarget_one: '并烧录失败了 {{num}} 个目标',
andFailTarget_other: '并烧录失败了 {{num}} 个目标',
succeedTo: '{{name}} 被成功烧录 {{target}}',
exitWhileFlashing:
'您当前正在刷机。 关闭 Etcher 可能会导致您的磁盘无法使用。',
looksLikeWindowsImage:
'看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
image: '镜像',
drive: '磁盘',
missingPartitionTable:
'看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。',
largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息',
systemDrive: '选择系统盘很危险,因为这将会删除你的系统',
sourceDrive: '源镜像位于这个分区中',
noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。',
genericFlashError:
'出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}',
validation:
'写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。',
openError: '打开 {{source}} 时出错。\n\n错误信息 {{error}}',
flashError: '烧录 {{image}} {{targets}} 失败。',
unplug:
'看起来 Etcher 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。',
cannotWrite:
'看起来 Etcher 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。',
childWriterDied:
'写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher 团队。',
badProtocol: '仅支持 http:// 和 https:// 开头的网址。',
},
target: {
selectTarget: '选择目标磁盘',
plugTarget: '请插入目标磁盘',
targets: '个目标',
change: '更改',
},
menu: {
edit: '编辑',
view: '视图',
devTool: '打开开发者工具',
window: '窗口',
help: '帮助',
pro: 'Etcher 专业版',
website: 'Etcher 的官网',
issue: '提交一个 issue',
about: '关于 Etcher',
hide: '隐藏 Etcher',
hideOthers: '隐藏其它窗口',
unhide: '取消隐藏',
quit: '退出 Etcher',
},
source: {
useSourceURL: '使用镜像网络地址',
auth: '验证',
username: '输入用户名',
password: '输入密码',
unsupportedProtocol: '不支持的协议',
windowsImage: '这可能是 Windows 系统镜像',
partitionTable: '找不到分区表',
errorOpen: '打开源镜像时出错',
fromFile: '从文件烧录',
fromURL: '从在线地址烧录',
clone: '克隆磁盘',
image: '镜像信息',
name: '名称:',
path: '路径:',
selectSource: '选择源',
plugSource: '请插入源磁盘',
osImages: '系统镜像格式',
allFiles: '任何文件格式',
enterValidURL: '请输入一个正确的地址',
},
drives: {
name: '名称',
size: '大小',
location: '位置',
find: '找到 {{length}} 个',
select: '选定 {{select}}',
showHidden: '显示 {{num}} 个隐藏的磁盘',
systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!',
openInBrowser: 'Etcher 会在浏览器中打开 {{link}}',
changeTarget: '改变目标',
largeDriveWarning: '您即将擦除一个非常大的磁盘',
largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?',
systemDriveWarning: '您将要擦除系统盘',
systemDriveWarningMsg: '您确定要烧录到系统盘吗?',
},
flash: {
another: '烧录另一目标',
target: '目标',
location: '位置',
error: '错误',
flash: '烧录',
flashNow: '现在烧录!',
skip: '跳过了验证',
moreInfo: '更多信息',
speedTip:
'通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分因此具有EXT分区的磁盘镜像烧录速度更快。',
speed: '速度:{{speed}} MB/秒',
speedShort: '{{speed}} MB/秒',
eta: '预计还需要:{{eta}}',
failedTarget: '失败的烧录目标',
failedRetry: '重试烧录失败目标',
flashFailed: '烧录失败。',
flashCompleted: '烧录成功!',
},
settings: {
errorReporting: '匿名地向 balena.io 报告运行错误和使用统计',
autoUpdate: '自动更新',
settings: '软件设置',
systemInformation: '系统信息',
},
},
};
export default translation;

154
lib/gui/app/i18n/zh-TW.ts Normal file
View File

@@ -0,0 +1,154 @@
const translation = {
translation: {
continue: '繼續',
ok: '好',
cancel: '取消',
skip: '跳過',
sure: '我確定',
warning: '請注意!',
attention: '請注意',
failed: '失敗',
completed: '完成',
yesContinue: '是的,繼續',
reallyExit: '真的要現在結束 Etcher 嗎?',
yesExit: '是的,可以結束',
progress: {
starting: '正在啟動……',
decompressing: '正在解壓縮……',
flashing: '正在燒錄……',
finishing: '正在結束……',
verifying: '正在驗證……',
failing: '失敗……',
},
message: {
sizeNotRecommended: '大小不建議',
tooSmall: '空間太小',
locked: '被鎖定',
system: '系統',
containsImage: '存放來源映像檔',
largeDrive: '很大的磁碟',
sourceLarger: '所選的映像檔比目標磁碟大了 {{byte}} 位元組。',
flashSucceed_one: '燒錄成功',
flashSucceed_other: '燒錄成功',
flashFail_one: '燒錄失敗',
flashFail_other: '燒錄失敗',
toDrive: '到 {{description}} ({{name}})',
toTarget_one: '到 {{num}} 個目標',
toTarget_other: '到 {{num}} 個目標',
andFailTarget_one: '並燒錄失敗了 {{num}} 個目標',
andFailTarget_other: '並燒錄失敗了 {{num}} 個目標',
succeedTo: '{{name}} 被成功燒錄 {{target}}',
exitWhileFlashing:
'您目前正在刷寫。關閉 Etcher 可能會導致您的磁碟無法使用。',
looksLikeWindowsImage:
'看起來您正在嘗試燒錄 Windows 映像檔。\n\n與其他映像檔不同Windows 映像檔需要特殊處理才能使其可啟動。我們建議您使用專門為此目的設計的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
image: '映像檔',
drive: '磁碟',
missingPartitionTable:
'看起來這不是一個可啟動的{{type}}。\n\n這個{{type}}似乎不包含分割表,因此您的設備可能無法識別或無法正確啟動。',
largeDriveSize:
'這是個很大容量的磁碟!請檢查並確認它不包含對您來說存放很重要的資料',
systemDrive: '選擇系統分割區很危險,因為這將會刪除你的系統',
sourceDrive: '來源映像檔位於這個分割區中',
noSpace: '磁碟空間不足。請插入另一個較大的磁碟並重試。',
genericFlashError:
'出了點問題。如果來源映像檔曾被壓縮過,請檢查它是否已損壞。\n{{error}}',
validation:
'寫入已成功完成,但 Etcher 在從磁碟讀取映像檔時檢測到潛在的損壞問題。\n\n請考慮將映像檔寫入其他磁碟。',
openError: '打開 {{source}} 時發生錯誤。\n\n錯誤訊息 {{error}}',
flashError: '燒錄 {{image}} {{targets}} 失敗。',
unplug:
'看起來 Etcher 失去了對磁碟的連接。是不是被意外拔掉了?\n\n有時這個錯誤是因為讀卡器出了故障。',
cannotWrite:
'看起來 Etcher 無法寫入磁碟的這個位置。此錯誤通常是由故障的磁碟、讀取器或連接埠引起的。\n\n請使用其他磁碟、讀卡器或連接埠重試。',
childWriterDied:
'寫入處理程序意外崩潰。請再試一次,如果問題仍然存在,請聯絡 Etcher 團隊。',
badProtocol: '僅支援 http:// 和 https:// 開頭的網址。',
},
target: {
selectTarget: '選擇目標磁碟',
plugTarget: '請插入目標磁碟',
targets: '個目標',
change: '更改',
},
source: {
useSourceURL: '使用映像檔網址',
auth: '驗證',
username: '輸入使用者名稱',
password: '輸入密碼',
unsupportedProtocol: '不支持的通訊協定',
windowsImage: '這可能是 Windows 系統映像檔',
partitionTable: '找不到分割表',
errorOpen: '打開來源映像檔時出錯',
fromFile: '從檔案燒錄',
fromURL: '從網址燒錄',
clone: '再製磁碟',
image: '映像檔訊息',
name: '名稱:',
path: '路徑:',
selectSource: '選擇來源',
plugSource: '請插入來源磁碟',
osImages: '系統映像檔格式',
allFiles: '任何檔案格式',
enterValidURL: '請輸入正確的網址',
},
drives: {
name: '名稱',
size: '大小',
location: '位置',
find: '找到 {{length}} 個',
select: '選取 {{select}}',
showHidden: '顯示 {{num}} 個隱藏的磁碟',
systemDriveDanger: '選擇系統分割區很危險,因為這將會刪除你的系統!',
openInBrowser: 'Etcher 會在瀏覽器中打開 {{link}}',
changeTarget: '更改目標',
largeDriveWarning: '您即將格式化一個非常大的磁碟',
largeDriveWarningMsg: '您確定所選磁碟不是儲存資料的磁碟嗎?',
systemDriveWarning: '您將要格式化系統分割區',
systemDriveWarningMsg: '您確定要燒錄到系統分割區嗎?',
},
flash: {
another: '燒錄另一目標',
target: '目標',
location: '位置',
error: '錯誤',
flash: '燒錄',
flashNow: '現在燒錄!',
skip: '跳過了驗證',
moreInfo: '更多資訊',
speedTip:
'透過將映像檔大小除以燒錄時間來計算速度。\n由於我們能夠跳過未使用的部分因此具有 ext 分割區的磁碟映像檔燒錄速度更快。',
speed: '速度:{{speed}} MB/秒',
speedShort: '{{speed}} MB/秒',
eta: '預計還需要:{{eta}}',
failedTarget: '目標燒錄失敗',
failedRetry: '重試燒錄失敗的目標',
flashFailed: '燒錄失敗。',
flashCompleted: '燒錄成功!',
},
settings: {
errorReporting: '匿名向 balena.io 回報程式錯誤和使用統計資料',
autoUpdate: '自動更新',
settings: '軟體設定',
systemInformation: '系統資訊',
trimExtPartitions: '修改原始映像檔上未分配的空間(在 ext 類型分割區中)',
},
menu: {
edit: '編輯',
view: '預覽',
devTool: '打開開發者工具',
window: '視窗',
help: '協助',
pro: 'Etcher 專業版',
website: 'Etcher 的官網',
issue: '提交 issue',
about: '關於 Etcher',
hide: '隱藏 Etcher',
hideOthers: '隱藏其它視窗',
unhide: '取消隱藏',
quit: '結束 Etcher',
},
},
};
export default translation;

View File

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

View File

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

View File

@@ -14,9 +14,10 @@
* limitations under the License.
*/
import * as electron from 'electron';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import { DrivelistDrive } from '../../../shared/drive-constraints';
import { bytesToMegabytes } from '../../../shared/units';
import { Actions, store } from './store';
@@ -45,6 +46,14 @@ export function isFlashing(): boolean {
* start a flash process.
*/
export function setFlashingFlag() {
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
try {
electron.ipcRenderer.invoke('disable-screensaver');
} catch (error) {
console.log(
"Can't disable-screensaver, we're probably not running on a balena-electron env",
);
}
store.dispatch({
type: Actions.SET_FLASHING_FLAG,
data: {},
@@ -66,6 +75,8 @@ export function unsetFlashingFlag(results: {
type: Actions.UNSET_FLASHING_FLAG,
data: results,
});
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.invoke('enable-screensaver');
}
export function setDevicePaths(devicePaths: string[]) {
@@ -75,25 +86,29 @@ export function setDevicePaths(devicePaths: string[]) {
});
}
export function addFailedDevicePath({
export function addFailedDeviceError({
device,
error,
}: {
device: sdk.scanner.adapters.DrivelistDrive;
device: DrivelistDrive;
error: Error;
}) {
const failedDevicePathsMap = new Map(
store.getState().toJS().failedDevicePaths,
const failedDeviceErrorsMap = new Map(
store.getState().toJS().failedDeviceErrors,
);
failedDevicePathsMap.set(device.device, {
if (failedDeviceErrorsMap.has(device.device)) {
// Only store the first error
return;
}
failedDeviceErrorsMap.set(device.device, {
description: device.description,
device: device.device,
devicePath: device.devicePath,
...error,
});
store.dispatch({
type: Actions.SET_FAILED_DEVICE_PATHS,
data: Array.from(failedDevicePathsMap),
type: Actions.SET_FAILED_DEVICE_ERRORS,
data: Array.from(failedDeviceErrorsMap),
});
}

View File

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

View File

@@ -72,26 +72,6 @@ export function getImage(): SourceMetadata | undefined {
return store.getState().toJS().selection.image;
}
export function getImagePath(): string | undefined {
return store.getState().toJS().selection.image?.path;
}
export function getImageSize(): number | undefined {
return store.getState().toJS().selection.image?.size;
}
export function getImageName(): string | undefined {
return store.getState().toJS().selection.image?.name;
}
export function getImageLogo(): string | undefined {
return store.getState().toJS().selection.image?.logo;
}
export function getImageSupportUrl(): string | undefined {
return store.getState().toJS().selection.image?.supportUrl;
}
/**
* @summary Check if there is a selected drive
*/

View File

@@ -38,23 +38,19 @@ export const DEFAULT_HEIGHT = 480;
* - `~/Library/Application Support/etcher` on macOS
* See https://electronjs.org/docs/api/app#appgetpathname
*
* NOTE: The ternary is due to this module being loaded both,
* Electron's main process and renderer process
* NOTE: We use the remote property when this module
* is loaded in the Electron's renderer process
*/
const app = electron.app || electron.remote.app;
const USER_DATA_DIR = app.getPath('userData');
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
const DOWNLOADS_DIR = app.getPath('downloads');
function getConfigPath() {
const app = electron.app || require('@electron/remote').app;
return join(app.getPath('userData'), 'config.json');
}
async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
let contents = '{}';
try {
contents = await fs.readFile(filename, { encoding: 'utf8' });
} catch (error) {
} catch (error: any) {
// noop
}
try {
@@ -67,7 +63,7 @@ async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
// exported for tests
export async function readAll() {
return await readConfigFile(CONFIG_PATH);
return await readConfigFile(getConfigPath());
}
// exported for tests
@@ -80,14 +76,10 @@ export async function writeConfigFile(
const DEFAULT_SETTINGS: _.Dictionary<any> = {
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true,
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
desktopNotifications: true,
autoBlockmapping: true,
decompressFirst: true,
saveUrlImage: false,
saveUrlImageTo: DOWNLOADS_DIR,
};
const settings = _.cloneDeep(DEFAULT_SETTINGS);
@@ -110,8 +102,8 @@ export async function set(
const previousValue = settings[key];
settings[key] = value;
try {
await writeConfigFileFn(CONFIG_PATH, settings);
} catch (error) {
await writeConfigFileFn(getConfigPath(), settings);
} catch (error: any) {
// Revert to previous value if persisting settings failed
settings[key] = previousValue;
throw error;

View File

@@ -16,6 +16,7 @@
import * as Immutable from 'immutable';
import * as _ from 'lodash';
import { basename } from 'path';
import * as redux from 'redux';
import { v4 as uuidV4 } from 'uuid';
@@ -62,7 +63,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
},
isFlashing: false,
devicePaths: [],
failedDevicePaths: [],
failedDeviceErrors: [],
flashResults: {},
flashState: {
active: 0,
@@ -79,7 +80,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
*/
export enum Actions {
SET_DEVICE_PATHS,
SET_FAILED_DEVICE_PATHS,
SET_FAILED_DEVICE_ERRORS,
SET_AVAILABLE_TARGETS,
SET_FLASH_STATE,
RESET_FLASH_STATE,
@@ -133,11 +134,16 @@ function storeReducer(
});
}
// Drives order is a list of devicePaths
const drivesOrder = settings.getSync('drivesOrder') ?? [];
drives = _.sortBy(drives, [
// System drives last
(d) => !!d.isSystem,
// Devices with no devicePath first (usbboot)
(d) => !!d.devicePath,
// Sort as defined in the drivesOrder setting if there is one (only for Linux with udev)
(d) => drivesOrder.indexOf(basename(d.devicePath || '')),
// Then sort by devicePath (only available on Linux with udev) or device
(d) => d.devicePath || d.device,
]);
@@ -169,7 +175,7 @@ function storeReducer(
);
const shouldAutoselectAll = Boolean(
settings.getSync('disableExplicitDriveSelection'),
settings.getSync('autoSelectAllDrives'),
);
const AUTOSELECT_DRIVE_COUNT = 1;
const nonStaleSelectedDevices = nonStaleNewState
@@ -191,18 +197,13 @@ function storeReducer(
drives,
(accState, drive) => {
if (
_.every([
constraints.isDriveValid(drive, image),
constraints.isDriveSizeRecommended(drive, image),
// We don't want to auto-select large drives
!constraints.isDriveSizeLarge(drive),
// We don't want to auto-select system drives,
// even when "unsafe mode" is enabled
!constraints.isSystemDrive(drive),
]) ||
(shouldAutoselectAll && constraints.isDriveValid(drive, image))
constraints.isDriveValid(drive, image) &&
!drive.isReadOnly &&
constraints.isDriveSizeRecommended(drive, image) &&
// We don't want to auto-select large drives except if autoSelectAllDrives is true
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
// We don't want to auto-select system drives
!constraints.isSystemDrive(drive)
) {
// Auto-select this drive
return storeReducer(accState, {
@@ -269,7 +270,7 @@ function storeReducer(
.set('flashState', DEFAULT_STATE.get('flashState'))
.set('flashResults', DEFAULT_STATE.get('flashResults'))
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
.set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths'))
.set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
.set(
'lastAverageFlashingSpeed',
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
@@ -516,8 +517,8 @@ function storeReducer(
return state.set('devicePaths', action.data);
}
case Actions.SET_FAILED_DEVICE_PATHS: {
return state.set('failedDevicePaths', action.data);
case Actions.SET_FAILED_DEVICE_ERRORS: {
return state.set('failedDeviceErrors', action.data);
}
default: {

View File

@@ -15,84 +15,188 @@
*/
import * as _ from 'lodash';
import * as resinCorvus from 'resin-corvus/browser';
import * as packageJSON from '../../../../package.json';
import { getConfig } from '../../../shared/utils';
import { Client, createClient, createNoopClient } from 'analytics-client';
import * as SentryRenderer from '@sentry/electron/renderer';
import * as settings from '../models/settings';
import { store } from '../models/store';
import * as packageJSON from '../../../../package.json';
const DEFAULT_PROBABILITY = 0.1;
type AnalyticsPayload = _.Dictionary<any>;
async function installCorvus(): Promise<void> {
const sentryToken =
(await settings.get('analyticsSentryToken')) ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
const mixpanelToken =
(await settings.get('analyticsMixpanelToken')) ||
_.get(packageJSON, ['analytics', 'mixpanel', 'token']);
resinCorvus.install({
services: {
sentry: sentryToken,
mixpanel: mixpanelToken,
},
options: {
release: packageJSON.version,
shouldReport: () => {
return settings.getSync('errorReporting');
},
mixpanelDeferred: true,
},
const clearUserPath = (filename: string): string => {
const generatedFile = filename.split('generated').reverse()[0];
return generatedFile !== filename ? `generated${generatedFile}` : filename;
};
export const anonymizeSentryData = (
event: SentryRenderer.Event,
): SentryRenderer.Event => {
event.exception?.values?.forEach((exception) => {
exception.stacktrace?.frames?.forEach((frame) => {
if (frame.filename) {
frame.filename = clearUserPath(frame.filename);
}
});
});
}
let mixpanelSample = DEFAULT_PROBABILITY;
event.breadcrumbs?.forEach((breadcrumb) => {
if (breadcrumb.data?.url) {
breadcrumb.data.url = clearUserPath(breadcrumb.data.url);
}
});
if (event.request?.url) {
event.request.url = clearUserPath(event.request.url);
}
return event;
};
const extractPathRegex = /(.*)(^|\s)(file\:\/\/)?(\w\:)?([\\\/].+)/;
const etcherSegmentMarkers = ['app.asar', 'Resources'];
export const anonymizePath = (input: string) => {
// First, extract a part of the value that matches a path pattern.
const match = extractPathRegex.exec(input);
if (match === null) {
return input;
}
const mainPart = match[5];
const space = match[2];
const beginning = match[1];
const uriPrefix = match[3] || '';
// We have to deal with both Windows and POSIX here.
// The path starts with its separator (we work with absolute paths).
const sep = mainPart[0];
const segments = mainPart.split(sep);
// Moving from the end, find the first marker and cut the path from there.
const startCutIndex = _.findLastIndex(segments, (segment) =>
etcherSegmentMarkers.includes(segment),
);
return (
beginning +
space +
uriPrefix +
'[PERSONAL PATH]' +
sep +
segments.splice(startCutIndex).join(sep)
);
};
const safeAnonymizePath = (input: string) => {
try {
return anonymizePath(input);
} catch (e) {
return '[ANONYMIZE PATH FAILED]';
}
};
const sensitiveEtcherProperties = [
'error.description',
'error.message',
'error.stack',
'image',
'image.path',
'path',
];
export const anonymizeAnalyticsPayload = (
data: AnalyticsPayload,
): AnalyticsPayload => {
for (const prop of sensitiveEtcherProperties) {
const value = data[prop];
if (value != null) {
data[prop] = safeAnonymizePath(value.toString());
}
}
return data;
};
let analyticsClient: Client;
/**
* @summary Init analytics configurations
*/
async function initConfig() {
await installCorvus();
let validatedConfig = null;
try {
const configUrl = await settings.get('configUrl');
const config = await getConfig(configUrl);
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
if (isClientEligible(mixpanelSample)) {
validatedConfig = validateMixpanelConfig(mixpanel);
}
} catch (err) {
resinCorvus.logException(err);
}
resinCorvus.setConfigs({
mixpanel: validatedConfig,
});
}
export const initAnalytics = _.once(() => {
const dsn =
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryRenderer.init({ dsn, beforeSend: anonymizeSentryData });
initConfig();
const projectName =
settings.getSync('analyticsAmplitudeToken') ||
_.get(packageJSON, ['analytics', 'amplitude', 'token']);
/**
* @summary Check that the client is eligible for analytics
*/
function isClientEligible(probability: number) {
return Math.random() < probability;
}
/**
* @summary Check that config has at least HTTP_PROTOCOL and api_host
*/
function validateMixpanelConfig(config: {
api_host?: string;
HTTP_PROTOCOL?: string;
}) {
const mixpanelConfig = {
api_host: 'https://api.mixpanel.com',
const clientConfig = {
projectName,
endpoint: 'data.balena-cloud.com',
componentName: 'etcher',
componentVersion: packageJSON.version,
};
if (config.HTTP_PROTOCOL !== undefined && config.api_host !== undefined) {
mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`;
analyticsClient = projectName
? createClient(clientConfig)
: createNoopClient();
});
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key: any, value: any) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
function flattenObject(obj: any) {
const toReturn: AnalyticsPayload = {};
for (const i in obj) {
if (!obj.hasOwnProperty(i)) {
continue;
}
if (Array.isArray(obj[i])) {
toReturn[i] = obj[i];
continue;
}
if (typeof obj[i] === 'object' && obj[i] !== null) {
const flatObject = flattenObject(obj[i]);
for (const x in flatObject) {
if (!flatObject.hasOwnProperty(x)) {
continue;
}
toReturn[i.toLowerCase() + '.' + x.toLowerCase()] = flatObject[x];
}
} else {
toReturn[i] = obj[i];
}
}
return mixpanelConfig;
return toReturn;
}
function formatEvent(data: any): AnalyticsPayload {
const event = JSON.parse(JSON.stringify(data, getCircularReplacer()));
return anonymizeAnalyticsPayload(flattenObject(event));
}
function reportAnalytics(message: string, data: AnalyticsPayload = {}) {
const { applicationSessionUuid, flashingWorkflowUuid } = store
.getState()
.toJS();
const event = formatEvent({
...data,
applicationSessionUuid,
flashingWorkflowUuid,
});
analyticsClient.track(message, event);
}
/**
@@ -101,17 +205,12 @@ function validateMixpanelConfig(config: {
* @description
* This function sends the debug message to product analytics services.
*/
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
const {
applicationSessionUuid,
flashingWorkflowUuid,
} = store.getState().toJS();
resinCorvus.logEvent(message, {
...data,
sample: mixpanelSample,
applicationSessionUuid,
flashingWorkflowUuid,
});
export async function logEvent(message: string, data: AnalyticsPayload = {}) {
const shouldReportAnalytics = await settings.get('errorReporting');
if (shouldReportAnalytics) {
initAnalytics();
reportAnalytics(message, data);
}
}
/**
@@ -120,4 +219,11 @@ export function logEvent(message: string, data: _.Dictionary<any> = {}) {
* @description
* This function logs an exception to error reporting services.
*/
export const logException = resinCorvus.logException;
export function logException(error: any) {
const shouldReportErrors = settings.getSync('errorReporting');
console.error(error);
if (shouldReportErrors) {
initAnalytics();
SentryRenderer.captureException(error);
}
}

184
lib/gui/app/modules/api.ts Normal file
View File

@@ -0,0 +1,184 @@
/** This function will :
* - start the ipc server (api)
* - spawn the child process (privileged or not)
* - wait for the child process to connect to the api
* - return a promise that will resolve with the emit function for the api
*
* //TODO:
* - this should be refactored to reverse the control flow:
* - the child process should be the server
* - this should be the client
* - replace the current node-ipc api with a websocket api
* - centralise the api for both the writer and the scanner instead of having two instances running
*/
import * as ipc from 'node-ipc';
import { spawn } from 'child_process';
import * as os from 'os';
import * as path from 'path';
import * as packageJSON from '../../../../package.json';
import * as permissions from '../../../shared/permissions';
import { getAppPath } from '../../../shared/get-app-path';
import * as errors from '../../../shared/errors';
const THREADS_PER_CPU = 16;
// NOTE: Ensure this isn't disabled, as it will cause
// the stdout maxBuffer size to be exceeded when flashing
ipc.config.silent = true;
function writerArgv(): string[] {
let entryPoint = path.join(getAppPath(), 'generated', 'etcher-util');
// AppImages run over FUSE, so the files inside the mount point
// can only be accessed by the user that mounted the AppImage.
// This means we can't re-spawn Etcher as root from the same
// mount-point, and as a workaround, we re-mount the original
// AppImage as root.
if (os.platform() === 'linux' && process.env.APPIMAGE && process.env.APPDIR) {
entryPoint = entryPoint.replace(process.env.APPDIR, '');
return [
process.env.APPIMAGE,
'-e',
`require(\`\${process.env.APPDIR}${entryPoint}\`)`,
];
} else {
return [entryPoint];
}
}
function writerEnv(
IPC_CLIENT_ID: string,
IPC_SERVER_ID: string,
IPC_SOCKET_ROOT: string,
) {
return {
IPC_SERVER_ID,
IPC_CLIENT_ID,
IPC_SOCKET_ROOT,
UV_THREADPOOL_SIZE: (os.cpus().length * THREADS_PER_CPU).toString(),
// This environment variable prevents the AppImages
// desktop integration script from presenting the
// "installation" dialog
SKIP: '1',
...(process.platform === 'win32' ? {} : process.env),
};
}
async function spawnChild({
withPrivileges,
IPC_CLIENT_ID,
IPC_SERVER_ID,
IPC_SOCKET_ROOT,
}: {
withPrivileges: boolean;
IPC_CLIENT_ID: string;
IPC_SERVER_ID: string;
IPC_SOCKET_ROOT: string;
}) {
const argv = writerArgv();
const env = writerEnv(IPC_CLIENT_ID, IPC_SERVER_ID, IPC_SOCKET_ROOT);
if (withPrivileges) {
return await permissions.elevateCommand(argv, {
applicationName: packageJSON.displayName,
environment: env,
});
} else {
const process = await spawn(argv[0], argv.slice(1), {
env,
});
return { cancelled: false, process };
}
}
function terminateServer(server: any) {
// Turns out we need to destroy all sockets for
// the server to actually close. Otherwise, it
// just stops receiving any further connections,
// but remains open if there are active ones.
// @ts-ignore (no Server.sockets in @types/node-ipc)
for (const socket of server.sockets) {
socket.destroy();
}
server.stop();
}
// TODO: replace the custom ipc events by one generic "message" for all communication with the backend
function startApiAndSpawnChild({
withPrivileges,
}: {
withPrivileges: boolean;
}): Promise<any> {
// There might be multiple Etcher instances running at
// the same time, also we might spawn multiple child and api so we must ensure each IPC
// server/client has a different name.
const IPC_SERVER_ID = `etcher-server-${process.pid}-${Date.now()}-${
withPrivileges ? 'privileged' : 'unprivileged'
}}}`;
const IPC_CLIENT_ID = `etcher-client-${process.pid}-${Date.now()}-${
withPrivileges ? 'privileged' : 'unprivileged'
}}`;
const IPC_SOCKET_ROOT = path.join(
process.env.XDG_RUNTIME_DIR || os.tmpdir(),
path.sep,
);
ipc.config.id = IPC_SERVER_ID;
ipc.config.socketRoot = IPC_SOCKET_ROOT;
return new Promise((resolve, reject) => {
ipc.serve();
// log is special message which brings back the logs from the child process and prints them to the console
ipc.server.on('log', (message: string) => {
console.log(message);
});
// api to register more handlers with callbacks
const registerHandler = (event: string, handler: any) => {
ipc.server.on(event, handler);
};
// once api is ready (means child process is connected) we pass the emit and terminate function to the caller
ipc.server.on('ready', (_: any, socket) => {
const emit = (channel: string, data: any) => {
ipc.server.emit(socket, channel, data);
};
resolve({
emit,
terminateServer: () => terminateServer(ipc.server),
registerHandler,
});
});
// on api error we terminate
ipc.server.on('error', (error: any) => {
terminateServer(ipc.server);
const errorObject = errors.fromJSON(error);
reject(errorObject);
});
// when the api is started we spawn the child process
ipc.server.on('start', async () => {
try {
const results = await spawnChild({
withPrivileges,
IPC_CLIENT_ID,
IPC_SERVER_ID,
IPC_SOCKET_ROOT,
});
// this will happen if the child is spawned withPrivileges and privileges has been rejected
if (results.cancelled) {
reject();
}
} catch (error) {
reject(error);
}
});
// start the server
ipc.server.start();
});
}
export { startApiAndSpawnChild };

View File

@@ -15,40 +15,17 @@
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as electron from 'electron';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import * as ipc from 'node-ipc';
import * as os from 'os';
import * as path from 'path';
import * as packageJSON from '../../../../package.json';
import { Dictionary } from 'lodash';
import * as errors from '../../../shared/errors';
import * as permissions from '../../../shared/permissions';
import { SourceMetadata } from '../components/source-selector/source-selector';
import { SourceMetadata } from '../../../shared/typings/source-selector';
import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state';
import * as settings from '../models/settings';
import * as analytics from '../modules/analytics';
import * as windowProgress from '../os/window-progress';
const THREADS_PER_CPU = 16;
// There might be multiple Etcher instances running at
// the same time, therefore we must ensure each IPC
// server/client has a different name.
const IPC_SERVER_ID = `etcher-server-${process.pid}`;
const IPC_CLIENT_ID = `etcher-client-${process.pid}`;
ipc.config.id = IPC_SERVER_ID;
ipc.config.socketRoot = path.join(
process.env.XDG_RUNTIME_DIR || os.tmpdir(),
path.sep,
);
// NOTE: Ensure this isn't disabled, as it will cause
// the stdout maxBuffer size to be exceeded when flashing
ipc.config.silent = true;
import { startApiAndSpawnChild } from './api';
import { terminateScanningServer } from '../app';
/**
* @summary Handle a flash error and log it to analytics
@@ -80,59 +57,19 @@ function handleErrorLogging(
}
}
function terminateServer() {
// Turns out we need to destroy all sockets for
// the server to actually close. Otherwise, it
// just stops receiving any further connections,
// but remains open if there are active ones.
// @ts-ignore (no Server.sockets in @types/node-ipc)
for (const socket of ipc.server.sockets) {
socket.destroy();
}
ipc.server.stop();
}
function writerArgv(): string[] {
let entryPoint = path.join(
electron.remote.app.getAppPath(),
'generated',
'child-writer.js',
);
// AppImages run over FUSE, so the files inside the mount point
// can only be accessed by the user that mounted the AppImage.
// This means we can't re-spawn Etcher as root from the same
// mount-point, and as a workaround, we re-mount the original
// AppImage as root.
if (os.platform() === 'linux' && process.env.APPIMAGE && process.env.APPDIR) {
entryPoint = entryPoint.replace(process.env.APPDIR, '');
return [
process.env.APPIMAGE,
'-e',
`require(\`\${process.env.APPDIR}${entryPoint}\`)`,
];
} else {
return [process.argv[0], entryPoint];
}
}
function writerEnv() {
return {
IPC_SERVER_ID,
IPC_CLIENT_ID,
IPC_SOCKET_ROOT: ipc.config.socketRoot,
ELECTRON_RUN_AS_NODE: '1',
UV_THREADPOOL_SIZE: (os.cpus().length * THREADS_PER_CPU).toString(),
// This environment variable prevents the AppImages
// desktop integration script from presenting the
// "installation" dialog
SKIP: '1',
...(process.platform === 'win32' ? {} : process.env),
};
}
let cancelEmitter: (type: string) => void | undefined;
interface FlashResults {
skip?: boolean;
cancelled?: boolean;
results?: {
bytesWritten: number;
devices: {
failed: number;
successful: number;
};
errors: Error[];
};
}
async function performWrite(
@@ -140,110 +77,66 @@ async function performWrite(
drives: DrivelistDrive[],
onProgress: sdk.multiWrite.OnProgressFunction,
): Promise<{ cancelled?: boolean }> {
let cancelled = false;
let skip = false;
ipc.serve();
const {
unmountOnSuccess,
validateWriteOnSuccess,
autoBlockmapping,
decompressFirst,
saveUrlImage,
saveUrlImageTo,
} = await settings.getAll();
return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => {
terminateServer();
const errorObject = errors.fromJSON(error);
reject(errorObject);
});
const { autoBlockmapping, decompressFirst } = await settings.getAll();
ipc.server.on('log', (message) => {
console.log(message);
});
console.log({ image, drives });
return await new Promise(async (resolve, reject) => {
const flashResults: FlashResults = {};
const analyticsData = {
image,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess,
validateWriteOnSuccess,
};
ipc.server.on('fail', ({ device, error }) => {
const onFail = ({ device, error }) => {
console.log('fail event');
console.log(device);
console.log(error);
if (device.devicePath) {
flashState.addFailedDevicePath({ device, error });
flashState.addFailedDeviceError({ device, error });
}
handleErrorLogging(error, analyticsData);
});
finish();
};
ipc.server.on('done', (event) => {
event.results.errors = _.map(event.results.errors, (data) => {
return errors.fromJSON(data);
});
_.merge(flashResults, event);
});
const onDone = (event) => {
console.log('done event');
event.results.errors = event.results.errors.map(
(data: Dictionary<any> & { message: string }) => {
return errors.fromJSON(data);
},
);
flashResults.results = event.results;
finish();
};
ipc.server.on('abort', () => {
terminateServer();
cancelled = true;
});
const onAbort = () => {
console.log('abort event');
flashResults.cancelled = true;
finish();
};
ipc.server.on('skip', () => {
terminateServer();
skip = true;
});
const onSkip = () => {
console.log('skip event');
flashResults.skip = true;
finish();
};
ipc.server.on('state', onProgress);
ipc.server.on('ready', (_data, socket) => {
ipc.server.emit(socket, 'write', {
image,
destinations: drives,
SourceType: image.SourceType.name,
validateWriteOnSuccess,
autoBlockmapping,
unmountOnSuccess,
decompressFirst,
saveUrlImage,
saveUrlImageTo,
});
});
const argv = writerArgv();
ipc.server.on('start', async () => {
console.log(`Elevating command: ${_.join(argv, ' ')}`);
const env = writerEnv();
try {
const results = await permissions.elevateCommand(argv, {
applicationName: packageJSON.displayName,
environment: env,
});
flashResults.cancelled = cancelled || results.cancelled;
flashResults.skip = skip;
} catch (error) {
// This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137;
if (error.code === SIGKILL_EXIT_CODE) {
error.code = 'ECHILDDIED';
}
reject(error);
} finally {
console.log('Terminating IPC server');
terminateServer();
}
const finish = () => {
console.log('Flash results', flashResults);
// This likely means the child died halfway through
// The flash wasn't cancelled and we didn't get a 'done' event
// Catch unexepected situation
if (
!flashResults.cancelled &&
!flashResults.skip &&
!_.get(flashResults, ['results', 'bytesWritten'])
flashResults.results === undefined
) {
console.log(flashResults);
reject(
errors.createUserError({
title: 'The writer process ended unexpectedly',
@@ -251,15 +144,40 @@ async function performWrite(
'Please try again, and contact the Etcher team if the problem persists',
}),
);
return;
}
resolve(flashResults);
});
// Clear the update lock timer to prevent longer
// flashing timing it out, and releasing the lock
ipc.server.start();
console.log('Terminating IPC server');
terminateServer();
resolve(flashResults);
};
// Spawn the child process with privileges and wait for the connection to be made
const { emit, registerHandler, terminateServer } =
await startApiAndSpawnChild({
withPrivileges: true,
});
registerHandler('state', onProgress);
registerHandler('fail', onFail);
registerHandler('done', onDone);
registerHandler('abort', onAbort);
registerHandler('skip', onSkip);
cancelEmitter = (cancelStatus: string) => emit(cancelStatus);
// Now that we know we're connected we can instruct the child process to start the write
const paramaters = {
image,
destinations: drives,
SourceType: image.SourceType,
autoBlockmapping,
decompressFirst,
};
console.log('params', paramaters);
emit('write', paramaters);
});
// The process continue in the event handler
}
/**
@@ -275,7 +193,8 @@ export async function flash(
throw new Error('There is already a flash in progress');
}
flashState.setFlashingFlag();
await flashState.setFlashingFlag();
flashState.setDevicePaths(
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
);
@@ -287,19 +206,24 @@ export async function flash(
uuid: flashState.getFlashUuid(),
status: 'started',
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
};
analytics.logEvent('Flash', analyticsData);
// start api and call the flasher
try {
const result = await write(image, drives, flashState.setProgressState);
flashState.unsetFlashingFlag(result);
} catch (error) {
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
await flashState.unsetFlashingFlag(result);
} catch (error: any) {
await flashState.unsetFlashingFlag({
cancelled: false,
errorCode: error.code,
});
windowProgress.clear();
const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
@@ -310,7 +234,9 @@ export async function flash(
analytics.logEvent('Write failed', eventData);
throw error;
}
windowProgress.clear();
if (flashState.wasLastFlashCancelled()) {
const eventData = {
...analyticsData,
@@ -333,31 +259,22 @@ export async function flash(
/**
* @summary Cancel write operation
* //TODO: find a better solution to handle cancellation
*/
export async function cancel(type: string) {
const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices();
const analyticsData = {
image: selectionState.getImagePath(),
image: selectionState.getImage()?.path,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
status,
};
analytics.logEvent('Cancel', analyticsData);
// Re-enable lock release on inactivity
try {
// @ts-ignore (no Server.sockets in @types/node-ipc)
const [socket] = ipc.server.sockets;
if (socket !== undefined) {
ipc.server.emit(socket, status);
}
} catch (error) {
analytics.logException(error);
if (cancelEmitter) {
cancelEmitter(status);
}
}

View File

@@ -15,6 +15,7 @@
*/
import * as prettyBytes from 'pretty-bytes';
import * as i18next from 'i18next';
export interface FlashState {
active: number;
@@ -22,7 +23,7 @@ export interface FlashState {
percentage?: number;
speed: number;
position: number;
type?: 'decompressing' | 'flashing' | 'verifying' | 'downloading';
type?: 'decompressing' | 'flashing' | 'verifying';
}
export function fromFlashState({
@@ -34,42 +35,45 @@ export function fromFlashState({
position?: string;
} {
if (type === undefined) {
return { status: 'Starting...' };
return { status: i18next.t('progress.starting') };
} else if (type === 'decompressing') {
if (percentage == null) {
return { status: 'Decompressing...' };
return { status: i18next.t('progress.decompressing') };
} else {
return { position: `${percentage}%`, status: 'Decompressing...' };
return {
position: `${percentage}%`,
status: i18next.t('progress.decompressing'),
};
}
} else if (type === 'flashing') {
if (percentage != null) {
if (percentage < 100) {
return { position: `${percentage}%`, status: 'Flashing...' };
return {
position: `${percentage}%`,
status: i18next.t('progress.flashing'),
};
} else {
return { status: 'Finishing...' };
return { status: i18next.t('progress.finishing') };
}
} else {
return {
status: 'Flashing...',
status: i18next.t('progress.flashing'),
position: `${position ? prettyBytes(position) : ''}`,
};
}
} else if (type === 'verifying') {
if (percentage == null) {
return { status: 'Validating...' };
return { status: i18next.t('progress.verifying') };
} else if (percentage < 100) {
return { position: `${percentage}%`, status: 'Validating...' };
return {
position: `${percentage}%`,
status: i18next.t('progress.verifying'),
};
} else {
return { status: 'Finishing...' };
}
} else if (type === 'downloading') {
if (percentage == null) {
return { status: 'Downloading...' };
} else if (percentage < 100) {
return { position: `${percentage}%`, status: 'Downloading...' };
return { status: i18next.t('progress.finishing') };
}
}
return { status: 'Failed' };
return { status: i18next.t('progress.failing') };
}
export function titleFromFlashState(

View File

@@ -15,11 +15,13 @@
*/
import * as electron from 'electron';
import * as remote from '@electron/remote';
import * as _ from 'lodash';
import * as errors from '../../../shared/errors';
import * as settings from '../../../gui/app/models/settings';
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
import * as i18next from 'i18next';
async function mountSourceDrive() {
// sourceDrivePath is the name of the link in /dev/disk/by-path
@@ -27,7 +29,7 @@ async function mountSourceDrive() {
if (sourceDrivePath) {
try {
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
} catch (error) {
} catch (error: any) {
// noop
}
}
@@ -40,12 +42,6 @@ async function mountSourceDrive() {
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*/
export async function selectImage(): Promise<string | undefined> {
return await openDialog();
}
export async function openDialog(
type: 'openFile' | 'openDirectory' = 'openFile',
) {
await mountSourceDrive();
const options: electron.OpenDialogOptions = {
// This variable is set when running in GNU/Linux from
@@ -56,26 +52,22 @@ export async function openDialog(
//
// See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7
defaultPath: process.env.OWD,
properties: [type, 'treatPackageAsDirectory'],
filters:
type === 'openFile'
? [
{
name: 'OS Images',
extensions: SUPPORTED_EXTENSIONS,
},
{
name: 'All',
extensions: ['*'],
},
]
: undefined,
properties: ['openFile', 'treatPackageAsDirectory'],
filters: [
{
name: i18next.t('source.osImages'),
extensions: SUPPORTED_EXTENSIONS,
},
{
name: i18next.t('source.allFiles'),
extensions: ['*'],
},
],
};
const currentWindow = electron.remote.getCurrentWindow();
const [path] = (
await electron.remote.dialog.showOpenDialog(currentWindow, options)
).filePaths;
return path;
const currentWindow = remote.getCurrentWindow();
const [file] = (await remote.dialog.showOpenDialog(currentWindow, options))
.filePaths;
return file;
}
/**
@@ -88,8 +80,8 @@ export async function showWarning(options: {
description: string;
}): Promise<boolean> {
_.defaults(options, {
confirmationLabel: 'OK',
rejectionLabel: 'Cancel',
confirmationLabel: i18next.t('ok'),
rejectionLabel: i18next.t('cancel'),
});
const BUTTONS = [options.confirmationLabel, options.rejectionLabel];
@@ -100,14 +92,14 @@ export async function showWarning(options: {
);
const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel);
const { response } = await electron.remote.dialog.showMessageBox(
electron.remote.getCurrentWindow(),
const { response } = await remote.dialog.showMessageBox(
remote.getCurrentWindow(),
{
type: 'warning',
buttons: BUTTONS,
defaultId: BUTTON_REJECTION_INDEX,
cancelId: BUTTON_REJECTION_INDEX,
title: 'Attention',
title: i18next.t('attention'),
message: options.title,
detail: options.description,
},
@@ -121,5 +113,5 @@ export async function showWarning(options: {
export function showError(error: Error) {
const title = errors.getTitle(error);
const message = errors.getDescription(error);
electron.remote.dialog.showErrorBox(title, message);
remote.dialog.showErrorBox(title, message);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import * as electron from 'electron';
import * as remote from '@electron/remote';
import * as settings from '../models/settings';
@@ -28,8 +28,8 @@ export async function send(title: string, body: string, icon: string) {
}
// `app.dock` is only defined in OS X
if (electron.remote.app.dock) {
electron.remote.app.dock.bounce();
if (remote.app.dock) {
remote.app.dock.bounce();
}
return new window.Notification(title, { body, icon });

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import * as electron from 'electron';
import * as remote from '@electron/remote';
import { percentageToFloat } from '../../../shared/utils';
import { FlashState, titleFromFlashState } from '../modules/progress-status';
@@ -40,7 +40,7 @@ function getWindowTitle(state?: FlashState) {
* @description
* We expose this property to `this` for testability purposes.
*/
export const currentWindow = electron.remote.getCurrentWindow();
export const currentWindow = remote.getCurrentWindow();
/**
* @summary Set operating system window progress

View File

@@ -15,6 +15,7 @@
*/
import { exec } from 'child_process';
import { withTmpFile } from 'etcher-sdk/build/tmp';
import { readFile } from 'fs';
import { chain, trim } from 'lodash';
import { platform } from 'os';
@@ -22,8 +23,6 @@ import { join } from 'path';
import { env } from 'process';
import { promisify } from 'util';
import { withTmpFile } from '../../../shared/tmp';
const readFileAsync = promisify(readFile);
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.
const options = {
// 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 ("-")
prefix: 'tmp',
};
return withTmpFile(options, async (path) => {
return withTmpFile(options, async ({ path }) => {
const command = [
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
'path',

View File

@@ -27,7 +27,6 @@ import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state';
import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer';
import * as notification from '../../os/notification';
import {
@@ -37,6 +36,7 @@ import {
import FlashSvg from '../../../assets/flash.svg';
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
import * as i18next from 'i18next';
const COMPLETED_PERCENTAGE = 100;
const SPEED_PRECISION = 2;
@@ -59,6 +59,27 @@ const getErrorMessageFromCode = (errorCode: string) => {
return '';
};
function notifySuccess(
iconPath: string,
basename: string,
drives: any,
devices: { successful: number; failed: number },
) {
notification.send(
'Flash complete!',
messages.info.flashComplete(basename, drives, devices),
iconPath,
);
}
function notifyFailure(iconPath: string, basename: string, drives: any) {
notification.send(
'Oops! Looks like the flash failed.',
messages.error.flashFailure(basename, drives),
iconPath,
);
}
async function flashImageToDrive(
isFlashing: boolean,
goToSuccess: () => void,
@@ -73,10 +94,6 @@ async function flashImageToDrive(
return '';
}
// Stop scanning drives when flashing
// otherwise Windows throws EPERM
driveScanner.stop();
const iconPath = path.join('media', 'icon.png');
const basename = path.basename(image.path);
try {
@@ -84,20 +101,20 @@ async function flashImageToDrive(
if (!flashState.wasLastFlashCancelled()) {
const {
results = { devices: { successful: 0, failed: 0 } },
skip,
cancelled,
} = flashState.getFlashResults();
notification.send(
'Flash complete!',
messages.info.flashComplete(basename, drives as any, results.devices),
iconPath,
);
if (!skip && !cancelled) {
if (results?.devices?.successful > 0) {
notifySuccess(iconPath, basename, drives, results.devices);
} else {
notifyFailure(iconPath, basename, drives);
}
}
goToSuccess();
}
} catch (error) {
notification.send(
'Oops! Looks like the flash failed.',
messages.error.flashFailure(path.basename(image.path), drives),
iconPath,
);
} catch (error: any) {
notifyFailure(iconPath, basename, drives);
let errorMessage = getErrorMessageFromCode(error.code);
if (!errorMessage) {
error.image = basename;
@@ -107,7 +124,6 @@ async function flashImageToDrive(
return errorMessage;
} finally {
availableDrives.setDrives([]);
driveScanner.start();
}
return '';
@@ -135,6 +151,7 @@ interface FlashStepProps {
failed: number;
speed?: number;
eta?: number;
width: string;
}
export interface DriveWithWarnings extends constraints.DrivelistDrive {
@@ -199,7 +216,11 @@ export class FlashStep extends React.PureComponent<
const drives = selection.getSelectedDrives().map((drive) => {
return {
...drive,
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
statuses: constraints.getDriveImageCompatibilityStatuses(
drive,
undefined,
true,
),
};
});
if (drives.length === 0 || this.props.isFlashing) {
@@ -237,6 +258,7 @@ export class FlashStep extends React.PureComponent<
<Flex
flexDirection="column"
alignItems="start"
width={this.props.width}
style={this.props.style}
>
<FlashSvg
@@ -266,9 +288,17 @@ export class FlashStep extends React.PureComponent<
color="#7e8085"
width="100%"
>
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
<Txt>
{i18next.t('flash.speedShort', {
speed: this.props.speed.toFixed(SPEED_PRECISION),
})}
</Txt>
{!_.isNil(this.props.eta) && (
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
<Txt>
{i18next.t('flash.eta', {
eta: formatSeconds(this.props.eta),
})}
</Txt>
)}
</Flex>
)}
@@ -308,6 +338,7 @@ export class FlashStep extends React.PureComponent<
)}
{this.state.showDriveSelectorModal && (
<TargetSelectorModal
write={true}
cancel={() => this.setState({ showDriveSelectorModal: false })}
done={(modalTargets) => {
selectAllTargets(modalTargets);

View File

@@ -26,10 +26,8 @@ import styled from 'styled-components';
import FinishPage from '../../components/finish/finish';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
import { SettingsModal } from '../../components/settings/settings';
import {
SourceMetadata,
SourceSelector,
} from '../../components/source-selector/source-selector';
import { SourceSelector } from '../../components/source-selector/source-selector';
import { SourceMetadata } from '../../../../shared/typings/source-selector';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
@@ -48,7 +46,6 @@ import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { colors } from '../../theme';
const Icon = styled(BaseIcon)`
margin-right: 20px;
@@ -88,7 +85,9 @@ const StepBorder = styled.div<{
position: relative;
height: 2px;
background-color: ${(props) =>
props.disabled ? colors.dark.disabled.foreground : colors.dark.foreground};
props.disabled
? props.theme.colors.dark.disabled.foreground
: props.theme.colors.dark.foreground};
width: 120px;
top: 19px;
@@ -131,12 +130,13 @@ export class MainPage extends React.Component<
}
private stateHelper(): MainPageStateFromStore {
const image = selectionState.getImage();
return {
isFlashing: flashState.isFlashing(),
hasImage: selectionState.hasImage(),
hasDrive: selectionState.hasDrive(),
imageLogo: selectionState.getImageLogo(),
imageSize: selectionState.getImageSize(),
imageLogo: image?.logo,
imageSize: image?.size,
imageName: getImageBasename(selectionState.getImage()),
driveTitle: getDrivesTitle(),
driveLabel: getDriveListLabel(),
@@ -146,7 +146,7 @@ export class MainPage extends React.Component<
private async getFeaturedProjectURL() {
const url = new URL(
(await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html',
'https://efp.balena.io/index.html',
);
url.searchParams.append('borderRight', 'false');
url.searchParams.append('darkBackground', 'true');
@@ -237,6 +237,7 @@ export class MainPage extends React.Component<
)}
<FlashStep
width={this.state.isWebviewShowing ? '220px' : '200px'}
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
@@ -273,9 +274,9 @@ export class MainPage extends React.Component<
style={{
// Allow window to be dragged from header
// @ts-ignore
'-webkit-app-region': 'drag',
WebkitAppRegion: 'drag',
position: 'relative',
zIndex: 1,
zIndex: 2,
}}
>
<Flex width="100%" />
@@ -301,7 +302,7 @@ export class MainPage extends React.Component<
onClick={() => this.setState({ hideSettings: false })}
style={{
// Make touch events click instead of dragging
'-webkit-app-region': 'no-drag',
WebkitAppRegion: 'no-drag',
}}
/>
{!settings.getSync('disableExternalLinks') && (
@@ -309,14 +310,14 @@ export class MainPage extends React.Component<
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
onClick={() =>
openExternal(
selectionState.getImageSupportUrl() ||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
selectionState.getImage()?.supportUrl ||
'https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md',
)
}
tabIndex={6}
style={{
// Make touch events click instead of dragging
'-webkit-app-region': 'no-drag',
WebkitAppRegion: 'no-drag',
}}
/>
)}

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

@@ -0,0 +1,15 @@
// @ts-nocheck
import { main } from './app';
import './i18n';
import { langParser } from './i18n';
import { ipcRenderer } from 'electron';
ipcRenderer.send('change-lng', langParser());
if (module.hot) {
module.hot.accept('./app', () => {
main();
});
}
main();

View File

@@ -126,47 +126,30 @@ const modalFooterShadowCss = css`
export const Modal = styled(({ style, children, ...props }) => {
return (
<Provider
theme={_.merge({}, theme, {
header: {
height: '50px',
<ModalBase
position="top"
width="97vw"
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
layer: {
extend: () => `
${theme.layer.extend()}
> div:last-child {
top: 0;
}
`,
},
})}
}}
style={{
height: '87.5vh',
...style,
}}
{...props}
>
<ModalBase
position="top"
width="97vw"
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
}}
style={{
height: '87.5vh',
...style,
}}
{...props}
>
<ScrollableFlex flexDirection="column" width="100%" height="90%">
{...children}
</ScrollableFlex>
</ModalBase>
</Provider>
<ScrollableFlex flexDirection="column" width="100%" height="90%">
{children.length ? children.map((c: any) => <>{c}</>) : children}
</ScrollableFlex>
</ModalBase>
);
})`
> div {
padding: 0;
height: 100%;
height: 99%;
> div:first-child {
height: 81%;

View File

@@ -71,7 +71,11 @@ export const colors = {
const font = 'SourceSansPro';
export const theme = _.merge({}, Theme, {
colors,
font,
header: {
height: '40px',
},
global: {
font: {
family: font,
@@ -96,6 +100,7 @@ export const theme = _.merge({}, Theme, {
font-size: 16px;
&& {
width: 200px;
height: 48px;
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2022 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Dictionary } from 'lodash';
type BalenaTag = {
id: number;
name: string;
value: string;
};
export class EtcherPro {
private supervisorAddr: string;
private supervisorKey: string;
private tags: Dictionary<string> | undefined;
public uuid: string;
constructor(supervisorAddr: string, supervisorKey: string) {
this.supervisorAddr = supervisorAddr;
this.supervisorKey = supervisorKey;
this.uuid = (process.env.BALENA_DEVICE_UUID ?? 'NO-UUID').substring(0, 7);
this.tags = undefined;
this.get_tags().then((tags) => (this.tags = tags));
}
async get_tags(): Promise<Dictionary<string>> {
const result = await fetch(
this.supervisorAddr + '/v2/device/tags?apikey=' + this.supervisorKey,
);
const parsed = await result.json();
if (parsed['status'] === 'success') {
return Object.assign(
{},
...parsed['tags'].map((tag: BalenaTag) => {
return { [tag.name]: tag.value };
}),
);
} else {
return {};
}
}
public get_serial(): string | undefined {
if (this.tags) {
return this.tags['Serial'];
} else {
return undefined;
}
}
}
export function etcherProInfo(): EtcherPro | undefined {
const BALENA_SUPERVISOR_ADDRESS = process.env.BALENA_SUPERVISOR_ADDRESS;
const BALENA_SUPERVISOR_API_KEY = process.env.BALENA_SUPERVISOR_API_KEY;
if (BALENA_SUPERVISOR_ADDRESS && BALENA_SUPERVISOR_API_KEY) {
return new EtcherPro(BALENA_SUPERVISOR_ADDRESS, BALENA_SUPERVISOR_API_KEY);
}
return undefined;
}

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2020 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @summary Truncate text from the start with an ellipsis
*/
export function startEllipsis(input: string, limit: number): string {
// Do nothing, the string doesn't need truncation.
if (input.length <= limit) {
return input;
}
const lastPart = input.slice(input.length - limit, input.length);
return `${lastPart}`;
}

18
lib/gui/assets/src.svg Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 39 90" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g transform="translate(-380 -166)">
<g transform="translate(380 166)">
<path d="m30.88 39.87h-23.363v23.209c0 0.6909 0.56062 1.251 1.251 1.251h20.861c0.69114 0 1.251-0.55986 1.251-1.251v-23.209zm-22.363 0.9999h21.363l4e-4 22.209c0 0.13886-0.11214 0.251-0.251 0.251h-20.861l-0.057452-0.0066403c-0.11075-0.026055-0.19355-0.12572-0.19355-0.24436l-4e-4 -22.209z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m16.558 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m25.97 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m37.398 5.418v30.534c0 2.43-1.988 4.418-4.418 4.418h-27.562c-2.43 0-4.418-1.988-4.418-4.418v-30.534c0-2.43 1.988-4.418 4.418-4.418h27.562c2.43 0 4.418 1.988 4.418 4.418" fill="#2A506F"/>
<path d="m32.98-5.6843e-14h-27.562c-2.9823 0-5.418 2.4357-5.418 5.418v30.534c0 2.9823 2.4357 5.418 5.418 5.418h27.562c2.9823 0 5.418-2.4357 5.418-5.418v-30.534c0-2.9823-2.4357-5.418-5.418-5.418zm-27.562 2h27.562c1.8777 0 3.418 1.5403 3.418 3.418v30.534c0 1.8777-1.5403 3.418-3.418 3.418h-27.562c-1.8777 0-3.418-1.5403-3.418-3.418v-30.534c0-1.8777 1.5403-3.418 3.418-3.418z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m19.147 73.551c0.24546 0 0.44961 0.17688 0.49194 0.41012l0.0080557 0.089876v14.882c0 0.27614-0.22386 0.5-0.5 0.5-0.24546 0-0.44961-0.17688-0.49194-0.41012l-0.0080557-0.089876v-14.882c0-0.27614 0.22386-0.5 0.5-0.5z" fill="#2A506F" fill-rule="nonzero"/>
<line x1="19.147" x2="14.532" y1="88.933" y2="84.214" stroke="#2A506F" stroke-linecap="round"/>
<line x1="19.147" x2="23.866" y1="88.933" y2="84.318" stroke="#2A506F" stroke-linecap="round"/>
<path d="m14.007 26.177c0.51076 0 0.96749-0.071211 1.3702-0.21363s0.74649-0.33887 1.0313-0.58934 0.50339-0.54268 0.65564-0.87664 0.22837-0.69247 0.22837-1.0755c0-0.3536-0.051567-0.66546-0.1547-0.93557s-0.2431-0.50585-0.4199-0.7072-0.38798-0.37816-0.63354-0.5304-0.50585-0.2873-0.78087-0.40517l-1.3702-0.58934c-0.19645-0.078578-0.38798-0.16452-0.5746-0.25783s-0.35851-0.20136-0.51567-0.32413-0.28239-0.2652-0.3757-0.42727-0.13997-0.36097-0.13997-0.5967c0-0.442 0.16452-0.78824 0.49357-1.0387s0.76368-0.3757 1.3039-0.3757c0.45182 0 0.85699 0.081034 1.2155 0.2431s0.6851 0.38552 0.97977 0.67037l0.663-0.7956c-0.34378-0.3536-0.76123-0.6409-1.2523-0.8619s-1.0264-0.3315-1.6059-0.3315c-0.442 0-0.84717 0.063845-1.2155 0.19153s-0.68756 0.30695-0.95767 0.53777-0.48129 0.50339-0.63354 0.8177-0.22837 0.65318-0.22837 1.0166c0 0.3536 0.058934 0.66546 0.1768 0.93557s0.27011 0.50339 0.45674 0.69984 0.3978 0.36342 0.63354 0.50094 0.46656 0.25538 0.69247 0.3536l1.3849 0.60407c0.22591 0.10804 0.43709 0.21118 0.63354 0.3094s0.36588 0.20872 0.5083 0.3315 0.25538 0.27011 0.33887 0.442 0.12523 0.38061 0.12523 0.62617c0 0.47147-0.1768 0.85208-0.5304 1.1418s-0.84963 0.43464-1.4881 0.43464c-0.50094 0-0.98468-0.1105-1.4512-0.3315s-0.87173-0.51321-1.2155-0.87664l-0.73667 0.85454c0.42236 0.442 0.92329 0.79069 1.5028 1.0461s1.2081 0.38307 1.8859 0.38307zm6.2664-0.1768v-4.5968c0.24556-0.60898 0.53286-1.0362 0.8619-1.2818s0.64581-0.36834 0.9503-0.36834c0.14733 0 0.27011 0.0098223 0.36834 0.029467s0.20627 0.049111 0.32413 0.0884l0.23573-1.0608c-0.22591-0.098223-0.48129-0.14733-0.76614-0.14733-0.41254 0-0.79315 0.1326-1.1418 0.3978s-0.64581 0.62371-0.89137 1.0755h-0.0442l-0.10313-1.2965h-1.0019v7.1604h1.2081zm6.5758 0.1768c0.43218 0 0.84471-0.081034 1.2376-0.2431s0.7514-0.38552 1.0755-0.67037l-0.5304-0.81034c-0.22591 0.19645-0.47884 0.36588-0.75877 0.5083s-0.58688 0.21363-0.92084 0.21363c-0.32413 0-0.62371-0.0663-0.89874-0.1989s-0.5083-0.31922-0.69984-0.55987-0.34132-0.52795-0.44937-0.8619-0.16207-0.7072-0.16207-1.1197 0.056478-0.78824 0.16943-1.1271 0.26766-0.63108 0.4641-0.87664 0.43218-0.43464 0.7072-0.56724 0.5746-0.1989 0.89874-0.1989c0.28485 0 0.54268 0.058934 0.7735 0.1768s0.44937 0.27011 0.65564 0.45674l0.6188-0.7956c-0.25538-0.22591-0.55005-0.42236-0.884-0.58934s-0.73667-0.25047-1.2081-0.25047c-0.46165 0-0.90119 0.083489-1.3186 0.25047s-0.78333 0.41254-1.0976 0.73667-0.56478 0.71948-0.7514 1.186-0.27993 0.99942-0.27993 1.5986c0 0.58934 0.085945 1.1173 0.25783 1.5838s0.40762 0.85945 0.7072 1.1787 0.65564 0.56232 1.0682 0.7293 0.85454 0.25047 1.326 0.25047z" fill="#fff" fill-rule="nonzero"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -15,24 +15,33 @@
*/
import * as electron from 'electron';
import * as remoteMain from '@electron/remote/main';
import { autoUpdater } from 'electron-updater';
import { promises as fs } from 'fs';
import { platform } from 'os';
import * as path from 'path';
import * as semver from 'semver';
import * as lodash from 'lodash';
import './app/i18n';
import { packageType, version } from '../../package.json';
import * as EXIT_CODES from '../shared/exit-codes';
import { delay, getConfig } from '../shared/utils';
import * as settings from './app/models/settings';
import { logException } from './app/modules/analytics';
import { buildWindowMenu } from './menu';
import * as i18n from 'i18next';
import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json';
import { anonymizeSentryData } from './app/modules/analytics';
const customProtocol = 'etcher';
const scheme = `${customProtocol}://`;
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
const packageUpdatable = updatablePackageTypes.includes(packageType);
let packageUpdated = false;
let mainWindow: any = null;
remoteMain.initialize();
async function checkForUpdates(interval: number) {
// We use a while loop instead of a setInterval to preserve
@@ -42,20 +51,28 @@ async function checkForUpdates(interval: number) {
try {
const release = await autoUpdater.checkForUpdates();
const isOutdated =
semver.compare(release.updateInfo.version, version) > 0;
const shouldUpdate = release.updateInfo.stagingPercentage || 0 > 0;
semver.compare(release!.updateInfo.version, version) > 0;
const shouldUpdate = release!.updateInfo.stagingPercentage !== 0; // undefined (default) means 100%
if (shouldUpdate && isOutdated) {
await autoUpdater.downloadUpdate();
packageUpdated = true;
}
} catch (err) {
logException(err);
logMainProcessException(err);
}
}
await delay(interval);
}
}
function logMainProcessException(error: any) {
const shouldReportErrors = settings.getSync('errorReporting');
console.error(error);
if (shouldReportErrors) {
SentryMain.captureException(error);
}
}
async function isFile(filePath: string): Promise<boolean> {
try {
const stat = await fs.stat(filePath);
@@ -90,6 +107,14 @@ async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
}
}
const initSentryMain = lodash.once(() => {
const dsn =
settings.getSync('analyticsSentryToken') ||
lodash.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryMain.init({ dsn, beforeSend: anonymizeSentryData });
});
const sourceSelectorReady = new Promise((resolve) => {
electron.ipcMain.on('source-selector-ready', resolve);
});
@@ -97,6 +122,7 @@ const sourceSelectorReady = new Promise((resolve) => {
async function selectImageURL(url?: string) {
// 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests
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;
await sourceSelectorReady;
electron.BrowserWindow.getAllWindows().forEach((window) => {
@@ -129,11 +155,11 @@ async function createMainWindow() {
if (fullscreen) {
({ width, height } = electron.screen.getPrimaryDisplay().bounds);
}
const mainWindow = new electron.BrowserWindow({
mainWindow = new electron.BrowserWindow({
width,
height,
frame: !fullscreen,
useContentSize: false,
useContentSize: true,
show: false,
resizable: false,
maximizable: false,
@@ -147,19 +173,18 @@ async function createMainWindow() {
webPreferences: {
backgroundThrottling: false,
nodeIntegration: true,
contextIsolation: false,
webviewTag: true,
zoomFactor: width / defaultWidth,
enableRemoteModule: true,
},
});
electron.app.setAsDefaultProtocolClient(customProtocol);
buildWindowMenu(mainWindow);
mainWindow.setFullScreen(true);
// mainWindow.setFullScreen(true);
// Prevent flash of white when starting the application
mainWindow.on('ready-to-show', () => {
mainWindow.once('ready-to-show', () => {
console.timeEnd('ready-to-show');
// Electron sometimes caches the zoomFactor
// making it obnoxious to switch back-and-forth
@@ -183,34 +208,26 @@ async function createMainWindow() {
);
const page = mainWindow.webContents;
remoteMain.enable(page);
page.once('did-frame-finish-load', async () => {
console.log('packageUpdatable', packageUpdatable);
autoUpdater.on('error', (err) => {
logException(err);
logMainProcessException(err);
});
if (packageUpdatable) {
try {
const configUrl = await settings.get('configUrl');
const onlineConfig = await getConfig(configUrl);
const autoUpdaterConfig: AutoUpdaterConfig = onlineConfig?.autoUpdates
?.autoUpdaterConfig ?? {
autoDownload: false,
};
for (const [key, value] of Object.entries(autoUpdaterConfig)) {
autoUpdater[key as keyof AutoUpdaterConfig] = value;
}
const checkForUpdatesTimer =
onlineConfig?.autoUpdates?.checkForUpdatesTimer ?? 300000;
const checkForUpdatesTimer = 300000;
checkForUpdates(checkForUpdatesTimer);
} catch (err) {
logException(err);
logMainProcessException(err);
}
}
});
return mainWindow;
}
electron.app.allowRendererProcessReuse = false;
electron.app.on('window-all-closed', electron.app.quit);
// Sending a `SIGINT` (e.g: Ctrl-C) to an Electron app that registers
@@ -228,6 +245,7 @@ async function main(): Promise<void> {
if (!electron.app.requestSingleInstanceLock()) {
electron.app.quit();
} else {
initSentryMain();
await electron.app.whenReady();
const window = await createMainWindow();
electron.app.on('second-instance', async (_event, argv) => {
@@ -238,9 +256,37 @@ async function main(): Promise<void> {
await selectImageURL(await getCommandLineURL(argv));
});
await selectImageURL(await getCommandLineURL(process.argv));
electron.ipcMain.on('change-lng', function (event, args) {
i18n.changeLanguage(args, () => {
console.log('Language changed to: ' + args);
});
if (mainWindow != null) {
buildWindowMenu(mainWindow);
} else {
console.log('Build menu failed. ');
}
});
electron.ipcMain.on('webview-dom-ready', (_, id) => {
const webview = electron.webContents.fromId(id);
// Open link in browser if it's opened as a 'foreground-tab'
webview.setWindowOpenHandler((event) => {
const url = new URL(event.url);
if (
(url.protocol === 'http:' || url.protocol === 'https:') &&
event.disposition === 'foreground-tab' &&
// Don't open links if they're disabled by the env var
!settings.getSync('disableExternalLinks')
) {
electron.shell.openExternal(url.href);
}
return { action: 'deny' };
});
});
}
}
main();
console.time('ready-to-show');

View File

@@ -17,6 +17,8 @@
import * as electron from 'electron';
import { displayName } from '../../package.json';
import * as i18next from 'i18next';
/**
* @summary Builds a native application menu for a given window
*/
@@ -42,12 +44,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
const menuTemplate: electron.MenuItemConstructorOptions[] = [
{
role: 'editMenu',
label: i18next.t('menu.edit'),
},
{
label: 'View',
label: i18next.t('menu.view'),
submenu: [
{
label: 'Toggle Developer Tools',
label: i18next.t('menu.devTool'),
accelerator:
process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I',
click: toggleDevTools,
@@ -56,12 +59,14 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
},
{
role: 'windowMenu',
label: i18next.t('menu.window'),
},
{
role: 'help',
label: i18next.t('menu.help'),
submenu: [
{
label: 'Etcher Pro',
label: i18next.t('menu.pro'),
click() {
electron.shell.openExternal(
'https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu',
@@ -69,13 +74,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
},
},
{
label: 'Etcher Website',
label: i18next.t('menu.website'),
click() {
electron.shell.openExternal('https://etcher.io?ref=etcher_menu');
},
},
{
label: 'Report an issue',
label: i18next.t('menu.issue'),
click() {
electron.shell.openExternal(
'https://github.com/balena-io/etcher/issues',
@@ -92,25 +97,29 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
submenu: [
{
role: 'about' as const,
label: 'About Etcher',
label: i18next.t('menu.about'),
},
{
type: 'separator' as const,
},
{
role: 'hide' as const,
label: i18next.t('menu.hide'),
},
{
role: 'hideOthers' as const,
label: i18next.t('menu.hideOthers'),
},
{
role: 'unhide' as const,
label: i18next.t('menu.unhide'),
},
{
type: 'separator' as const,
},
{
role: 'quit' as const,
label: i18next.t('menu.quit'),
},
],
});

View File

@@ -1,362 +0,0 @@
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import { promises as fs } from 'fs';
import * as _ from 'lodash';
import * as ipc from 'node-ipc';
import { totalmem } from 'os';
import { BlockDevice, File, Http } from 'etcher-sdk/build/source-destination';
import { toJSON } from '../../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
import { delay } from '../../shared/utils';
import { SourceMetadata } from '../app/components/source-selector/source-selector';
ipc.config.id = process.env.IPC_CLIENT_ID as string;
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
// NOTE: Ensure this isn't disabled, as it will cause
// the stdout maxBuffer size to be exceeded when flashing
ipc.config.silent = true;
// > If set to 0, the client will NOT try to reconnect.
// See https://github.com/RIAEvangelist/node-ipc/
//
// The purpose behind this change is for this process
// to emit a "disconnect" event as soon as the GUI
// process is closed, so we can kill this process as well.
// @ts-ignore (0 is a valid value for stopRetrying and is not the same as false)
ipc.config.stopRetrying = 0;
const DISCONNECT_DELAY = 100;
const IPC_SERVER_ID = process.env.IPC_SERVER_ID as string;
/**
* @summary Send a log debug message to the IPC server
*/
function log(message: string) {
ipc.of[IPC_SERVER_ID].emit('log', message);
}
/**
* @summary Terminate the child writer process
*/
async function terminate(exitCode: number) {
ipc.disconnect(IPC_SERVER_ID);
await cleanupTmpFiles(Date.now());
process.nextTick(() => {
process.exit(exitCode || SUCCESS);
});
}
/**
* @summary Handle a child writer error
*/
async function handleError(error: Error) {
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
await delay(DISCONNECT_DELAY);
await terminate(GENERAL_ERROR);
}
interface WriteResult {
bytesWritten: number;
devices: {
failed: number;
successful: number;
};
errors: Array<Error & { device: string }>;
sourceMetadata: sdk.sourceDestination.Metadata;
}
/**
* @summary writes the source to the destinations and valiates the writes
* @param {SourceDestination} source - source
* @param {SourceDestination[]} destinations - destinations
* @param {Boolean} verify - whether to validate the writes or not
* @param {Boolean} autoBlockmapping - whether to trim ext partitions before writing
* @param {Function} onProgress - function to call on progress
* @param {Function} onFail - function to call on fail
* @returns {Promise<{ bytesWritten, devices, errors} >}
*/
async function writeAndValidate({
source,
destinations,
verify,
autoBlockmapping,
decompressFirst,
onProgress,
onFail,
}: {
source: sdk.sourceDestination.SourceDestination;
destinations: sdk.sourceDestination.BlockDevice[];
verify: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
onProgress: sdk.multiWrite.OnProgressFunction;
onFail: sdk.multiWrite.OnFailFunction;
}): Promise<WriteResult> {
const {
sourceMetadata,
failures,
bytesWritten,
} = await sdk.multiWrite.decompressThenFlash({
source,
destinations,
onFail,
onProgress,
verify,
trim: autoBlockmapping,
numBuffers: Math.min(
2 + (destinations.length - 1) * 32,
256,
Math.floor(totalmem() / 1024 ** 2 / 8),
),
decompressFirst,
});
const result: WriteResult = {
bytesWritten,
devices: {
failed: failures.size,
successful: destinations.length - failures.size,
},
errors: [],
sourceMetadata,
};
for (const [destination, error] of failures) {
const err = error as Error & { device: string; description: string };
const drive = destination as sdk.sourceDestination.BlockDevice;
err.device = drive.device;
err.description = drive.description;
result.errors.push(err);
}
return result;
}
interface WriteOptions {
image: SourceMetadata;
destinations: DrivelistDrive[];
unmountOnSuccess: boolean;
validateWriteOnSuccess: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
SourceType: string;
saveUrlImage: boolean;
saveUrlImageTo: string;
}
interface ProgressState
extends Omit<sdk.multiWrite.MultiDestinationProgress, 'type'> {
type: sdk.multiWrite.MultiDestinationProgress['type'] | 'downloading';
}
ipc.connectTo(IPC_SERVER_ID, () => {
// Remove leftover tmp files older than 1 hour
cleanupTmpFiles(Date.now() - 60 * 60 * 1000);
process.once('uncaughtException', handleError);
// Gracefully exit on the following cases. If the parent
// process detects that child exit successfully but
// no flashing information is available, then it will
// assume that the child died halfway through.
process.once('SIGINT', async () => {
await terminate(SUCCESS);
});
process.once('SIGTERM', async () => {
await terminate(SUCCESS);
});
// The IPC server failed. Abort.
ipc.of[IPC_SERVER_ID].on('error', async () => {
await terminate(SUCCESS);
});
// The IPC server was disconnected. Abort.
ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
await terminate(SUCCESS);
});
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
/**
* @summary Progress handler
* @param {Object} state - progress state
* @example
* writer.on('progress', onProgress)
*/
const onProgress = (state: ProgressState) => {
ipc.of[IPC_SERVER_ID].emit('state', state);
};
let exitCode = SUCCESS;
/**
* @summary Abort handler
* @example
* writer.on('abort', onAbort)
*/
const onAbort = async () => {
log('Abort');
ipc.of[IPC_SERVER_ID].emit('abort');
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
};
const onSkip = async () => {
log('Skip validation');
ipc.of[IPC_SERVER_ID].emit('skip');
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
};
ipc.of[IPC_SERVER_ID].on('cancel', onAbort);
ipc.of[IPC_SERVER_ID].on('skip', onSkip);
/**
* @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination
* @param {Error} error - error
* @example
* writer.on('fail', onFail)
*/
const onFail = (
destination: sdk.sourceDestination.SourceDestination,
error: Error,
) => {
ipc.of[IPC_SERVER_ID].emit('fail', {
// TODO: device should be destination
// @ts-ignore (destination.drive is private)
device: destination.drive,
error: toJSON(error),
});
};
const destinations = options.destinations.map((d) => d.device);
const imagePath = options.image.path;
log(`Image: ${imagePath}`);
log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Validate on success: ${options.validateWriteOnSuccess}`);
log(`Auto blockmapping: ${options.autoBlockmapping}`);
log(`Decompress first: ${options.decompressFirst}`);
const dests = options.destinations.map((destination) => {
return new sdk.sourceDestination.BlockDevice({
drive: destination,
unmountOnSuccess: options.unmountOnSuccess,
write: true,
direct: true,
});
});
const { SourceType } = options;
try {
let source;
if (options.image.drive) {
source = new BlockDevice({
drive: options.image.drive,
direct: !options.autoBlockmapping,
});
} else {
if (SourceType === File.name) {
source = new File({
path: imagePath,
});
} else {
if (options.saveUrlImage) {
source = await saveFileBeforeFlash(
imagePath,
options.saveUrlImageTo,
onProgress,
onFail,
);
} else {
source = new Http({ url: imagePath, avoidRandomAccess: true });
}
}
}
const results = await writeAndValidate({
source,
destinations: dests,
verify: options.validateWriteOnSuccess,
autoBlockmapping: options.autoBlockmapping,
decompressFirst: options.decompressFirst,
onProgress,
onFail,
});
log(`Finish: ${results.bytesWritten}`);
results.errors = results.errors.map((error) => {
return toJSON(error);
});
ipc.of[IPC_SERVER_ID].emit('done', { results });
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
} catch (error) {
log(`Error: ${error.message}`);
exitCode = GENERAL_ERROR;
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
}
});
ipc.of[IPC_SERVER_ID].on('connect', () => {
log(
`Successfully connected to IPC server: ${IPC_SERVER_ID}, socket root ${ipc.config.socketRoot}`,
);
ipc.of[IPC_SERVER_ID].emit('ready', {});
});
});
async function saveFileBeforeFlash(
imagePath: string,
saveUrlImageTo: string,
onProgress: (state: ProgressState) => void,
onFail: (
destination: sdk.sourceDestination.SourceDestination,
error: Error,
) => void,
) {
const urlImage = new Http({ url: imagePath, avoidRandomAccess: true });
const source = await urlImage.getInnerSource();
const metadata = await source.getMetadata();
const fileName = `${saveUrlImageTo}/${metadata.name}`;
let alreadyDownloaded = false;
try {
alreadyDownloaded = (await fs.stat(fileName)).isFile();
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
if (!alreadyDownloaded) {
await sdk.multiWrite.decompressThenFlash({
source,
destinations: [new File({ path: fileName, write: true })],
onProgress: (progress) => {
onProgress({
...progress,
type: 'downloading',
});
},
onFail: (...args) => {
onFail(...args);
},
verify: true,
});
}
return new File({ path: fileName });
}

10
lib/pkg-sidekick.json Normal file
View File

@@ -0,0 +1,10 @@
{
"bin": "build/util/child-writer.js",
"pkg": {
"assets": [
"node_modules/usb/prebuilds/darwin-x64+arm64/node.napi.node",
"node_modules/lzma-native/prebuilds/darwin-arm64/node.napi.node",
"node_modules/drivelist/build/Release/drivelist.node"
]
}
}

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env osascript -l JavaScript
ObjC.import('stdlib')
const app = Application.currentApplication()
app.includeStandardAdditions = true
const result = app.displayDialog('balenaEtcher 需要来自管理员的权限才能烧录镜像到磁盘。\n\n输入您的密码以允许此操作。', {
defaultAnswer: '',
withIcon: 'caution',
buttons: ['取消', '好'],
defaultButton: '好',
hiddenAnswer: true,
})
if (result.buttonReturned === '好') {
result.textReturned
} else {
$.exit(255)
}

View File

@@ -15,11 +15,13 @@
*/
import { execFile } from 'child_process';
import { app, remote } from 'electron';
import { join } from 'path';
import { env } from 'process';
import { promisify } from 'util';
import { getAppPath } from '../get-app-path';
import { supportedLocales } from '../../gui/app/i18n';
const execFileAsync = promisify(execFile);
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
@@ -29,6 +31,15 @@ export async function sudo(
command: string,
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
try {
let lang = Intl.DateTimeFormat().resolvedOptions().locale;
lang = lang.substr(0, 2);
if (supportedLocales.indexOf(lang) > -1) {
// language should be present
} else {
// fallback to eng
lang = 'en';
}
const { stdout, stderr } = await execFileAsync(
'sudo',
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
@@ -37,9 +48,9 @@ export async function sudo(
env: {
PATH: env.PATH,
SUDO_ASKPASS: join(
(app || remote.app).getAppPath(),
getAppPath(),
__dirname,
'sudo-askpass.osascript.js',
`sudo-askpass.osascript-${lang}.js`,
),
},
},
@@ -49,7 +60,7 @@ export async function sudo(
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
stderr,
};
} catch (error) {
} catch (error: any) {
if (error.code === 1) {
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
return { cancelled: true };

View File

@@ -15,11 +15,11 @@
*/
import { Drive } from 'drivelist';
import * as _ from 'lodash';
import { isNil } from 'lodash';
import * as pathIsInside from 'path-is-inside';
import * as messages from './messages';
import { SourceMetadata } from '../gui/app/components/source-selector/source-selector';
import { SourceMetadata } from './typings/source-selector';
/**
* @summary The default unknown size for things such as images and drives
@@ -34,16 +34,6 @@ export type DrivelistDrive = Drive & {
displayName: string;
};
/**
* @summary Check if a drive is locked
*
* @description
* This usually points out a locked SD Card.
*/
export function isDriveLocked(drive: DrivelistDrive): boolean {
return Boolean(drive.isReadOnly);
}
/**
* @summary Check if a drive is a system drive
*/
@@ -73,9 +63,7 @@ export function isSourceDrive(
): boolean {
if (selection) {
if (selection.drive) {
const sourcePath = selection.drive.devicePath || selection.drive.device;
const drivePath = drive.devicePath || drive.device;
return pathIsInside(sourcePath, drivePath);
return selection.drive.device === drive.device;
}
if (selection.path) {
return sourceIsInsideDrive(selection.path, drive);
@@ -117,24 +105,18 @@ export function isDriveLargeEnough(
}
/**
* @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. not locked and large enough for an image
* @summary Check if a drive is valid, i.e. large enough for an image
*/
export function isDriveValid(
drive: DrivelistDrive,
image?: SourceMetadata,
write: boolean = true,
): boolean {
return (
!isDriveLocked(drive) &&
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata) &&
!isDriveDisabled(drive)
!write ||
(!drive.disabled &&
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata))
);
}
@@ -215,19 +197,21 @@ export const statuses = {
*/
export function getDriveImageCompatibilityStatuses(
drive: DrivelistDrive,
image?: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
const statusList = [];
// Mind the order of the if-statements if you modify.
if (isDriveLocked(drive)) {
if (drive.isReadOnly && write) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.locked(),
});
} else if (
!_.isNil(drive) &&
!_.isNil(drive.size) &&
}
if (
!isNil(drive) &&
!isNil(drive.size) &&
!isDriveLargeEnough(drive, image)
) {
statusList.push(statuses.small);
@@ -245,7 +229,7 @@ export function getDriveImageCompatibilityStatuses(
if (
image !== undefined &&
!_.isNil(drive) &&
!isNil(drive) &&
!isDriveSizeRecommended(drive, image)
) {
statusList.push(statuses.sizeNotRecommended);
@@ -264,10 +248,11 @@ export function getDriveImageCompatibilityStatuses(
*/
export function getListDriveImageCompatibilityStatuses(
drives: DrivelistDrive[],
image: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
return drives.flatMap((drive) => {
return getDriveImageCompatibilityStatuses(drive, image);
return getDriveImageCompatibilityStatuses(drive, image, write);
});
}
@@ -279,9 +264,12 @@ export function getListDriveImageCompatibilityStatuses(
*/
export function hasDriveImageCompatibilityStatus(
drive: DrivelistDrive,
image: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
return Boolean(
getDriveImageCompatibilityStatuses(drive, image, write).length,
);
}
export interface DriveStatus {

View File

@@ -0,0 +1,12 @@
export function getAppPath(): string {
return (
(require('electron').app || require('@electron/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 : '',
)
);
}

View File

@@ -17,16 +17,16 @@
import { Dictionary } from 'lodash';
import { outdent } from 'outdent';
import * as prettyBytes from 'pretty-bytes';
import '../gui/app/i18n';
import * as i18next from 'i18next';
export const progress: Dictionary<(quantity: number) => string> = {
successful: (quantity: number) => {
const plural = quantity === 1 ? '' : 's';
return `Successful target${plural}`;
return i18next.t('message.flashSucceed', { count: quantity });
},
failed: (quantity: number) => {
const plural = quantity === 1 ? '' : 's';
return `Failed target${plural}`;
return i18next.t('message.flashFail', { count: quantity });
},
};
@@ -38,124 +38,121 @@ export const info = {
) => {
const targets = [];
if (failed + successful === 1) {
targets.push(`to ${drive.description} (${drive.displayName})`);
targets.push(
i18next.t('message.toDrive', {
description: drive.description,
name: drive.displayName,
}),
);
} else {
if (successful) {
const plural = successful === 1 ? '' : 's';
targets.push(`to ${successful} target${plural}`);
targets.push(
i18next.t('message.toTarget', {
count: successful,
num: successful,
}),
);
}
if (failed) {
const plural = failed === 1 ? '' : 's';
targets.push(`and failed to be flashed to ${failed} target${plural}`);
targets.push(
i18next.t('message.andFailTarget', { count: failed, num: failed }),
);
}
}
return `${imageBasename} was successfully flashed ${targets.join(' ')}`;
return i18next.t('message.succeedTo', {
name: imageBasename,
target: targets.join(' '),
});
},
};
export const compatibility = {
sizeNotRecommended: () => {
return 'Not recommended';
return i18next.t('message.sizeNotRecommended');
},
tooSmall: () => {
return 'Too small';
return i18next.t('message.tooSmall');
},
locked: () => {
return 'Locked';
return i18next.t('message.locked');
},
system: () => {
return 'System drive';
return i18next.t('message.system');
},
containsImage: () => {
return 'Source drive';
return i18next.t('message.containsImage');
},
// The drive is large and therefore likely not a medium you want to write to.
largeDrive: () => {
return 'Large drive';
return i18next.t('message.largeDrive');
},
} as const;
export const warning = {
unrecommendedDriveSize: (
image: { recommendedDriveSize: number },
drive: { device: string; size: number },
) => {
tooSmall: (source: { size: number }, target: { size: number }) => {
return outdent({ newline: ' ' })`
This image recommends a ${prettyBytes(image.recommendedDriveSize)}
drive, however ${drive.device} is only ${prettyBytes(drive.size)}.
${i18next.t('message.sourceLarger', {
byte: prettyBytes(source.size - target.size),
})}
`;
},
exitWhileFlashing: () => {
return [
'You are currently flashing a drive.',
'Closing Etcher may leave your drive in an unusable state.',
].join(' ');
return i18next.t('message.exitWhileFlashing');
},
looksLikeWindowsImage: () => {
return [
'It looks like you are trying to burn a Windows image.\n\n',
'Unlike other images, Windows images require special processing to be made bootable.',
'We suggest you use a tool specially designed for this purpose, such as',
'<a href="https://rufus.akeo.ie">Rufus</a> (Windows),',
'<a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux),',
'or Boot Camp Assistant (macOS).',
].join(' ');
return i18next.t('message.looksLikeWindowsImage');
},
missingPartitionTable: () => {
return [
'It looks like this is not a bootable image.\n\n',
'The image does not appear to contain a partition table,',
'and might not be recognized or bootable by your device.',
].join(' ');
return i18next.t('message.missingPartitionTable', {
type: i18next.t('message.image'),
});
},
driveMissingPartitionTable: () => {
return i18next.t('message.missingPartitionTable', {
type: i18next.t('message.drive'),
});
},
largeDriveSize: () => {
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
return i18next.t('message.largeDriveSize');
},
systemDrive: () => {
return 'Selecting your system drive is dangerous and will erase your drive!';
return i18next.t('message.systemDrive');
},
sourceDrive: () => {
return 'Contains the image you chose to flash';
return i18next.t('message.sourceDrive');
},
};
export const error = {
notEnoughSpaceInDrive: () => {
return [
'Not enough space on the drive.',
'Please insert larger one and try again.',
].join(' ');
return i18next.t('message.noSpace');
},
genericFlashError: (err: Error) => {
return `Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n${err.message}`;
return i18next.t('message.genericFlashError', { error: err.message });
},
validation: () => {
return [
'The write has been completed successfully but Etcher detected potential',
'corruption issues when reading the image back from the drive.',
'\n\nPlease consider writing the image to a different drive.',
].join(' ');
return i18next.t('message.validation');
},
openSource: (sourceName: string, errorMessage: string) => {
return outdent`
Something went wrong while opening ${sourceName}
Error: ${errorMessage}
`;
return i18next.t('message.openError', {
source: sourceName,
error: errorMessage,
});
},
flashFailure: (
@@ -164,35 +161,33 @@ export const error = {
) => {
const target =
drives.length === 1
? `${drives[0].description} (${drives[0].displayName})`
: `${drives.length} targets`;
return `Something went wrong while writing ${imageBasename} to ${target}.`;
? i18next.t('message.toDrive', {
description: drives[0].description,
name: drives[0].displayName,
})
: i18next.t('message.toTarget', {
count: drives.length,
num: drives.length,
});
return i18next.t('message.flashError', {
image: imageBasename,
targets: target,
});
},
driveUnplugged: () => {
return [
'Looks like Etcher lost access to the drive.',
'Did it get unplugged accidentally?',
"\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
].join(' ');
return i18next.t('message.unplug');
},
inputOutput: () => {
return [
'Looks like Etcher is not able to write to this location of the drive.',
'This error is usually caused by a faulty drive, reader, or port.',
'\n\nPlease try again with another drive, reader, or port.',
].join(' ');
return i18next.t('message.cannotWrite');
},
childWriterDied: () => {
return [
'The writer process ended unexpectedly.',
'Please try again, and contact the Etcher team if the problem persists.',
].join(' ');
return i18next.t('message.childWriterDied');
},
unsupportedProtocol: () => {
return 'Only http:// and https:// URLs are supported.';
return i18next.t('message.badProtocol');
},
};

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
import { GPTPartition, MBRPartition } from 'partitioninfo';
import { sourceDestination } from 'etcher-sdk';
import { DrivelistDrive } from '../drive-constraints';
export type Source = 'File' | 'BlockDevice' | 'Http';
export interface SourceMetadata extends sourceDestination.Metadata {
hasMBR?: boolean;
partitions?: MBRPartition[] | GPTPartition[];
path: string;
displayName: string;
description: string;
SourceType: Source;
drive?: DrivelistDrive;
extension?: string;
archiveExtension?: string;
auth?: Authentication;
}
export interface Authentication {
username: string;
password: string;
}

View File

@@ -14,9 +14,6 @@
* limitations under the License.
*/
import axios from 'axios';
import { Dictionary } from 'lodash';
import * as errors from './errors';
export function isValidPercentage(percentage: any): boolean {
@@ -32,18 +29,17 @@ export function percentageToFloat(percentage: any) {
return percentage / 100;
}
/**
* @summary Get etcher configs stored online
* @param {String} - url where config.json is stored
*/
export async function getConfig(configUrl?: string): Promise<Dictionary<any>> {
configUrl = configUrl ?? 'https://balena.io/etcher/static/config.json';
const response = await axios.get(configUrl, { responseType: 'json' });
return response.data;
}
export async function delay(duration: number): Promise<void> {
await new Promise((resolve) => {
setTimeout(resolve, duration);
});
}
export function isJson(jsonString: string) {
try {
JSON.parse(jsonString);
} catch (e) {
return false;
}
return true;
}

201
lib/util/api.ts Normal file
View File

@@ -0,0 +1,201 @@
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as ipc from 'node-ipc';
import { toJSON } from '../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes';
import { delay } from '../shared/utils';
import { WriteOptions } from './types/types';
import { MultiDestinationProgress } from 'etcher-sdk/build/multi-write';
import { write, cleanup } from './child-writer';
import { startScanning } from './scanner';
import { getSourceMetadata } from './source-metadata';
import { DrivelistDrive } from '../shared/drive-constraints';
import { Dictionary, values } from 'lodash';
ipc.config.id = process.env.IPC_CLIENT_ID as string;
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
// NOTE: Ensure this isn't disabled, as it will cause
// the stdout maxBuffer size to be exceeded when flashing
ipc.config.silent = true;
// > If set to 0, the client will NOT try to reconnect.
// See https://github.com/RIAEvangelist/node-ipc/
//
// The purpose behind this change is for this process
// to emit a "disconnect" event as soon as the GUI
// process is closed, so we can kill this process as well.
// @ts-ignore (0 is a valid value for stopRetrying and is not the same as false)
ipc.config.stopRetrying = 0;
const DISCONNECT_DELAY = 100;
const IPC_SERVER_ID = process.env.IPC_SERVER_ID as string;
/**
* @summary Send a message to the IPC server
*/
function emit(channel: string, message?: any) {
ipc.of[IPC_SERVER_ID].emit(channel, message);
}
/**
* @summary Send a log debug message to the IPC server
*/
function log(message: string) {
if (console?.log) {
console.log(message);
}
emit('log', message);
}
/**
* @summary Terminate the child process
*/
async function terminate(exitCode: number) {
ipc.disconnect(IPC_SERVER_ID);
await cleanup(Date.now());
process.nextTick(() => {
process.exit(exitCode || SUCCESS);
});
}
/**
* @summary Handle errors
*/
async function handleError(error: Error) {
emit('error', toJSON(error));
await delay(DISCONNECT_DELAY);
await terminate(GENERAL_ERROR);
}
/**
* @summary Abort handler
* @example
*/
const onAbort = async (exitCode: number) => {
log('Abort');
emit('abort');
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
};
const onSkip = async (exitCode: number) => {
log('Skip validation');
emit('skip');
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
};
ipc.connectTo(IPC_SERVER_ID, () => {
// Gracefully exit on the following cases. If the parent
// process detects that child exit successfully but
// no flashing information is available, then it will
// assume that the child died halfway through.
process.once('uncaughtException', handleError);
process.once('SIGINT', async () => {
await terminate(SUCCESS);
});
process.once('SIGTERM', async () => {
await terminate(SUCCESS);
});
// The IPC server failed. Abort.
ipc.of[IPC_SERVER_ID].on('error', async () => {
await terminate(SUCCESS);
});
// The IPC server was disconnected. Abort.
ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
await terminate(SUCCESS);
});
ipc.of[IPC_SERVER_ID].on('sourceMetadata', async (params) => {
const { selected, SourceType, auth } = JSON.parse(params);
try {
const sourceMatadata = await getSourceMetadata(
selected,
SourceType,
auth,
);
emitSourceMetadata(sourceMatadata);
} catch (error: any) {
emitFail(error);
}
});
ipc.of[IPC_SERVER_ID].on('scan', async () => {
startScanning();
});
// write handler
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
// Remove leftover tmp files older than 1 hour
cleanup(Date.now() - 60 * 60 * 1000);
let exitCode = SUCCESS;
ipc.of[IPC_SERVER_ID].on('cancel', () => onAbort(exitCode));
ipc.of[IPC_SERVER_ID].on('skip', () => onSkip(exitCode));
const results = await write(options);
if (results.errors.length > 0) {
results.errors = results.errors.map((error: any) => {
return toJSON(error);
});
exitCode = GENERAL_ERROR;
}
emit('done', { results });
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
});
ipc.of[IPC_SERVER_ID].on('connect', () => {
log(
`Successfully connected to IPC server: ${IPC_SERVER_ID}, socket root ${ipc.config.socketRoot}`,
);
emit('ready', {});
});
});
function emitLog(message: string) {
log(message);
}
function emitState(state: MultiDestinationProgress) {
emit('state', state);
}
function emitFail(data: any) {
emit('fail', data);
}
function emitDrives(drives: Dictionary<DrivelistDrive>) {
emit('drives', JSON.stringify(values(drives)));
}
function emitSourceMetadata(sourceMetadata: any) {
emit('sourceMetadata', JSON.stringify(sourceMetadata));
}
export { emitLog, emitState, emitFail, emitDrives, emitSourceMetadata };

200
lib/util/child-writer.ts Normal file
View File

@@ -0,0 +1,200 @@
/*
* Copyright 2023 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file handles the writer process.
*/
import {
OnProgressFunction,
OnFailFunction,
decompressThenFlash,
DECOMPRESSED_IMAGE_PREFIX,
MultiDestinationProgress,
} from 'etcher-sdk/build/multi-write';
import { totalmem } from 'os';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import {
File,
Http,
BlockDevice,
SourceDestination,
} from 'etcher-sdk/build/source-destination';
import { WriteResult, FlashError, WriteOptions } from './types/types';
import { isJson } from '../shared/utils';
import { toJSON } from '../shared/errors';
import axios from 'axios';
import { omit } from 'lodash';
import { emitLog, emitState, emitFail } from './api';
async function write(options: WriteOptions) {
/**
* @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination
* @param {Error} error - error
*/
const onFail = (destination: SourceDestination, error: Error) => {
emitFail({
// TODO: device should be destination
// @ts-ignore (destination.drive is private)
device: destination.drive,
error: toJSON(error),
});
};
/**
* @summary Progress handler
* @param {Object} state - progress state
* @example
* writer.on('progress', onProgress)
*/
const onProgress = (state: MultiDestinationProgress) => {
emitState(state);
};
// Write the image to the destinations
const destinations = options.destinations.map((d) => d.device);
const imagePath = options.image.path;
emitLog(`Image: ${imagePath}`);
emitLog(`Devices: ${destinations.join(', ')}`);
emitLog(`Auto blockmapping: ${options.autoBlockmapping}`);
emitLog(`Decompress first: ${options.decompressFirst}`);
const dests = options.destinations.map((destination) => {
return new BlockDevice({
drive: destination,
unmountOnSuccess: true,
write: true,
direct: true,
});
});
const { SourceType } = options;
try {
let source;
if (options.image.drive) {
source = new BlockDevice({
drive: options.image.drive,
direct: !options.autoBlockmapping,
});
} else {
if (SourceType === File.name) {
source = new File({
path: imagePath,
});
} else {
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({
source,
destinations: dests,
verify: true,
autoBlockmapping: options.autoBlockmapping,
decompressFirst: options.decompressFirst,
onProgress,
onFail,
});
return results;
} catch (error: any) {
return { errors: [error] };
}
}
/** @summary clean up tmp files */
export async function cleanup(until: number) {
await cleanupTmpFiles(until, DECOMPRESSED_IMAGE_PREFIX);
}
/**
* @summary writes the source to the destinations and validates the writes
* @param {SourceDestination} source - source
* @param {SourceDestination[]} destinations - destinations
* @param {Boolean} verify - whether to validate the writes or not
* @param {Boolean} autoBlockmapping - whether to trim ext partitions before writing
* @param {Function} onProgress - function to call on progress
* @param {Function} onFail - function to call on fail
* @returns {Promise<{ bytesWritten, devices, errors} >}
*/
async function writeAndValidate({
source,
destinations,
verify,
autoBlockmapping,
decompressFirst,
onProgress,
onFail,
}: {
source: SourceDestination;
destinations: BlockDevice[];
verify: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
onProgress: OnProgressFunction;
onFail: OnFailFunction;
}): Promise<WriteResult> {
const { sourceMetadata, failures, bytesWritten } = await decompressThenFlash({
source,
destinations,
onFail,
onProgress,
verify,
trim: autoBlockmapping,
numBuffers: Math.min(
2 + (destinations.length - 1) * 32,
256,
Math.floor(totalmem() / 1024 ** 2 / 8),
),
decompressFirst,
});
const result: WriteResult = {
bytesWritten,
devices: {
failed: failures.size,
successful: destinations.length - failures.size,
},
errors: [],
sourceMetadata,
};
for (const [destination, error] of failures) {
const err = error as FlashError;
const drive = destination as BlockDevice;
err.device = drive.device;
err.description = drive.description;
result.errors.push(err);
}
return result;
}
export { write };

View File

@@ -15,10 +15,15 @@
*/
import * as sdk from 'etcher-sdk';
import {
Adapter,
BlockDeviceAdapter,
UsbbootDeviceAdapter,
} from 'etcher-sdk/build/scanner/adapters';
import { geteuid, platform } from 'process';
const adapters: sdk.scanner.adapters.Adapter[] = [
new sdk.scanner.adapters.BlockDeviceAdapter({
const adapters: Adapter[] = [
new BlockDeviceAdapter({
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
// module.exports = scanner right now.
if (platform !== 'linux' || geteuid() === 0) {
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter());
adapters.push(new UsbbootDeviceAdapter());
}
if (
platform === 'win32' &&
sdk.scanner.adapters.DriverlessDeviceAdapter !== undefined
) {
adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter());
if (platform === 'win32') {
const {
DriverlessDeviceAdapter: driverless,
// tslint:disable-next-line:no-var-requires
} = require('etcher-sdk/build/scanner/adapters/driverless');
adapters.push(new driverless());
}
export const scanner = new sdk.scanner.Scanner(adapters);

180
lib/util/scanner.ts Normal file
View File

@@ -0,0 +1,180 @@
import { scanner as driveScanner } from './drive-scanner';
import * as sdk from 'etcher-sdk';
import { DrivelistDrive } from '../shared/drive-constraints';
import outdent from 'outdent';
import { Dictionary, values, keyBy, padStart } from 'lodash';
import { emitDrives } from './api';
let availableDrives: DrivelistDrive[] = [];
export function hasAvailableDrives() {
return availableDrives.length > 0;
}
driveScanner.on('error', (error) => {
// Stop the drive scanning loop in case of errors,
// otherwise we risk presenting the same error over
// and over again to the user, while also heavily
// spamming our error reporting service.
driveScanner.stop();
console.log('scanner error', error);
});
function setDrives(drives: Dictionary<DrivelistDrive>) {
availableDrives = values(drives);
emitDrives(drives);
}
function getDrives() {
return keyBy(availableDrives, 'device');
}
async function addDrive(drive: Drive) {
const preparedDrive = prepareDrive(drive);
if (!(await driveIsAllowed(preparedDrive))) {
return;
}
const drives = getDrives();
drives[preparedDrive.device] = preparedDrive;
setDrives(drives);
}
function removeDrive(drive: Drive) {
const preparedDrive = prepareDrive(drive);
const drives = getDrives();
delete drives[preparedDrive.device];
setDrives(drives);
}
async function driveIsAllowed(drive: {
devicePath: string;
device: string;
raw: string;
}) {
// const driveBlacklist = (await settings.get("driveBlacklist")) || [];
const driveBlacklist: any[] = [];
return !(
driveBlacklist.includes(drive.devicePath) ||
driveBlacklist.includes(drive.device) ||
driveBlacklist.includes(drive.raw)
);
}
type Drive =
| sdk.sourceDestination.BlockDevice
| sdk.sourceDestination.UsbbootDrive
| sdk.sourceDestination.DriverlessDevice;
function prepareDrive(drive: Drive) {
if (drive instanceof sdk.sourceDestination.BlockDevice) {
// @ts-ignore (BlockDevice.drive is private)
return drive.drive;
} else if (drive instanceof sdk.sourceDestination.UsbbootDrive) {
// This is a workaround etcher expecting a device string and a size
// @ts-ignore
drive.device = drive.usbDevice.portId;
drive.size = null;
// @ts-ignore
drive.progress = 0;
drive.disabled = true;
drive.on('progress', (progress) => {
updateDriveProgress(drive, progress);
});
return drive;
} else if (drive instanceof sdk.sourceDestination.DriverlessDevice) {
const description =
COMPUTE_MODULE_DESCRIPTIONS[
drive.deviceDescriptor.idProduct.toString()
] || 'Compute Module';
return {
device: `${usbIdToString(
drive.deviceDescriptor.idVendor,
)}:${usbIdToString(drive.deviceDescriptor.idProduct)}`,
displayName: 'Missing drivers',
description,
mountpoints: [],
isReadOnly: false,
isSystem: false,
disabled: true,
icon: 'warning',
size: null,
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
linkCTA: 'Install',
linkTitle: 'Install missing drivers',
linkMessage: outdent`
Would you like to download the necessary drivers from the Raspberry Pi Foundation?
This will open your browser.
Once opened, download and run the installer from the "Windows Installer" section to install the drivers
`,
};
}
}
/**
* @summary The radix used by USB ID numbers
*/
const USB_ID_RADIX = 16;
/**
* @summary The expected length of a USB ID number
*/
const USB_ID_LENGTH = 4;
/**
* @summary Convert a USB id (e.g. product/vendor) to a string
*
* @example
* console.log(usbIdToString(2652))
* > '0x0a5c'
*/
function usbIdToString(id: number): string {
return `0x${padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`;
}
function updateDriveProgress(
drive: sdk.sourceDestination.UsbbootDrive,
progress: number,
) {
const drives = getDrives();
// @ts-ignore
const driveInMap = drives[drive.device];
if (driveInMap) {
// @ts-ignore
drives[drive.device] = { ...driveInMap, progress };
setDrives(drives);
}
}
/**
* @summary Product ID of BCM2708
*/
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763;
/**
* @summary Product ID of BCM2710
*/
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764;
/**
* @summary Compute module descriptions
*/
const COMPUTE_MODULE_DESCRIPTIONS: Dictionary<string> = {
[USB_PRODUCT_ID_BCM2708_BOOT]: 'Compute Module 1',
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3',
};
const startScanning = () => {
driveScanner.on('attach', (drive) => addDrive(drive));
driveScanner.on('detach', (drive) => removeDrive(drive));
driveScanner.start();
};
const stopScanning = () => {
driveScanner.stop();
};
export { startScanning, stopScanning };

View File

@@ -0,0 +1,93 @@
/** Get metadata for a source */
import { sourceDestination } from 'etcher-sdk';
import { replaceWindowsNetworkDriveLetter } from '../gui/app/os/windows-network-drives';
import axios, { AxiosRequestConfig } from 'axios';
import { isJson } from '../shared/utils';
import * as path from 'path';
import {
SourceMetadata,
Authentication,
Source,
} from '../shared/typings/source-selector';
import { DrivelistDrive } from '../shared/drive-constraints';
import { omit } from 'lodash';
function isString(value: any): value is string {
return typeof value === 'string';
}
async function createSource(
selected: string,
SourceType: Source,
auth?: Authentication,
) {
try {
selected = await replaceWindowsNetworkDriveLetter(selected);
} catch (error: any) {
// TODO: 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 === 'File') {
return new sourceDestination.File({
path: selected,
});
}
return new sourceDestination.Http({ url: selected, auth });
}
async function getMetadata(
source: sourceDestination.SourceDestination,
selected: string | DrivelistDrive,
) {
const metadata = (await source.getMetadata()) as SourceMetadata;
const partitionTable = await source.getPartitionTable();
if (partitionTable) {
metadata.hasMBR = true;
metadata.partitions = partitionTable.partitions;
} else {
metadata.hasMBR = false;
}
if (isString(selected)) {
metadata.extension = path.extname(selected).slice(1);
metadata.path = selected;
}
return metadata;
}
async function getSourceMetadata(
selected: string | DrivelistDrive,
SourceType: Source,
auth?: Authentication,
) {
if (isString(selected)) {
const source = await createSource(selected, SourceType, auth);
try {
const innerSource = await source.getInnerSource();
const metadata = await getMetadata(innerSource, selected);
return metadata;
} catch (error: any) {
// TODO: handle error
} finally {
try {
await source.close();
} catch (error: any) {
// Noop
}
}
}
}
export { getSourceMetadata };

33
lib/util/types/types.d.ts vendored Normal file
View File

@@ -0,0 +1,33 @@
import { Metadata } from 'etcher-sdk/build/source-destination';
import { SourceMetadata } from '../../shared/typings/source-selector';
import { Drive as DrivelistDrive } from 'drivelist';
export interface WriteResult {
bytesWritten?: number;
devices?: {
failed: number;
successful: number;
};
errors: FlashError[];
sourceMetadata?: Metadata;
}
export interface FlashError extends Error {
description: string;
device: string;
code: string;
}
export interface FlashResults extends WriteResult {
skip?: boolean;
cancelled?: boolean;
}
interface WriteOptions {
image: SourceMetadata;
destinations: DrivelistDrive[];
autoBlockmapping: boolean;
decompressFirst: boolean;
SourceType: string;
httpRequest?: any;
}

40607
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "balena-etcher",
"private": true,
"displayName": "balenaEtcher",
"version": "1.5.109",
"version": "1.18.13",
"packageType": "local",
"main": "generated/etcher.js",
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
@@ -13,22 +13,27 @@
"url": "git@github.com:balena-io/etcher.git"
},
"scripts": {
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
"build": "npm run webpack && npm run build:sidecar",
"build:rebuild-mountutils": "cd node_modules/mountutils && npm rebuild",
"build:sidecar": "npm run build:rebuild-mountutils && tsc --project tsconfig.sidecar.json && pkg build/util/api.js -c pkg-sidecar.json --target node18 --output generated/etcher-util",
"flowzone-preinstall-linux": "sudo apt-get update && sudo apt-get install -y xvfb libudev-dev && cat < electron-builder.yml | yq e .deb.depends[] - | xargs -L1 echo | sed 's/|//g' | xargs -L1 sudo apt-get --ignore-missing install || true",
"flowzone-preinstall-macos": "true",
"flowzone-preinstall-windows": "npx node-gyp install",
"flowzone-preinstall": "npm run flowzone-preinstall-linux",
"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 && npm run lint-spell",
"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 --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 --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",
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
"lint": "npm run lint-ts && npm run lint-css",
"postinstall": "electron-rebuild -t prod,dev,optional",
"sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh",
"start": "./node_modules/.bin/electron .",
"postshrinkwrap": "ts-node ./scripts/clean-shrinkwrap.ts",
"webpack": "webpack",
"watch": "webpack --watch",
"concourse-build-electron": "npm run webpack",
"concourse-test": "npx npm@6.14.5 test",
"concourse-test-electron": "npx npm@6.14.5 test"
"test-gui": "electron-mocha --recursive --reporter spec --window-config tests/gui/window-config.json --require ts-node/register/transpile-only --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-macos": "npm run lint && npm run test-gui && npm run test-shared && npm run sanity-checks",
"test-linux": "npm run lint && xvfb-run --auto-servernum npm run test-gui && xvfb-run --auto-servernum npm run test-shared && npm run sanity-checks",
"test-windows": "npm run lint && npm run test-gui && npm run test-shared && npm run sanity-checks",
"test": "echo npm run test-{linux,windows,macos}",
"watch": "webpack serve --no-optimization-minimize --config ./webpack.dev.config.ts",
"webpack": "webpack"
},
"husky": {
"hooks": {
@@ -43,74 +48,88 @@
"npm run lint-css"
]
},
"author": "Balena Inc. <hello@etcher.io>",
"author": "Balena Ltd. <hello@balena.io>",
"license": "Apache-2.0",
"platformSpecificDependencies": [
"fsevents",
"winusb-driver-generator"
],
"devDependencies": {
"@balena/lint": "^5.0.4",
"@fortawesome/fontawesome-free": "^5.13.1",
"@svgr/webpack": "^5.4.0",
"@types/chai": "^4.2.7",
"@types/copy-webpack-plugin": "^6.0.0",
"@types/mime-types": "^2.1.0",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/mocha": "^8.0.3",
"@types/node": "^12.12.39",
"@types/node-ipc": "^9.1.2",
"@types/react-dom": "^16.8.4",
"@types/semver": "^7.1.0",
"@types/sinon": "^9.0.0",
"@types/terser-webpack-plugin": "^4.1.0",
"@types/tmp": "^0.2.0",
"@types/webpack-node-externals": "^2.5.0",
"chai": "^4.2.0",
"copy-webpack-plugin": "^6.0.1",
"css-loader": "^4.2.1",
"d3": "^4.13.0",
"debug": "^4.2.0",
"electron": "9.2.1",
"electron-builder": "^22.7.0",
"electron-mocha": "^9.1.0",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^1.11.0",
"electron-updater": "^4.3.2",
"etcher-sdk": "^4.1.30",
"file-loader": "^6.0.0",
"husky": "^4.2.5",
"immutable": "^3.8.1",
"lint-staged": "^10.2.2",
"lodash": "^4.17.10",
"mini-css-extract-plugin": "^0.10.0",
"mocha": "^8.0.1",
"native-addon-loader": "^2.0.1",
"node-ipc": "^9.1.1",
"omit-deep-lodash": "1.1.4",
"outdent": "^0.7.1",
"path-is-inside": "^1.0.2",
"pretty-bytes": "^5.3.0",
"react": "^16.8.5",
"react-dom": "^16.8.5",
"redux": "^4.0.5",
"rendition": "^18.8.3",
"resin-corvus": "^2.0.5",
"semver": "^7.3.2",
"simple-progress-webpack-plugin": "^1.1.2",
"sinon": "^9.0.2",
"spectron": "^11.0.0",
"string-replace-loader": "^2.3.0",
"styled-components": "^5.1.0",
"sudo-prompt": "github:zvin/sudo-prompt#workaround-windows-amperstand-in-username",
"sys-class-rgb-led": "^2.1.0",
"tmp": "^0.2.1",
"ts-loader": "^8.0.0",
"ts-node": "^9.0.0",
"tslib": "^2.0.0",
"typescript": "^4.0.2",
"uuid": "^8.1.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9"
"@babel/register": "^7.22.15",
"@balena/lint": "5.4.2",
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
"@electron/remote": "^2.0.9",
"@fortawesome/fontawesome-free": "5.15.4",
"@sentry/electron": "^4.1.2",
"@svgr/webpack": "5.5.0",
"@types/chai": "4.3.4",
"@types/copy-webpack-plugin": "6.4.3",
"@types/mime-types": "2.1.1",
"@types/mini-css-extract-plugin": "1.4.3",
"@types/mocha": "^9.1.1",
"@types/node": "^16.18.12",
"@types/node-ipc": "9.2.0",
"@types/react": "16.14.34",
"@types/react-dom": "16.9.17",
"@types/semver": "7.3.13",
"@types/sinon": "9.0.11",
"@types/terser-webpack-plugin": "5.0.4",
"@types/tmp": "0.2.3",
"@types/webpack-node-externals": "2.5.3",
"analytics-client": "^2.0.1",
"axios": "^0.27.2",
"chai": "4.3.7",
"copy-webpack-plugin": "7.0.0",
"css-loader": "5.2.7",
"d3": "4.13.0",
"debug": "4.3.4",
"electron": "^25.8.2",
"electron-builder": "^23.6.0",
"electron-mocha": "^11.0.2",
"electron-notarize": "1.2.2",
"electron-rebuild": "^3.2.9",
"electron-updater": "5.3.0",
"esbuild-loader": "2.20.0",
"etcher-sdk": "8.3.1",
"file-loader": "6.2.0",
"husky": "4.3.8",
"i18next": "21.10.0",
"immutable": "3.8.2",
"lint-staged": "10.5.4",
"lodash": "4.17.21",
"mini-css-extract-plugin": "1.6.2",
"mocha": "^9.1.1",
"native-addon-loader": "2.0.1",
"node-ipc": "9.2.1",
"omit-deep-lodash": "1.1.7",
"outdent": "0.8.0",
"path-is-inside": "1.0.2",
"pkg": "^5.8.1",
"pnp-webpack-plugin": "1.7.0",
"pretty-bytes": "5.6.0",
"react": "16.8.5",
"react-dom": "16.8.5",
"react-i18next": "11.18.6",
"redux": "4.2.0",
"rendition": "19.3.2",
"semver": "7.3.8",
"simple-progress-webpack-plugin": "1.1.2",
"sinon": "9.2.4",
"string-replace-loader": "3.1.0",
"style-loader": "2.0.0",
"styled-components": "5.3.6",
"sys-class-rgb-led": "3.0.1",
"terser-webpack-plugin": "5.3.6",
"ts-loader": "8.4.0",
"ts-node": "9.1.1",
"tslib": "2.4.1",
"typescript": "4.4.4",
"url-loader": "4.1.1",
"uuid": "8.3.2",
"webpack": "5.75.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.11.1"
},
"engines": {
"node": ">=18 <20"
},
"versionist": {
"publishedAt": "2023-10-16T13:32:27.552Z"
}
}

10
pkg-sidecar.json Normal file
View File

@@ -0,0 +1,10 @@
{
"assets": [
"node_modules/usb/**",
"node_modules/lzma-native/**",
"node_modules/drivelist/**",
"node_modules/mountutils/**",
"node_modules/winusb-driver-generator/**",
"node_modules/node-raspberrypi-usbboot/**"
]
}

View File

@@ -1,11 +1,21 @@
---
type: electron
release: github
publishMetadata: true
sentry:
org: balenaetcher
team: resinio
type: electron
org: balenaetcher
team: resinio
type: electron
triggerNotification:
version: 1.5.81
stagingPercentage: 100
version: 1.7.9
stagingPercentage: 100
upstream:
- repo: etcher-sdk
url: https://github.com/balena-io-modules/etcher-sdk
module: etcher-sdk
- repo: sys-class-rgb-led
url: https://github.com/balena-io-modules/sys-class-rgb-led
module: sys-class-rgb-led
- repo: rendition
url: https://github.com/balena-io-modules/rendition
module: rendition

View File

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

Some files were not shown because too many files have changed in this diff Show More