mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-16 07:46:31 +00:00
Compare commits
No commits in common. "master" and "v1.19.11" have entirely different histories.
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -62,3 +62,7 @@ CODEOWNERS text
|
||||
*.ttf binary diff=hex
|
||||
xz-without-extension binary diff=hex
|
||||
wmic-output.txt binary diff=hex
|
||||
|
||||
# gitsecret
|
||||
*.secret binary
|
||||
.gitsecret/** binary
|
||||
|
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
|
||||
inputs:
|
||||
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
|
||||
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
|
||||
|
||||
# --- custom environment
|
||||
@ -15,14 +15,14 @@ inputs:
|
||||
# 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)
|
||||
# https://github.com/vercel/pkg-fetch/releases
|
||||
default: '20.x'
|
||||
default: "20.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: 'true'
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: 'composite'
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@v4
|
||||
@ -53,13 +53,6 @@ runs:
|
||||
shell: bash
|
||||
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
|
||||
if: runner.os == 'macOS'
|
||||
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
|
||||
@ -138,7 +131,7 @@ runs:
|
||||
PLATFORM=Windows
|
||||
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 \
|
||||
-H "x-api-key:$SM_API_KEY" \
|
||||
-o smtools-windows-x64.msi
|
||||
@ -146,8 +139,8 @@ runs:
|
||||
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
||||
smksp_registrar.exe list
|
||||
smctl.exe keypair ls
|
||||
smctl.exe windows certsync
|
||||
/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
|
||||
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
||||
|
8
.github/actions/test/action.yml
vendored
8
.github/actions/test/action.yml
vendored
@ -12,7 +12,7 @@ inputs:
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.19'
|
||||
default: '20.10'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: 'true'
|
||||
@ -55,12 +55,6 @@ runs:
|
||||
# fi
|
||||
|
||||
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 package
|
||||
npm run wdio # test stage, note that it requires the package to be done first
|
||||
|
19
.github/workflows/flowzone.yml
vendored
19
.github/workflows/flowzone.yml
vendored
@ -18,24 +18,7 @@ jobs:
|
||||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
|
||||
secrets: inherit
|
||||
with:
|
||||
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"]
|
||||
]
|
||||
}
|
||||
custom_runs_on: '[["ubuntu-20.04"],["windows-2019"],["macos-12"],["macos-latest-xlarge"]]'
|
||||
restrict_custom_actions: false
|
||||
github_prerelease: true
|
||||
cloudflare_website: "etcher"
|
||||
|
BIN
.gitsecret/keys/pubring.kbx
Normal file
BIN
.gitsecret/keys/pubring.kbx
Normal file
Binary file not shown.
BIN
.gitsecret/keys/pubring.kbx~
Normal file
BIN
.gitsecret/keys/pubring.kbx~
Normal file
Binary file not shown.
BIN
.gitsecret/keys/trustdb.gpg
Normal file
BIN
.gitsecret/keys/trustdb.gpg
Normal file
Binary file not shown.
5
.gitsecret/paths/mapping.cfg
Normal file
5
.gitsecret/paths/mapping.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1
|
||||
secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714
|
||||
secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a
|
@ -1,268 +1,3 @@
|
||||
- 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
|
||||
@ -1307,10 +1042,13 @@
|
||||
nested: []
|
||||
- subject: "Patch: run linux build on ubuntu-20.04"
|
||||
hash: adcd8e0325bc891460b3e51aa5403f8675189f13
|
||||
body: |-
|
||||
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/)
|
||||
body: >-
|
||||
as [`18.04` has been
|
||||
removed](https://github.blog/changelog/2022-08-09-github-actions-the-ubuntu-18-04-actions-runner-image-is-being-deprecated-and-will-be-removed-by-12-1-22/)
|
||||
|
||||
We cannot use `latest` as the glibc version will cause issue with older ubuntu version.
|
||||
|
||||
We cannot use `latest` as the glibc version will cause issue with older
|
||||
ubuntu version.
|
||||
footer: {}
|
||||
author: Edwin Joassart
|
||||
nested: []
|
||||
@ -12055,19 +11793,40 @@
|
||||
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
|
||||
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
|
||||
|
||||
without any selection:
|
||||
|
||||
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.tryCatcher (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/util.js:16:23)
|
||||
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 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 Number.indexedGetter
|
||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/call_get.js:106:15)
|
||||
|
||||
at Number.tryCatcher
|
||||
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/util.js:16:23)
|
||||
|
||||
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 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)
|
||||
- hash: 6bd086f1c5c6654a47125cf2d46788655cae2553
|
||||
author: Juan Cruz Viotti
|
||||
@ -12664,14 +12423,21 @@
|
||||
changelog-entry: Use info icon instead of "SHOW FULL FILE NAME" in first step.
|
||||
fixes: https://github.com/resin-io/etcher/issues/458
|
||||
subject: Make use of AppImage desktop integration script
|
||||
body: |-
|
||||
body: >-
|
||||
This is useful to prompt the user to install the `.desktop` file.
|
||||
|
||||
The `Description` key in `Etcher.desktop` was changed to `Comment` since
|
||||
|
||||
`desktop-file-validate` complained with:
|
||||
|
||||
Etcher.desktop: error: file contains key "Description" in group "Desktop
|
||||
|
||||
Entry", but keys extending the format should start with "X-"
|
||||
|
||||
After checking the desktop file format specification, the correct key
|
||||
|
||||
should be "Comment"
|
||||
|
||||
(https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html).
|
||||
- hash: c3e360e61933ef0044c005b5e92c879ff9a47c49
|
||||
author: Juan Cruz Viotti
|
||||
@ -12884,12 +12650,17 @@
|
||||
changelog-entry: Fix flashing never starting after elevation in GNU/Linux.
|
||||
fixes: https://github.com/resin-io/etcher/issues/665
|
||||
subject: Make all angular modules export the name of the module
|
||||
body: |-
|
||||
body: >-
|
||||
This makes them very nicely require-able, for example:
|
||||
|
||||
angular.module('MyModule', [
|
||||
|
||||
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
|
||||
author: Juan Cruz Viotti
|
||||
footers:
|
||||
|
104
CHANGELOG.md
104
CHANGELOG.md
@ -3,110 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
# Maintaining Etcher
|
||||
Maintaining Etcher
|
||||
==================
|
||||
|
||||
This document is meant to serve as a guide for maintainers to perform common tasks.
|
||||
|
||||
## Releasing
|
||||
Releasing
|
||||
---------
|
||||
|
||||
### Release Types
|
||||
|
||||
@ -11,15 +13,16 @@ This document is meant to serve as a guide for maintainers to perform common tas
|
||||
- **release**: Full releases
|
||||
|
||||
Draft release is created from each PR, tagged with the branch name.
|
||||
All merged PR will generate a new tag/version as a _pre-release_.
|
||||
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.
|
||||
|
||||
|
||||
#### Preparation
|
||||
|
||||
- [Prepare the new version](#preparing-a-new-version)
|
||||
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
|
||||
- [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
|
||||
|
||||
@ -32,7 +35,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)
|
||||
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
|
||||
- [Update the website](https://github.com/balena-io/etcher-homepage)
|
||||
- Wait 2-3 hours for analytics (Sentry) to trickle in and check for elevated error rates, or regressions
|
||||
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions
|
||||
- If regressions arise; pull the release, and release a patched version, else:
|
||||
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
|
||||
- Post changelog with `#release-notes` tag on internal chat
|
||||
@ -48,6 +51,7 @@ Make sure to set the analytics tokens when generating production release binarie
|
||||
|
||||
```bash
|
||||
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
|
||||
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
|
||||
```
|
||||
|
||||
#### Linux
|
||||
@ -67,6 +71,7 @@ npm run make
|
||||
|
||||
Our CI will appropriately sign artifacts for macOS and some Windows targets.
|
||||
|
||||
|
||||
### Uploading packages to Cloudfront
|
||||
|
||||
Log in to cloudfront and upload the `rpm` and `deb` files.
|
||||
@ -94,6 +99,7 @@ aws s3api delete-object --bucket <bucket name> --key <file name>
|
||||
|
||||
The Bintray dashboard provides an easy way to delete a version's files.
|
||||
|
||||
|
||||
### Submitting binaries to Symantec
|
||||
|
||||
- [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/)
|
||||
|
@ -1,19 +1,22 @@
|
||||
# Manual Testing
|
||||
Manual Testing
|
||||
==============
|
||||
|
||||
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.
|
||||
|
||||
## Image Selection
|
||||
Image Selection
|
||||
---------------
|
||||
|
||||
- [ ] Cancel image selection dialog
|
||||
- [ ] 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 tar archive (with any compression method)
|
||||
- [ ] Change image selection
|
||||
- [ ] Select a Windows image, and expect a sensible warning
|
||||
|
||||
## Drive Selection
|
||||
Drive Selection
|
||||
---------------
|
||||
|
||||
- [ ] Open the drive selection modal
|
||||
- [ ] Switch drive selection
|
||||
@ -22,15 +25,16 @@ should aim to replace items on this list with automated Spectron test cases.
|
||||
- [ ] Insert a locked SD Card 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
|
||||
that contains it
|
||||
that contains it
|
||||
- [ ] 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
|
||||
image, but not big enough to hold the uncompressed version
|
||||
uncompressed size) into a drive that is big enough to hold the compressed
|
||||
image, but not big enough to hold the uncompressed version
|
||||
- [ ] Enable "Unsafe Mode" and attempt to select a system drive
|
||||
- [ ] 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:
|
||||
|
||||
@ -47,17 +51,18 @@ Run the following tests with and without validation enabled:
|
||||
- [ ] Flash an archive image containing a blockmap file
|
||||
- [ ] Flash an archive image containing a manifest metadata file
|
||||
|
||||
## Flashing Process
|
||||
Flashing Process
|
||||
----------------
|
||||
|
||||
- [ ] Unplug the drive during flash or validation
|
||||
- [ ] Click "Flash", cancel elevation dialog, and click "Flash" again
|
||||
- [ ] 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
|
||||
|
||||
- [ ] 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 OS keyboard shortcut
|
||||
- [ ] Close the application from the terminal using Ctrl-C while flashing
|
||||
@ -67,10 +72,11 @@ 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
|
||||
permissions to see the elevated child writer process.
|
||||
|
||||
## GUI
|
||||
GUI
|
||||
----
|
||||
|
||||
- [ ] Close application from the terminal using Ctrl-C while the application is
|
||||
idle
|
||||
idle
|
||||
- [ ] Click footer links that take you to an external website
|
||||
- [ ] Attempt to change image or drive selection while flashing
|
||||
- [ ] Go to the settings page while flashing and come back
|
||||
@ -79,20 +85,31 @@ permissions to see the elevated child writer process.
|
||||
- [ ] Minimize the application
|
||||
- [ ] Start the application given no internet connection
|
||||
|
||||
## Success Banner
|
||||
Success Banner
|
||||
--------------
|
||||
|
||||
- [ ] Click an external link on the success banner (with and without internet
|
||||
connection)
|
||||
connection)
|
||||
|
||||
## Elevation Prompt
|
||||
Elevation Prompt
|
||||
----------------
|
||||
|
||||
- [ ] Flash an image as `root`/administrator
|
||||
- [ ] Reject elevation prompt
|
||||
- [ ] Put incorrect elevation prompt password
|
||||
- [ ] Unplug the drive during elevation
|
||||
|
||||
## Unmounting
|
||||
Unmounting
|
||||
----------
|
||||
|
||||
- [ ] Disable unmounting and flash an image
|
||||
- [ ] 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,6 +122,7 @@ run Etcher on a GNU/Linux system.
|
||||
- xrender
|
||||
- xtst
|
||||
- xscrnsaver
|
||||
- gconf-2.0
|
||||
- gmodule-2.0
|
||||
- nss
|
||||
|
||||
|
@ -60,7 +60,6 @@ const config: ForgeConfig = {
|
||||
new MakerZIP(),
|
||||
new MakerSquirrel({
|
||||
setupIcon: 'assets/icon.ico',
|
||||
loadingGif: 'assets/icon.png',
|
||||
...winSigningConfig,
|
||||
}),
|
||||
new MakerDMG({
|
||||
@ -136,6 +135,25 @@ const config: ForgeConfig = {
|
||||
new sidecar.SidecarPlugin(),
|
||||
],
|
||||
hooks: {
|
||||
readPackageJson: async (_config, packageJson) => {
|
||||
packageJson.analytics = {};
|
||||
|
||||
if (process.env.SENTRY_TOKEN) {
|
||||
packageJson.analytics.sentry = {
|
||||
token: process.env.SENTRY_TOKEN,
|
||||
};
|
||||
}
|
||||
|
||||
if (process.env.AMPLITUDE_TOKEN) {
|
||||
packageJson.analytics.amplitude = {
|
||||
token: 'balena-etcher',
|
||||
};
|
||||
}
|
||||
|
||||
// packageJson.packageType = 'dmg' | 'AppImage' | 'rpm' | 'deb' | 'zip' | 'nsis' | 'portable'
|
||||
|
||||
return packageJson;
|
||||
},
|
||||
postPackage: async (_forgeConfig, options) => {
|
||||
if (options.platform === 'linux') {
|
||||
// symlink the etcher binary from balena-etcher to balenaEtcher to ensure compatibility with the wdio suite and the old name
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PluginBase } from '@electron-forge/plugin-base';
|
||||
import type {
|
||||
import {
|
||||
ForgeHookMap,
|
||||
ResolvedForgeConfig,
|
||||
} from '@electron-forge/shared-types';
|
||||
|
@ -16,15 +16,14 @@
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as remote from '@electron/remote';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { debounce, capitalize, values } from 'lodash';
|
||||
import { debounce, capitalize, Dictionary, values } from 'lodash';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as packageJSON from '../../../package.json';
|
||||
import type { DrivelistDrive } from '../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../shared/drive-constraints';
|
||||
import * as EXIT_CODES from '../../shared/exit-codes';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as availableDrives from './models/available-drives';
|
||||
@ -39,7 +38,7 @@ import * as windowProgress from './os/window-progress';
|
||||
import MainPage from './pages/main/MainPage';
|
||||
import './css/main.css';
|
||||
import * as i18next from 'i18next';
|
||||
import type { SourceMetadata } from '../../shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../../shared/typings/source-selector';
|
||||
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
@ -64,6 +63,9 @@ store.dispatch({
|
||||
data: uuidV4(),
|
||||
});
|
||||
|
||||
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid;
|
||||
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid;
|
||||
|
||||
console.log(outdent`
|
||||
${outdent}
|
||||
_____ _ _
|
||||
@ -79,6 +81,13 @@ console.log(outdent`
|
||||
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 });
|
||||
|
||||
function pluralize(word: string, quantity: number) {
|
||||
@ -132,29 +141,25 @@ export let requestMetadata: any;
|
||||
// start the api and spawn the child process
|
||||
spawnChildAndConnect({
|
||||
withPrivileges: false,
|
||||
})
|
||||
.then(({ emit, registerHandler }) => {
|
||||
// start scanning
|
||||
emit('scan', {});
|
||||
}).then(({ emit, registerHandler }) => {
|
||||
// start scanning
|
||||
emit('scan', {});
|
||||
|
||||
// make the sourceMetada awaitable to be used on source selection
|
||||
requestMetadata = async (params: any): Promise<SourceMetadata> => {
|
||||
emit('sourceMetadata', JSON.stringify(params));
|
||||
// make the sourceMetada awaitable to be used on source selection
|
||||
requestMetadata = async (params: any): Promise<SourceMetadata> => {
|
||||
emit('sourceMetadata', JSON.stringify(params));
|
||||
|
||||
return new Promise((resolve) =>
|
||||
registerHandler('sourceMetadata', (data: any) => {
|
||||
resolve(JSON.parse(data));
|
||||
}),
|
||||
);
|
||||
};
|
||||
return new Promise((resolve) =>
|
||||
registerHandler('sourceMetadata', (data: any) => {
|
||||
resolve(JSON.parse(data));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
registerHandler('drives', (data: any) => {
|
||||
setDrives(JSON.parse(data));
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
throw new Error(`Failed to start the flasher process. error: ${error}`);
|
||||
registerHandler('drives', (data: any) => {
|
||||
setDrives(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
|
||||
let popupExists = false;
|
||||
|
||||
@ -162,6 +167,9 @@ analytics.initAnalytics();
|
||||
|
||||
window.addEventListener('beforeunload', async (event) => {
|
||||
if (!flashState.isFlashing() || popupExists) {
|
||||
analytics.logEvent('Close application', {
|
||||
isFlashing: flashState.isFlashing(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -171,6 +179,8 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
// Don't open any more popups
|
||||
popupExists = true;
|
||||
|
||||
analytics.logEvent('Close attempt while flashing');
|
||||
|
||||
try {
|
||||
const confirmed = await osDialog.showWarning({
|
||||
confirmationLabel: i18next.t('yesExit'),
|
||||
@ -179,11 +189,19 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
description: messages.warning.exitWhileFlashing(),
|
||||
});
|
||||
if (confirmed) {
|
||||
analytics.logEvent('Close confirmed while flashing', {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
});
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
// remote.app.quit() which does not.
|
||||
remote.process.exit(EXIT_CODES.SUCCESS);
|
||||
}
|
||||
|
||||
analytics.logEvent('Close rejected while flashing', {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
popupExists = false;
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
|
@ -16,32 +16,33 @@
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import type * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||
import * as React from 'react';
|
||||
import type { ModalProps, TableColumn } from 'rendition';
|
||||
import { Flex, Txt, Badge, Link } from 'rendition';
|
||||
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {
|
||||
DriveStatus,
|
||||
DrivelistDrive,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
isDriveValid,
|
||||
DriveStatus,
|
||||
DrivelistDrive,
|
||||
isDriveSizeLarge,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||
import { store } from '../../models/store';
|
||||
import { logException } from '../../modules/analytics';
|
||||
import { logEvent, logException } from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import type { GenericTableProps } from '../../styled-components';
|
||||
import { Alert, Modal, Table } from '../../styled-components';
|
||||
import {
|
||||
Alert,
|
||||
GenericTableProps,
|
||||
Modal,
|
||||
Table,
|
||||
} from '../../styled-components';
|
||||
|
||||
import type { SourceMetadata } from '../../../../shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../../../../shared/typings/source-selector';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
@ -355,6 +356,9 @@ export class DriveSelector extends React.Component<
|
||||
|
||||
private installMissingDrivers(drive: DriverlessDrive) {
|
||||
if (drive.link) {
|
||||
logEvent('Open driver link modal', {
|
||||
url: drive.link,
|
||||
});
|
||||
this.setState({ missingDriversModal: { drive } });
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import * as React from 'react';
|
||||
import type { ModalProps } from 'rendition';
|
||||
import { Badge, Flex, Txt } from 'rendition';
|
||||
import { Badge, Flex, Txt, ModalProps } from 'rendition';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import type { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
|
@ -22,13 +22,14 @@ import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import { Actions, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { FlashAnother } from '../flash-another/flash-another';
|
||||
import type { FlashError } from '../flash-results/flash-results';
|
||||
import { FlashResults } from '../flash-results/flash-results';
|
||||
import { FlashResults, FlashError } from '../flash-results/flash-results';
|
||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||
|
||||
function restart(goToMain: () => void) {
|
||||
selectionState.deselectAllDrives();
|
||||
analytics.logEvent('Restart');
|
||||
|
||||
// Reset the flashing workflow uuid
|
||||
store.dispatch({
|
||||
|
@ -18,8 +18,7 @@ import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg';
|
||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg';
|
||||
import * as React from 'react';
|
||||
import type { FlexProps, TableColumn } from 'rendition';
|
||||
import { Flex, Link, Txt } from 'rendition';
|
||||
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { progress } from '../../../../shared/messages';
|
||||
|
@ -21,6 +21,7 @@ import * as React from 'react';
|
||||
|
||||
import * as packageJSON from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
|
||||
/**
|
||||
* @summary Electron session identifier
|
||||
@ -195,6 +196,10 @@ export class SafeWebview extends React.PureComponent<
|
||||
// only care about this event if it's a request for the main frame
|
||||
if (event.resourceType === 'mainFrame') {
|
||||
const HTTP_OK = 200;
|
||||
const { webContents, ...webviewEvent } = event;
|
||||
analytics.logEvent('SafeWebview loaded', {
|
||||
...webviewEvent,
|
||||
});
|
||||
this.setState({
|
||||
shouldShow: event.statusCode === HTTP_OK,
|
||||
});
|
||||
|
@ -21,6 +21,7 @@ import { Box, Checkbox, Flex, Txt } from 'rendition';
|
||||
|
||||
import { version, packageType } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { Modal } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
@ -88,6 +89,7 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
|
||||
const toggleSetting = async (setting: string) => {
|
||||
const value = currentSettings[setting];
|
||||
analytics.logEvent('Toggle setting', { setting, value });
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
|
@ -20,17 +20,16 @@ import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
||||
import type { IpcRendererEvent } from 'electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import { uniqBy, isNil } from 'lodash';
|
||||
import * as path from 'path';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import { requestMetadata } from '../../app';
|
||||
|
||||
import type { ButtonProps } from 'rendition';
|
||||
import {
|
||||
Flex,
|
||||
ButtonProps,
|
||||
Modal as SmallModal,
|
||||
Txt,
|
||||
Card as BaseCard,
|
||||
@ -64,9 +63,9 @@ import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import SrcSvg from '../../../assets/src.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import type { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import { isJson } from '../../../../shared/utils';
|
||||
import type {
|
||||
import {
|
||||
SourceMetadata,
|
||||
Authentication,
|
||||
Source,
|
||||
@ -308,7 +307,6 @@ const FlowSelector = styled(
|
||||
|
||||
interface SourceSelectorProps {
|
||||
flashing: boolean;
|
||||
hideAnalyticsAlert: () => void;
|
||||
}
|
||||
|
||||
interface SourceSelectorState {
|
||||
@ -360,20 +358,6 @@ export class SourceSelector extends React.Component<
|
||||
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) {
|
||||
this.setState({ imageLoading: true });
|
||||
await this.selectSource(
|
||||
@ -392,8 +376,11 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private reselectSource() {
|
||||
analytics.logEvent('Reselect image', {
|
||||
previousImage: selectionState.getImage(),
|
||||
});
|
||||
|
||||
selectionState.deselectImage();
|
||||
this.props.hideAnalyticsAlert();
|
||||
}
|
||||
|
||||
private selectSource(
|
||||
@ -422,6 +409,7 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||
analytics.logEvent('Possibly Windows image', { image: selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
@ -434,17 +422,10 @@ export class SourceSelector extends React.Component<
|
||||
// this will send an event down the ipcMain asking for metadata
|
||||
// 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 });
|
||||
|
||||
if (!metadata?.hasMBR && this.state.warning === null) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
@ -462,6 +443,7 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
} else {
|
||||
if (selected.partitionTableType === null) {
|
||||
analytics.logEvent('Missing partition table', { selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.driveMissingPartitionTable(),
|
||||
@ -483,6 +465,15 @@ export class SourceSelector extends React.Component<
|
||||
metadata.auth = auth;
|
||||
metadata.SourceType = SourceType;
|
||||
selectionState.selectSource(metadata);
|
||||
analytics.logEvent('Select image', {
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image: {
|
||||
...metadata,
|
||||
logo: Boolean(metadata.logo),
|
||||
blockMap: Boolean(metadata.blockMap),
|
||||
},
|
||||
});
|
||||
}
|
||||
})(),
|
||||
};
|
||||
@ -503,9 +494,11 @@ export class SourceSelector extends React.Component<
|
||||
analytics.logException(error);
|
||||
return;
|
||||
}
|
||||
analytics.logEvent(title, { path: sourcePath });
|
||||
}
|
||||
|
||||
private async openImageSelector() {
|
||||
analytics.logEvent('Open image selector');
|
||||
this.setState({ imageSelectorOpen: true });
|
||||
|
||||
try {
|
||||
@ -513,6 +506,7 @@ export class SourceSelector extends React.Component<
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!imagePath) {
|
||||
analytics.logEvent('Image selector closed');
|
||||
return;
|
||||
}
|
||||
await this.selectSource(imagePath, 'File').promise;
|
||||
@ -531,12 +525,16 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private openURLSelector() {
|
||||
analytics.logEvent('Open image URL selector');
|
||||
|
||||
this.setState({
|
||||
showURLSelector: true,
|
||||
});
|
||||
}
|
||||
|
||||
private openDriveSelector() {
|
||||
analytics.logEvent('Open drive selector');
|
||||
|
||||
this.setState({
|
||||
showDriveSelector: true,
|
||||
});
|
||||
@ -553,6 +551,10 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private showSelectedImageDetails() {
|
||||
analytics.logEvent('Show selected image tooltip', {
|
||||
imagePath: selectionState.getImage()?.path,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
showImageDetails: true,
|
||||
});
|
||||
@ -732,7 +734,9 @@ export class SourceSelector extends React.Component<
|
||||
done={async (imageURL: string, auth?: Authentication) => {
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (imageURL) {
|
||||
if (!imageURL) {
|
||||
analytics.logEvent('URL selector closed');
|
||||
} else {
|
||||
let promise;
|
||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||
imageURL,
|
||||
|
@ -16,13 +16,14 @@
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import * as React from 'react';
|
||||
import type { FlexProps } from 'rendition';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
import { Flex, FlexProps, Txt } from 'rendition';
|
||||
|
||||
import type { DriveStatus } from '../../../../shared/drive-constraints';
|
||||
import { getDriveImageCompatibilityStatuses } from '../../../../shared/drive-constraints';
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
DriveStatus,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||
import {
|
||||
ChangeButton,
|
||||
|
@ -17,9 +17,12 @@
|
||||
import * as React from 'react';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import type { DriveSelectorProps } from '../drive-selector/drive-selector';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import {
|
||||
DriveSelector,
|
||||
DriveSelectorProps,
|
||||
} from '../drive-selector/drive-selector';
|
||||
import {
|
||||
isDriveSelected,
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
deselectDrive,
|
||||
@ -27,12 +30,13 @@ import {
|
||||
deselectAllDrives,
|
||||
} from '../../models/selection-state';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { TargetSelectorButton } from './target-selector-button';
|
||||
|
||||
import TgtSvg from '../../../assets/tgt.svg';
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
import type { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export const getDriveListLabel = () => {
|
||||
@ -75,10 +79,21 @@ export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
|
||||
);
|
||||
// deselect drives
|
||||
deselected.forEach((drive) => {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: true,
|
||||
});
|
||||
deselectDrive(drive.device);
|
||||
});
|
||||
// select drives
|
||||
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);
|
||||
});
|
||||
};
|
||||
@ -87,14 +102,12 @@ interface TargetSelectorProps {
|
||||
disabled: boolean;
|
||||
hasDrive: boolean;
|
||||
flashing: boolean;
|
||||
hideAnalyticsAlert: () => void;
|
||||
}
|
||||
|
||||
export const TargetSelector = ({
|
||||
disabled,
|
||||
hasDrive,
|
||||
flashing,
|
||||
hideAnalyticsAlert,
|
||||
}: TargetSelectorProps) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
|
||||
@ -126,9 +139,9 @@ export const TargetSelector = ({
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowTargetSelectorModal(true);
|
||||
hideAnalyticsAlert();
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive');
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
flashing={flashing}
|
||||
|
@ -133,7 +133,8 @@ const translation = {
|
||||
flashCompleted: 'Flash Completed!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: 'Anonymously report errors to balena.io',
|
||||
errorReporting:
|
||||
'Anonymously report errors and usage statistics to balena.io',
|
||||
autoUpdate: 'Auto-updates enabled',
|
||||
settings: 'Settings',
|
||||
systemInformation: 'System Information',
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
export function hasAvailableDrives() {
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import type * as sdk from 'etcher-sdk';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { bytesToMegabytes } from '../../../shared/units';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
|
@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import type { AnimationFunction, Color } from 'sys-class-rgb-led';
|
||||
import { Animator, RGBLed } from 'sys-class-rgb-led';
|
||||
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { isSourceDrive } from '../../../shared/drive-constraints';
|
||||
import {
|
||||
DrivelistDrive,
|
||||
isSourceDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import { getDrives } from './available-drives';
|
||||
import { getSelectedDrives } from './selection-state';
|
||||
import * as settings from './settings';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
@ -15,7 +15,7 @@ import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { SourceMetadata } from '../../../shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../../../shared/typings/source-selector';
|
||||
|
||||
import * as availableDrives from './available-drives';
|
||||
import { Actions, store } from './store';
|
||||
|
@ -14,9 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { findLastIndex, once } from 'lodash';
|
||||
import * as _ from 'lodash';
|
||||
import { Client, createClient, createNoopClient } from 'analytics-client';
|
||||
import * as SentryRenderer from '@sentry/electron/renderer';
|
||||
import * as settings from '../models/settings';
|
||||
import { store } from '../models/store';
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
|
||||
type AnalyticsPayload = _.Dictionary<any>;
|
||||
|
||||
@ -69,7 +72,7 @@ export const anonymizePath = (input: string) => {
|
||||
const segments = mainPart.split(sep);
|
||||
|
||||
// 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),
|
||||
);
|
||||
return (
|
||||
@ -111,19 +114,105 @@ export const anonymizeAnalyticsPayload = (
|
||||
return data;
|
||||
};
|
||||
|
||||
let analyticsClient: Client;
|
||||
/**
|
||||
* @summary Init analytics configurations
|
||||
*/
|
||||
export const initAnalytics = once(() => {
|
||||
export const initAnalytics = _.once(() => {
|
||||
const dsn =
|
||||
settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
|
||||
SentryRenderer.init({
|
||||
dsn,
|
||||
beforeSend: anonymizeSentryData,
|
||||
debug: process.env.ETCHER_SENTRY_DEBUG === 'true',
|
||||
});
|
||||
settings.getSync('analyticsSentryToken') ||
|
||||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
|
||||
SentryRenderer.init({ dsn, beforeSend: anonymizeSentryData });
|
||||
|
||||
const projectName =
|
||||
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
|
||||
*
|
||||
|
@ -14,17 +14,50 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Drive as DrivelistDrive } from 'drivelist';
|
||||
import type * as sdk from 'etcher-sdk';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import { Dictionary } from 'lodash';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import type { SourceMetadata } from '../../../shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../../../shared/typings/source-selector';
|
||||
import * as flashState from '../models/flash-state';
|
||||
import * as selectionState from '../models/selection-state';
|
||||
import * as settings from '../models/settings';
|
||||
import * as analytics from '../modules/analytics';
|
||||
import * as windowProgress from '../os/window-progress';
|
||||
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;
|
||||
|
||||
interface FlashResults {
|
||||
skip?: boolean;
|
||||
cancelled?: boolean;
|
||||
@ -55,6 +88,14 @@ async function performWrite(
|
||||
|
||||
const flashResults: FlashResults = {};
|
||||
|
||||
const analyticsData = {
|
||||
image,
|
||||
drives,
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
};
|
||||
|
||||
const onFail = ({ device, error }: { device: any; error: any }) => {
|
||||
console.log('fail event');
|
||||
console.log(device);
|
||||
@ -62,6 +103,7 @@ async function performWrite(
|
||||
if (device.devicePath) {
|
||||
flashState.addFailedDeviceError({ device, error });
|
||||
}
|
||||
handleErrorLogging(error, analyticsData);
|
||||
finish();
|
||||
};
|
||||
|
||||
@ -153,6 +195,17 @@ export async function flash(
|
||||
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
|
||||
try {
|
||||
const result = await write(image, drives, flashState.setProgressState);
|
||||
@ -167,10 +220,39 @@ export async function flash(
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,6 +261,16 @@ export async function flash(
|
||||
*/
|
||||
export async function cancel(type: string) {
|
||||
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) {
|
||||
cancelEmitter(status);
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export interface FlashState {
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as settings from '../../../models/settings';
|
||||
import { logEvent } from '../../../modules/analytics';
|
||||
|
||||
/**
|
||||
* @summary Open an external resource
|
||||
@ -26,6 +27,8 @@ export async function open(url: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
logEvent('Open external link', { url });
|
||||
|
||||
if (url) {
|
||||
electron.shell.openExternal(url);
|
||||
}
|
||||
|
@ -17,8 +17,7 @@
|
||||
import * as remote from '@electron/remote';
|
||||
|
||||
import { percentageToFloat } from '../../../shared/utils';
|
||||
import type { FlashState } from '../modules/progress-status';
|
||||
import { titleFromFlashState } from '../modules/progress-status';
|
||||
import { FlashState, titleFromFlashState } from '../modules/progress-status';
|
||||
|
||||
/**
|
||||
* @summary The title of the main window upon program launch
|
||||
|
@ -198,7 +198,9 @@ export class FlashStep extends React.PureComponent<
|
||||
private handleFlashErrorResponse(shouldRetry: boolean) {
|
||||
this.setState({ errorMessage: '' });
|
||||
flashState.resetState();
|
||||
if (!shouldRetry) {
|
||||
if (shouldRetry) {
|
||||
analytics.logEvent('Restart after failure');
|
||||
} else {
|
||||
selection.clear();
|
||||
}
|
||||
}
|
||||
|
@ -15,20 +15,19 @@
|
||||
*/
|
||||
|
||||
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 * as path from 'path';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import { Alert, Flex, Link } from 'rendition';
|
||||
import { Flex } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import FinishPage from '../../components/finish/finish';
|
||||
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
||||
import { SettingsModal } from '../../components/settings/settings';
|
||||
import { SourceSelector } from '../../components/source-selector/source-selector';
|
||||
import type { SourceMetadata } from '../../../../shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../../../../shared/typings/source-selector';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
@ -36,7 +35,6 @@ import { observe } from '../../models/store';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import {
|
||||
IconButton as BaseIcon,
|
||||
IconButton,
|
||||
ThemedProvider,
|
||||
} from '../../styled-components';
|
||||
|
||||
@ -48,7 +46,6 @@ import { FlashStep } from './Flash';
|
||||
|
||||
import EtcherSvg from '../../../assets/etcher.svg';
|
||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
import { theme } from '../../theme';
|
||||
|
||||
const Icon = styled(BaseIcon)`
|
||||
margin-right: 20px;
|
||||
@ -100,8 +97,6 @@ const StepBorder = styled.div<{
|
||||
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
|
||||
`;
|
||||
|
||||
const ANALYTICS_ALERT_VISIBILITY_KEY = 'analytics_alert_visible';
|
||||
|
||||
interface MainPageStateFromStore {
|
||||
isFlashing: boolean;
|
||||
hasImage: boolean;
|
||||
@ -118,7 +113,6 @@ interface MainPageState {
|
||||
isWebviewShowing: boolean;
|
||||
hideSettings: boolean;
|
||||
featuredProjectURL?: string;
|
||||
analyticsAlertIsVisible: boolean;
|
||||
}
|
||||
|
||||
export class MainPage extends React.Component<
|
||||
@ -131,8 +125,6 @@ export class MainPage extends React.Component<
|
||||
current: 'main',
|
||||
isWebviewShowing: false,
|
||||
hideSettings: true,
|
||||
analyticsAlertIsVisible:
|
||||
localStorage.getItem(ANALYTICS_ALERT_VISIBILITY_KEY) !== 'false',
|
||||
...this.stateHelper(),
|
||||
};
|
||||
}
|
||||
@ -161,13 +153,6 @@ export class MainPage extends React.Component<
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private hideAnalyticsAlert = () => {
|
||||
if (this.state.analyticsAlertIsVisible) {
|
||||
localStorage.setItem(ANALYTICS_ALERT_VISIBILITY_KEY, 'false');
|
||||
this.setState({ analyticsAlertIsVisible: false });
|
||||
}
|
||||
};
|
||||
|
||||
public async componentDidMount() {
|
||||
observe(() => {
|
||||
this.setState(this.stateHelper());
|
||||
@ -175,17 +160,6 @@ export class MainPage extends React.Component<
|
||||
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() {
|
||||
const state = flashState.getFlashState();
|
||||
const shouldDriveStepBeDisabled = !this.state.hasImage;
|
||||
@ -195,127 +169,86 @@ export class MainPage extends React.Component<
|
||||
!this.state.isFlashing || !this.state.isWebviewShowing;
|
||||
return (
|
||||
<Flex
|
||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px 18px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||
flexDirection="column"
|
||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
mb={this.state.analyticsAlertIsVisible ? '0px' : '92px'}
|
||||
>
|
||||
{notFlashingOrSplitView && (
|
||||
<>
|
||||
<SourceSelector
|
||||
flashing={this.state.isFlashing}
|
||||
hideAnalyticsAlert={this.hideAnalyticsAlert}
|
||||
/>
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</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 && (
|
||||
<Flex
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '36.2vw',
|
||||
height: '100vh',
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<ReducedFlashingInfos
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={this.state.imageName}
|
||||
imageSize={
|
||||
typeof this.state.imageSize === 'number'
|
||||
? prettyBytes(this.state.imageSize)
|
||||
: ''
|
||||
}
|
||||
driveTitle={this.state.driveTitle}
|
||||
driveLabel={this.state.driveLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
color: '#fff',
|
||||
left: 35,
|
||||
top: 72,
|
||||
}}
|
||||
/>
|
||||
{notFlashingOrSplitView && (
|
||||
<>
|
||||
<SourceSelector flashing={this.state.isFlashing} />
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</Flex>
|
||||
)}
|
||||
{this.state.isFlashing && this.state.featuredProjectURL && (
|
||||
<SafeWebview
|
||||
src={this.state.featuredProjectURL}
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
<TargetSelector
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
/>
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||
<Flex
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '36.2vw',
|
||||
height: '100vh',
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<ReducedFlashingInfos
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={this.state.imageName}
|
||||
imageSize={
|
||||
typeof this.state.imageSize === 'number'
|
||||
? prettyBytes(this.state.imageSize)
|
||||
: ''
|
||||
}
|
||||
driveTitle={this.state.driveTitle}
|
||||
driveLabel={this.state.driveLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
color: '#fff',
|
||||
left: 35,
|
||||
top: 72,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlashStep
|
||||
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
isFlashing={this.state.isFlashing}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
position={state.position}
|
||||
failed={state.failed}
|
||||
speed={state.speed}
|
||||
eta={state.eta}
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
</Flex>
|
||||
{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>
|
||||
)}
|
||||
{this.state.isFlashing && this.state.featuredProjectURL && (
|
||||
<SafeWebview
|
||||
src={this.state.featuredProjectURL}
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlashStep
|
||||
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
isFlashing={this.state.isFlashing}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
position={state.position}
|
||||
failed={state.failed}
|
||||
speed={state.speed}
|
||||
eta={state.eta}
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -15,18 +15,16 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import type {
|
||||
FlexProps,
|
||||
ButtonProps,
|
||||
TableProps as BaseTableProps,
|
||||
} from 'rendition';
|
||||
import {
|
||||
Alert as AlertBase,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Button,
|
||||
ButtonProps,
|
||||
Modal as ModalBase,
|
||||
Provider,
|
||||
Table as BaseTable,
|
||||
TableProps as BaseTableProps,
|
||||
Txt,
|
||||
} from 'rendition';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { Dictionary } from 'lodash';
|
||||
|
||||
type BalenaTag = {
|
||||
id: number;
|
||||
|
@ -27,7 +27,7 @@ import { promises as fs } from 'fs';
|
||||
import { platform } from 'os';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
import { once } from 'lodash';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
import './app/i18n';
|
||||
|
||||
@ -37,6 +37,7 @@ import * as settings from './app/models/settings';
|
||||
import { buildWindowMenu } from './menu';
|
||||
import * as i18n from 'i18next';
|
||||
import * as SentryMain from '@sentry/electron/main';
|
||||
import * as packageJSON from '../../package.json';
|
||||
import { anonymizeSentryData } from './app/modules/analytics';
|
||||
|
||||
import { delay } from '../shared/utils';
|
||||
@ -114,16 +115,12 @@ async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
|
||||
}
|
||||
}
|
||||
|
||||
const initSentryMain = once(() => {
|
||||
const initSentryMain = lodash.once(() => {
|
||||
const dsn =
|
||||
settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
|
||||
settings.getSync('analyticsSentryToken') ||
|
||||
lodash.get(packageJSON, ['analytics', 'sentry', 'token']);
|
||||
|
||||
SentryMain.init({
|
||||
dsn,
|
||||
beforeSend: anonymizeSentryData,
|
||||
debug: process.env.ETCHER_SENTRY_DEBUG === 'true',
|
||||
});
|
||||
console.log(SentryMain.getCurrentScope());
|
||||
SentryMain.init({ dsn, beforeSend: anonymizeSentryData });
|
||||
});
|
||||
|
||||
const sourceSelectorReady = new Promise((resolve) => {
|
||||
@ -303,7 +300,7 @@ async function main(): Promise<void> {
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
if (require('electron-squirrel-startup')) {
|
||||
electron.app.quit();
|
||||
app.quit();
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Drive } from 'drivelist';
|
||||
import { Drive } from 'drivelist';
|
||||
import { isNil } from 'lodash';
|
||||
import * as pathIsInside from 'path-is-inside';
|
||||
|
||||
import * as messages from './messages';
|
||||
import type { SourceMetadata } from './typings/source-selector';
|
||||
import { SourceMetadata } from './typings/source-selector';
|
||||
|
||||
/**
|
||||
* @summary The default unknown size for things such as images and drives
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { Dictionary } from 'lodash';
|
||||
import { outdent } from 'outdent';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import '../gui/app/i18n';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
import type { sourceDestination } from 'etcher-sdk';
|
||||
import type { DrivelistDrive } from '../drive-constraints';
|
||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { DrivelistDrive } from '../drive-constraints';
|
||||
|
||||
export type Source = 'File' | 'BlockDevice' | 'Http';
|
||||
|
||||
|
@ -15,19 +15,18 @@
|
||||
*/
|
||||
|
||||
import { WebSocketServer } from 'ws';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { values } from 'lodash';
|
||||
import { Dictionary, values } from 'lodash';
|
||||
|
||||
import type { MultiDestinationProgress } from 'etcher-sdk/build/multi-write';
|
||||
|
||||
import { toJSON } from '../shared/errors';
|
||||
import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes';
|
||||
import type { WriteOptions } from './types/types';
|
||||
import { WriteOptions } from './types/types';
|
||||
import { write, cleanup } from './child-writer';
|
||||
import { startScanning } from './scanner';
|
||||
import { getSourceMetadata } from './source-metadata';
|
||||
import type { DrivelistDrive } from '../shared/drive-constraints';
|
||||
import type { SourceMetadata } from '../shared/typings/source-selector';
|
||||
import { DrivelistDrive } from '../shared/drive-constraints';
|
||||
import { SourceMetadata } from '../shared/typings/source-selector';
|
||||
|
||||
const ETCHER_SERVER_ADDRESS = process.env.ETCHER_SERVER_ADDRESS as string;
|
||||
const ETCHER_SERVER_PORT = process.env.ETCHER_SERVER_PORT as string;
|
||||
|
@ -16,24 +16,26 @@
|
||||
* This file handles the writer process.
|
||||
*/
|
||||
|
||||
import type {
|
||||
import {
|
||||
OnProgressFunction,
|
||||
OnFailFunction,
|
||||
MultiDestinationProgress,
|
||||
} from 'etcher-sdk/build/multi-write';
|
||||
import {
|
||||
decompressThenFlash,
|
||||
DECOMPRESSED_IMAGE_PREFIX,
|
||||
MultiDestinationProgress,
|
||||
} from 'etcher-sdk/build/multi-write';
|
||||
|
||||
import { totalmem } from 'os';
|
||||
|
||||
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||
|
||||
import type { SourceDestination } from 'etcher-sdk/build/source-destination';
|
||||
import { File, Http, BlockDevice } from 'etcher-sdk/build/source-destination';
|
||||
import {
|
||||
File,
|
||||
Http,
|
||||
BlockDevice,
|
||||
SourceDestination,
|
||||
} from 'etcher-sdk/build/source-destination';
|
||||
|
||||
import type { WriteResult, FlashError, WriteOptions } from './types/types';
|
||||
import { WriteResult, FlashError, WriteOptions } from './types/types';
|
||||
|
||||
import { isJson } from '../shared/utils';
|
||||
import { toJSON } from '../shared/errors';
|
||||
|
@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import type { Adapter } from 'etcher-sdk/build/scanner/adapters';
|
||||
import {
|
||||
Adapter,
|
||||
BlockDeviceAdapter,
|
||||
UsbbootDeviceAdapter,
|
||||
} from 'etcher-sdk/build/scanner/adapters';
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { scanner as driveScanner } from './drive-scanner';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import type { DrivelistDrive } from '../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../shared/drive-constraints';
|
||||
import outdent from 'outdent';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { values, keyBy, padStart } from 'lodash';
|
||||
import { Dictionary, values, keyBy, padStart } from 'lodash';
|
||||
import { emitDrives } from './api';
|
||||
|
||||
let availableDrives: DrivelistDrive[] = [];
|
||||
|
@ -2,16 +2,15 @@
|
||||
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { replaceWindowsNetworkDriveLetter } from '../gui/app/os/windows-network-drives';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { isJson } from '../shared/utils';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
import {
|
||||
SourceMetadata,
|
||||
Authentication,
|
||||
Source,
|
||||
} from '../shared/typings/source-selector';
|
||||
import type { DrivelistDrive } from '../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../shared/drive-constraints';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
function isString(value: any): value is string {
|
||||
|
6
lib/util/types/types.d.ts
vendored
6
lib/util/types/types.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
import type { Metadata } from 'etcher-sdk/build/source-destination';
|
||||
import type { SourceMetadata } from '../../shared/typings/source-selector';
|
||||
import type { Drive as DrivelistDrive } from 'drivelist';
|
||||
import { Metadata } from 'etcher-sdk/build/source-destination';
|
||||
import { SourceMetadata } from '../../shared/typings/source-selector';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
|
||||
export interface WriteResult {
|
||||
bytesWritten?: number;
|
||||
|
37483
npm-shrinkwrap.json
generated
37483
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"displayName": "balenaEtcher",
|
||||
"productName": "balenaEtcher",
|
||||
"version": "2.1.3",
|
||||
"version": "1.19.11",
|
||||
"packageType": "local",
|
||||
"main": ".webpack/main",
|
||||
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
|
||||
@ -30,64 +30,65 @@
|
||||
"author": "Balena Ltd. <hello@balena.io>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.1.2",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@electron/remote": "^2.1.0",
|
||||
"@fortawesome/fontawesome-free": "6.5.1",
|
||||
"@ronomon/direct-io": "^3.0.1",
|
||||
"@sentry/electron": "^4.24.0",
|
||||
"axios": "^1.6.8",
|
||||
"@sentry/electron": "^4.15.1",
|
||||
"analytics-client": "^2.0.1",
|
||||
"axios": "^1.6.0",
|
||||
"debug": "4.3.4",
|
||||
"drivelist": "^12.0.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-updater": "6.1.8",
|
||||
"etcher-sdk": "9.1.2",
|
||||
"i18next": "23.11.2",
|
||||
"electron-updater": "6.1.7",
|
||||
"etcher-sdk": "9.0.7",
|
||||
"i18next": "23.7.8",
|
||||
"immutable": "3.8.2",
|
||||
"lodash": "4.17.21",
|
||||
"outdent": "0.8.0",
|
||||
"path-is-inside": "1.0.2",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-i18next": "13.5.0",
|
||||
"redux": "4.2.1",
|
||||
"rendition": "35.2.0",
|
||||
"semver": "7.6.0",
|
||||
"rendition": "35.1.2",
|
||||
"semver": "7.5.4",
|
||||
"styled-components": "5.3.6",
|
||||
"sys-class-rgb-led": "3.0.1",
|
||||
"uuid": "9.0.1",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@balena/lint": "8.0.2",
|
||||
"@electron-forge/cli": "7.4.0",
|
||||
"@electron-forge/maker-deb": "7.4.0",
|
||||
"@electron-forge/maker-dmg": "7.4.0",
|
||||
"@electron-forge/maker-rpm": "7.4.0",
|
||||
"@electron-forge/maker-squirrel": "7.4.0",
|
||||
"@electron-forge/maker-zip": "7.4.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.4.0",
|
||||
"@electron-forge/plugin-webpack": "7.4.0",
|
||||
"@balena/lint": "7.2.4",
|
||||
"@electron-forge/cli": "7.2.0",
|
||||
"@electron-forge/maker-deb": "7.2.0",
|
||||
"@electron-forge/maker-dmg": "7.2.0",
|
||||
"@electron-forge/maker-rpm": "7.2.0",
|
||||
"@electron-forge/maker-squirrel": "7.2.0",
|
||||
"@electron-forge/maker-zip": "7.2.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.2.0",
|
||||
"@electron-forge/plugin-webpack": "7.2.0",
|
||||
"@reforged/maker-appimage": "3.3.2",
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@types/chai": "4.3.14",
|
||||
"@types/chai": "4.3.11",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/node": "^20.11.6",
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.2",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/sinon": "17.0.3",
|
||||
"@types/semver": "7.5.6",
|
||||
"@types/sinon": "17.0.2",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@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",
|
||||
"@yao-pkg/pkg": "^5.11.1",
|
||||
"catch-uncommitted": "^2.0.0",
|
||||
"chai": "4.3.10",
|
||||
"css-loader": "5.2.7",
|
||||
"electron": "30.0.1",
|
||||
"electron": "27.1.3",
|
||||
"file-loader": "6.2.0",
|
||||
"husky": "8.0.3",
|
||||
"native-addon-loader": "2.0.1",
|
||||
@ -105,6 +106,8 @@
|
||||
},
|
||||
"hostDependencies": {
|
||||
"debian": [
|
||||
"gconf-service",
|
||||
"gconf2",
|
||||
"libasound2",
|
||||
"libatk1.0-0",
|
||||
"libc6",
|
||||
@ -116,6 +119,7 @@
|
||||
"libfreetype6",
|
||||
"libgbm1",
|
||||
"libgcc1",
|
||||
"libgconf-2-4",
|
||||
"libgdk-pixbuf2.0-0",
|
||||
"libglib2.0-0",
|
||||
"libgtk-3-0",
|
||||
@ -143,11 +147,10 @@
|
||||
"node": ">=20 <21"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2025-05-15T18:09:56.320Z"
|
||||
"publishedAt": "2024-04-25T13:00:14.270Z"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"utf-8-validate": "^5.0.10",
|
||||
"winusb-driver-generator": "2.1.2"
|
||||
"utf-8-validate": "^5.0.10"
|
||||
}
|
||||
}
|
||||
|
BIN
secrets/APPLE_SIGNING.p12.secret
Normal file
BIN
secrets/APPLE_SIGNING.p12.secret
Normal file
Binary file not shown.
BIN
secrets/APPLE_SIGNING_PASSWORD.txt.secret
Normal file
BIN
secrets/APPLE_SIGNING_PASSWORD.txt.secret
Normal file
Binary file not shown.
BIN
secrets/WINDOWS_SIGNING.pfx.secret
Normal file
BIN
secrets/WINDOWS_SIGNING.pfx.secret
Normal file
Binary file not shown.
BIN
secrets/WINDOWS_SIGNING_PASSWORD.txt.secret
Normal file
BIN
secrets/WINDOWS_SIGNING_PASSWORD.txt.secret
Normal file
Binary file not shown.
BIN
secrets/XCODE_APP_LOADER_PASSWORD.txt.secret
Normal file
BIN
secrets/XCODE_APP_LOADER_PASSWORD.txt.secret
Normal file
Binary file not shown.
@ -22,11 +22,10 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import type { Drive as DrivelistDrive } from 'drivelist';
|
||||
import type { SinonStub } from 'sinon';
|
||||
import { assert, stub } from 'sinon';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import { assert, SinonStub, stub } from 'sinon';
|
||||
|
||||
import type { SourceMetadata } from '../../../lib/shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../../../lib/shared/typings/source-selector';
|
||||
import * as flashState from '../../../lib/gui/app/models/flash-state';
|
||||
import * as imageWriter from '../../../lib/gui/app/modules/image-writer';
|
||||
|
||||
|
@ -17,8 +17,7 @@
|
||||
import { expect } from 'chai';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as os from 'os';
|
||||
import type { SinonStub } from 'sinon';
|
||||
import { stub } from 'sinon';
|
||||
import { SinonStub, stub } from 'sinon';
|
||||
|
||||
import * as wnd from '../../../lib/gui/app/os/windows-network-drives';
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as path from 'path';
|
||||
import type { SourceMetadata } from '../../lib/shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../../lib/shared/typings/source-selector';
|
||||
|
||||
import * as constraints from '../../lib/shared/drive-constraints';
|
||||
import * as messages from '../../lib/shared/messages';
|
||||
|
@ -1,21 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "es2019",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "esnext"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react",
|
||||
"pretty": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "es2019",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "esnext"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react",
|
||||
"pretty": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ export const config: Options.Testrunner = {
|
||||
//
|
||||
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',
|
||||
@ -41,9 +40,6 @@ export const config: Options.Testrunner = {
|
||||
'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',
|
||||
],
|
||||
//
|
||||
// ============
|
||||
|
@ -17,7 +17,29 @@
|
||||
import type { Configuration, ModuleOptions } from 'webpack';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { BannerPlugin, IgnorePlugin, DefinePlugin } from 'webpack';
|
||||
import {
|
||||
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'] = [
|
||||
// Add support for native node modules
|
||||
@ -59,17 +81,24 @@ const rules: Required<ModuleOptions>['rules'] = [
|
||||
test: /\.svg$/,
|
||||
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 = {
|
||||
module: {
|
||||
rules,
|
||||
},
|
||||
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
|
||||
// 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
|
||||
@ -83,7 +112,6 @@ export const rendererConfig: Configuration = {
|
||||
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
|
||||
raw: true,
|
||||
}),
|
||||
injectAnalyticsToken,
|
||||
],
|
||||
|
||||
resolve: {
|
||||
@ -105,5 +133,4 @@ export const mainConfig: Configuration = {
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
|
||||
},
|
||||
plugins: [injectAnalyticsToken],
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user