mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-24 11:46:31 +00:00
Compare commits
82 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
391164bf15 | ||
![]() |
7c2c2bc3d6 | ||
![]() |
c2d160f5c7 | ||
![]() |
385bf45883 | ||
![]() |
aa6d526fea | ||
![]() |
fdd082b9cd | ||
![]() |
624dc77969 | ||
![]() |
a1e9be2f94 | ||
![]() |
c2fc36971c | ||
![]() |
85b1e3c2c2 | ||
![]() |
e5d1b4ce23 | ||
![]() |
aac092fd4d | ||
![]() |
ff852c029e | ||
![]() |
4759bc7686 | ||
![]() |
039a022353 | ||
![]() |
4375b960c2 | ||
![]() |
ee5505d596 | ||
![]() |
c726b51dca | ||
![]() |
676eaf82e7 | ||
![]() |
87fb4df9eb | ||
![]() |
e43ee788ec | ||
![]() |
3dc17c89b4 | ||
![]() |
5774dded7b | ||
![]() |
9f408241f9 | ||
![]() |
2ed779ef37 | ||
![]() |
5fd6376f45 | ||
![]() |
818dcd3b13 | ||
![]() |
52d396aa7e | ||
![]() |
c748c2a9c0 | ||
![]() |
a5dac57b09 | ||
![]() |
8dad81ae34 | ||
![]() |
d28719daf2 | ||
![]() |
98db4df0dc | ||
![]() |
52144f4a6e | ||
![]() |
39b02f2168 | ||
![]() |
c4d3f8db87 | ||
![]() |
6d796df017 | ||
![]() |
326a3c740f | ||
![]() |
8223130e8d | ||
![]() |
3245439744 | ||
![]() |
74854f1720 | ||
![]() |
4ffda6e208 | ||
![]() |
62ac0b98b9 | ||
![]() |
ae70c20779 | ||
![]() |
e94767aca7 | ||
![]() |
6a648e9215 | ||
![]() |
fa8220d5ba | ||
![]() |
2dfa795129 | ||
![]() |
73afb2fc55 | ||
![]() |
c5a8bfc0dc | ||
![]() |
cb03fb8375 | ||
![]() |
c756b10a38 | ||
![]() |
ebeacc9be9 | ||
![]() |
fa642270f7 | ||
![]() |
0cc7440573 | ||
![]() |
bf5c00a839 | ||
![]() |
bc3340960a | ||
![]() |
d498248a0f | ||
![]() |
2e8e0d77bc | ||
![]() |
8389537bf4 | ||
![]() |
afd659f9e5 | ||
![]() |
ffdeccf7ef | ||
![]() |
37ac323e10 | ||
![]() |
7c8f3c35d3 | ||
![]() |
4aa4140d65 | ||
![]() |
0642611079 | ||
![]() |
2f4a12a48f | ||
![]() |
70f0fb677c | ||
![]() |
58c82b33ec | ||
![]() |
a661d102bc | ||
![]() |
b132352464 | ||
![]() |
0a243caf35 | ||
![]() |
ccc31bb9aa | ||
![]() |
b3e33824ed | ||
![]() |
6582260355 | ||
![]() |
b1d2bdaa06 | ||
![]() |
5ad8d5a72a | ||
![]() |
ad1c4c7175 | ||
![]() |
003abfb88f | ||
![]() |
dc5c68a6a1 | ||
![]() |
d76adfb081 | ||
![]() |
c696c389c9 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -62,7 +62,3 @@ 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
|
|
||||||
|
21
.github/actions/publish/action.yml
vendored
21
.github/actions/publish/action.yml
vendored
@ -3,10 +3,10 @@ name: package and publish GitHub (draft) release
|
|||||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||||
inputs:
|
inputs:
|
||||||
json:
|
json:
|
||||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
description: 'JSON stringified object containing all the inputs from the calling workflow'
|
||||||
required: true
|
required: true
|
||||||
secrets:
|
secrets:
|
||||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
description: 'JSON stringified object containing all the secrets from the calling workflow'
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
# --- custom environment
|
# --- custom environment
|
||||||
@ -15,14 +15,14 @@ inputs:
|
|||||||
# Beware that native modules will be built for this version,
|
# Beware that native modules will be built for this version,
|
||||||
# which might not be compatible with the one used by pkg (see forge.sidecar.ts)
|
# which might not be compatible with the one used by pkg (see forge.sidecar.ts)
|
||||||
# https://github.com/vercel/pkg-fetch/releases
|
# https://github.com/vercel/pkg-fetch/releases
|
||||||
default: "18.x"
|
default: '20.x'
|
||||||
VERBOSE:
|
VERBOSE:
|
||||||
type: string
|
type: string
|
||||||
default: "true"
|
default: 'true'
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||||
using: "composite"
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Download custom source artifact
|
- name: Download custom source artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@ -53,6 +53,13 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: sudo apt-get install -y --no-install-recommends fakeroot dpkg rpm
|
run: sudo apt-get install -y --no-install-recommends fakeroot dpkg rpm
|
||||||
|
|
||||||
|
# rpmbuild will strip binaries by default, which breaks the sidecar.
|
||||||
|
# Use a macro to override the "strip" to bypass stripping.
|
||||||
|
- name: Configure rpmbuild to not strip executables
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: echo '%__strip /usr/bin/true' > ~/.rpmmacros
|
||||||
|
|
||||||
- name: Install host dependencies
|
- name: Install host dependencies
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
|
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
|
||||||
@ -131,7 +138,7 @@ runs:
|
|||||||
PLATFORM=Windows
|
PLATFORM=Windows
|
||||||
SHA256SUM_BIN=sha256sum
|
SHA256SUM_BIN=sha256sum
|
||||||
|
|
||||||
# Install DigiCert Signing Manager Tools
|
# Install DigiCert Signing Manager Tools
|
||||||
curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \
|
curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \
|
||||||
-H "x-api-key:$SM_API_KEY" \
|
-H "x-api-key:$SM_API_KEY" \
|
||||||
-o smtools-windows-x64.msi
|
-o smtools-windows-x64.msi
|
||||||
@ -139,8 +146,8 @@ runs:
|
|||||||
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
||||||
smksp_registrar.exe list
|
smksp_registrar.exe list
|
||||||
smctl.exe keypair ls
|
smctl.exe keypair ls
|
||||||
|
smctl.exe windows certsync
|
||||||
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
|
||||||
smksp_cert_sync.exe
|
|
||||||
|
|
||||||
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
|
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
|
||||||
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
||||||
|
20
.github/actions/test/action.yml
vendored
20
.github/actions/test/action.yml
vendored
@ -3,23 +3,23 @@ name: test release
|
|||||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||||
inputs:
|
inputs:
|
||||||
json:
|
json:
|
||||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
description: 'JSON stringified object containing all the inputs from the calling workflow'
|
||||||
required: true
|
required: true
|
||||||
secrets:
|
secrets:
|
||||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
description: 'JSON stringified object containing all the secrets from the calling workflow'
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
# --- custom environment
|
# --- custom environment
|
||||||
NODE_VERSION:
|
NODE_VERSION:
|
||||||
type: string
|
type: string
|
||||||
default: "18.18"
|
default: '20.19'
|
||||||
VERBOSE:
|
VERBOSE:
|
||||||
type: string
|
type: string
|
||||||
default: "true"
|
default: 'true'
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||||
using: "composite"
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@ -32,7 +32,7 @@ runs:
|
|||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y --no-install-recommends xvfb libudev-dev
|
sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb libudev-dev
|
||||||
cat < package.json | jq -r '.hostDependencies[][]' - | \
|
cat < package.json | jq -r '.hostDependencies[][]' - | \
|
||||||
xargs -L1 echo | sed 's/|//g' | xargs -L1 \
|
xargs -L1 echo | sed 's/|//g' | xargs -L1 \
|
||||||
sudo apt-get --ignore-missing install || true
|
sudo apt-get --ignore-missing install || true
|
||||||
@ -55,9 +55,15 @@ runs:
|
|||||||
# fi
|
# fi
|
||||||
|
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
|
# as the shrinkwrap might have been done on mac/linux, this is ensure the package is there for windows
|
||||||
|
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||||
|
npm i -D winusb-driver-generator
|
||||||
|
fi
|
||||||
|
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run package
|
npm run package
|
||||||
npm run test
|
npm run wdio # test stage, note that it requires the package to be done first
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# https://www.electronjs.org/docs/latest/api/environment-variables
|
# https://www.electronjs.org/docs/latest/api/environment-variables
|
||||||
|
19
.github/workflows/flowzone.yml
vendored
19
.github/workflows/flowzone.yml
vendored
@ -18,7 +18,24 @@ jobs:
|
|||||||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
|
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
custom_runs_on: '[["ubuntu-20.04"],["windows-2019"],["macos-12"],["macos-latest-xlarge"]]'
|
custom_test_matrix: >
|
||||||
|
{
|
||||||
|
"os": [
|
||||||
|
["ubuntu-22.04"],
|
||||||
|
["windows-2019"],
|
||||||
|
["macos-13"],
|
||||||
|
["macos-latest-xlarge"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
custom_publish_matrix: >
|
||||||
|
{
|
||||||
|
"os": [
|
||||||
|
["ubuntu-22.04"],
|
||||||
|
["windows-2019"],
|
||||||
|
["macos-13"],
|
||||||
|
["macos-latest-xlarge"]
|
||||||
|
]
|
||||||
|
}
|
||||||
restrict_custom_actions: false
|
restrict_custom_actions: false
|
||||||
github_prerelease: true
|
github_prerelease: true
|
||||||
cloudflare_website: "etcher"
|
cloudflare_website: "etcher"
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +0,0 @@
|
|||||||
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,3 +1,329 @@
|
|||||||
|
- commits:
|
||||||
|
- subject: Remove stale secrets
|
||||||
|
hash: c2fc36971c9460eac6bd02cfc7bdcabec7b97a6d
|
||||||
|
body: ""
|
||||||
|
footer:
|
||||||
|
change-type: patch
|
||||||
|
author: Anton Belodedenko
|
||||||
|
nested: []
|
||||||
|
version: 2.1.3
|
||||||
|
title: ""
|
||||||
|
date: 2025-05-15T18:09:55.848Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: remove analytics"
|
||||||
|
hash: aa6d526fea010d181f49dd81ae3bdaefb8d1938e
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 2.1.2
|
||||||
|
title: ""
|
||||||
|
date: 2025-05-08T08:51:44.810Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: fix signin windows artifacts"
|
||||||
|
hash: a1e9be2f94629447e02994e52e12c67ec98de831
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 2.1.1
|
||||||
|
title: ""
|
||||||
|
date: 2025-05-05T17:19:50.443Z
|
||||||
|
- commits:
|
||||||
|
- subject: Add informational notice about how to disable analytics collection
|
||||||
|
hash: aac092fd4df8750024c082b25dcbd0ae6ee618fd
|
||||||
|
body: ""
|
||||||
|
footer:
|
||||||
|
Change-type: minor
|
||||||
|
change-type: minor
|
||||||
|
author: myarmolinsky
|
||||||
|
nested: []
|
||||||
|
version: 2.1.0
|
||||||
|
title: ""
|
||||||
|
date: 2025-02-27T16:16:57.036Z
|
||||||
|
- commits:
|
||||||
|
- subject: "major: build on ubuntu 22 and macos 13"
|
||||||
|
hash: 039a022353d1980ef9ddd19166515c531e48aba4
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 2.0.0
|
||||||
|
title: ""
|
||||||
|
date: 2025-02-20T14:27:01.338Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: bump etcher-sdk to 9.1.2"
|
||||||
|
hash: c726b51dca3383c76f4bf824fd5d594ac3069180
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.25
|
||||||
|
title: ""
|
||||||
|
date: 2024-10-10T10:03:29.519Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: etcher-util is corrupted in RPM package"
|
||||||
|
hash: e43ee788ec5ec49e105ff804206919bb10a59ea7
|
||||||
|
body: |
|
||||||
|
rpmbuild strips executables by default when generating an rpm packge.
|
||||||
|
This was causing the JavaScript code bundled in the etcher-util file
|
||||||
|
to be removed, causing "Pkg: Error reading from file." whenever
|
||||||
|
etcher-util was called.
|
||||||
|
|
||||||
|
This in turn caused balena-etcher to generate the error message
|
||||||
|
`Error: (0, h.requestMetadata) is not a function` when attempting
|
||||||
|
to write an SD card.
|
||||||
|
|
||||||
|
This fixes the issue for RPM builds by replacing the `strip` command
|
||||||
|
with `true` so that rpmbuild no longer strips the executables and
|
||||||
|
the embeded code stays intact.
|
||||||
|
|
||||||
|
See: https://github.com/balena-io/etcher/issues/4150
|
||||||
|
footer:
|
||||||
|
Signed-off-by: Richard Glidden <richard@glidden.org>
|
||||||
|
signed-off-by: Richard Glidden <richard@glidden.org>
|
||||||
|
author: Richard Glidden
|
||||||
|
nested: []
|
||||||
|
version: 1.19.24
|
||||||
|
title: ""
|
||||||
|
date: 2024-10-09T14:22:56.623Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: remove gconf2 libgconf-2-4 deps"
|
||||||
|
hash: 2ed779ef371db367e4e413c9d0d08fcd738edb5b
|
||||||
|
body: "Closes #4096"
|
||||||
|
footer: {}
|
||||||
|
author: Marc-Aurèle Brothier
|
||||||
|
nested: []
|
||||||
|
version: 1.19.23
|
||||||
|
title: ""
|
||||||
|
date: 2024-10-09T13:52:54.936Z
|
||||||
|
- commits:
|
||||||
|
- subject: Replace deprecated Flowzone inputs
|
||||||
|
hash: 52d396aa7ea9ae1ef6d68151f582f04f57191b14
|
||||||
|
body: ""
|
||||||
|
footer:
|
||||||
|
Change-type: patch
|
||||||
|
change-type: patch
|
||||||
|
author: Kyle Harding
|
||||||
|
nested: []
|
||||||
|
version: 1.19.22
|
||||||
|
title: ""
|
||||||
|
date: 2024-07-18T18:12:56.368Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: fix missing windows dependency"
|
||||||
|
hash: 8dad81ae34b8d71f3d4f7151ee60717e6207ccd8
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: fix missing windows dependency"
|
||||||
|
hash: d28719daf249f2994acdf94b4bb7ea937ffcab9b
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: fix missing windows dependency"
|
||||||
|
hash: 98db4df0dc147e5fec9180c50f4e21acf1fd0a58
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.21
|
||||||
|
title: ""
|
||||||
|
date: 2024-05-30T15:00:35.706Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: fix missing windows dependency"
|
||||||
|
hash: c4d3f8db8769418925a9909ac700edc5f425a068
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.20
|
||||||
|
title: ""
|
||||||
|
date: 2024-05-30T10:17:29.075Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: add sentry debug flag"
|
||||||
|
hash: 8223130e8dfce180481550d77f022064255601e4
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.19
|
||||||
|
title: ""
|
||||||
|
date: 2024-05-28T12:09:51.167Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: fix Sentry DSN for main process"
|
||||||
|
hash: 4ffda6e208a6e2f109f652d39e1248bec23a2ddf
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.18
|
||||||
|
title: ""
|
||||||
|
date: 2024-05-22T13:28:03.659Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: fix injection of analytics key at build time"
|
||||||
|
hash: e94767aca7b07e674bd60176ef77c11440131ace
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: JOASSART Edwin
|
||||||
|
nested: []
|
||||||
|
version: 1.19.17
|
||||||
|
title: ""
|
||||||
|
date: 2024-05-09T06:33:45.091Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: hold request for metadata while waiting for flasher"
|
||||||
|
hash: 2dfa795129e287f887b9ea02f2eca717575d27ac
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.16
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-26T14:33:19.111Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: bump etcher-sdk to 9.0.11 to fix url loading using http/2"
|
||||||
|
hash: cb03fb83754f38d647fc951b94470725b46b2b31
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.15
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-26T13:26:57.047Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: pretty-bytes to 6.1.1"
|
||||||
|
hash: fa642270f7153f14e45ee03a73bad1f0797cbd51
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: JOASSART Edwin
|
||||||
|
nested: []
|
||||||
|
version: 1.19.14
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-25T21:11:35.350Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: use etcher icon as loading for windows installer"
|
||||||
|
hash: bc3340960a765e99f2f02bc21adace91d228d26f
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: fix windows squirrel install"
|
||||||
|
hash: d498248a0f1416045b836646b72c7b4c588119d3
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.13
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-25T19:02:23.576Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: bump minors & patch"
|
||||||
|
hash: afd659f9e586e012be7e3b02490d14a8ac64bb35
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: bump @electron-forge/* to 7.4.0"
|
||||||
|
hash: ffdeccf7efd1412a2e2838fd07df5b21f1233efe
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: bump electron to 30.0.1 & @electron/remote to 2.1.2"
|
||||||
|
hash: 37ac323e10c07db35a7e47b576d07e1d4d41a470
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: npm upgrade"
|
||||||
|
hash: 7c8f3c35d3d159e7be73442ab215019dc2388f54
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: bump @balena/lint to 8.0.2 and fix formating"
|
||||||
|
hash: 4aa4140d65189920938c42c41a6a781c97148c8a
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: fix pretty-bytes imports"
|
||||||
|
hash: 064261107954dd64d03f94d6aeffd95cd2211df0
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: bump etcher-sdk to 9.0.9"
|
||||||
|
hash: 2f4a12a48facf0634ed457fe6ed7c50e21b419ee
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.12
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-25T16:47:43.024Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: setup wdio and port (most) tests"
|
||||||
|
hash: a661d102bc94bf2707f01958d1e9d260efc06c14
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.11
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-25T13:00:13.805Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: remove node-ipc and tests"
|
||||||
|
hash: ccc31bb9aaba8df88b2af612824d9106051e2804
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: switch api; use ws; integrate sudo-prompt - switch api roles
|
||||||
|
flow - use websocket instead of node-ipc - integrate; modernize;
|
||||||
|
simplify and deprecate sudo-prompt"
|
||||||
|
hash: b3e33824ed1f70719b04f18dcb7f7dd76451b7f6
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: refactor api to use a single topic"
|
||||||
|
hash: 6582260355fcc5280932bee771602fbfb5190619
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
- subject: "patch: set require node engine to 20"
|
||||||
|
hash: b1d2bdaa06bfb35f4a66d92275ca21c731d1cf8e
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.10
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-23T10:28:00.127Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: prevent rebuild of native deps by @electron/rebuild"
|
||||||
|
hash: 003abfb88f2c7bff0ee291828f3815c738340afa
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.9
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-22T10:20:10.534Z
|
||||||
|
- commits:
|
||||||
|
- subject: "patch: replace deprecated pkg with yao-pkg and bump etcher-util node v
|
||||||
|
to 20.10"
|
||||||
|
hash: c696c389c9988c75ad9ccc472bdac7edefe762ed
|
||||||
|
body: ""
|
||||||
|
footer: {}
|
||||||
|
author: Edwin Joassart
|
||||||
|
nested: []
|
||||||
|
version: 1.19.8
|
||||||
|
title: ""
|
||||||
|
date: 2024-04-22T09:37:37.561Z
|
||||||
- commits:
|
- commits:
|
||||||
- subject: "patch: fix formating"
|
- subject: "patch: fix formating"
|
||||||
hash: 1a9a3d2cdc5642a754b73628f4ae2636e3ffd8eb
|
hash: 1a9a3d2cdc5642a754b73628f4ae2636e3ffd8eb
|
||||||
@ -981,13 +1307,10 @@
|
|||||||
nested: []
|
nested: []
|
||||||
- subject: "Patch: run linux build on ubuntu-20.04"
|
- subject: "Patch: run linux build on ubuntu-20.04"
|
||||||
hash: adcd8e0325bc891460b3e51aa5403f8675189f13
|
hash: adcd8e0325bc891460b3e51aa5403f8675189f13
|
||||||
body: >-
|
body: |-
|
||||||
as [`18.04` has been
|
as [`18.04` has been removed](https://github.blog/changelog/2022-08-09-github-actions-the-ubuntu-18-04-actions-runner-image-is-being-deprecated-and-will-be-removed-by-12-1-22/)
|
||||||
removed](https://github.blog/changelog/2022-08-09-github-actions-the-ubuntu-18-04-actions-runner-image-is-being-deprecated-and-will-be-removed-by-12-1-22/)
|
|
||||||
|
|
||||||
|
We cannot use `latest` as the glibc version will cause issue with older ubuntu version.
|
||||||
We cannot use `latest` as the glibc version will cause issue with older
|
|
||||||
ubuntu version.
|
|
||||||
footer: {}
|
footer: {}
|
||||||
author: Edwin Joassart
|
author: Edwin Joassart
|
||||||
nested: []
|
nested: []
|
||||||
@ -11732,40 +12055,19 @@
|
|||||||
changelog-entry: Don't include user paths in Mixpanel usage reports
|
changelog-entry: Don't include user paths in Mixpanel usage reports
|
||||||
link: https://github.com/resin-io-modules/etcher-image-stream/blob/master/CHANGELOG.md
|
link: https://github.com/resin-io-modules/etcher-image-stream/blob/master/CHANGELOG.md
|
||||||
subject: Fix uncaught exception if no file was selected from a dialog
|
subject: Fix uncaught exception if no file was selected from a dialog
|
||||||
body: >-
|
body: |-
|
||||||
The following error is thrown if the open file dialog is cancelled
|
The following error is thrown if the open file dialog is cancelled
|
||||||
|
|
||||||
without any selection:
|
without any selection:
|
||||||
|
|
||||||
Unhandled rejection TypeError: Cannot read property '0' of undefined
|
Unhandled rejection TypeError: Cannot read property '0' of undefined
|
||||||
|
at Number.indexedGetter (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/call_get.js:106:15)
|
||||||
at Number.indexedGetter
|
at Number.tryCatcher (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/util.js:16:23)
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/call_get.js:106:15)
|
at Promise._settlePromiseFromHandler (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:503:31)
|
||||||
|
at Promise._settlePromise (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:560:18)
|
||||||
at Number.tryCatcher
|
at Promise._settlePromise0 (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:605:10)
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/util.js:16:23)
|
at Promise._settlePromises (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:684:18)
|
||||||
|
at Async._drainQueue (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:126:16)
|
||||||
at Promise._settlePromiseFromHandler
|
at Async._drainQueues (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:136:10)
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:503:31)
|
at Immediate.Async.drainQueues [as _onImmediate] (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:16:14)
|
||||||
|
|
||||||
at Promise._settlePromise
|
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:560:18)
|
|
||||||
|
|
||||||
at Promise._settlePromise0
|
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:605:10)
|
|
||||||
|
|
||||||
at Promise._settlePromises
|
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:684:18)
|
|
||||||
|
|
||||||
at Async._drainQueue
|
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:126:16)
|
|
||||||
|
|
||||||
at Async._drainQueues
|
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:136:10)
|
|
||||||
|
|
||||||
at Immediate.Async.drainQueues [as _onImmediate]
|
|
||||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:16:14)
|
|
||||||
|
|
||||||
at processImmediate [as _immediateCallback] (timers.js:383:17)
|
at processImmediate [as _immediateCallback] (timers.js:383:17)
|
||||||
- hash: 6bd086f1c5c6654a47125cf2d46788655cae2553
|
- hash: 6bd086f1c5c6654a47125cf2d46788655cae2553
|
||||||
author: Juan Cruz Viotti
|
author: Juan Cruz Viotti
|
||||||
@ -12362,21 +12664,14 @@
|
|||||||
changelog-entry: Use info icon instead of "SHOW FULL FILE NAME" in first step.
|
changelog-entry: Use info icon instead of "SHOW FULL FILE NAME" in first step.
|
||||||
fixes: https://github.com/resin-io/etcher/issues/458
|
fixes: https://github.com/resin-io/etcher/issues/458
|
||||||
subject: Make use of AppImage desktop integration script
|
subject: Make use of AppImage desktop integration script
|
||||||
body: >-
|
body: |-
|
||||||
This is useful to prompt the user to install the `.desktop` file.
|
This is useful to prompt the user to install the `.desktop` file.
|
||||||
|
|
||||||
The `Description` key in `Etcher.desktop` was changed to `Comment` since
|
The `Description` key in `Etcher.desktop` was changed to `Comment` since
|
||||||
|
|
||||||
`desktop-file-validate` complained with:
|
`desktop-file-validate` complained with:
|
||||||
|
|
||||||
Etcher.desktop: error: file contains key "Description" in group "Desktop
|
Etcher.desktop: error: file contains key "Description" in group "Desktop
|
||||||
|
|
||||||
Entry", but keys extending the format should start with "X-"
|
Entry", but keys extending the format should start with "X-"
|
||||||
|
|
||||||
After checking the desktop file format specification, the correct key
|
After checking the desktop file format specification, the correct key
|
||||||
|
|
||||||
should be "Comment"
|
should be "Comment"
|
||||||
|
|
||||||
(https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html).
|
(https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html).
|
||||||
- hash: c3e360e61933ef0044c005b5e92c879ff9a47c49
|
- hash: c3e360e61933ef0044c005b5e92c879ff9a47c49
|
||||||
author: Juan Cruz Viotti
|
author: Juan Cruz Viotti
|
||||||
@ -12589,17 +12884,12 @@
|
|||||||
changelog-entry: Fix flashing never starting after elevation in GNU/Linux.
|
changelog-entry: Fix flashing never starting after elevation in GNU/Linux.
|
||||||
fixes: https://github.com/resin-io/etcher/issues/665
|
fixes: https://github.com/resin-io/etcher/issues/665
|
||||||
subject: Make all angular modules export the name of the module
|
subject: Make all angular modules export the name of the module
|
||||||
body: >-
|
body: |-
|
||||||
This makes them very nicely require-able, for example:
|
This makes them very nicely require-able, for example:
|
||||||
|
|
||||||
angular.module('MyModule', [
|
angular.module('MyModule', [
|
||||||
|
|
||||||
require('my-dependency');
|
require('my-dependency');
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
From https://medium.com/@kentcdodds/how-to-distribute-your-angularjs-module-e04d4dd58ddc#.yqg2zo8im
|
||||||
From
|
|
||||||
https://medium.com/@kentcdodds/how-to-distribute-your-angularjs-module-e04d4dd58ddc#.yqg2zo8im
|
|
||||||
- hash: b8f63af3f81bca3abd055303bc91ab35eb126655
|
- hash: b8f63af3f81bca3abd055303bc91ab35eb126655
|
||||||
author: Juan Cruz Viotti
|
author: Juan Cruz Viotti
|
||||||
footers:
|
footers:
|
||||||
|
127
CHANGELOG.md
127
CHANGELOG.md
@ -3,6 +3,133 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
# v2.1.3
|
||||||
|
## (2025-05-15)
|
||||||
|
|
||||||
|
* Remove stale secrets [Anton Belodedenko]
|
||||||
|
|
||||||
|
# v2.1.2
|
||||||
|
## (2025-05-08)
|
||||||
|
|
||||||
|
* patch: remove analytics [Edwin Joassart]
|
||||||
|
|
||||||
|
# v2.1.1
|
||||||
|
## (2025-05-05)
|
||||||
|
|
||||||
|
* patch: fix signin windows artifacts [Edwin Joassart]
|
||||||
|
|
||||||
|
# v2.1.0
|
||||||
|
## (2025-02-27)
|
||||||
|
|
||||||
|
* Add informational notice about how to disable analytics collection [myarmolinsky]
|
||||||
|
|
||||||
|
# v2.0.0
|
||||||
|
## (2025-02-20)
|
||||||
|
|
||||||
|
* major: build on ubuntu 22 and macos 13 [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.25
|
||||||
|
## (2024-10-10)
|
||||||
|
|
||||||
|
* patch: bump etcher-sdk to 9.1.2 [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.24
|
||||||
|
## (2024-10-09)
|
||||||
|
|
||||||
|
* patch: etcher-util is corrupted in RPM package [Richard Glidden]
|
||||||
|
|
||||||
|
# v1.19.23
|
||||||
|
## (2024-10-09)
|
||||||
|
|
||||||
|
* patch: remove gconf2 libgconf-2-4 deps [Marc-Aurèle Brothier]
|
||||||
|
|
||||||
|
# v1.19.22
|
||||||
|
## (2024-07-18)
|
||||||
|
|
||||||
|
* Replace deprecated Flowzone inputs [Kyle Harding]
|
||||||
|
|
||||||
|
# v1.19.21
|
||||||
|
## (2024-05-30)
|
||||||
|
|
||||||
|
* patch: fix missing windows dependency [Edwin Joassart]
|
||||||
|
* patch: fix missing windows dependency [Edwin Joassart]
|
||||||
|
* patch: fix missing windows dependency [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.20
|
||||||
|
## (2024-05-30)
|
||||||
|
|
||||||
|
* patch: fix missing windows dependency [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.19
|
||||||
|
## (2024-05-28)
|
||||||
|
|
||||||
|
* patch: add sentry debug flag [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.18
|
||||||
|
## (2024-05-22)
|
||||||
|
|
||||||
|
* patch: fix Sentry DSN for main process [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.17
|
||||||
|
## (2024-05-09)
|
||||||
|
|
||||||
|
* patch: fix injection of analytics key at build time [JOASSART Edwin]
|
||||||
|
|
||||||
|
# v1.19.16
|
||||||
|
## (2024-04-26)
|
||||||
|
|
||||||
|
* patch: hold request for metadata while waiting for flasher [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.15
|
||||||
|
## (2024-04-26)
|
||||||
|
|
||||||
|
* patch: bump etcher-sdk to 9.0.11 to fix url loading using http/2 [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.14
|
||||||
|
## (2024-04-25)
|
||||||
|
|
||||||
|
* patch: pretty-bytes to 6.1.1 [JOASSART Edwin]
|
||||||
|
|
||||||
|
# v1.19.13
|
||||||
|
## (2024-04-25)
|
||||||
|
|
||||||
|
* patch: use etcher icon as loading for windows installer [Edwin Joassart]
|
||||||
|
* patch: fix windows squirrel install [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.12
|
||||||
|
## (2024-04-25)
|
||||||
|
|
||||||
|
* patch: bump minors & patch [Edwin Joassart]
|
||||||
|
* patch: bump @electron-forge/* to 7.4.0 [Edwin Joassart]
|
||||||
|
* patch: bump electron to 30.0.1 & @electron/remote to 2.1.2 [Edwin Joassart]
|
||||||
|
* patch: npm upgrade [Edwin Joassart]
|
||||||
|
* patch: bump @balena/lint to 8.0.2 and fix formating [Edwin Joassart]
|
||||||
|
* patch: fix pretty-bytes imports [Edwin Joassart]
|
||||||
|
* patch: bump etcher-sdk to 9.0.9 [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.11
|
||||||
|
## (2024-04-25)
|
||||||
|
|
||||||
|
* patch: setup wdio and port (most) tests [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.10
|
||||||
|
## (2024-04-23)
|
||||||
|
|
||||||
|
* patch: remove node-ipc and tests [Edwin Joassart]
|
||||||
|
* patch: switch api; use ws; integrate sudo-prompt - switch api roles flow - use websocket instead of node-ipc - integrate; modernize; simplify and deprecate sudo-prompt [Edwin Joassart]
|
||||||
|
* patch: refactor api to use a single topic [Edwin Joassart]
|
||||||
|
* patch: set require node engine to 20 [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.9
|
||||||
|
## (2024-04-22)
|
||||||
|
|
||||||
|
* patch: prevent rebuild of native deps by @electron/rebuild [Edwin Joassart]
|
||||||
|
|
||||||
|
# v1.19.8
|
||||||
|
## (2024-04-22)
|
||||||
|
|
||||||
|
* patch: replace deprecated pkg with yao-pkg and bump etcher-util node v to 20.10 [Edwin Joassart]
|
||||||
|
|
||||||
# v1.19.7
|
# v1.19.7
|
||||||
## (2024-04-22)
|
## (2024-04-22)
|
||||||
|
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
Maintaining Etcher
|
# Maintaining Etcher
|
||||||
==================
|
|
||||||
|
|
||||||
This document is meant to serve as a guide for maintainers to perform common tasks.
|
This document is meant to serve as a guide for maintainers to perform common tasks.
|
||||||
|
|
||||||
Releasing
|
## Releasing
|
||||||
---------
|
|
||||||
|
|
||||||
### Release Types
|
### Release Types
|
||||||
|
|
||||||
@ -13,16 +11,15 @@ Releasing
|
|||||||
- **release**: Full releases
|
- **release**: Full releases
|
||||||
|
|
||||||
Draft release is created from each PR, tagged with the branch name.
|
Draft release is created from each PR, tagged with the branch name.
|
||||||
All merged PR will generate a new tag/version as a *pre-release*.
|
All merged PR will generate a new tag/version as a _pre-release_.
|
||||||
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
|
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
|
||||||
|
|
||||||
|
|
||||||
#### Preparation
|
#### Preparation
|
||||||
|
|
||||||
- [Prepare the new version](#preparing-a-new-version)
|
- [Prepare the new version](#preparing-a-new-version)
|
||||||
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
|
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
|
||||||
- [Draft a release on GitHub](https://github.com/balena-io/etcher/releases)
|
- [Draft a release on GitHub](https://github.com/balena-io/etcher/releases)
|
||||||
- Upload build artifacts to GitHub release draft
|
- Upload build artifacts to GitHub release draft
|
||||||
|
|
||||||
#### Testing
|
#### Testing
|
||||||
|
|
||||||
@ -35,7 +32,7 @@ Mark the pre-release as final when it is necessary, then distribute the packages
|
|||||||
- [Post release note to forums](https://forums.balena.io/c/etcher)
|
- [Post release note to forums](https://forums.balena.io/c/etcher)
|
||||||
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
|
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
|
||||||
- [Update the website](https://github.com/balena-io/etcher-homepage)
|
- [Update the website](https://github.com/balena-io/etcher-homepage)
|
||||||
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions
|
- Wait 2-3 hours for analytics (Sentry) to trickle in and check for elevated error rates, or regressions
|
||||||
- If regressions arise; pull the release, and release a patched version, else:
|
- If regressions arise; pull the release, and release a patched version, else:
|
||||||
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
|
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
|
||||||
- Post changelog with `#release-notes` tag on internal chat
|
- Post changelog with `#release-notes` tag on internal chat
|
||||||
@ -51,7 +48,6 @@ Make sure to set the analytics tokens when generating production release binarie
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
|
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
|
||||||
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
@ -71,7 +67,6 @@ npm run make
|
|||||||
|
|
||||||
Our CI will appropriately sign artifacts for macOS and some Windows targets.
|
Our CI will appropriately sign artifacts for macOS and some Windows targets.
|
||||||
|
|
||||||
|
|
||||||
### Uploading packages to Cloudfront
|
### Uploading packages to Cloudfront
|
||||||
|
|
||||||
Log in to cloudfront and upload the `rpm` and `deb` files.
|
Log in to cloudfront and upload the `rpm` and `deb` files.
|
||||||
@ -99,7 +94,6 @@ aws s3api delete-object --bucket <bucket name> --key <file name>
|
|||||||
|
|
||||||
The Bintray dashboard provides an easy way to delete a version's files.
|
The Bintray dashboard provides an easy way to delete a version's files.
|
||||||
|
|
||||||
|
|
||||||
### Submitting binaries to Symantec
|
### Submitting binaries to Symantec
|
||||||
|
|
||||||
- [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/)
|
- [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/)
|
||||||
|
@ -1,22 +1,19 @@
|
|||||||
Manual Testing
|
# Manual Testing
|
||||||
==============
|
|
||||||
|
|
||||||
This document describes a high-level script of manual tests to check for. We
|
This document describes a high-level script of manual tests to check for. We
|
||||||
should aim to replace items on this list with automated Spectron test cases.
|
should aim to replace items on this list with automated Spectron test cases.
|
||||||
|
|
||||||
Image Selection
|
## Image Selection
|
||||||
---------------
|
|
||||||
|
|
||||||
- [ ] Cancel image selection dialog
|
- [ ] Cancel image selection dialog
|
||||||
- [ ] Select an unbootable image (without a partition table), and expect a
|
- [ ] Select an unbootable image (without a partition table), and expect a
|
||||||
sensible warning
|
sensible warning
|
||||||
- [ ] Attempt to select a ZIP archive with more than one image
|
- [ ] Attempt to select a ZIP archive with more than one image
|
||||||
- [ ] Attempt to select a tar archive (with any compression method)
|
- [ ] Attempt to select a tar archive (with any compression method)
|
||||||
- [ ] Change image selection
|
- [ ] Change image selection
|
||||||
- [ ] Select a Windows image, and expect a sensible warning
|
- [ ] Select a Windows image, and expect a sensible warning
|
||||||
|
|
||||||
Drive Selection
|
## Drive Selection
|
||||||
---------------
|
|
||||||
|
|
||||||
- [ ] Open the drive selection modal
|
- [ ] Open the drive selection modal
|
||||||
- [ ] Switch drive selection
|
- [ ] Switch drive selection
|
||||||
@ -25,16 +22,15 @@ Drive Selection
|
|||||||
- [ ] Insert a locked SD Card and expect a warning
|
- [ ] Insert a locked SD Card and expect a warning
|
||||||
- [ ] Insert a too small drive and expect a warning
|
- [ ] Insert a too small drive and expect a warning
|
||||||
- [ ] Put an image into a drive and attempt to flash the image to the drive
|
- [ ] Put an image into a drive and attempt to flash the image to the drive
|
||||||
that contains it
|
that contains it
|
||||||
- [ ] Attempt to flash a compressed image (for which we can get the
|
- [ ] Attempt to flash a compressed image (for which we can get the
|
||||||
uncompressed size) into a drive that is big enough to hold the compressed
|
uncompressed size) into a drive that is big enough to hold the compressed
|
||||||
image, but not big enough to hold the uncompressed version
|
image, but not big enough to hold the uncompressed version
|
||||||
- [ ] Enable "Unsafe Mode" and attempt to select a system drive
|
- [ ] Enable "Unsafe Mode" and attempt to select a system drive
|
||||||
- [ ] Enable "Unsafe Mode", and if there is only one system drive (and no
|
- [ ] Enable "Unsafe Mode", and if there is only one system drive (and no
|
||||||
removable ones), don't expect autoselection
|
removable ones), don't expect autoselection
|
||||||
|
|
||||||
Image Support
|
## Image Support
|
||||||
-------------
|
|
||||||
|
|
||||||
Run the following tests with and without validation enabled:
|
Run the following tests with and without validation enabled:
|
||||||
|
|
||||||
@ -51,18 +47,17 @@ Run the following tests with and without validation enabled:
|
|||||||
- [ ] Flash an archive image containing a blockmap file
|
- [ ] Flash an archive image containing a blockmap file
|
||||||
- [ ] Flash an archive image containing a manifest metadata file
|
- [ ] Flash an archive image containing a manifest metadata file
|
||||||
|
|
||||||
Flashing Process
|
## Flashing Process
|
||||||
----------------
|
|
||||||
|
|
||||||
- [ ] Unplug the drive during flash or validation
|
- [ ] Unplug the drive during flash or validation
|
||||||
- [ ] Click "Flash", cancel elevation dialog, and click "Flash" again
|
- [ ] Click "Flash", cancel elevation dialog, and click "Flash" again
|
||||||
- [ ] Start flashing an image, try to close Etcher, cancel the application
|
- [ ] Start flashing an image, try to close Etcher, cancel the application
|
||||||
close warning dialog, and check that Etcher continues to flash the image
|
close warning dialog, and check that Etcher continues to flash the image
|
||||||
|
|
||||||
### Child Writer
|
### Child Writer
|
||||||
|
|
||||||
- [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and
|
- [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and
|
||||||
check that the UI reacts appropriately
|
check that the UI reacts appropriately
|
||||||
- [ ] Close the application while flashing using the window manager close icon
|
- [ ] Close the application while flashing using the window manager close icon
|
||||||
- [ ] Close the application while flashing using the OS keyboard shortcut
|
- [ ] Close the application while flashing using the OS keyboard shortcut
|
||||||
- [ ] Close the application from the terminal using Ctrl-C while flashing
|
- [ ] Close the application from the terminal using Ctrl-C while flashing
|
||||||
@ -72,11 +67,10 @@ In all these cases, the child writer process should not remain alive. Note that
|
|||||||
in some systems you need to open your process monitor tool of choice with extra
|
in some systems you need to open your process monitor tool of choice with extra
|
||||||
permissions to see the elevated child writer process.
|
permissions to see the elevated child writer process.
|
||||||
|
|
||||||
GUI
|
## GUI
|
||||||
----
|
|
||||||
|
|
||||||
- [ ] Close application from the terminal using Ctrl-C while the application is
|
- [ ] Close application from the terminal using Ctrl-C while the application is
|
||||||
idle
|
idle
|
||||||
- [ ] Click footer links that take you to an external website
|
- [ ] Click footer links that take you to an external website
|
||||||
- [ ] Attempt to change image or drive selection while flashing
|
- [ ] Attempt to change image or drive selection while flashing
|
||||||
- [ ] Go to the settings page while flashing and come back
|
- [ ] Go to the settings page while flashing and come back
|
||||||
@ -85,31 +79,20 @@ GUI
|
|||||||
- [ ] Minimize the application
|
- [ ] Minimize the application
|
||||||
- [ ] Start the application given no internet connection
|
- [ ] Start the application given no internet connection
|
||||||
|
|
||||||
Success Banner
|
## Success Banner
|
||||||
--------------
|
|
||||||
|
|
||||||
- [ ] Click an external link on the success banner (with and without internet
|
- [ ] Click an external link on the success banner (with and without internet
|
||||||
connection)
|
connection)
|
||||||
|
|
||||||
Elevation Prompt
|
## Elevation Prompt
|
||||||
----------------
|
|
||||||
|
|
||||||
- [ ] Flash an image as `root`/administrator
|
- [ ] Flash an image as `root`/administrator
|
||||||
- [ ] Reject elevation prompt
|
- [ ] Reject elevation prompt
|
||||||
- [ ] Put incorrect elevation prompt password
|
- [ ] Put incorrect elevation prompt password
|
||||||
- [ ] Unplug the drive during elevation
|
- [ ] Unplug the drive during elevation
|
||||||
|
|
||||||
Unmounting
|
## Unmounting
|
||||||
----------
|
|
||||||
|
|
||||||
- [ ] Disable unmounting and flash an image
|
- [ ] Disable unmounting and flash an image
|
||||||
- [ ] Flash an image with a file system that is readable by the host OS, and
|
- [ ] Flash an image with a file system that is readable by the host OS, and
|
||||||
check that is unmounted correctly
|
check that is unmounted correctly
|
||||||
|
|
||||||
Analytics
|
|
||||||
---------
|
|
||||||
|
|
||||||
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
|
|
||||||
check that no request is sent
|
|
||||||
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
|
|
||||||
F5), and check that initial events are not sent to Amplitude**
|
|
||||||
|
@ -122,7 +122,6 @@ run Etcher on a GNU/Linux system.
|
|||||||
- xrender
|
- xrender
|
||||||
- xtst
|
- xtst
|
||||||
- xscrnsaver
|
- xscrnsaver
|
||||||
- gconf-2.0
|
|
||||||
- gmodule-2.0
|
- gmodule-2.0
|
||||||
- nss
|
- nss
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { MakerDMG } from '@electron-forge/maker-dmg';
|
|||||||
import { MakerAppImage } from '@reforged/maker-appimage';
|
import { MakerAppImage } from '@reforged/maker-appimage';
|
||||||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
|
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
|
||||||
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
|
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
|
||||||
import { mainConfig, rendererConfig } from './webpack.config';
|
import { mainConfig, rendererConfig } from './webpack.config';
|
||||||
import * as sidecar from './forge.sidecar';
|
import * as sidecar from './forge.sidecar';
|
||||||
@ -41,8 +42,8 @@ const config: ForgeConfig = {
|
|||||||
darwinDarkModeSupport: true,
|
darwinDarkModeSupport: true,
|
||||||
protocols: [{ name: 'etcher', schemes: ['etcher'] }],
|
protocols: [{ name: 'etcher', schemes: ['etcher'] }],
|
||||||
extraResource: [
|
extraResource: [
|
||||||
'lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js',
|
'lib/shared/sudo/sudo-askpass.osascript-zh.js',
|
||||||
'lib/shared/catalina-sudo/sudo-askpass.osascript-en.js',
|
'lib/shared/sudo/sudo-askpass.osascript-en.js',
|
||||||
],
|
],
|
||||||
osxSign: {
|
osxSign: {
|
||||||
optionsForFile: () => ({
|
optionsForFile: () => ({
|
||||||
@ -52,11 +53,14 @@ const config: ForgeConfig = {
|
|||||||
},
|
},
|
||||||
...osxSigningConfig,
|
...osxSigningConfig,
|
||||||
},
|
},
|
||||||
rebuildConfig: {},
|
rebuildConfig: {
|
||||||
|
onlyModules: [], // prevent rebuilding *any* native modules as they won't be used by electron but by the sidecar
|
||||||
|
},
|
||||||
makers: [
|
makers: [
|
||||||
new MakerZIP(),
|
new MakerZIP(),
|
||||||
new MakerSquirrel({
|
new MakerSquirrel({
|
||||||
setupIcon: 'assets/icon.ico',
|
setupIcon: 'assets/icon.ico',
|
||||||
|
loadingGif: 'assets/icon.png',
|
||||||
...winSigningConfig,
|
...winSigningConfig,
|
||||||
}),
|
}),
|
||||||
new MakerDMG({
|
new MakerDMG({
|
||||||
@ -132,24 +136,22 @@ const config: ForgeConfig = {
|
|||||||
new sidecar.SidecarPlugin(),
|
new sidecar.SidecarPlugin(),
|
||||||
],
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
readPackageJson: async (_config, packageJson) => {
|
postPackage: async (_forgeConfig, options) => {
|
||||||
packageJson.analytics = {};
|
if (options.platform === 'linux') {
|
||||||
|
// symlink the etcher binary from balena-etcher to balenaEtcher to ensure compatibility with the wdio suite and the old name
|
||||||
if (process.env.SENTRY_TOKEN) {
|
await new Promise<void>((resolve, reject) => {
|
||||||
packageJson.analytics.sentry = {
|
exec(
|
||||||
token: process.env.SENTRY_TOKEN,
|
`ln -s "${options.outputPaths}/balena-etcher" "${options.outputPaths}/balenaEtcher"`,
|
||||||
};
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.AMPLITUDE_TOKEN) {
|
|
||||||
packageJson.analytics.amplitude = {
|
|
||||||
token: 'balena-etcher',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// packageJson.packageType = 'dmg' | 'AppImage' | 'rpm' | 'deb' | 'zip' | 'nsis' | 'portable'
|
|
||||||
|
|
||||||
return packageJson;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { PluginBase } from '@electron-forge/plugin-base';
|
import { PluginBase } from '@electron-forge/plugin-base';
|
||||||
import {
|
import type {
|
||||||
ForgeHookMap,
|
ForgeHookMap,
|
||||||
ResolvedForgeConfig,
|
ResolvedForgeConfig,
|
||||||
} from '@electron-forge/shared-types';
|
} from '@electron-forge/shared-types';
|
||||||
@ -90,7 +90,7 @@ function build(
|
|||||||
// always build for host platform and node version
|
// always build for host platform and node version
|
||||||
// https://github.com/vercel/pkg-fetch/releases
|
// https://github.com/vercel/pkg-fetch/releases
|
||||||
'--target',
|
'--target',
|
||||||
`node18-${arch}`,
|
`node20-${arch}`,
|
||||||
'--output',
|
'--output',
|
||||||
binPath,
|
binPath,
|
||||||
],
|
],
|
||||||
|
@ -16,14 +16,15 @@
|
|||||||
|
|
||||||
import * as electron from 'electron';
|
import * as electron from 'electron';
|
||||||
import * as remote from '@electron/remote';
|
import * as remote from '@electron/remote';
|
||||||
import { debounce, capitalize, Dictionary, values } from 'lodash';
|
import type { Dictionary } from 'lodash';
|
||||||
|
import { debounce, capitalize, values } from 'lodash';
|
||||||
import outdent from 'outdent';
|
import outdent from 'outdent';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDOM from 'react-dom';
|
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 { DrivelistDrive } from '../../shared/drive-constraints';
|
import type { DrivelistDrive } 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';
|
||||||
@ -31,14 +32,14 @@ import * as flashState from './models/flash-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';
|
||||||
import { startApiAndSpawnChild } from './modules/api';
|
import { spawnChildAndConnect } from './modules/api';
|
||||||
import * as exceptionReporter from './modules/exception-reporter';
|
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';
|
import './css/main.css';
|
||||||
import * as i18next from 'i18next';
|
import * as i18next from 'i18next';
|
||||||
import { SourceMetadata } from '../../shared/typings/source-selector';
|
import type { SourceMetadata } from '../../shared/typings/source-selector';
|
||||||
|
|
||||||
window.addEventListener(
|
window.addEventListener(
|
||||||
'unhandledrejection',
|
'unhandledrejection',
|
||||||
@ -63,9 +64,6 @@ store.dispatch({
|
|||||||
data: uuidV4(),
|
data: uuidV4(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid;
|
|
||||||
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid;
|
|
||||||
|
|
||||||
console.log(outdent`
|
console.log(outdent`
|
||||||
${outdent}
|
${outdent}
|
||||||
_____ _ _
|
_____ _ _
|
||||||
@ -81,13 +79,6 @@ console.log(outdent`
|
|||||||
Version = ${packageJSON.version}, Type = ${packageJSON.packageType}
|
Version = ${packageJSON.version}, Type = ${packageJSON.packageType}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const currentVersion = packageJSON.version;
|
|
||||||
|
|
||||||
analytics.logEvent('Application start', {
|
|
||||||
packageType: packageJSON.packageType,
|
|
||||||
version: currentVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 });
|
const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 });
|
||||||
|
|
||||||
function pluralize(word: string, quantity: number) {
|
function pluralize(word: string, quantity: number) {
|
||||||
@ -139,27 +130,31 @@ function setDrives(drives: Dictionary<DrivelistDrive>) {
|
|||||||
export let requestMetadata: any;
|
export let requestMetadata: any;
|
||||||
|
|
||||||
// start the api and spawn the child process
|
// start the api and spawn the child process
|
||||||
startApiAndSpawnChild({
|
spawnChildAndConnect({
|
||||||
withPrivileges: false,
|
withPrivileges: false,
|
||||||
}).then(({ emit, registerHandler }) => {
|
})
|
||||||
// start scanning
|
.then(({ emit, registerHandler }) => {
|
||||||
emit('scan');
|
// start scanning
|
||||||
|
emit('scan', {});
|
||||||
|
|
||||||
// make the sourceMetada awaitable to be used on source selection
|
// make the sourceMetada awaitable to be used on source selection
|
||||||
requestMetadata = async (params: any): Promise<SourceMetadata> => {
|
requestMetadata = async (params: any): Promise<SourceMetadata> => {
|
||||||
emit('sourceMetadata', JSON.stringify(params));
|
emit('sourceMetadata', JSON.stringify(params));
|
||||||
|
|
||||||
return new Promise((resolve) =>
|
return new Promise((resolve) =>
|
||||||
registerHandler('sourceMetadata', (data: any) => {
|
registerHandler('sourceMetadata', (data: any) => {
|
||||||
resolve(JSON.parse(data));
|
resolve(JSON.parse(data));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
registerHandler('drives', (data: any) => {
|
registerHandler('drives', (data: any) => {
|
||||||
setDrives(JSON.parse(data));
|
setDrives(JSON.parse(data));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
throw new Error(`Failed to start the flasher process. error: ${error}`);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
let popupExists = false;
|
let popupExists = false;
|
||||||
|
|
||||||
@ -167,9 +162,6 @@ analytics.initAnalytics();
|
|||||||
|
|
||||||
window.addEventListener('beforeunload', async (event) => {
|
window.addEventListener('beforeunload', async (event) => {
|
||||||
if (!flashState.isFlashing() || popupExists) {
|
if (!flashState.isFlashing() || popupExists) {
|
||||||
analytics.logEvent('Close application', {
|
|
||||||
isFlashing: flashState.isFlashing(),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,8 +171,6 @@ window.addEventListener('beforeunload', async (event) => {
|
|||||||
// Don't open any more popups
|
// Don't open any more popups
|
||||||
popupExists = true;
|
popupExists = true;
|
||||||
|
|
||||||
analytics.logEvent('Close attempt while flashing');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const confirmed = await osDialog.showWarning({
|
const confirmed = await osDialog.showWarning({
|
||||||
confirmationLabel: i18next.t('yesExit'),
|
confirmationLabel: i18next.t('yesExit'),
|
||||||
@ -189,19 +179,11 @@ window.addEventListener('beforeunload', async (event) => {
|
|||||||
description: messages.warning.exitWhileFlashing(),
|
description: messages.warning.exitWhileFlashing(),
|
||||||
});
|
});
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
analytics.logEvent('Close confirmed while flashing', {
|
|
||||||
flashInstanceUuid: flashState.getFlashUuid(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// This circumvents the 'beforeunload' event unlike
|
// This circumvents the 'beforeunload' event unlike
|
||||||
// remote.app.quit() which does not.
|
// remote.app.quit() which does not.
|
||||||
remote.process.exit(EXIT_CODES.SUCCESS);
|
remote.process.exit(EXIT_CODES.SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
analytics.logEvent('Close rejected while flashing', {
|
|
||||||
applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid,
|
|
||||||
});
|
|
||||||
popupExists = false;
|
popupExists = false;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
exceptionReporter.report(error);
|
exceptionReporter.report(error);
|
||||||
|
@ -16,33 +16,32 @@
|
|||||||
|
|
||||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||||
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 type * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
|
import type { ModalProps, TableColumn } from 'rendition';
|
||||||
|
import { Flex, Txt, Badge, Link } from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
DriveStatus,
|
||||||
|
DrivelistDrive,
|
||||||
|
} from '../../../../shared/drive-constraints';
|
||||||
import {
|
import {
|
||||||
getDriveImageCompatibilityStatuses,
|
getDriveImageCompatibilityStatuses,
|
||||||
isDriveValid,
|
isDriveValid,
|
||||||
DriveStatus,
|
|
||||||
DrivelistDrive,
|
|
||||||
isDriveSizeLarge,
|
isDriveSizeLarge,
|
||||||
} 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 prettyBytes from 'pretty-bytes';
|
||||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||||
import { getImage, isDriveSelected } from '../../models/selection-state';
|
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||||
import { store } from '../../models/store';
|
import { store } from '../../models/store';
|
||||||
import { logEvent, logException } from '../../modules/analytics';
|
import { 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 {
|
import type { GenericTableProps } from '../../styled-components';
|
||||||
Alert,
|
import { Alert, Modal, Table } from '../../styled-components';
|
||||||
GenericTableProps,
|
|
||||||
Modal,
|
|
||||||
Table,
|
|
||||||
} from '../../styled-components';
|
|
||||||
|
|
||||||
import { SourceMetadata } from '../../../../shared/typings/source-selector';
|
import type { SourceMetadata } from '../../../../shared/typings/source-selector';
|
||||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
import * as i18next from 'i18next';
|
import * as i18next from 'i18next';
|
||||||
|
|
||||||
@ -356,9 +355,6 @@ export class DriveSelector extends React.Component<
|
|||||||
|
|
||||||
private installMissingDrivers(drive: DriverlessDrive) {
|
private installMissingDrivers(drive: DriverlessDrive) {
|
||||||
if (drive.link) {
|
if (drive.link) {
|
||||||
logEvent('Open driver link modal', {
|
|
||||||
url: drive.link,
|
|
||||||
});
|
|
||||||
this.setState({ missingDriversModal: { drive } });
|
this.setState({ missingDriversModal: { drive } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge, Flex, Txt, ModalProps } from 'rendition';
|
import type { ModalProps } from 'rendition';
|
||||||
|
import { Badge, Flex, Txt } from 'rendition';
|
||||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import { DriveWithWarnings } from '../../pages/main/Flash';
|
import type { DriveWithWarnings } from '../../pages/main/Flash';
|
||||||
import * as i18next from 'i18next';
|
import * as i18next from 'i18next';
|
||||||
|
|
||||||
const DriveStatusWarningModal = ({
|
const DriveStatusWarningModal = ({
|
||||||
|
@ -22,14 +22,13 @@ 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 * as settings from '../../models/settings';
|
||||||
import { Actions, store } from '../../models/store';
|
import { Actions, store } from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
|
||||||
import { FlashAnother } from '../flash-another/flash-another';
|
import { FlashAnother } from '../flash-another/flash-another';
|
||||||
import { FlashResults, FlashError } from '../flash-results/flash-results';
|
import type { FlashError } from '../flash-results/flash-results';
|
||||||
|
import { FlashResults } from '../flash-results/flash-results';
|
||||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||||
|
|
||||||
function restart(goToMain: () => void) {
|
function restart(goToMain: () => void) {
|
||||||
selectionState.deselectAllDrives();
|
selectionState.deselectAllDrives();
|
||||||
analytics.logEvent('Restart');
|
|
||||||
|
|
||||||
// Reset the flashing workflow uuid
|
// Reset the flashing workflow uuid
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
|
@ -18,7 +18,8 @@ import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
|||||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg';
|
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg';
|
||||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg';
|
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
import type { FlexProps, TableColumn } from 'rendition';
|
||||||
|
import { Flex, Link, Txt } from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { progress } from '../../../../shared/messages';
|
import { progress } from '../../../../shared/messages';
|
||||||
|
@ -21,7 +21,6 @@ import * as React from 'react';
|
|||||||
|
|
||||||
import * as packageJSON from '../../../../../package.json';
|
import * as packageJSON from '../../../../../package.json';
|
||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
import * as analytics from '../../modules/analytics';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Electron session identifier
|
* @summary Electron session identifier
|
||||||
@ -196,10 +195,6 @@ export class SafeWebview extends React.PureComponent<
|
|||||||
// only care about this event if it's a request for the main frame
|
// only care about this event if it's a request for the main frame
|
||||||
if (event.resourceType === 'mainFrame') {
|
if (event.resourceType === 'mainFrame') {
|
||||||
const HTTP_OK = 200;
|
const HTTP_OK = 200;
|
||||||
const { webContents, ...webviewEvent } = event;
|
|
||||||
analytics.logEvent('SafeWebview loaded', {
|
|
||||||
...webviewEvent,
|
|
||||||
});
|
|
||||||
this.setState({
|
this.setState({
|
||||||
shouldShow: event.statusCode === HTTP_OK,
|
shouldShow: event.statusCode === HTTP_OK,
|
||||||
});
|
});
|
||||||
|
@ -21,7 +21,6 @@ import { Box, Checkbox, Flex, 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';
|
||||||
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';
|
||||||
import * as i18next from 'i18next';
|
import * as i18next from 'i18next';
|
||||||
@ -89,7 +88,6 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
|||||||
|
|
||||||
const toggleSetting = async (setting: string) => {
|
const toggleSetting = async (setting: string) => {
|
||||||
const value = currentSettings[setting];
|
const value = currentSettings[setting];
|
||||||
analytics.logEvent('Toggle setting', { setting, value });
|
|
||||||
await settings.set(setting, !value);
|
await settings.set(setting, !value);
|
||||||
setCurrentSettings({
|
setCurrentSettings({
|
||||||
...currentSettings,
|
...currentSettings,
|
||||||
|
@ -20,16 +20,17 @@ import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
|||||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||||
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import type { IpcRendererEvent } from 'electron';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
import { uniqBy, isNil } from 'lodash';
|
import { uniqBy, isNil } from 'lodash';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { requestMetadata } from '../../app';
|
import { requestMetadata } from '../../app';
|
||||||
|
|
||||||
|
import type { ButtonProps } from 'rendition';
|
||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
ButtonProps,
|
|
||||||
Modal as SmallModal,
|
Modal as SmallModal,
|
||||||
Txt,
|
Txt,
|
||||||
Card as BaseCard,
|
Card as BaseCard,
|
||||||
@ -63,9 +64,9 @@ 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 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 type { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||||
import { isJson } from '../../../../shared/utils';
|
import { isJson } from '../../../../shared/utils';
|
||||||
import {
|
import type {
|
||||||
SourceMetadata,
|
SourceMetadata,
|
||||||
Authentication,
|
Authentication,
|
||||||
Source,
|
Source,
|
||||||
@ -307,6 +308,7 @@ const FlowSelector = styled(
|
|||||||
|
|
||||||
interface SourceSelectorProps {
|
interface SourceSelectorProps {
|
||||||
flashing: boolean;
|
flashing: boolean;
|
||||||
|
hideAnalyticsAlert: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceSelectorState {
|
interface SourceSelectorState {
|
||||||
@ -358,6 +360,20 @@ export class SourceSelector extends React.Component<
|
|||||||
ipcRenderer.removeListener('select-image', this.onSelectImage);
|
ipcRenderer.removeListener('select-image', this.onSelectImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(
|
||||||
|
_prevProps: Readonly<SourceSelectorProps>,
|
||||||
|
prevState: Readonly<SourceSelectorState>,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
(!prevState.showDriveSelector && this.state.showDriveSelector) ||
|
||||||
|
(!prevState.showURLSelector && this.state.showURLSelector) ||
|
||||||
|
(!prevState.showImageDetails && this.state.showImageDetails) ||
|
||||||
|
(!prevState.imageSelectorOpen && this.state.imageSelectorOpen)
|
||||||
|
) {
|
||||||
|
this.props.hideAnalyticsAlert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||||
this.setState({ imageLoading: true });
|
this.setState({ imageLoading: true });
|
||||||
await this.selectSource(
|
await this.selectSource(
|
||||||
@ -376,11 +392,8 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private reselectSource() {
|
private reselectSource() {
|
||||||
analytics.logEvent('Reselect image', {
|
|
||||||
previousImage: selectionState.getImage(),
|
|
||||||
});
|
|
||||||
|
|
||||||
selectionState.deselectImage();
|
selectionState.deselectImage();
|
||||||
|
this.props.hideAnalyticsAlert();
|
||||||
}
|
}
|
||||||
|
|
||||||
private selectSource(
|
private selectSource(
|
||||||
@ -409,7 +422,6 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||||
analytics.logEvent('Possibly Windows image', { image: selected });
|
|
||||||
this.setState({
|
this.setState({
|
||||||
warning: {
|
warning: {
|
||||||
message: messages.warning.looksLikeWindowsImage(),
|
message: messages.warning.looksLikeWindowsImage(),
|
||||||
@ -422,10 +434,17 @@ export class SourceSelector extends React.Component<
|
|||||||
// this will send an event down the ipcMain asking for metadata
|
// this will send an event down the ipcMain asking for metadata
|
||||||
// we'll get the response through an event
|
// we'll get the response through an event
|
||||||
|
|
||||||
|
// FIXME: This is a poor man wait while loading to prevent a potential race condition without completely blocking the interface
|
||||||
|
// This should be addressed when refactoring the GUI
|
||||||
|
let retriesLeft = 10;
|
||||||
|
while (requestMetadata === undefined && retriesLeft > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1050)); // api is trying to connect every 1000, this is offset to make sure we fall between retries
|
||||||
|
retriesLeft--;
|
||||||
|
}
|
||||||
|
|
||||||
metadata = await requestMetadata({ selected, SourceType, auth });
|
metadata = await requestMetadata({ selected, SourceType, auth });
|
||||||
|
|
||||||
if (!metadata?.hasMBR && this.state.warning === null) {
|
if (!metadata?.hasMBR && this.state.warning === null) {
|
||||||
analytics.logEvent('Missing partition table', { metadata });
|
|
||||||
this.setState({
|
this.setState({
|
||||||
warning: {
|
warning: {
|
||||||
message: messages.warning.missingPartitionTable(),
|
message: messages.warning.missingPartitionTable(),
|
||||||
@ -443,7 +462,6 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (selected.partitionTableType === null) {
|
if (selected.partitionTableType === null) {
|
||||||
analytics.logEvent('Missing partition table', { selected });
|
|
||||||
this.setState({
|
this.setState({
|
||||||
warning: {
|
warning: {
|
||||||
message: messages.warning.driveMissingPartitionTable(),
|
message: messages.warning.driveMissingPartitionTable(),
|
||||||
@ -465,15 +483,6 @@ export class SourceSelector extends React.Component<
|
|||||||
metadata.auth = auth;
|
metadata.auth = auth;
|
||||||
metadata.SourceType = SourceType;
|
metadata.SourceType = SourceType;
|
||||||
selectionState.selectSource(metadata);
|
selectionState.selectSource(metadata);
|
||||||
analytics.logEvent('Select image', {
|
|
||||||
// An easy way so we can quickly identify if we're making use of
|
|
||||||
// certain features without printing pages of text to DevTools.
|
|
||||||
image: {
|
|
||||||
...metadata,
|
|
||||||
logo: Boolean(metadata.logo),
|
|
||||||
blockMap: Boolean(metadata.blockMap),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
};
|
};
|
||||||
@ -494,11 +503,9 @@ export class SourceSelector extends React.Component<
|
|||||||
analytics.logException(error);
|
analytics.logException(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
analytics.logEvent(title, { path: sourcePath });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async openImageSelector() {
|
private async openImageSelector() {
|
||||||
analytics.logEvent('Open image selector');
|
|
||||||
this.setState({ imageSelectorOpen: true });
|
this.setState({ imageSelectorOpen: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -506,7 +513,6 @@ export class SourceSelector extends React.Component<
|
|||||||
// 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 (!imagePath) {
|
if (!imagePath) {
|
||||||
analytics.logEvent('Image selector closed');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.selectSource(imagePath, 'File').promise;
|
await this.selectSource(imagePath, 'File').promise;
|
||||||
@ -525,16 +531,12 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private openURLSelector() {
|
private openURLSelector() {
|
||||||
analytics.logEvent('Open image URL selector');
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
showURLSelector: true,
|
showURLSelector: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private openDriveSelector() {
|
private openDriveSelector() {
|
||||||
analytics.logEvent('Open drive selector');
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
showDriveSelector: true,
|
showDriveSelector: true,
|
||||||
});
|
});
|
||||||
@ -551,10 +553,6 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showSelectedImageDetails() {
|
private showSelectedImageDetails() {
|
||||||
analytics.logEvent('Show selected image tooltip', {
|
|
||||||
imagePath: selectionState.getImage()?.path,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
showImageDetails: true,
|
showImageDetails: true,
|
||||||
});
|
});
|
||||||
@ -734,9 +732,7 @@ export class SourceSelector extends React.Component<
|
|||||||
done={async (imageURL: string, auth?: Authentication) => {
|
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) {
|
||||||
analytics.logEvent('URL selector closed');
|
|
||||||
} else {
|
|
||||||
let promise;
|
let promise;
|
||||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||||
imageURL,
|
imageURL,
|
||||||
|
@ -16,14 +16,13 @@
|
|||||||
|
|
||||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, FlexProps, Txt } from 'rendition';
|
import type { FlexProps } from 'rendition';
|
||||||
|
import { Flex, Txt } from 'rendition';
|
||||||
|
|
||||||
import {
|
import type { DriveStatus } from '../../../../shared/drive-constraints';
|
||||||
getDriveImageCompatibilityStatuses,
|
import { getDriveImageCompatibilityStatuses } from '../../../../shared/drive-constraints';
|
||||||
DriveStatus,
|
|
||||||
} from '../../../../shared/drive-constraints';
|
|
||||||
import { compatibility, warning } from '../../../../shared/messages';
|
import { compatibility, warning } from '../../../../shared/messages';
|
||||||
import * as prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||||
import {
|
import {
|
||||||
ChangeButton,
|
ChangeButton,
|
||||||
|
@ -17,12 +17,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, Txt } from 'rendition';
|
import { Flex, Txt } from 'rendition';
|
||||||
|
|
||||||
|
import type { DriveSelectorProps } from '../drive-selector/drive-selector';
|
||||||
|
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||||
import {
|
import {
|
||||||
DriveSelector,
|
|
||||||
DriveSelectorProps,
|
|
||||||
} from '../drive-selector/drive-selector';
|
|
||||||
import {
|
|
||||||
isDriveSelected,
|
|
||||||
getImage,
|
getImage,
|
||||||
getSelectedDrives,
|
getSelectedDrives,
|
||||||
deselectDrive,
|
deselectDrive,
|
||||||
@ -30,13 +27,12 @@ import {
|
|||||||
deselectAllDrives,
|
deselectAllDrives,
|
||||||
} from '../../models/selection-state';
|
} from '../../models/selection-state';
|
||||||
import { observe } from '../../models/store';
|
import { observe } from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
|
||||||
import { TargetSelectorButton } from './target-selector-button';
|
import { TargetSelectorButton } from './target-selector-button';
|
||||||
|
|
||||||
import TgtSvg from '../../../assets/tgt.svg';
|
import TgtSvg from '../../../assets/tgt.svg';
|
||||||
import DriveSvg from '../../../assets/drive.svg';
|
import DriveSvg from '../../../assets/drive.svg';
|
||||||
import { warning } from '../../../../shared/messages';
|
import { warning } from '../../../../shared/messages';
|
||||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
import type { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||||
import * as i18next from 'i18next';
|
import * as i18next from 'i18next';
|
||||||
|
|
||||||
export const getDriveListLabel = () => {
|
export const getDriveListLabel = () => {
|
||||||
@ -79,21 +75,10 @@ export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
|
|||||||
);
|
);
|
||||||
// deselect drives
|
// deselect drives
|
||||||
deselected.forEach((drive) => {
|
deselected.forEach((drive) => {
|
||||||
analytics.logEvent('Toggle drive', {
|
|
||||||
drive,
|
|
||||||
previouslySelected: true,
|
|
||||||
});
|
|
||||||
deselectDrive(drive.device);
|
deselectDrive(drive.device);
|
||||||
});
|
});
|
||||||
// select drives
|
// select drives
|
||||||
modalTargets.forEach((drive) => {
|
modalTargets.forEach((drive) => {
|
||||||
// Don't send events for drives that were already selected
|
|
||||||
if (!isDriveSelected(drive.device)) {
|
|
||||||
analytics.logEvent('Toggle drive', {
|
|
||||||
drive,
|
|
||||||
previouslySelected: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
selectDrive(drive.device);
|
selectDrive(drive.device);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -102,12 +87,14 @@ interface TargetSelectorProps {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
hasDrive: boolean;
|
hasDrive: boolean;
|
||||||
flashing: boolean;
|
flashing: boolean;
|
||||||
|
hideAnalyticsAlert: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TargetSelector = ({
|
export const TargetSelector = ({
|
||||||
disabled,
|
disabled,
|
||||||
hasDrive,
|
hasDrive,
|
||||||
flashing,
|
flashing,
|
||||||
|
hideAnalyticsAlert,
|
||||||
}: TargetSelectorProps) => {
|
}: TargetSelectorProps) => {
|
||||||
// TODO: inject these from redux-connector
|
// TODO: inject these from redux-connector
|
||||||
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
|
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
|
||||||
@ -139,9 +126,9 @@ export const TargetSelector = ({
|
|||||||
tooltip={driveListLabel}
|
tooltip={driveListLabel}
|
||||||
openDriveSelector={() => {
|
openDriveSelector={() => {
|
||||||
setShowTargetSelectorModal(true);
|
setShowTargetSelectorModal(true);
|
||||||
|
hideAnalyticsAlert();
|
||||||
}}
|
}}
|
||||||
reselectDrive={() => {
|
reselectDrive={() => {
|
||||||
analytics.logEvent('Reselect drive');
|
|
||||||
setShowTargetSelectorModal(true);
|
setShowTargetSelectorModal(true);
|
||||||
}}
|
}}
|
||||||
flashing={flashing}
|
flashing={flashing}
|
||||||
|
@ -133,8 +133,7 @@ const translation = {
|
|||||||
flashCompleted: 'Flash Completed!',
|
flashCompleted: 'Flash Completed!',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
errorReporting:
|
errorReporting: 'Anonymously report errors to balena.io',
|
||||||
'Anonymously report errors and usage statistics to balena.io',
|
|
||||||
autoUpdate: 'Auto-updates enabled',
|
autoUpdate: 'Auto-updates enabled',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
systemInformation: 'System Information',
|
systemInformation: 'System Information',
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||||
import { Actions, store } from './store';
|
import { Actions, store } from './store';
|
||||||
|
|
||||||
export function hasAvailableDrives() {
|
export function hasAvailableDrives() {
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as electron from 'electron';
|
import * as electron from 'electron';
|
||||||
import * as sdk from 'etcher-sdk';
|
import type * as sdk from 'etcher-sdk';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
import type { 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';
|
||||||
|
|
||||||
|
@ -15,12 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
import type { AnimationFunction, Color } from 'sys-class-rgb-led';
|
||||||
|
import { Animator, RGBLed } from 'sys-class-rgb-led';
|
||||||
|
|
||||||
import {
|
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||||
DrivelistDrive,
|
import { isSourceDrive } from '../../../shared/drive-constraints';
|
||||||
isSourceDrive,
|
|
||||||
} from '../../../shared/drive-constraints';
|
|
||||||
import { getDrives } from './available-drives';
|
import { getDrives } from './available-drives';
|
||||||
import { getSelectedDrives } from './selection-state';
|
import { getSelectedDrives } from './selection-state';
|
||||||
import * as settings from './settings';
|
import * as settings from './settings';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 balena.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
@ -15,7 +15,7 @@ import { DrivelistDrive } from '../../../shared/drive-constraints';
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
import type { SourceMetadata } from '../../../shared/typings/source-selector';
|
||||||
|
|
||||||
import * as availableDrives from './available-drives';
|
import * as availableDrives from './available-drives';
|
||||||
import { Actions, store } from './store';
|
import { Actions, store } from './store';
|
||||||
|
@ -14,12 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
import { findLastIndex, once } from 'lodash';
|
||||||
import { Client, createClient, createNoopClient } from 'analytics-client';
|
|
||||||
import * as SentryRenderer from '@sentry/electron/renderer';
|
import * as SentryRenderer from '@sentry/electron/renderer';
|
||||||
import * as settings from '../models/settings';
|
import * as settings from '../models/settings';
|
||||||
import { store } from '../models/store';
|
|
||||||
import * as packageJSON from '../../../../package.json';
|
|
||||||
|
|
||||||
type AnalyticsPayload = _.Dictionary<any>;
|
type AnalyticsPayload = _.Dictionary<any>;
|
||||||
|
|
||||||
@ -72,7 +69,7 @@ export const anonymizePath = (input: string) => {
|
|||||||
const segments = mainPart.split(sep);
|
const segments = mainPart.split(sep);
|
||||||
|
|
||||||
// Moving from the end, find the first marker and cut the path from there.
|
// Moving from the end, find the first marker and cut the path from there.
|
||||||
const startCutIndex = _.findLastIndex(segments, (segment) =>
|
const startCutIndex = findLastIndex(segments, (segment) =>
|
||||||
etcherSegmentMarkers.includes(segment),
|
etcherSegmentMarkers.includes(segment),
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
@ -114,104 +111,18 @@ export const anonymizeAnalyticsPayload = (
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
let analyticsClient: Client;
|
|
||||||
/**
|
/**
|
||||||
* @summary Init analytics configurations
|
* @summary Init analytics configurations
|
||||||
*/
|
*/
|
||||||
export const initAnalytics = _.once(() => {
|
export const initAnalytics = once(() => {
|
||||||
const dsn =
|
const dsn =
|
||||||
settings.getSync('analyticsSentryToken') ||
|
settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
|
||||||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
|
SentryRenderer.init({
|
||||||
SentryRenderer.init({ dsn, beforeSend: anonymizeSentryData });
|
dsn,
|
||||||
|
beforeSend: anonymizeSentryData,
|
||||||
const projectName =
|
debug: process.env.ETCHER_SENTRY_DEBUG === 'true',
|
||||||
settings.getSync('analyticsAmplitudeToken') ||
|
|
||||||
_.get(packageJSON, ['analytics', 'amplitude', 'token']);
|
|
||||||
|
|
||||||
const clientConfig = {
|
|
||||||
projectName,
|
|
||||||
endpoint: 'data.balena-cloud.com',
|
|
||||||
componentName: 'etcher',
|
|
||||||
componentVersion: packageJSON.version,
|
|
||||||
};
|
|
||||||
analyticsClient = projectName
|
|
||||||
? createClient(clientConfig)
|
|
||||||
: createNoopClient();
|
|
||||||
});
|
|
||||||
|
|
||||||
const getCircularReplacer = () => {
|
|
||||||
const seen = new WeakSet();
|
|
||||||
return (key: any, value: any) => {
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
if (seen.has(value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
seen.add(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function flattenObject(obj: any) {
|
|
||||||
const toReturn: AnalyticsPayload = {};
|
|
||||||
|
|
||||||
for (const i in obj) {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, i)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(obj[i])) {
|
|
||||||
toReturn[i] = obj[i];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof obj[i] === 'object' && obj[i] !== null) {
|
|
||||||
const flatObject = flattenObject(obj[i]);
|
|
||||||
for (const x in flatObject) {
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(flatObject, x)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
toReturn[i.toLowerCase() + '.' + x.toLowerCase()] = flatObject[x];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toReturn[i] = obj[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return toReturn;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatEvent(data: any): AnalyticsPayload {
|
|
||||||
const event = JSON.parse(JSON.stringify(data, getCircularReplacer()));
|
|
||||||
return anonymizeAnalyticsPayload(flattenObject(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportAnalytics(message: string, data: AnalyticsPayload = {}) {
|
|
||||||
const { applicationSessionUuid, flashingWorkflowUuid } = store
|
|
||||||
.getState()
|
|
||||||
.toJS();
|
|
||||||
|
|
||||||
const event = formatEvent({
|
|
||||||
...data,
|
|
||||||
applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid,
|
|
||||||
});
|
});
|
||||||
analyticsClient.track(message, event);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Log an event
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This function sends the debug message to product analytics services.
|
|
||||||
*/
|
|
||||||
export async function logEvent(message: string, data: AnalyticsPayload = {}) {
|
|
||||||
const shouldReportAnalytics = await settings.get('errorReporting');
|
|
||||||
if (shouldReportAnalytics) {
|
|
||||||
initAnalytics();
|
|
||||||
reportAnalytics(message, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Log an exception
|
* @summary Log an exception
|
||||||
|
@ -12,19 +12,16 @@
|
|||||||
* - centralise the api for both the writer and the scanner instead of having two instances running
|
* - centralise the api for both the writer and the scanner instead of having two instances running
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ipc from 'node-ipc';
|
import WebSocket from 'ws'; // (no types for wrapper, this is expected)
|
||||||
import { spawn } from 'child_process';
|
import { spawn, exec } from 'child_process';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
|
||||||
import * as packageJSON from '../../../../package.json';
|
import * as packageJSON from '../../../../package.json';
|
||||||
import * as permissions from '../../../shared/permissions';
|
import * as permissions from '../../../shared/permissions';
|
||||||
import * as errors from '../../../shared/errors';
|
import * as errors from '../../../shared/errors';
|
||||||
|
|
||||||
const THREADS_PER_CPU = 16;
|
const THREADS_PER_CPU = 16;
|
||||||
|
const connectionRetryDelay = 1000;
|
||||||
// NOTE: Ensure this isn't disabled, as it will cause
|
const connectionRetryAttempts = 10;
|
||||||
// the stdout maxBuffer size to be exceeded when flashing
|
|
||||||
ipc.config.silent = true;
|
|
||||||
|
|
||||||
async function writerArgv(): Promise<string[]> {
|
async function writerArgv(): Promise<string[]> {
|
||||||
let entryPoint = await window.etcher.getEtcherUtilPath();
|
let entryPoint = await window.etcher.getEtcherUtilPath();
|
||||||
@ -45,15 +42,17 @@ async function writerArgv(): Promise<string[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writerEnv(
|
async function spawnChild(
|
||||||
IPC_CLIENT_ID: string,
|
withPrivileges: boolean,
|
||||||
IPC_SERVER_ID: string,
|
etcherServerId: string,
|
||||||
IPC_SOCKET_ROOT: string,
|
etcherServerAddress: string,
|
||||||
|
etcherServerPort: string,
|
||||||
) {
|
) {
|
||||||
return {
|
const argv = await writerArgv();
|
||||||
IPC_SERVER_ID,
|
const env: any = {
|
||||||
IPC_CLIENT_ID,
|
ETCHER_SERVER_ADDRESS: etcherServerAddress,
|
||||||
IPC_SOCKET_ROOT,
|
ETCHER_SERVER_ID: etcherServerId,
|
||||||
|
ETCHER_SERVER_PORT: etcherServerPort,
|
||||||
UV_THREADPOOL_SIZE: (os.cpus().length * THREADS_PER_CPU).toString(),
|
UV_THREADPOOL_SIZE: (os.cpus().length * THREADS_PER_CPU).toString(),
|
||||||
// This environment variable prevents the AppImages
|
// This environment variable prevents the AppImages
|
||||||
// desktop integration script from presenting the
|
// desktop integration script from presenting the
|
||||||
@ -61,123 +60,192 @@ function writerEnv(
|
|||||||
SKIP: '1',
|
SKIP: '1',
|
||||||
...(process.platform === 'win32' ? {} : process.env),
|
...(process.platform === 'win32' ? {} : process.env),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
async function spawnChild({
|
|
||||||
withPrivileges,
|
|
||||||
IPC_CLIENT_ID,
|
|
||||||
IPC_SERVER_ID,
|
|
||||||
IPC_SOCKET_ROOT,
|
|
||||||
}: {
|
|
||||||
withPrivileges: boolean;
|
|
||||||
IPC_CLIENT_ID: string;
|
|
||||||
IPC_SERVER_ID: string;
|
|
||||||
IPC_SOCKET_ROOT: string;
|
|
||||||
}) {
|
|
||||||
const argv = await writerArgv();
|
|
||||||
const env = writerEnv(IPC_CLIENT_ID, IPC_SERVER_ID, IPC_SOCKET_ROOT);
|
|
||||||
if (withPrivileges) {
|
if (withPrivileges) {
|
||||||
return await permissions.elevateCommand(argv, {
|
console.log('... with privileges ...');
|
||||||
|
return permissions.elevateCommand(argv, {
|
||||||
applicationName: packageJSON.displayName,
|
applicationName: packageJSON.displayName,
|
||||||
environment: env,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const process = await spawn(argv[0], argv.slice(1), {
|
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
return { cancelled: false, process };
|
} else {
|
||||||
}
|
if (process.platform === 'win32') {
|
||||||
}
|
// we need to ensure we reset the env as a previous elevation process might have kept them in a wrong state
|
||||||
|
const envCommand = [];
|
||||||
function terminateServer(server: any) {
|
for (const key in env) {
|
||||||
// Turns out we need to destroy all sockets for
|
if (Object.prototype.hasOwnProperty.call(env, key)) {
|
||||||
// the server to actually close. Otherwise, it
|
envCommand.push(`set ${key}=${env[key]}`);
|
||||||
// just stops receiving any further connections,
|
}
|
||||||
// but remains open if there are active ones.
|
}
|
||||||
// @ts-ignore (no Server.sockets in @types/node-ipc)
|
await exec(envCommand.join(' && '));
|
||||||
for (const socket of server.sockets) {
|
}
|
||||||
socket.destroy();
|
const spawned = await spawn(argv[0], argv.slice(1), {
|
||||||
}
|
env,
|
||||||
server.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: replace the custom ipc events by one generic "message" for all communication with the backend
|
|
||||||
function startApiAndSpawnChild({
|
|
||||||
withPrivileges,
|
|
||||||
}: {
|
|
||||||
withPrivileges: boolean;
|
|
||||||
}): Promise<any> {
|
|
||||||
// There might be multiple Etcher instances running at
|
|
||||||
// the same time, also we might spawn multiple child and api so we must ensure each IPC
|
|
||||||
// server/client has a different name.
|
|
||||||
const IPC_SERVER_ID = `etcher-server-${process.pid}-${Date.now()}-${
|
|
||||||
withPrivileges ? 'privileged' : 'unprivileged'
|
|
||||||
}`;
|
|
||||||
const IPC_CLIENT_ID = `etcher-client-${process.pid}-${Date.now()}-${
|
|
||||||
withPrivileges ? 'privileged' : 'unprivileged'
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const IPC_SOCKET_ROOT = path.join(
|
|
||||||
process.env.XDG_RUNTIME_DIR || os.tmpdir(),
|
|
||||||
path.sep,
|
|
||||||
);
|
|
||||||
|
|
||||||
ipc.config.id = IPC_SERVER_ID;
|
|
||||||
ipc.config.socketRoot = IPC_SOCKET_ROOT;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
ipc.serve();
|
|
||||||
|
|
||||||
// log is special message which brings back the logs from the child process and prints them to the console
|
|
||||||
ipc.server.on('log', (message: string) => {
|
|
||||||
console.log(message);
|
|
||||||
});
|
});
|
||||||
|
return { cancelled: false, spawned };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// api to register more handlers with callbacks
|
type ChildApi = {
|
||||||
const registerHandler = (event: string, handler: any) => {
|
emit: (type: string, payload: any) => void;
|
||||||
ipc.server.on(event, handler);
|
registerHandler: (event: string, handler: any) => void;
|
||||||
|
failed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function connectToChildProcess(
|
||||||
|
etcherServerAddress: string,
|
||||||
|
etcherServerPort: string,
|
||||||
|
etcherServerId: string,
|
||||||
|
): Promise<ChildApi | { failed: boolean }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// TODO: default to IPC connections https://github.com/websockets/ws/blob/master/doc/ws.md#ipc-connections
|
||||||
|
// TOOD: use the path as cheap authentication
|
||||||
|
console.log(etcherServerId);
|
||||||
|
|
||||||
|
const url = `ws://${etcherServerAddress}:${etcherServerPort}`;
|
||||||
|
|
||||||
|
const ws = new WebSocket(url);
|
||||||
|
|
||||||
|
let heartbeat: any;
|
||||||
|
|
||||||
|
const startHeartbeat = (emit: any) => {
|
||||||
|
console.log('start heartbeat');
|
||||||
|
heartbeat = setInterval(() => {
|
||||||
|
emit('heartbeat', {});
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// once api is ready (means child process is connected) we pass the emit and terminate function to the caller
|
const stopHeartbeat = () => {
|
||||||
ipc.server.on('ready', (_: any, socket) => {
|
console.log('stop heartbeat');
|
||||||
const emit = (channel: string, data: any) => {
|
clearInterval(heartbeat);
|
||||||
ipc.server.emit(socket, channel, data);
|
};
|
||||||
};
|
|
||||||
resolve({
|
|
||||||
emit,
|
|
||||||
terminateServer: () => terminateServer(ipc.server),
|
|
||||||
registerHandler,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// on api error we terminate
|
ws.on('error', (error: any) => {
|
||||||
ipc.server.on('error', (error: any) => {
|
if (error.code === 'ECONNREFUSED') {
|
||||||
terminateServer(ipc.server);
|
resolve({
|
||||||
const errorObject = errors.fromJSON(error);
|
failed: true,
|
||||||
reject(errorObject);
|
});
|
||||||
});
|
} else {
|
||||||
|
stopHeartbeat();
|
||||||
// when the api is started we spawn the child process
|
reject({
|
||||||
ipc.server.on('start', async () => {
|
failed: true,
|
||||||
try {
|
|
||||||
const results = await spawnChild({
|
|
||||||
withPrivileges,
|
|
||||||
IPC_CLIENT_ID,
|
|
||||||
IPC_SERVER_ID,
|
|
||||||
IPC_SOCKET_ROOT,
|
|
||||||
});
|
});
|
||||||
// this will happen if the child is spawned withPrivileges and privileges has been rejected
|
|
||||||
if (results.cancelled) {
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// start the server
|
ws.on('open', () => {
|
||||||
ipc.server.start();
|
const emit = (type: string, payload: any) => {
|
||||||
|
ws.send(JSON.stringify({ type, payload }));
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('ready', {});
|
||||||
|
|
||||||
|
// parse and route messages
|
||||||
|
const messagesHandler: any = {
|
||||||
|
log: (message: any) => {
|
||||||
|
console.log(`CHILD LOG: ${message}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
error: (error: any) => {
|
||||||
|
const errorObject = errors.fromJSON(error);
|
||||||
|
console.error('CHILD ERROR', errorObject);
|
||||||
|
stopHeartbeat();
|
||||||
|
},
|
||||||
|
|
||||||
|
// once api is ready (means child process is connected) we pass the emit function to the caller
|
||||||
|
ready: () => {
|
||||||
|
console.log('CHILD READY');
|
||||||
|
|
||||||
|
startHeartbeat(emit);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
failed: false,
|
||||||
|
emit,
|
||||||
|
registerHandler,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.on('message', (jsonData: any) => {
|
||||||
|
const data = JSON.parse(jsonData);
|
||||||
|
const message = messagesHandler[data.type];
|
||||||
|
if (message) {
|
||||||
|
message(data.payload);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown message type: ${data.type}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// api to register more handlers with callbacks
|
||||||
|
const registerHandler = (event: string, handler: any) => {
|
||||||
|
messagesHandler[event] = handler;
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { startApiAndSpawnChild };
|
async function spawnChildAndConnect({
|
||||||
|
withPrivileges,
|
||||||
|
}: {
|
||||||
|
withPrivileges: boolean;
|
||||||
|
}): Promise<ChildApi> {
|
||||||
|
const etcherServerAddress = process.env.ETCHER_SERVER_ADDRESS ?? '127.0.0.1'; // localhost
|
||||||
|
const etcherServerPort =
|
||||||
|
process.env.ETCHER_SERVER_PORT ?? withPrivileges ? '3435' : '3434';
|
||||||
|
const etcherServerId =
|
||||||
|
process.env.ETCHER_SERVER_ID ??
|
||||||
|
`etcher-${Math.random().toString(36).substring(7)}`;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Spawning ${
|
||||||
|
withPrivileges ? 'priviledged' : 'unpriviledged'
|
||||||
|
} sidecar on port ${etcherServerPort}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// spawn the child process, which will act as the ws server
|
||||||
|
// ETCHER_NO_SPAWN_UTIL can be set to launch a GUI only version of etcher, in that case you'll probably want to set other ENV to match your setup
|
||||||
|
if (!process.env.ETCHER_NO_SPAWN_UTIL) {
|
||||||
|
try {
|
||||||
|
const result = await spawnChild(
|
||||||
|
withPrivileges,
|
||||||
|
etcherServerId,
|
||||||
|
etcherServerAddress,
|
||||||
|
etcherServerPort,
|
||||||
|
);
|
||||||
|
if (result.cancelled) {
|
||||||
|
throw new Error('Spwaning the child process was cancelled');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error spawning child process', error);
|
||||||
|
throw new Error('Error spawning the child process');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to connect to the ws server, retrying if necessary, until the connection is established
|
||||||
|
try {
|
||||||
|
let retry = 0;
|
||||||
|
while (retry < connectionRetryAttempts) {
|
||||||
|
const { emit, registerHandler, failed } = await connectToChildProcess(
|
||||||
|
etcherServerAddress,
|
||||||
|
etcherServerPort,
|
||||||
|
etcherServerId,
|
||||||
|
);
|
||||||
|
if (failed) {
|
||||||
|
retry++;
|
||||||
|
console.log(
|
||||||
|
`Retrying to connect to child process in ${connectionRetryDelay}... ${retry} / ${connectionRetryAttempts}`,
|
||||||
|
);
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, connectionRetryDelay),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return { failed, emit, registerHandler };
|
||||||
|
}
|
||||||
|
throw new Error('Connection to etcher-util timed out');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error connecting to child process', error);
|
||||||
|
throw new Error('Connection to etcher-util failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { spawnChildAndConnect };
|
||||||
|
@ -14,50 +14,17 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
import type { Drive as DrivelistDrive } from 'drivelist';
|
||||||
import * as sdk from 'etcher-sdk';
|
import type * as sdk from 'etcher-sdk';
|
||||||
import { Dictionary } from 'lodash';
|
import type { Dictionary } from 'lodash';
|
||||||
import * as errors from '../../../shared/errors';
|
import * as errors from '../../../shared/errors';
|
||||||
import { SourceMetadata } from '../../../shared/typings/source-selector';
|
import type { SourceMetadata } from '../../../shared/typings/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 settings from '../models/settings';
|
import * as settings from '../models/settings';
|
||||||
import * as analytics from '../modules/analytics';
|
|
||||||
import * as windowProgress from '../os/window-progress';
|
import * as windowProgress from '../os/window-progress';
|
||||||
import { startApiAndSpawnChild } from './api';
|
import { spawnChildAndConnect } from './api';
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Handle a flash error and log it to analytics
|
|
||||||
*/
|
|
||||||
function handleErrorLogging(
|
|
||||||
error: Error & { code: string },
|
|
||||||
analyticsData: any,
|
|
||||||
) {
|
|
||||||
const eventData = {
|
|
||||||
...analyticsData,
|
|
||||||
flashInstanceUuid: flashState.getFlashUuid(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error.code === 'EVALIDATION') {
|
|
||||||
analytics.logEvent('Validation error', eventData);
|
|
||||||
} else if (error.code === 'EUNPLUGGED') {
|
|
||||||
analytics.logEvent('Drive unplugged', eventData);
|
|
||||||
} else if (error.code === 'EIO') {
|
|
||||||
analytics.logEvent('Input/output error', eventData);
|
|
||||||
} else if (error.code === 'ENOSPC') {
|
|
||||||
analytics.logEvent('Out of space', eventData);
|
|
||||||
} else if (error.code === 'ECHILDDIED') {
|
|
||||||
analytics.logEvent('Child died unexpectedly', eventData);
|
|
||||||
} else {
|
|
||||||
analytics.logEvent('Flash error', {
|
|
||||||
...eventData,
|
|
||||||
error: errors.toJSON(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelEmitter: (type: string) => void | undefined;
|
let cancelEmitter: (type: string) => void | undefined;
|
||||||
|
|
||||||
interface FlashResults {
|
interface FlashResults {
|
||||||
skip?: boolean;
|
skip?: boolean;
|
||||||
cancelled?: boolean;
|
cancelled?: boolean;
|
||||||
@ -78,24 +45,15 @@ async function performWrite(
|
|||||||
): Promise<{ cancelled?: boolean }> {
|
): Promise<{ cancelled?: boolean }> {
|
||||||
const { autoBlockmapping, decompressFirst } = await settings.getAll();
|
const { autoBlockmapping, decompressFirst } = await settings.getAll();
|
||||||
|
|
||||||
console.log({ image, drives });
|
|
||||||
|
|
||||||
// Spawn the child process with privileges and wait for the connection to be made
|
// Spawn the child process with privileges and wait for the connection to be made
|
||||||
const { emit, registerHandler, terminateServer } =
|
const { emit, registerHandler } = await spawnChildAndConnect({
|
||||||
await startApiAndSpawnChild({
|
withPrivileges: true,
|
||||||
withPrivileges: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const flashResults: FlashResults = {};
|
// if the connection failed, reject the promise
|
||||||
|
|
||||||
const analyticsData = {
|
const flashResults: FlashResults = {};
|
||||||
image,
|
|
||||||
drives,
|
|
||||||
driveCount: drives.length,
|
|
||||||
uuid: flashState.getFlashUuid(),
|
|
||||||
flashInstanceUuid: flashState.getFlashUuid(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFail = ({ device, error }: { device: any; error: any }) => {
|
const onFail = ({ device, error }: { device: any; error: any }) => {
|
||||||
console.log('fail event');
|
console.log('fail event');
|
||||||
@ -104,29 +62,28 @@ async function performWrite(
|
|||||||
if (device.devicePath) {
|
if (device.devicePath) {
|
||||||
flashState.addFailedDeviceError({ device, error });
|
flashState.addFailedDeviceError({ device, error });
|
||||||
}
|
}
|
||||||
handleErrorLogging(error, analyticsData);
|
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDone = (event: any) => {
|
const onDone = (payload: any) => {
|
||||||
console.log('done event');
|
console.log('CHILD: flash done', payload);
|
||||||
event.results.errors = event.results.errors.map(
|
payload.results.errors = payload.results.errors.map(
|
||||||
(data: Dictionary<any> & { message: string }) => {
|
(data: Dictionary<any> & { message: string }) => {
|
||||||
return errors.fromJSON(data);
|
return errors.fromJSON(data);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
flashResults.results = event.results;
|
flashResults.results = payload.results;
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAbort = () => {
|
const onAbort = () => {
|
||||||
console.log('abort event');
|
console.log('CHILD: flash aborted');
|
||||||
flashResults.cancelled = true;
|
flashResults.cancelled = true;
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSkip = () => {
|
const onSkip = () => {
|
||||||
console.log('skip event');
|
console.log('CHILD: validation skipped');
|
||||||
flashResults.skip = true;
|
flashResults.skip = true;
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
@ -151,8 +108,6 @@ async function performWrite(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Terminating IPC server');
|
|
||||||
terminateServer();
|
|
||||||
resolve(flashResults);
|
resolve(flashResults);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -162,7 +117,7 @@ async function performWrite(
|
|||||||
registerHandler('abort', onAbort);
|
registerHandler('abort', onAbort);
|
||||||
registerHandler('skip', onSkip);
|
registerHandler('skip', onSkip);
|
||||||
|
|
||||||
cancelEmitter = (cancelStatus: string) => emit(cancelStatus);
|
cancelEmitter = (cancelStatus: string) => emit('cancel', cancelStatus);
|
||||||
|
|
||||||
// Now that we know we're connected we can instruct the child process to start the write
|
// Now that we know we're connected we can instruct the child process to start the write
|
||||||
const parameters = {
|
const parameters = {
|
||||||
@ -198,21 +153,12 @@ export async function flash(
|
|||||||
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const analyticsData = {
|
|
||||||
image,
|
|
||||||
drives,
|
|
||||||
driveCount: drives.length,
|
|
||||||
uuid: flashState.getFlashUuid(),
|
|
||||||
status: 'started',
|
|
||||||
flashInstanceUuid: flashState.getFlashUuid(),
|
|
||||||
};
|
|
||||||
|
|
||||||
analytics.logEvent('Flash', analyticsData);
|
|
||||||
|
|
||||||
// start api and call the flasher
|
// start api and call the flasher
|
||||||
try {
|
try {
|
||||||
const result = await write(image, drives, flashState.setProgressState);
|
const result = await write(image, drives, flashState.setProgressState);
|
||||||
|
console.log('got results', result);
|
||||||
await flashState.unsetFlashingFlag(result);
|
await flashState.unsetFlashingFlag(result);
|
||||||
|
console.log('removed flashing flag');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
await flashState.unsetFlashingFlag({
|
await flashState.unsetFlashingFlag({
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
@ -221,39 +167,10 @@ export async function flash(
|
|||||||
|
|
||||||
windowProgress.clear();
|
windowProgress.clear();
|
||||||
|
|
||||||
const { results = {} } = flashState.getFlashResults();
|
|
||||||
|
|
||||||
const eventData = {
|
|
||||||
...analyticsData,
|
|
||||||
errors: results.errors,
|
|
||||||
devices: results.devices,
|
|
||||||
status: 'failed',
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
analytics.logEvent('Write failed', eventData);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
windowProgress.clear();
|
windowProgress.clear();
|
||||||
|
|
||||||
if (flashState.wasLastFlashCancelled()) {
|
|
||||||
const eventData = {
|
|
||||||
...analyticsData,
|
|
||||||
status: 'cancel',
|
|
||||||
};
|
|
||||||
analytics.logEvent('Elevation cancelled', eventData);
|
|
||||||
} else {
|
|
||||||
const { results = {} } = flashState.getFlashResults();
|
|
||||||
const eventData = {
|
|
||||||
...analyticsData,
|
|
||||||
errors: results.errors,
|
|
||||||
devices: results.devices,
|
|
||||||
status: 'finished',
|
|
||||||
bytesWritten: results.bytesWritten,
|
|
||||||
sourceMetadata: results.sourceMetadata,
|
|
||||||
};
|
|
||||||
analytics.logEvent('Done', eventData);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -262,16 +179,6 @@ export async function flash(
|
|||||||
*/
|
*/
|
||||||
export async function cancel(type: string) {
|
export async function cancel(type: string) {
|
||||||
const status = type.toLowerCase();
|
const status = type.toLowerCase();
|
||||||
const drives = selectionState.getSelectedDevices();
|
|
||||||
const analyticsData = {
|
|
||||||
image: selectionState.getImage()?.path,
|
|
||||||
drives,
|
|
||||||
driveCount: drives.length,
|
|
||||||
uuid: flashState.getFlashUuid(),
|
|
||||||
flashInstanceUuid: flashState.getFlashUuid(),
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
analytics.logEvent('Cancel', analyticsData);
|
|
||||||
|
|
||||||
if (cancelEmitter) {
|
if (cancelEmitter) {
|
||||||
cancelEmitter(status);
|
cancelEmitter(status);
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import * as i18next from 'i18next';
|
import * as i18next from 'i18next';
|
||||||
|
|
||||||
export interface FlashState {
|
export interface FlashState {
|
||||||
@ -34,6 +34,8 @@ export function fromFlashState({
|
|||||||
status: string;
|
status: string;
|
||||||
position?: string;
|
position?: string;
|
||||||
} {
|
} {
|
||||||
|
console.log(i18next.t('progress.starting'));
|
||||||
|
|
||||||
if (type === undefined) {
|
if (type === undefined) {
|
||||||
return { status: i18next.t('progress.starting') };
|
return { status: i18next.t('progress.starting') };
|
||||||
} else if (type === 'decompressing') {
|
} else if (type === 'decompressing') {
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import * as electron from 'electron';
|
import * as electron from 'electron';
|
||||||
import * as settings from '../../../models/settings';
|
import * as settings from '../../../models/settings';
|
||||||
import { logEvent } from '../../../modules/analytics';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Open an external resource
|
* @summary Open an external resource
|
||||||
@ -27,8 +26,6 @@ export async function open(url: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logEvent('Open external link', { url });
|
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
electron.shell.openExternal(url);
|
electron.shell.openExternal(url);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
import * as remote from '@electron/remote';
|
import * as remote from '@electron/remote';
|
||||||
|
|
||||||
import { percentageToFloat } from '../../../shared/utils';
|
import { percentageToFloat } from '../../../shared/utils';
|
||||||
import { FlashState, titleFromFlashState } from '../modules/progress-status';
|
import type { FlashState } from '../modules/progress-status';
|
||||||
|
import { titleFromFlashState } from '../modules/progress-status';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary The title of the main window upon program launch
|
* @summary The title of the main window upon program launch
|
||||||
|
@ -198,9 +198,7 @@ export class FlashStep extends React.PureComponent<
|
|||||||
private handleFlashErrorResponse(shouldRetry: boolean) {
|
private handleFlashErrorResponse(shouldRetry: boolean) {
|
||||||
this.setState({ errorMessage: '' });
|
this.setState({ errorMessage: '' });
|
||||||
flashState.resetState();
|
flashState.resetState();
|
||||||
if (shouldRetry) {
|
if (!shouldRetry) {
|
||||||
analytics.logEvent('Restart after failure');
|
|
||||||
} else {
|
|
||||||
selection.clear();
|
selection.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,19 +15,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg';
|
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg';
|
||||||
|
import CloseSvg from '@fortawesome/fontawesome-free/svgs/solid/x.svg';
|
||||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg';
|
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg';
|
||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex } from 'rendition';
|
import { Alert, Flex, Link } from 'rendition';
|
||||||
import styled from 'styled-components';
|
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 { SettingsModal } from '../../components/settings/settings';
|
import { SettingsModal } from '../../components/settings/settings';
|
||||||
import { SourceSelector } from '../../components/source-selector/source-selector';
|
import { SourceSelector } from '../../components/source-selector/source-selector';
|
||||||
import { SourceMetadata } from '../../../../shared/typings/source-selector';
|
import type { SourceMetadata } from '../../../../shared/typings/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';
|
||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
@ -35,6 +36,7 @@ import { observe } from '../../models/store';
|
|||||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||||
import {
|
import {
|
||||||
IconButton as BaseIcon,
|
IconButton as BaseIcon,
|
||||||
|
IconButton,
|
||||||
ThemedProvider,
|
ThemedProvider,
|
||||||
} from '../../styled-components';
|
} from '../../styled-components';
|
||||||
|
|
||||||
@ -46,6 +48,7 @@ import { FlashStep } from './Flash';
|
|||||||
|
|
||||||
import EtcherSvg from '../../../assets/etcher.svg';
|
import EtcherSvg from '../../../assets/etcher.svg';
|
||||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||||
|
import { theme } from '../../theme';
|
||||||
|
|
||||||
const Icon = styled(BaseIcon)`
|
const Icon = styled(BaseIcon)`
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
@ -97,6 +100,8 @@ const StepBorder = styled.div<{
|
|||||||
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
|
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const ANALYTICS_ALERT_VISIBILITY_KEY = 'analytics_alert_visible';
|
||||||
|
|
||||||
interface MainPageStateFromStore {
|
interface MainPageStateFromStore {
|
||||||
isFlashing: boolean;
|
isFlashing: boolean;
|
||||||
hasImage: boolean;
|
hasImage: boolean;
|
||||||
@ -113,6 +118,7 @@ interface MainPageState {
|
|||||||
isWebviewShowing: boolean;
|
isWebviewShowing: boolean;
|
||||||
hideSettings: boolean;
|
hideSettings: boolean;
|
||||||
featuredProjectURL?: string;
|
featuredProjectURL?: string;
|
||||||
|
analyticsAlertIsVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MainPage extends React.Component<
|
export class MainPage extends React.Component<
|
||||||
@ -125,6 +131,8 @@ export class MainPage extends React.Component<
|
|||||||
current: 'main',
|
current: 'main',
|
||||||
isWebviewShowing: false,
|
isWebviewShowing: false,
|
||||||
hideSettings: true,
|
hideSettings: true,
|
||||||
|
analyticsAlertIsVisible:
|
||||||
|
localStorage.getItem(ANALYTICS_ALERT_VISIBILITY_KEY) !== 'false',
|
||||||
...this.stateHelper(),
|
...this.stateHelper(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -153,6 +161,13 @@ export class MainPage extends React.Component<
|
|||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hideAnalyticsAlert = () => {
|
||||||
|
if (this.state.analyticsAlertIsVisible) {
|
||||||
|
localStorage.setItem(ANALYTICS_ALERT_VISIBILITY_KEY, 'false');
|
||||||
|
this.setState({ analyticsAlertIsVisible: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
observe(() => {
|
observe(() => {
|
||||||
this.setState(this.stateHelper());
|
this.setState(this.stateHelper());
|
||||||
@ -160,6 +175,17 @@ export class MainPage extends React.Component<
|
|||||||
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
|
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(
|
||||||
|
_prevProps: object,
|
||||||
|
prevState: Readonly<MainPageState & MainPageStateFromStore>,
|
||||||
|
) {
|
||||||
|
if (this.state.analyticsAlertIsVisible) {
|
||||||
|
if (prevState.hideSettings !== this.state.hideSettings) {
|
||||||
|
this.setState({ analyticsAlertIsVisible: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private renderMain() {
|
private renderMain() {
|
||||||
const state = flashState.getFlashState();
|
const state = flashState.getFlashState();
|
||||||
const shouldDriveStepBeDisabled = !this.state.hasImage;
|
const shouldDriveStepBeDisabled = !this.state.hasImage;
|
||||||
@ -169,86 +195,127 @@ export class MainPage extends React.Component<
|
|||||||
!this.state.isFlashing || !this.state.isWebviewShowing;
|
!this.state.isFlashing || !this.state.isWebviewShowing;
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px 18px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||||
justifyContent="space-between"
|
flexDirection="column"
|
||||||
>
|
>
|
||||||
{notFlashingOrSplitView && (
|
<Flex
|
||||||
<>
|
justifyContent="space-between"
|
||||||
<SourceSelector flashing={this.state.isFlashing} />
|
mb={this.state.analyticsAlertIsVisible ? '0px' : '92px'}
|
||||||
<Flex>
|
>
|
||||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
{notFlashingOrSplitView && (
|
||||||
</Flex>
|
<>
|
||||||
<TargetSelector
|
<SourceSelector
|
||||||
disabled={shouldDriveStepBeDisabled}
|
flashing={this.state.isFlashing}
|
||||||
hasDrive={this.state.hasDrive}
|
hideAnalyticsAlert={this.hideAnalyticsAlert}
|
||||||
flashing={this.state.isFlashing}
|
/>
|
||||||
/>
|
<Flex>
|
||||||
<Flex>
|
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
</Flex>
|
||||||
</Flex>
|
<TargetSelector
|
||||||
</>
|
disabled={shouldDriveStepBeDisabled}
|
||||||
)}
|
hasDrive={this.state.hasDrive}
|
||||||
|
flashing={this.state.isFlashing}
|
||||||
|
hideAnalyticsAlert={this.hideAnalyticsAlert}
|
||||||
|
/>
|
||||||
|
<Flex>
|
||||||
|
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||||
<Flex
|
<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={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
color: '#fff',
|
top: 0,
|
||||||
left: 35,
|
left: 0,
|
||||||
top: 72,
|
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',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</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
|
<FlashStep
|
||||||
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
||||||
goToSuccess={() => this.setState({ current: 'success' })}
|
goToSuccess={() => this.setState({ current: 'success' })}
|
||||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||||
isFlashing={this.state.isFlashing}
|
isFlashing={this.state.isFlashing}
|
||||||
step={state.type}
|
step={state.type}
|
||||||
percentage={state.percentage}
|
percentage={state.percentage}
|
||||||
position={state.position}
|
position={state.position}
|
||||||
failed={state.failed}
|
failed={state.failed}
|
||||||
speed={state.speed}
|
speed={state.speed}
|
||||||
eta={state.eta}
|
eta={state.eta}
|
||||||
style={{ zIndex: 1 }}
|
style={{ zIndex: 1 }}
|
||||||
/>
|
/>
|
||||||
|
</Flex>
|
||||||
|
{this.state.analyticsAlertIsVisible && (
|
||||||
|
<Alert mt="18px" style={{ boxShadow: 'none', fontSize: '12px' }}>
|
||||||
|
<Flex alignItems="center" justifyContent="space-between">
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<div>
|
||||||
|
Etcher collects a limited amount of anonymous data to help us
|
||||||
|
improve user experience. You can opt out in the{' '}
|
||||||
|
<Link onClick={() => this.setState({ hideSettings: false })}>
|
||||||
|
settings
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
For more information about how we use this data, see our{' '}
|
||||||
|
<Link
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openExternal('https://www.balena.io/privacy-policy');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
privacy policy
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
{/* TODO: can we use onDismiss instead? */}
|
||||||
|
<IconButton onClick={this.hideAnalyticsAlert}>
|
||||||
|
<CloseSvg height="0.75rem" fill={theme.colors.text.main} />
|
||||||
|
</IconButton>
|
||||||
|
</Flex>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,16 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import type {
|
||||||
|
FlexProps,
|
||||||
|
ButtonProps,
|
||||||
|
TableProps as BaseTableProps,
|
||||||
|
} from 'rendition';
|
||||||
import {
|
import {
|
||||||
Alert as AlertBase,
|
Alert as AlertBase,
|
||||||
Flex,
|
Flex,
|
||||||
FlexProps,
|
|
||||||
Button,
|
Button,
|
||||||
ButtonProps,
|
|
||||||
Modal as ModalBase,
|
Modal as ModalBase,
|
||||||
Provider,
|
Provider,
|
||||||
Table as BaseTable,
|
Table as BaseTable,
|
||||||
TableProps as BaseTableProps,
|
|
||||||
Txt,
|
Txt,
|
||||||
} from 'rendition';
|
} from 'rendition';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dictionary } from 'lodash';
|
import type { Dictionary } from 'lodash';
|
||||||
|
|
||||||
type BalenaTag = {
|
type BalenaTag = {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -27,7 +27,7 @@ import { promises as fs } from 'fs';
|
|||||||
import { platform } from 'os';
|
import { platform } from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import * as lodash from 'lodash';
|
import { once } from 'lodash';
|
||||||
|
|
||||||
import './app/i18n';
|
import './app/i18n';
|
||||||
|
|
||||||
@ -37,7 +37,6 @@ import * as settings from './app/models/settings';
|
|||||||
import { buildWindowMenu } from './menu';
|
import { buildWindowMenu } from './menu';
|
||||||
import * as i18n from 'i18next';
|
import * as i18n from 'i18next';
|
||||||
import * as SentryMain from '@sentry/electron/main';
|
import * as SentryMain from '@sentry/electron/main';
|
||||||
import * as packageJSON from '../../package.json';
|
|
||||||
import { anonymizeSentryData } from './app/modules/analytics';
|
import { anonymizeSentryData } from './app/modules/analytics';
|
||||||
|
|
||||||
import { delay } from '../shared/utils';
|
import { delay } from '../shared/utils';
|
||||||
@ -115,12 +114,16 @@ async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initSentryMain = lodash.once(() => {
|
const initSentryMain = once(() => {
|
||||||
const dsn =
|
const dsn =
|
||||||
settings.getSync('analyticsSentryToken') ||
|
settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
|
||||||
lodash.get(packageJSON, ['analytics', 'sentry', 'token']);
|
|
||||||
|
|
||||||
SentryMain.init({ dsn, beforeSend: anonymizeSentryData });
|
SentryMain.init({
|
||||||
|
dsn,
|
||||||
|
beforeSend: anonymizeSentryData,
|
||||||
|
debug: process.env.ETCHER_SENTRY_DEBUG === 'true',
|
||||||
|
});
|
||||||
|
console.log(SentryMain.getCurrentScope());
|
||||||
});
|
});
|
||||||
|
|
||||||
const sourceSelectorReady = new Promise((resolve) => {
|
const sourceSelectorReady = new Promise((resolve) => {
|
||||||
@ -300,7 +303,7 @@ async function main(): Promise<void> {
|
|||||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
// tslint:disable-next-line:no-var-requires
|
// tslint:disable-next-line:no-var-requires
|
||||||
if (require('electron-squirrel-startup')) {
|
if (require('electron-squirrel-startup')) {
|
||||||
app.quit();
|
electron.app.quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Drive } from 'drivelist';
|
import type { Drive } from 'drivelist';
|
||||||
import { isNil } from 'lodash';
|
import { isNil } from 'lodash';
|
||||||
import * as pathIsInside from 'path-is-inside';
|
import * as pathIsInside from 'path-is-inside';
|
||||||
|
|
||||||
import * as messages from './messages';
|
import * as messages from './messages';
|
||||||
import { SourceMetadata } from './typings/source-selector';
|
import type { SourceMetadata } from './typings/source-selector';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary The default unknown size for things such as images and drives
|
* @summary The default unknown size for things such as images and drives
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dictionary } from 'lodash';
|
import type { Dictionary } from 'lodash';
|
||||||
import { outdent } from 'outdent';
|
import { outdent } from 'outdent';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import '../gui/app/i18n';
|
import '../gui/app/i18n';
|
||||||
|
@ -14,41 +14,27 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as childProcess from 'child_process';
|
/**
|
||||||
|
* TODO:
|
||||||
|
* This is convoluted and needlessly complex. It should be simplified and modernized.
|
||||||
|
* The environment variable setting and escaping should be greatly simplified by letting {linux|catalina}-sudo handle that.
|
||||||
|
* We shouldn't need to write a script to a file and then execute it. We should be able to forwatd the command to the sudo code directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, exec } from 'child_process';
|
||||||
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
|
import { promisify } from 'util';
|
||||||
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 '@balena/sudo-prompt';
|
|
||||||
import { promisify } from 'util';
|
|
||||||
|
|
||||||
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
import { sudo as darwinSudo } from './sudo/darwin';
|
||||||
|
import { sudo as linuxSudo } from './sudo/linux';
|
||||||
|
import { sudo as winSudo } from './sudo/windows';
|
||||||
import * as errors from './errors';
|
import * as errors from './errors';
|
||||||
|
|
||||||
const execAsync = promisify(childProcess.exec);
|
const execAsync = promisify(exec);
|
||||||
const execFileAsync = promisify(childProcess.execFile);
|
|
||||||
|
|
||||||
type Std = string | Buffer | undefined;
|
|
||||||
|
|
||||||
function sudoExecAsync(
|
|
||||||
cmd: string,
|
|
||||||
options: { name: string },
|
|
||||||
): Promise<{ stdout: Std; stderr: Std }> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
sudoPrompt.exec(
|
|
||||||
cmd,
|
|
||||||
options,
|
|
||||||
(error: Error | undefined, stdout: Std, stderr: Std) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
} else {
|
|
||||||
resolve({ stdout, stderr });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary The user id of the UNIX "superuser"
|
* @summary The user id of the UNIX "superuser"
|
||||||
@ -125,10 +111,11 @@ export function createLaunchScript(
|
|||||||
async function elevateScriptWindows(
|
async function elevateScriptWindows(
|
||||||
path: string,
|
path: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
env: any,
|
||||||
): Promise<{ cancelled: false }> {
|
): Promise<{ cancelled: false }> {
|
||||||
// '&' needs to be escaped here (but not when written to a .cmd file)
|
// '&' needs to be escaped here (but not when written to a .cmd file)
|
||||||
const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')].join(' ');
|
const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')].join(' ');
|
||||||
await sudoExecAsync(cmd, { name });
|
await winSudo(cmd, name, env);
|
||||||
return { cancelled: false };
|
return { cancelled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +124,7 @@ async function elevateScriptUnix(
|
|||||||
name: string,
|
name: string,
|
||||||
): Promise<{ cancelled: boolean }> {
|
): Promise<{ cancelled: boolean }> {
|
||||||
const cmd = ['bash', escapeSh(path)].join(' ');
|
const cmd = ['bash', escapeSh(path)].join(' ');
|
||||||
await sudoExecAsync(cmd, { name });
|
await linuxSudo(cmd, { name });
|
||||||
return { cancelled: false };
|
return { cancelled: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +133,7 @@ async function elevateScriptCatalina(
|
|||||||
): Promise<{ cancelled: boolean }> {
|
): Promise<{ cancelled: boolean }> {
|
||||||
const cmd = ['bash', escapeSh(path)].join(' ');
|
const cmd = ['bash', escapeSh(path)].join(' ');
|
||||||
try {
|
try {
|
||||||
const { cancelled } = await catalinaSudo(cmd);
|
const { cancelled } = await darwinSudo(cmd);
|
||||||
return { cancelled };
|
return { cancelled };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw errors.createError({ title: error.stderr });
|
throw errors.createError({ title: error.stderr });
|
||||||
@ -156,13 +143,13 @@ async function elevateScriptCatalina(
|
|||||||
export async function elevateCommand(
|
export async function elevateCommand(
|
||||||
command: string[],
|
command: string[],
|
||||||
options: {
|
options: {
|
||||||
environment: _.Dictionary<string | undefined>;
|
env: _.Dictionary<string | undefined>;
|
||||||
applicationName: string;
|
applicationName: string;
|
||||||
},
|
},
|
||||||
): Promise<{ cancelled: boolean }> {
|
): Promise<{ cancelled: boolean }> {
|
||||||
if (await isElevated()) {
|
if (await isElevated()) {
|
||||||
await execFileAsync(command[0], command.slice(1), {
|
spawn(command[0], command.slice(1), {
|
||||||
env: options.environment,
|
env: options.env,
|
||||||
});
|
});
|
||||||
return { cancelled: false };
|
return { cancelled: false };
|
||||||
}
|
}
|
||||||
@ -170,7 +157,7 @@ export async function elevateCommand(
|
|||||||
const launchScript = createLaunchScript(
|
const launchScript = createLaunchScript(
|
||||||
command[0],
|
command[0],
|
||||||
command.slice(1),
|
command.slice(1),
|
||||||
options.environment,
|
options.env,
|
||||||
);
|
);
|
||||||
return await withTmpFile(
|
return await withTmpFile(
|
||||||
{
|
{
|
||||||
@ -181,7 +168,7 @@ export async function elevateCommand(
|
|||||||
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, options.env);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
os.platform() === 'darwin' &&
|
os.platform() === 'darwin' &&
|
||||||
@ -191,7 +178,7 @@ export async function elevateCommand(
|
|||||||
return elevateScriptCatalina(path);
|
return elevateScriptCatalina(path);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await elevateScriptUnix(path, options.applicationName);
|
return elevateScriptUnix(path, options.applicationName);
|
||||||
} catch (error: any) {
|
} 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
|
||||||
|
@ -14,14 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFile } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
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 { supportedLocales } from '../../gui/app/i18n';
|
import { supportedLocales } from '../../gui/app/i18n';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
// const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||||
const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`;
|
const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`;
|
||||||
@ -48,22 +48,48 @@ export async function sudo(
|
|||||||
lang = 'en';
|
lang = 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { stdout, stderr } = await execFileAsync(
|
const elevateProcess = spawn(
|
||||||
'sudo',
|
'sudo',
|
||||||
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
|
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
|
||||||
{
|
{
|
||||||
encoding: 'utf8',
|
// encoding: "utf8",
|
||||||
env: {
|
env: {
|
||||||
PATH: env.PATH,
|
PATH: env.PATH,
|
||||||
SUDO_ASKPASS: getAskPassScriptPath(lang),
|
SUDO_ASKPASS: getAskPassScriptPath(lang),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return {
|
|
||||||
cancelled: false,
|
let elevated = 'pending';
|
||||||
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
|
|
||||||
stderr,
|
elevateProcess.stdout.on('data', (data) => {
|
||||||
};
|
if (data.toString().includes(SUCCESSFUL_AUTH_MARKER)) {
|
||||||
|
// if the first data comming out of the sudo command is the expected marker we resolve the promise
|
||||||
|
elevated = 'granted';
|
||||||
|
} else {
|
||||||
|
// if the first data comming out of the sudo command is not the expected marker we reject the promise
|
||||||
|
elevated = 'rejected';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// we don't spawn or read stdout in the promise otherwise resolving stop the process
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const checkElevation = setInterval(() => {
|
||||||
|
if (elevated === 'granted') {
|
||||||
|
clearInterval(checkElevation);
|
||||||
|
resolve({ cancelled: false });
|
||||||
|
} else if (elevated === 'rejected') {
|
||||||
|
clearInterval(checkElevation);
|
||||||
|
resolve({ cancelled: true });
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
// if the elevation didn't occured in 30 seconds we reject the promise
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkElevation);
|
||||||
|
reject(new Error('Elevation timeout'));
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} 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)) {
|
142
lib/shared/sudo/linux.ts
Normal file
142
lib/shared/sudo/linux.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* This is heavily inspired (read: a ripof) https://github.com/balena-io-modules/sudo-prompt
|
||||||
|
* Which was a fork of https://github.com/jorangreef/sudo-prompt
|
||||||
|
*
|
||||||
|
* This and the original code was released under The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Joran Dirk Greef
|
||||||
|
* Copyright (c) 2024 Balena
|
||||||
|
*
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { access, constants } from 'fs/promises';
|
||||||
|
import { env } from 'process';
|
||||||
|
|
||||||
|
// const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||||
|
|
||||||
|
/** Check for kdesudo or pkexec */
|
||||||
|
function checkLinuxBinary() {
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
// We used to prefer gksudo over pkexec since it enabled a better prompt.
|
||||||
|
// However, gksudo cannot run multiple commands concurrently.
|
||||||
|
|
||||||
|
const paths = ['/usr/bin/kdesudo', '/usr/bin/pkexec'];
|
||||||
|
for (const path of paths) {
|
||||||
|
try {
|
||||||
|
// check if the file exist and is executable
|
||||||
|
await access(path, constants.X_OK);
|
||||||
|
resolve(path);
|
||||||
|
} catch (error: any) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reject('Unable to find pkexec or kdesudo.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeDoubleQuotes(escapeString: string) {
|
||||||
|
return escapeString.replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sudo(
|
||||||
|
command: string,
|
||||||
|
{ name }: { name: string },
|
||||||
|
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
||||||
|
const linuxBinary: string = (await checkLinuxBinary()) as string;
|
||||||
|
if (!linuxBinary) {
|
||||||
|
throw new Error('Unable to find pkexec or kdesudo.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameters = [];
|
||||||
|
|
||||||
|
if (/kdesudo/i.test(linuxBinary)) {
|
||||||
|
parameters.push(
|
||||||
|
'--comment',
|
||||||
|
`"${name} wants to make changes.
|
||||||
|
Enter your password to allow this."`,
|
||||||
|
);
|
||||||
|
parameters.push('-d'); // Do not show the command to be run in the dialog.
|
||||||
|
parameters.push('--');
|
||||||
|
} else if (/pkexec/i.test(linuxBinary)) {
|
||||||
|
parameters.push('--disable-internal-agent');
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.push('/bin/bash');
|
||||||
|
parameters.push('-c');
|
||||||
|
parameters.push(
|
||||||
|
`echo ${SUCCESSFUL_AUTH_MARKER} && ${escapeDoubleQuotes(command)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const elevateProcess = spawn(linuxBinary, parameters, {
|
||||||
|
// encoding: "utf8",
|
||||||
|
env: {
|
||||||
|
PATH: env.PATH,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let elevated = '';
|
||||||
|
|
||||||
|
elevateProcess.stdout.on('data', (data) => {
|
||||||
|
// console.log(`stdout: ${data.toString()}`);
|
||||||
|
if (data.toString().includes(SUCCESSFUL_AUTH_MARKER)) {
|
||||||
|
// if the first data comming out of the sudo command is the expected marker we resolve the promise
|
||||||
|
elevated = 'granted';
|
||||||
|
} else {
|
||||||
|
// if the first data comming out of the sudo command is not the expected marker we reject the promise
|
||||||
|
elevated = 'refused';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// elevateProcess.stderr.on('data', (data) => {
|
||||||
|
// // console.log(`stderr: ${data.toString()}`);
|
||||||
|
// // if (data.toString().includes(SUCCESSFUL_AUTH_MARKER)) {
|
||||||
|
// // // if the first data comming out of the sudo command is the expected marker we resolve the promise
|
||||||
|
// // elevated = 'granted';
|
||||||
|
// // } else {
|
||||||
|
// // // if the first data comming out of the sudo command is not the expected marker we reject the promise
|
||||||
|
// // elevated = 'refused';
|
||||||
|
// // }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// we don't spawn or read stdout in the promise otherwise resolving stop the process
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const checkElevation = setInterval(() => {
|
||||||
|
if (elevated === 'granted') {
|
||||||
|
clearInterval(checkElevation);
|
||||||
|
resolve({ cancelled: false });
|
||||||
|
} else if (elevated === 'refused') {
|
||||||
|
clearInterval(checkElevation);
|
||||||
|
resolve({ cancelled: true });
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
// if the elevation didn't occured in 30 seconds we reject the promise
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkElevation);
|
||||||
|
reject(new Error('Elevation timeout'));
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
}
|
218
lib/shared/sudo/windows.ts
Normal file
218
lib/shared/sudo/windows.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
* This is heavily inspired (read: a ripof) https://github.com/balena-io-modules/sudo-prompt
|
||||||
|
* Which was a fork of https://github.com/jorangreef/sudo-prompt
|
||||||
|
*
|
||||||
|
* This and the original code was released under The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015 Joran Dirk Greef
|
||||||
|
* Copyright (c) 2024 Balena
|
||||||
|
*
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
// import { env } from 'process';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { join, sep } from 'path';
|
||||||
|
import { mkdir, writeFile, copyFile, readFile } from 'fs/promises';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO:
|
||||||
|
* Migrate, modernize and clenup the windows elevation code from the old @balena/sudo-prompt package in a similar way to linux-sudo.ts and catalina-sudo files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function sudo(
|
||||||
|
command: string,
|
||||||
|
_name: string,
|
||||||
|
env: any,
|
||||||
|
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
|
||||||
|
const temp = tmpdir();
|
||||||
|
if (!temp) {
|
||||||
|
throw new Error('os.tmpdir() not defined.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmpFolder = join(temp, uuid);
|
||||||
|
|
||||||
|
if (/"/.test(tmpFolder)) {
|
||||||
|
// We expect double quotes to be reserved on Windows.
|
||||||
|
// Even so, we test for this and abort if they are present.
|
||||||
|
throw new Error('instance.path cannot contain double-quotes.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeScriptPath = join(tmpFolder, 'execute.bat');
|
||||||
|
const commandScriptPath = join(tmpFolder, 'command.bat');
|
||||||
|
const stdoutPath = join(tmpFolder, 'stdout');
|
||||||
|
const stderrPath = join(tmpFolder, 'stderr');
|
||||||
|
const statusPath = join(tmpFolder, 'status');
|
||||||
|
|
||||||
|
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mkdir(tmpFolder);
|
||||||
|
|
||||||
|
// WindowsWriteExecuteScript(instance, end)
|
||||||
|
const executeScript = `
|
||||||
|
@echo off\r\n
|
||||||
|
call "${commandScriptPath}" > "${stdoutPath}" 2> "${stderrPath}"\r\n
|
||||||
|
(echo %ERRORLEVEL%) > "${statusPath}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
await writeFile(executeScriptPath, executeScript, 'utf-8');
|
||||||
|
|
||||||
|
// WindowsWriteCommandScript(instance, end)
|
||||||
|
const cwd = process.cwd();
|
||||||
|
if (/"/.test(cwd)) {
|
||||||
|
// We expect double quotes to be reserved on Windows.
|
||||||
|
// Even so, we test for this and abort if they are present.
|
||||||
|
throw new Error('process.cwd() cannot contain double-quotes.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandScriptArray = [];
|
||||||
|
commandScriptArray.push('@echo off');
|
||||||
|
// Set code page to UTF-8:
|
||||||
|
commandScriptArray.push('chcp 65001>nul');
|
||||||
|
// Preserve current working directory:
|
||||||
|
// We pass /d as an option in case the cwd is on another drive (issue 70).
|
||||||
|
commandScriptArray.push(`cd /d "${cwd}"`);
|
||||||
|
// Export environment variables:
|
||||||
|
for (const key in env) {
|
||||||
|
// "The characters <, >, |, &, ^ are special command shell characters, and
|
||||||
|
// they must be preceded by the escape character (^) or enclosed in
|
||||||
|
// quotation marks. If you use quotation marks to enclose a string that
|
||||||
|
// contains one of the special characters, the quotation marks are set as
|
||||||
|
// part of the environment variable value."
|
||||||
|
// In other words, Windows assigns everything that follows the equals sign
|
||||||
|
// to the value of the variable, whereas Unix systems ignore double quotes.
|
||||||
|
if (Object.prototype.hasOwnProperty.call(env, key)) {
|
||||||
|
const value = env[key];
|
||||||
|
commandScriptArray.push(
|
||||||
|
`set ${key}=${value!.replace(/([<>\\|&^])/g, '^$1')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
commandScriptArray.push(`echo ${SUCCESSFUL_AUTH_MARKER}`);
|
||||||
|
commandScriptArray.push(command);
|
||||||
|
await writeFile(
|
||||||
|
commandScriptPath,
|
||||||
|
commandScriptArray.join('\r\n'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
// WindowsCopyCmd(instance, end)
|
||||||
|
if (windowsNeedsCopyCmd(tmpFolder)) {
|
||||||
|
// Work around https://github.com/jorangreef/sudo-prompt/issues/97
|
||||||
|
// Powershell can't properly escape amperstands in paths.
|
||||||
|
// We work around this by copying cmd.exe in our temporary folder and running
|
||||||
|
// it from here (see WindowsElevate below).
|
||||||
|
// That way, we don't have to pass the path containing the amperstand at all.
|
||||||
|
// A symlink would probably work too but you have to be an administrator in
|
||||||
|
// order to create symlinks on Windows.
|
||||||
|
await copyFile(
|
||||||
|
join(process.env.SystemRoot!, 'System32', 'cmd.exe'),
|
||||||
|
join(tmpFolder, 'cmd.exe'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowsElevate(instance, end)
|
||||||
|
// We used to use this for executing elevate.vbs:
|
||||||
|
// var command = 'cscript.exe //NoLogo "' + instance.pathElevate + '"';
|
||||||
|
const spawnCommand = [];
|
||||||
|
// spawnCommand.push("powershell.exe") // as we use spawn this one is out of the array
|
||||||
|
spawnCommand.push('Start-Process');
|
||||||
|
spawnCommand.push('-FilePath');
|
||||||
|
const options: any = { encoding: 'utf8' };
|
||||||
|
if (windowsNeedsCopyCmd(tmpFolder)) {
|
||||||
|
// Node.path.join('.', 'cmd.exe') would return 'cmd.exe'
|
||||||
|
spawnCommand.push(['.', 'cmd.exe'].join(sep));
|
||||||
|
spawnCommand.push('-ArgumentList');
|
||||||
|
spawnCommand.push('"/C","execute.bat"');
|
||||||
|
options.cwd = tmpFolder;
|
||||||
|
} else {
|
||||||
|
// Escape characters for cmd using double quotes:
|
||||||
|
// Escape characters for PowerShell using single quotes:
|
||||||
|
// Escape single quotes for PowerShell using backtick:
|
||||||
|
// See: https://ss64.com/ps/syntax-esc.html
|
||||||
|
spawnCommand.push(`'${executeScriptPath.replace(/'/g, "`'")}'`);
|
||||||
|
}
|
||||||
|
spawnCommand.push('-WindowStyle hidden');
|
||||||
|
spawnCommand.push('-Verb runAs');
|
||||||
|
|
||||||
|
spawn('powershell.exe', spawnCommand);
|
||||||
|
|
||||||
|
// setTimeout(() => {elevated = "granted"}, 5000)
|
||||||
|
|
||||||
|
// we don't spawn or read stdout in the promise otherwise resolving stop the process
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const checkElevation = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const result = await readFile(stdoutPath, 'utf-8');
|
||||||
|
const error = await readFile(stderrPath, 'utf-8');
|
||||||
|
|
||||||
|
if (error && error !== '') {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should track something more generic
|
||||||
|
if (result.includes(SUCCESSFUL_AUTH_MARKER)) {
|
||||||
|
clearInterval(checkElevation);
|
||||||
|
resolve({ cancelled: false });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
'Error while reading flasher elevation script output',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// if the elevation didn't occured in 30 seconds we reject the promise
|
||||||
|
setTimeout(() => {
|
||||||
|
clearInterval(checkElevation);
|
||||||
|
reject(new Error('Elevation timeout'));
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// WindowsWaitForStatus(instance, end)
|
||||||
|
|
||||||
|
// WindowsResult(instance, end)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Can't elevate process ${error}`);
|
||||||
|
} finally {
|
||||||
|
// TODO: cleanup
|
||||||
|
// // Remove(instance.path, function (errorRemove) {
|
||||||
|
// // if (error) return callback(error)
|
||||||
|
// // if (errorRemove) return callback(errorRemove)
|
||||||
|
// // callback(undefined, stdout, stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowsNeedsCopyCmd(path: string) {
|
||||||
|
const specialChars = ['&', '`', "'", '"', '<', '>', '|', '^'];
|
||||||
|
for (const specialChar of specialChars) {
|
||||||
|
if (path.includes(specialChar)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
import type { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||||
import { sourceDestination } from 'etcher-sdk';
|
import type { sourceDestination } from 'etcher-sdk';
|
||||||
import { DrivelistDrive } from '../drive-constraints';
|
import type { DrivelistDrive } from '../drive-constraints';
|
||||||
|
|
||||||
export type Source = 'File' | 'BlockDevice' | 'Http';
|
export type Source = 'File' | 'BlockDevice' | 'Http';
|
||||||
|
|
||||||
|
397
lib/util/api.ts
397
lib/util/api.ts
@ -14,190 +14,279 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as ipc from 'node-ipc';
|
import { WebSocketServer } from 'ws';
|
||||||
import { Dictionary, values } from 'lodash';
|
import type { Dictionary } from 'lodash';
|
||||||
|
import { values } from 'lodash';
|
||||||
|
|
||||||
import type { MultiDestinationProgress } from 'etcher-sdk/build/multi-write';
|
import type { MultiDestinationProgress } from 'etcher-sdk/build/multi-write';
|
||||||
|
|
||||||
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 type { WriteOptions } from './types/types';
|
||||||
import { WriteOptions } from './types/types';
|
|
||||||
import { write, cleanup } from './child-writer';
|
import { write, cleanup } from './child-writer';
|
||||||
import { startScanning } from './scanner';
|
import { startScanning } from './scanner';
|
||||||
import { getSourceMetadata } from './source-metadata';
|
import { getSourceMetadata } from './source-metadata';
|
||||||
import { DrivelistDrive } from '../shared/drive-constraints';
|
import type { DrivelistDrive } from '../shared/drive-constraints';
|
||||||
|
import type { SourceMetadata } from '../shared/typings/source-selector';
|
||||||
|
|
||||||
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
const ETCHER_SERVER_ADDRESS = process.env.ETCHER_SERVER_ADDRESS as string;
|
||||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
const ETCHER_SERVER_PORT = process.env.ETCHER_SERVER_PORT as string;
|
||||||
|
// const ETCHER_SERVER_ID = process.env.ETCHER_SERVER_ID as string;
|
||||||
|
|
||||||
// NOTE: Ensure this isn't disabled, as it will cause
|
const ETCHER_TERMINATE_TIMEOUT: number = parseInt(
|
||||||
// the stdout maxBuffer size to be exceeded when flashing
|
process.env.ETCHER_TERMINATE_TIMEOUT ?? '10000',
|
||||||
ipc.config.silent = true;
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
// > If set to 0, the client will NOT try to reconnect.
|
const host = ETCHER_SERVER_ADDRESS ?? '127.0.0.1';
|
||||||
// See https://github.com/RIAEvangelist/node-ipc/
|
const port = parseInt(ETCHER_SERVER_PORT || '3434', 10);
|
||||||
//
|
// const path = ETCHER_SERVER_ID || "etcher";
|
||||||
// The purpose behind this change is for this process
|
|
||||||
// to emit a "disconnect" event as soon as the GUI
|
|
||||||
// process is closed, so we can kill this process as well.
|
|
||||||
|
|
||||||
// @ts-ignore (0 is a valid value for stopRetrying and is not the same as false)
|
// TODO: use the path as cheap authentication
|
||||||
ipc.config.stopRetrying = 0;
|
|
||||||
|
|
||||||
const DISCONNECT_DELAY = 100;
|
const wss = new WebSocketServer({ host, port });
|
||||||
const IPC_SERVER_ID = process.env.IPC_SERVER_ID as string;
|
|
||||||
|
|
||||||
/**
|
// hold emit functions
|
||||||
* @summary Send a message to the IPC server
|
let emitLog: (message: string) => void | undefined;
|
||||||
*/
|
let emitState: (state: MultiDestinationProgress) => void | undefined;
|
||||||
function emit(channel: string, message?: any) {
|
let emitFail: (data: any) => void | undefined;
|
||||||
ipc.of[IPC_SERVER_ID].emit(channel, message);
|
let emitDrives: (drives: Dictionary<DrivelistDrive>) => void | undefined;
|
||||||
}
|
let emitSourceMetadata: (
|
||||||
|
sourceMetadata: SourceMetadata | Record<string, never>,
|
||||||
|
) => void | undefined; // Record<string, never> means an empty object
|
||||||
|
|
||||||
/**
|
// Terminate the child process
|
||||||
* @summary Send a log debug message to the IPC server
|
async function terminate(exitCode?: number) {
|
||||||
*/
|
|
||||||
function log(message: string) {
|
|
||||||
if (console?.log) {
|
|
||||||
console.log(message);
|
|
||||||
}
|
|
||||||
emit('log', message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Terminate the child process
|
|
||||||
*/
|
|
||||||
async function terminate(exitCode: number) {
|
|
||||||
ipc.disconnect(IPC_SERVER_ID);
|
|
||||||
await cleanup(Date.now());
|
await cleanup(Date.now());
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
process.exit(exitCode || SUCCESS);
|
process.exit(exitCode || SUCCESS);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// kill the process if no initila connections or heartbeat for X sec (default 10)
|
||||||
* @summary Handle errors
|
function setTerminateTimeout() {
|
||||||
*/
|
if (ETCHER_TERMINATE_TIMEOUT > 0) {
|
||||||
async function handleError(error: Error) {
|
return setTimeout(() => {
|
||||||
emit('error', toJSON(error));
|
console.log(
|
||||||
await delay(DISCONNECT_DELAY);
|
`no connections or heartbeat for ${ETCHER_TERMINATE_TIMEOUT} ms, terminating`,
|
||||||
await terminate(GENERAL_ERROR);
|
);
|
||||||
|
terminate();
|
||||||
|
}, ETCHER_TERMINATE_TIMEOUT);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// terminate the process cleanly on SIGINT
|
||||||
* @summary Abort handler
|
process.once('SIGINT', async () => {
|
||||||
* @example
|
await terminate(SUCCESS);
|
||||||
*/
|
|
||||||
const onAbort = async (exitCode: number) => {
|
|
||||||
log('Abort');
|
|
||||||
emit('abort');
|
|
||||||
await delay(DISCONNECT_DELAY);
|
|
||||||
await terminate(exitCode);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSkip = async (exitCode: number) => {
|
|
||||||
log('Skip validation');
|
|
||||||
emit('skip');
|
|
||||||
await delay(DISCONNECT_DELAY);
|
|
||||||
await terminate(exitCode);
|
|
||||||
};
|
|
||||||
|
|
||||||
ipc.connectTo(IPC_SERVER_ID, () => {
|
|
||||||
// Gracefully exit on the following cases. If the parent
|
|
||||||
// process detects that child exit successfully but
|
|
||||||
// no flashing information is available, then it will
|
|
||||||
// assume that the child died halfway through.
|
|
||||||
|
|
||||||
process.once('uncaughtException', handleError);
|
|
||||||
|
|
||||||
process.once('SIGINT', async () => {
|
|
||||||
await terminate(SUCCESS);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.once('SIGTERM', async () => {
|
|
||||||
await terminate(SUCCESS);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The IPC server failed. Abort.
|
|
||||||
ipc.of[IPC_SERVER_ID].on('error', async () => {
|
|
||||||
await terminate(SUCCESS);
|
|
||||||
});
|
|
||||||
|
|
||||||
// The IPC server was disconnected. Abort.
|
|
||||||
ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
|
|
||||||
await terminate(SUCCESS);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.of[IPC_SERVER_ID].on('sourceMetadata', async (params) => {
|
|
||||||
const { selected, SourceType, auth } = JSON.parse(params);
|
|
||||||
try {
|
|
||||||
const sourceMatadata = await getSourceMetadata(
|
|
||||||
selected,
|
|
||||||
SourceType,
|
|
||||||
auth,
|
|
||||||
);
|
|
||||||
emitSourceMetadata(sourceMatadata);
|
|
||||||
} catch (error: any) {
|
|
||||||
emitFail(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.of[IPC_SERVER_ID].on('scan', async () => {
|
|
||||||
startScanning();
|
|
||||||
});
|
|
||||||
|
|
||||||
// write handler
|
|
||||||
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
|
|
||||||
// Remove leftover tmp files older than 1 hour
|
|
||||||
cleanup(Date.now() - 60 * 60 * 1000);
|
|
||||||
|
|
||||||
let exitCode = SUCCESS;
|
|
||||||
|
|
||||||
ipc.of[IPC_SERVER_ID].on('cancel', () => onAbort(exitCode));
|
|
||||||
|
|
||||||
ipc.of[IPC_SERVER_ID].on('skip', () => onSkip(exitCode));
|
|
||||||
|
|
||||||
const results = await write(options);
|
|
||||||
|
|
||||||
if (results.errors.length > 0) {
|
|
||||||
results.errors = results.errors.map((error: any) => {
|
|
||||||
return toJSON(error);
|
|
||||||
});
|
|
||||||
exitCode = GENERAL_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('done', { results });
|
|
||||||
await delay(DISCONNECT_DELAY);
|
|
||||||
await terminate(exitCode);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.of[IPC_SERVER_ID].on('connect', () => {
|
|
||||||
log(
|
|
||||||
`Successfully connected to IPC server: ${IPC_SERVER_ID}, socket root ${ipc.config.socketRoot}`,
|
|
||||||
);
|
|
||||||
emit('ready', {});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function emitLog(message: string) {
|
// terminate the process cleanly on SIGTERM
|
||||||
log(message);
|
process.once('SIGTERM', async () => {
|
||||||
|
await terminate(SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
let terminateInterval = setTerminateTimeout();
|
||||||
|
|
||||||
|
interface EmitLog {
|
||||||
|
emit: (channel: string, message: object | string) => void;
|
||||||
|
log: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitState(state: MultiDestinationProgress) {
|
function setup(): Promise<EmitLog> {
|
||||||
emit('state', state);
|
return new Promise((resolve, reject) => {
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
console.log('connection established... setting up');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Send a message to the IPC server
|
||||||
|
*/
|
||||||
|
function emit(type: string, payload?: object | string) {
|
||||||
|
ws.send(JSON.stringify({ type, payload }));
|
||||||
|
// ipc.of[IPC_SERVER_ID].emit("message", { type, payload });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Print logs and send them back to client
|
||||||
|
*/
|
||||||
|
function log(message: string) {
|
||||||
|
console.log(message);
|
||||||
|
emit('log', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Handle `errors`
|
||||||
|
*/
|
||||||
|
async function handleError(error: Error) {
|
||||||
|
emit('error', toJSON(error));
|
||||||
|
await terminate(GENERAL_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Handle `abort` from client
|
||||||
|
*/
|
||||||
|
const onAbort = async (exitCode: number) => {
|
||||||
|
log('Abort');
|
||||||
|
emit('abort');
|
||||||
|
await terminate(exitCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Handle `skip` from client; skip validation
|
||||||
|
*/
|
||||||
|
const onSkip = async (exitCode: number) => {
|
||||||
|
log('Skip validation');
|
||||||
|
emit('skip');
|
||||||
|
await terminate(exitCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Handle `write` from client; start writing to the drives
|
||||||
|
*/
|
||||||
|
const onWrite = async (options: WriteOptions) => {
|
||||||
|
log('write requested');
|
||||||
|
|
||||||
|
// Remove leftover tmp files older than 1 hour
|
||||||
|
cleanup(Date.now() - 60 * 60 * 1000);
|
||||||
|
|
||||||
|
let exitCode = SUCCESS;
|
||||||
|
|
||||||
|
// Write to the drives
|
||||||
|
const results = await write(options);
|
||||||
|
|
||||||
|
// handle potential errors from the write process
|
||||||
|
if (results.errors.length > 0) {
|
||||||
|
results.errors = results.errors.map(toJSON);
|
||||||
|
exitCode = GENERAL_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send the results back to the client
|
||||||
|
emit('done', { results });
|
||||||
|
|
||||||
|
// terminate this process
|
||||||
|
await terminate(exitCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Handle `sourceMetadata` from client; get source metadata
|
||||||
|
*/
|
||||||
|
const onSourceMetadata = async (params: any) => {
|
||||||
|
log('sourceMetadata requested');
|
||||||
|
const { selected, SourceType, auth } = JSON.parse(params);
|
||||||
|
try {
|
||||||
|
const sourceMatadata = await getSourceMetadata(
|
||||||
|
selected,
|
||||||
|
SourceType,
|
||||||
|
auth,
|
||||||
|
);
|
||||||
|
emitSourceMetadata(sourceMatadata);
|
||||||
|
} catch (error: any) {
|
||||||
|
emitFail(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle uncaught exceptions
|
||||||
|
process.once('uncaughtException', handleError);
|
||||||
|
|
||||||
|
// terminate the process if the connection is closed
|
||||||
|
ws.on('error', async () => {
|
||||||
|
await terminate(SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
// route messages from the client by `type`
|
||||||
|
const messagesHandler: any = {
|
||||||
|
// terminate the process
|
||||||
|
terminate: () => terminate(SUCCESS),
|
||||||
|
|
||||||
|
/*
|
||||||
|
receive a `heartbeat`, reset the terminate timeout
|
||||||
|
this mechanism ensure the process will be terminated if the client is disconnected
|
||||||
|
*/
|
||||||
|
heartbeat: () => {
|
||||||
|
if (terminateInterval) {
|
||||||
|
clearTimeout(terminateInterval);
|
||||||
|
}
|
||||||
|
terminateInterval = setTerminateTimeout();
|
||||||
|
},
|
||||||
|
|
||||||
|
// resolve the setup promise when the client is ready
|
||||||
|
ready: () => {
|
||||||
|
log('Ready ...');
|
||||||
|
resolve({ emit, log });
|
||||||
|
},
|
||||||
|
|
||||||
|
// start scanning for drives
|
||||||
|
scan: () => {
|
||||||
|
log('Scan requested');
|
||||||
|
startScanning();
|
||||||
|
},
|
||||||
|
|
||||||
|
// route `cancel` from client
|
||||||
|
cancel: () => onAbort(GENERAL_ERROR),
|
||||||
|
|
||||||
|
// route `skip` from client
|
||||||
|
skip: () => onSkip(GENERAL_ERROR),
|
||||||
|
|
||||||
|
// route `write` from client
|
||||||
|
write: async (options: WriteOptions) => onWrite(options),
|
||||||
|
|
||||||
|
// route `sourceMetadata` from client
|
||||||
|
sourceMetadata: async (params: any) => onSourceMetadata(params),
|
||||||
|
};
|
||||||
|
|
||||||
|
// message handler, parse and route messages coming on WS
|
||||||
|
ws.on('message', async (jsonData: any) => {
|
||||||
|
const data = JSON.parse(jsonData);
|
||||||
|
const message = messagesHandler[data.type];
|
||||||
|
if (message) {
|
||||||
|
await message(data.payload);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown message type: ${data.type}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// inform the client that the server is ready to receive messages
|
||||||
|
emit('ready', {});
|
||||||
|
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitFail(data: any) {
|
// setTimeout(() => console.log('wss', wss.address()), 1000);
|
||||||
emit('fail', data);
|
console.log('waiting for connection...');
|
||||||
}
|
|
||||||
|
|
||||||
function emitDrives(drives: Dictionary<DrivelistDrive>) {
|
setup().then(({ emit, log }: EmitLog) => {
|
||||||
emit('drives', JSON.stringify(values(drives)));
|
// connection is established, clear initial terminate timeout
|
||||||
}
|
if (terminateInterval) {
|
||||||
|
clearInterval(terminateInterval);
|
||||||
|
}
|
||||||
|
|
||||||
function emitSourceMetadata(sourceMetadata: any) {
|
console.log('waiting for instruction...');
|
||||||
emit('sourceMetadata', JSON.stringify(sourceMetadata));
|
|
||||||
}
|
// set the exportable emit functions
|
||||||
|
emitLog = (message) => {
|
||||||
|
log(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
emitState = (state) => {
|
||||||
|
emit('state', state);
|
||||||
|
};
|
||||||
|
|
||||||
|
emitFail = (data) => {
|
||||||
|
emit('fail', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
emitDrives = (drives) => {
|
||||||
|
emit('drives', JSON.stringify(values(drives)));
|
||||||
|
};
|
||||||
|
|
||||||
|
emitSourceMetadata = (sourceMetadata) => {
|
||||||
|
emit('sourceMetadata', JSON.stringify(sourceMetadata));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export { emitLog, emitState, emitFail, emitDrives, emitSourceMetadata };
|
export { emitLog, emitState, emitFail, emitDrives, emitSourceMetadata };
|
||||||
|
@ -16,26 +16,24 @@
|
|||||||
* This file handles the writer process.
|
* This file handles the writer process.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
OnProgressFunction,
|
OnProgressFunction,
|
||||||
OnFailFunction,
|
OnFailFunction,
|
||||||
|
MultiDestinationProgress,
|
||||||
|
} from 'etcher-sdk/build/multi-write';
|
||||||
|
import {
|
||||||
decompressThenFlash,
|
decompressThenFlash,
|
||||||
DECOMPRESSED_IMAGE_PREFIX,
|
DECOMPRESSED_IMAGE_PREFIX,
|
||||||
MultiDestinationProgress,
|
|
||||||
} from 'etcher-sdk/build/multi-write';
|
} from 'etcher-sdk/build/multi-write';
|
||||||
|
|
||||||
import { totalmem } from 'os';
|
import { totalmem } from 'os';
|
||||||
|
|
||||||
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||||
|
|
||||||
import {
|
import type { SourceDestination } from 'etcher-sdk/build/source-destination';
|
||||||
File,
|
import { File, Http, BlockDevice } from 'etcher-sdk/build/source-destination';
|
||||||
Http,
|
|
||||||
BlockDevice,
|
|
||||||
SourceDestination,
|
|
||||||
} from 'etcher-sdk/build/source-destination';
|
|
||||||
|
|
||||||
import { WriteResult, FlashError, WriteOptions } from './types/types';
|
import type { WriteResult, FlashError, WriteOptions } from './types/types';
|
||||||
|
|
||||||
import { isJson } from '../shared/utils';
|
import { isJson } from '../shared/utils';
|
||||||
import { toJSON } from '../shared/errors';
|
import { toJSON } from '../shared/errors';
|
||||||
@ -146,7 +144,7 @@ export async function cleanup(until: number) {
|
|||||||
* @param {Boolean} autoBlockmapping - whether to trim ext partitions before writing
|
* @param {Boolean} autoBlockmapping - whether to trim ext partitions before writing
|
||||||
* @param {Function} onProgress - function to call on progress
|
* @param {Function} onProgress - function to call on progress
|
||||||
* @param {Function} onFail - function to call on fail
|
* @param {Function} onFail - function to call on fail
|
||||||
* @returns {Promise<{ bytesWritten, devices, errors} >}
|
* @returns {Promise<{ bytesWritten, devices, errors }>}
|
||||||
*/
|
*/
|
||||||
async function writeAndValidate({
|
async function writeAndValidate({
|
||||||
source,
|
source,
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as sdk from 'etcher-sdk';
|
import * as sdk from 'etcher-sdk';
|
||||||
|
import type { Adapter } from 'etcher-sdk/build/scanner/adapters';
|
||||||
import {
|
import {
|
||||||
Adapter,
|
|
||||||
BlockDeviceAdapter,
|
BlockDeviceAdapter,
|
||||||
UsbbootDeviceAdapter,
|
UsbbootDeviceAdapter,
|
||||||
} from 'etcher-sdk/build/scanner/adapters';
|
} from 'etcher-sdk/build/scanner/adapters';
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { scanner as driveScanner } from './drive-scanner';
|
import { scanner as driveScanner } from './drive-scanner';
|
||||||
import * as sdk from 'etcher-sdk';
|
import * as sdk from 'etcher-sdk';
|
||||||
import { DrivelistDrive } from '../shared/drive-constraints';
|
import type { DrivelistDrive } from '../shared/drive-constraints';
|
||||||
import outdent from 'outdent';
|
import outdent from 'outdent';
|
||||||
import { Dictionary, values, keyBy, padStart } from 'lodash';
|
import type { Dictionary } from 'lodash';
|
||||||
|
import { values, keyBy, padStart } from 'lodash';
|
||||||
import { emitDrives } from './api';
|
import { emitDrives } from './api';
|
||||||
|
|
||||||
let availableDrives: DrivelistDrive[] = [];
|
let availableDrives: DrivelistDrive[] = [];
|
||||||
|
@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
import { sourceDestination } from 'etcher-sdk';
|
import { sourceDestination } from 'etcher-sdk';
|
||||||
import { replaceWindowsNetworkDriveLetter } from '../gui/app/os/windows-network-drives';
|
import { replaceWindowsNetworkDriveLetter } from '../gui/app/os/windows-network-drives';
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
import { isJson } from '../shared/utils';
|
import { isJson } from '../shared/utils';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {
|
import type {
|
||||||
SourceMetadata,
|
SourceMetadata,
|
||||||
Authentication,
|
Authentication,
|
||||||
Source,
|
Source,
|
||||||
} from '../shared/typings/source-selector';
|
} from '../shared/typings/source-selector';
|
||||||
import { DrivelistDrive } from '../shared/drive-constraints';
|
import type { DrivelistDrive } from '../shared/drive-constraints';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
function isString(value: any): value is string {
|
function isString(value: any): value is string {
|
||||||
@ -68,7 +69,8 @@ async function getSourceMetadata(
|
|||||||
selected: string | DrivelistDrive,
|
selected: string | DrivelistDrive,
|
||||||
SourceType: Source,
|
SourceType: Source,
|
||||||
auth?: Authentication,
|
auth?: Authentication,
|
||||||
) {
|
): Promise<SourceMetadata | Record<string, never>> {
|
||||||
|
// `Record<string, never>` means an empty object
|
||||||
if (isString(selected)) {
|
if (isString(selected)) {
|
||||||
const source = await createSource(selected, SourceType, auth);
|
const source = await createSource(selected, SourceType, auth);
|
||||||
|
|
||||||
@ -80,13 +82,12 @@ async function getSourceMetadata(
|
|||||||
return metadata;
|
return metadata;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
|
return {};
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
await source.close();
|
||||||
await source.close();
|
|
||||||
} catch (error: any) {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
lib/util/types/types.d.ts
vendored
6
lib/util/types/types.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
import { Metadata } from 'etcher-sdk/build/source-destination';
|
import type { Metadata } from 'etcher-sdk/build/source-destination';
|
||||||
import { SourceMetadata } from '../../shared/typings/source-selector';
|
import type { SourceMetadata } from '../../shared/typings/source-selector';
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
import type { Drive as DrivelistDrive } from 'drivelist';
|
||||||
|
|
||||||
export interface WriteResult {
|
export interface WriteResult {
|
||||||
bytesWritten?: number;
|
bytesWritten?: number;
|
||||||
|
35383
npm-shrinkwrap.json
generated
35383
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
91
package.json
91
package.json
@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "balenaEtcher",
|
"displayName": "balenaEtcher",
|
||||||
"productName": "balenaEtcher",
|
"productName": "balenaEtcher",
|
||||||
"version": "1.19.7",
|
"version": "2.1.3",
|
||||||
"packageType": "local",
|
"packageType": "local",
|
||||||
"main": ".webpack/main",
|
"main": ".webpack/main",
|
||||||
"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.",
|
||||||
@ -16,12 +16,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"prettify": "prettier --write lib/**/*.css && balena-lint --fix --typescript typings lib tests forge.config.ts forge.sidecar.ts webpack.config.ts",
|
"prettify": "prettier --write lib/**/*.css && balena-lint --fix --typescript typings lib tests forge.config.ts forge.sidecar.ts webpack.config.ts",
|
||||||
"lint": "npm run prettify && catch-uncommitted",
|
"lint": "npm run prettify && catch-uncommitted",
|
||||||
"test-gui": "xvfb-maybe electron-mocha --recursive --reporter spec --window-config tests/gui/window-config.json --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
|
"test": "echo 'Only use custom tests; if you want to test locally, use `npm run wdio`' && exit 0",
|
||||||
"test-shared": "xvfb-maybe electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
|
|
||||||
"test": "npm run test-gui && npm run test-shared",
|
|
||||||
"package": "electron-forge package",
|
"package": "electron-forge package",
|
||||||
"start": "electron-forge start",
|
"start": "electron-forge start",
|
||||||
"make": "electron-forge make"
|
"make": "electron-forge make",
|
||||||
|
"wdio": "xvfb-maybe wdio run ./wdio.conf.ts"
|
||||||
},
|
},
|
||||||
"husky": {
|
"husky": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
@ -31,70 +30,69 @@
|
|||||||
"author": "Balena Ltd. <hello@balena.io>",
|
"author": "Balena Ltd. <hello@balena.io>",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
|
"@electron/remote": "^2.1.2",
|
||||||
"@electron/remote": "^2.1.0",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@fortawesome/fontawesome-free": "6.5.1",
|
"@ronomon/direct-io": "^3.0.1",
|
||||||
"@sentry/electron": "^4.15.1",
|
"@sentry/electron": "^4.24.0",
|
||||||
"analytics-client": "^2.0.1",
|
"axios": "^1.6.8",
|
||||||
"axios": "^1.6.0",
|
|
||||||
"debug": "4.3.4",
|
"debug": "4.3.4",
|
||||||
|
"drivelist": "^12.0.2",
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
"electron-updater": "6.1.7",
|
"electron-updater": "6.1.8",
|
||||||
"etcher-sdk": "9.0.0",
|
"etcher-sdk": "9.1.2",
|
||||||
"i18next": "23.7.8",
|
"i18next": "23.11.2",
|
||||||
"immutable": "3.8.2",
|
"immutable": "3.8.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"node-ipc": "9.2.1",
|
|
||||||
"outdent": "0.8.0",
|
"outdent": "0.8.0",
|
||||||
"path-is-inside": "1.0.2",
|
"path-is-inside": "1.0.2",
|
||||||
"pretty-bytes": "5.6.0",
|
"pretty-bytes": "6.1.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-i18next": "13.5.0",
|
"react-i18next": "13.5.0",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"rendition": "35.1.2",
|
"rendition": "35.2.0",
|
||||||
"semver": "7.5.4",
|
"semver": "7.6.0",
|
||||||
"styled-components": "5.3.6",
|
"styled-components": "5.3.6",
|
||||||
"sys-class-rgb-led": "3.0.1",
|
"sys-class-rgb-led": "3.0.1",
|
||||||
"uuid": "9.0.1"
|
"uuid": "9.0.1",
|
||||||
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@balena/lint": "7.2.4",
|
"@balena/lint": "8.0.2",
|
||||||
"@electron-forge/cli": "7.2.0",
|
"@electron-forge/cli": "7.4.0",
|
||||||
"@electron-forge/maker-deb": "7.2.0",
|
"@electron-forge/maker-deb": "7.4.0",
|
||||||
"@electron-forge/maker-dmg": "7.2.0",
|
"@electron-forge/maker-dmg": "7.4.0",
|
||||||
"@electron-forge/maker-rpm": "7.2.0",
|
"@electron-forge/maker-rpm": "7.4.0",
|
||||||
"@electron-forge/maker-squirrel": "7.2.0",
|
"@electron-forge/maker-squirrel": "7.4.0",
|
||||||
"@electron-forge/maker-zip": "7.2.0",
|
"@electron-forge/maker-zip": "7.4.0",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "7.2.0",
|
"@electron-forge/plugin-auto-unpack-natives": "7.4.0",
|
||||||
"@electron-forge/plugin-webpack": "7.2.0",
|
"@electron-forge/plugin-webpack": "7.4.0",
|
||||||
"@reforged/maker-appimage": "3.3.2",
|
"@reforged/maker-appimage": "3.3.2",
|
||||||
"@svgr/webpack": "8.1.0",
|
"@svgr/webpack": "8.1.0",
|
||||||
"@types/chai": "4.3.11",
|
"@types/chai": "4.3.14",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/mocha": "^10.0.6",
|
"@types/node": "^20.11.6",
|
||||||
"@types/node": "^18.11.9",
|
|
||||||
"@types/node-ipc": "9.2.3",
|
|
||||||
"@types/react": "17.0.2",
|
"@types/react": "17.0.2",
|
||||||
"@types/react-dom": "17.0.2",
|
"@types/react-dom": "17.0.2",
|
||||||
"@types/semver": "7.5.6",
|
"@types/semver": "7.5.8",
|
||||||
"@types/sinon": "17.0.2",
|
"@types/sinon": "17.0.3",
|
||||||
"@types/tmp": "0.2.6",
|
"@types/tmp": "0.2.6",
|
||||||
"@vercel/webpack-asset-relocator-loader": "1.7.3",
|
"@vercel/webpack-asset-relocator-loader": "1.7.3",
|
||||||
|
"@wdio/cli": "^8.36.1",
|
||||||
|
"@wdio/local-runner": "^8.36.1",
|
||||||
|
"@wdio/mocha-framework": "^8.36.1",
|
||||||
|
"@wdio/spec-reporter": "^8.36.1",
|
||||||
|
"@yao-pkg/pkg": "^5.11.5",
|
||||||
"catch-uncommitted": "^2.0.0",
|
"catch-uncommitted": "^2.0.0",
|
||||||
"chai": "4.3.10",
|
"chai": "4.3.10",
|
||||||
"css-loader": "5.2.7",
|
"css-loader": "5.2.7",
|
||||||
"electron": "27.1.3",
|
"electron": "30.0.1",
|
||||||
"electron-mocha": "^12.2.0",
|
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"husky": "8.0.3",
|
"husky": "8.0.3",
|
||||||
"mocha": "^10.2.0",
|
|
||||||
"native-addon-loader": "2.0.1",
|
"native-addon-loader": "2.0.1",
|
||||||
"node-loader": "^2.0.0",
|
"node-loader": "^2.0.0",
|
||||||
"omit-deep-lodash": "1.1.7",
|
"sinon": "^17.0.1",
|
||||||
"pkg": "^5.8.1",
|
|
||||||
"sinon": "17.0.1",
|
|
||||||
"string-replace-loader": "3.1.0",
|
"string-replace-loader": "3.1.0",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
@ -102,12 +100,11 @@
|
|||||||
"tslib": "2.6.2",
|
"tslib": "2.6.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"url-loader": "4.1.1",
|
"url-loader": "4.1.1",
|
||||||
|
"wdio-electron-service": "^6.4.1",
|
||||||
"xvfb-maybe": "^0.2.1"
|
"xvfb-maybe": "^0.2.1"
|
||||||
},
|
},
|
||||||
"hostDependencies": {
|
"hostDependencies": {
|
||||||
"debian": [
|
"debian": [
|
||||||
"gconf-service",
|
|
||||||
"gconf2",
|
|
||||||
"libasound2",
|
"libasound2",
|
||||||
"libatk1.0-0",
|
"libatk1.0-0",
|
||||||
"libc6",
|
"libc6",
|
||||||
@ -119,7 +116,6 @@
|
|||||||
"libfreetype6",
|
"libfreetype6",
|
||||||
"libgbm1",
|
"libgbm1",
|
||||||
"libgcc1",
|
"libgcc1",
|
||||||
"libgconf-2-4",
|
|
||||||
"libgdk-pixbuf2.0-0",
|
"libgdk-pixbuf2.0-0",
|
||||||
"libglib2.0-0",
|
"libglib2.0-0",
|
||||||
"libgtk-3-0",
|
"libgtk-3-0",
|
||||||
@ -144,9 +140,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18 <20"
|
"node": ">=20 <21"
|
||||||
},
|
},
|
||||||
"versionist": {
|
"versionist": {
|
||||||
"publishedAt": "2024-04-22T06:52:19.375Z"
|
"publishedAt": "2025-05-15T18:09:56.320Z"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bufferutil": "^4.0.8",
|
||||||
|
"utf-8-validate": "^5.0.10",
|
||||||
|
"winusb-driver-generator": "2.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
182
test-wrapper.ts
182
test-wrapper.ts
@ -1,182 +0,0 @@
|
|||||||
/*
|
|
||||||
* This is a test wrapper for etcher-utils.
|
|
||||||
* The only use for this file is debugging while developing etcher-utils.
|
|
||||||
* It will create a IPC server, spawn the cli version of etcher-writer, and wait for it to connect.
|
|
||||||
* Requires elevated privileges to work (launch with sudo)
|
|
||||||
* Note that you'll need to to edit `ipc.server.on('ready', ...` function based on what you want to test.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as ipc from 'node-ipc';
|
|
||||||
import * as os from 'os';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import * as packageJSON from './package.json';
|
|
||||||
import * as permissions from './lib/shared/permissions';
|
|
||||||
|
|
||||||
// if (process.argv.length !== 3) {
|
|
||||||
// console.error('Expects an image to flash as only arg!');
|
|
||||||
// process.exit(1);
|
|
||||||
// }
|
|
||||||
|
|
||||||
const THREADS_PER_CPU = 16;
|
|
||||||
|
|
||||||
// There might be multiple Etcher instances running at
|
|
||||||
// the same time, therefore we must ensure each IPC
|
|
||||||
// server/client has a different name.
|
|
||||||
const IPC_SERVER_ID = `etcher-server-${process.pid}`;
|
|
||||||
const IPC_CLIENT_ID = `etcher-client-${process.pid}`;
|
|
||||||
|
|
||||||
ipc.config.id = IPC_SERVER_ID;
|
|
||||||
ipc.config.socketRoot = path.join(
|
|
||||||
process.env.XDG_RUNTIME_DIR || os.tmpdir(),
|
|
||||||
path.sep,
|
|
||||||
);
|
|
||||||
|
|
||||||
// NOTE: Ensure this isn't disabled, as it will cause
|
|
||||||
// the stdout maxBuffer size to be exceeded when flashing
|
|
||||||
ipc.config.silent = true;
|
|
||||||
|
|
||||||
function writerArgv(): string[] {
|
|
||||||
const entryPoint = path.join('./generated/etcher-util');
|
|
||||||
return [entryPoint];
|
|
||||||
}
|
|
||||||
|
|
||||||
function writerEnv() {
|
|
||||||
return {
|
|
||||||
IPC_SERVER_ID,
|
|
||||||
IPC_CLIENT_ID,
|
|
||||||
IPC_SOCKET_ROOT: ipc.config.socketRoot,
|
|
||||||
UV_THREADPOOL_SIZE: (os.cpus().length * THREADS_PER_CPU).toString(),
|
|
||||||
// This environment variable prevents the AppImages
|
|
||||||
// desktop integration script from presenting the
|
|
||||||
// "installation" dialog
|
|
||||||
SKIP: '1',
|
|
||||||
...(process.platform === 'win32' ? {} : process.env),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function start(): Promise<any> {
|
|
||||||
ipc.serve();
|
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
ipc.server.on('error', (message) => {
|
|
||||||
console.log('IPC server error', message);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on('log', (message) => {
|
|
||||||
console.log('log', message);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on('fail', ({ device, error }) => {
|
|
||||||
console.log('failure', error, device);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on('done', (event) => {
|
|
||||||
console.log('done', event);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on('abort', () => {
|
|
||||||
console.log('abort');
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on('skip', () => {
|
|
||||||
console.log('skip');
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on('state', (progress) => {
|
|
||||||
console.log('progress', progress);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on('drives', (drives) => {
|
|
||||||
console.log('drives', drives);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipc.server.on('ready', (_data, socket) => {
|
|
||||||
console.log('ready');
|
|
||||||
ipc.server.emit(socket, 'scan', {});
|
|
||||||
// ipc.server.emit(socket, "hello", { message: "world" });
|
|
||||||
// ipc.server.emit(socket, "write", {
|
|
||||||
// image: {
|
|
||||||
// path: process.argv[2],
|
|
||||||
// displayName: "Random image for test",
|
|
||||||
// description: "Random image for test",
|
|
||||||
// SourceType: "File",
|
|
||||||
// },
|
|
||||||
// destinations: [
|
|
||||||
// {
|
|
||||||
// size: 15938355200,
|
|
||||||
// isVirtual: false,
|
|
||||||
// enumerator: "DiskArbitration",
|
|
||||||
// logicalBlockSize: 512,
|
|
||||||
// raw: "/dev/rdisk4",
|
|
||||||
// error: null,
|
|
||||||
// isReadOnly: false,
|
|
||||||
// displayName: "/dev/disk4",
|
|
||||||
// blockSize: 512,
|
|
||||||
// isSCSI: false,
|
|
||||||
// isRemovable: true,
|
|
||||||
// device: "/dev/disk4",
|
|
||||||
// busVersion: null,
|
|
||||||
// isSystem: false,
|
|
||||||
// busType: "USB",
|
|
||||||
// isCard: false,
|
|
||||||
// isUSB: true,
|
|
||||||
// devicePath:
|
|
||||||
// "IODeviceTree:/arm-io@10F00000/usb-drd1@2280000/usb-drd1-port-hs@01100000",
|
|
||||||
// mountpoints: [
|
|
||||||
// {
|
|
||||||
// path: "/Volumes/flash-rootB",
|
|
||||||
// label: "flash-rootB",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: "/Volumes/flash-rootA",
|
|
||||||
// label: "flash-rootA",
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// path: "/Volumes/flash-boot",
|
|
||||||
// label: "flash-boot",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// description: "Generic Flash Disk Media",
|
|
||||||
// isUAS: null,
|
|
||||||
// partitionTableType: "mbr",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// SourceType: "File",
|
|
||||||
// autoBlockmapping: true,
|
|
||||||
// decompressFirst: true,
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
|
|
||||||
const argv = writerArgv();
|
|
||||||
|
|
||||||
ipc.server.on('start', async () => {
|
|
||||||
console.log(`Elevating command: ${argv.join(' ')}`);
|
|
||||||
const env = writerEnv();
|
|
||||||
try {
|
|
||||||
await permissions.elevateCommand(argv, {
|
|
||||||
applicationName: packageJSON.displayName,
|
|
||||||
environment: env,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.log('error', error);
|
|
||||||
// This happens when the child is killed using SIGKILL
|
|
||||||
const SIGKILL_EXIT_CODE = 137;
|
|
||||||
if (error.code === SIGKILL_EXIT_CODE) {
|
|
||||||
error.code = 'ECHILDDIED';
|
|
||||||
}
|
|
||||||
reject(error);
|
|
||||||
} finally {
|
|
||||||
console.log('Terminating IPC server');
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the update lock timer to prevent longer
|
|
||||||
// flashing timing it out, and releasing the lock
|
|
||||||
ipc.server.start();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
start();
|
|
@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { File } from 'etcher-sdk/build/source-destination';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
||||||
@ -165,7 +164,7 @@ describe('Model: availableDrives', function () {
|
|||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
SourceType: File,
|
SourceType: 'File',
|
||||||
recommendedDriveSize: 2000000000,
|
recommendedDriveSize: 2000000000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,13 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { File } from 'etcher-sdk/build/source-destination';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector';
|
import type { SourceMetadata } from '../../../lib/shared/typings/source-selector';
|
||||||
|
|
||||||
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
||||||
import * as selectionState from '../../../lib/gui/app/models/selection-state';
|
import * as selectionState from '../../../lib/gui/app/models/selection-state';
|
||||||
import { DrivelistDrive } from '../../../lib/shared/drive-constraints';
|
import type { DrivelistDrive } from '../../../lib/shared/drive-constraints';
|
||||||
|
|
||||||
describe('Model: selectionState', function () {
|
describe('Model: selectionState', function () {
|
||||||
describe('given a clean state', function () {
|
describe('given a clean state', function () {
|
||||||
@ -375,7 +374,7 @@ describe('Model: selectionState', function () {
|
|||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
SourceType: File,
|
SourceType: 'File',
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImage()?.path;
|
const imagePath = selectionState.getImage()?.path;
|
||||||
@ -408,7 +407,7 @@ describe('Model: selectionState', function () {
|
|||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
SourceType: File,
|
SourceType: 'File',
|
||||||
recommendedDriveSize: 2000000000,
|
recommendedDriveSize: 2000000000,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -581,7 +580,7 @@ describe('Model: selectionState', function () {
|
|||||||
path: 'foo.img',
|
path: 'foo.img',
|
||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
SourceType: File,
|
SourceType: 'File',
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -670,7 +669,7 @@ describe('Model: selectionState', function () {
|
|||||||
path: 'foo.img',
|
path: 'foo.img',
|
||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
SourceType: File,
|
SourceType: 'File',
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 balena.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { expect } from 'chai';
|
|
||||||
import * as ipc from 'node-ipc';
|
|
||||||
|
|
||||||
import('../../../lib/gui/modules/child-writer');
|
|
||||||
|
|
||||||
describe('Browser: childWriter', function () {
|
|
||||||
it('should have the ipc config set to silent', function () {
|
|
||||||
expect(ipc.config.silent).to.be.true;
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* TODO:
|
||||||
|
* This test should be replaced by an E2E test.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2020 balena.io
|
* Copyright 2020 balena.io
|
||||||
*
|
*
|
||||||
@ -15,12 +22,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
import type { Drive as DrivelistDrive } from 'drivelist';
|
||||||
import { sourceDestination } from 'etcher-sdk';
|
import type { SinonStub } from 'sinon';
|
||||||
import * as ipc from 'node-ipc';
|
import { assert, stub } from 'sinon';
|
||||||
import { assert, SinonStub, stub } from 'sinon';
|
|
||||||
|
|
||||||
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector';
|
import type { SourceMetadata } from '../../../lib/shared/typings/source-selector';
|
||||||
import * as flashState from '../../../lib/gui/app/models/flash-state';
|
import * as flashState from '../../../lib/gui/app/models/flash-state';
|
||||||
import * as imageWriter from '../../../lib/gui/app/modules/image-writer';
|
import * as imageWriter from '../../../lib/gui/app/modules/image-writer';
|
||||||
|
|
||||||
@ -35,7 +41,7 @@ describe('Browser: imageWriter', () => {
|
|||||||
description: 'foo.img',
|
description: 'foo.img',
|
||||||
displayName: 'foo.img',
|
displayName: 'foo.img',
|
||||||
path: 'foo.img',
|
path: 'foo.img',
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
extension: 'img',
|
extension: 'img',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -140,11 +146,4 @@ describe('Browser: imageWriter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.performWrite()', function () {
|
|
||||||
it('should set the ipc config to silent', function () {
|
|
||||||
// Reset this value as it can persist from other tests
|
|
||||||
expect(ipc.config.silent).to.be.true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -15,12 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import * as i18next from 'i18next';
|
||||||
|
import en_translation from '../../../lib/gui/app/i18n/en';
|
||||||
|
|
||||||
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 () {
|
||||||
describe('.titleFromFlashState()', function () {
|
describe('.titleFromFlashState()', function () {
|
||||||
beforeEach(function () {
|
beforeEach(async function () {
|
||||||
this.state = {
|
this.state = {
|
||||||
active: 1,
|
active: 1,
|
||||||
type: 'flashing',
|
type: 'flashing',
|
||||||
@ -29,6 +31,13 @@ describe('Browser: progressStatus', function () {
|
|||||||
eta: 15,
|
eta: 15,
|
||||||
speed: 100000000000000,
|
speed: 100000000000000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await i18next.init({
|
||||||
|
lng: 'en', // Set the default language
|
||||||
|
resources: {
|
||||||
|
en: en_translation,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should report 0% if percentage == 0 but speed != 0', function () {
|
it('should report 0% if percentage == 0 but speed != 0', function () {
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
*
|
||||||
|
* TODO:
|
||||||
|
* This test should be replaced by an E2E test.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 balena.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { SinonStub, stub } from 'sinon';
|
import type { SinonStub } from 'sinon';
|
||||||
|
import { stub } from 'sinon';
|
||||||
|
|
||||||
import * as wnd from '../../../lib/gui/app/os/windows-network-drives';
|
import * as wnd from '../../../lib/gui/app/os/windows-network-drives';
|
||||||
|
|
||||||
|
@ -15,9 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { sourceDestination } from 'etcher-sdk';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { SourceMetadata } from '../../lib/gui/app/components/source-selector/source-selector';
|
import type { SourceMetadata } from '../../lib/shared/typings/source-selector';
|
||||||
|
|
||||||
import * as constraints from '../../lib/shared/drive-constraints';
|
import * as constraints from '../../lib/shared/drive-constraints';
|
||||||
import * as messages from '../../lib/shared/messages';
|
import * as messages from '../../lib/shared/messages';
|
||||||
@ -87,7 +86,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: '/Volumes/Untitled/image.img',
|
path: '/Volumes/Untitled/image.img',
|
||||||
hasMBR: false,
|
hasMBR: false,
|
||||||
partitions: [],
|
partitions: [],
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -101,7 +100,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: 'E:\\image.img',
|
path: 'E:\\image.img',
|
||||||
hasMBR: false,
|
hasMBR: false,
|
||||||
partitions: [],
|
partitions: [],
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
};
|
};
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.separator = path.sep;
|
this.separator = path.sep;
|
||||||
@ -207,7 +206,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: '/Volumes/Untitled/image.img',
|
path: '/Volumes/Untitled/image.img',
|
||||||
hasMBR: false,
|
hasMBR: false,
|
||||||
partitions: [],
|
partitions: [],
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
};
|
};
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.separator = path.sep;
|
this.separator = path.sep;
|
||||||
@ -522,7 +521,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
size: 1000000000,
|
size: 1000000000,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
recommendedDriveSize: 2000000000,
|
recommendedDriveSize: 2000000000,
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
};
|
};
|
||||||
it('should return true if the drive size is greater than the recommended size ', function () {
|
it('should return true if the drive size is greater than the recommended size ', function () {
|
||||||
const result = constraints.isDriveSizeRecommended(
|
const result = constraints.isDriveSizeRecommended(
|
||||||
@ -626,7 +625,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
description: 'rpi.img',
|
description: 'rpi.img',
|
||||||
displayName: 'rpi.img',
|
displayName: 'rpi.img',
|
||||||
path: '',
|
path: '',
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
size: 2000000000,
|
size: 2000000000,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
};
|
};
|
||||||
@ -672,7 +671,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
description: 'rpi.img',
|
description: 'rpi.img',
|
||||||
displayName: 'rpi.img',
|
displayName: 'rpi.img',
|
||||||
path: '',
|
path: '',
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
size: 2000000000,
|
size: 2000000000,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
};
|
};
|
||||||
@ -720,7 +719,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
description: 'rpi.img',
|
description: 'rpi.img',
|
||||||
displayName: 'rpi.img',
|
displayName: 'rpi.img',
|
||||||
path: '',
|
path: '',
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
size: 2000000000,
|
size: 2000000000,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
};
|
};
|
||||||
@ -829,7 +828,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.image = {
|
this.image = {
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
path: path.join(__dirname, 'rpi.img'),
|
||||||
size: this.drive.size - 1,
|
size: this.drive.size - 1,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
@ -874,7 +873,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.image = {
|
this.image = {
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
path: path.join(__dirname, 'rpi.img'),
|
||||||
size: this.drive.size - 1,
|
size: this.drive.size - 1,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
@ -1227,7 +1226,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
description: 'rpi.img',
|
description: 'rpi.img',
|
||||||
displayName: 'rpi.img',
|
displayName: 'rpi.img',
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
path: path.join(__dirname, 'rpi.img'),
|
||||||
SourceType: sourceDestination.File,
|
SourceType: 'File',
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
size: drives[2].size + 1,
|
size: drives[2].size + 1,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
|
7
tests/test.e2e.ts
Normal file
7
tests/test.e2e.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { browser } from '@wdio/globals';
|
||||||
|
|
||||||
|
describe('Electron Testing', () => {
|
||||||
|
it('should print application title', async () => {
|
||||||
|
console.log('Hello', await browser.getTitle(), 'application!');
|
||||||
|
});
|
||||||
|
});
|
@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "es2019",
|
"target": "es2019",
|
||||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"lib": ["dom", "esnext"],
|
"lib": ["dom", "esnext"],
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"pretty": true,
|
"pretty": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
318
wdio.conf.ts
Normal file
318
wdio.conf.ts
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
/// <reference types="wdio-electron-service" />
|
||||||
|
import type { Options } from '@wdio/types';
|
||||||
|
|
||||||
|
export const config: Options.Testrunner = {
|
||||||
|
//
|
||||||
|
// ====================
|
||||||
|
// Runner Configuration
|
||||||
|
// ====================
|
||||||
|
// WebdriverIO supports running e2e tests as well as unit and component tests.
|
||||||
|
runner: 'local',
|
||||||
|
autoCompileOpts: {
|
||||||
|
autoCompile: true,
|
||||||
|
tsNodeOpts: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
transpileOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// ==================
|
||||||
|
// Specify Test Files
|
||||||
|
// ==================
|
||||||
|
// Define which test specs should run. The pattern is relative to the directory
|
||||||
|
// of the configuration file being run.
|
||||||
|
//
|
||||||
|
// The specs are defined as an array of spec files (optionally using wildcards
|
||||||
|
// that will be expanded). The test for each spec file will be run in a separate
|
||||||
|
// worker process. In order to have a group of spec files run in the same worker
|
||||||
|
// process simply enclose them in an array within the specs array.
|
||||||
|
//
|
||||||
|
// The path of the spec files will be resolved relative from the directory of
|
||||||
|
// of the config file unless it's absolute.
|
||||||
|
//
|
||||||
|
specs: ['./tests/**/*.spec.ts'],
|
||||||
|
// Patterns to exclude.
|
||||||
|
// FIXME: Remove the following exclusions once the tests are ported to WDIO
|
||||||
|
exclude: [
|
||||||
|
'tests/gui/modules/image-writer.spec.ts',
|
||||||
|
'tests/gui/os/window-progress.spec.ts',
|
||||||
|
'tests/gui/models/available-drives.spec.ts',
|
||||||
|
'tests/gui/models/flash-state.spec.ts',
|
||||||
|
'tests/gui/models/selection-state.spec.ts',
|
||||||
|
'tests/gui/models/settings.spec.ts',
|
||||||
|
'tests/shared/drive-constraints.spec.ts',
|
||||||
|
'tests/shared/messages.spec.ts',
|
||||||
|
'tests/gui/modules/progress-status.spec.ts',
|
||||||
|
],
|
||||||
|
//
|
||||||
|
// ============
|
||||||
|
// Capabilities
|
||||||
|
// ============
|
||||||
|
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
|
||||||
|
// time. Depending on the number of capabilities, WebdriverIO launches several test
|
||||||
|
// sessions. Within your capabilities you can overwrite the spec and exclude options in
|
||||||
|
// order to group specific specs to a specific capability.
|
||||||
|
//
|
||||||
|
// First, you can define how many instances should be started at the same time. Let's
|
||||||
|
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
|
||||||
|
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
|
||||||
|
// files and you set maxInstances to 10, all spec files will get tested at the same time
|
||||||
|
// and 30 processes will get spawned. The property handles how many capabilities
|
||||||
|
// from the same test should run tests.
|
||||||
|
//
|
||||||
|
maxInstances: 10,
|
||||||
|
//
|
||||||
|
// If you have trouble getting all important capabilities together, check out the
|
||||||
|
// Sauce Labs platform configurator - a great tool to configure your capabilities:
|
||||||
|
// https://saucelabs.com/platform/platform-configurator
|
||||||
|
//
|
||||||
|
capabilities: [
|
||||||
|
{
|
||||||
|
browserName: 'electron',
|
||||||
|
// Electron service options
|
||||||
|
// see https://webdriver.io/docs/desktop-testing/electron/configuration/#service-options
|
||||||
|
'wdio:electronServiceOptions': {
|
||||||
|
appArgs: process.platform === 'linux' ? ['headless'] : [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
//
|
||||||
|
// ===================
|
||||||
|
// Test Configurations
|
||||||
|
// ===================
|
||||||
|
// Define all options that are relevant for the WebdriverIO instance here
|
||||||
|
//
|
||||||
|
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||||
|
logLevel: 'info',
|
||||||
|
//
|
||||||
|
// Set specific log levels per logger
|
||||||
|
// loggers:
|
||||||
|
// - webdriver, webdriverio
|
||||||
|
// - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
|
||||||
|
// - @wdio/mocha-framework, @wdio/jasmine-framework
|
||||||
|
// - @wdio/local-runner
|
||||||
|
// - @wdio/sumologic-reporter
|
||||||
|
// - @wdio/cli, @wdio/config, @wdio/utils
|
||||||
|
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||||
|
// logLevels: {
|
||||||
|
// webdriver: 'info',
|
||||||
|
// '@wdio/appium-service': 'info'
|
||||||
|
// },
|
||||||
|
//
|
||||||
|
// If you only want to run your tests until a specific amount of tests have failed use
|
||||||
|
// bail (default is 0 - don't bail, run all tests).
|
||||||
|
bail: 0,
|
||||||
|
//
|
||||||
|
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
|
||||||
|
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
|
||||||
|
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
|
||||||
|
// gets prepended directly.
|
||||||
|
// baseUrl: 'http://localhost:8080',
|
||||||
|
//
|
||||||
|
// Default timeout for all waitFor* commands.
|
||||||
|
waitforTimeout: 10000,
|
||||||
|
//
|
||||||
|
// Default timeout in milliseconds for request
|
||||||
|
// if browser driver or grid doesn't send response
|
||||||
|
connectionRetryTimeout: 120000,
|
||||||
|
//
|
||||||
|
// Default request retries count
|
||||||
|
connectionRetryCount: 3,
|
||||||
|
//
|
||||||
|
// Test runner services
|
||||||
|
// Services take over a specific job you don't want to take care of. They enhance
|
||||||
|
// your test setup with almost no effort. Unlike plugins, they don't add new
|
||||||
|
// commands. Instead, they hook themselves up into the test process.
|
||||||
|
services: ['electron'],
|
||||||
|
|
||||||
|
// Framework you want to run your specs with.
|
||||||
|
// The following are supported: Mocha, Jasmine, and Cucumber
|
||||||
|
// see also: https://webdriver.io/docs/frameworks
|
||||||
|
//
|
||||||
|
// Make sure you have the wdio adapter package for the specific framework installed
|
||||||
|
// before running any tests.
|
||||||
|
framework: 'mocha',
|
||||||
|
|
||||||
|
//
|
||||||
|
// The number of times to retry the entire specfile when it fails as a whole
|
||||||
|
// specFileRetries: 1,
|
||||||
|
//
|
||||||
|
// Delay in seconds between the spec file retry attempts
|
||||||
|
// specFileRetriesDelay: 0,
|
||||||
|
//
|
||||||
|
// Whether or not retried spec files should be retried immediately or deferred to the end of the queue
|
||||||
|
// specFileRetriesDeferred: false,
|
||||||
|
//
|
||||||
|
// Test reporter for stdout.
|
||||||
|
// The only one supported by default is 'dot'
|
||||||
|
// see also: https://webdriver.io/docs/dot-reporter
|
||||||
|
reporters: ['spec'],
|
||||||
|
|
||||||
|
// Options to be passed to Mocha.
|
||||||
|
// See the full list at http://mochajs.org/
|
||||||
|
mochaOpts: {
|
||||||
|
ui: 'bdd',
|
||||||
|
timeout: 60000,
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// =====
|
||||||
|
// Hooks
|
||||||
|
// =====
|
||||||
|
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
|
||||||
|
// it and to build services around it. You can either apply a single function or an array of
|
||||||
|
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
|
||||||
|
// resolved to continue.
|
||||||
|
/**
|
||||||
|
* Gets executed once before all workers get launched.
|
||||||
|
* @param {object} config wdio configuration object
|
||||||
|
* @param {Array.<Object>} capabilities list of capabilities details
|
||||||
|
*/
|
||||||
|
// onPrepare: function (config, capabilities) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Gets executed before a worker process is spawned and can be used to initialize specific service
|
||||||
|
* for that worker as well as modify runtime environments in an async fashion.
|
||||||
|
* @param {string} cid capability id (e.g 0-0)
|
||||||
|
* @param {object} caps object containing capabilities for session that will be spawn in the worker
|
||||||
|
* @param {object} specs specs to be run in the worker process
|
||||||
|
* @param {object} args object that will be merged with the main configuration once worker is initialized
|
||||||
|
* @param {object} execArgv list of string arguments passed to the worker process
|
||||||
|
*/
|
||||||
|
// onWorkerStart: function (cid, caps, specs, args, execArgv) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Gets executed just after a worker process has exited.
|
||||||
|
* @param {string} cid capability id (e.g 0-0)
|
||||||
|
* @param {number} exitCode 0 - success, 1 - fail
|
||||||
|
* @param {object} specs specs to be run in the worker process
|
||||||
|
* @param {number} retries number of retries used
|
||||||
|
*/
|
||||||
|
// onWorkerEnd: function (cid, exitCode, specs, retries) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Gets executed just before initialising the webdriver session and test framework. It allows you
|
||||||
|
* to manipulate configurations depending on the capability or spec.
|
||||||
|
* @param {object} config wdio configuration object
|
||||||
|
* @param {Array.<Object>} capabilities list of capabilities details
|
||||||
|
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||||
|
* @param {string} cid worker id (e.g. 0-0)
|
||||||
|
*/
|
||||||
|
// beforeSession: function (config, capabilities, specs, cid) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Gets executed before test execution begins. At this point you can access to all global
|
||||||
|
* variables like `browser`. It is the perfect place to define custom commands.
|
||||||
|
* @param {Array.<Object>} capabilities list of capabilities details
|
||||||
|
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||||
|
* @param {object} browser instance of created browser/device session
|
||||||
|
*/
|
||||||
|
// before: function (capabilities, specs) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Runs before a WebdriverIO command gets executed.
|
||||||
|
* @param {string} commandName hook command name
|
||||||
|
* @param {Array} args arguments that command would receive
|
||||||
|
*/
|
||||||
|
// beforeCommand: function (commandName, args) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Hook that gets executed before the suite starts
|
||||||
|
* @param {object} suite suite details
|
||||||
|
*/
|
||||||
|
// beforeSuite: function (suite) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Function to be executed before a test (in Mocha/Jasmine) starts.
|
||||||
|
*/
|
||||||
|
// beforeTest: function (test, context) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
|
||||||
|
* beforeEach in Mocha)
|
||||||
|
*/
|
||||||
|
// beforeHook: function (test, context, hookName) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
|
||||||
|
* afterEach in Mocha)
|
||||||
|
*/
|
||||||
|
// afterHook: function (test, context, { error, result, duration, passed, retries }, hookName) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Function to be executed after a test (in Mocha/Jasmine only)
|
||||||
|
* @param {object} test test object
|
||||||
|
* @param {object} context scope object the test was executed with
|
||||||
|
* @param {Error} result.error error object in case the test fails, otherwise `undefined`
|
||||||
|
* @param {*} result.result return object of test function
|
||||||
|
* @param {number} result.duration duration of test
|
||||||
|
* @param {boolean} result.passed true if test has passed, otherwise false
|
||||||
|
* @param {object} result.retries information about spec related retries, e.g. `{ attempts: 0, limit: 0 }`
|
||||||
|
*/
|
||||||
|
// afterTest: function(test, context, { error, result, duration, passed, retries }) {
|
||||||
|
// },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook that gets executed after the suite has ended
|
||||||
|
* @param {object} suite suite details
|
||||||
|
*/
|
||||||
|
// afterSuite: function (suite) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Runs after a WebdriverIO command gets executed
|
||||||
|
* @param {string} commandName hook command name
|
||||||
|
* @param {Array} args arguments that command would receive
|
||||||
|
* @param {number} result 0 - command success, 1 - command error
|
||||||
|
* @param {object} error error object if any
|
||||||
|
*/
|
||||||
|
// afterCommand: function (commandName, args, result, error) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Gets executed after all tests are done. You still have access to all global variables from
|
||||||
|
* the test.
|
||||||
|
* @param {number} result 0 - test pass, 1 - test fail
|
||||||
|
* @param {Array.<Object>} capabilities list of capabilities details
|
||||||
|
* @param {Array.<String>} specs List of spec file paths that ran
|
||||||
|
*/
|
||||||
|
// after: function (result, capabilities, specs) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Gets executed right after terminating the webdriver session.
|
||||||
|
* @param {object} config wdio configuration object
|
||||||
|
* @param {Array.<Object>} capabilities list of capabilities details
|
||||||
|
* @param {Array.<String>} specs List of spec file paths that ran
|
||||||
|
*/
|
||||||
|
// afterSession: function (config, capabilities, specs) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Gets executed after all workers got shut down and the process is about to exit. An error
|
||||||
|
* thrown in the onComplete hook will result in the test run failing.
|
||||||
|
* @param {object} exitCode 0 - success, 1 - fail
|
||||||
|
* @param {object} config wdio configuration object
|
||||||
|
* @param {Array.<Object>} capabilities list of capabilities details
|
||||||
|
* @param {<Object>} results object containing test results
|
||||||
|
*/
|
||||||
|
// onComplete: function(exitCode, config, capabilities, results) {
|
||||||
|
// },
|
||||||
|
/**
|
||||||
|
* Gets executed when a refresh happens.
|
||||||
|
* @param {string} oldSessionId session ID of the old session
|
||||||
|
* @param {string} newSessionId session ID of the new session
|
||||||
|
*/
|
||||||
|
// onReload: function(oldSessionId, newSessionId) {
|
||||||
|
// }
|
||||||
|
/**
|
||||||
|
* Hook that gets executed before a WebdriverIO assertion happens.
|
||||||
|
* @param {object} params information about the assertion to be executed
|
||||||
|
*/
|
||||||
|
// beforeAssertion: function(params) {
|
||||||
|
// }
|
||||||
|
/**
|
||||||
|
* Hook that gets executed after a WebdriverIO assertion happened.
|
||||||
|
* @param {object} params information about the assertion that was executed, including its results
|
||||||
|
*/
|
||||||
|
// afterAssertion: function(params) {
|
||||||
|
// }
|
||||||
|
};
|
@ -15,30 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Configuration, ModuleOptions } from 'webpack';
|
import type { Configuration, ModuleOptions } from 'webpack';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
|
||||||
import {
|
import { BannerPlugin, IgnorePlugin, DefinePlugin } from 'webpack';
|
||||||
BannerPlugin,
|
|
||||||
IgnorePlugin,
|
|
||||||
NormalModuleReplacementPlugin,
|
|
||||||
} from 'webpack';
|
|
||||||
|
|
||||||
interface ReplacementRule {
|
|
||||||
search: string;
|
|
||||||
replace: string | (() => string);
|
|
||||||
}
|
|
||||||
|
|
||||||
function slashOrAntislash(pattern: RegExp): RegExp {
|
|
||||||
return new RegExp(pattern.source.replace(/\\\//g, '(\\/|\\\\)'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function replace(test: RegExp, ...replacements: ReplacementRule[]) {
|
|
||||||
return {
|
|
||||||
loader: 'string-replace-loader',
|
|
||||||
// Handle windows path separators
|
|
||||||
test: slashOrAntislash(test),
|
|
||||||
options: { multiple: replacements.map((r) => ({ ...r, strict: true })) },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules: Required<ModuleOptions>['rules'] = [
|
const rules: Required<ModuleOptions>['rules'] = [
|
||||||
// Add support for native node modules
|
// Add support for native node modules
|
||||||
@ -80,24 +59,17 @@ const rules: Required<ModuleOptions>['rules'] = [
|
|||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
use: '@svgr/webpack',
|
use: '@svgr/webpack',
|
||||||
},
|
},
|
||||||
// force axios to use http backend (not xhr) to support streams
|
|
||||||
replace(/node_modules\/axios\/lib\/defaults\.js$/, {
|
|
||||||
search: './adapters/xhr',
|
|
||||||
replace: './adapters/http',
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const injectAnalyticsToken = new DefinePlugin({
|
||||||
|
'process.env.SENTRY_TOKEN': JSON.stringify(process.env.SENTRY_TOKEN || ''),
|
||||||
|
});
|
||||||
|
|
||||||
export const rendererConfig: Configuration = {
|
export const rendererConfig: Configuration = {
|
||||||
module: {
|
module: {
|
||||||
rules,
|
rules,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// Force axios to use http.js, not xhr.js as we need stream support
|
|
||||||
// (its package.json file replaces http with xhr for browser targets).
|
|
||||||
new NormalModuleReplacementPlugin(
|
|
||||||
slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/),
|
|
||||||
'./http.js',
|
|
||||||
),
|
|
||||||
// Ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used
|
// Ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used
|
||||||
// by etcher-sdk and does a runtime check to its availability. We’re not currently
|
// by etcher-sdk and does a runtime check to its availability. We’re not currently
|
||||||
// using the “assume role” functionality (AFAIU) of aws4-axios and we don’t care that
|
// using the “assume role” functionality (AFAIU) of aws4-axios and we don’t care that
|
||||||
@ -111,9 +83,15 @@ export const rendererConfig: Configuration = {
|
|||||||
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
|
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
|
||||||
raw: true,
|
raw: true,
|
||||||
}),
|
}),
|
||||||
|
injectAnalyticsToken,
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
|
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
|
||||||
|
alias: {
|
||||||
|
// need to alias ws to the wrapper to avoid the browser fake version to be used
|
||||||
|
ws: resolve(__dirname, 'node_modules/ws/wrapper.mjs'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,4 +105,5 @@ export const mainConfig: Configuration = {
|
|||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
|
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
|
||||||
},
|
},
|
||||||
|
plugins: [injectAnalyticsToken],
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user