mirror of
https://github.com/balena-io/etcher.git
synced 2025-09-10 05:28:31 +00:00
Compare commits
375 Commits
save-url-i
...
v1.10.28
Author | SHA1 | Date | |
---|---|---|---|
![]() |
620307568f | ||
![]() |
a349c5d9ac | ||
![]() |
0d740ad12d | ||
![]() |
85a3f28869 | ||
![]() |
dbd5397405 | ||
![]() |
85c183b9ef | ||
![]() |
0d0af1d1dd | ||
![]() |
ad423fc187 | ||
![]() |
d8b2a7a236 | ||
![]() |
13ec8cbe98 | ||
![]() |
a7cae23612 | ||
![]() |
86bb093f3d | ||
![]() |
997e1eb2f2 | ||
![]() |
34cc8b8933 | ||
![]() |
f26b074811 | ||
![]() |
adaa07b4b0 | ||
![]() |
96f4569342 | ||
![]() |
be190c6c80 | ||
![]() |
809617a82d | ||
![]() |
df02732002 | ||
![]() |
d35f3c3049 | ||
![]() |
8b047e3b14 | ||
![]() |
fa41d21e27 | ||
![]() |
54e6c5e2c1 | ||
![]() |
43fc3dd7eb | ||
![]() |
12a1340c8e | ||
![]() |
cf8b5790a1 | ||
![]() |
659d85a833 | ||
![]() |
96c44d31c9 | ||
![]() |
ba812b4f64 | ||
![]() |
4087258fbd | ||
![]() |
955be13129 | ||
![]() |
32011c0dea | ||
![]() |
b68325c71c | ||
![]() |
84bce86fce | ||
![]() |
d68eab1dda | ||
![]() |
09cf014d14 | ||
![]() |
d5bab5805f | ||
![]() |
b5ab500a14 | ||
![]() |
49253d37c9 | ||
![]() |
97cf3b25ad | ||
![]() |
99862b95a5 | ||
![]() |
8b765d58e5 | ||
![]() |
8f566e45b8 | ||
![]() |
b8af86e30c | ||
![]() |
784f193b6d | ||
![]() |
3967adb1b5 | ||
![]() |
0667d1110f | ||
![]() |
61dd22bdf3 | ||
![]() |
24eb8b05b0 | ||
![]() |
6991a4950b | ||
![]() |
bb169cf674 | ||
![]() |
e5d0d2e262 | ||
![]() |
72b4d4f4fa | ||
![]() |
9b2f2eb4c3 | ||
![]() |
ce52ef95a9 | ||
![]() |
aa3756ad17 | ||
![]() |
73081e726d | ||
![]() |
d53dc4149b | ||
![]() |
0d5bb4935f | ||
![]() |
14aeb0060b | ||
![]() |
239726f3ce | ||
![]() |
4ed3002716 | ||
![]() |
7286fba240 | ||
![]() |
895c306fb7 | ||
![]() |
f3844d56e2 | ||
![]() |
540dc3150a | ||
![]() |
035c8dfec3 | ||
![]() |
03d6a011db | ||
![]() |
27f64650f9 | ||
![]() |
ccca009972 | ||
![]() |
57a6ceff0e | ||
![]() |
30c4baa58b | ||
![]() |
a930d77064 | ||
![]() |
0d1cfffa5c | ||
![]() |
3c7422764c | ||
![]() |
55176b9f8f | ||
![]() |
156b9314b5 | ||
![]() |
76d22280dc | ||
![]() |
e4251a3862 | ||
![]() |
831339bd2c | ||
![]() |
952ea80e15 | ||
![]() |
813c497e4b | ||
![]() |
1b5b647135 | ||
![]() |
7de99003ca | ||
![]() |
e09bdd734b | ||
![]() |
306e087ec6 | ||
![]() |
c6b0178a87 | ||
![]() |
4e581ea1ce | ||
![]() |
26dc2d19e5 | ||
![]() |
b99282acfb | ||
![]() |
4e48724d0c | ||
![]() |
448ce141d5 | ||
![]() |
695f287190 | ||
![]() |
4de3271e15 | ||
![]() |
77b33b127d | ||
![]() |
9cd13ba381 | ||
![]() |
9df23c8a3f | ||
![]() |
e3618b939e | ||
![]() |
6a39f5869a | ||
![]() |
fd472efadc | ||
![]() |
7e2c2eae63 | ||
![]() |
5266571ca4 | ||
![]() |
797868c474 | ||
![]() |
2c2a5c7c2b | ||
![]() |
9e536d5337 | ||
![]() |
860e680dd9 | ||
![]() |
7bb52aa170 | ||
![]() |
1c370f9100 | ||
![]() |
ec7c772d0b | ||
![]() |
cc0285a77d | ||
![]() |
256d3550d1 | ||
![]() |
db3a5f3b0a | ||
![]() |
0e58edf113 | ||
![]() |
db136926a9 | ||
![]() |
d84e7211be | ||
![]() |
8357cc19d2 | ||
![]() |
2752b9fa95 | ||
![]() |
0214be4953 | ||
![]() |
a4f944e795 | ||
![]() |
cd2ebf15fc | ||
![]() |
7a7ea374e9 | ||
![]() |
330df325f9 | ||
![]() |
2fc0882b2e | ||
![]() |
4dd779e010 | ||
![]() |
3dc54405fe | ||
![]() |
3f1aa5bac3 | ||
![]() |
8f52fdb900 | ||
![]() |
1b93891ed8 | ||
![]() |
33adc8ecf8 | ||
![]() |
0455f7ea58 | ||
![]() |
ea5a167f4f | ||
![]() |
8a1c4a4cc8 | ||
![]() |
bd8bc81713 | ||
![]() |
98a5ddf58a | ||
![]() |
6223dbc541 | ||
![]() |
7c56621c57 | ||
![]() |
a61aa8e2be | ||
![]() |
7df4f9615b | ||
![]() |
5742452fdf | ||
![]() |
fe09f9f862 | ||
![]() |
3a4687ea0f | ||
![]() |
db6490fb1b | ||
![]() |
1642297101 | ||
![]() |
5ecd223cfc | ||
![]() |
306e40fd7b | ||
![]() |
b58249b9c8 | ||
![]() |
b23b4b34d0 | ||
![]() |
73bc921713 | ||
![]() |
f356e4c303 | ||
![]() |
9888167f2e | ||
![]() |
4561690478 | ||
![]() |
576113febf | ||
![]() |
cc139bf750 | ||
![]() |
ae91958c06 | ||
![]() |
33dea6267f | ||
![]() |
c9a8bca96f | ||
![]() |
8af376e608 | ||
![]() |
9ab307df4f | ||
![]() |
e8a716f8bb | ||
![]() |
a40e64f6cd | ||
![]() |
2e53feb38c | ||
![]() |
5945ab1f50 | ||
![]() |
59d67220d4 | ||
![]() |
61610ded84 | ||
![]() |
c87a132f40 | ||
![]() |
350d4de32b | ||
![]() |
f5f9025d6d | ||
![]() |
549d744d04 | ||
![]() |
6194460dc2 | ||
![]() |
8370f638b4 | ||
![]() |
ac34c51125 | ||
![]() |
b241470fe1 | ||
![]() |
179697040c | ||
![]() |
335766ed12 | ||
![]() |
4c5d052a71 | ||
![]() |
86423342a8 | ||
![]() |
d8b41552e3 | ||
![]() |
11c65fb392 | ||
![]() |
bed126506f | ||
![]() |
f6aeb52b16 | ||
![]() |
a5201942b8 | ||
![]() |
c1f7164273 | ||
![]() |
6774bf784c | ||
![]() |
56ec8b4eac | ||
![]() |
35868509af | ||
![]() |
3ab6749f49 | ||
![]() |
7a012a92bc | ||
![]() |
aba01825a0 | ||
![]() |
907a3308de | ||
![]() |
4366bb372f | ||
![]() |
a6f6cd4a19 | ||
![]() |
03ee428039 | ||
![]() |
8d652d064d | ||
![]() |
28adc34239 | ||
![]() |
120e9bf42f | ||
![]() |
59f54e194b | ||
![]() |
c4834e61a7 | ||
![]() |
e4d02bc561 | ||
![]() |
b9e54e39f7 | ||
![]() |
f3c32eac65 | ||
![]() |
9a303ab344 | ||
![]() |
9c1b55bebc | ||
![]() |
30ae4bbd86 | ||
![]() |
c6126a980a | ||
![]() |
ef90d048ca | ||
![]() |
b938132038 | ||
![]() |
3cb2e78fe7 | ||
![]() |
ea9875ddf0 | ||
![]() |
65dacd2ff2 | ||
![]() |
a190818827 | ||
![]() |
98e33b619b | ||
![]() |
685ed715ac | ||
![]() |
3cf3c4b398 | ||
![]() |
1c2ef4b1d4 | ||
![]() |
d22fc91585 | ||
![]() |
0a28af5c35 | ||
![]() |
0c1e5b88ef | ||
![]() |
790201be90 | ||
![]() |
d8d379f05e | ||
![]() |
b5e9701048 | ||
![]() |
292f86d6f5 | ||
![]() |
76ca9934c8 | ||
![]() |
37b826ee4e | ||
![]() |
1e1bd3c508 | ||
![]() |
00e8f11913 | ||
![]() |
a3c24a26a0 | ||
![]() |
4232928ad8 | ||
![]() |
b165fb78da | ||
![]() |
e9f6c5ead9 | ||
![]() |
b2d0c1c9dd | ||
![]() |
14d91400a4 | ||
![]() |
d0114aece7 | ||
![]() |
dff2df4aab | ||
![]() |
13159f93ee | ||
![]() |
3ece1fd841 | ||
![]() |
f46963b6b3 | ||
![]() |
b97f4e0031 | ||
![]() |
e2d233d74b | ||
![]() |
a7ca2e527b | ||
![]() |
396a053c0a | ||
![]() |
d1a3f1cb88 | ||
![]() |
9f96558cdd | ||
![]() |
b3bc589d70 | ||
![]() |
18d2c28110 | ||
![]() |
b272ef296d | ||
![]() |
32ca28a3a9 | ||
![]() |
4d5e5a3b0b | ||
![]() |
8b3f37102d | ||
![]() |
4b74253631 | ||
![]() |
a81b552b95 | ||
![]() |
53f53c0f75 | ||
![]() |
fdaf5c69d6 | ||
![]() |
061afca5d3 | ||
![]() |
ccb08a48f1 | ||
![]() |
a8f3d45b12 | ||
![]() |
7e333caaf9 | ||
![]() |
70229e8684 | ||
![]() |
261700389b | ||
![]() |
250aed2eb1 | ||
![]() |
ed1f008fe2 | ||
![]() |
e9ce270dab | ||
![]() |
1ee110bc95 | ||
![]() |
33dd07c675 | ||
![]() |
39ccbbeeda | ||
![]() |
55d2400ac7 | ||
![]() |
0bdea5c54c | ||
![]() |
3be372d49f | ||
![]() |
d0c66b2c48 | ||
![]() |
65082c4790 | ||
![]() |
e87ed9beed | ||
![]() |
bc5563d9c2 | ||
![]() |
ad83ab5dcc | ||
![]() |
0dc1cf9701 | ||
![]() |
11489c6538 | ||
![]() |
2619d4bc86 | ||
![]() |
3730efd350 | ||
![]() |
6ece32c546 | ||
![]() |
fd9996a3cc | ||
![]() |
f06cc89152 | ||
![]() |
c1d7ab3fa9 | ||
![]() |
b206483c7c | ||
![]() |
c3eb8c7b56 | ||
![]() |
0849d4f435 | ||
![]() |
1dba3ae19b | ||
![]() |
f33f2e3771 | ||
![]() |
e56aaed973 | ||
![]() |
a4659f038e | ||
![]() |
cd462818da | ||
![]() |
37769efbed | ||
![]() |
0f70c4bbce | ||
![]() |
48b5e8b9d9 | ||
![]() |
1f138f0ecc | ||
![]() |
73f67e99ca | ||
![]() |
9114da2445 | ||
![]() |
554bbcc780 | ||
![]() |
4db2289cfd | ||
![]() |
c15b56bc23 | ||
![]() |
9f52dda6ae | ||
![]() |
fadcefb11a | ||
![]() |
361c32913c | ||
![]() |
5c2042198e | ||
![]() |
99df53098c | ||
![]() |
aa563c87bd | ||
![]() |
1188888956 | ||
![]() |
f9d7991dc8 | ||
![]() |
53954e81fd | ||
![]() |
f82996bfd1 | ||
![]() |
b74069eb41 | ||
![]() |
e8c7591751 | ||
![]() |
3521b61a81 | ||
![]() |
93db90c725 | ||
![]() |
1dc56aed14 | ||
![]() |
d814202424 | ||
![]() |
c54856a616 | ||
![]() |
fc45df270a | ||
![]() |
3cde2faed0 | ||
![]() |
b4b8c89aad | ||
![]() |
36d05724c0 | ||
![]() |
b1e4e681d1 | ||
![]() |
3987078c11 | ||
![]() |
de0010eb72 | ||
![]() |
1f94f44b18 | ||
![]() |
fe0b45cae6 | ||
![]() |
c32e485f27 | ||
![]() |
409b78fc21 | ||
![]() |
2f08142f5a | ||
![]() |
8c4edaabba | ||
![]() |
05497ce85c | ||
![]() |
d3df2fe57e | ||
![]() |
a0f07082f2 | ||
![]() |
b7efa8e1f0 | ||
![]() |
3647457bb5 | ||
![]() |
2e5a39dcd8 | ||
![]() |
edabacfb3a | ||
![]() |
f46176fd10 | ||
![]() |
2158e20380 | ||
![]() |
fa593e33d1 | ||
![]() |
50730bd3df | ||
![]() |
4e68955981 | ||
![]() |
3c0084d012 | ||
![]() |
8bd11a01ae | ||
![]() |
da3a22d0f6 | ||
![]() |
e708212d41 | ||
![]() |
a5ceba8435 | ||
![]() |
446e8e1253 | ||
![]() |
c69b2fa053 | ||
![]() |
0597c0e908 | ||
![]() |
af2b6bc8ca | ||
![]() |
a2c7a542df | ||
![]() |
e37ae2743f | ||
![]() |
644d955f08 | ||
![]() |
e7b4f09021 | ||
![]() |
1e0a6a3129 | ||
![]() |
ef3b8915d8 | ||
![]() |
e58cfd89c5 | ||
![]() |
1c52379ee3 | ||
![]() |
e2c2b40690 | ||
![]() |
bddb89e4a1 | ||
![]() |
560ed91e2e | ||
![]() |
1f8f7ad7f8 | ||
![]() |
a2a0f2ef41 | ||
![]() |
40e5fb2287 | ||
![]() |
6c49c71b3f | ||
![]() |
deb3db0fff | ||
![]() |
4872fa3d6e | ||
![]() |
640a7409ee | ||
![]() |
a7637ad8d4 | ||
![]() |
31409c61ca | ||
![]() |
e74dc9eb60 | ||
![]() |
06997fdf29 | ||
![]() |
611e659626 | ||
![]() |
e484ae9837 | ||
![]() |
7e7ca9524e | ||
![]() |
db09b7440d |
@@ -7,7 +7,6 @@ indent_size = 2
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
# default
|
||||||
|
* text
|
||||||
|
|
||||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||||
*.js text eol=lf
|
*.js text eol=lf
|
||||||
*.jsx text eol=lf
|
*.jsx text eol=lf
|
||||||
@@ -27,6 +30,7 @@ Makefile text
|
|||||||
*.yml text
|
*.yml text
|
||||||
*.patch text
|
*.patch text
|
||||||
*.txt text
|
*.txt text
|
||||||
|
*.tpl text
|
||||||
CODEOWNERS text
|
CODEOWNERS text
|
||||||
*.plist text
|
*.plist text
|
||||||
|
|
||||||
@@ -58,3 +62,7 @@ CODEOWNERS text
|
|||||||
*.ttf binary diff=hex
|
*.ttf binary diff=hex
|
||||||
xz-without-extension binary diff=hex
|
xz-without-extension binary diff=hex
|
||||||
wmic-output.txt binary diff=hex
|
wmic-output.txt binary diff=hex
|
||||||
|
|
||||||
|
# gitsecret
|
||||||
|
*.secret binary
|
||||||
|
.gitsecret/** binary
|
||||||
|
7
.github/ISSUE_TEMPLATE.md
vendored
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,11 @@
|
|||||||
- **Etcher version:**
|
- **Etcher version:**
|
||||||
- **Operating system and architecture:**
|
- **Operating system and architecture:**
|
||||||
- **Image flashed:**
|
- **Image flashed:**
|
||||||
|
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
|
||||||
|
- **What happened:**
|
||||||
- **Do you see any meaningful error information in the DevTools?**
|
- **Do you see any meaningful error information in the DevTools?**
|
||||||
|
|
||||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
|
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
|
||||||
|
|
||||||
|
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
|
||||||
|
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
|
||||||
|
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->
|
||||||
|
214
.github/actions/publish/action.yml
vendored
Normal file
214
.github/actions/publish/action.yml
vendored
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
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: "14.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
|
||||||
|
shell: pwsh
|
||||||
|
working-directory: .
|
||||||
|
run: tar -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'
|
||||||
|
|
||||||
|
# FIXME: resinci-deploy is not actively maintained
|
||||||
|
# https://github.com/product-os/resinci-deploy
|
||||||
|
- name: Checkout resinci-deploy
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: product-os/resinci-deploy
|
||||||
|
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||||
|
path: resinci-deploy
|
||||||
|
|
||||||
|
- name: Build and install resinci-deploy
|
||||||
|
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:]')"
|
||||||
|
|
||||||
|
rm -rf ../resinci-deploy && mv resinci-deploy ..
|
||||||
|
|
||||||
|
pushd ../resinci-deploy && npm ci && npm link && popd
|
||||||
|
|
||||||
|
if [[ $runner_os =~ linux|macos ]]; then
|
||||||
|
chmod +x "$(dirname "$(which node)")/resinci-deploy" && which resinci-deploy
|
||||||
|
fi
|
||||||
|
|
||||||
|
# FIXME: store sentry workflow is not documented
|
||||||
|
# https://github.com/product-os/resinci-deploy/blob/master/lib/sentry.ts
|
||||||
|
# https://github.com/getsentry/sentry-cli
|
||||||
|
# https://docs.sentry.io/api/projects/create-a-new-client-key/
|
||||||
|
- name: Generate Sentry DSN
|
||||||
|
id: sentry
|
||||||
|
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||||
|
run: |
|
||||||
|
set -ea
|
||||||
|
|
||||||
|
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||||
|
|
||||||
|
branch="$(echo '${{ github.event.pull_request.head.ref }}' | sed 's/[^[:alnum:]]/-/g')"
|
||||||
|
|
||||||
|
stdout="$(resinci-deploy store sentry \
|
||||||
|
--branch="${branch}" \
|
||||||
|
--name="$(jq -r '.name' package.json)" \
|
||||||
|
--team="$(yq e '.sentry.team' repo.yml)" \
|
||||||
|
--org="$(yq e '.sentry.org' repo.yml)" \
|
||||||
|
--type="$(yq e '.sentry.type' repo.yml)")"
|
||||||
|
|
||||||
|
echo "dsn=$(echo "${stdout}" | tail -n 1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
env:
|
||||||
|
SENTRY_TOKEN: ${{ fromJSON(inputs.secrets).SENTRY_AUTH_TOKEN }}
|
||||||
|
|
||||||
|
# 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='${{ steps.sentry.outputs.dsn }}' \
|
||||||
|
--c.extraMetadata.analytics.mixpanel.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
|
58
.github/actions/test/action.yml
vendored
Normal file
58
.github/actions/test/action.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
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: "14.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
|
||||||
|
shell: pwsh
|
||||||
|
run: tar -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
|
29
.github/workflows/flowzone.yml
vendored
Normal file
29
.github/workflows/flowzone.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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-18.04","macos-latest","windows-2019"]'
|
||||||
|
restrict_custom_actions: false
|
||||||
|
github_prerelease: true
|
||||||
|
repo_config: true
|
||||||
|
repo_description: "Flash OS images to SD cards & USB drives, safely and easily."
|
||||||
|
repo_homepage: https://etcher.io/
|
||||||
|
repo_enable_wiki: true
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -51,3 +51,9 @@ node_modules
|
|||||||
# VSCode files
|
# VSCode files
|
||||||
|
|
||||||
.vscode
|
.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
BIN
.gitsecret/keys/pubring.kbx
Normal file
Binary file not shown.
BIN
.gitsecret/keys/pubring.kbx~
Normal file
BIN
.gitsecret/keys/pubring.kbx~
Normal file
Binary file not shown.
BIN
.gitsecret/keys/trustdb.gpg
Normal file
BIN
.gitsecret/keys/trustdb.gpg
Normal file
Binary file not shown.
5
.gitsecret/paths/mapping.cfg
Normal file
5
.gitsecret/paths/mapping.cfg
Normal 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,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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
12483
.versionbot/CHANGELOG.yml
Normal file
12483
.versionbot/CHANGELOG.yml
Normal file
File diff suppressed because it is too large
Load Diff
1102
CHANGELOG.md
1102
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
* @thundron @zvin @jviotti
|
|
||||||
/scripts @nazrhom
|
|
4
FAQ.md
4
FAQ.md
@@ -37,10 +37,10 @@ modules=xwayland.so
|
|||||||
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
|
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
|
||||||
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
|
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
|
||||||
|
|
||||||
## I receive ”No polkit authentication agent found” error in GNU/Linux
|
## I receive "No polkit authentication agent found" error in GNU/Linux
|
||||||
|
|
||||||
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
|
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
|
||||||
|
|
||||||
## May I run Etcher in older macOS versions?
|
## May I run Etcher in older macOS versions?
|
||||||
|
|
||||||
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.9 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||||
|
15
Makefile
15
Makefile
@@ -3,7 +3,7 @@
|
|||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
|
|
||||||
RESIN_SCRIPTS ?= ./scripts/resin
|
RESIN_SCRIPTS ?= ./scripts/resin
|
||||||
export NPM_VERSION ?= 6.14.5
|
export NPM_VERSION ?= 6.14.8
|
||||||
S3_BUCKET = artifacts.ci.balena-cloud.com
|
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||||
|
|
||||||
# This directory will be completely deleted by the `clean` rule
|
# This directory will be completely deleted by the `clean` rule
|
||||||
@@ -66,6 +66,9 @@ else
|
|||||||
ifeq ($(shell uname -m),x86_64)
|
ifeq ($(shell uname -m),x86_64)
|
||||||
HOST_ARCH = x64
|
HOST_ARCH = x64
|
||||||
endif
|
endif
|
||||||
|
ifeq ($(shell uname -m),arm64)
|
||||||
|
HOST_ARCH = aarch64
|
||||||
|
endif
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -86,11 +89,9 @@ TARGET_ARCH ?= $(HOST_ARCH)
|
|||||||
# Electron
|
# Electron
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
electron-develop:
|
electron-develop:
|
||||||
$(RESIN_SCRIPTS)/electron/install.sh \
|
git submodule update --init && \
|
||||||
-b $(shell pwd) \
|
npm ci && \
|
||||||
-r $(TARGET_ARCH) \
|
npm run webpack
|
||||||
-s $(PLATFORM) \
|
|
||||||
-m $(NPM_VERSION)
|
|
||||||
|
|
||||||
electron-test:
|
electron-test:
|
||||||
$(RESIN_SCRIPTS)/electron/test.sh \
|
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||||
@@ -125,7 +126,7 @@ TARGETS = \
|
|||||||
|
|
||||||
.PHONY: $(TARGETS)
|
.PHONY: $(TARGETS)
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
npm run lint
|
npm run lint
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
185
README.md
185
README.md
@@ -5,16 +5,15 @@
|
|||||||
Etcher is a powerful OS image flasher built with web technologies to ensure
|
Etcher is a powerful OS image flasher built with web technologies to ensure
|
||||||
flashing an SDCard or USB drive is a pleasant and safe experience. It protects
|
flashing an SDCard or USB drive is a pleasant and safe experience. It protects
|
||||||
you from accidentally writing to your hard-drives, ensures every byte of data
|
you from accidentally writing to your hard-drives, ensures every byte of data
|
||||||
was written correctly and much more. It can also flash directly Raspberry Pi devices that support the usbboot protocol
|
was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
|
||||||
|
|
||||||
[](https://balena.io/etcher)
|
[](https://balena.io/etcher)
|
||||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||||
[](https://david-dm.org/balena-io/etcher)
|
|
||||||
[](https://forums.balena.io/c/etcher)
|
[](https://forums.balena.io/c/etcher)
|
||||||
|
|
||||||
***
|
---
|
||||||
|
|
||||||
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones]
|
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
|
||||||
|
|
||||||
## Supported Operating Systems
|
## Supported Operating Systems
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ was written correctly and much more. It can also flash directly Raspberry Pi dev
|
|||||||
- macOS 10.10 (Yosemite) and later
|
- macOS 10.10 (Yosemite) and later
|
||||||
- Microsoft Windows 7 and later
|
- Microsoft Windows 7 and later
|
||||||
|
|
||||||
Note that Etcher will run on any platform officially supported by
|
**Note**: Etcher will run on any platform officially supported by
|
||||||
[Electron][electron]. Read more in their
|
[Electron][electron]. Read more in their
|
||||||
[documentation][electron-supported-platforms].
|
[documentation][electron-supported-platforms].
|
||||||
|
|
||||||
@@ -31,81 +30,118 @@ Note that Etcher will run on any platform officially supported by
|
|||||||
Refer to the [downloads page][etcher] for the latest pre-made
|
Refer to the [downloads page][etcher] for the latest pre-made
|
||||||
installers for all supported operating systems.
|
installers for all supported operating systems.
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
> [](https://cloudsmith.com) \
|
||||||
|
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
|
||||||
|
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
|
||||||
|
enables your organization to create, store and share packages in any format, to any place, with total
|
||||||
|
confidence.
|
||||||
|
|
||||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||||
|
|
||||||
1. Add Etcher debian repository:
|
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
|
||||||
|
|
||||||
```sh
|
1. Add Etcher Debian repository:
|
||||||
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Trust Bintray.com's GPG key:
|
```sh
|
||||||
|
curl -1sLf \
|
||||||
|
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
|
||||||
|
| sudo -E bash
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
2. Update and install:
|
||||||
sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys 379CE192D401AB61
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Update and install:
|
```sh
|
||||||
|
sudo apt-get update
|
||||||
```sh
|
sudo apt-get install balena-etcher-electron
|
||||||
sudo apt-get update
|
```
|
||||||
sudo apt-get install balena-etcher-electron
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Uninstall
|
##### Uninstall
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt-get remove balena-etcher-electron
|
sudo apt-get remove balena-etcher-electron
|
||||||
sudo rm /etc/apt/sources.list.d/balena-etcher.list
|
rm /etc/apt/sources.list.d/balena-etcher.list
|
||||||
sudo apt-get update
|
apt-get clean
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
apt-get update
|
||||||
```
|
```
|
||||||
|
|
||||||
##### OpenSUSE LEAP & Tumbleweed install
|
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
|
||||||
|
|
||||||
|
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
|
||||||
|
|
||||||
|
|
||||||
|
##### DNF
|
||||||
|
|
||||||
|
1. Add Etcher rpm repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -1sLf \
|
||||||
|
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||||
|
| sudo -E bash
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update and install:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo dnf install -y balena-etcher-electron
|
||||||
|
```
|
||||||
|
|
||||||
|
###### Uninstall
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper ar https://balena.io/etcher/static/etcher-rpm.repo
|
rm /etc/yum.repos.d/balena-etcher.repo
|
||||||
sudo zypper ref
|
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||||
sudo zypper in balena-etcher-electron
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Yum
|
||||||
|
|
||||||
|
1. Add Etcher rpm repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -1sLf \
|
||||||
|
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||||
|
| sudo -E bash
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update and install:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo yum install -y balena-etcher-electron
|
||||||
|
```
|
||||||
|
|
||||||
|
###### Uninstall
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo yum remove -y balena-etcher-electron
|
||||||
|
rm /etc/yum.repos.d/balena-etcher.repo
|
||||||
|
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OpenSUSE LEAP & Tumbleweed install (zypper)
|
||||||
|
|
||||||
|
1. Add the repo
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -1sLf \
|
||||||
|
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||||
|
| sudo -E bash
|
||||||
|
```
|
||||||
|
2. Update and install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo zypper up
|
||||||
|
sudo zypper install balena-etcher-electron
|
||||||
|
```
|
||||||
|
|
||||||
##### Uninstall
|
##### Uninstall
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo zypper rm balena-etcher-electron
|
sudo zypper rm balena-etcher-electron
|
||||||
```
|
# remove the repo
|
||||||
|
sudo zypper rr balena-etcher
|
||||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
sudo zypper rr balena-etcher-source
|
||||||
|
|
||||||
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)
|
#### Solus (GNU/Linux x64)
|
||||||
@@ -120,11 +156,10 @@ sudo eopkg it etcher
|
|||||||
sudo eopkg rm etcher
|
sudo eopkg rm etcher
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Arch Linux / Manjaro (GNU/Linux x64)
|
#### Arch/Manjaro Linux (GNU/Linux x64)
|
||||||
|
|
||||||
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
|
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
|
||||||
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yay -S balena-etcher
|
yay -S balena-etcher
|
||||||
```
|
```
|
||||||
@@ -135,22 +170,6 @@ yay -S balena-etcher
|
|||||||
yay -R balena-etcher
|
yay -R balena-etcher
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Brew Cask (macOS)
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
brew cask install balenaetcher
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Uninstall
|
|
||||||
|
|
||||||
```sh
|
|
||||||
brew cask uninstall balenaetcher
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Chocolatey (Windows)
|
#### Chocolatey (Windows)
|
||||||
|
|
||||||
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
|
||||||
@@ -168,20 +187,22 @@ choco uninstall etcher
|
|||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
If you're having any problem, please [raise an issue][newissue] on GitHub and
|
If you're having any problem, please [raise an issue][newissue] on GitHub, and
|
||||||
the balena.io team will be happy to help.
|
the balena.io team will be happy to help.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Etcher is free software, and may be redistributed under the terms specified in
|
Etcher is free software and may be redistributed under the terms specified in
|
||||||
the [license].
|
the [license].
|
||||||
|
|
||||||
[etcher]: https://balena.io/etcher
|
[etcher]: https://balena.io/etcher
|
||||||
[electron]: https://electronjs.org/
|
[electron]: https://electronjs.org/
|
||||||
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||||
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
[support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||||
[CONTRIBUTING]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||||
[USER-DOCUMENTATION]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||||
[milestones]: https://github.com/balena-io/etcher/milestones
|
[milestones]: https://github.com/balena-io/etcher/milestones
|
||||||
[newissue]: https://github.com/balena-io/etcher/issues/new
|
[newissue]: https://github.com/balena-io/etcher/issues/new
|
||||||
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
|
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
|
||||||
|
|
||||||
|
|
||||||
|
17
SUPPORT.md
17
SUPPORT.md
@@ -1,9 +1,16 @@
|
|||||||
Getting help with Etcher
|
Getting help with BalenaEtcher
|
||||||
========================
|
===============================
|
||||||
|
|
||||||
There are various ways to get support for Etcher if you experience an issue or
|
There are various ways to get support for Etcher if you experience an issue or
|
||||||
have an idea you'd like to share with us.
|
have an idea you'd like to share with us.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
------
|
||||||
|
|
||||||
|
We have answers to a variety of frequently asked questions in the [user
|
||||||
|
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
|
||||||
|
|
||||||
|
|
||||||
Forums
|
Forums
|
||||||
------
|
------
|
||||||
|
|
||||||
@@ -15,7 +22,7 @@ a look at the existing threads before opening a new one!
|
|||||||
Make sure to mention the following information to help us provide better
|
Make sure to mention the following information to help us provide better
|
||||||
support:
|
support:
|
||||||
|
|
||||||
- The Etcher version you're running.
|
- The BalenaEtcher version you're running.
|
||||||
|
|
||||||
- The operating system you're running Etcher in.
|
- The operating system you're running Etcher in.
|
||||||
|
|
||||||
@@ -25,10 +32,12 @@ support:
|
|||||||
GitHub
|
GitHub
|
||||||
------
|
------
|
||||||
|
|
||||||
If you encounter an issue or have a suggestion, head on over to Etcher's [issue
|
If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
|
||||||
tracker][issues] and if there isn't a ticket covering it, [create
|
tracker][issues] and if there isn't a ticket covering it, [create
|
||||||
one][new-issue].
|
one][new-issue].
|
||||||
|
|
||||||
[discourse]: https://forums.balena.io/c/etcher
|
[discourse]: https://forums.balena.io/c/etcher
|
||||||
[issues]: https://github.com/balena-io/etcher/issues
|
[issues]: https://github.com/balena-io/etcher/issues
|
||||||
[new-issue]: https://github.com/balena-io/etcher/issues/new
|
[new-issue]: https://github.com/balena-io/etcher/issues/new
|
||||||
|
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||||
|
[faq]: https://etcher.io
|
||||||
|
11
after-install.tpl
Normal file
11
after-install.tpl
Normal 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
|
@@ -10,13 +10,15 @@ async function main(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appName = context.packager.appInfo.productFilename
|
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({
|
await notarize({
|
||||||
appBundleId: 'io.balena.etcher',
|
appBundleId: 'io.balena.etcher',
|
||||||
appPath: `${appOutDir}/${appName}.app`,
|
appPath: `${appOutDir}/${appName}.app`,
|
||||||
appleId,
|
appleId,
|
||||||
appleIdPassword: `@keychain:Application Loader: ${appleId}`
|
appleIdPassword
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIN
assets/icon.icns
BIN
assets/icon.icns
Binary file not shown.
@@ -1,26 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
|
|
||||||
const cp = require('child_process');
|
|
||||||
const rimraf = require('rimraf');
|
|
||||||
const process = require('process');
|
|
||||||
|
|
||||||
// Rebuild native modules for ia32 and run webpack again for the ia32 part of windows packages
|
|
||||||
exports.default = function(context) {
|
|
||||||
if (context.platform.name === 'windows') {
|
|
||||||
cp.execFileSync(
|
|
||||||
'bash',
|
|
||||||
['./node_modules/.bin/electron-rebuild', '--types', 'dev', '--arch', context.arch],
|
|
||||||
);
|
|
||||||
rimraf.sync('generated');
|
|
||||||
cp.execFileSync(
|
|
||||||
'bash',
|
|
||||||
['./node_modules/.bin/webpack'],
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
npm_config_target_arch: context.arch,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -91,7 +91,7 @@ make electron-develop
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Build the GUI
|
# Build the GUI
|
||||||
make webpack
|
npm run webpack
|
||||||
# Start Electron
|
# Start Electron
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
@@ -159,6 +159,18 @@ pre-installed in all modern Windows versions.
|
|||||||
|
|
||||||
- Run `clean`. This command will completely clean your drive by erasing any
|
- Run `clean`. This command will completely clean your drive by erasing any
|
||||||
existent filesystem.
|
existent filesystem.
|
||||||
|
|
||||||
|
- Run `create partition primary`. This command will create a new partition.
|
||||||
|
|
||||||
|
- Run `active`. This command will active the partition.
|
||||||
|
|
||||||
|
- Run `list partition`. This command will show available partition.
|
||||||
|
|
||||||
|
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
|
||||||
|
|
||||||
|
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
|
||||||
|
|
||||||
|
- Run `exit` to quit diskpart.
|
||||||
|
|
||||||
### OS X
|
### OS X
|
||||||
|
|
||||||
|
@@ -1,11 +1,9 @@
|
|||||||
|
# https://www.electron.build/configuration/configuration
|
||||||
appId: io.balena.etcher
|
appId: io.balena.etcher
|
||||||
copyright: Copyright 2016-2020 Balena Ltd
|
copyright: Copyright 2016-2021 Balena Ltd
|
||||||
productName: balenaEtcher
|
productName: balenaEtcher
|
||||||
npmRebuild: true
|
afterPack: ./afterPack.js
|
||||||
nodeGypRebuild: false
|
afterSign: ./afterSignHook.js
|
||||||
publish: null
|
|
||||||
beforeBuild: "./beforeBuild.js"
|
|
||||||
afterPack: "./afterPack.js"
|
|
||||||
asar: false
|
asar: false
|
||||||
files:
|
files:
|
||||||
- generated
|
- generated
|
||||||
@@ -16,6 +14,9 @@ mac:
|
|||||||
hardenedRuntime: true
|
hardenedRuntime: true
|
||||||
entitlements: "entitlements.mac.plist"
|
entitlements: "entitlements.mac.plist"
|
||||||
entitlementsInherit: "entitlements.mac.plist"
|
entitlementsInherit: "entitlements.mac.plist"
|
||||||
|
artifactName: "${productName}-${version}.${ext}"
|
||||||
|
target:
|
||||||
|
- dmg
|
||||||
dmg:
|
dmg:
|
||||||
background: assets/dmg/background.tiff
|
background: assets/dmg/background.tiff
|
||||||
icon: assets/icon.icns
|
icon: assets/icon.icns
|
||||||
@@ -32,6 +33,10 @@ dmg:
|
|||||||
height: 405
|
height: 405
|
||||||
win:
|
win:
|
||||||
icon: assets/icon.ico
|
icon: assets/icon.ico
|
||||||
|
target:
|
||||||
|
- zip
|
||||||
|
- nsis
|
||||||
|
- portable
|
||||||
nsis:
|
nsis:
|
||||||
oneClick: true
|
oneClick: true
|
||||||
runAfterFinish: true
|
runAfterFinish: true
|
||||||
@@ -44,17 +49,23 @@ portable:
|
|||||||
artifactName: "${productName}-Portable-${version}.${ext}"
|
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||||
requestExecutionLevel: user
|
requestExecutionLevel: user
|
||||||
linux:
|
linux:
|
||||||
|
icon: assets/iconset
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- rpm
|
||||||
|
- deb
|
||||||
category: Utility
|
category: Utility
|
||||||
packageCategory: utils
|
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.
|
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:
|
deb:
|
||||||
priority: optional
|
priority: optional
|
||||||
|
compression: bzip2
|
||||||
depends:
|
depends:
|
||||||
- gconf2
|
|
||||||
- gconf-service
|
- gconf-service
|
||||||
- libappindicator1
|
- gconf2
|
||||||
- libasound2
|
- libasound2
|
||||||
- libatk1.0-0
|
- libatk1.0-0
|
||||||
- libc6
|
- libc6
|
||||||
@@ -88,6 +99,7 @@ deb:
|
|||||||
- libxss1
|
- libxss1
|
||||||
- libxtst6
|
- libxtst6
|
||||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||||
|
afterInstall: "./after-install.tpl"
|
||||||
rpm:
|
rpm:
|
||||||
depends:
|
depends:
|
||||||
- util-linux
|
- util-linux
|
||||||
|
@@ -23,17 +23,12 @@ import * as ReactDOM from 'react-dom';
|
|||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
|
||||||
import * as packageJSON from '../../../package.json';
|
import * as packageJSON from '../../../package.json';
|
||||||
import {
|
import { DrivelistDrive, isSourceDrive } from '../../shared/drive-constraints';
|
||||||
DrivelistDrive,
|
|
||||||
isDriveValid,
|
|
||||||
isSourceDrive,
|
|
||||||
} from '../../shared/drive-constraints';
|
|
||||||
import * as EXIT_CODES from '../../shared/exit-codes';
|
import * as EXIT_CODES from '../../shared/exit-codes';
|
||||||
import * as messages from '../../shared/messages';
|
import * as messages from '../../shared/messages';
|
||||||
import * as availableDrives from './models/available-drives';
|
import * as availableDrives from './models/available-drives';
|
||||||
import * as flashState from './models/flash-state';
|
import * as flashState from './models/flash-state';
|
||||||
import { init as ledsInit } from './models/leds';
|
import { deselectImage, getImage } from './models/selection-state';
|
||||||
import { deselectImage, getImage, selectDrive } from './models/selection-state';
|
|
||||||
import * as settings from './models/settings';
|
import * as settings from './models/settings';
|
||||||
import { Actions, observe, store } from './models/store';
|
import { Actions, observe, store } from './models/store';
|
||||||
import * as analytics from './modules/analytics';
|
import * as analytics from './modules/analytics';
|
||||||
@@ -42,6 +37,7 @@ import * as exceptionReporter from './modules/exception-reporter';
|
|||||||
import * as osDialog from './os/dialog';
|
import * as osDialog from './os/dialog';
|
||||||
import * as windowProgress from './os/window-progress';
|
import * as windowProgress from './os/window-progress';
|
||||||
import MainPage from './pages/main/MainPage';
|
import MainPage from './pages/main/MainPage';
|
||||||
|
import './css/main.css';
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
'unhandledrejection',
|
'unhandledrejection',
|
||||||
@@ -220,8 +216,7 @@ function prepareDrive(drive: Drive) {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
icon: 'warning',
|
icon: 'warning',
|
||||||
size: null,
|
size: null,
|
||||||
link:
|
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
|
||||||
'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
|
|
||||||
linkCTA: 'Install',
|
linkCTA: 'Install',
|
||||||
linkTitle: 'Install missing drivers',
|
linkTitle: 'Install missing drivers',
|
||||||
linkMessage: outdent`
|
linkMessage: outdent`
|
||||||
@@ -251,14 +246,6 @@ async function addDrive(drive: Drive) {
|
|||||||
const drives = getDrives();
|
const drives = getDrives();
|
||||||
drives[preparedDrive.device] = preparedDrive;
|
drives[preparedDrive.device] = preparedDrive;
|
||||||
setDrives(drives);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeDrive(drive: Drive) {
|
function removeDrive(drive: Drive) {
|
||||||
@@ -346,17 +333,31 @@ window.addEventListener('beforeunload', async (event) => {
|
|||||||
flashingWorkflowUuid,
|
flashingWorkflowUuid,
|
||||||
});
|
});
|
||||||
popupExists = false;
|
popupExists = false;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
exceptionReporter.report(error);
|
exceptionReporter.report(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
export async function main() {
|
||||||
await ledsInit();
|
try {
|
||||||
|
const { init: ledsInit } = require('./models/leds');
|
||||||
|
await ledsInit();
|
||||||
|
} catch (error: any) {
|
||||||
|
exceptionReporter.report(error);
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
React.createElement(MainPage),
|
React.createElement(MainPage),
|
||||||
document.getElementById('main'),
|
document.getElementById('main'),
|
||||||
|
// callback to set the correct zoomFactor for webviews as well
|
||||||
|
async () => {
|
||||||
|
const fullscreen = await settings.get('fullscreen');
|
||||||
|
const width = fullscreen ? window.screen.width : window.outerWidth;
|
||||||
|
try {
|
||||||
|
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
|
||||||
|
} catch (err) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
|
||||||
|
@@ -18,15 +18,7 @@ import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exc
|
|||||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||||
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
|
||||||
Flex,
|
|
||||||
ModalProps,
|
|
||||||
Txt,
|
|
||||||
Badge,
|
|
||||||
Link,
|
|
||||||
Table,
|
|
||||||
TableColumn,
|
|
||||||
} from 'rendition';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -43,10 +35,15 @@ import { getImage, isDriveSelected } from '../../models/selection-state';
|
|||||||
import { store } from '../../models/store';
|
import { store } from '../../models/store';
|
||||||
import { logEvent, logException } from '../../modules/analytics';
|
import { logEvent, logException } from '../../modules/analytics';
|
||||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||||
import { Alert, Modal, ScrollableFlex } from '../../styled-components';
|
import {
|
||||||
|
Alert,
|
||||||
|
GenericTableProps,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
} from '../../styled-components';
|
||||||
|
|
||||||
import DriveSVGIcon from '../../../assets/tgt.svg';
|
|
||||||
import { SourceMetadata } from '../source-selector/source-selector';
|
import { SourceMetadata } from '../source-selector/source-selector';
|
||||||
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
|
||||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||||
progress: number;
|
progress: number;
|
||||||
@@ -75,74 +72,29 @@ function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
|
|||||||
return typeof (drive as DrivelistDrive).size === 'number';
|
return typeof (drive as DrivelistDrive).size === 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
const DrivesTable = styled(({ refFn, ...props }) => (
|
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
|
||||||
<div>
|
<Table<Drive> {...props} />
|
||||||
<Table<Drive> ref={refFn} {...props} />
|
|
||||||
</div>
|
|
||||||
))`
|
))`
|
||||||
[data-display='table-head']
|
[data-display='table-head'],
|
||||||
> [data-display='table-row']
|
[data-display='table-body'] {
|
||||||
> [data-display='table-cell'] {
|
> [data-display='table-row'] > [data-display='table-cell'] {
|
||||||
position: sticky;
|
&:nth-child(2) {
|
||||||
top: 0;
|
width: 32%;
|
||||||
background-color: ${(props) => props.theme.colors.quartenary.light};
|
|
||||||
|
|
||||||
input[type='checkbox'] + div {
|
|
||||||
display: ${({ multipleSelection }) =>
|
|
||||||
multipleSelection ? 'flex' : 'none'};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
padding-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
width: 38%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3) {
|
|
||||||
width: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(4) {
|
|
||||||
width: 15%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(5) {
|
|
||||||
width: 32%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-display='table-body'] > [data-display='table-row'] {
|
|
||||||
> [data-display='table-cell']:first-child {
|
|
||||||
padding-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> [data-display='table-cell']:last-child {
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-highlight='true'] {
|
|
||||||
&.system {
|
|
||||||
background-color: ${(props) =>
|
|
||||||
props.showWarnings ? '#fff5e6' : '#e8f5fc'};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> [data-display='table-cell']:first-child {
|
&:nth-child(3) {
|
||||||
box-shadow: none;
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(5) {
|
||||||
|
width: 32%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&& [data-display='table-row'] > [data-display='table-cell'] {
|
|
||||||
padding: 6px 8px;
|
|
||||||
color: #2a506f;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='checkbox'] + div {
|
|
||||||
border-radius: ${({ multipleSelection }) =>
|
|
||||||
multipleSelection ? '4px' : '50%'};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function badgeShadeFromStatus(status: string) {
|
function badgeShadeFromStatus(status: string) {
|
||||||
@@ -185,15 +137,18 @@ const InitProgress = styled(
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export interface DriveSelectorProps
|
export interface DriveSelectorProps
|
||||||
extends Omit<ModalProps, 'done' | 'cancel'> {
|
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
|
||||||
|
write: boolean;
|
||||||
multipleSelection: boolean;
|
multipleSelection: boolean;
|
||||||
showWarnings?: boolean;
|
showWarnings?: boolean;
|
||||||
cancel: () => void;
|
cancel: (drives: DrivelistDrive[]) => void;
|
||||||
done: (drives: DrivelistDrive[]) => void;
|
done: (drives: DrivelistDrive[]) => void;
|
||||||
titleLabel: string;
|
titleLabel: string;
|
||||||
emptyListLabel: string;
|
emptyListLabel: string;
|
||||||
|
emptyListIcon: JSX.Element;
|
||||||
selectedList?: DrivelistDrive[];
|
selectedList?: DrivelistDrive[];
|
||||||
updateSelectedList?: () => DrivelistDrive[];
|
updateSelectedList?: () => DrivelistDrive[];
|
||||||
|
onSelect?: (drive: DrivelistDrive) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DriveSelectorState {
|
interface DriveSelectorState {
|
||||||
@@ -214,12 +169,14 @@ export class DriveSelector extends React.Component<
|
|||||||
> {
|
> {
|
||||||
private unsubscribe: (() => void) | undefined;
|
private unsubscribe: (() => void) | undefined;
|
||||||
tableColumns: Array<TableColumn<Drive>>;
|
tableColumns: Array<TableColumn<Drive>>;
|
||||||
|
originalList: DrivelistDrive[];
|
||||||
|
|
||||||
constructor(props: DriveSelectorProps) {
|
constructor(props: DriveSelectorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||||
const selectedList = this.props.selectedList || [];
|
const selectedList = this.props.selectedList || [];
|
||||||
|
this.originalList = [...(this.props.selectedList || [])];
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
drives: getDrives(),
|
drives: getDrives(),
|
||||||
@@ -246,7 +203,9 @@ export class DriveSelector extends React.Component<
|
|||||||
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt>
|
<Txt ml={(hasWarnings && 8) || 0}>
|
||||||
|
{middleEllipsis(description, 32)}
|
||||||
|
</Txt>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -306,7 +265,8 @@ export class DriveSelector extends React.Component<
|
|||||||
return (
|
return (
|
||||||
isUsbbootDrive(drive) ||
|
isUsbbootDrive(drive) ||
|
||||||
isDriverlessDrive(drive) ||
|
isDriverlessDrive(drive) ||
|
||||||
!isDriveValid(drive, image)
|
!isDriveValid(drive, image, this.props.write) ||
|
||||||
|
(this.props.write && drive.isReadOnly)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,9 +309,9 @@ export class DriveSelector extends React.Component<
|
|||||||
case compatibility.system():
|
case compatibility.system():
|
||||||
return warning.systemDrive();
|
return warning.systemDrive();
|
||||||
case compatibility.tooSmall():
|
case compatibility.tooSmall():
|
||||||
const recommendedDriveSize =
|
const size =
|
||||||
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||||
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive);
|
return warning.tooSmall({ size }, drive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +319,7 @@ export class DriveSelector extends React.Component<
|
|||||||
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
||||||
drive,
|
drive,
|
||||||
this.state.image,
|
this.state.image,
|
||||||
|
this.props.write,
|
||||||
).slice(0, 2);
|
).slice(0, 2);
|
||||||
return (
|
return (
|
||||||
// the column render fn expects a single Element
|
// the column render fn expects a single Element
|
||||||
@@ -418,8 +379,8 @@ export class DriveSelector extends React.Component<
|
|||||||
const displayedDrives = this.getDisplayedDrives(drives);
|
const displayedDrives = this.getDisplayedDrives(drives);
|
||||||
const disabledDrives = this.getDisabledDrives(drives, image);
|
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||||
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
||||||
const numberOfDisplayedSystemDrives = displayedDrives.filter(isSystemDrive)
|
const numberOfDisplayedSystemDrives =
|
||||||
.length;
|
displayedDrives.filter(isSystemDrive).length;
|
||||||
const numberOfHiddenSystemDrives =
|
const numberOfHiddenSystemDrives =
|
||||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||||
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
||||||
@@ -443,7 +404,7 @@ export class DriveSelector extends React.Component<
|
|||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||||
cancel={cancel}
|
cancel={() => cancel(this.originalList)}
|
||||||
done={() => done(selectedList)}
|
done={() => done(selectedList)}
|
||||||
action={`Select (${selectedList.length})`}
|
action={`Select (${selectedList.length})`}
|
||||||
primaryButtonProps={{
|
primaryButtonProps={{
|
||||||
@@ -453,95 +414,115 @@ export class DriveSelector extends React.Component<
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Flex width="100%" height="90%">
|
{!hasAvailableDrives() ? (
|
||||||
{!hasAvailableDrives() ? (
|
<Flex
|
||||||
<Flex
|
flexDirection="column"
|
||||||
flexDirection="column"
|
justifyContent="center"
|
||||||
justifyContent="center"
|
alignItems="center"
|
||||||
alignItems="center"
|
width="100%"
|
||||||
width="100%"
|
>
|
||||||
>
|
{this.props.emptyListIcon}
|
||||||
<DriveSVGIcon width="40px" height="90px" />
|
<b>{this.props.emptyListLabel}</b>
|
||||||
<b>{this.props.emptyListLabel}</b>
|
</Flex>
|
||||||
</Flex>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<ScrollableFlex flexDirection="column" width="100%">
|
<DrivesTable
|
||||||
<DrivesTable
|
refFn={(t) => {
|
||||||
refFn={(t: Table<Drive>) => {
|
if (t !== null) {
|
||||||
if (t !== null) {
|
t.setRowSelection(selectedList);
|
||||||
t.setRowSelection(selectedList);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
multipleSelection={this.props.multipleSelection}
|
|
||||||
columns={this.tableColumns}
|
|
||||||
data={displayedDrives}
|
|
||||||
disabledRows={disabledDrives}
|
|
||||||
getRowClass={(row: Drive) =>
|
|
||||||
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
|
||||||
}
|
}
|
||||||
rowKey="displayName"
|
}}
|
||||||
onCheck={(rows: Drive[]) => {
|
checkedRowsNumber={selectedList.length}
|
||||||
const newSelection = rows.filter(isDrivelistDrive);
|
multipleSelection={this.props.multipleSelection}
|
||||||
if (this.props.multipleSelection) {
|
columns={this.tableColumns}
|
||||||
this.setState({
|
data={displayedDrives}
|
||||||
selectedList: newSelection,
|
disabledRows={disabledDrives}
|
||||||
});
|
getRowClass={(row: Drive) =>
|
||||||
return;
|
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
||||||
|
}
|
||||||
|
rowKey="displayName"
|
||||||
|
onCheck={(rows: Drive[]) => {
|
||||||
|
let newSelection = rows.filter(isDrivelistDrive);
|
||||||
|
if (this.props.multipleSelection) {
|
||||||
|
if (rows.length === 0) {
|
||||||
|
newSelection = [];
|
||||||
}
|
}
|
||||||
this.setState({
|
const deselecting = selectedList.filter(
|
||||||
selectedList: newSelection.slice(newSelection.length - 1),
|
(selected) =>
|
||||||
});
|
newSelection.filter(
|
||||||
}}
|
(row) => row.device === selected.device,
|
||||||
onRowClick={(row: Drive) => {
|
).length === 0,
|
||||||
if (
|
);
|
||||||
!isDrivelistDrive(row) ||
|
const selecting = newSelection.filter(
|
||||||
this.driveShouldBeDisabled(row, image)
|
(row) =>
|
||||||
) {
|
selectedList.filter(
|
||||||
return;
|
(selected) => row.device === selected.device,
|
||||||
}
|
).length === 0,
|
||||||
if (this.props.multipleSelection) {
|
);
|
||||||
const newList = [...selectedList];
|
deselecting.concat(selecting).forEach((row) => {
|
||||||
const selectedIndex = selectedList.findIndex(
|
if (this.props.onSelect) {
|
||||||
(drive) => drive.device === row.device,
|
this.props.onSelect(row);
|
||||||
);
|
|
||||||
if (selectedIndex === -1) {
|
|
||||||
newList.push(row);
|
|
||||||
} else {
|
|
||||||
// Deselect if selected
|
|
||||||
newList.splice(selectedIndex, 1);
|
|
||||||
}
|
}
|
||||||
this.setState({
|
|
||||||
selectedList: newList,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
selectedList: [row],
|
|
||||||
});
|
});
|
||||||
}}
|
this.setState({
|
||||||
/>
|
selectedList: newSelection,
|
||||||
{numberOfHiddenSystemDrives > 0 && (
|
});
|
||||||
<Link
|
return;
|
||||||
mt={15}
|
}
|
||||||
mb={15}
|
if (this.props.onSelect) {
|
||||||
fontSize="14px"
|
this.props.onSelect(newSelection[newSelection.length - 1]);
|
||||||
onClick={() => this.setState({ showSystemDrives: true })}
|
}
|
||||||
>
|
this.setState({
|
||||||
<Flex alignItems="center">
|
selectedList: newSelection.slice(newSelection.length - 1),
|
||||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
});
|
||||||
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
}}
|
||||||
</Flex>
|
onRowClick={(row: Drive) => {
|
||||||
</Link>
|
if (
|
||||||
)}
|
!isDrivelistDrive(row) ||
|
||||||
</ScrollableFlex>
|
this.driveShouldBeDisabled(row, image)
|
||||||
)}
|
) {
|
||||||
{this.props.showWarnings && hasSystemDrives ? (
|
return;
|
||||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
}
|
||||||
Selecting your system drive is dangerous and will erase your
|
if (this.props.onSelect) {
|
||||||
drive!
|
this.props.onSelect(row);
|
||||||
</Alert>
|
}
|
||||||
) : null}
|
const index = selectedList.findIndex(
|
||||||
</Flex>
|
(d) => d.device === row.device,
|
||||||
|
);
|
||||||
|
const newList = this.props.multipleSelection
|
||||||
|
? [...selectedList]
|
||||||
|
: [];
|
||||||
|
if (index === -1) {
|
||||||
|
newList.push(row);
|
||||||
|
} else {
|
||||||
|
// Deselect if selected
|
||||||
|
newList.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
selectedList: newList,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{numberOfHiddenSystemDrives > 0 && (
|
||||||
|
<Link
|
||||||
|
mt={15}
|
||||||
|
mb={15}
|
||||||
|
fontSize="14px"
|
||||||
|
onClick={() => this.setState({ showSystemDrives: true })}
|
||||||
|
>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||||
|
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
||||||
|
</Flex>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{this.props.showWarnings && hasSystemDrives ? (
|
||||||
|
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||||
|
Selecting your system drive is dangerous and will erase your drive!
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{missingDriversModal.drive !== undefined && (
|
{missingDriversModal.drive !== undefined && (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -553,7 +534,7 @@ export class DriveSelector extends React.Component<
|
|||||||
if (missingDriversModal.drive !== undefined) {
|
if (missingDriversModal.drive !== undefined) {
|
||||||
openExternal(missingDriversModal.drive.link);
|
openExternal(missingDriversModal.drive.link);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
logException(error);
|
logException(error);
|
||||||
} finally {
|
} finally {
|
||||||
this.setState({ missingDriversModal: {} });
|
this.setState({ missingDriversModal: {} });
|
||||||
|
@@ -14,22 +14,18 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex } from 'rendition';
|
import { Flex } from 'rendition';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
|
||||||
import * as flashState from '../../models/flash-state';
|
import * as flashState from '../../models/flash-state';
|
||||||
import * as selectionState from '../../models/selection-state';
|
import * as selectionState from '../../models/selection-state';
|
||||||
|
import * as settings from '../../models/settings';
|
||||||
import { Actions, store } from '../../models/store';
|
import { Actions, store } from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
import * as analytics from '../../modules/analytics';
|
||||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
|
||||||
import { FlashAnother } from '../flash-another/flash-another';
|
import { FlashAnother } from '../flash-another/flash-another';
|
||||||
import { FlashResults } from '../flash-results/flash-results';
|
import { FlashResults, FlashError } from '../flash-results/flash-results';
|
||||||
|
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||||
import EtcherSvg from '../../../assets/etcher.svg';
|
|
||||||
import LoveSvg from '../../../assets/love.svg';
|
|
||||||
import BalenaSvg from '../../../assets/balena.svg';
|
|
||||||
|
|
||||||
function restart(goToMain: () => void) {
|
function restart(goToMain: () => void) {
|
||||||
selectionState.deselectAllDrives();
|
selectionState.deselectAllDrives();
|
||||||
@@ -44,22 +40,62 @@ function restart(goToMain: () => void) {
|
|||||||
goToMain();
|
goToMain();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formattedErrors() {
|
async function getSuccessBannerURL() {
|
||||||
const errors = _.map(
|
return (
|
||||||
_.get(flashState.getFlashResults(), ['results', 'errors']),
|
(await settings.get('successBannerURL')) ??
|
||||||
(error) => {
|
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
|
||||||
return `${error.device}: ${error.message || error.code}`;
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return errors.join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||||
const results = flashState.getFlashResults().results || {};
|
const [webviewShowing, setWebviewShowing] = React.useState(false);
|
||||||
|
const [successBannerURL, setSuccessBannerURL] = React.useState('');
|
||||||
|
(async () => {
|
||||||
|
setSuccessBannerURL(await getSuccessBannerURL());
|
||||||
|
})();
|
||||||
|
const flashResults = flashState.getFlashResults();
|
||||||
|
const errors: FlashError[] = (
|
||||||
|
store.getState().toJS().failedDeviceErrors || []
|
||||||
|
).map(([, error]: [string, FlashError]) => ({
|
||||||
|
...error,
|
||||||
|
}));
|
||||||
|
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
|
||||||
|
flashState.getFlashState();
|
||||||
|
const {
|
||||||
|
skip,
|
||||||
|
results = {
|
||||||
|
bytesWritten,
|
||||||
|
sourceMetadata: {
|
||||||
|
size,
|
||||||
|
blockmappedSize,
|
||||||
|
},
|
||||||
|
averageFlashingSpeed: averageSpeed,
|
||||||
|
devices: { failed, successful: 0 },
|
||||||
|
},
|
||||||
|
} = flashResults;
|
||||||
return (
|
return (
|
||||||
<Flex flexDirection="column" width="100%" color="#fff">
|
<Flex height="100%" justifyContent="space-between">
|
||||||
<Flex height="160px" alignItems="center" justifyContent="center">
|
<Flex
|
||||||
<FlashResults results={results} errors={formattedErrors()} />
|
width={webviewShowing ? '36.2vw' : '100vw'}
|
||||||
|
height="100vh"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
flexDirection="column"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 1,
|
||||||
|
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FlashResults
|
||||||
|
image={selectionState.getImage()?.name}
|
||||||
|
results={results}
|
||||||
|
skip={skip}
|
||||||
|
errors={errors}
|
||||||
|
mb="32px"
|
||||||
|
goToMain={goToMain}
|
||||||
|
/>
|
||||||
|
|
||||||
<FlashAnother
|
<FlashAnother
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -67,34 +103,20 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{successBannerURL.length && (
|
||||||
<Flex
|
<SafeWebview
|
||||||
flexDirection="column"
|
src={successBannerURL}
|
||||||
height="320px"
|
onWebviewShow={setWebviewShowing}
|
||||||
justifyContent="space-between"
|
style={{
|
||||||
alignItems="center"
|
display: webviewShowing ? 'flex' : 'none',
|
||||||
>
|
position: 'absolute',
|
||||||
<Flex fontSize="28px" mt="40px">
|
right: 0,
|
||||||
Thanks for using
|
bottom: 0,
|
||||||
<EtcherSvg
|
width: '63.8vw',
|
||||||
width="165px"
|
height: '100vh',
|
||||||
style={{ margin: '0 10px', cursor: 'pointer' }}
|
}}
|
||||||
onClick={() =>
|
/>
|
||||||
openExternal('https://balena.io/etcher?ref=etcher_offline_banner')
|
)}
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Flex mb="10px">
|
|
||||||
made with
|
|
||||||
<LoveSvg height="20px" style={{ margin: '0 10px' }} />
|
|
||||||
by
|
|
||||||
<BalenaSvg
|
|
||||||
height="20px"
|
|
||||||
style={{ margin: '0 10px', cursor: 'pointer' }}
|
|
||||||
onClick={() => openExternal('https://balena.io?ref=etcher_success')}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,7 @@ export interface FlashAnotherProps {
|
|||||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||||
return (
|
return (
|
||||||
<BaseButton primary onClick={props.onClick}>
|
<BaseButton primary onClick={props.onClick}>
|
||||||
Flash Another
|
Flash another
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -16,76 +16,183 @@
|
|||||||
|
|
||||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
|
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
|
||||||
import * as _ from 'lodash';
|
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
|
||||||
import outdent from 'outdent';
|
import outdent from 'outdent';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, Txt } from 'rendition';
|
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { progress } from '../../../../shared/messages';
|
import { progress } from '../../../../shared/messages';
|
||||||
import { bytesToMegabytes } from '../../../../shared/units';
|
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';
|
||||||
|
|
||||||
|
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||||
|
&&& [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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const DoneIcon = (props: {
|
||||||
|
skipped: boolean;
|
||||||
|
color: string;
|
||||||
|
allFailed: boolean;
|
||||||
|
}) => {
|
||||||
|
const svgProps = {
|
||||||
|
width: '28px',
|
||||||
|
fill: props.color,
|
||||||
|
style: {
|
||||||
|
marginTop: '-25px',
|
||||||
|
marginLeft: '13px',
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return props.allFailed && !props.skipped ? (
|
||||||
|
<TimesCircleSvg {...svgProps} />
|
||||||
|
) : (
|
||||||
|
<CheckCircleSvg {...svgProps} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FlashError extends Error {
|
||||||
|
description: string;
|
||||||
|
device: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formattedErrors(errors: FlashError[]) {
|
||||||
|
return errors
|
||||||
|
.map((error) => `${error.device}: ${error.message || error.code}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: Array<TableColumn<FlashError>> = [
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
label: 'Target',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'device',
|
||||||
|
label: 'Location',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'message',
|
||||||
|
label: '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({
|
export function FlashResults({
|
||||||
|
goToMain,
|
||||||
|
image = '',
|
||||||
errors,
|
errors,
|
||||||
results,
|
results,
|
||||||
|
skip,
|
||||||
|
...props
|
||||||
}: {
|
}: {
|
||||||
errors: string;
|
goToMain: () => void;
|
||||||
|
image?: string;
|
||||||
|
errors: FlashError[];
|
||||||
|
skip: boolean;
|
||||||
results: {
|
results: {
|
||||||
bytesWritten: number;
|
|
||||||
sourceMetadata: {
|
sourceMetadata: {
|
||||||
size: number;
|
size: number;
|
||||||
blockmappedSize: number;
|
blockmappedSize?: number;
|
||||||
};
|
};
|
||||||
averageFlashingSpeed: number;
|
averageFlashingSpeed: number;
|
||||||
devices: { failed: number; successful: number };
|
devices: { failed: number; successful: number };
|
||||||
};
|
};
|
||||||
}) {
|
} & FlexProps) {
|
||||||
const allDevicesFailed = results.devices.successful === 0;
|
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||||
const effectiveSpeed = _.round(
|
const allFailed = !skip && results.devices.successful === 0;
|
||||||
bytesToMegabytes(
|
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
|
||||||
results.sourceMetadata.size /
|
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
|
||||||
(results.bytesWritten / results.averageFlashingSpeed),
|
|
||||||
),
|
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex flexDirection="column" {...props}>
|
||||||
flexDirection="column"
|
<Flex alignItems="center" flexDirection="column">
|
||||||
mr="80px"
|
<Flex
|
||||||
height="90px"
|
alignItems="center"
|
||||||
style={{
|
mt="50px"
|
||||||
position: 'relative',
|
mb="32px"
|
||||||
top: '25px',
|
color="#7e8085"
|
||||||
}}
|
flexDirection="column"
|
||||||
>
|
>
|
||||||
<Flex alignItems="center">
|
<FlashSvg width="40px" height="40px" className="disabled" />
|
||||||
<CheckCircleSvg
|
<DoneIcon
|
||||||
width="24px"
|
skipped={skip}
|
||||||
fill={allDevicesFailed ? '#c6c8c9' : '#1ac135'}
|
allFailed={allFailed}
|
||||||
style={{
|
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
|
||||||
margin: '0 15px 0 0',
|
/>
|
||||||
}}
|
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||||
/>
|
</Flex>
|
||||||
<Txt fontSize={24} color="#fff">
|
<Txt fontSize={24} color="#fff" mb="17px">
|
||||||
Flash Complete!
|
Flash {allFailed ? 'Failed' : 'Complete'}!
|
||||||
</Txt>
|
</Txt>
|
||||||
|
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex flexDirection="column" mr="0" mb="0" ml="40px" color="#7e8085">
|
<Flex flexDirection="column" color="#7e8085">
|
||||||
{Object.entries(results.devices).map(([type, quantity]) => {
|
{results.devices.successful !== 0 ? (
|
||||||
return quantity ? (
|
<Flex alignItems="center">
|
||||||
<Flex
|
<CircleSvg width="14px" fill="#1ac135" />
|
||||||
alignItems="center"
|
<Txt ml="10px" color="#fff">
|
||||||
tooltip={type === 'failed' ? errors : undefined}
|
{results.devices.successful}
|
||||||
>
|
</Txt>
|
||||||
<CircleSvg
|
<Txt ml="10px">
|
||||||
width="14px"
|
{progress.successful(results.devices.successful)}
|
||||||
fill={type === 'failed' ? '#ff4444' : '#1ac135'}
|
</Txt>
|
||||||
/>
|
</Flex>
|
||||||
<Txt ml={10}>{quantity}</Txt>
|
) : null}
|
||||||
<Txt ml={10}>{progress[type](quantity)}</Txt>
|
{errors.length !== 0 ? (
|
||||||
</Flex>
|
<Flex alignItems="center">
|
||||||
) : null;
|
<CircleSvg width="14px" fill="#ff4444" />
|
||||||
})}
|
<Txt ml="10px" color="#fff">
|
||||||
{!allDevicesFailed && (
|
{errors.length}
|
||||||
|
</Txt>
|
||||||
|
<Txt ml="10px" tooltip={formattedErrors(errors)}>
|
||||||
|
{progress.failed(errors.length)}
|
||||||
|
</Txt>
|
||||||
|
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||||
|
more info
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
) : null}
|
||||||
|
{!allFailed && (
|
||||||
<Txt
|
<Txt
|
||||||
fontSize="10px"
|
fontSize="10px"
|
||||||
style={{
|
style={{
|
||||||
@@ -101,6 +208,36 @@ export function FlashResults({
|
|||||||
</Txt>
|
</Txt>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
{showErrorsInfo && (
|
||||||
|
<Modal
|
||||||
|
titleElement={
|
||||||
|
<Flex alignItems="baseline" mb={18}>
|
||||||
|
<Txt fontSize={24} align="left">
|
||||||
|
Failed targets
|
||||||
|
</Txt>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
action="Retry failed targets"
|
||||||
|
cancel={() => setShowErrorsInfo(false)}
|
||||||
|
done={() => {
|
||||||
|
setShowErrorsInfo(false);
|
||||||
|
resetState();
|
||||||
|
getDrives()
|
||||||
|
.map((drive) => {
|
||||||
|
selection.deselectDrive(drive.device);
|
||||||
|
return drive.device;
|
||||||
|
})
|
||||||
|
.filter((driveDevice) =>
|
||||||
|
errors.some((error) => error.device === driveDevice),
|
||||||
|
)
|
||||||
|
.forEach((driveDevice) => selection.selectDrive(driveDevice));
|
||||||
|
goToMain();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ErrorsTable columns={columns} data={errors} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@ import { StepButton } from '../../styled-components';
|
|||||||
|
|
||||||
const FlashProgressBar = styled(ProgressBar)`
|
const FlashProgressBar = styled(ProgressBar)`
|
||||||
> div {
|
> div {
|
||||||
width: 220px;
|
width: 100%;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
text-shadow: none !important;
|
text-shadow: none !important;
|
||||||
@@ -33,7 +33,7 @@ const FlashProgressBar = styled(ProgressBar)`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
width: 220px;
|
width: 100%;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -49,7 +49,7 @@ interface ProgressButtonProps {
|
|||||||
percentage: number;
|
percentage: number;
|
||||||
position: number;
|
position: number;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
cancel: () => void;
|
cancel: (type: string) => void;
|
||||||
callback: () => void;
|
callback: () => void;
|
||||||
warning?: boolean;
|
warning?: boolean;
|
||||||
}
|
}
|
||||||
@@ -60,11 +60,14 @@ const colors = {
|
|||||||
verifying: '#1ac135',
|
verifying: '#1ac135',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const CancelButton = styled((props) => (
|
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||||
<Button plain {...props}>
|
const status = type === 'verifying' ? 'Skip' : 'Cancel';
|
||||||
Cancel
|
return (
|
||||||
</Button>
|
<Button plain onClick={() => onClick(status)} {...props}>
|
||||||
))`
|
{status}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})`
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
&&& {
|
&&& {
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -75,11 +78,14 @@ const CancelButton = styled((props) => (
|
|||||||
|
|
||||||
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||||
public render() {
|
public render() {
|
||||||
|
const percentage = this.props.percentage;
|
||||||
|
const warning = this.props.warning;
|
||||||
const { status, position } = fromFlashState({
|
const { status, position } = fromFlashState({
|
||||||
type: this.props.type,
|
type: this.props.type,
|
||||||
|
percentage,
|
||||||
position: this.props.position,
|
position: this.props.position,
|
||||||
percentage: this.props.percentage,
|
|
||||||
});
|
});
|
||||||
|
const type = this.props.type || 'default';
|
||||||
if (this.props.active) {
|
if (this.props.active) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -96,21 +102,24 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
|||||||
>
|
>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Txt color="#fff">{status} </Txt>
|
<Txt color="#fff">{status} </Txt>
|
||||||
<Txt color={colors[this.props.type]}>{position}</Txt>
|
<Txt color={colors[type]}>{position}</Txt>
|
||||||
</Flex>
|
</Flex>
|
||||||
<CancelButton onClick={this.props.cancel} color="#00aeef" />
|
{type && (
|
||||||
|
<CancelButton
|
||||||
|
type={type}
|
||||||
|
onClick={this.props.cancel}
|
||||||
|
color="#00aeef"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<FlashProgressBar
|
<FlashProgressBar background={colors[type]} value={percentage} />
|
||||||
background={colors[this.props.type]}
|
|
||||||
value={this.props.percentage}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<StepButton
|
<StepButton
|
||||||
primary={!this.props.warning}
|
primary={!warning}
|
||||||
warning={this.props.warning}
|
warning={warning}
|
||||||
onClick={this.props.callback}
|
onClick={this.props.callback}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
style={{
|
style={{
|
||||||
|
@@ -31,9 +31,7 @@ interface ReducedFlashingInfosProps {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReducedFlashingInfos extends React.Component<
|
export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
|
||||||
ReducedFlashingInfosProps
|
|
||||||
> {
|
|
||||||
constructor(props: ReducedFlashingInfosProps) {
|
constructor(props: ReducedFlashingInfosProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {};
|
this.state = {};
|
||||||
|
@@ -16,9 +16,8 @@
|
|||||||
|
|
||||||
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
|
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as os from 'os';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, Checkbox, Txt } from 'rendition';
|
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
|
||||||
|
|
||||||
import { version, packageType } from '../../../../../package.json';
|
import { version, packageType } from '../../../../../package.json';
|
||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
@@ -26,50 +25,39 @@ import * as analytics from '../../modules/analytics';
|
|||||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||||
import { Modal } from '../../styled-components';
|
import { Modal } from '../../styled-components';
|
||||||
|
|
||||||
const platform = os.platform();
|
|
||||||
|
|
||||||
interface Setting {
|
interface Setting {
|
||||||
name: string;
|
name: string;
|
||||||
label: string | JSX.Element;
|
label: string | JSX.Element;
|
||||||
options?: {
|
|
||||||
description: string;
|
|
||||||
confirmLabel: string;
|
|
||||||
};
|
|
||||||
hide?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSettingsList(): Promise<Setting[]> {
|
async function getSettingsList(): Promise<Setting[]> {
|
||||||
return [
|
const list: Setting[] = [
|
||||||
{
|
{
|
||||||
name: 'errorReporting',
|
name: 'errorReporting',
|
||||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
name: 'unmountOnSuccess',
|
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||||
/**
|
list.push({
|
||||||
* On Windows, "Unmounting" basically means "ejecting".
|
|
||||||
* On top of that, Windows users are usually not even
|
|
||||||
* familiar with the meaning of "unmount", which comes
|
|
||||||
* from the UNIX world.
|
|
||||||
*/
|
|
||||||
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'validateWriteOnSuccess',
|
|
||||||
label: 'Validate write on success',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'updatesEnabled',
|
name: 'updatesEnabled',
|
||||||
label: 'Auto-updates enabled',
|
label: 'Auto-updates enabled',
|
||||||
hide: _.includes(['rpm', 'deb'], packageType),
|
});
|
||||||
},
|
}
|
||||||
];
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
toggleModal: (value: boolean) => void;
|
toggleModal: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UUID = process.env.BALENA_DEVICE_UUID;
|
||||||
|
|
||||||
|
const InfoBox = (props: any) => (
|
||||||
|
<Box fontSize={14}>
|
||||||
|
<Txt>{props.label}</Txt>
|
||||||
|
<TextWithCopy code text={props.value} copy={props.value} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||||
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -90,25 +78,14 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleSetting = async (
|
const toggleSetting = async (setting: string) => {
|
||||||
setting: string,
|
|
||||||
options?: Setting['options'],
|
|
||||||
) => {
|
|
||||||
const value = currentSettings[setting];
|
const value = currentSettings[setting];
|
||||||
const dangerous = options !== undefined;
|
analytics.logEvent('Toggle setting', { setting, value });
|
||||||
|
|
||||||
analytics.logEvent('Toggle setting', {
|
|
||||||
setting,
|
|
||||||
value,
|
|
||||||
dangerous,
|
|
||||||
});
|
|
||||||
|
|
||||||
await settings.set(setting, !value);
|
await settings.set(setting, !value);
|
||||||
setCurrentSettings({
|
setCurrentSettings({
|
||||||
...currentSettings,
|
...currentSettings,
|
||||||
[setting]: !value,
|
[setting]: !value,
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -121,26 +98,33 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
|||||||
done={() => toggleModal(false)}
|
done={() => toggleModal(false)}
|
||||||
>
|
>
|
||||||
<Flex flexDirection="column">
|
<Flex flexDirection="column">
|
||||||
{_.map(settingsList, (setting: Setting, i: number) => {
|
{settingsList.map((setting: Setting, i: number) => {
|
||||||
return setting.hide ? null : (
|
return (
|
||||||
<Flex key={setting.name}>
|
<Flex key={setting.name} mb={14}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
toggle
|
toggle
|
||||||
tabIndex={6 + i}
|
tabIndex={6 + i}
|
||||||
label={setting.label}
|
label={setting.label}
|
||||||
checked={currentSettings[setting.name]}
|
checked={currentSettings[setting.name]}
|
||||||
onChange={() => toggleSetting(setting.name, setting.options)}
|
onChange={() => toggleSetting(setting.name)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{UUID !== undefined && (
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<Txt fontSize={24}>System Information</Txt>
|
||||||
|
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
<Flex
|
<Flex
|
||||||
mt={28}
|
mt={18}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
color="#00aeef"
|
color="#00aeef"
|
||||||
style={{
|
style={{
|
||||||
width: 'fit-content',
|
width: 'fit-content',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
fontSize: 14,
|
||||||
}}
|
}}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openExternal(
|
openExternal(
|
||||||
|
@@ -18,6 +18,8 @@ import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
|||||||
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||||
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||||
|
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||||
|
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
||||||
import { sourceDestination } from 'etcher-sdk';
|
import { sourceDestination } from 'etcher-sdk';
|
||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
Card as BaseCard,
|
Card as BaseCard,
|
||||||
Input,
|
Input,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
Link,
|
||||||
} from 'rendition';
|
} from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -58,8 +61,11 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
|
|||||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||||
|
|
||||||
import ImageSvg from '../../../assets/image.svg';
|
import ImageSvg from '../../../assets/image.svg';
|
||||||
|
import SrcSvg from '../../../assets/src.svg';
|
||||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
import { isJson } from '../../../../shared/utils';
|
||||||
|
|
||||||
const recentUrlImagesKey = 'recentUrlImages';
|
const recentUrlImagesKey = 'recentUrlImages';
|
||||||
|
|
||||||
@@ -71,7 +77,7 @@ function normalizeRecentUrlImages(urls: any[]): URL[] {
|
|||||||
.map((url) => {
|
.map((url) => {
|
||||||
try {
|
try {
|
||||||
return new URL(url);
|
return new URL(url);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// Invalid URL, skip
|
// Invalid URL, skip
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -116,10 +122,11 @@ const ModalText = styled.p`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function getState() {
|
function getState() {
|
||||||
|
const image = selectionState.getImage();
|
||||||
return {
|
return {
|
||||||
hasImage: selectionState.hasImage(),
|
hasImage: selectionState.hasImage(),
|
||||||
imageName: selectionState.getImageName(),
|
imageName: image?.name,
|
||||||
imageSize: selectionState.getImageSize(),
|
imageSize: image?.size,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,12 +138,15 @@ const URLSelector = ({
|
|||||||
done,
|
done,
|
||||||
cancel,
|
cancel,
|
||||||
}: {
|
}: {
|
||||||
done: (imageURL: string) => void;
|
done: (imageURL: string, auth?: Authentication) => void;
|
||||||
cancel: () => void;
|
cancel: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [imageURL, setImageURL] = React.useState('');
|
const [imageURL, setImageURL] = React.useState('');
|
||||||
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
|
||||||
|
const [username, setUsername] = React.useState('');
|
||||||
|
const [password, setPassword] = React.useState('');
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchRecentUrlImages = async () => {
|
const fetchRecentUrlImages = async () => {
|
||||||
const recentUrlImages: URL[] = await getRecentUrlImages();
|
const recentUrlImages: URL[] = await getRecentUrlImages();
|
||||||
@@ -159,11 +169,12 @@ const URLSelector = ({
|
|||||||
imageURL,
|
imageURL,
|
||||||
]);
|
]);
|
||||||
setRecentUrlImages(normalizedRecentUrls);
|
setRecentUrlImages(normalizedRecentUrls);
|
||||||
await done(imageURL);
|
const auth = username ? { username, password } : undefined;
|
||||||
|
await done(imageURL, auth);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex flexDirection="column">
|
<Flex flexDirection="column">
|
||||||
<Flex style={{ width: '100%' }} flexDirection="column">
|
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
|
||||||
<Txt mb="10px" fontSize="24px">
|
<Txt mb="10px" fontSize="24px">
|
||||||
Use Image URL
|
Use Image URL
|
||||||
</Txt>
|
</Txt>
|
||||||
@@ -175,6 +186,49 @@ const URLSelector = ({
|
|||||||
setImageURL(evt.target.value)
|
setImageURL(evt.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
mt={15}
|
||||||
|
mb={15}
|
||||||
|
fontSize="14px"
|
||||||
|
onClick={() => {
|
||||||
|
if (showBasicAuth) {
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
}
|
||||||
|
setShowBasicAuth(!showBasicAuth);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
{showBasicAuth && (
|
||||||
|
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||||
|
)}
|
||||||
|
{!showBasicAuth && (
|
||||||
|
<ChevronRightSvg height="1em" fill="currentColor" />
|
||||||
|
)}
|
||||||
|
<Txt ml={8}>Authentication</Txt>
|
||||||
|
</Flex>
|
||||||
|
</Link>
|
||||||
|
{showBasicAuth && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Input
|
||||||
|
mb={15}
|
||||||
|
value={username}
|
||||||
|
placeholder="Enter username"
|
||||||
|
type="text"
|
||||||
|
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setUsername(evt.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={password}
|
||||||
|
placeholder="Enter password"
|
||||||
|
type="password"
|
||||||
|
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setPassword(evt.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
{recentImages.length > 0 && (
|
{recentImages.length > 0 && (
|
||||||
<Flex flexDirection="column" height="78.6%">
|
<Flex flexDirection="column" height="78.6%">
|
||||||
@@ -213,22 +267,28 @@ interface Flow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FlowSelector = styled(
|
const FlowSelector = styled(
|
||||||
({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => {
|
({ flow, ...props }: { flow: Flow } & ButtonProps) => (
|
||||||
return (
|
<StepButton
|
||||||
<StepButton
|
plain={!props.primary}
|
||||||
plain
|
primary={props.primary}
|
||||||
onClick={(evt) => flow.onClick(evt)}
|
onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
|
||||||
icon={flow.icon}
|
flow.onClick(evt)
|
||||||
{...props}
|
}
|
||||||
>
|
icon={flow.icon}
|
||||||
{flow.label}
|
{...props}
|
||||||
</StepButton>
|
>
|
||||||
);
|
{flow.label}
|
||||||
},
|
</StepButton>
|
||||||
|
),
|
||||||
)`
|
)`
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
|
||||||
|
:enabled:focus,
|
||||||
|
:enabled:focus svg {
|
||||||
|
color: ${colors.primary.foreground} !important;
|
||||||
|
}
|
||||||
|
|
||||||
:enabled:hover {
|
:enabled:hover {
|
||||||
background-color: ${colors.primary.background};
|
background-color: ${colors.primary.background};
|
||||||
color: ${colors.primary.foreground};
|
color: ${colors.primary.foreground};
|
||||||
@@ -255,6 +315,7 @@ export interface SourceMetadata extends sourceDestination.Metadata {
|
|||||||
drive?: DrivelistDrive;
|
drive?: DrivelistDrive;
|
||||||
extension?: string;
|
extension?: string;
|
||||||
archiveExtension?: string;
|
archiveExtension?: string;
|
||||||
|
auth?: Authentication;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceSelectorProps {
|
interface SourceSelectorProps {
|
||||||
@@ -269,6 +330,14 @@ interface SourceSelectorState {
|
|||||||
showImageDetails: boolean;
|
showImageDetails: boolean;
|
||||||
showURLSelector: boolean;
|
showURLSelector: boolean;
|
||||||
showDriveSelector: boolean;
|
showDriveSelector: boolean;
|
||||||
|
defaultFlowActive: boolean;
|
||||||
|
imageSelectorOpen: boolean;
|
||||||
|
imageLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Authentication {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SourceSelector extends React.Component<
|
export class SourceSelector extends React.Component<
|
||||||
@@ -285,7 +354,13 @@ export class SourceSelector extends React.Component<
|
|||||||
showImageDetails: false,
|
showImageDetails: false,
|
||||||
showURLSelector: false,
|
showURLSelector: false,
|
||||||
showDriveSelector: false,
|
showDriveSelector: false,
|
||||||
|
defaultFlowActive: true,
|
||||||
|
imageSelectorOpen: false,
|
||||||
|
imageLoading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bind `this` since it's used in an event's callback
|
||||||
|
this.onSelectImage = this.onSelectImage.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
@@ -302,25 +377,52 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||||
|
this.setState({ imageLoading: true });
|
||||||
await this.selectSource(
|
await this.selectSource(
|
||||||
imagePath,
|
imagePath,
|
||||||
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
|
isURL(this.normalizeImagePath(imagePath))
|
||||||
|
? sourceDestination.Http
|
||||||
|
: sourceDestination.File,
|
||||||
).promise;
|
).promise;
|
||||||
|
this.setState({ imageLoading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSource(selected: string, SourceType: Source) {
|
private async createSource(
|
||||||
|
selected: string,
|
||||||
|
SourceType: Source,
|
||||||
|
auth?: Authentication,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
selected = await replaceWindowsNetworkDriveLetter(selected);
|
selected = await replaceWindowsNetworkDriveLetter(selected);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
analytics.logException(error);
|
analytics.logException(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isJson(decodeURIComponent(selected))) {
|
||||||
|
const config: AxiosRequestConfig = JSON.parse(
|
||||||
|
decodeURIComponent(selected),
|
||||||
|
);
|
||||||
|
return new sourceDestination.Http({
|
||||||
|
url: config.url!,
|
||||||
|
axiosInstance: axios.create(_.omit(config, ['url'])),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (SourceType === sourceDestination.File) {
|
if (SourceType === sourceDestination.File) {
|
||||||
return new sourceDestination.File({
|
return new sourceDestination.File({
|
||||||
path: selected,
|
path: selected,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return new sourceDestination.Http({ url: selected });
|
|
||||||
|
return new sourceDestination.Http({ url: selected, auth });
|
||||||
|
}
|
||||||
|
|
||||||
|
public normalizeImagePath(imgPath: string) {
|
||||||
|
const decodedPath = decodeURIComponent(imgPath);
|
||||||
|
if (isJson(decodedPath)) {
|
||||||
|
return JSON.parse(decodedPath).url ?? decodedPath;
|
||||||
|
}
|
||||||
|
return decodedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private reselectSource() {
|
private reselectSource() {
|
||||||
@@ -334,6 +436,7 @@ export class SourceSelector extends React.Component<
|
|||||||
private selectSource(
|
private selectSource(
|
||||||
selected: string | DrivelistDrive,
|
selected: string | DrivelistDrive,
|
||||||
SourceType: Source,
|
SourceType: Source,
|
||||||
|
auth?: Authentication,
|
||||||
): { promise: Promise<void>; cancel: () => void } {
|
): { promise: Promise<void>; cancel: () => void } {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
return {
|
return {
|
||||||
@@ -345,7 +448,10 @@ export class SourceSelector extends React.Component<
|
|||||||
let source;
|
let source;
|
||||||
let metadata: SourceMetadata | undefined;
|
let metadata: SourceMetadata | undefined;
|
||||||
if (isString(selected)) {
|
if (isString(selected)) {
|
||||||
if (SourceType === sourceDestination.Http && !isURL(selected)) {
|
if (
|
||||||
|
SourceType === sourceDestination.Http &&
|
||||||
|
!isURL(this.normalizeImagePath(selected))
|
||||||
|
) {
|
||||||
this.handleError(
|
this.handleError(
|
||||||
'Unsupported protocol',
|
'Unsupported protocol',
|
||||||
selected,
|
selected,
|
||||||
@@ -363,7 +469,7 @@ export class SourceSelector extends React.Component<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
source = await this.createSource(selected, SourceType);
|
source = await this.createSource(selected, SourceType, auth);
|
||||||
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
@@ -380,7 +486,7 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
metadata.SourceType = SourceType;
|
metadata.SourceType = SourceType;
|
||||||
|
|
||||||
if (!metadata.hasMBR) {
|
if (!metadata.hasMBR && this.state.warning === null) {
|
||||||
analytics.logEvent('Missing partition table', { metadata });
|
analytics.logEvent('Missing partition table', { metadata });
|
||||||
this.setState({
|
this.setState({
|
||||||
warning: {
|
warning: {
|
||||||
@@ -389,7 +495,7 @@ export class SourceSelector extends React.Component<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
this.handleError(
|
this.handleError(
|
||||||
'Error opening source',
|
'Error opening source',
|
||||||
sourcePath,
|
sourcePath,
|
||||||
@@ -399,11 +505,20 @@ export class SourceSelector extends React.Component<
|
|||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
await source.close();
|
await source.close();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// Noop
|
// Noop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (selected.partitionTableType === null) {
|
||||||
|
analytics.logEvent('Missing partition table', { selected });
|
||||||
|
this.setState({
|
||||||
|
warning: {
|
||||||
|
message: messages.warning.driveMissingPartitionTable(),
|
||||||
|
title: 'Missing partition table',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
metadata = {
|
metadata = {
|
||||||
path: selected.device,
|
path: selected.device,
|
||||||
displayName: selected.displayName,
|
displayName: selected.displayName,
|
||||||
@@ -415,6 +530,7 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (metadata !== undefined) {
|
if (metadata !== undefined) {
|
||||||
|
metadata.auth = auth;
|
||||||
selectionState.selectSource(metadata);
|
selectionState.selectSource(metadata);
|
||||||
analytics.logEvent('Select image', {
|
analytics.logEvent('Select image', {
|
||||||
// An easy way so we can quickly identify if we're making use of
|
// An easy way so we can quickly identify if we're making use of
|
||||||
@@ -469,6 +585,7 @@ export class SourceSelector extends React.Component<
|
|||||||
|
|
||||||
private async openImageSelector() {
|
private async openImageSelector() {
|
||||||
analytics.logEvent('Open image selector');
|
analytics.logEvent('Open image selector');
|
||||||
|
this.setState({ imageSelectorOpen: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imagePath = await osDialog.selectImage();
|
const imagePath = await osDialog.selectImage();
|
||||||
@@ -479,8 +596,10 @@ export class SourceSelector extends React.Component<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.selectSource(imagePath, sourceDestination.File).promise;
|
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
exceptionReporter.report(error);
|
exceptionReporter.report(error);
|
||||||
|
} finally {
|
||||||
|
this.setState({ imageSelectorOpen: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +638,7 @@ export class SourceSelector extends React.Component<
|
|||||||
|
|
||||||
private showSelectedImageDetails() {
|
private showSelectedImageDetails() {
|
||||||
analytics.logEvent('Show selected image tooltip', {
|
analytics.logEvent('Show selected image tooltip', {
|
||||||
imagePath: selectionState.getImagePath(),
|
imagePath: selectionState.getImage()?.path,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -527,10 +646,25 @@ export class SourceSelector extends React.Component<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setDefaultFlowActive(defaultFlowActive: boolean) {
|
||||||
|
this.setState({ defaultFlowActive });
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeModal() {
|
||||||
|
this.setState({
|
||||||
|
showDriveSelector: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// TODO add a visual change when dragging a file over the selector
|
// TODO add a visual change when dragging a file over the selector
|
||||||
public render() {
|
public render() {
|
||||||
const { flashing } = this.props;
|
const { flashing } = this.props;
|
||||||
const { showImageDetails, showURLSelector, showDriveSelector } = this.state;
|
const {
|
||||||
|
showImageDetails,
|
||||||
|
showURLSelector,
|
||||||
|
showDriveSelector,
|
||||||
|
imageLoading,
|
||||||
|
} = this.state;
|
||||||
const selectionImage = selectionState.getImage();
|
const selectionImage = selectionState.getImage();
|
||||||
let image: SourceMetadata | DrivelistDrive =
|
let image: SourceMetadata | DrivelistDrive =
|
||||||
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||||
@@ -568,16 +702,18 @@ export class SourceSelector extends React.Component<
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectionImage !== undefined ? (
|
{selectionImage !== undefined || imageLoading ? (
|
||||||
<>
|
<>
|
||||||
<StepNameButton
|
<StepNameButton
|
||||||
plain
|
plain
|
||||||
onClick={() => this.showSelectedImageDetails()}
|
onClick={() => this.showSelectedImageDetails()}
|
||||||
tooltip={imageName || imageBasename}
|
tooltip={imageName || imageBasename}
|
||||||
>
|
>
|
||||||
{middleEllipsis(imageName || imageBasename, 20)}
|
<Spinner show={imageLoading}>
|
||||||
|
{middleEllipsis(imageName || imageBasename, 20)}
|
||||||
|
</Spinner>
|
||||||
</StepNameButton>
|
</StepNameButton>
|
||||||
{!flashing && (
|
{!flashing && !imageLoading && (
|
||||||
<ChangeButton
|
<ChangeButton
|
||||||
plain
|
plain
|
||||||
mb={14}
|
mb={14}
|
||||||
@@ -586,19 +722,23 @@ export class SourceSelector extends React.Component<
|
|||||||
Remove
|
Remove
|
||||||
</ChangeButton>
|
</ChangeButton>
|
||||||
)}
|
)}
|
||||||
{!_.isNil(imageSize) && (
|
{!_.isNil(imageSize) && !imageLoading && (
|
||||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FlowSelector
|
<FlowSelector
|
||||||
|
disabled={this.state.imageSelectorOpen}
|
||||||
|
primary={this.state.defaultFlowActive}
|
||||||
key="Flash from file"
|
key="Flash from file"
|
||||||
flow={{
|
flow={{
|
||||||
onClick: () => this.openImageSelector(),
|
onClick: () => this.openImageSelector(),
|
||||||
label: 'Flash from file',
|
label: 'Flash from file',
|
||||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||||
|
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||||
/>
|
/>
|
||||||
<FlowSelector
|
<FlowSelector
|
||||||
key="Flash from URL"
|
key="Flash from URL"
|
||||||
@@ -607,6 +747,8 @@ export class SourceSelector extends React.Component<
|
|||||||
label: 'Flash from URL',
|
label: 'Flash from URL',
|
||||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||||
|
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||||
/>
|
/>
|
||||||
<FlowSelector
|
<FlowSelector
|
||||||
key="Clone drive"
|
key="Clone drive"
|
||||||
@@ -615,6 +757,8 @@ export class SourceSelector extends React.Component<
|
|||||||
label: 'Clone drive',
|
label: 'Clone drive',
|
||||||
icon: <CopySvg height="1em" fill="currentColor" />,
|
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||||
|
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -622,6 +766,9 @@ export class SourceSelector extends React.Component<
|
|||||||
|
|
||||||
{this.state.warning != null && (
|
{this.state.warning != null && (
|
||||||
<SmallModal
|
<SmallModal
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
|
||||||
|
}}
|
||||||
titleElement={
|
titleElement={
|
||||||
<span>
|
<span>
|
||||||
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
||||||
@@ -670,7 +817,7 @@ export class SourceSelector extends React.Component<
|
|||||||
showURLSelector: false,
|
showURLSelector: false,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
done={async (imageURL: string) => {
|
done={async (imageURL: string, auth?: Authentication) => {
|
||||||
// Avoid analytics and selection state changes
|
// Avoid analytics and selection state changes
|
||||||
// if no file was resolved from the dialog.
|
// if no file was resolved from the dialog.
|
||||||
if (!imageURL) {
|
if (!imageURL) {
|
||||||
@@ -680,6 +827,7 @@ export class SourceSelector extends React.Component<
|
|||||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||||
imageURL,
|
imageURL,
|
||||||
sourceDestination.Http,
|
sourceDestination.Http,
|
||||||
|
auth,
|
||||||
));
|
));
|
||||||
await promise;
|
await promise;
|
||||||
}
|
}
|
||||||
@@ -692,24 +840,35 @@ export class SourceSelector extends React.Component<
|
|||||||
|
|
||||||
{showDriveSelector && (
|
{showDriveSelector && (
|
||||||
<DriveSelector
|
<DriveSelector
|
||||||
|
write={false}
|
||||||
multipleSelection={false}
|
multipleSelection={false}
|
||||||
titleLabel="Select source"
|
titleLabel="Select source"
|
||||||
emptyListLabel="Plug a source"
|
emptyListLabel="Plug a source drive"
|
||||||
cancel={() => {
|
emptyListIcon={<SrcSvg width="40px" />}
|
||||||
this.setState({
|
cancel={(originalList) => {
|
||||||
showDriveSelector: false,
|
if (originalList.length) {
|
||||||
});
|
const originalSource = originalList[0];
|
||||||
}}
|
if (selectionImage?.drive?.device !== originalSource.device) {
|
||||||
done={async (drives: DrivelistDrive[]) => {
|
this.selectSource(
|
||||||
if (drives.length) {
|
originalSource,
|
||||||
await this.selectSource(
|
sourceDestination.BlockDevice,
|
||||||
drives[0],
|
);
|
||||||
sourceDestination.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, sourceDestination.BlockDevice);
|
||||||
}
|
}
|
||||||
this.setState({
|
|
||||||
showDriveSelector: false,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@@ -24,7 +24,7 @@ import {
|
|||||||
} from '../../../../shared/drive-constraints';
|
} from '../../../../shared/drive-constraints';
|
||||||
import { compatibility, warning } from '../../../../shared/messages';
|
import { compatibility, warning } from '../../../../shared/messages';
|
||||||
import * as prettyBytes from 'pretty-bytes';
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
import { getSelectedDrives } from '../../models/selection-state';
|
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||||
import {
|
import {
|
||||||
ChangeButton,
|
ChangeButton,
|
||||||
DetailsText,
|
DetailsText,
|
||||||
@@ -80,9 +80,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
|||||||
|
|
||||||
if (targets.length === 1) {
|
if (targets.length === 1) {
|
||||||
const target = targets[0];
|
const target = targets[0];
|
||||||
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
const warnings = getDriveImageCompatibilityStatuses(
|
||||||
getDriveWarning,
|
target,
|
||||||
);
|
getImage(),
|
||||||
|
true,
|
||||||
|
).map(getDriveWarning);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepNameButton plain tooltip={props.tooltip}>
|
<StepNameButton plain tooltip={props.tooltip}>
|
||||||
@@ -106,9 +108,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
|||||||
if (targets.length > 1) {
|
if (targets.length > 1) {
|
||||||
const targetsTemplate = [];
|
const targetsTemplate = [];
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
const warnings = getDriveImageCompatibilityStatuses(
|
||||||
getDriveWarning,
|
target,
|
||||||
);
|
getImage(),
|
||||||
|
true,
|
||||||
|
).map(getDriveWarning);
|
||||||
targetsTemplate.push(
|
targetsTemplate.push(
|
||||||
<DetailsText
|
<DetailsText
|
||||||
key={target.device}
|
key={target.device}
|
||||||
|
@@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { scanner } from 'etcher-sdk';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, Txt } from 'rendition';
|
import { Flex, Txt } from 'rendition';
|
||||||
|
|
||||||
@@ -28,14 +27,16 @@ import {
|
|||||||
getSelectedDrives,
|
getSelectedDrives,
|
||||||
deselectDrive,
|
deselectDrive,
|
||||||
selectDrive,
|
selectDrive,
|
||||||
|
deselectAllDrives,
|
||||||
} from '../../models/selection-state';
|
} from '../../models/selection-state';
|
||||||
import * as settings from '../../models/settings';
|
|
||||||
import { observe } from '../../models/store';
|
import { observe } from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
import * as analytics from '../../modules/analytics';
|
||||||
import { TargetSelectorButton } from './target-selector-button';
|
import { TargetSelectorButton } from './target-selector-button';
|
||||||
|
|
||||||
|
import TgtSvg from '../../../assets/tgt.svg';
|
||||||
import DriveSvg from '../../../assets/drive.svg';
|
import DriveSvg from '../../../assets/drive.svg';
|
||||||
import { warning } from '../../../../shared/messages';
|
import { warning } from '../../../../shared/messages';
|
||||||
|
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||||
|
|
||||||
export const getDriveListLabel = () => {
|
export const getDriveListLabel = () => {
|
||||||
return getSelectedDrives()
|
return getSelectedDrives()
|
||||||
@@ -45,12 +46,7 @@ export const getDriveListLabel = () => {
|
|||||||
.join('\n');
|
.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowDrivesButton = () => {
|
|
||||||
return !settings.getSync('disableExplicitDriveSelection');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDriveSelectionStateSlice = () => ({
|
const getDriveSelectionStateSlice = () => ({
|
||||||
showDrivesButton: shouldShowDrivesButton(),
|
|
||||||
driveListLabel: getDriveListLabel(),
|
driveListLabel: getDriveListLabel(),
|
||||||
targets: getSelectedDrives(),
|
targets: getSelectedDrives(),
|
||||||
image: getImage(),
|
image: getImage(),
|
||||||
@@ -59,13 +55,14 @@ const getDriveSelectionStateSlice = () => ({
|
|||||||
export const TargetSelectorModal = (
|
export const TargetSelectorModal = (
|
||||||
props: Omit<
|
props: Omit<
|
||||||
DriveSelectorProps,
|
DriveSelectorProps,
|
||||||
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
|
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
|
||||||
>,
|
>,
|
||||||
) => (
|
) => (
|
||||||
<DriveSelector
|
<DriveSelector
|
||||||
multipleSelection={true}
|
multipleSelection={true}
|
||||||
titleLabel="Select target"
|
titleLabel="Select target"
|
||||||
emptyListLabel="Plug a target drive"
|
emptyListLabel="Plug a target drive"
|
||||||
|
emptyListIcon={<TgtSvg width="40px" />}
|
||||||
showWarnings={true}
|
showWarnings={true}
|
||||||
selectedList={getSelectedDrives()}
|
selectedList={getSelectedDrives()}
|
||||||
updateSelectedList={getSelectedDrives}
|
updateSelectedList={getSelectedDrives}
|
||||||
@@ -73,9 +70,7 @@ export const TargetSelectorModal = (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectAllTargets = (
|
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
|
||||||
modalTargets: scanner.adapters.DrivelistDrive[],
|
|
||||||
) => {
|
|
||||||
const selectedDrivesFromState = getSelectedDrives();
|
const selectedDrivesFromState = getSelectedDrives();
|
||||||
const deselected = selectedDrivesFromState.filter(
|
const deselected = selectedDrivesFromState.filter(
|
||||||
(drive) =>
|
(drive) =>
|
||||||
@@ -114,13 +109,11 @@ export const TargetSelector = ({
|
|||||||
flashing,
|
flashing,
|
||||||
}: TargetSelectorProps) => {
|
}: TargetSelectorProps) => {
|
||||||
// TODO: inject these from redux-connector
|
// TODO: inject these from redux-connector
|
||||||
const [
|
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
|
||||||
{ showDrivesButton, driveListLabel, targets },
|
getDriveSelectionStateSlice(),
|
||||||
setStateSlice,
|
|
||||||
] = React.useState(getDriveSelectionStateSlice());
|
|
||||||
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
|
const [showTargetSelectorModal, setShowTargetSelectorModal] =
|
||||||
|
React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return observe(() => {
|
return observe(() => {
|
||||||
@@ -141,7 +134,7 @@ export const TargetSelector = ({
|
|||||||
|
|
||||||
<TargetSelectorButton
|
<TargetSelectorButton
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
show={!hasDrive && showDrivesButton}
|
show={!hasDrive}
|
||||||
tooltip={driveListLabel}
|
tooltip={driveListLabel}
|
||||||
openDriveSelector={() => {
|
openDriveSelector={() => {
|
||||||
setShowTargetSelectorModal(true);
|
setShowTargetSelectorModal(true);
|
||||||
@@ -168,11 +161,31 @@ export const TargetSelector = ({
|
|||||||
|
|
||||||
{showTargetSelectorModal && (
|
{showTargetSelectorModal && (
|
||||||
<TargetSelectorModal
|
<TargetSelectorModal
|
||||||
cancel={() => setShowTargetSelectorModal(false)}
|
write={true}
|
||||||
done={(modalTargets) => {
|
cancel={(originalList) => {
|
||||||
selectAllTargets(modalTargets);
|
if (originalList.length) {
|
||||||
|
selectAllTargets(originalList);
|
||||||
|
} else {
|
||||||
|
deselectAllDrives();
|
||||||
|
}
|
||||||
setShowTargetSelectorModal(false);
|
setShowTargetSelectorModal(false);
|
||||||
}}
|
}}
|
||||||
|
done={(modalTargets) => {
|
||||||
|
if (modalTargets.length === 0) {
|
||||||
|
deselectAllDrives();
|
||||||
|
}
|
||||||
|
setShowTargetSelectorModal(false);
|
||||||
|
}}
|
||||||
|
onSelect={(drive) => {
|
||||||
|
if (
|
||||||
|
getSelectedDrives().find(
|
||||||
|
(selectedDrive) => selectedDrive.device === drive.device,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return deselectDrive(drive.device);
|
||||||
|
}
|
||||||
|
selectDrive(drive.device);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
12
lib/gui/app/index.dev.html
Normal file
12
lib/gui/app/index.dev.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>balenaEtcher</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="index.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main id="main"></main>
|
||||||
|
<script src="http://localhost:3030/gui.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Etcher</title>
|
<title>balenaEtcher</title>
|
||||||
<link rel="stylesheet" type="text/css" href="index.css">
|
<link rel="stylesheet" type="text/css" href="index.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@@ -14,9 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as electron from 'electron';
|
||||||
import * as sdk from 'etcher-sdk';
|
import * as sdk from 'etcher-sdk';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||||
import { bytesToMegabytes } from '../../../shared/units';
|
import { bytesToMegabytes } from '../../../shared/units';
|
||||||
import { Actions, store } from './store';
|
import { Actions, store } from './store';
|
||||||
|
|
||||||
@@ -45,6 +46,8 @@ export function isFlashing(): boolean {
|
|||||||
* start a flash process.
|
* start a flash process.
|
||||||
*/
|
*/
|
||||||
export function setFlashingFlag() {
|
export function setFlashingFlag() {
|
||||||
|
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||||
|
electron.ipcRenderer.invoke('disable-screensaver');
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: Actions.SET_FLASHING_FLAG,
|
type: Actions.SET_FLASHING_FLAG,
|
||||||
data: {},
|
data: {},
|
||||||
@@ -66,6 +69,8 @@ export function unsetFlashingFlag(results: {
|
|||||||
type: Actions.UNSET_FLASHING_FLAG,
|
type: Actions.UNSET_FLASHING_FLAG,
|
||||||
data: results,
|
data: results,
|
||||||
});
|
});
|
||||||
|
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||||
|
electron.ipcRenderer.invoke('enable-screensaver');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setDevicePaths(devicePaths: string[]) {
|
export function setDevicePaths(devicePaths: string[]) {
|
||||||
@@ -75,14 +80,29 @@ export function setDevicePaths(devicePaths: string[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addFailedDevicePath(devicePath: string) {
|
export function addFailedDeviceError({
|
||||||
const failedDevicePathsSet = new Set(
|
device,
|
||||||
store.getState().toJS().failedDevicePaths,
|
error,
|
||||||
|
}: {
|
||||||
|
device: DrivelistDrive;
|
||||||
|
error: Error;
|
||||||
|
}) {
|
||||||
|
const failedDeviceErrorsMap = new Map(
|
||||||
|
store.getState().toJS().failedDeviceErrors,
|
||||||
);
|
);
|
||||||
failedDevicePathsSet.add(devicePath);
|
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({
|
store.dispatch({
|
||||||
type: Actions.SET_FAILED_DEVICE_PATHS,
|
type: Actions.SET_FAILED_DEVICE_ERRORS,
|
||||||
data: Array.from(failedDevicePathsSet),
|
data: Array.from(failedDeviceErrorsMap),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,40 +15,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isSourceDrive,
|
|
||||||
DrivelistDrive,
|
DrivelistDrive,
|
||||||
|
isSourceDrive,
|
||||||
} from '../../../shared/drive-constraints';
|
} from '../../../shared/drive-constraints';
|
||||||
|
import { getDrives } from './available-drives';
|
||||||
|
import { getSelectedDrives } from './selection-state';
|
||||||
import * as settings from './settings';
|
import * as settings from './settings';
|
||||||
import { DEFAULT_STATE, observe } from './store';
|
import { observe, store } from './store';
|
||||||
|
|
||||||
const leds: Map<string, RGBLed> = new Map();
|
const leds: Map<string, RGBLed> = new Map();
|
||||||
|
const animator = new Animator([], 10);
|
||||||
function setLeds(
|
|
||||||
drivesPaths: Set<string>,
|
|
||||||
colorOrAnimation: Color | AnimationFunction,
|
|
||||||
frequency?: number,
|
|
||||||
) {
|
|
||||||
for (const path of drivesPaths) {
|
|
||||||
const led = leds.get(path);
|
|
||||||
if (led) {
|
|
||||||
if (Array.isArray(colorOrAnimation)) {
|
|
||||||
led.setStaticColor(colorOrAnimation);
|
|
||||||
} else {
|
|
||||||
led.setAnimation(colorOrAnimation, frequency);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const red: Color = [1, 0, 0];
|
|
||||||
const green: Color = [0, 1, 0];
|
|
||||||
const blue: Color = [0, 0, 1];
|
|
||||||
const white: Color = [1, 1, 1];
|
|
||||||
const black: Color = [0, 0, 0];
|
|
||||||
const purple: Color = [0.5, 0, 0.5];
|
|
||||||
|
|
||||||
function createAnimationFunction(
|
function createAnimationFunction(
|
||||||
intensityFunction: (t: number) => number,
|
intensityFunction: (t: number) => number,
|
||||||
@@ -56,21 +35,39 @@ function createAnimationFunction(
|
|||||||
): AnimationFunction {
|
): AnimationFunction {
|
||||||
return (t: number): Color => {
|
return (t: number): Color => {
|
||||||
const intensity = intensityFunction(t);
|
const intensity = intensityFunction(t);
|
||||||
return color.map((v) => v * intensity) as Color;
|
return color.map((v: number) => v * intensity) as Color;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function blink(t: number) {
|
function blink(t: number) {
|
||||||
return Math.floor(t / 1000) % 2;
|
return Math.floor(t) % 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function breathe(t: number) {
|
function one(_t: number) {
|
||||||
return (1 + Math.sin(t / 1000)) / 2;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const breatheBlue = createAnimationFunction(breathe, blue);
|
type LEDColors = {
|
||||||
const blinkGreen = createAnimationFunction(blink, green);
|
green: Color;
|
||||||
const blinkPurple = createAnimationFunction(blink, purple);
|
purple: Color;
|
||||||
|
red: Color;
|
||||||
|
blue: Color;
|
||||||
|
white: Color;
|
||||||
|
black: Color;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LEDAnimationFunctions = {
|
||||||
|
blinkGreen: AnimationFunction;
|
||||||
|
blinkPurple: AnimationFunction;
|
||||||
|
staticRed: AnimationFunction;
|
||||||
|
staticGreen: AnimationFunction;
|
||||||
|
staticBlue: AnimationFunction;
|
||||||
|
staticWhite: AnimationFunction;
|
||||||
|
staticBlack: AnimationFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ledColors: LEDColors;
|
||||||
|
let ledAnimationFunctions: LEDAnimationFunctions;
|
||||||
|
|
||||||
interface LedsState {
|
interface LedsState {
|
||||||
step: 'main' | 'flashing' | 'verifying' | 'finish';
|
step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||||
@@ -80,6 +77,17 @@ interface LedsState {
|
|||||||
failedDrives: string[];
|
failedDrives: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setLeds(animation: AnimationFunction, drivesPaths: Set<string>) {
|
||||||
|
const rgbLeds: RGBLed[] = [];
|
||||||
|
for (const path of drivesPaths) {
|
||||||
|
const led = leds.get(path);
|
||||||
|
if (led) {
|
||||||
|
rgbLeds.push(led);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { animation, rgbLeds };
|
||||||
|
}
|
||||||
|
|
||||||
// Source slot (1st slot): behaves as a target unless it is chosen as source
|
// Source slot (1st slot): behaves as a target unless it is chosen as source
|
||||||
// No drive: black
|
// No drive: black
|
||||||
// Drive plugged: blue - on
|
// Drive plugged: blue - on
|
||||||
@@ -110,6 +118,7 @@ export function updateLeds({
|
|||||||
// Remove selected devices from plugged set
|
// Remove selected devices from plugged set
|
||||||
for (const d of selectedOk) {
|
for (const d of selectedOk) {
|
||||||
plugged.delete(d);
|
plugged.delete(d);
|
||||||
|
unplugged.delete(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove plugged devices from unplugged set
|
// Remove plugged devices from unplugged set
|
||||||
@@ -122,79 +131,98 @@ export function updateLeds({
|
|||||||
selectedOk.delete(d);
|
selectedOk.delete(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mapping: Array<{
|
||||||
|
animation: AnimationFunction;
|
||||||
|
rgbLeds: RGBLed[];
|
||||||
|
}> = [];
|
||||||
// Handle source slot
|
// Handle source slot
|
||||||
if (sourceDrive !== undefined) {
|
if (sourceDrive !== undefined) {
|
||||||
if (unplugged.has(sourceDrive)) {
|
if (plugged.has(sourceDrive)) {
|
||||||
unplugged.delete(sourceDrive);
|
|
||||||
// TODO
|
|
||||||
setLeds(new Set([sourceDrive]), breatheBlue, 2);
|
|
||||||
} else if (plugged.has(sourceDrive)) {
|
|
||||||
plugged.delete(sourceDrive);
|
plugged.delete(sourceDrive);
|
||||||
setLeds(new Set([sourceDrive]), blue);
|
mapping.push(
|
||||||
|
setLeds(ledAnimationFunctions.staticBlue, new Set([sourceDrive])),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (step === 'main') {
|
if (step === 'main') {
|
||||||
setLeds(unplugged, black);
|
mapping.push(
|
||||||
setLeds(plugged, black);
|
setLeds(
|
||||||
setLeds(selectedOk, white);
|
ledAnimationFunctions.staticBlack,
|
||||||
setLeds(selectedFailed, white);
|
new Set([...unplugged, ...plugged]),
|
||||||
|
),
|
||||||
|
setLeds(
|
||||||
|
ledAnimationFunctions.staticWhite,
|
||||||
|
new Set([...selectedOk, ...selectedFailed]),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (step === 'flashing') {
|
} else if (step === 'flashing') {
|
||||||
setLeds(unplugged, black);
|
mapping.push(
|
||||||
setLeds(plugged, black);
|
setLeds(
|
||||||
setLeds(selectedOk, blinkPurple, 2);
|
ledAnimationFunctions.staticBlack,
|
||||||
setLeds(selectedFailed, red);
|
new Set([...unplugged, ...plugged]),
|
||||||
|
),
|
||||||
|
setLeds(ledAnimationFunctions.blinkPurple, selectedOk),
|
||||||
|
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||||
|
);
|
||||||
} else if (step === 'verifying') {
|
} else if (step === 'verifying') {
|
||||||
setLeds(unplugged, black);
|
mapping.push(
|
||||||
setLeds(plugged, black);
|
setLeds(
|
||||||
setLeds(selectedOk, blinkGreen, 2);
|
ledAnimationFunctions.staticBlack,
|
||||||
setLeds(selectedFailed, red);
|
new Set([...unplugged, ...plugged]),
|
||||||
|
),
|
||||||
|
setLeds(ledAnimationFunctions.blinkGreen, selectedOk),
|
||||||
|
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||||
|
);
|
||||||
} else if (step === 'finish') {
|
} else if (step === 'finish') {
|
||||||
setLeds(unplugged, black);
|
mapping.push(
|
||||||
setLeds(plugged, black);
|
setLeds(
|
||||||
setLeds(selectedOk, green);
|
ledAnimationFunctions.staticBlack,
|
||||||
setLeds(selectedFailed, red);
|
new Set([...unplugged, ...plugged]),
|
||||||
|
),
|
||||||
|
setLeds(ledAnimationFunctions.staticGreen, selectedOk),
|
||||||
|
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
animator.mapping = mapping;
|
||||||
|
|
||||||
interface DeviceFromState {
|
|
||||||
devicePath?: string;
|
|
||||||
device: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ledsState: LedsState | undefined;
|
let ledsState: LedsState | undefined;
|
||||||
|
|
||||||
function stateObserver(state: typeof DEFAULT_STATE) {
|
function stateObserver() {
|
||||||
const s = state.toJS();
|
const s = store.getState().toJS();
|
||||||
let step: 'main' | 'flashing' | 'verifying' | 'finish';
|
let step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||||
if (s.isFlashing) {
|
if (s.isFlashing) {
|
||||||
step = s.flashState.type;
|
step = s.flashState.type;
|
||||||
} else {
|
} else {
|
||||||
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
|
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
|
||||||
}
|
}
|
||||||
const availableDrives = s.availableDrives.filter(
|
const availableDrives = getDrives().filter(
|
||||||
(d: DeviceFromState) => d.devicePath,
|
(d: DrivelistDrive) => d.devicePath,
|
||||||
);
|
);
|
||||||
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
|
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
|
||||||
isSourceDrive(d, s.selection.image),
|
isSourceDrive(d, s.selection.image),
|
||||||
)[0]?.devicePath;
|
)[0]?.devicePath;
|
||||||
const availableDrivesPaths = availableDrives.map(
|
const availableDrivesPaths = availableDrives.map(
|
||||||
(d: DeviceFromState) => d.devicePath,
|
(d: DrivelistDrive) => d.devicePath,
|
||||||
);
|
);
|
||||||
let selectedDrivesPaths: string[];
|
let selectedDrivesPaths: string[];
|
||||||
if (step === 'main') {
|
if (step === 'main') {
|
||||||
selectedDrivesPaths = availableDrives
|
selectedDrivesPaths = getSelectedDrives()
|
||||||
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device))
|
.filter((drive) => drive.devicePath !== null)
|
||||||
.map((d: DrivelistDrive) => d.devicePath);
|
.map((drive) => drive.devicePath) as string[];
|
||||||
} else {
|
} else {
|
||||||
selectedDrivesPaths = s.devicePaths;
|
selectedDrivesPaths = s.devicePaths;
|
||||||
}
|
}
|
||||||
|
const failedDevicePaths = s.failedDeviceErrors.map(
|
||||||
|
([, { devicePath }]: [string, { devicePath: string }]) => devicePath,
|
||||||
|
);
|
||||||
const newLedsState = {
|
const newLedsState = {
|
||||||
step,
|
step,
|
||||||
sourceDrive: sourceDrivePath,
|
sourceDrive: sourceDrivePath,
|
||||||
availableDrives: availableDrivesPaths,
|
availableDrives: availableDrivesPaths,
|
||||||
selectedDrives: selectedDrivesPaths,
|
selectedDrives: selectedDrivesPaths,
|
||||||
failedDrives: s.failedDevicePaths,
|
failedDrives: failedDevicePaths,
|
||||||
};
|
} as LedsState;
|
||||||
if (!_.isEqual(newLedsState, ledsState)) {
|
if (!_.isEqual(newLedsState, ledsState)) {
|
||||||
updateLeds(newLedsState);
|
updateLeds(newLedsState);
|
||||||
ledsState = newLedsState;
|
ledsState = newLedsState;
|
||||||
@@ -217,6 +245,16 @@ export async function init(): Promise<void> {
|
|||||||
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
|
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
|
||||||
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
|
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
|
||||||
}
|
}
|
||||||
|
ledColors = (await settings.get('ledColors')) || {};
|
||||||
|
ledAnimationFunctions = {
|
||||||
|
blinkGreen: createAnimationFunction(blink, ledColors['green']),
|
||||||
|
blinkPurple: createAnimationFunction(blink, ledColors['purple']),
|
||||||
|
staticRed: createAnimationFunction(one, ledColors['red']),
|
||||||
|
staticGreen: createAnimationFunction(one, ledColors['green']),
|
||||||
|
staticBlue: createAnimationFunction(one, ledColors['blue']),
|
||||||
|
staticWhite: createAnimationFunction(one, ledColors['white']),
|
||||||
|
staticBlack: createAnimationFunction(one, ledColors['black']),
|
||||||
|
};
|
||||||
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
|
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -72,26 +72,6 @@ export function getImage(): SourceMetadata | undefined {
|
|||||||
return store.getState().toJS().selection.image;
|
return store.getState().toJS().selection.image;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImagePath() {
|
|
||||||
return getImage()?.path;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getImageSize() {
|
|
||||||
return getImage()?.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getImageName() {
|
|
||||||
return getImage()?.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getImageLogo() {
|
|
||||||
return getImage()?.logo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getImageSupportUrl() {
|
|
||||||
return getImage()?.supportUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Check if there is a selected drive
|
* @summary Check if there is a selected drive
|
||||||
*/
|
*/
|
||||||
|
@@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings');
|
|||||||
|
|
||||||
const JSON_INDENT = 2;
|
const JSON_INDENT = 2;
|
||||||
|
|
||||||
|
export const DEFAULT_WIDTH = 800;
|
||||||
|
export const DEFAULT_HEIGHT = 480;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Userdata directory path
|
* @summary Userdata directory path
|
||||||
* @description
|
* @description
|
||||||
@@ -35,12 +38,12 @@ const JSON_INDENT = 2;
|
|||||||
* - `~/Library/Application Support/etcher` on macOS
|
* - `~/Library/Application Support/etcher` on macOS
|
||||||
* See https://electronjs.org/docs/api/app#appgetpathname
|
* See https://electronjs.org/docs/api/app#appgetpathname
|
||||||
*
|
*
|
||||||
* NOTE: The ternary is due to this module being loaded both,
|
* NOTE: We use the remote property when this module
|
||||||
* Electron's main process and renderer process
|
* is loaded in the Electron's renderer process
|
||||||
*/
|
*/
|
||||||
const USER_DATA_DIR = electron.app
|
const app = electron.app || electron.remote.app;
|
||||||
? electron.app.getPath('userData')
|
|
||||||
: electron.remote.app.getPath('userData');
|
const USER_DATA_DIR = app.getPath('userData');
|
||||||
|
|
||||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
|||||||
let contents = '{}';
|
let contents = '{}';
|
||||||
try {
|
try {
|
||||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -74,9 +77,7 @@ export async function writeConfigFile(
|
|||||||
|
|
||||||
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||||
errorReporting: true,
|
errorReporting: true,
|
||||||
unmountOnSuccess: true,
|
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
|
||||||
validateWriteOnSuccess: true,
|
|
||||||
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
|
|
||||||
desktopNotifications: true,
|
desktopNotifications: true,
|
||||||
autoBlockmapping: true,
|
autoBlockmapping: true,
|
||||||
decompressFirst: true,
|
decompressFirst: true,
|
||||||
@@ -103,7 +104,7 @@ export async function set(
|
|||||||
settings[key] = value;
|
settings[key] = value;
|
||||||
try {
|
try {
|
||||||
await writeConfigFileFn(CONFIG_PATH, settings);
|
await writeConfigFileFn(CONFIG_PATH, settings);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// Revert to previous value if persisting settings failed
|
// Revert to previous value if persisting settings failed
|
||||||
settings[key] = previousValue;
|
settings[key] = previousValue;
|
||||||
throw error;
|
throw error;
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import * as Immutable from 'immutable';
|
import * as Immutable from 'immutable';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
import { basename } from 'path';
|
||||||
import * as redux from 'redux';
|
import * as redux from 'redux';
|
||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
|||||||
},
|
},
|
||||||
isFlashing: false,
|
isFlashing: false,
|
||||||
devicePaths: [],
|
devicePaths: [],
|
||||||
failedDevicePaths: [],
|
failedDeviceErrors: [],
|
||||||
flashResults: {},
|
flashResults: {},
|
||||||
flashState: {
|
flashState: {
|
||||||
active: 0,
|
active: 0,
|
||||||
@@ -79,7 +80,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
|||||||
*/
|
*/
|
||||||
export enum Actions {
|
export enum Actions {
|
||||||
SET_DEVICE_PATHS,
|
SET_DEVICE_PATHS,
|
||||||
SET_FAILED_DEVICE_PATHS,
|
SET_FAILED_DEVICE_ERRORS,
|
||||||
SET_AVAILABLE_TARGETS,
|
SET_AVAILABLE_TARGETS,
|
||||||
SET_FLASH_STATE,
|
SET_FLASH_STATE,
|
||||||
RESET_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, [
|
drives = _.sortBy(drives, [
|
||||||
// System drives last
|
// System drives last
|
||||||
(d) => !!d.isSystem,
|
(d) => !!d.isSystem,
|
||||||
// Devices with no devicePath first (usbboot)
|
// Devices with no devicePath first (usbboot)
|
||||||
(d) => !!d.devicePath,
|
(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
|
// Then sort by devicePath (only available on Linux with udev) or device
|
||||||
(d) => d.devicePath || d.device,
|
(d) => d.devicePath || d.device,
|
||||||
]);
|
]);
|
||||||
@@ -169,7 +175,7 @@ function storeReducer(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const shouldAutoselectAll = Boolean(
|
const shouldAutoselectAll = Boolean(
|
||||||
settings.getSync('disableExplicitDriveSelection'),
|
settings.getSync('autoSelectAllDrives'),
|
||||||
);
|
);
|
||||||
const AUTOSELECT_DRIVE_COUNT = 1;
|
const AUTOSELECT_DRIVE_COUNT = 1;
|
||||||
const nonStaleSelectedDevices = nonStaleNewState
|
const nonStaleSelectedDevices = nonStaleNewState
|
||||||
@@ -191,18 +197,13 @@ function storeReducer(
|
|||||||
drives,
|
drives,
|
||||||
(accState, drive) => {
|
(accState, drive) => {
|
||||||
if (
|
if (
|
||||||
_.every([
|
constraints.isDriveValid(drive, image) &&
|
||||||
constraints.isDriveValid(drive, image),
|
!drive.isReadOnly &&
|
||||||
constraints.isDriveSizeRecommended(drive, image),
|
constraints.isDriveSizeRecommended(drive, image) &&
|
||||||
|
// We don't want to auto-select large drives execpt is autoSelectAllDrives is true
|
||||||
// We don't want to auto-select large drives
|
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
|
||||||
!constraints.isDriveSizeLarge(drive),
|
// We don't want to auto-select system drives
|
||||||
|
!constraints.isSystemDrive(drive)
|
||||||
// We don't want to auto-select system drives,
|
|
||||||
// even when "unsafe mode" is enabled
|
|
||||||
!constraints.isSystemDrive(drive),
|
|
||||||
]) ||
|
|
||||||
(shouldAutoselectAll && constraints.isDriveValid(drive, image))
|
|
||||||
) {
|
) {
|
||||||
// Auto-select this drive
|
// Auto-select this drive
|
||||||
return storeReducer(accState, {
|
return storeReducer(accState, {
|
||||||
@@ -269,7 +270,7 @@ function storeReducer(
|
|||||||
.set('flashState', DEFAULT_STATE.get('flashState'))
|
.set('flashState', DEFAULT_STATE.get('flashState'))
|
||||||
.set('flashResults', DEFAULT_STATE.get('flashResults'))
|
.set('flashResults', DEFAULT_STATE.get('flashResults'))
|
||||||
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
|
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
|
||||||
.set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths'))
|
.set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
|
||||||
.set(
|
.set(
|
||||||
'lastAverageFlashingSpeed',
|
'lastAverageFlashingSpeed',
|
||||||
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
|
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
|
||||||
@@ -295,6 +296,7 @@ function storeReducer(
|
|||||||
|
|
||||||
_.defaults(action.data, {
|
_.defaults(action.data, {
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
|
skip: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!_.isBoolean(action.data.cancelled)) {
|
if (!_.isBoolean(action.data.cancelled)) {
|
||||||
@@ -335,6 +337,12 @@ function storeReducer(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.data.skip) {
|
||||||
|
return state
|
||||||
|
.set('isFlashing', false)
|
||||||
|
.set('flashResults', Immutable.fromJS(action.data));
|
||||||
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
.set('isFlashing', false)
|
.set('isFlashing', false)
|
||||||
.set('flashResults', Immutable.fromJS(action.data))
|
.set('flashResults', Immutable.fromJS(action.data))
|
||||||
@@ -509,8 +517,8 @@ function storeReducer(
|
|||||||
return state.set('devicePaths', action.data);
|
return state.set('devicePaths', action.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Actions.SET_FAILED_DEVICE_PATHS: {
|
case Actions.SET_FAILED_DEVICE_ERRORS: {
|
||||||
return state.set('failedDevicePaths', action.data);
|
return state.set('failedDeviceErrors', action.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
|
@@ -102,10 +102,9 @@ function validateMixpanelConfig(config: {
|
|||||||
* This function sends the debug message to product analytics services.
|
* This function sends the debug message to product analytics services.
|
||||||
*/
|
*/
|
||||||
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
|
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
|
||||||
const {
|
const { applicationSessionUuid, flashingWorkflowUuid } = store
|
||||||
applicationSessionUuid,
|
.getState()
|
||||||
flashingWorkflowUuid,
|
.toJS();
|
||||||
} = store.getState().toJS();
|
|
||||||
resinCorvus.logEvent(message, {
|
resinCorvus.logEvent(message, {
|
||||||
...data,
|
...data,
|
||||||
sample: mixpanelSample,
|
sample: mixpanelSample,
|
||||||
|
@@ -15,10 +15,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as sdk from 'etcher-sdk';
|
import * as sdk from 'etcher-sdk';
|
||||||
|
import {
|
||||||
|
Adapter,
|
||||||
|
BlockDeviceAdapter,
|
||||||
|
UsbbootDeviceAdapter,
|
||||||
|
} from 'etcher-sdk/build/scanner/adapters';
|
||||||
import { geteuid, platform } from 'process';
|
import { geteuid, platform } from 'process';
|
||||||
|
|
||||||
const adapters: sdk.scanner.adapters.Adapter[] = [
|
const adapters: Adapter[] = [
|
||||||
new sdk.scanner.adapters.BlockDeviceAdapter({
|
new BlockDeviceAdapter({
|
||||||
includeSystemDrives: () => true,
|
includeSystemDrives: () => true,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -26,14 +31,15 @@ const adapters: sdk.scanner.adapters.Adapter[] = [
|
|||||||
// Can't use permissions.isElevated() here as it returns a promise and we need to set
|
// Can't use permissions.isElevated() here as it returns a promise and we need to set
|
||||||
// module.exports = scanner right now.
|
// module.exports = scanner right now.
|
||||||
if (platform !== 'linux' || geteuid() === 0) {
|
if (platform !== 'linux' || geteuid() === 0) {
|
||||||
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter());
|
adapters.push(new UsbbootDeviceAdapter());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (platform === 'win32') {
|
||||||
platform === 'win32' &&
|
const {
|
||||||
sdk.scanner.adapters.DriverlessDeviceAdapter !== undefined
|
DriverlessDeviceAdapter: driverless,
|
||||||
) {
|
// tslint:disable-next-line:no-var-requires
|
||||||
adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter());
|
} = require('etcher-sdk/build/scanner/adapters/driverless');
|
||||||
|
adapters.push(new driverless());
|
||||||
}
|
}
|
||||||
|
|
||||||
export const scanner = new sdk.scanner.Scanner(adapters);
|
export const scanner = new sdk.scanner.Scanner(adapters);
|
||||||
|
@@ -15,9 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
import { Drive as DrivelistDrive } from 'drivelist';
|
||||||
import * as electron from 'electron';
|
|
||||||
import * as sdk from 'etcher-sdk';
|
import * as sdk from 'etcher-sdk';
|
||||||
import * as _ from 'lodash';
|
import { Dictionary } from 'lodash';
|
||||||
import * as ipc from 'node-ipc';
|
import * as ipc from 'node-ipc';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -25,6 +24,7 @@ import * as path from 'path';
|
|||||||
import * as packageJSON from '../../../../package.json';
|
import * as packageJSON from '../../../../package.json';
|
||||||
import * as errors from '../../../shared/errors';
|
import * as errors from '../../../shared/errors';
|
||||||
import * as permissions from '../../../shared/permissions';
|
import * as permissions from '../../../shared/permissions';
|
||||||
|
import { getAppPath } from '../../../shared/utils';
|
||||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||||
import * as flashState from '../models/flash-state';
|
import * as flashState from '../models/flash-state';
|
||||||
import * as selectionState from '../models/selection-state';
|
import * as selectionState from '../models/selection-state';
|
||||||
@@ -93,11 +93,7 @@ function terminateServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writerArgv(): string[] {
|
function writerArgv(): string[] {
|
||||||
let entryPoint = path.join(
|
let entryPoint = path.join(getAppPath(), 'generated', 'child-writer.js');
|
||||||
electron.remote.app.getAppPath(),
|
|
||||||
'generated',
|
|
||||||
'child-writer.js',
|
|
||||||
);
|
|
||||||
// AppImages run over FUSE, so the files inside the mount point
|
// AppImages run over FUSE, so the files inside the mount point
|
||||||
// can only be accessed by the user that mounted the AppImage.
|
// can only be accessed by the user that mounted the AppImage.
|
||||||
// This means we can't re-spawn Etcher as root from the same
|
// This means we can't re-spawn Etcher as root from the same
|
||||||
@@ -131,7 +127,16 @@ function writerEnv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FlashResults {
|
interface FlashResults {
|
||||||
|
skip?: boolean;
|
||||||
cancelled?: boolean;
|
cancelled?: boolean;
|
||||||
|
results?: {
|
||||||
|
bytesWritten: number;
|
||||||
|
devices: {
|
||||||
|
failed: number;
|
||||||
|
successful: number;
|
||||||
|
};
|
||||||
|
errors: Error[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performWrite(
|
async function performWrite(
|
||||||
@@ -140,13 +145,9 @@ async function performWrite(
|
|||||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||||
): Promise<{ cancelled?: boolean }> {
|
): Promise<{ cancelled?: boolean }> {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
let skip = false;
|
||||||
ipc.serve();
|
ipc.serve();
|
||||||
const {
|
const { autoBlockmapping, decompressFirst } = await settings.getAll();
|
||||||
unmountOnSuccess,
|
|
||||||
validateWriteOnSuccess,
|
|
||||||
autoBlockmapping,
|
|
||||||
decompressFirst,
|
|
||||||
} = await settings.getAll();
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
ipc.server.on('error', (error) => {
|
ipc.server.on('error', (error) => {
|
||||||
terminateServer();
|
terminateServer();
|
||||||
@@ -165,22 +166,22 @@ async function performWrite(
|
|||||||
driveCount: drives.length,
|
driveCount: drives.length,
|
||||||
uuid: flashState.getFlashUuid(),
|
uuid: flashState.getFlashUuid(),
|
||||||
flashInstanceUuid: flashState.getFlashUuid(),
|
flashInstanceUuid: flashState.getFlashUuid(),
|
||||||
unmountOnSuccess,
|
|
||||||
validateWriteOnSuccess,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ipc.server.on('fail', ({ device, error }) => {
|
ipc.server.on('fail', ({ device, error }) => {
|
||||||
if (device.devicePath) {
|
if (device.devicePath) {
|
||||||
flashState.addFailedDevicePath(device.devicePath);
|
flashState.addFailedDeviceError({ device, error });
|
||||||
}
|
}
|
||||||
handleErrorLogging(error, analyticsData);
|
handleErrorLogging(error, analyticsData);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.server.on('done', (event) => {
|
ipc.server.on('done', (event) => {
|
||||||
event.results.errors = _.map(event.results.errors, (data) => {
|
event.results.errors = event.results.errors.map(
|
||||||
return errors.fromJSON(data);
|
(data: Dictionary<any> & { message: string }) => {
|
||||||
});
|
return errors.fromJSON(data);
|
||||||
_.merge(flashResults, event);
|
},
|
||||||
|
);
|
||||||
|
flashResults.results = event.results;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.server.on('abort', () => {
|
ipc.server.on('abort', () => {
|
||||||
@@ -188,6 +189,11 @@ async function performWrite(
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipc.server.on('skip', () => {
|
||||||
|
terminateServer();
|
||||||
|
skip = true;
|
||||||
|
});
|
||||||
|
|
||||||
ipc.server.on('state', onProgress);
|
ipc.server.on('state', onProgress);
|
||||||
|
|
||||||
ipc.server.on('ready', (_data, socket) => {
|
ipc.server.on('ready', (_data, socket) => {
|
||||||
@@ -195,9 +201,7 @@ async function performWrite(
|
|||||||
image,
|
image,
|
||||||
destinations: drives,
|
destinations: drives,
|
||||||
SourceType: image.SourceType.name,
|
SourceType: image.SourceType.name,
|
||||||
validateWriteOnSuccess,
|
|
||||||
autoBlockmapping,
|
autoBlockmapping,
|
||||||
unmountOnSuccess,
|
|
||||||
decompressFirst,
|
decompressFirst,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -205,7 +209,7 @@ async function performWrite(
|
|||||||
const argv = writerArgv();
|
const argv = writerArgv();
|
||||||
|
|
||||||
ipc.server.on('start', async () => {
|
ipc.server.on('start', async () => {
|
||||||
console.log(`Elevating command: ${_.join(argv, ' ')}`);
|
console.log(`Elevating command: ${argv.join(' ')}`);
|
||||||
const env = writerEnv();
|
const env = writerEnv();
|
||||||
try {
|
try {
|
||||||
const results = await permissions.elevateCommand(argv, {
|
const results = await permissions.elevateCommand(argv, {
|
||||||
@@ -213,7 +217,8 @@ async function performWrite(
|
|||||||
environment: env,
|
environment: env,
|
||||||
});
|
});
|
||||||
flashResults.cancelled = cancelled || results.cancelled;
|
flashResults.cancelled = cancelled || results.cancelled;
|
||||||
} catch (error) {
|
flashResults.skip = skip;
|
||||||
|
} catch (error: any) {
|
||||||
// This happens when the child is killed using SIGKILL
|
// This happens when the child is killed using SIGKILL
|
||||||
const SIGKILL_EXIT_CODE = 137;
|
const SIGKILL_EXIT_CODE = 137;
|
||||||
if (error.code === SIGKILL_EXIT_CODE) {
|
if (error.code === SIGKILL_EXIT_CODE) {
|
||||||
@@ -226,10 +231,11 @@ async function performWrite(
|
|||||||
}
|
}
|
||||||
console.log('Flash results', flashResults);
|
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
|
||||||
if (
|
if (
|
||||||
!flashResults.cancelled &&
|
!flashResults.cancelled &&
|
||||||
!_.get(flashResults, ['results', 'bytesWritten'])
|
!flashResults.skip &&
|
||||||
|
flashResults.results === undefined
|
||||||
) {
|
) {
|
||||||
reject(
|
reject(
|
||||||
errors.createUserError({
|
errors.createUserError({
|
||||||
@@ -262,7 +268,7 @@ export async function flash(
|
|||||||
throw new Error('There is already a flash in progress');
|
throw new Error('There is already a flash in progress');
|
||||||
}
|
}
|
||||||
|
|
||||||
flashState.setFlashingFlag();
|
await flashState.setFlashingFlag();
|
||||||
flashState.setDevicePaths(
|
flashState.setDevicePaths(
|
||||||
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
||||||
);
|
);
|
||||||
@@ -274,20 +280,20 @@ export async function flash(
|
|||||||
uuid: flashState.getFlashUuid(),
|
uuid: flashState.getFlashUuid(),
|
||||||
status: 'started',
|
status: 'started',
|
||||||
flashInstanceUuid: flashState.getFlashUuid(),
|
flashInstanceUuid: flashState.getFlashUuid(),
|
||||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
|
||||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
analytics.logEvent('Flash', analyticsData);
|
analytics.logEvent('Flash', analyticsData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await write(image, drives, flashState.setProgressState);
|
const result = await write(image, drives, flashState.setProgressState);
|
||||||
flashState.unsetFlashingFlag(result);
|
await flashState.unsetFlashingFlag(result);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
|
await flashState.unsetFlashingFlag({
|
||||||
|
cancelled: false,
|
||||||
|
errorCode: error.code,
|
||||||
|
});
|
||||||
windowProgress.clear();
|
windowProgress.clear();
|
||||||
let { results } = flashState.getFlashResults();
|
const { results = {} } = flashState.getFlashResults();
|
||||||
results = results || {};
|
|
||||||
const eventData = {
|
const eventData = {
|
||||||
...analyticsData,
|
...analyticsData,
|
||||||
errors: results.errors,
|
errors: results.errors,
|
||||||
@@ -306,7 +312,7 @@ export async function flash(
|
|||||||
};
|
};
|
||||||
analytics.logEvent('Elevation cancelled', eventData);
|
analytics.logEvent('Elevation cancelled', eventData);
|
||||||
} else {
|
} else {
|
||||||
const { results } = flashState.getFlashResults();
|
const { results = {} } = flashState.getFlashResults();
|
||||||
const eventData = {
|
const eventData = {
|
||||||
...analyticsData,
|
...analyticsData,
|
||||||
errors: results.errors,
|
errors: results.errors,
|
||||||
@@ -322,17 +328,16 @@ export async function flash(
|
|||||||
/**
|
/**
|
||||||
* @summary Cancel write operation
|
* @summary Cancel write operation
|
||||||
*/
|
*/
|
||||||
export async function cancel() {
|
export async function cancel(type: string) {
|
||||||
|
const status = type.toLowerCase();
|
||||||
const drives = selectionState.getSelectedDevices();
|
const drives = selectionState.getSelectedDevices();
|
||||||
const analyticsData = {
|
const analyticsData = {
|
||||||
image: selectionState.getImagePath(),
|
image: selectionState.getImage()?.path,
|
||||||
drives,
|
drives,
|
||||||
driveCount: drives.length,
|
driveCount: drives.length,
|
||||||
uuid: flashState.getFlashUuid(),
|
uuid: flashState.getFlashUuid(),
|
||||||
flashInstanceUuid: flashState.getFlashUuid(),
|
flashInstanceUuid: flashState.getFlashUuid(),
|
||||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
status,
|
||||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
|
||||||
status: 'cancel',
|
|
||||||
};
|
};
|
||||||
analytics.logEvent('Cancel', analyticsData);
|
analytics.logEvent('Cancel', analyticsData);
|
||||||
|
|
||||||
@@ -342,9 +347,9 @@ export async function cancel() {
|
|||||||
// @ts-ignore (no Server.sockets in @types/node-ipc)
|
// @ts-ignore (no Server.sockets in @types/node-ipc)
|
||||||
const [socket] = ipc.server.sockets;
|
const [socket] = ipc.server.sockets;
|
||||||
if (socket !== undefined) {
|
if (socket !== undefined) {
|
||||||
ipc.server.emit(socket, 'cancel');
|
ipc.server.emit(socket, status);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
analytics.logException(error);
|
analytics.logException(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,7 +27,7 @@ async function mountSourceDrive() {
|
|||||||
if (sourceDrivePath) {
|
if (sourceDrivePath) {
|
||||||
try {
|
try {
|
||||||
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
|
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
|
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
||||||
import { readFile } from 'fs';
|
import { readFile } from 'fs';
|
||||||
import { chain, trim } from 'lodash';
|
import { chain, trim } from 'lodash';
|
||||||
import { platform } from 'os';
|
import { platform } from 'os';
|
||||||
@@ -22,8 +23,6 @@ import { join } from 'path';
|
|||||||
import { env } from 'process';
|
import { env } from 'process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
import { withTmpFile } from '../../../shared/tmp';
|
|
||||||
|
|
||||||
const readFileAsync = promisify(readFile);
|
const readFileAsync = promisify(readFile);
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -41,11 +40,11 @@ async function getWmicNetworkDrivesOutput(): Promise<string> {
|
|||||||
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
|
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
|
||||||
const options = {
|
const options = {
|
||||||
// Close the file once it's created
|
// Close the file once it's created
|
||||||
discardDescriptor: true,
|
keepOpen: false,
|
||||||
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
|
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
|
||||||
prefix: 'tmp',
|
prefix: 'tmp',
|
||||||
};
|
};
|
||||||
return withTmpFile(options, async (path) => {
|
return withTmpFile(options, async ({ path }) => {
|
||||||
const command = [
|
const command = [
|
||||||
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
|
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
|
||||||
'path',
|
'path',
|
||||||
|
@@ -59,6 +59,27 @@ const getErrorMessageFromCode = (errorCode: string) => {
|
|||||||
return '';
|
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(
|
async function flashImageToDrive(
|
||||||
isFlashing: boolean,
|
isFlashing: boolean,
|
||||||
goToSuccess: () => void,
|
goToSuccess: () => void,
|
||||||
@@ -82,24 +103,22 @@ async function flashImageToDrive(
|
|||||||
try {
|
try {
|
||||||
await imageWriter.flash(image, drives);
|
await imageWriter.flash(image, drives);
|
||||||
if (!flashState.wasLastFlashCancelled()) {
|
if (!flashState.wasLastFlashCancelled()) {
|
||||||
const flashResults: any = flashState.getFlashResults();
|
const {
|
||||||
notification.send(
|
results = { devices: { successful: 0, failed: 0 } },
|
||||||
'Flash complete!',
|
skip,
|
||||||
messages.info.flashComplete(
|
cancelled,
|
||||||
basename,
|
} = flashState.getFlashResults();
|
||||||
drives as any,
|
if (!skip && !cancelled) {
|
||||||
flashResults.results.devices,
|
if (results.devices.successful > 0) {
|
||||||
),
|
notifySuccess(iconPath, basename, drives, results.devices);
|
||||||
iconPath,
|
} else {
|
||||||
);
|
notifyFailure(iconPath, basename, drives);
|
||||||
|
}
|
||||||
|
}
|
||||||
goToSuccess();
|
goToSuccess();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
notification.send(
|
notifyFailure(iconPath, basename, drives);
|
||||||
'Oops! Looks like the flash failed.',
|
|
||||||
messages.error.flashFailure(path.basename(image.path), drives),
|
|
||||||
iconPath,
|
|
||||||
);
|
|
||||||
let errorMessage = getErrorMessageFromCode(error.code);
|
let errorMessage = getErrorMessageFromCode(error.code);
|
||||||
if (!errorMessage) {
|
if (!errorMessage) {
|
||||||
error.image = basename;
|
error.image = basename;
|
||||||
@@ -137,6 +156,7 @@ interface FlashStepProps {
|
|||||||
failed: number;
|
failed: number;
|
||||||
speed?: number;
|
speed?: number;
|
||||||
eta?: number;
|
eta?: number;
|
||||||
|
width: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DriveWithWarnings extends constraints.DrivelistDrive {
|
export interface DriveWithWarnings extends constraints.DrivelistDrive {
|
||||||
@@ -201,7 +221,11 @@ export class FlashStep extends React.PureComponent<
|
|||||||
const drives = selection.getSelectedDrives().map((drive) => {
|
const drives = selection.getSelectedDrives().map((drive) => {
|
||||||
return {
|
return {
|
||||||
...drive,
|
...drive,
|
||||||
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
|
statuses: constraints.getDriveImageCompatibilityStatuses(
|
||||||
|
drive,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (drives.length === 0 || this.props.isFlashing) {
|
if (drives.length === 0 || this.props.isFlashing) {
|
||||||
@@ -239,6 +263,7 @@ export class FlashStep extends React.PureComponent<
|
|||||||
<Flex
|
<Flex
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems="start"
|
alignItems="start"
|
||||||
|
width={this.props.width}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
>
|
>
|
||||||
<FlashSvg
|
<FlashSvg
|
||||||
@@ -310,6 +335,7 @@ export class FlashStep extends React.PureComponent<
|
|||||||
)}
|
)}
|
||||||
{this.state.showDriveSelectorModal && (
|
{this.state.showDriveSelectorModal && (
|
||||||
<TargetSelectorModal
|
<TargetSelectorModal
|
||||||
|
write={true}
|
||||||
cancel={() => this.setState({ showDriveSelectorModal: false })}
|
cancel={() => this.setState({ showDriveSelectorModal: false })}
|
||||||
done={(modalTargets) => {
|
done={(modalTargets) => {
|
||||||
selectAllTargets(modalTargets);
|
selectAllTargets(modalTargets);
|
||||||
|
@@ -25,7 +25,6 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
import FinishPage from '../../components/finish/finish';
|
import FinishPage from '../../components/finish/finish';
|
||||||
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
||||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
|
||||||
import { SettingsModal } from '../../components/settings/settings';
|
import { SettingsModal } from '../../components/settings/settings';
|
||||||
import {
|
import {
|
||||||
SourceMetadata,
|
SourceMetadata,
|
||||||
@@ -48,6 +47,7 @@ import {
|
|||||||
import { FlashStep } from './Flash';
|
import { FlashStep } from './Flash';
|
||||||
|
|
||||||
import EtcherSvg from '../../../assets/etcher.svg';
|
import EtcherSvg from '../../../assets/etcher.svg';
|
||||||
|
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||||
|
|
||||||
const Icon = styled(BaseIcon)`
|
const Icon = styled(BaseIcon)`
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
@@ -132,12 +132,13 @@ export class MainPage extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private stateHelper(): MainPageStateFromStore {
|
private stateHelper(): MainPageStateFromStore {
|
||||||
|
const image = selectionState.getImage();
|
||||||
return {
|
return {
|
||||||
isFlashing: flashState.isFlashing(),
|
isFlashing: flashState.isFlashing(),
|
||||||
hasImage: selectionState.hasImage(),
|
hasImage: selectionState.hasImage(),
|
||||||
hasDrive: selectionState.hasDrive(),
|
hasDrive: selectionState.hasDrive(),
|
||||||
imageLogo: selectionState.getImageLogo(),
|
imageLogo: image?.logo,
|
||||||
imageSize: selectionState.getImageSize(),
|
imageSize: image?.size,
|
||||||
imageName: getImageBasename(selectionState.getImage()),
|
imageName: getImageBasename(selectionState.getImage()),
|
||||||
driveTitle: getDrivesTitle(),
|
driveTitle: getDrivesTitle(),
|
||||||
driveLabel: getDriveListLabel(),
|
driveLabel: getDriveListLabel(),
|
||||||
@@ -169,7 +170,105 @@ export class MainPage extends React.Component<
|
|||||||
const notFlashingOrSplitView =
|
const notFlashingOrSplitView =
|
||||||
!this.state.isFlashing || !this.state.isWebviewShowing;
|
!this.state.isFlashing || !this.state.isWebviewShowing;
|
||||||
return (
|
return (
|
||||||
<>
|
<Flex
|
||||||
|
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
{notFlashingOrSplitView && (
|
||||||
|
<>
|
||||||
|
<SourceSelector flashing={this.state.isFlashing} />
|
||||||
|
<Flex>
|
||||||
|
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||||
|
</Flex>
|
||||||
|
<TargetSelector
|
||||||
|
disabled={shouldDriveStepBeDisabled}
|
||||||
|
hasDrive={this.state.hasDrive}
|
||||||
|
flashing={this.state.isFlashing}
|
||||||
|
/>
|
||||||
|
<Flex>
|
||||||
|
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '36.2vw',
|
||||||
|
height: '100vh',
|
||||||
|
zIndex: 1,
|
||||||
|
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReducedFlashingInfos
|
||||||
|
imageLogo={this.state.imageLogo}
|
||||||
|
imageName={this.state.imageName}
|
||||||
|
imageSize={
|
||||||
|
typeof this.state.imageSize === 'number'
|
||||||
|
? prettyBytes(this.state.imageSize)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
driveTitle={this.state.driveTitle}
|
||||||
|
driveLabel={this.state.driveLabel}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
color: '#fff',
|
||||||
|
left: 35,
|
||||||
|
top: 72,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{this.state.isFlashing && this.state.featuredProjectURL && (
|
||||||
|
<SafeWebview
|
||||||
|
src={this.state.featuredProjectURL}
|
||||||
|
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||||
|
this.setState({ isWebviewShowing });
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '63.8vw',
|
||||||
|
height: '100vh',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FlashStep
|
||||||
|
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
||||||
|
goToSuccess={() => this.setState({ current: 'success' })}
|
||||||
|
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||||
|
isFlashing={this.state.isFlashing}
|
||||||
|
step={state.type}
|
||||||
|
percentage={state.percentage}
|
||||||
|
position={state.position}
|
||||||
|
failed={state.failed}
|
||||||
|
speed={state.speed}
|
||||||
|
eta={state.eta}
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSuccess() {
|
||||||
|
return (
|
||||||
|
<FinishPage
|
||||||
|
goToMain={() => {
|
||||||
|
flashState.resetState();
|
||||||
|
this.setState({ current: 'main' });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<ThemedProvider style={{ height: '100%', width: '100%' }}>
|
||||||
<Flex
|
<Flex
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
@@ -177,9 +276,9 @@ export class MainPage extends React.Component<
|
|||||||
style={{
|
style={{
|
||||||
// Allow window to be dragged from header
|
// Allow window to be dragged from header
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
'-webkit-app-region': 'drag',
|
WebkitAppRegion: 'drag',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 1,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex width="100%" />
|
<Flex width="100%" />
|
||||||
@@ -205,7 +304,7 @@ export class MainPage extends React.Component<
|
|||||||
onClick={() => this.setState({ hideSettings: false })}
|
onClick={() => this.setState({ hideSettings: false })}
|
||||||
style={{
|
style={{
|
||||||
// Make touch events click instead of dragging
|
// Make touch events click instead of dragging
|
||||||
'-webkit-app-region': 'no-drag',
|
WebkitAppRegion: 'no-drag',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!settings.getSync('disableExternalLinks') && (
|
{!settings.getSync('disableExternalLinks') && (
|
||||||
@@ -213,14 +312,14 @@ export class MainPage extends React.Component<
|
|||||||
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
|
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openExternal(
|
openExternal(
|
||||||
selectionState.getImageSupportUrl() ||
|
selectionState.getImage()?.supportUrl ||
|
||||||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
|
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
tabIndex={6}
|
tabIndex={6}
|
||||||
style={{
|
style={{
|
||||||
// Make touch events click instead of dragging
|
// Make touch events click instead of dragging
|
||||||
'-webkit-app-region': 'no-drag',
|
WebkitAppRegion: 'no-drag',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -233,117 +332,6 @@ export class MainPage extends React.Component<
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex
|
|
||||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
|
||||||
justifyContent="space-between"
|
|
||||||
>
|
|
||||||
{notFlashingOrSplitView && (
|
|
||||||
<>
|
|
||||||
<SourceSelector flashing={this.state.isFlashing} />
|
|
||||||
<Flex>
|
|
||||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
|
||||||
</Flex>
|
|
||||||
<TargetSelector
|
|
||||||
disabled={shouldDriveStepBeDisabled}
|
|
||||||
hasDrive={this.state.hasDrive}
|
|
||||||
flashing={this.state.isFlashing}
|
|
||||||
/>
|
|
||||||
<Flex>
|
|
||||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
|
||||||
<Flex
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: '36.2vw',
|
|
||||||
height: '100vh',
|
|
||||||
zIndex: 1,
|
|
||||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ReducedFlashingInfos
|
|
||||||
imageLogo={this.state.imageLogo}
|
|
||||||
imageName={this.state.imageName}
|
|
||||||
imageSize={
|
|
||||||
typeof this.state.imageSize === 'number'
|
|
||||||
? prettyBytes(this.state.imageSize)
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
driveTitle={this.state.driveTitle}
|
|
||||||
driveLabel={this.state.driveLabel}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
color: '#fff',
|
|
||||||
left: 35,
|
|
||||||
top: 72,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
{this.state.isFlashing && this.state.featuredProjectURL && (
|
|
||||||
<SafeWebview
|
|
||||||
src={this.state.featuredProjectURL}
|
|
||||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
|
||||||
this.setState({ isWebviewShowing });
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '63.8vw',
|
|
||||||
height: '100vh',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FlashStep
|
|
||||||
goToSuccess={() => this.setState({ current: 'success' })}
|
|
||||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
|
||||||
isFlashing={this.state.isFlashing}
|
|
||||||
step={state.type}
|
|
||||||
percentage={state.percentage}
|
|
||||||
position={state.position}
|
|
||||||
failed={state.failed}
|
|
||||||
speed={state.speed}
|
|
||||||
eta={state.eta}
|
|
||||||
style={{ zIndex: 1 }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSuccess() {
|
|
||||||
return (
|
|
||||||
<Flex flexDirection="column" alignItems="center" height="100%">
|
|
||||||
<FinishPage
|
|
||||||
goToMain={() => {
|
|
||||||
flashState.resetState();
|
|
||||||
this.setState({ current: 'main' });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SafeWebview
|
|
||||||
src="https://www.balena.io/etcher/success-banner/"
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '320px',
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
return (
|
|
||||||
<ThemedProvider style={{ height: '100%', width: '100%' }}>
|
|
||||||
{this.state.current === 'main'
|
{this.state.current === 'main'
|
||||||
? this.renderMain()
|
? this.renderMain()
|
||||||
: this.renderSuccess()}
|
: this.renderSuccess()}
|
||||||
|
10
lib/gui/app/renderer.ts
Normal file
10
lib/gui/app/renderer.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { main } from './app';
|
||||||
|
|
||||||
|
if (module.hot) {
|
||||||
|
module.hot.accept('./app', () => {
|
||||||
|
main();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
@@ -14,6 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as _ from 'lodash';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Alert as AlertBase,
|
Alert as AlertBase,
|
||||||
@@ -23,27 +24,16 @@ import {
|
|||||||
ButtonProps,
|
ButtonProps,
|
||||||
Modal as ModalBase,
|
Modal as ModalBase,
|
||||||
Provider,
|
Provider,
|
||||||
|
Table as BaseTable,
|
||||||
|
TableProps as BaseTableProps,
|
||||||
Txt,
|
Txt,
|
||||||
Theme as renditionTheme,
|
|
||||||
} from 'rendition';
|
} from 'rendition';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
import { colors, theme } from './theme';
|
import { colors, theme } from './theme';
|
||||||
|
|
||||||
const defaultTheme = {
|
|
||||||
...renditionTheme,
|
|
||||||
...theme,
|
|
||||||
layer: {
|
|
||||||
extend: () => `
|
|
||||||
> div:first-child {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ThemedProvider = (props: any) => (
|
export const ThemedProvider = (props: any) => (
|
||||||
<Provider theme={defaultTheme} {...props}></Provider>
|
<Provider theme={theme} {...props}></Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BaseButton = styled(Button)`
|
export const BaseButton = styled(Button)`
|
||||||
@@ -134,46 +124,37 @@ const modalFooterShadowCss = css`
|
|||||||
background-attachment: local, local, scroll, scroll;
|
background-attachment: local, local, scroll, scroll;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Modal = styled(({ style, ...props }) => {
|
export const Modal = styled(({ style, children, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<Provider
|
<ModalBase
|
||||||
theme={{
|
position="top"
|
||||||
...defaultTheme,
|
width="97vw"
|
||||||
header: {
|
cancelButtonProps={{
|
||||||
height: '50px',
|
style: {
|
||||||
},
|
marginRight: '20px',
|
||||||
layer: {
|
border: 'solid 1px #2a506f',
|
||||||
extend: () => `
|
|
||||||
${defaultTheme.layer.extend()}
|
|
||||||
|
|
||||||
> div:last-child {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
style={{
|
||||||
|
height: '87.5vh',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<ModalBase
|
<ScrollableFlex flexDirection="column" width="100%" height="90%">
|
||||||
position="top"
|
{...children}
|
||||||
width="97vw"
|
</ScrollableFlex>
|
||||||
cancelButtonProps={{
|
</ModalBase>
|
||||||
style: {
|
|
||||||
marginRight: '20px',
|
|
||||||
border: 'solid 1px #2a506f',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
height: '87.5vh',
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
);
|
);
|
||||||
})`
|
})`
|
||||||
> div {
|
> div {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100%;
|
height: 99%;
|
||||||
|
|
||||||
|
> div:first-child {
|
||||||
|
height: 81%;
|
||||||
|
padding: 24px 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
> h3 {
|
> h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -188,11 +169,8 @@ export const Modal = styled(({ style, ...props }) => {
|
|||||||
|
|
||||||
> div:nth-child(2) {
|
> div:nth-child(2) {
|
||||||
height: 61%;
|
height: 61%;
|
||||||
|
padding: 0 30px;
|
||||||
> div:not(.system-drive-alert) {
|
${modalFooterShadowCss}
|
||||||
padding: 0 30px;
|
|
||||||
${modalFooterShadowCss}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
> div:last-child {
|
> div:last-child {
|
||||||
@@ -249,3 +227,99 @@ export const Alert = styled((props) => (
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export interface GenericTableProps<T> extends BaseTableProps<T> {
|
||||||
|
refFn: (t: BaseTable<T>) => void;
|
||||||
|
data: T[];
|
||||||
|
checkedRowsNumber?: number;
|
||||||
|
multipleSelection: boolean;
|
||||||
|
showWarnings?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenericTable: <T>(
|
||||||
|
props: GenericTableProps<T>,
|
||||||
|
) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({
|
||||||
|
refFn,
|
||||||
|
...props
|
||||||
|
}: GenericTableProps<T>) => (
|
||||||
|
<div>
|
||||||
|
<BaseTable<T> ref={refFn} {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
function StyledTable<T>() {
|
||||||
|
return styled((props: GenericTableProps<T>) => (
|
||||||
|
<GenericTable<T> {...props} />
|
||||||
|
))`
|
||||||
|
[data-display='table-head']
|
||||||
|
> [data-display='table-row']
|
||||||
|
> [data-display='table-cell'] {
|
||||||
|
position: sticky;
|
||||||
|
background-color: #f8f9fd;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
input[type='checkbox'] + div {
|
||||||
|
display: ${(props) => (props.multipleSelection ? 'flex' : 'none')};
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.multipleSelection &&
|
||||||
|
props.checkedRowsNumber !== 0 &&
|
||||||
|
props.checkedRowsNumber !== props.data.length
|
||||||
|
? `
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${colors.primary.foreground};
|
||||||
|
background: ${colors.primary.background};
|
||||||
|
|
||||||
|
::after {
|
||||||
|
content: '–';
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: ''}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='table-head'] > [data-display='table-row'],
|
||||||
|
[data-display='table-body'] > [data-display='table-row'] {
|
||||||
|
> [data-display='table-cell']:first-child {
|
||||||
|
padding-left: 15px;
|
||||||
|
width: 6%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> [data-display='table-cell']:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='table-body'] > [data-display='table-row'] {
|
||||||
|
&:nth-of-type(2n) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-highlight='true'] {
|
||||||
|
&.system {
|
||||||
|
background-color: ${(props) => (props.showWarnings ? '#fff5e6' : '#e8f5fc')};
|
||||||
|
}
|
||||||
|
|
||||||
|
> [data-display='table-cell']:first-child {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&& [data-display='table-row'] > [data-display='table-cell'] {
|
||||||
|
padding: 6px 8px;
|
||||||
|
color: #2a506f;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] + div {
|
||||||
|
border-radius: ${(props) => (props.multipleSelection ? '4px' : '50%')};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Table = <T extends {}>(props: GenericTableProps<T>) => {
|
||||||
|
const TypedStyledFunctional = StyledTable<T>();
|
||||||
|
return <TypedStyledFunctional {...props} />;
|
||||||
|
};
|
||||||
|
@@ -14,6 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import { Theme } from 'rendition';
|
||||||
|
|
||||||
export const colors = {
|
export const colors = {
|
||||||
dark: {
|
dark: {
|
||||||
foreground: '#fff',
|
foreground: '#fff',
|
||||||
@@ -67,9 +70,12 @@ export const colors = {
|
|||||||
|
|
||||||
const font = 'SourceSansPro';
|
const font = 'SourceSansPro';
|
||||||
|
|
||||||
export const theme = {
|
export const theme = _.merge({}, Theme, {
|
||||||
colors,
|
colors,
|
||||||
font,
|
font,
|
||||||
|
header: {
|
||||||
|
height: '40px',
|
||||||
|
},
|
||||||
global: {
|
global: {
|
||||||
font: {
|
font: {
|
||||||
family: font,
|
family: font,
|
||||||
@@ -94,6 +100,7 @@ export const theme = {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
&& {
|
&& {
|
||||||
|
width: 200px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,4 +116,11 @@ export const theme = {
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
};
|
layer: {
|
||||||
|
extend: () => `
|
||||||
|
> div:first-child {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
18
lib/gui/assets/src.svg
Normal file
18
lib/gui/assets/src.svg
Normal 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 |
@@ -43,7 +43,7 @@ async function checkForUpdates(interval: number) {
|
|||||||
const release = await autoUpdater.checkForUpdates();
|
const release = await autoUpdater.checkForUpdates();
|
||||||
const isOutdated =
|
const isOutdated =
|
||||||
semver.compare(release.updateInfo.version, version) > 0;
|
semver.compare(release.updateInfo.version, version) > 0;
|
||||||
const shouldUpdate = release.updateInfo.stagingPercentage || 0 > 0;
|
const shouldUpdate = release.updateInfo.stagingPercentage !== 0; // undefinded (default) means 100%
|
||||||
if (shouldUpdate && isOutdated) {
|
if (shouldUpdate && isOutdated) {
|
||||||
await autoUpdater.downloadUpdate();
|
await autoUpdater.downloadUpdate();
|
||||||
packageUpdated = true;
|
packageUpdated = true;
|
||||||
@@ -97,6 +97,7 @@ const sourceSelectorReady = new Promise((resolve) => {
|
|||||||
async function selectImageURL(url?: string) {
|
async function selectImageURL(url?: string) {
|
||||||
// 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests
|
// 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests
|
||||||
if (url !== undefined && url !== 'data:,') {
|
if (url !== undefined && url !== 'data:,') {
|
||||||
|
url = url.replace(/\/$/, ''); // on windows the url ends with an extra slash
|
||||||
url = url.startsWith(scheme) ? url.slice(scheme.length) : url;
|
url = url.startsWith(scheme) ? url.slice(scheme.length) : url;
|
||||||
await sourceSelectorReady;
|
await sourceSelectorReady;
|
||||||
electron.BrowserWindow.getAllWindows().forEach((window) => {
|
electron.BrowserWindow.getAllWindows().forEach((window) => {
|
||||||
@@ -122,8 +123,8 @@ interface AutoUpdaterConfig {
|
|||||||
|
|
||||||
async function createMainWindow() {
|
async function createMainWindow() {
|
||||||
const fullscreen = Boolean(await settings.get('fullscreen'));
|
const fullscreen = Boolean(await settings.get('fullscreen'));
|
||||||
const defaultWidth = 800;
|
const defaultWidth = settings.DEFAULT_WIDTH;
|
||||||
const defaultHeight = 480;
|
const defaultHeight = settings.DEFAULT_HEIGHT;
|
||||||
let width = defaultWidth;
|
let width = defaultWidth;
|
||||||
let height = defaultHeight;
|
let height = defaultHeight;
|
||||||
if (fullscreen) {
|
if (fullscreen) {
|
||||||
@@ -133,7 +134,7 @@ async function createMainWindow() {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
frame: !fullscreen,
|
frame: !fullscreen,
|
||||||
useContentSize: false,
|
useContentSize: true,
|
||||||
show: false,
|
show: false,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
maximizable: false,
|
maximizable: false,
|
||||||
@@ -147,6 +148,7 @@ async function createMainWindow() {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
backgroundThrottling: false,
|
backgroundThrottling: false,
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
webviewTag: true,
|
webviewTag: true,
|
||||||
zoomFactor: width / defaultWidth,
|
zoomFactor: width / defaultWidth,
|
||||||
enableRemoteModule: true,
|
enableRemoteModule: true,
|
||||||
|
@@ -15,16 +15,30 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
import { Drive as DrivelistDrive } from 'drivelist';
|
||||||
import * as sdk from 'etcher-sdk';
|
import {
|
||||||
|
BlockDevice,
|
||||||
|
File,
|
||||||
|
Http,
|
||||||
|
Metadata,
|
||||||
|
SourceDestination,
|
||||||
|
} from 'etcher-sdk/build/source-destination';
|
||||||
|
import {
|
||||||
|
MultiDestinationProgress,
|
||||||
|
OnProgressFunction,
|
||||||
|
OnFailFunction,
|
||||||
|
decompressThenFlash,
|
||||||
|
DECOMPRESSED_IMAGE_PREFIX,
|
||||||
|
} from 'etcher-sdk/build/multi-write';
|
||||||
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||||
import * as ipc from 'node-ipc';
|
import * as ipc from 'node-ipc';
|
||||||
import { totalmem } from 'os';
|
import { totalmem } from 'os';
|
||||||
|
|
||||||
import { BlockDevice, File, Http } from 'etcher-sdk/build/source-destination';
|
|
||||||
import { toJSON } from '../../shared/errors';
|
import { toJSON } from '../../shared/errors';
|
||||||
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
||||||
import { delay } from '../../shared/utils';
|
import { delay, isJson } from '../../shared/utils';
|
||||||
import { SourceMetadata } from '../app/components/source-selector/source-selector';
|
import { SourceMetadata } from '../app/components/source-selector/source-selector';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
||||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
||||||
@@ -55,8 +69,9 @@ function log(message: string) {
|
|||||||
/**
|
/**
|
||||||
* @summary Terminate the child writer process
|
* @summary Terminate the child writer process
|
||||||
*/
|
*/
|
||||||
function terminate(exitCode: number) {
|
async function terminate(exitCode: number) {
|
||||||
ipc.disconnect(IPC_SERVER_ID);
|
ipc.disconnect(IPC_SERVER_ID);
|
||||||
|
await cleanupTmpFiles(Date.now(), DECOMPRESSED_IMAGE_PREFIX);
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
process.exit(exitCode || SUCCESS);
|
process.exit(exitCode || SUCCESS);
|
||||||
});
|
});
|
||||||
@@ -68,17 +83,28 @@ function terminate(exitCode: number) {
|
|||||||
async function handleError(error: Error) {
|
async function handleError(error: Error) {
|
||||||
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
||||||
await delay(DISCONNECT_DELAY);
|
await delay(DISCONNECT_DELAY);
|
||||||
terminate(GENERAL_ERROR);
|
await terminate(GENERAL_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WriteResult {
|
export interface FlashError extends Error {
|
||||||
bytesWritten: number;
|
description: string;
|
||||||
devices: {
|
device: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WriteResult {
|
||||||
|
bytesWritten?: number;
|
||||||
|
devices?: {
|
||||||
failed: number;
|
failed: number;
|
||||||
successful: number;
|
successful: number;
|
||||||
};
|
};
|
||||||
errors: Array<Error & { device: string }>;
|
errors: FlashError[];
|
||||||
sourceMetadata: sdk.sourceDestination.Metadata;
|
sourceMetadata?: Metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlashResults extends WriteResult {
|
||||||
|
skip?: boolean;
|
||||||
|
cancelled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,19 +126,15 @@ async function writeAndValidate({
|
|||||||
onProgress,
|
onProgress,
|
||||||
onFail,
|
onFail,
|
||||||
}: {
|
}: {
|
||||||
source: sdk.sourceDestination.SourceDestination;
|
source: SourceDestination;
|
||||||
destinations: sdk.sourceDestination.BlockDevice[];
|
destinations: BlockDevice[];
|
||||||
verify: boolean;
|
verify: boolean;
|
||||||
autoBlockmapping: boolean;
|
autoBlockmapping: boolean;
|
||||||
decompressFirst: boolean;
|
decompressFirst: boolean;
|
||||||
onProgress: sdk.multiWrite.OnProgressFunction;
|
onProgress: OnProgressFunction;
|
||||||
onFail: sdk.multiWrite.OnFailFunction;
|
onFail: OnFailFunction;
|
||||||
}): Promise<WriteResult> {
|
}): Promise<WriteResult> {
|
||||||
const {
|
const { sourceMetadata, failures, bytesWritten } = await decompressThenFlash({
|
||||||
sourceMetadata,
|
|
||||||
failures,
|
|
||||||
bytesWritten,
|
|
||||||
} = await sdk.multiWrite.decompressThenFlash({
|
|
||||||
source,
|
source,
|
||||||
destinations,
|
destinations,
|
||||||
onFail,
|
onFail,
|
||||||
@@ -136,8 +158,10 @@ async function writeAndValidate({
|
|||||||
sourceMetadata,
|
sourceMetadata,
|
||||||
};
|
};
|
||||||
for (const [destination, error] of failures) {
|
for (const [destination, error] of failures) {
|
||||||
const err = error as Error & { device: string };
|
const err = error as FlashError;
|
||||||
err.device = (destination as sdk.sourceDestination.BlockDevice).device;
|
const drive = destination as BlockDevice;
|
||||||
|
err.device = drive.device;
|
||||||
|
err.description = drive.description;
|
||||||
result.errors.push(err);
|
result.errors.push(err);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -146,11 +170,10 @@ async function writeAndValidate({
|
|||||||
interface WriteOptions {
|
interface WriteOptions {
|
||||||
image: SourceMetadata;
|
image: SourceMetadata;
|
||||||
destinations: DrivelistDrive[];
|
destinations: DrivelistDrive[];
|
||||||
unmountOnSuccess: boolean;
|
|
||||||
validateWriteOnSuccess: boolean;
|
|
||||||
autoBlockmapping: boolean;
|
autoBlockmapping: boolean;
|
||||||
decompressFirst: boolean;
|
decompressFirst: boolean;
|
||||||
SourceType: string;
|
SourceType: string;
|
||||||
|
httpRequest?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
ipc.connectTo(IPC_SERVER_ID, () => {
|
ipc.connectTo(IPC_SERVER_ID, () => {
|
||||||
@@ -163,22 +186,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
// no flashing information is available, then it will
|
// no flashing information is available, then it will
|
||||||
// assume that the child died halfway through.
|
// assume that the child died halfway through.
|
||||||
|
|
||||||
process.once('SIGINT', () => {
|
process.once('SIGINT', async () => {
|
||||||
terminate(SUCCESS);
|
await terminate(SUCCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.once('SIGTERM', () => {
|
process.once('SIGTERM', async () => {
|
||||||
terminate(SUCCESS);
|
await terminate(SUCCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The IPC server failed. Abort.
|
// The IPC server failed. Abort.
|
||||||
ipc.of[IPC_SERVER_ID].on('error', () => {
|
ipc.of[IPC_SERVER_ID].on('error', async () => {
|
||||||
terminate(SUCCESS);
|
await terminate(SUCCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The IPC server was disconnected. Abort.
|
// The IPC server was disconnected. Abort.
|
||||||
ipc.of[IPC_SERVER_ID].on('disconnect', () => {
|
ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
|
||||||
terminate(SUCCESS);
|
await terminate(SUCCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
|
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
|
||||||
@@ -188,7 +211,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
* @example
|
* @example
|
||||||
* writer.on('progress', onProgress)
|
* writer.on('progress', onProgress)
|
||||||
*/
|
*/
|
||||||
const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => {
|
const onProgress = (state: MultiDestinationProgress) => {
|
||||||
ipc.of[IPC_SERVER_ID].emit('state', state);
|
ipc.of[IPC_SERVER_ID].emit('state', state);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -203,11 +226,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
log('Abort');
|
log('Abort');
|
||||||
ipc.of[IPC_SERVER_ID].emit('abort');
|
ipc.of[IPC_SERVER_ID].emit('abort');
|
||||||
await delay(DISCONNECT_DELAY);
|
await delay(DISCONNECT_DELAY);
|
||||||
terminate(exitCode);
|
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('cancel', onAbort);
|
||||||
|
|
||||||
|
ipc.of[IPC_SERVER_ID].on('skip', onSkip);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Failure handler (non-fatal errors)
|
* @summary Failure handler (non-fatal errors)
|
||||||
* @param {SourceDestination} destination - destination
|
* @param {SourceDestination} destination - destination
|
||||||
@@ -215,10 +247,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
* @example
|
* @example
|
||||||
* writer.on('fail', onFail)
|
* writer.on('fail', onFail)
|
||||||
*/
|
*/
|
||||||
const onFail = (
|
const onFail = (destination: SourceDestination, error: Error) => {
|
||||||
destination: sdk.sourceDestination.SourceDestination,
|
|
||||||
error: Error,
|
|
||||||
) => {
|
|
||||||
ipc.of[IPC_SERVER_ID].emit('fail', {
|
ipc.of[IPC_SERVER_ID].emit('fail', {
|
||||||
// TODO: device should be destination
|
// TODO: device should be destination
|
||||||
// @ts-ignore (destination.drive is private)
|
// @ts-ignore (destination.drive is private)
|
||||||
@@ -231,14 +260,12 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
const imagePath = options.image.path;
|
const imagePath = options.image.path;
|
||||||
log(`Image: ${imagePath}`);
|
log(`Image: ${imagePath}`);
|
||||||
log(`Devices: ${destinations.join(', ')}`);
|
log(`Devices: ${destinations.join(', ')}`);
|
||||||
log(`Umount on success: ${options.unmountOnSuccess}`);
|
|
||||||
log(`Validate on success: ${options.validateWriteOnSuccess}`);
|
|
||||||
log(`Auto blockmapping: ${options.autoBlockmapping}`);
|
log(`Auto blockmapping: ${options.autoBlockmapping}`);
|
||||||
log(`Decompress first: ${options.decompressFirst}`);
|
log(`Decompress first: ${options.decompressFirst}`);
|
||||||
const dests = options.destinations.map((destination) => {
|
const dests = options.destinations.map((destination) => {
|
||||||
return new sdk.sourceDestination.BlockDevice({
|
return new BlockDevice({
|
||||||
drive: destination,
|
drive: destination,
|
||||||
unmountOnSuccess: options.unmountOnSuccess,
|
unmountOnSuccess: true,
|
||||||
write: true,
|
write: true,
|
||||||
direct: true,
|
direct: true,
|
||||||
});
|
});
|
||||||
@@ -257,13 +284,28 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
path: imagePath,
|
path: imagePath,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
source = new Http({ url: imagePath, avoidRandomAccess: true });
|
const decodedImagePath = decodeURIComponent(imagePath);
|
||||||
|
if (isJson(decodedImagePath)) {
|
||||||
|
const imagePathObject = JSON.parse(decodedImagePath);
|
||||||
|
source = new Http({
|
||||||
|
url: imagePathObject.url,
|
||||||
|
avoidRandomAccess: true,
|
||||||
|
axiosInstance: axios.create(_.omit(imagePathObject, ['url'])),
|
||||||
|
auth: options.image.auth,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
source = new Http({
|
||||||
|
url: imagePath,
|
||||||
|
avoidRandomAccess: true,
|
||||||
|
auth: options.image.auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const results = await writeAndValidate({
|
const results = await writeAndValidate({
|
||||||
source,
|
source,
|
||||||
destinations: dests,
|
destinations: dests,
|
||||||
verify: options.validateWriteOnSuccess,
|
verify: true,
|
||||||
autoBlockmapping: options.autoBlockmapping,
|
autoBlockmapping: options.autoBlockmapping,
|
||||||
decompressFirst: options.decompressFirst,
|
decompressFirst: options.decompressFirst,
|
||||||
onProgress,
|
onProgress,
|
||||||
@@ -275,9 +317,8 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
});
|
});
|
||||||
ipc.of[IPC_SERVER_ID].emit('done', { results });
|
ipc.of[IPC_SERVER_ID].emit('done', { results });
|
||||||
await delay(DISCONNECT_DELAY);
|
await delay(DISCONNECT_DELAY);
|
||||||
terminate(exitCode);
|
await terminate(exitCode);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
log(`Error: ${error.message}`);
|
|
||||||
exitCode = GENERAL_ERROR;
|
exitCode = GENERAL_ERROR;
|
||||||
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
||||||
}
|
}
|
||||||
|
@@ -15,11 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
import { app, remote } from 'electron';
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { env } from 'process';
|
import { env } from 'process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
import { getAppPath } from '../utils';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||||
@@ -37,7 +38,7 @@ export async function sudo(
|
|||||||
env: {
|
env: {
|
||||||
PATH: env.PATH,
|
PATH: env.PATH,
|
||||||
SUDO_ASKPASS: join(
|
SUDO_ASKPASS: join(
|
||||||
(app || remote.app).getAppPath(),
|
getAppPath(),
|
||||||
__dirname,
|
__dirname,
|
||||||
'sudo-askpass.osascript.js',
|
'sudo-askpass.osascript.js',
|
||||||
),
|
),
|
||||||
@@ -49,7 +50,7 @@ export async function sudo(
|
|||||||
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
|
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
|
||||||
stderr,
|
stderr,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
if (error.code === 1) {
|
if (error.code === 1) {
|
||||||
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
|
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
|
||||||
return { cancelled: true };
|
return { cancelled: true };
|
||||||
|
@@ -34,16 +34,6 @@ export type DrivelistDrive = Drive & {
|
|||||||
displayName: string;
|
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
|
* @summary Check if a drive is a system drive
|
||||||
*/
|
*/
|
||||||
@@ -73,9 +63,7 @@ export function isSourceDrive(
|
|||||||
): boolean {
|
): boolean {
|
||||||
if (selection) {
|
if (selection) {
|
||||||
if (selection.drive) {
|
if (selection.drive) {
|
||||||
const sourcePath = selection.drive.devicePath || selection.drive.device;
|
return selection.drive.device === drive.device;
|
||||||
const drivePath = drive.devicePath || drive.device;
|
|
||||||
return pathIsInside(sourcePath, drivePath);
|
|
||||||
}
|
}
|
||||||
if (selection.path) {
|
if (selection.path) {
|
||||||
return sourceIsInsideDrive(selection.path, drive);
|
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)
|
* @summary Check if a drive is valid, i.e. large enough for an image
|
||||||
*/
|
|
||||||
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
|
|
||||||
*/
|
*/
|
||||||
export function isDriveValid(
|
export function isDriveValid(
|
||||||
drive: DrivelistDrive,
|
drive: DrivelistDrive,
|
||||||
image?: SourceMetadata,
|
image?: SourceMetadata,
|
||||||
|
write: boolean = true,
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return (
|
||||||
!isDriveLocked(drive) &&
|
!write ||
|
||||||
isDriveLargeEnough(drive, image) &&
|
(!drive.disabled &&
|
||||||
!isSourceDrive(drive, image as SourceMetadata) &&
|
isDriveLargeEnough(drive, image) &&
|
||||||
!isDriveDisabled(drive)
|
!isSourceDrive(drive, image as SourceMetadata))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,17 +197,19 @@ export const statuses = {
|
|||||||
*/
|
*/
|
||||||
export function getDriveImageCompatibilityStatuses(
|
export function getDriveImageCompatibilityStatuses(
|
||||||
drive: DrivelistDrive,
|
drive: DrivelistDrive,
|
||||||
image?: SourceMetadata,
|
image: SourceMetadata | undefined,
|
||||||
|
write: boolean,
|
||||||
) {
|
) {
|
||||||
const statusList = [];
|
const statusList = [];
|
||||||
|
|
||||||
// Mind the order of the if-statements if you modify.
|
// Mind the order of the if-statements if you modify.
|
||||||
if (isDriveLocked(drive)) {
|
if (drive.isReadOnly && write) {
|
||||||
statusList.push({
|
statusList.push({
|
||||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||||
message: messages.compatibility.locked(),
|
message: messages.compatibility.locked(),
|
||||||
});
|
});
|
||||||
} else if (
|
}
|
||||||
|
if (
|
||||||
!_.isNil(drive) &&
|
!_.isNil(drive) &&
|
||||||
!_.isNil(drive.size) &&
|
!_.isNil(drive.size) &&
|
||||||
!isDriveLargeEnough(drive, image)
|
!isDriveLargeEnough(drive, image)
|
||||||
@@ -264,10 +248,11 @@ export function getDriveImageCompatibilityStatuses(
|
|||||||
*/
|
*/
|
||||||
export function getListDriveImageCompatibilityStatuses(
|
export function getListDriveImageCompatibilityStatuses(
|
||||||
drives: DrivelistDrive[],
|
drives: DrivelistDrive[],
|
||||||
image: SourceMetadata,
|
image: SourceMetadata | undefined,
|
||||||
|
write: boolean,
|
||||||
) {
|
) {
|
||||||
return drives.flatMap((drive) => {
|
return drives.flatMap((drive) => {
|
||||||
return getDriveImageCompatibilityStatuses(drive, image);
|
return getDriveImageCompatibilityStatuses(drive, image, write);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,9 +264,12 @@ export function getListDriveImageCompatibilityStatuses(
|
|||||||
*/
|
*/
|
||||||
export function hasDriveImageCompatibilityStatus(
|
export function hasDriveImageCompatibilityStatus(
|
||||||
drive: DrivelistDrive,
|
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 {
|
export interface DriveStatus {
|
||||||
|
@@ -81,13 +81,10 @@ export const compatibility = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const warning = {
|
export const warning = {
|
||||||
unrecommendedDriveSize: (
|
tooSmall: (source: { size: number }, target: { size: number }) => {
|
||||||
image: { recommendedDriveSize: number },
|
|
||||||
drive: { device: string; size: number },
|
|
||||||
) => {
|
|
||||||
return outdent({ newline: ' ' })`
|
return outdent({ newline: ' ' })`
|
||||||
This image recommends a ${prettyBytes(image.recommendedDriveSize)}
|
The selected source is ${prettyBytes(source.size - target.size)}
|
||||||
drive, however ${drive.device} is only ${prettyBytes(drive.size)}.
|
larger than this drive.
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -117,8 +114,16 @@ export const warning = {
|
|||||||
].join(' ');
|
].join(' ');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
driveMissingPartitionTable: () => {
|
||||||
|
return outdent({ newline: ' ' })`
|
||||||
|
It looks like this is not a bootable drive.
|
||||||
|
The drive does not appear to contain a partition table,
|
||||||
|
and might not be recognized or bootable by your device.
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
largeDriveSize: () => {
|
largeDriveSize: () => {
|
||||||
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
|
return "This is a large drive! Make sure it doesn't contain files that you want to keep.";
|
||||||
},
|
},
|
||||||
|
|
||||||
systemDrive: () => {
|
systemDrive: () => {
|
||||||
|
@@ -15,30 +15,32 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
|
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import * as sudoPrompt from 'sudo-prompt';
|
import * as sudoPrompt from '@balena/sudo-prompt';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
import { withTmpFile } from './tmp';
|
|
||||||
|
|
||||||
const execAsync = promisify(childProcess.exec);
|
const execAsync = promisify(childProcess.exec);
|
||||||
const execFileAsync = promisify(childProcess.execFile);
|
const execFileAsync = promisify(childProcess.execFile);
|
||||||
|
|
||||||
|
type Std = string | Buffer | undefined;
|
||||||
|
|
||||||
function sudoExecAsync(
|
function sudoExecAsync(
|
||||||
cmd: string,
|
cmd: string,
|
||||||
options: { name: string },
|
options: { name: string },
|
||||||
): Promise<{ stdout: string; stderr: string }> {
|
): Promise<{ stdout: Std; stderr: Std }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
sudoPrompt.exec(
|
sudoPrompt.exec(
|
||||||
cmd,
|
cmd,
|
||||||
options,
|
options,
|
||||||
(error: Error | null, stdout: string, stderr: string) => {
|
(error: Error | undefined, stdout: Std, stderr: Std) => {
|
||||||
if (error != null) {
|
if (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else {
|
} else {
|
||||||
resolve({ stdout, stderr });
|
resolve({ stdout, stderr });
|
||||||
@@ -60,7 +62,7 @@ export async function isElevated(): Promise<boolean> {
|
|||||||
// See http://stackoverflow.com/a/28268802
|
// See http://stackoverflow.com/a/28268802
|
||||||
try {
|
try {
|
||||||
await execAsync('fltmc');
|
await execAsync('fltmc');
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
if (error.code === os.constants.errno.EPERM) {
|
if (error.code === os.constants.errno.EPERM) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -146,7 +148,7 @@ async function elevateScriptCatalina(
|
|||||||
try {
|
try {
|
||||||
const { cancelled } = await catalinaSudo(cmd);
|
const { cancelled } = await catalinaSudo(cmd);
|
||||||
return { cancelled };
|
return { cancelled };
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
throw errors.createError({ title: error.stderr });
|
throw errors.createError({ title: error.stderr });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,10 +174,11 @@ export async function elevateCommand(
|
|||||||
);
|
);
|
||||||
return await withTmpFile(
|
return await withTmpFile(
|
||||||
{
|
{
|
||||||
|
keepOpen: false,
|
||||||
prefix: 'balena-etcher-electron-',
|
prefix: 'balena-etcher-electron-',
|
||||||
postfix: '.cmd',
|
postfix: '.cmd',
|
||||||
},
|
},
|
||||||
async (path) => {
|
async ({ path }) => {
|
||||||
await fs.writeFile(path, launchScript);
|
await fs.writeFile(path, launchScript);
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
return elevateScriptWindows(path, options.applicationName);
|
return elevateScriptWindows(path, options.applicationName);
|
||||||
@@ -189,7 +192,7 @@ export async function elevateCommand(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await elevateScriptUnix(path, options.applicationName);
|
return await elevateScriptUnix(path, options.applicationName);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
||||||
// There doesn't seem to be a better way to handle these errors, so
|
// There doesn't seem to be a better way to handle these errors, so
|
||||||
// for now, we should make sure we double check if the error messages
|
// for now, we should make sure we double check if the error messages
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
import * as tmp from 'tmp';
|
|
||||||
|
|
||||||
function tmpFileAsync(
|
|
||||||
options: tmp.FileOptions,
|
|
||||||
): Promise<{ path: string; cleanup: () => void }> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
tmp.file(options, (error, path, _fd, cleanup) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
} else {
|
|
||||||
resolve({ path, cleanup });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function withTmpFile<T>(
|
|
||||||
options: tmp.FileOptions,
|
|
||||||
fn: (path: string) => Promise<T>,
|
|
||||||
): Promise<T> {
|
|
||||||
const { path, cleanup } = await tmpFileAsync(options);
|
|
||||||
try {
|
|
||||||
return await fn(path);
|
|
||||||
} finally {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { app, remote } from 'electron';
|
||||||
import { Dictionary } from 'lodash';
|
import { Dictionary } from 'lodash';
|
||||||
|
|
||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
@@ -47,3 +48,25 @@ export async function delay(duration: number): Promise<void> {
|
|||||||
setTimeout(resolve, duration);
|
setTimeout(resolve, duration);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAppPath(): string {
|
||||||
|
return (
|
||||||
|
(app || remote.app)
|
||||||
|
.getAppPath()
|
||||||
|
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
|
||||||
|
// include the app-x64 or app-arm64 folder depending on the arch.
|
||||||
|
// We don't care about the app.asar file, we want the actual folder.
|
||||||
|
.replace(/\.asar$/, () =>
|
||||||
|
process.platform === 'darwin' ? '-' + process.arch : '',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJson(jsonString: string) {
|
||||||
|
try {
|
||||||
|
JSON.parse(jsonString);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
16722
npm-shrinkwrap.json → package-lock.json
generated
16722
npm-shrinkwrap.json → package-lock.json
generated
File diff suppressed because it is too large
Load Diff
173
package.json
173
package.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "balena-etcher",
|
"name": "balena-etcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "balenaEtcher",
|
"displayName": "balenaEtcher",
|
||||||
"version": "1.5.109",
|
"version": "1.10.28",
|
||||||
"packageType": "local",
|
"packageType": "local",
|
||||||
"main": "generated/etcher.js",
|
"main": "generated/etcher.js",
|
||||||
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
|
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
|
||||||
@@ -13,22 +13,26 @@
|
|||||||
"url": "git@github.com:balena-io/etcher.git"
|
"url": "git@github.com:balena-io/etcher.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
|
"build": "npm run webpack",
|
||||||
|
"flowzone-preinstall-linux": "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": "true",
|
||||||
|
"flowzone-preinstall": "npm run flowzone-preinstall-linux",
|
||||||
"lint-css": "prettier --write lib/**/*.css",
|
"lint-css": "prettier --write lib/**/*.css",
|
||||||
"lint-spell": "codespell --dictionary - --dictionary dictionary.txt --skip *.ttf *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension lib tests docs Makefile *.md LICENSE",
|
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
|
||||||
"lint": "npm run lint-ts && npm run lint-css && npm run lint-spell",
|
"lint": "npm run lint-ts && npm run lint-css",
|
||||||
"test-spectron": "mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts",
|
"postinstall": "electron-rebuild -t prod,dev,optional",
|
||||||
"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",
|
|
||||||
"sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh",
|
"sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh",
|
||||||
"start": "./node_modules/.bin/electron .",
|
"start": "./node_modules/.bin/electron .",
|
||||||
"postshrinkwrap": "ts-node ./scripts/clean-shrinkwrap.ts",
|
"test-macos": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks",
|
||||||
"webpack": "webpack",
|
"test-gui": "electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
|
||||||
"watch": "webpack --watch",
|
"test-linux": "npm run lint && xvfb-run --auto-servernum npm run test-gui && xvfb-run --auto-servernum npm run test-shared && xvfb-run --auto-servernum npm run test-spectron && npm run sanity-checks",
|
||||||
"concourse-build-electron": "npm run webpack",
|
"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",
|
||||||
"concourse-test": "npx npm@6.14.5 test",
|
"test-spectron": "mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts",
|
||||||
"concourse-test-electron": "npx npm@6.14.5 test"
|
"test-windows": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && 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": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
@@ -45,72 +49,81 @@
|
|||||||
},
|
},
|
||||||
"author": "Balena Inc. <hello@etcher.io>",
|
"author": "Balena Inc. <hello@etcher.io>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"platformSpecificDependencies": [
|
|
||||||
"fsevents",
|
|
||||||
"winusb-driver-generator"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@balena/lint": "^5.0.4",
|
"@balena/lint": "5.4.2",
|
||||||
"@fortawesome/fontawesome-free": "^5.13.1",
|
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
|
||||||
"@svgr/webpack": "^5.4.0",
|
"@fortawesome/fontawesome-free": "5.15.4",
|
||||||
"@types/chai": "^4.2.7",
|
"@svgr/webpack": "5.5.0",
|
||||||
"@types/copy-webpack-plugin": "^6.0.0",
|
"@types/chai": "4.3.4",
|
||||||
"@types/mime-types": "^2.1.0",
|
"@types/copy-webpack-plugin": "6.4.3",
|
||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mime-types": "2.1.1",
|
||||||
"@types/mocha": "^8.0.3",
|
"@types/mini-css-extract-plugin": "1.4.3",
|
||||||
"@types/node": "^12.12.39",
|
"@types/mocha": "8.2.3",
|
||||||
"@types/node-ipc": "^9.1.2",
|
"@types/node": "14.18.34",
|
||||||
"@types/react-dom": "^16.8.4",
|
"@types/node-ipc": "9.2.0",
|
||||||
"@types/semver": "^7.1.0",
|
"@types/react": "16.14.34",
|
||||||
"@types/sinon": "^9.0.0",
|
"@types/react-dom": "16.9.17",
|
||||||
"@types/terser-webpack-plugin": "^4.1.0",
|
"@types/semver": "7.3.13",
|
||||||
"@types/tmp": "^0.2.0",
|
"@types/sinon": "9.0.0",
|
||||||
"@types/webpack-node-externals": "^2.5.0",
|
"@types/terser-webpack-plugin": "5.0.2",
|
||||||
"chai": "^4.2.0",
|
"@types/tmp": "0.2.3",
|
||||||
"copy-webpack-plugin": "^6.0.1",
|
"@types/webpack-node-externals": "2.5.3",
|
||||||
"css-loader": "^4.2.1",
|
"aws4-axios": "2.4.9",
|
||||||
"d3": "^4.13.0",
|
"chai": "4.3.7",
|
||||||
"debug": "^4.2.0",
|
"copy-webpack-plugin": "7.0.0",
|
||||||
"electron": "9.2.1",
|
"css-loader": "5.2.7",
|
||||||
"electron-builder": "^22.7.0",
|
"d3": "4.13.0",
|
||||||
"electron-mocha": "^9.1.0",
|
"debug": "4.3.4",
|
||||||
"electron-notarize": "^1.0.0",
|
"electron": "12.2.3",
|
||||||
"electron-rebuild": "^1.11.0",
|
"electron-builder": "22.14.13",
|
||||||
"electron-updater": "^4.3.2",
|
"electron-mocha": "9.3.3",
|
||||||
"etcher-sdk": "^4.1.30",
|
"electron-notarize": "1.2.2",
|
||||||
"file-loader": "^6.0.0",
|
"electron-rebuild": "3.2.9",
|
||||||
"husky": "^4.2.5",
|
"electron-updater": "4.6.5",
|
||||||
"immutable": "^3.8.1",
|
"esbuild-loader": "2.20.0",
|
||||||
"lint-staged": "^10.2.2",
|
"etcher-sdk": "7.4.0",
|
||||||
"lodash": "^4.17.10",
|
"file-loader": "6.2.0",
|
||||||
"mini-css-extract-plugin": "^0.10.0",
|
"husky": "4.3.8",
|
||||||
"mocha": "^8.0.1",
|
"immutable": "3.8.2",
|
||||||
"native-addon-loader": "^2.0.1",
|
"lint-staged": "10.5.4",
|
||||||
"node-ipc": "^9.1.1",
|
"lodash": "4.17.21",
|
||||||
"omit-deep-lodash": "1.1.4",
|
"mini-css-extract-plugin": "1.6.2",
|
||||||
"outdent": "^0.7.1",
|
"mocha": "8.4.0",
|
||||||
"path-is-inside": "^1.0.2",
|
"native-addon-loader": "2.0.1",
|
||||||
"pretty-bytes": "^5.3.0",
|
"node-ipc": "9.2.1",
|
||||||
"react": "^16.8.5",
|
"omit-deep-lodash": "1.1.7",
|
||||||
"react-dom": "^16.8.5",
|
"outdent": "0.8.0",
|
||||||
"redux": "^4.0.5",
|
"path-is-inside": "1.0.2",
|
||||||
"rendition": "^18.4.1",
|
"pnp-webpack-plugin": "1.7.0",
|
||||||
"resin-corvus": "^2.0.5",
|
"pretty-bytes": "5.6.0",
|
||||||
"semver": "^7.3.2",
|
"react": "16.8.5",
|
||||||
"simple-progress-webpack-plugin": "^1.1.2",
|
"react-dom": "16.8.5",
|
||||||
"sinon": "^9.0.2",
|
"redux": "4.2.0",
|
||||||
"spectron": "^11.0.0",
|
"rendition": "19.3.2",
|
||||||
"string-replace-loader": "^2.3.0",
|
"resin-corvus": "2.0.5",
|
||||||
"styled-components": "^5.1.0",
|
"semver": "7.3.8",
|
||||||
"sudo-prompt": "github:zvin/sudo-prompt#workaround-windows-amperstand-in-username",
|
"simple-progress-webpack-plugin": "1.1.2",
|
||||||
"sys-class-rgb-led": "^2.1.0",
|
"sinon": "9.0.2",
|
||||||
"tmp": "^0.2.1",
|
"spectron": "14.0.0",
|
||||||
"ts-loader": "^8.0.0",
|
"string-replace-loader": "3.0.1",
|
||||||
"ts-node": "^9.0.0",
|
"style-loader": "2.0.0",
|
||||||
"tslib": "^2.0.0",
|
"styled-components": "5.1.0",
|
||||||
"typescript": "^4.0.2",
|
"sys-class-rgb-led": "3.0.1",
|
||||||
"uuid": "^8.1.0",
|
"terser-webpack-plugin": "5.2.5",
|
||||||
"webpack": "^4.40.2",
|
"ts-loader": "8.0.12",
|
||||||
"webpack-cli": "^3.3.9"
|
"ts-node": "9.1.1",
|
||||||
|
"tslib": "2.0.0",
|
||||||
|
"typescript": "4.4.4",
|
||||||
|
"url-loader": "4.1.1",
|
||||||
|
"uuid": "8.1.0",
|
||||||
|
"webpack": "5.11.0",
|
||||||
|
"webpack-cli": "4.2.0",
|
||||||
|
"webpack-dev-server": "4.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14 < 16"
|
||||||
|
},
|
||||||
|
"versionist": {
|
||||||
|
"publishedAt": "2022-12-10T02:10:43.342Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
22
repo.yml
22
repo.yml
@@ -1,11 +1,21 @@
|
|||||||
|
---
|
||||||
type: electron
|
type: electron
|
||||||
release: github
|
release: github
|
||||||
publishMetadata: true
|
publishMetadata: true
|
||||||
sentry:
|
sentry:
|
||||||
org: balenaetcher
|
org: balenaetcher
|
||||||
team: resinio
|
team: resinio
|
||||||
type: electron
|
type: electron
|
||||||
triggerNotification:
|
triggerNotification:
|
||||||
version: 1.5.81
|
version: 1.7.9
|
||||||
stagingPercentage: 100
|
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
|
||||||
|
@@ -1,3 +1,2 @@
|
|||||||
codespell==1.12.0
|
awscli==1.27.26
|
||||||
awscli==1.11.87
|
|
||||||
shyaml==0.5.0
|
shyaml==0.5.0
|
||||||
|
@@ -29,11 +29,15 @@ const SHRINKWRAP_FILENAME = path.join(__dirname, '..', 'npm-shrinkwrap.json');
|
|||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies);
|
const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies);
|
||||||
|
for (const item of Object.values(cleaned.dependencies)) {
|
||||||
|
// @ts-ignore
|
||||||
|
item.dev = true;
|
||||||
|
}
|
||||||
await writeFileAsync(
|
await writeFileAsync(
|
||||||
SHRINKWRAP_FILENAME,
|
SHRINKWRAP_FILENAME,
|
||||||
JSON.stringify(cleaned, null, JSON_INDENT),
|
JSON.stringify(cleaned, null, JSON_INDENT),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`);
|
console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
|
Submodule scripts/resin updated: 02c8c7ca1f...8dfa21cfc2
BIN
secrets/APPLE_SIGNING.p12.secret
Normal file
BIN
secrets/APPLE_SIGNING.p12.secret
Normal file
Binary file not shown.
BIN
secrets/APPLE_SIGNING_PASSWORD.txt.secret
Normal file
BIN
secrets/APPLE_SIGNING_PASSWORD.txt.secret
Normal file
Binary file not shown.
BIN
secrets/WINDOWS_SIGNING.pfx.secret
Normal file
BIN
secrets/WINDOWS_SIGNING.pfx.secret
Normal file
Binary file not shown.
BIN
secrets/WINDOWS_SIGNING_PASSWORD.txt.secret
Normal file
BIN
secrets/WINDOWS_SIGNING_PASSWORD.txt.secret
Normal file
Binary file not shown.
BIN
secrets/XCODE_APP_LOADER_PASSWORD.txt.secret
Normal file
BIN
secrets/XCODE_APP_LOADER_PASSWORD.txt.secret
Normal file
Binary file not shown.
@@ -393,6 +393,7 @@ describe('Model: flashState', function () {
|
|||||||
|
|
||||||
expect(flashResults).to.deep.equal({
|
expect(flashResults).to.deep.equal({
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
|
skip: false,
|
||||||
sourceChecksum: '1234',
|
sourceChecksum: '1234',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -572,7 +573,8 @@ describe('Model: flashState', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('.getFlashUuid()', function () {
|
describe('.getFlashUuid()', function () {
|
||||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
const UUID_REGEX =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||||
|
|
||||||
it('should be initially undefined', function () {
|
it('should be initially undefined', function () {
|
||||||
expect(flashState.getFlashUuid()).to.be.undefined;
|
expect(flashState.getFlashUuid()).to.be.undefined;
|
||||||
|
@@ -33,26 +33,6 @@ describe('Model: selectionState', function () {
|
|||||||
expect(selectionState.getImage()).to.be.undefined;
|
expect(selectionState.getImage()).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getImagePath() should return undefined', function () {
|
|
||||||
expect(selectionState.getImagePath()).to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getImageSize() should return undefined', function () {
|
|
||||||
expect(selectionState.getImageSize()).to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getImageName() should return undefined', function () {
|
|
||||||
expect(selectionState.getImageName()).to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getImageLogo() should return undefined', function () {
|
|
||||||
expect(selectionState.getImageLogo()).to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getImageSupportUrl() should return undefined', function () {
|
|
||||||
expect(selectionState.getImageSupportUrl()).to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hasDrive() should return false', function () {
|
it('hasDrive() should return false', function () {
|
||||||
const hasDrive = selectionState.hasDrive();
|
const hasDrive = selectionState.hasDrive();
|
||||||
expect(hasDrive).to.be.false;
|
expect(hasDrive).to.be.false;
|
||||||
@@ -379,43 +359,6 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.getImagePath()', function () {
|
|
||||||
it('should return the image path', function () {
|
|
||||||
const imagePath = selectionState.getImagePath();
|
|
||||||
expect(imagePath).to.equal('foo.img');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.getImageSize()', function () {
|
|
||||||
it('should return the image size', function () {
|
|
||||||
const imageSize = selectionState.getImageSize();
|
|
||||||
expect(imageSize).to.equal(999999999);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.getImageName()', function () {
|
|
||||||
it('should return the image name', function () {
|
|
||||||
const imageName = selectionState.getImageName();
|
|
||||||
expect(imageName).to.equal('Raspbian');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.getImageLogo()', function () {
|
|
||||||
it('should return the image logo', function () {
|
|
||||||
const imageLogo = selectionState.getImageLogo();
|
|
||||||
expect(imageLogo).to.equal(
|
|
||||||
'<svg><text fill="red">Raspbian</text></svg>',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.getImageSupportUrl()', function () {
|
|
||||||
it('should return the image support url', function () {
|
|
||||||
const imageSupportUrl = selectionState.getImageSupportUrl();
|
|
||||||
expect(imageSupportUrl).to.equal('https://www.raspbian.org/forums/');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.hasImage()', function () {
|
describe('.hasImage()', function () {
|
||||||
it('should return true', function () {
|
it('should return true', function () {
|
||||||
const hasImage = selectionState.hasImage();
|
const hasImage = selectionState.hasImage();
|
||||||
@@ -435,9 +378,9 @@ describe('Model: selectionState', function () {
|
|||||||
SourceType: File,
|
SourceType: File,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImage()?.path;
|
||||||
expect(imagePath).to.equal('bar.img');
|
expect(imagePath).to.equal('bar.img');
|
||||||
const imageSize = selectionState.getImageSize();
|
const imageSize = selectionState.getImage()?.size;
|
||||||
expect(imageSize).to.equal(999999999);
|
expect(imageSize).to.equal(999999999);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -446,9 +389,9 @@ describe('Model: selectionState', function () {
|
|||||||
it('should clear the image', function () {
|
it('should clear the image', function () {
|
||||||
selectionState.deselectImage();
|
selectionState.deselectImage();
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImage()?.path;
|
||||||
expect(imagePath).to.be.undefined;
|
expect(imagePath).to.be.undefined;
|
||||||
const imageSize = selectionState.getImageSize();
|
const imageSize = selectionState.getImage()?.size;
|
||||||
expect(imageSize).to.be.undefined;
|
expect(imageSize).to.be.undefined;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -472,9 +415,9 @@ describe('Model: selectionState', function () {
|
|||||||
it('should be able to set an image', function () {
|
it('should be able to set an image', function () {
|
||||||
selectionState.selectSource(image);
|
selectionState.selectSource(image);
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImage()?.path;
|
||||||
expect(imagePath).to.equal('foo.img');
|
expect(imagePath).to.equal('foo.img');
|
||||||
const imageSize = selectionState.getImageSize();
|
const imageSize = selectionState.getImage()?.size;
|
||||||
expect(imageSize).to.equal(999999999);
|
expect(imageSize).to.equal(999999999);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -485,7 +428,7 @@ describe('Model: selectionState', function () {
|
|||||||
archiveExtension: 'zip',
|
archiveExtension: 'zip',
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImage()?.path;
|
||||||
expect(imagePath).to.equal('foo.zip');
|
expect(imagePath).to.equal('foo.zip');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -496,7 +439,7 @@ describe('Model: selectionState', function () {
|
|||||||
archiveExtension: 'xz',
|
archiveExtension: 'xz',
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImage()?.path;
|
||||||
expect(imagePath).to.equal('foo.xz');
|
expect(imagePath).to.equal('foo.xz');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -507,7 +450,7 @@ describe('Model: selectionState', function () {
|
|||||||
archiveExtension: 'gz',
|
archiveExtension: 'gz',
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImage()?.path;
|
||||||
expect(imagePath).to.equal('something.linux-x86-64.gz');
|
expect(imagePath).to.equal('something.linux-x86-64.gz');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -675,12 +618,12 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('getImagePath() should return undefined', function () {
|
it('getImagePath() should return undefined', function () {
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImage()?.path;
|
||||||
expect(imagePath).to.be.undefined;
|
expect(imagePath).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getImageSize() should return undefined', function () {
|
it('getImageSize() should return undefined', function () {
|
||||||
const imageSize = selectionState.getImageSize();
|
const imageSize = selectionState.getImage()?.size;
|
||||||
expect(imageSize).to.be.undefined;
|
expect(imageSize).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -700,12 +643,12 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('getImagePath() should return the image path', function () {
|
it('getImagePath() should return the image path', function () {
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImage()?.path;
|
||||||
expect(imagePath).to.equal('foo.img');
|
expect(imagePath).to.equal('foo.img');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getImageSize() should return the image size', function () {
|
it('getImageSize() should return the image size', function () {
|
||||||
const imageSize = selectionState.getImageSize();
|
const imageSize = selectionState.getImage()?.size;
|
||||||
expect(imageSize).to.equal(999999999);
|
expect(imageSize).to.equal(999999999);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -23,7 +23,7 @@ import * as settings from '../../../lib/gui/app/models/settings';
|
|||||||
async function checkError(promise: Promise<any>, fn: (err: Error) => any) {
|
async function checkError(promise: Promise<any>, fn: (err: Error) => any) {
|
||||||
try {
|
try {
|
||||||
await promise;
|
await promise;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
await fn(error);
|
await fn(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -83,7 +83,7 @@ describe('Browser: imageWriter', () => {
|
|||||||
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||||
]);
|
]);
|
||||||
assert.fail('Writing twice should fail');
|
assert.fail('Writing twice should fail');
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
expect(error.message).to.equal(
|
expect(error.message).to.equal(
|
||||||
'There is already a flash in progress',
|
'There is already a flash in progress',
|
||||||
);
|
);
|
||||||
@@ -133,7 +133,7 @@ describe('Browser: imageWriter', () => {
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
expect(error).to.be.an.instanceof(Error);
|
expect(error).to.be.an.instanceof(Error);
|
||||||
expect(error.message).to.equal('write error');
|
expect(error.message).to.equal('write error');
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
|
||||||
import * as settings from '../../../lib/gui/app/models/settings';
|
|
||||||
import * as progressStatus from '../../../lib/gui/app/modules/progress-status';
|
import * as progressStatus from '../../../lib/gui/app/modules/progress-status';
|
||||||
|
|
||||||
describe('Browser: progressStatus', function () {
|
describe('Browser: progressStatus', function () {
|
||||||
@@ -30,9 +29,6 @@ describe('Browser: progressStatus', function () {
|
|||||||
eta: 15,
|
eta: 15,
|
||||||
speed: 100000000000000,
|
speed: 100000000000000,
|
||||||
};
|
};
|
||||||
|
|
||||||
settings.set('unmountOnSuccess', true);
|
|
||||||
settings.set('validateWriteOnSuccess', true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should report 0% if percentage == 0 but speed != 0', function () {
|
it('should report 0% if percentage == 0 but speed != 0', function () {
|
||||||
@@ -41,22 +37,14 @@ describe('Browser: progressStatus', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle percentage == 0, flashing, unmountOnSuccess', function () {
|
it('should handle percentage == 0, flashing', function () {
|
||||||
this.state.speed = 0;
|
this.state.speed = 0;
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||||
'0% Flashing...',
|
'0% Flashing...',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle percentage == 0, flashing, !unmountOnSuccess', function () {
|
it('should handle percentage == 0, verifying', function () {
|
||||||
this.state.speed = 0;
|
|
||||||
settings.set('unmountOnSuccess', false);
|
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
|
||||||
'0% Flashing...',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle percentage == 0, verifying, unmountOnSuccess', function () {
|
|
||||||
this.state.speed = 0;
|
this.state.speed = 0;
|
||||||
this.state.type = 'verifying';
|
this.state.type = 'verifying';
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||||
@@ -64,31 +52,14 @@ describe('Browser: progressStatus', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle percentage == 0, verifying, !unmountOnSuccess', function () {
|
it('should handle percentage == 50, flashing', function () {
|
||||||
this.state.speed = 0;
|
|
||||||
this.state.type = 'verifying';
|
|
||||||
settings.set('unmountOnSuccess', false);
|
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
|
||||||
'0% Validating...',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle percentage == 50, flashing, unmountOnSuccess', function () {
|
|
||||||
this.state.percentage = 50;
|
this.state.percentage = 50;
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||||
'50% Flashing...',
|
'50% Flashing...',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle percentage == 50, flashing, !unmountOnSuccess', function () {
|
it('should handle percentage == 50, verifying', function () {
|
||||||
this.state.percentage = 50;
|
|
||||||
settings.set('unmountOnSuccess', false);
|
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
|
||||||
'50% Flashing...',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle percentage == 50, verifying, unmountOnSuccess', function () {
|
|
||||||
this.state.percentage = 50;
|
this.state.percentage = 50;
|
||||||
this.state.type = 'verifying';
|
this.state.type = 'verifying';
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||||
@@ -96,40 +67,14 @@ describe('Browser: progressStatus', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle percentage == 50, verifying, !unmountOnSuccess', function () {
|
it('should handle percentage == 100, flashing', function () {
|
||||||
this.state.percentage = 50;
|
|
||||||
this.state.type = 'verifying';
|
|
||||||
settings.set('unmountOnSuccess', false);
|
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
|
||||||
'50% Validating...',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle percentage == 100, flashing, unmountOnSuccess, validateWriteOnSuccess', function () {
|
|
||||||
this.state.percentage = 100;
|
this.state.percentage = 100;
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||||
'Finishing...',
|
'Finishing...',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle percentage == 100, flashing, unmountOnSuccess, !validateWriteOnSuccess', function () {
|
it('should handle percentage == 100, verifying', function () {
|
||||||
this.state.percentage = 100;
|
|
||||||
settings.set('validateWriteOnSuccess', false);
|
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
|
||||||
'Finishing...',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle percentage == 100, flashing, !unmountOnSuccess, !validateWriteOnSuccess', function () {
|
|
||||||
this.state.percentage = 100;
|
|
||||||
settings.set('unmountOnSuccess', false);
|
|
||||||
settings.set('validateWriteOnSuccess', false);
|
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
|
||||||
'Finishing...',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle percentage == 100, verifying, unmountOnSuccess', function () {
|
|
||||||
this.state.percentage = 100;
|
this.state.percentage = 100;
|
||||||
this.state.type = 'verifying';
|
this.state.type = 'verifying';
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||||
@@ -137,9 +82,8 @@ describe('Browser: progressStatus', function () {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () {
|
it('should handle percentage == 100, validating', function () {
|
||||||
this.state.percentage = 100;
|
this.state.percentage = 100;
|
||||||
settings.set('unmountOnSuccess', false);
|
|
||||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||||
'Finishing...',
|
'Finishing...',
|
||||||
);
|
);
|
||||||
|
@@ -23,37 +23,6 @@ import * as constraints from '../../lib/shared/drive-constraints';
|
|||||||
import * as messages from '../../lib/shared/messages';
|
import * as messages from '../../lib/shared/messages';
|
||||||
|
|
||||||
describe('Shared: DriveConstraints', function () {
|
describe('Shared: DriveConstraints', function () {
|
||||||
describe('.isDriveLocked()', function () {
|
|
||||||
it('should return true if the drive is read-only', function () {
|
|
||||||
const result = constraints.isDriveLocked({
|
|
||||||
device: '/dev/disk2',
|
|
||||||
size: 999999999,
|
|
||||||
isReadOnly: true,
|
|
||||||
} as constraints.DrivelistDrive);
|
|
||||||
|
|
||||||
expect(result).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if the drive is not read-only', function () {
|
|
||||||
const result = constraints.isDriveLocked({
|
|
||||||
device: '/dev/disk2',
|
|
||||||
size: 999999999,
|
|
||||||
isReadOnly: false,
|
|
||||||
} as constraints.DrivelistDrive);
|
|
||||||
|
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return false if we don't know if the drive is read-only", function () {
|
|
||||||
const result = constraints.isDriveLocked({
|
|
||||||
device: '/dev/disk2',
|
|
||||||
size: 999999999,
|
|
||||||
} as constraints.DrivelistDrive);
|
|
||||||
|
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.isSystemDrive()', function () {
|
describe('.isSystemDrive()', function () {
|
||||||
it('should return true if the drive is a system drive', function () {
|
it('should return true if the drive is a system drive', function () {
|
||||||
const result = constraints.isSystemDrive({
|
const result = constraints.isSystemDrive({
|
||||||
@@ -545,40 +514,6 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.isDriveDisabled()', function () {
|
|
||||||
it('should return true if the drive is disabled', function () {
|
|
||||||
const result = constraints.isDriveDisabled(({
|
|
||||||
device: '/dev/disk1',
|
|
||||||
size: 1000000000,
|
|
||||||
isReadOnly: false,
|
|
||||||
disabled: true,
|
|
||||||
} as unknown) as constraints.DrivelistDrive);
|
|
||||||
|
|
||||||
expect(result).to.be.true;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if the drive is not disabled', function () {
|
|
||||||
const result = constraints.isDriveDisabled(({
|
|
||||||
device: '/dev/disk1',
|
|
||||||
size: 1000000000,
|
|
||||||
isReadOnly: false,
|
|
||||||
disabled: false,
|
|
||||||
} as unknown) as constraints.DrivelistDrive);
|
|
||||||
|
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if "disabled" is undefined', function () {
|
|
||||||
const result = constraints.isDriveDisabled({
|
|
||||||
device: '/dev/disk1',
|
|
||||||
size: 1000000000,
|
|
||||||
isReadOnly: false,
|
|
||||||
} as constraints.DrivelistDrive);
|
|
||||||
|
|
||||||
expect(result).to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.isDriveSizeRecommended()', function () {
|
describe('.isDriveSizeRecommended()', function () {
|
||||||
const image: SourceMetadata = {
|
const image: SourceMetadata = {
|
||||||
description: 'rpi.img',
|
description: 'rpi.img',
|
||||||
@@ -700,11 +635,6 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||||
console.log('YAYYY', {
|
|
||||||
...image,
|
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
|
||||||
size: 5000000000,
|
|
||||||
});
|
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
...image,
|
...image,
|
||||||
@@ -750,7 +680,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
this.drive.disabled = false;
|
this.drive.disabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
it('should return false if the drive is not large enough and is the source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
...image,
|
...image,
|
||||||
@@ -760,7 +690,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
it('should return false if the drive is not large enough and is not the source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
...image,
|
...image,
|
||||||
@@ -770,17 +700,17 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is large enough and is a source drive', function () {
|
it('should return true if the drive is large enough and is the source drive', function () {
|
||||||
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
expect(constraints.isDriveValid(this.drive, image)).to.be.true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
it('should return true if the drive is large enough and is not the source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
...image,
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -988,6 +918,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).to.deep.equal([]);
|
expect(result).to.deep.equal([]);
|
||||||
@@ -1000,6 +931,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedTuples: Array<['WARNING' | 'ERROR', string]> = [];
|
const expectedTuples: Array<['WARNING' | 'ERROR', string]> = [];
|
||||||
@@ -1014,6 +946,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const expectedTuples = [['ERROR', 'containsImage']];
|
const expectedTuples = [['ERROR', 'containsImage']];
|
||||||
@@ -1030,6 +963,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const expectedTuples = [['WARNING', 'system']];
|
const expectedTuples = [['WARNING', 'system']];
|
||||||
|
|
||||||
@@ -1045,6 +979,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{
|
||||||
@@ -1065,6 +1000,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const expectedTuples = [];
|
const expectedTuples = [];
|
||||||
@@ -1081,6 +1017,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const expectedTuples = [['ERROR', 'locked']];
|
const expectedTuples = [['ERROR', 'locked']];
|
||||||
@@ -1097,6 +1034,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const expectedTuples = [['WARNING', 'sizeNotRecommended']];
|
const expectedTuples = [['WARNING', 'sizeNotRecommended']];
|
||||||
@@ -1113,6 +1051,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const expectedTuples = [['WARNING', 'largeDrive']];
|
const expectedTuples = [['WARNING', 'largeDrive']];
|
||||||
|
|
||||||
@@ -1133,9 +1072,13 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const expectedTuples = [['ERROR', 'locked']];
|
const expectedTuples = [
|
||||||
|
['ERROR', 'locked'],
|
||||||
|
['ERROR', 'containsImage'],
|
||||||
|
];
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expectStatusTypesAndMessagesToBe(result, expectedTuples);
|
expectStatusTypesAndMessagesToBe(result, expectedTuples);
|
||||||
@@ -1149,6 +1092,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const expectedTuples = [['ERROR', 'locked']];
|
const expectedTuples = [['ERROR', 'locked']];
|
||||||
@@ -1166,6 +1110,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{
|
||||||
@@ -1186,6 +1131,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.getDriveImageCompatibilityStatuses(
|
const result = constraints.getDriveImageCompatibilityStatuses(
|
||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const expectedTuples = [
|
const expectedTuples = [
|
||||||
@@ -1212,7 +1158,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
'/dev/disk6',
|
'/dev/disk6',
|
||||||
];
|
];
|
||||||
const drives = [
|
const drives = [
|
||||||
({
|
{
|
||||||
device: drivePaths[0],
|
device: drivePaths[0],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
size: 123456789,
|
size: 123456789,
|
||||||
@@ -1220,8 +1166,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [{ path: __dirname }],
|
mountpoints: [{ path: __dirname }],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as constraints.DrivelistDrive,
|
} as unknown as constraints.DrivelistDrive,
|
||||||
({
|
{
|
||||||
device: drivePaths[1],
|
device: drivePaths[1],
|
||||||
description: 'My Other Drive',
|
description: 'My Other Drive',
|
||||||
size: 123456789,
|
size: 123456789,
|
||||||
@@ -1229,8 +1175,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
} as unknown) as constraints.DrivelistDrive,
|
} as unknown as constraints.DrivelistDrive,
|
||||||
({
|
{
|
||||||
device: drivePaths[2],
|
device: drivePaths[2],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
size: 1234567,
|
size: 1234567,
|
||||||
@@ -1238,8 +1184,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as constraints.DrivelistDrive,
|
} as unknown as constraints.DrivelistDrive,
|
||||||
({
|
{
|
||||||
device: drivePaths[3],
|
device: drivePaths[3],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
size: 123456789,
|
size: 123456789,
|
||||||
@@ -1247,8 +1193,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as constraints.DrivelistDrive,
|
} as unknown as constraints.DrivelistDrive,
|
||||||
({
|
{
|
||||||
device: drivePaths[4],
|
device: drivePaths[4],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
size: 128000000001,
|
size: 128000000001,
|
||||||
@@ -1256,8 +1202,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as constraints.DrivelistDrive,
|
} as unknown as constraints.DrivelistDrive,
|
||||||
({
|
{
|
||||||
device: drivePaths[5],
|
device: drivePaths[5],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
size: 12345678,
|
size: 12345678,
|
||||||
@@ -1265,8 +1211,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as constraints.DrivelistDrive,
|
} as unknown as constraints.DrivelistDrive,
|
||||||
({
|
{
|
||||||
device: drivePaths[6],
|
device: drivePaths[6],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
size: 123456789,
|
size: 123456789,
|
||||||
@@ -1274,7 +1220,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as constraints.DrivelistDrive,
|
} as unknown as constraints.DrivelistDrive,
|
||||||
];
|
];
|
||||||
|
|
||||||
const image: SourceMetadata = {
|
const image: SourceMetadata = {
|
||||||
@@ -1292,7 +1238,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
describe('given no drives', function () {
|
describe('given no drives', function () {
|
||||||
it('should return no statuses', function () {
|
it('should return no statuses', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.getListDriveImageCompatibilityStatuses([], image),
|
constraints.getListDriveImageCompatibilityStatuses([], image, true),
|
||||||
).to.deep.equal([]);
|
).to.deep.equal([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1303,6 +1249,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
constraints.getListDriveImageCompatibilityStatuses(
|
constraints.getListDriveImageCompatibilityStatuses(
|
||||||
[drives[0]],
|
[drives[0]],
|
||||||
image,
|
image,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
@@ -1317,6 +1264,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
constraints.getListDriveImageCompatibilityStatuses(
|
constraints.getListDriveImageCompatibilityStatuses(
|
||||||
[drives[1]],
|
[drives[1]],
|
||||||
image,
|
image,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
@@ -1331,6 +1279,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
constraints.getListDriveImageCompatibilityStatuses(
|
constraints.getListDriveImageCompatibilityStatuses(
|
||||||
[drives[2]],
|
[drives[2]],
|
||||||
image,
|
image,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
@@ -1345,6 +1294,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
constraints.getListDriveImageCompatibilityStatuses(
|
constraints.getListDriveImageCompatibilityStatuses(
|
||||||
[drives[3]],
|
[drives[3]],
|
||||||
image,
|
image,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
@@ -1359,6 +1309,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
constraints.getListDriveImageCompatibilityStatuses(
|
constraints.getListDriveImageCompatibilityStatuses(
|
||||||
[drives[4]],
|
[drives[4]],
|
||||||
image,
|
image,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
@@ -1373,6 +1324,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
constraints.getListDriveImageCompatibilityStatuses(
|
constraints.getListDriveImageCompatibilityStatuses(
|
||||||
[drives[5]],
|
[drives[5]],
|
||||||
image,
|
image,
|
||||||
|
true,
|
||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
@@ -1386,7 +1338,11 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
describe('given multiple drives with all warnings/errors', function () {
|
describe('given multiple drives with all warnings/errors', function () {
|
||||||
it('should return all statuses', function () {
|
it('should return all statuses', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.getListDriveImageCompatibilityStatuses(drives, image),
|
constraints.getListDriveImageCompatibilityStatuses(
|
||||||
|
drives,
|
||||||
|
image,
|
||||||
|
true,
|
||||||
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
message: 'Source drive',
|
message: 'Source drive',
|
||||||
|
@@ -30,9 +30,8 @@ describe('Shared: SupportedFormats', function () {
|
|||||||
],
|
],
|
||||||
(imagePath) => {
|
(imagePath) => {
|
||||||
it(`should return true if filename is ${imagePath}`, function () {
|
it(`should return true if filename is ${imagePath}`, function () {
|
||||||
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(
|
const looksLikeWindowsImage =
|
||||||
imagePath,
|
supportedFormats.looksLikeWindowsImage(imagePath);
|
||||||
);
|
|
||||||
expect(looksLikeWindowsImage).to.be.true;
|
expect(looksLikeWindowsImage).to.be.true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -45,9 +44,8 @@ describe('Shared: SupportedFormats', function () {
|
|||||||
],
|
],
|
||||||
(imagePath) => {
|
(imagePath) => {
|
||||||
it(`should return false if filename is ${imagePath}`, function () {
|
it(`should return false if filename is ${imagePath}`, function () {
|
||||||
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(
|
const looksLikeWindowsImage =
|
||||||
imagePath,
|
supportedFormats.looksLikeWindowsImage(imagePath);
|
||||||
);
|
|
||||||
expect(looksLikeWindowsImage).to.be.false;
|
expect(looksLikeWindowsImage).to.be.false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@@ -15,43 +15,52 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import { platform } from 'os';
|
||||||
import { Application } from 'spectron';
|
import { Application } from 'spectron';
|
||||||
import * as electronPath from 'electron';
|
import * as electronPath from 'electron';
|
||||||
|
|
||||||
describe('Spectron', function () {
|
// TODO: spectron fails to start on the CI with:
|
||||||
// Mainly for CI jobs
|
// Error: Failed to create session.
|
||||||
this.timeout(40000);
|
// unknown error: Chrome failed to start: exited abnormally
|
||||||
|
if (platform() !== 'darwin') {
|
||||||
|
describe('Spectron', function () {
|
||||||
|
// Mainly for CI jobs
|
||||||
|
this.timeout(40000);
|
||||||
|
|
||||||
const app = new Application({
|
const app = new Application({
|
||||||
path: (electronPath as unknown) as string,
|
path: electronPath as unknown as string,
|
||||||
args: ['--no-sandbox', '.'],
|
args: ['--no-sandbox', '.'],
|
||||||
});
|
|
||||||
|
|
||||||
before('app:start', async () => {
|
|
||||||
await app.start();
|
|
||||||
});
|
|
||||||
|
|
||||||
after('app:stop', async () => {
|
|
||||||
if (app && app.isRunning()) {
|
|
||||||
await app.stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Browser Window', () => {
|
|
||||||
it('should open a browser window', async () => {
|
|
||||||
// We can't use `isVisible()` here as it won't work inside
|
|
||||||
// a Windows Docker container, but we can approximate it
|
|
||||||
// with these set of checks:
|
|
||||||
const bounds = await app.browserWindow.getBounds();
|
|
||||||
expect(bounds.height).to.be.above(0);
|
|
||||||
expect(bounds.width).to.be.above(0);
|
|
||||||
expect(await app.browserWindow.isMinimized()).to.be.false;
|
|
||||||
expect(await app.browserWindow.isVisible()).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set a proper title', async () => {
|
before('app:start', async () => {
|
||||||
// @ts-ignore (SpectronClient.getTitle exists)
|
await app.start();
|
||||||
return expect(await app.client.getTitle()).to.equal('Etcher');
|
});
|
||||||
|
|
||||||
|
after('app:stop', async () => {
|
||||||
|
if (app && app.isRunning()) {
|
||||||
|
await app.stop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Browser Window', () => {
|
||||||
|
it('should open a browser window', async () => {
|
||||||
|
// We can't use `isVisible()` here as it won't work inside
|
||||||
|
// a Windows Docker container, but we can approximate it
|
||||||
|
// with these set of checks:
|
||||||
|
const bounds = await app.browserWindow.getBounds();
|
||||||
|
expect(bounds.height).to.be.above(0);
|
||||||
|
expect(bounds.width).to.be.above(0);
|
||||||
|
expect(await app.browserWindow.isMinimized()).to.be.false;
|
||||||
|
expect(
|
||||||
|
(await app.browserWindow.isVisible()) ||
|
||||||
|
(await app.browserWindow.isFocused()),
|
||||||
|
).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set a proper title', async () => {
|
||||||
|
// @ts-ignore (SpectronClient.getTitle exists)
|
||||||
|
return expect(await app.client.getTitle()).to.equal('balenaEtcher');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
@@ -1,12 +1,21 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"target": "es2019",
|
||||||
|
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["dom", "esnext"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"pretty": true,
|
||||||
|
"sourceMap": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"resolveJsonModule": true,
|
"noImplicitReturns": true,
|
||||||
"target": "es2019",
|
"noFallthroughCasesInSwitch": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"jsx": "react",
|
"allowSyntheticDefaultImports": true,
|
||||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
"resolveJsonModule": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,16 @@
|
|||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"lib": ["dom", "esnext"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"pretty": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"allowJs": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"lib/**/*.ts",
|
"lib/**/*.ts",
|
||||||
|
1
typings/pnp-webpack-plugin/index.d.ts
vendored
Normal file
1
typings/pnp-webpack-plugin/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module 'pnp-webpack-plugin';
|
2
typings/sudo-prompt/index.d.ts
vendored
2
typings/sudo-prompt/index.d.ts
vendored
@@ -1 +1 @@
|
|||||||
declare module 'sudo-prompt';
|
declare module '@balena/sudo-prompt';
|
||||||
|
@@ -17,7 +17,6 @@
|
|||||||
import * as CopyPlugin from 'copy-webpack-plugin';
|
import * as CopyPlugin from 'copy-webpack-plugin';
|
||||||
import { readdirSync } from 'fs';
|
import { readdirSync } from 'fs';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import outdent from 'outdent';
|
import outdent from 'outdent';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -25,6 +24,9 @@ import { env } from 'process';
|
|||||||
import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin';
|
import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin';
|
||||||
import * as TerserPlugin from 'terser-webpack-plugin';
|
import * as TerserPlugin from 'terser-webpack-plugin';
|
||||||
import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
|
import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
|
||||||
|
import * as PnpWebpackPlugin from 'pnp-webpack-plugin';
|
||||||
|
|
||||||
|
import * as tsconfigRaw from './tsconfig.webpack.json';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Don't webpack package.json as mixpanel & sentry tokens
|
* Don't webpack package.json as mixpanel & sentry tokens
|
||||||
@@ -32,8 +34,7 @@ import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
|
|||||||
*/
|
*/
|
||||||
function externalPackageJson(packageJsonPath: string) {
|
function externalPackageJson(packageJsonPath: string) {
|
||||||
return (
|
return (
|
||||||
_context: string,
|
{ request }: { context: string; request: string },
|
||||||
request: string,
|
|
||||||
callback: (error?: Error | null, result?: string) => void,
|
callback: (error?: Error | null, result?: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (_.endsWith(request, 'package.json')) {
|
if (_.endsWith(request, 'package.json')) {
|
||||||
@@ -50,8 +51,7 @@ function platformSpecificModule(
|
|||||||
) {
|
) {
|
||||||
// Resolves module on platform, otherwise resolves the replacement
|
// Resolves module on platform, otherwise resolves the replacement
|
||||||
return (
|
return (
|
||||||
_context: string,
|
{ request }: { context: string; request: string },
|
||||||
request: string,
|
|
||||||
callback: (error?: Error, result?: string, type?: string) => void,
|
callback: (error?: Error, result?: string, type?: string) => void,
|
||||||
) => {
|
) => {
|
||||||
if (request === module && os.platform() !== platform) {
|
if (request === module && os.platform() !== platform) {
|
||||||
@@ -70,16 +70,70 @@ function renameNodeModules(resourcePath: string) {
|
|||||||
path
|
path
|
||||||
.relative(__dirname, resourcePath)
|
.relative(__dirname, resourcePath)
|
||||||
.replace('node_modules', 'modules')
|
.replace('node_modules', 'modules')
|
||||||
|
// use the same name on all architectures so electron-builder can build a universal dmg on mac
|
||||||
|
.replace(LZMA_BINDINGS_FOLDER, LZMA_BINDINGS_FOLDER_RENAMED)
|
||||||
// file-loader expects posix paths, even on Windows
|
// file-loader expects posix paths, even on Windows
|
||||||
.replace(/\\/g, '/')
|
.replace(/\\/g, '/')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findUsbPrebuild(): string[] {
|
||||||
|
const usbPrebuildsFolder = path.join('node_modules', 'usb', 'prebuilds')
|
||||||
|
const prebuildFolders = readdirSync(usbPrebuildsFolder);
|
||||||
|
let bindingFile: string | undefined = 'node.napi.node';
|
||||||
|
const platformFolder = prebuildFolders.find(
|
||||||
|
(f) =>
|
||||||
|
f.startsWith(os.platform()) &&
|
||||||
|
f.indexOf(os.arch()) > -1,
|
||||||
|
);
|
||||||
|
if (platformFolder === undefined) {
|
||||||
|
throw new Error('Could not find usb prebuild. Should try fallback to node-gyp and use /build/Release instead of /prebuilds');
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindingFiles = readdirSync(
|
||||||
|
path.join(usbPrebuildsFolder, platformFolder)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!bindingFiles.length) {
|
||||||
|
throw new Error('Could not find usb prebuild for platform')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindingFiles.length === 1) {
|
||||||
|
bindingFile = bindingFiles[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// armv6 vs v7 in linux-arm and
|
||||||
|
// glibc vs musl in linux-x64
|
||||||
|
if (bindingFiles.length > 1) {
|
||||||
|
bindingFile = bindingFiles.find((file) => {
|
||||||
|
if (bindingFiles.indexOf('arm') > -1) {
|
||||||
|
const process = require('process')
|
||||||
|
return file.indexOf(process.config.variables.arm_version) > -1
|
||||||
|
} else {
|
||||||
|
return file.indexOf('glibc') > -1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindingFile === undefined) {
|
||||||
|
throw new Error('Could not find usb prebuild for platform')
|
||||||
|
}
|
||||||
|
|
||||||
|
return [platformFolder, bindingFile];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
USB_BINDINGS_FOLDER,
|
||||||
|
USB_BINDINGS_FILE,
|
||||||
|
] = findUsbPrebuild();
|
||||||
|
|
||||||
function findLzmaNativeBindingsFolder(): string {
|
function findLzmaNativeBindingsFolder(): string {
|
||||||
const files = readdirSync(path.join('node_modules', 'lzma-native'));
|
const files = readdirSync(
|
||||||
|
path.join('node_modules', 'lzma-native', 'prebuilds'),
|
||||||
|
);
|
||||||
const bindingsFolder = files.find(
|
const bindingsFolder = files.find(
|
||||||
(f) =>
|
(f) =>
|
||||||
f.startsWith('binding-') &&
|
f.startsWith(os.platform()) &&
|
||||||
f.endsWith(env.npm_config_target_arch || os.arch()),
|
f.endsWith(env.npm_config_target_arch || os.arch()),
|
||||||
);
|
);
|
||||||
if (bindingsFolder === undefined) {
|
if (bindingsFolder === undefined) {
|
||||||
@@ -89,6 +143,7 @@ function findLzmaNativeBindingsFolder(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LZMA_BINDINGS_FOLDER = findLzmaNativeBindingsFolder();
|
const LZMA_BINDINGS_FOLDER = findLzmaNativeBindingsFolder();
|
||||||
|
const LZMA_BINDINGS_FOLDER_RENAMED = 'binding';
|
||||||
|
|
||||||
interface ReplacementRule {
|
interface ReplacementRule {
|
||||||
search: string;
|
search: string;
|
||||||
@@ -108,19 +163,45 @@ function replace(test: RegExp, ...replacements: ReplacementRule[]) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchWasm(...where: string[]) {
|
||||||
|
const whereStr = where.map((x) => `'${x}'`).join(', ');
|
||||||
|
return outdent`
|
||||||
|
const Path = require('path');
|
||||||
|
let electron;
|
||||||
|
try {
|
||||||
|
// This doesn't exist in the child-writer
|
||||||
|
electron = require('electron');
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
function appPath() {
|
||||||
|
return Path.isAbsolute(__dirname) ?
|
||||||
|
__dirname :
|
||||||
|
Path.join(
|
||||||
|
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
|
||||||
|
// include the app-x64 or app-arm64 folder depending on the arch.
|
||||||
|
// We don't care about the app.asar file, we want the actual folder.
|
||||||
|
electron.remote.app.getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
|
||||||
|
'generated'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
scriptDirectory = Path.join(appPath(), 'modules', ${whereStr}, '/');
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const commonConfig = {
|
const commonConfig = {
|
||||||
mode: 'production',
|
mode: 'production',
|
||||||
optimization: {
|
optimization: {
|
||||||
|
moduleIds: 'natural',
|
||||||
minimize: true,
|
minimize: true,
|
||||||
minimizer: [
|
minimizer: [
|
||||||
new TerserPlugin({
|
new TerserPlugin({
|
||||||
|
parallel: true,
|
||||||
terserOptions: {
|
terserOptions: {
|
||||||
compress: false,
|
compress: false,
|
||||||
mangle: false,
|
mangle: false,
|
||||||
output: {
|
format: {
|
||||||
beautify: true,
|
|
||||||
comments: false,
|
comments: false,
|
||||||
ecma: 2018,
|
ecma: 2020,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
extractComments: false,
|
extractComments: false,
|
||||||
@@ -131,7 +212,12 @@ const commonConfig = {
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: 'css-loader',
|
use: ['style-loader', 'css-loader'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: { name: renameNodeModules },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
@@ -141,9 +227,11 @@ const commonConfig = {
|
|||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'ts-loader',
|
loader: 'esbuild-loader',
|
||||||
options: {
|
options: {
|
||||||
configFile: 'tsconfig.webpack.json',
|
loader: 'tsx',
|
||||||
|
target: 'es2021',
|
||||||
|
tsconfigRaw,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -174,13 +262,8 @@ const commonConfig = {
|
|||||||
/node_modules\/lzma-native\/index\.js$/,
|
/node_modules\/lzma-native\/index\.js$/,
|
||||||
// remove node-pre-gyp magic from lzma-native
|
// remove node-pre-gyp magic from lzma-native
|
||||||
{
|
{
|
||||||
search: 'require(binding_path)',
|
search: `require('node-gyp-build')(__dirname);`,
|
||||||
replace: () => {
|
replace: `require('./prebuilds/${LZMA_BINDINGS_FOLDER}/electron.napi.node')`,
|
||||||
return `require('./${path.posix.join(
|
|
||||||
LZMA_BINDINGS_FOLDER,
|
|
||||||
'lzma_native.node',
|
|
||||||
)}')`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// use regular stream module instead of readable-stream
|
// use regular stream module instead of readable-stream
|
||||||
{
|
{
|
||||||
@@ -189,14 +272,9 @@ const commonConfig = {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
// remove node-pre-gyp magic from usb
|
// remove node-pre-gyp magic from usb
|
||||||
replace(/node_modules\/@balena.io\/usb\/usb\.js$/, {
|
replace(/node_modules\/usb\/dist\/usb\/bindings\.js$/, {
|
||||||
search: 'require(binding_path)',
|
search: `require('node-gyp-build')(path_1.join(__dirname, '..', '..'));`,
|
||||||
replace: "require('./build/Release/usb_bindings.node')",
|
replace: `require('../../prebuilds/${USB_BINDINGS_FOLDER}/${USB_BINDINGS_FILE}')`,
|
||||||
}),
|
|
||||||
// remove bindings magic from ext2fs
|
|
||||||
replace(/node_modules\/ext2fs\/lib\/(ext2fs|binding)\.js$/, {
|
|
||||||
search: "require('bindings')('bindings')",
|
|
||||||
replace: "require('../build/Release/bindings.node')",
|
|
||||||
}),
|
}),
|
||||||
// remove bindings magic from mountutils
|
// remove bindings magic from mountutils
|
||||||
replace(/node_modules\/mountutils\/index\.js$/, {
|
replace(/node_modules\/mountutils\/index\.js$/, {
|
||||||
@@ -229,9 +307,33 @@ const commonConfig = {
|
|||||||
"return await readFile(Path.join(__dirname, '..', 'blobs', filename));",
|
"return await readFile(Path.join(__dirname, '..', 'blobs', filename));",
|
||||||
replace: outdent`
|
replace: outdent`
|
||||||
const { app, remote } = require('electron');
|
const { app, remote } = require('electron');
|
||||||
return await readFile(Path.join((app || remote.app).getAppPath(), 'generated', __dirname.replace('node_modules', 'modules'), '..', 'blobs', filename));
|
return await readFile(
|
||||||
|
Path.join(
|
||||||
|
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
|
||||||
|
// include the app-x64 or app-arm64 folder depending on the arch.
|
||||||
|
// We don't care about the app.asar file, we want the actual folder.
|
||||||
|
(app || remote.app).getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
|
||||||
|
'generated',
|
||||||
|
__dirname.replace('node_modules', 'modules'),
|
||||||
|
'..',
|
||||||
|
'blobs',
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
);
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
|
// Use the libext2fs.wasm file in the generated folder
|
||||||
|
// The way to find the app directory depends on whether we run in the renderer or in the child-writer
|
||||||
|
// We use __dirname in the child-writer and electron.remote.app.getAppPath() in the renderer
|
||||||
|
replace(/node_modules\/ext2fs\/lib\/libext2fs\.js$/, {
|
||||||
|
search: 'scriptDirectory=__dirname+"/"',
|
||||||
|
replace: fetchWasm('ext2fs', 'lib'),
|
||||||
|
}),
|
||||||
|
// Same for node-crc-utils
|
||||||
|
replace(/node_modules\/@balena\/node-crc-utils\/crc32\.js$/, {
|
||||||
|
search: 'scriptDirectory=__dirname+"/"',
|
||||||
|
replace: fetchWasm('@balena', 'node-crc-utils'),
|
||||||
|
}),
|
||||||
// Copy native modules to generated folder
|
// Copy native modules to generated folder
|
||||||
{
|
{
|
||||||
test: /\.node$/,
|
test: /\.node$/,
|
||||||
@@ -248,16 +350,20 @@ const commonConfig = {
|
|||||||
extensions: ['.node', '.js', '.json', '.ts', '.tsx'],
|
extensions: ['.node', '.js', '.json', '.ts', '.tsx'],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
PnpWebpackPlugin,
|
||||||
new SimpleProgressWebpackPlugin({
|
new SimpleProgressWebpackPlugin({
|
||||||
format: process.env.WEBPACK_PROGRESS || 'verbose',
|
format: process.env.WEBPACK_PROGRESS || 'verbose',
|
||||||
}),
|
}),
|
||||||
// Force axios to use http.js, not xhr.js as we need stream support
|
// Force axios to use http.js, not xhr.js as we need stream support
|
||||||
// (it's package.json file replaces http with xhr for browser targets).
|
// (its package.json file replaces http with xhr for browser targets).
|
||||||
new NormalModuleReplacementPlugin(
|
new NormalModuleReplacementPlugin(
|
||||||
slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/),
|
slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/),
|
||||||
'./http.js',
|
'./http.js',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
resolveLoader: {
|
||||||
|
plugins: [PnpWebpackPlugin.moduleLoader(module)],
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, 'generated'),
|
path: path.join(__dirname, 'generated'),
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
@@ -281,13 +387,21 @@ const guiConfigCopyPatterns = [
|
|||||||
from: 'node_modules/node-raspberrypi-usbboot/blobs',
|
from: 'node_modules/node-raspberrypi-usbboot/blobs',
|
||||||
to: 'modules/node-raspberrypi-usbboot/blobs',
|
to: 'modules/node-raspberrypi-usbboot/blobs',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
from: 'node_modules/ext2fs/lib/libext2fs.wasm',
|
||||||
|
to: 'modules/ext2fs/lib/libext2fs.wasm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'node_modules/@balena/node-crc-utils/crc32.wasm',
|
||||||
|
to: 'modules/@balena/node-crc-utils/crc32.wasm',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (os.platform() === 'win32') {
|
if (os.platform() === 'win32') {
|
||||||
// liblzma.dll is required on Windows for lzma-native
|
// liblzma.dll is required on Windows for lzma-native
|
||||||
guiConfigCopyPatterns.push({
|
guiConfigCopyPatterns.push({
|
||||||
from: `node_modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
|
from: `node_modules/lzma-native/prebuilds/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
|
||||||
to: `modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
|
to: `modules/lzma-native/prebuilds/${LZMA_BINDINGS_FOLDER_RENAMED}/liblzma.dll`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,10 +413,19 @@ const guiConfig = {
|
|||||||
__filename: true,
|
__filename: true,
|
||||||
},
|
},
|
||||||
entry: {
|
entry: {
|
||||||
gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.ts'),
|
gui: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
|
||||||
},
|
},
|
||||||
|
// entry: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
|
||||||
plugins: [
|
plugins: [
|
||||||
...commonConfig.plugins,
|
...commonConfig.plugins,
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [
|
||||||
|
{ from: 'lib/gui/app/index.html', to: 'index.html' },
|
||||||
|
// electron-builder doesn't bundle folders named "assets"
|
||||||
|
// See https://github.com/electron-userland/electron-builder/issues/4545
|
||||||
|
{ from: 'assets/icon.png', to: 'media/icon.png' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
// Remove "Download the React DevTools for a better development experience" message
|
// Remove "Download the React DevTools for a better development experience" message
|
||||||
new BannerPlugin({
|
new BannerPlugin({
|
||||||
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
|
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
|
||||||
@@ -341,41 +464,4 @@ const childWriterConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const cssConfig = {
|
export default [guiConfig, etcherConfig, childWriterConfig];
|
||||||
mode: 'production',
|
|
||||||
optimization: {
|
|
||||||
minimize: false,
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: { name: renameNodeModules },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new MiniCssExtractPlugin({ filename: '[name].css' }),
|
|
||||||
new CopyPlugin({
|
|
||||||
patterns: [
|
|
||||||
{ from: 'lib/gui/app/index.html', to: 'index.html' },
|
|
||||||
// electron-builder doesn't bundle folders named "assets"
|
|
||||||
// See https://github.com/electron-userland/electron-builder/issues/4545
|
|
||||||
{ from: 'assets/icon.png', to: 'media/icon.png' },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
entry: {
|
|
||||||
index: path.join(__dirname, 'lib', 'gui', 'app', 'css', 'main.css'),
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, 'generated'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = [cssConfig, guiConfig, etcherConfig, childWriterConfig];
|
|
||||||
|
24
webpack.dev.config.ts
Normal file
24
webpack.dev.config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import configs from './webpack.config';
|
||||||
|
import { WebpackOptionsNormalized } from 'webpack';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
const [
|
||||||
|
guiConfig,
|
||||||
|
etcherConfig,
|
||||||
|
childWriterConfig,
|
||||||
|
] = (configs as unknown) as WebpackOptionsNormalized[];
|
||||||
|
|
||||||
|
configs.forEach((config) => {
|
||||||
|
config.mode = 'development';
|
||||||
|
// @ts-ignore
|
||||||
|
config.devtool = 'source-map';
|
||||||
|
});
|
||||||
|
|
||||||
|
guiConfig.devServer = {
|
||||||
|
hot: true,
|
||||||
|
port: 3030,
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.copyFileSync('./lib/gui/app/index.dev.html', './generated/index.html');
|
||||||
|
|
||||||
|
export default [guiConfig, etcherConfig, childWriterConfig];
|
Reference in New Issue
Block a user