Compare commits

...

66 Commits

Author SHA1 Message Date
flowzone-app[bot]
dc5c68a6a1 v1.19.8 2024-04-22 09:37:44 +00:00
flowzone-app[bot]
d76adfb081 Merge pull request #4166 from balena-io/aethernet/switch-yao-pkg
patch: replace deprecated pkg with yao-pkg and bump etcher-util to node20
2024-04-22 09:36:43 +00:00
Edwin Joassart
c696c389c9 patch: replace deprecated pkg with yao-pkg and bump etcher-util node v to 20.10 2024-04-22 09:54:56 +03:00
flowzone-app[bot]
96f00aa024 v1.19.7 2024-04-22 06:52:22 +00:00
flowzone-app[bot]
59356c5bd1 Merge pull request #4194 from balena-io/balena-lint/prettier
patch: configure prettier in the project to use balena-lint config
2024-04-22 06:51:31 +00:00
Edwin Joassart
1a9a3d2cdc patch: fix formating 2024-04-19 18:10:27 +02:00
Edwin Joassart
faeaa58ec5 patch: configure prettier in the project to use balena-lint configuration 2024-04-19 18:10:27 +02:00
flowzone-app[bot]
3957273f40 v1.19.6 2024-04-19 15:59:31 +00:00
flowzone-app[bot]
a02a233177 Merge pull request #4193 from balena-io/fix-windows-signature
patch: fix windows signature
2024-04-19 15:58:46 +00:00
Edwin Joassart
f629e6d53b patch: fix win signature process 2024-04-19 17:24:34 +02:00
flowzone-app[bot]
37618ce2fd v1.19.5 2024-02-14 19:51:20 +00:00
flowzone-app[bot]
14c3e28642 Merge pull request #4176 from balena-io/kyle/custom-runs-on
Replace deprecated flowzone input tests_run_on
2024-02-14 19:50:18 +00:00
Kyle Harding
bec0e50741 Replace deprecated flowzone input tests_run_on
The `custom_runs_on` array supports multiple runner labels
in nested arrays.

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2024-02-14 14:23:00 -05:00
flowzone-app[bot]
9ea7a25323 v1.19.4 2024-01-26 17:29:30 +00:00
flowzone-app[bot]
e71d432675 Merge pull request #4167 from balena-io/aethernet/fix-screensaver-error
Aethernet/fix screensaver error
2024-01-26 17:28:37 +00:00
Edwin Joassart
196fd8ae24 patch: remove screensaver error when not on etcher-pro 2024-01-26 18:06:22 +01:00
Edwin Joassart
5d43699242 patch: fix typo in IPC server id 2024-01-26 17:35:35 +01:00
flowzone-app[bot]
3626ffc7ef v1.19.3 2023-12-22 16:13:06 +00:00
dfunckt
cb8e57bfbe Merge pull request #4145 from balena-io/aethernet/upgrade-deps
Update dependencies
2023-12-22 18:11:50 +02:00
Akis Kesoglou
4a7fb996e4 Simplify test script
Interestingly, even before this commit and despite using `xvfb` a bunch of errors are printed during Linux tests, but they seem to run successfully to completion:

[5300:1222/133804.075080:ERROR:bus.cc(407)] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix")
[5323:1222/133804.332045:ERROR:viz_main_impl.cc(196)] Exiting GPU process due to errors during initialization
[5333:1222/133804.352286:ERROR:command_buffer_proxy_impl.cc(127)] ContextResult::kTransientFailure: Failed to send GpuControl.CreateCommandBuffer.
2023-12-22 15:42:01 +02:00
Edwin Joassart
0f2b4dbc10 Update dependencies
- upgrade pretty_bytes to 6.1.1
- upgrade electron-remote to 2.1.0
- upgrade semver to 7.5.4 + @types/semver to 7.5.6
- upgrade chai to 4.3.11 + @types/chai to 4.3.10
- upgrade mocha to 10.2.0 + @types/mocha to 10.0.6
- upgrade sinon to 17.0.1 + @types/sinon to 17.0.2
- remove useless @types
- upgrade @svgr/webpack to 8.1.0
- upgrade @sentry/electron to 4.15.1
- upgrade tslib to 2.6.2
- upgrade immutable to 4.3.4
- upgrade redux to 4.2.1
- upgrade ts-node to 10.9.2 & ts-loader to 9.5.1
- remove mini-css-extract-plugin
- upgrade husky to 8.0.3
- upgrade uuid to 9.0.1
- upgrade lint-staged to 15.2.1
- upgrade @types/node to 18.11.9
- upgrade @fortawesome/fontawesome-free to 6.5.1
- upgrade i18next to 23.7.8 & react-i18next to 11.18.6
- bump react, react-dom + related @types to 17.0.2 and rendition to 35.1.0
- fix getuid for ts
- fix @types/react being in wrong deps
- upgrade @types/tmp to 0.2.6
- upgrade typescript to 5.3.3
- upgrade @types/mime-types to 2.1.4
- remove d3 from deps
- upgrade electron-updater to 6.1.7
- upgrade rendition to 35.1.2
- upgrade node-ipc to 9.2.3
- upgrade @types/node-ipc to 9.2.3
- upgrade electron to 27.1.3
- upgrade @electron-forge/* to 7.2.0
- upgrade @reforged/marker-appimage to 3.3.2
- upgrade style-loader to 3.3.3
- upgrade balena-lint to 7.2.4
- run CI with node 18.19
- add xxhash-addon to sidecar assets

Change-type: patch
2023-12-22 15:20:28 +02:00
flowzone-app[bot]
70304b492d v1.19.2 2023-12-22 12:57:38 +00:00
dfunckt
8eacab2c4b Merge pull request #4146 from Rotzbua/fix_typo
fix: typos
2023-12-22 14:56:47 +02:00
Rotzbua
aaac133670 fix: typos
Change-type: patch
2023-12-22 09:27:40 +01:00
flowzone-app[bot]
d1b5a2aea1 v1.19.1 2023-12-22 08:12:38 +00:00
dfunckt
fffe5e278f Merge pull request #4091 from vedantmgoyal2009/patch-1
patch: update winget-releaser v2
2023-12-22 10:11:52 +02:00
Vedant
ea184eb635 patch: update winget-releaser v2 2023-12-22 13:09:00 +05:30
flowzone-app[bot]
5bb8ba857a v1.19.0 2023-12-21 16:42:01 +00:00
dfunckt
6e4db830e9 Merge pull request #4132 from balena-io/switch-to-electron-forge
Modernize build pipeline
2023-12-21 18:41:10 +02:00
Akis Kesoglou
a0dd6c5401 Update docs 2023-12-21 18:19:33 +02:00
Akis Kesoglou
01a96bb6de Use native ARM runner for Apple Silicon builds
Change-type: minor
2023-12-21 18:17:33 +02:00
Akis Kesoglou
2e3a75e685 Calculate and upload build artifact sha256 checksums
Change-type: minor
2023-12-21 18:15:42 +02:00
Akis Kesoglou
da4f3ca28e Bundle etcher-util with main app 2023-12-21 14:38:47 +02:00
Akis Kesoglou
a22d2468fd Run on CI 2023-12-21 14:38:47 +02:00
Akis Kesoglou
559f2b4d68 Define packaging targets 2023-12-21 13:33:57 +02:00
Akis Kesoglou
bd33c5b092 Migrate build pipeline to Electron Forge
Change-type: minor
2023-12-21 13:33:25 +02:00
flowzone-app[bot]
2cdf65b244 v1.18.14 2023-12-20 16:23:04 +00:00
Kyle Harding
8645273fef Merge pull request #4149 from balena-io/kyle/upload-artifact-v4
Update actions/upload-artifact to v4
2023-12-20 11:22:07 -05:00
Kyle Harding
ecb24dad25 Remove repo config from flowzone.yml
This functionality is being deprecated in Flowzone.

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

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
2023-12-20 11:02:10 -05:00
Kyle Harding
a970f55b55 Update actions/upload-artifact to v4
Also ensure we are generating unique artifact names on upload.

Change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
See: https://github.com/product-os/flowzone/pull/827
2023-12-19 13:57:25 -05:00
flowzone-app[bot]
e969735955 v1.18.13 2023-10-16 13:32:31 +00:00
flowzone-app[bot]
45bb29a393 Merge pull request #4102 from balena-io/aethernet/childwriter-standalone
patch: compile child-writer.ts as a standalone cli
2023-10-16 13:31:21 +00:00
Edwin Joassart
f38bca290f patch: upgrade to electron 25 2023-10-16 14:49:06 +02:00
Edwin Joassart
fb8ed5b529 patch: refactor scanner, loader and flasher out of gui + upgrade to electron 25 2023-10-16 14:49:06 +02:00
flowzone-app[bot]
09e13e9b43 v1.18.12 2023-07-19 10:24:26 +00:00
Edwin Joassart
13e1e8e504 Merge pull request #4060 from jcapona/master
patch: update instructions for installing deb file
2023-07-19 12:23:20 +02:00
Jorge Capona
acab03ad77 Update instructions for installing deb file
Change-type: patch
2023-07-14 15:27:38 -05:00
flowzone-app[bot]
0a6c15f702 v1.18.11 2023-07-13 14:31:44 +00:00
dfunckt
589ce9c28e Merge pull request #4075 from leadpogrommer/fix_focus_stealing
Prevent stealing window focus from auth dialog
2023-07-13 17:30:44 +03:00
leadpogrommer
f716c74ef7 fix: prevent stealing window focus from auth dialog
Change-type: patch
2023-07-12 22:06:04 +07:00
flowzone-app[bot]
2d7a6220cd v1.18.10 2023-07-12 11:22:02 +00:00
dfunckt
e0b26d455c Merge pull request #3765 from jsoref/spelling
Spelling
2023-07-12 14:21:07 +03:00
Josh Soref
06d246e3fd spelling: validates
Change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2023-07-12 13:40:47 +03:00
Josh Soref
67b26a5b69 spelling: undefined
Change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2023-07-12 13:40:47 +03:00
Josh Soref
b4b9db7ffa spelling: except if
Change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
2023-07-12 13:40:47 +03:00
flowzone-app[bot]
cc037d23c4 v1.18.9 2023-07-12 09:07:22 +00:00
dfunckt
9c9c036956 Merge pull request #4086 from balena-io/fix-opening-links-in-safe-webview
Fix opening links from within SafeWebView
2023-07-12 12:06:25 +03:00
Akis Kesoglou
7fdbc439f7 Fix Publish action on Windows 2023-07-12 11:34:22 +03:00
Akis Kesoglou
9410669294 Fix lint issues 2023-07-06 21:50:29 +03:00
Akis Kesoglou
497bb0e2cb Fix opening links from within SafeWebView
Change-type: patch
2023-07-06 21:41:59 +03:00
balenaCI
a42be8ee74 v1.18.8 2023-04-26 09:57:47 +00:00
flowzone-app[bot]
16b50d2a71 Merge pull request #4056 from balena-io/update-support
Patch: Fix support link
2023-04-26 09:57:05 +00:00
Oliver Plummer
882b385c88 Patch: Fix Support link 2023-04-26 11:25:55 +02:00
balenaCI
059a36659e v1.18.7 2023-04-25 15:25:37 +00:00
flowzone-app[bot]
cd9cf09422 Merge pull request #4055 from balena-io/update-docs
Update docs
2023-04-25 15:24:48 +00:00
Edwin Joassart
02a4067118 patch: update docs to remove cloudsmith install instructions for linux 2023-04-25 16:55:30 +02:00
82 changed files with 41847 additions and 38850 deletions

10
.eslintrc.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
extends: ["./node_modules/@balena/lint/config/.eslintrc.js"],
root: true,
ignorePatterns: ["node_modules/"],
rules: {
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-comment": "off",
},
};

View File

@@ -1,454 +0,0 @@
env:
browser: true
commonjs: true
es6: true
node: true
mocha: true
plugins:
- lodash
- jsdoc
- node
- react
extends: 'standard'
parserOptions:
sourceType: 'script'
ecmaFeatures:
jsx: true
settings:
jsdoc:
additionalTagNames:
customTags:
- fulfil
rules:
# Possible Errors
no-console:
- off
no-empty:
- error
no-extra-semi:
- error
no-negated-in-lhs:
- error
no-prototype-builtins:
- error
valid-jsdoc:
- error
- requireReturn: false
requireReturnDescription: false
requireReturnType: true
requireParamDescription: true
preferType:
boolean: "Boolean"
number: "Number"
object: "Object"
string: "String"
array: "Array"
prefer:
arg: "param"
return: "returns"
# Best Practices
array-callback-return:
- error
block-scoped-var:
- error
class-methods-use-this:
- error
complexity:
- off
consistent-return:
- error
curly:
- error
default-case:
- error
dot-notation:
- error
guard-for-in:
- error
no-alert:
- error
no-case-declarations:
- error
no-div-regex:
- error
no-else-return:
- error
no-empty-function:
- error
no-eq-null:
- error
no-extra-label:
- error
no-implicit-coercion:
- error
no-implicit-globals:
- error
no-loop-func:
- error
no-magic-numbers:
- error
no-native-reassign:
- error
no-param-reassign:
- error
no-restricted-properties:
- error
- property: __proto__
no-return-await:
- error
no-script-url:
- error
no-unused-expressions:
- error
no-unused-labels:
- error
no-useless-concat:
- error
no-void:
- error
no-warning-comments:
- off
radix:
- error
vars-on-top:
- off
# Strict mode
strict:
- error
- global
# Variables
init-declarations:
- error
- always
no-catch-shadow:
- error
no-restricted-globals:
- error
- event
no-shadow:
- error
no-undefined:
- error
no-unused-vars:
- error
no-use-before-define:
- error
# NodeJS and CommonJS
callback-return:
- error
global-require:
- off
no-mixed-requires:
- error
no-process-env:
- off
no-process-exit:
- off
no-sync:
- off
# Stylistic Issues
array-bracket-spacing:
- error
- always
capitalized-comments:
- error
- always
- ignoreConsecutiveComments: true
comma-spacing:
- error
- before: false
after: true
computed-property-spacing:
- error
- never
consistent-this:
- error
- self
func-name-matching:
- error
- always
func-names:
- error
- never
func-style:
- error
- expression
id-blacklist:
- error
id-length:
- error
- min: 2
exceptions:
- "_"
id-match:
- error
- "^[_0-9A-Za-z\\$]+$"
line-comment-position:
- error
- position: above
linebreak-style:
- error
- unix
lines-around-comment:
- error
- beforeBlockComment: true
afterBlockComment: false
beforeLineComment: true
afterLineComment: false
allowBlockStart: true
allowBlockEnd: false
allowObjectStart: true
allowObjectEnd: false
allowArrayStart: true
allowArrayEnd: false
lines-around-directive:
- error
- always
max-len:
- error
- code: 130
comments: 150
ignoreComments: false
ignoreTrailingComments: false
ignoreUrls: true
max-params:
- off
max-statements-per-line:
- error
- max: 1
multiline-ternary:
- off
newline-per-chained-call:
- off
no-bitwise:
- error
no-continue:
- error
no-inline-comments:
- error
no-lonely-if:
- error
no-mixed-operators:
- error
no-multi-assign:
- error
no-negated-condition:
- error
no-nested-ternary:
- error
no-plusplus:
- error
no-restricted-syntax:
- error
- WithStatement
- ForInStatement
no-spaced-func:
- error
no-underscore-dangle:
- error
- allowAfterThis: false
object-curly-newline:
- error
- minProperties: 3
consistent: true
object-curly-spacing:
- error
- always
one-var-declaration-per-line:
- error
- always
operator-assignment:
- error
- always
quotes:
- error
- single
quote-props:
- error
- as-needed
require-jsdoc:
- error
- require:
FunctionDeclaration: true
ClassDeclaration: true
MethodDefinition: true
ArrowFunctionExpression: true
space-before-function-paren:
- error
- anonymous: always
named: always
asyncArrow: always
template-tag-spacing:
- error
- always
unicode-bom:
- error
# ECMAScript 6
arrow-parens:
- error
- always
arrow-spacing:
- error
- before: true
after: true
generator-star-spacing:
- error
- before: true
after: false
no-confusing-arrow:
- error
no-var:
- error
object-shorthand:
- error
- always
prefer-const:
- error
prefer-spread:
- error
prefer-numeric-literals:
- error
prefer-rest-params:
- error
prefer-template:
- error
prefer-arrow-callback:
- error
- allowNamedFunctions: false
require-yield:
- error
symbol-description:
- error
# Lodash
lodash/chain-style:
- error
- explicit
lodash/identity-shorthand:
- error
- always
lodash/import-scope:
- error
- full
lodash/matches-prop-shorthand:
- error
- always
lodash/matches-shorthand:
- error
- always
lodash/no-commit:
- error
lodash/path-style:
- error
- array
lodash/prefer-compact:
- error
lodash/prefer-filter:
- error
- 5
lodash/prefer-flat-map:
- error
lodash/prefer-invoke-map:
- error
lodash/prefer-map:
- error
lodash/prefer-reject:
- error
lodash/prefer-thru:
- error
lodash/prefer-wrapper-method:
- error
lodash/prop-shorthand:
- error
- always
lodash/prefer-constant:
- error
- true
- true
lodash/prefer-get:
- error
- 2
lodash/prefer-includes:
- error
- includeNative: true
lodash/prefer-is-nil:
- error
lodash/prefer-lodash-chain:
- error
lodash/prefer-lodash-method:
- error
lodash/prefer-lodash-typecheck:
- error
lodash/prefer-matches:
- error
- 3
lodash/prefer-noop:
- error
lodash/prefer-over-quantifier:
- error
lodash/prefer-startswith:
- error
lodash/prefer-times:
- error
# JSDoc
jsdoc/check-param-names:
- error
jsdoc/check-tag-names:
- error
jsdoc/newline-after-description:
- error
jsdoc/require-example:
- error
jsdoc/require-hyphen-before-param-description:
- error
jsdoc/require-param:
- error
jsdoc/require-param-description:
- error
jsdoc/require-param-type:
- error
jsdoc/require-returns-type:
- error
# Node
node/no-deprecated-api:
- error
node/no-missing-import:
- error
node/no-missing-require:
- error
node/process-exit-as-throw:
- error
node/no-extraneous-require:
- error
node/no-extraneous-import:
- error
# React
react/jsx-uses-vars:
- error
overrides:
files: ['*.jsx']
rules:
require-jsdoc:
- off

View File

@@ -10,12 +10,12 @@ inputs:
required: true
# --- custom environment
XCODE_APP_LOADER_EMAIL:
type: string
default: "accounts+apple@balena.io"
NODE_VERSION:
type: string
default: "16.x"
# 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"
VERBOSE:
type: string
default: "true"
@@ -25,138 +25,174 @@ runs:
using: "composite"
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}
- name: Extract custom source artifact
shell: pwsh
if: runner.os != 'Windows'
shell: bash
working-directory: .
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Extract custom source artifact
if: runner.os == 'Windows'
shell: pwsh
working-directory: .
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -xf ${{ runner.temp }}\custom.tgz
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Install yq
shell: bash --noprofile --norc -eo pipefail -x {0}
run: choco install yq
if: runner.os == 'Windows'
- name: Install host dependencies
if: runner.os == 'Linux'
shell: bash
run: sudo apt-get install -y --no-install-recommends fakeroot dpkg rpm
- name: Install host dependencies
if: runner.os == 'macOS'
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
# This is a temporary workaround to make the job use Python 3.11 until
# we update to npm 10+.
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
with:
python-version: '3.11'
# https://www.electron.build/code-signing.html
# https://github.com/Apple-Actions/import-codesign-certs
# https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
- name: Import Apple code signing certificate
if: runner.os == 'macOS'
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
shell: bash
run: |
KEY_CHAIN=build.keychain
CERTIFICATE_P12=certificate.p12
# Recreate the certificate from the secure environment variable
echo $CERTIFICATE_P12_B64 | base64 --decode > $CERTIFICATE_P12
# Create a keychain
security create-keychain -p actions $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
# remove certs
rm -fr *.p12
env:
CERTIFICATE_P12_B64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
- name: Import Windows code signing certificate
if: runner.os == 'Windows'
id: import_win_signing_cert
shell: powershell
run: |
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:SM_CLIENT_CERT_FILE_B64
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/Certificate_pkcs12.p12
Remove-Item -path ${{ runner.temp }} -include certificate.base64
Import-PfxCertificate `
-FilePath ${{ runner.temp }}/certificate.pfx `
-CertStoreLocation Cert:\CurrentUser\My `
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
Remove-Item -path ${{ runner.temp }} -include certificate.pfx
echo "certFilePath=${{ runner.temp }}/Certificate_pkcs12.p12" >> $GITHUB_OUTPUT
env:
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
SM_CLIENT_CERT_FILE_B64: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_FILE_B64 }}
# ... or refactor (e.g.) https://github.com/samuelmeuli/action-electron-builder
# https://github.com/product-os/scripts/tree/master/electron
# https://github.com/product-os/scripts/tree/master/shared
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
- name: Package release
id: package_release
shell: bash --noprofile --norc -eo pipefail -x {0}
shell: bash
# IMPORTANT: before making changes to this step please consult @engineering in balena's chat.
run: |
set -ea
## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
# export DEBUG='electron-forge:*,sidecar'
# fi
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
ELECTRON_BUILDER_ARCHITECTURE="${runner_arch}"
APPLICATION_VERSION="$(jq -r '.version' package.json)"
ARCHITECTURE_FLAGS="--${ELECTRON_BUILDER_ARCHITECTURE}"
HOST_ARCH="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
if [[ $runner_os =~ linux ]]; then
ELECTRON_BUILDER_OS='--linux'
TARGETS="$(yq e .linux.target[] electron-builder.yml)"
if [[ "${RUNNER_OS}" == Linux ]]; then
PLATFORM=Linux
SHA256SUM_BIN=sha256sum
elif [[ $runner_os =~ darwin|macos|osx ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
ELECTRON_BUILDER_OS='--mac'
TARGETS="$(yq e .mac.target[] electron-builder.yml)"
elif [[ "${RUNNER_OS}" == macOS ]]; then
PLATFORM=Darwin
SHA256SUM_BIN='shasum -a 256'
elif [[ $runner_os =~ windows|win ]]; then
ARCHITECTURE_FLAGS="--ia32 ${ARCHITECTURE_FLAGS}"
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
CSC_LINK=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
ELECTRON_BUILDER_OS='--win'
TARGETS="$(yq e .win.target[] electron-builder.yml)"
elif [[ "${RUNNER_OS}" == Windows ]]; then
PLATFORM=Windows
SHA256SUM_BIN=sha256sum
# 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
msiexec -i smtools-windows-x64.msi -qn
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
smksp_registrar.exe list
smctl.exe keypair ls
/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}"
else
exit 1
echo "ERROR: unexpected runner OS: ${RUNNER_OS}"
exit 1
fi
npm link electron-builder
for target in ${TARGETS}; do
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
--c.extraMetadata.analytics.sentry.token='https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632' \
--c.extraMetadata.analytics.amplitude.token='balena-etcher' \
--c.extraMetadata.packageType="${target}"
find dist -type f -maxdepth 1
done
# Currently, we can only build for the host architecture.
npx electron-forge make
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
# collect all artifacts from subdirectories under a common top-level directory
mkdir -p dist
find ./out/make -type f \( \
-iname "*.zip" -o \
-iname "*.dmg" -o \
-iname "*.rpm" -o \
-iname "*.deb" -o \
-iname "*.AppImage" -o \
-iname "*Setup.exe" \
\) -ls -exec cp '{}' dist/ \;
if [[ -n "${SHA256SUM_BIN}" ]]; then
# Compute and save digests.
cd dist/
${SHA256SUM_BIN} *.* >"SHA256SUMS.${PLATFORM}.${HOST_ARCH}.txt"
fi
env:
# Apple notarization (afterSignHook.js)
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
# ensure we sign the artifacts
NODE_ENV: production
# analytics tokens
SENTRY_TOKEN: https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632
AMPLITUDE_TOKEN: 'balena-etcher'
# Apple notarization
XCODE_APP_LOADER_EMAIL: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
CSC_FOR_PULL_REQUEST: true
# https://www.electron.build/auto-update.html#staged-rollouts
- name: Configure staged rollout(s)
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
percentage="$(cat < repo.yml | yq e .triggerNotification.stagingPercentage)"
find dist -type f -maxdepth 1 \
-name "latest*.yml" \
-exec yq -i e .version=\"${{ steps.package_release.outputs.version }}\" {} \;
find dist -type f -maxdepth 1 \
-name "latest*.yml" \
-exec yq -i e .stagingPercentage=\"$percentage\" {} \;
XCODE_APP_LOADER_TEAM_ID: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_TEAM_ID }}
# Windows signing
SM_CLIENT_CERT_PASSWORD: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_PASSWORD }}
SM_CLIENT_CERT_FILE: '${{ runner.temp }}\Certificate_pkcs12.p12'
SM_HOST: ${{ fromJSON(inputs.secrets).SM_HOST }}
SM_API_KEY: ${{ fromJSON(inputs.secrets).SM_API_KEY }}
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ fromJSON(inputs.secrets).SM_CODE_SIGNING_CERT_SHA1_HASH }}
TIMESTAMP_SERVER: http://timestamp.digicert.com
- name: Upload artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: dist
retention-days: 1
if-no-files-found: error

View File

@@ -12,7 +12,7 @@ inputs:
# --- custom environment
NODE_VERSION:
type: string
default: "16.x"
default: "20.10"
VERBOSE:
type: string
default: "true"
@@ -28,31 +28,54 @@ runs:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Test release
shell: bash --noprofile --norc -eo pipefail -x {0}
- name: Install host dependencies
if: runner.os == 'Linux'
shell: bash
run: |
set -ea
sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb libudev-dev
cat < package.json | jq -r '.hostDependencies[][]' - | \
xargs -L1 echo | sed 's/|//g' | xargs -L1 \
sudo apt-get --ignore-missing install || true
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
- name: Install host dependencies
if: runner.os == 'macOS'
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
# This is a temporary workaround to make the job use Python 3.11 until
# we update to npm 10+.
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
with:
python-version: '3.11'
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
- name: Test release
shell: bash
run: |
## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
# export DEBUG='electron-forge:*,sidecar'
# fi
npm run flowzone-preinstall-${runner_os}
npm ci
npm run build
npm run test-${runner_os}
npm run lint
npm run package
npm run test
env:
# https://www.electronjs.org/docs/latest/api/environment-variables
ELECTRON_NO_ATTACH_CONSOLE: true
ELECTRON_NO_ATTACH_CONSOLE: 'true'
- name: Compress custom source
shell: pwsh
if: runner.os != 'Windows'
shell: bash
run: tar -acf ${{ runner.temp }}/custom.tgz .
- name: Compress custom source
if: runner.os == 'Windows'
shell: pwsh
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -acf ${{ runner.temp }}\custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

View File

@@ -1,5 +1,4 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
@@ -8,7 +7,6 @@ on:
pull_request_target:
types: [opened, synchronize, closed]
branches: [main, master]
jobs:
flowzone:
name: Flowzone
@@ -20,11 +18,7 @@ jobs:
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
secrets: inherit
with:
tests_run_on: '["ubuntu-20.04","macos-latest","windows-2019"]'
custom_runs_on: '[["ubuntu-20.04"],["windows-2019"],["macos-12"],["macos-latest-xlarge"]]'
restrict_custom_actions: false
github_prerelease: true
repo_config: true
repo_description: "Flash OS images to SD cards & USB drives, safely and easily."
repo_homepage: https://etcher.io/
repo_enable_wiki: true
cloudflare_website: "etcher"

View File

@@ -6,8 +6,9 @@ jobs:
publish:
runs-on: windows-latest # action can only be run on windows
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: Balena.Etcher
installers-regex: 'balenaEtcher-Setup.*.exe$'
# matches something like "balenaEtcher-1.19.0.Setup.exe"
installers-regex: 'balenaEtcher-[\d.-]+\.Setup.exe$'
token: ${{ secrets.WINGET_PAT }}

112
.gitignore vendored
View File

@@ -1,40 +1,103 @@
# -- ADD NEW ENTRIES AT THE END OF THE FILE ---
# Logs
/logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
/lib-cov
# Image stream output directory
/tests/image-stream/output
lib-cov
# Coverage directory used by tools like istanbul
/coverage
coverage
*.lcov
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
/build
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Generated files
/generated
# Dependency directories
node_modules/
jspm_packages/
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# TypeScript v1 declaration files
typings/
# Compiled Etcher releases
/dist
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Vite
.vite/
# Electron-Forge
out/
# ---- Do not modify entries above this line ----
# Build artifacts
dist/
# Certificates
*.spc
@@ -44,16 +107,17 @@ node_modules
*.crt
*.pem
# OSX files
.DS_Store
# VSCode files
.vscode
# Secrets
.gitsecret/keys/random_seed
!*.secret
secrets/APPLE_SIGNING_PASSWORD.txt
secrets/WINDOWS_SIGNING_PASSWORD.txt
secrets/XCODE_APP_LOADER_PASSWORD.txt
secrets/WINDOWS_SIGNING.pfx
# Image stream output directory
/tests/image-stream/output
#local development
.yalc
yalc.lock

4
.gitmodules vendored
View File

@@ -1,4 +0,0 @@
[submodule "scripts/resin"]
path = scripts/resin
url = https://github.com/balena-io/scripts.git
branch = master

2
.nvmrc
View File

@@ -1 +1 @@
16
18

6
.prettierrc.js Normal file
View File

@@ -0,0 +1,6 @@
const fs = require("fs");
const path = require("path");
module.exports = JSON.parse(
fs.readFileSync(path.join(__dirname, "node_modules", "@balena", "lint", "config", ".prettierrc"), "utf8"),
);

View File

@@ -1,3 +1,348 @@
- commits:
- subject: "patch: replace deprecated pkg with yao-pkg and bump etcher-util node v
to 20.10"
hash: c696c389c9988c75ad9ccc472bdac7edefe762ed
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.8
title: ""
date: 2024-04-22T09:37:37.561Z
- commits:
- subject: "patch: fix formating"
hash: 1a9a3d2cdc5642a754b73628f4ae2636e3ffd8eb
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: configure prettier in the project to use balena-lint
configuration"
hash: faeaa58ec548e47abaf30b2498ab145e7c0c6f76
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.7
title: ""
date: 2024-04-22T06:52:18.878Z
- commits:
- subject: "patch: fix win signature process"
hash: f629e6d53b5329cd7e8105050df042f3873a35ee
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.6
title: ""
date: 2024-04-19T15:59:28.200Z
- commits:
- subject: Replace deprecated flowzone input tests_run_on
hash: bec0e50741bfeda63ca9785217576613f74ca043
body: |
The `custom_runs_on` array supports multiple runner labels
in nested arrays.
footer:
Change-type: patch
change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
signed-off-by: Kyle Harding <kyle@balena.io>
author: Kyle Harding
nested: []
version: 1.19.5
title: ""
date: 2024-02-14T19:51:16.321Z
- commits:
- subject: "patch: remove screensaver error when not on etcher-pro"
hash: 196fd8ae24de2a23ebaeae736c6ca41007162fa1
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: fix typo in IPC server id"
hash: 5d436992423961258ad861c01e3b9b30f3317aab
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.4
title: ""
date: 2024-01-26T17:29:27.301Z
- commits:
- subject: Update dependencies
hash: 0f2b4dbc106c55fe104f0b10e62c35c16bcfe9b3
body: >
- upgrade pretty_bytes to 6.1.1
- upgrade electron-remote to 2.1.0
- upgrade semver to 7.5.4 + @types/semver to 7.5.6
- upgrade chai to 4.3.11 + @types/chai to 4.3.10
- upgrade mocha to 10.2.0 + @types/mocha to 10.0.6
- upgrade sinon to 17.0.1 + @types/sinon to 17.0.2
- remove useless @types
- upgrade @svgr/webpack to 8.1.0
- upgrade @sentry/electron to 4.15.1
- upgrade tslib to 2.6.2
- upgrade immutable to 4.3.4
- upgrade redux to 4.2.1
- upgrade ts-node to 10.9.2 & ts-loader to 9.5.1
- remove mini-css-extract-plugin
- upgrade husky to 8.0.3
- upgrade uuid to 9.0.1
- upgrade lint-staged to 15.2.1
- upgrade @types/node to 18.11.9
- upgrade @fortawesome/fontawesome-free to 6.5.1
- upgrade i18next to 23.7.8 & react-i18next to 11.18.6
- bump react, react-dom + related @types to 17.0.2 and rendition to
35.1.0
- fix getuid for ts
- fix @types/react being in wrong deps
- upgrade @types/tmp to 0.2.6
- upgrade typescript to 5.3.3
- upgrade @types/mime-types to 2.1.4
- remove d3 from deps
- upgrade electron-updater to 6.1.7
- upgrade rendition to 35.1.2
- upgrade node-ipc to 9.2.3
- upgrade @types/node-ipc to 9.2.3
- upgrade electron to 27.1.3
- upgrade @electron-forge/* to 7.2.0
- upgrade @reforged/marker-appimage to 3.3.2
- upgrade style-loader to 3.3.3
- upgrade balena-lint to 7.2.4
- run CI with node 18.19
- add xxhash-addon to sidecar assets
footer:
Change-type: patch
change-type: patch
author: Edwin Joassart
nested: []
version: 1.19.3
title: ""
date: 2023-12-22T16:13:00.924Z
- commits:
- subject: "fix: typos"
hash: aaac1336702b7ac4a07992f41db4f0bcdb931c70
body: ""
footer:
Change-type: patch
change-type: patch
author: Rotzbua
nested: []
version: 1.19.2
title: ""
date: 2023-12-22T12:57:35.441Z
- commits:
- subject: "patch: update winget-releaser v2"
hash: ea184eb6352b7988c6ab1f439d30c297610cd84e
body: ""
footer: {}
author: Vedant
nested: []
version: 1.19.1
title: ""
date: 2023-12-22T08:12:34.451Z
- commits:
- subject: Use native ARM runner for Apple Silicon builds
hash: 01a96bb6de1ff00d20f7784469dd05286069e014
body: ""
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
- subject: Calculate and upload build artifact sha256 checksums
hash: 2e3a75e685258961bc8efdb95dde12727b93a04a
body: ""
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
- subject: Migrate build pipeline to Electron Forge
hash: bd33c5b092cb5224c8dfc4d5a2caf4684cee161d
body: ""
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
version: 1.19.0
title: ""
date: 2023-12-21T16:41:57.426Z
- commits:
- subject: Remove repo config from flowzone.yml
hash: ecb24dad251fbb9b3f92e5b404b66aedd155a584
body: |
This functionality is being deprecated in Flowzone.
See: https://github.com/product-os/flowzone/pull/833
footer:
Change-type: patch
change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
signed-off-by: Kyle Harding <kyle@balena.io>
author: Kyle Harding
nested: []
- subject: Update actions/upload-artifact to v4
hash: a970f55b555f69c5fcb40374eb50ad7b98cc8f96
body: |
Also ensure we are generating unique artifact names on upload.
footer:
Change-type: patch
change-type: patch
Signed-off-by: Kyle Harding <kyle@balena.io>
signed-off-by: Kyle Harding <kyle@balena.io>
See: https://github.com/product-os/flowzone/pull/827
see: https://github.com/product-os/flowzone/pull/827
author: Kyle Harding
nested: []
version: 1.18.14
title: ""
date: 2023-12-20T16:23:00.875Z
- commits:
- subject: "patch: upgrade to electron 25"
hash: f38bca290fe26121bed58d1131265e1aa350ddb5
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: refactor scanner, loader and flasher out of gui + upgrade to
electron 25"
hash: fb8ed5b529e22bc9e766bfe99c2b6955ed695b58
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.18.13
title: ""
date: 2023-10-16T13:32:26.738Z
- commits:
- subject: Update instructions for installing deb file
hash: acab03ad77a1c1901d0c8a65999e93c1d27169a0
body: ""
footer:
Change-type: patch
change-type: patch
author: Jorge Capona
nested: []
version: 1.18.12
title: ""
date: 2023-07-19T10:24:22.407Z
- commits:
- subject: "fix: prevent stealing window focus from auth dialog"
hash: f716c74ef7cb164b4d825828e4e46033484ad9af
body: ""
footer:
Change-type: patch
change-type: patch
author: leadpogrommer
nested: []
version: 1.18.11
title: ""
date: 2023-07-13T14:31:40.021Z
- commits:
- subject: "spelling: validates"
hash: 06d246e3fd1c573b9e04d23ab3bc3c4036fb9859
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
author: Josh Soref
nested: []
- subject: "spelling: undefined"
hash: 67b26a5b69f819066c6419d3d915846b63fdbcf0
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
author: Josh Soref
nested: []
- subject: "spelling: except if"
hash: b4b9db7ffa2104c19e7bd079e4f394a817f40bc0
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
signed-off-by: Josh Soref <jsoref@users.noreply.github.com>
author: Josh Soref
nested: []
version: 1.18.10
title: ""
date: 2023-07-12T11:21:59.231Z
- commits:
- subject: Fix opening links from within SafeWebView
hash: 497bb0e2cbefad3e9a1188ee5df49cf61f6bd6e4
body: ""
footer:
Change-type: patch
change-type: patch
author: Akis Kesoglou
nested: []
version: 1.18.9
title: ""
date: 2023-07-12T09:07:17.666Z
- commits:
- subject: "Patch: Fix Support link"
hash: 882b385c88111a192e5f37e20c1c8aeca9950b21
body: ""
footer: {}
author: Oliver Plummer
nested: []
version: 1.18.8
title: ""
date: 2023-04-26T09:57:46.155Z
- commits:
- subject: "patch: update docs to remove cloudsmith install instructions for linux"
hash: 02a406711852cf237e41da4cd39350d8acc1f0b0
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.18.7
title: ""
date: 2023-04-25T15:25:35.584Z
- commits:
- subject: add-flash-with-etcher-to-docs
hash: 856b426dc98925f5e339976a5cac144f4bb4ea59
@@ -190,10 +535,11 @@
- subject: Switch to `@electron/remote`
hash: 7ee174edcecbfc2d7370db6d4185b3ee4eedbe28
body: >
Electron 12 deprecated `electron.remote` and the functionality was removed
in Electron 14, but became available as a separate `@electron/remote`
module. This commit makes the transition to the external module as an
intermediary step to enable updating to a newer Electron version.
Electron 12 deprecated `electron.remote` and the functionality was
removed in Electron 14, but became available as a separate
`@electron/remote` module. This commit makes the transition to the
external module as an intermediary step to enable updating to a newer
Electron version.
footer:
Change-type: patch
change-type: patch
@@ -238,8 +584,8 @@
- subject: Lazily import Electron from child-writer process
hash: 851219f835ed037d9fd970f538095e4b339c5342
body: >
No idea how this *used* to work, but it doesnt since 887ec428 and this is
fixing it properly.
No idea how this *used* to work, but it doesnt since 887ec428 and this
is fixing it properly.
footer:
Change-type: patch
change-type: patch
@@ -256,20 +602,33 @@
step forward to upgrading to a newer Electron and Node version.
Updates etcher-sdk and switches the redundant aws4-axios dependency to just axios.
Updates etcher-sdk and switches the redundant aws4-axios dependency to
just axios.
Also changed bundler to stop trying to bundle wasm files — they must be included inline with JS code as data — and removed some now redundant code.
Also changed bundler to stop trying to bundle wasm files — they must be
included inline with JS code as data — and removed some now redundant
code.
The crucial changes that enable support are:
1. The update to etcher-sdk@8 where some dependency fixes and updates took place
1. The update to etcher-sdk@8 where some dependency fixes and updates
took place
2. The downgrade and pinning of "electron-rebuild" to v3.2.3 until were able to update to Electron >= 14.2. The patch we need to avoid is https://github.com/electron/rebuild/pull/907. Also see: https://github.com/nodejs/node-gyp/issues/2673 and https://github.com/electron/rebuild/issues/913
2. The downgrade and pinning of "electron-rebuild" to v3.2.3 until were
able to update to Electron >= 14.2. The patch we need to avoid is
https://github.com/electron/rebuild/pull/907. Also see:
https://github.com/nodejs/node-gyp/issues/2673 and
https://github.com/electron/rebuild/issues/913
3. A rule in webpack.config to ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used by etcher-sdk and does a runtime check to its availability. Were not currently using the “assume role” functionality (AFAIU) of aws4-axios and we dont care that its not found, so force webpack to ignore the import. See https://github.com/aws/aws-sdk-js-v3/issues/3025
3. A rule in webpack.config to ignore `aws-crt` which is a dependency of
(ultimately) `aws4-axios` which is used by etcher-sdk and does a runtime
check to its availability. Were not currently using the “assume role”
functionality (AFAIU) of aws4-axios and we dont care that its not
found, so force webpack to ignore the import. See
https://github.com/aws/aws-sdk-js-v3/issues/3025
footer:
Change-type: minor
change-type: minor
@@ -549,7 +908,8 @@
body: >
Optimized several translations.
This commit itself is only a patch, but as a pull request must have at least one commit with a change-type.
This commit itself is only a patch, but as a pull request must have at
least one commit with a change-type.
footer:
Change-Type: minor
change-type: minor
@@ -637,7 +997,8 @@
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: []
@@ -1184,7 +1545,8 @@
- subject: Fixing call to electron block screensaver methods invocation
hash: 1b5b64713505dfb69448bc2184839b4c23bd677b
body: >
Replacing `send` calls to `invoke` for `enable/disable-screensaver` calls.
Replacing `send` calls to `invoke` for `enable/disable-screensaver`
calls.
footer:
Change-type: patch
change-type: patch
@@ -1936,11 +2298,11 @@
- subject: Add support for basic auth when downloading images from URL.
hash: b2d0c1c9ddbbfe87d5a905d420d615821610e825
body: >
When selecting "Flash from URL" the user can optionally provide a username
and password for basic authentication. The authentication input fields
are collapsed by default. When the authentication input fields are
collapsed after entering values the values are cleared to ensure that
the user sees all parameter passed to the server.
When selecting "Flash from URL" the user can optionally provide a
username and password for basic authentication. The authentication input
fields are collapsed by default. When the authentication input fields
are collapsed after entering values the values are cleared to ensure
that the user sees all parameter passed to the server.
footer:
Change-Type: minor
change-type: minor
@@ -2167,7 +2529,8 @@
reloads without reloading the whole electron app.
This patch also runs the development environment in development mode, which is much, much faster on builds and rebuilds.
This patch also runs the development environment in development mode,
which is much, much faster on builds and rebuilds.
footer: {}
author: Zane Hitchcox
nested: []
@@ -2497,7 +2860,8 @@
- subject: Ignore ENOENT errors on unlink in withTmpFile
hash: 7bb2a23c4e94dcda6a7b494fe0435c0b59b56b06
body: >
The temporary file might have been already deleted by cleanupTmpFiles
The temporary file might have been already deleted by
cleanupTmpFiles
footer:
Change-type: patch
change-type: patch
@@ -2567,12 +2931,15 @@
- subject: Pass strings between methods as std::string instead of char *
hash: 1ec6a8ffc4c9e138b78210f0db84a9ebd6c9182b
body: >
- Fixes "basic_string::_M_construct null not valid" exception
- Fixes "basic_string::_M_construct null not valid"
exception
aborting program, because WCharToUtf8() returned NULL
in some cases, and NULL was being fed to string constructor.
- Fixes memory leak because memory allocated with calloc()
- Fixes memory leak because memory allocated with
calloc()
in WCharToUtf8() was not being freed anywhere
- Fixes undefined behavior because GetEnumeratorName() returns
- Fixes undefined behavior because GetEnumeratorName()
returns
pointer to stack memory, that goes outside of scope while
pointer still is being used.
@@ -4494,7 +4861,8 @@
Although it's possible to use a PC keyboard on a Mac, it's unusual.
In any case, all of the macOS (not "Mac OS" for some years now) documentation refers to the "Opt" key.
In any case, all of the macOS (not "Mac OS" for some years now)
documentation refers to the "Opt" key.
- hash: ea11f179542794294f773f503d83dad3a10cda56
author: Tom
footers:
@@ -4655,10 +5023,11 @@
change-type: patch
subject: Fixes the Command for macOS drive recovery
body: >-
Changes the documentation to update the disktutil command which didn't fix
my case, cause the boot partition was broken.
Changes the documentation to update the disktutil command which didn't
fix my case, cause the boot partition was broken.
This way it rewrites the drive into a FAT32 partition editable in Unix/Windows.
This way it rewrites the drive into a FAT32 partition editable in
Unix/Windows.
- hash: b3f25c176b1bdb487d1a7bf111d7f170fe008842
author: Lorenzo Alberto Maria Ambrosi
footers:
@@ -7358,7 +7727,8 @@
performance improvement
- Make Breadcrumbs and Icon pure components to stop frequent re-rendering
- Make Breadcrumbs and Icon pure components to stop frequent
re-rendering
- Initial support for array constraints
@@ -7489,9 +7859,11 @@
the `ETCHER_EXPERIMENTAL_FILE_PICKER` environment variable. Further
customisation can be done with the `ETCHER_FILE_BROWSER_CONSTRAIN_FOLDER`
customisation can be done with the
`ETCHER_FILE_BROWSER_CONSTRAIN_FOLDER`
variable that takes a path and allows one to constrain the file-picker to
variable that takes a path and allows one to constrain the file-picker
to
a folder.
- hash: 687e0b563b0dc3619ece4ce49d353d5838a21ff6
@@ -7585,15 +7957,18 @@
changelog-entry: Add support for configuration files
subject: "feat(gui): Add ability to read settings from a config file"
body: >-
This adds the capability to configure settings via a `.etcher.json` file,
This adds the capability to configure settings via a `.etcher.json`
file,
either in the user's home directory, or the current working directory.
In the case of the home directory, the config file is `$HOME/.config/etcher/config.json`,
In the case of the home directory, the config file is
`$HOME/.config/etcher/config.json`,
while on Windows `$HOME/.etcher.json` is used.
The defined settings are merged with localStorage settings, and preceding
The defined settings are merged with localStorage settings, and
preceding
configuration files.
@@ -7703,7 +8078,8 @@
change-type: patch
subject: "doc: Update MAINTAINERS.md with Symantec Whitelisting"
body: >-
This adds instructions for submitting Etcher for false positive detection
This adds instructions for submitting Etcher for false positive
detection
to Symantec Endpoint Protection.
- hash: bb2dac75040554c0ba2c7e50ff9ecd61608e7d38
@@ -7879,9 +8255,11 @@
change-type: patch
subject: "fix(image-writer): Remove use of _.isError"
body: >-
`_.isError()` returns `true` for anything that has a `name` and `message`
`_.isError()` returns `true` for anything that has a `name` and
`message`
property, causing the check here to always keep the plain object as error.
property, causing the check here to always keep the plain object as
error.
- hash: 355373f24df6be0989fad9429c2230166b33a3bf
author: Jonas Hermsmeier
footers:
@@ -7895,9 +8273,11 @@
change-type: patch
subject: "upgrade(package): Update drivelist 6.1.5 -> 6.1.7"
body: >-
This fixes a ReferenceError that could occur when the DeviceNode was null,
This fixes a ReferenceError that could occur when the DeviceNode was
null,
as well as devices being null when run after the system recovers from sleep / standby.
as well as devices being null when run after the system recovers from
sleep / standby.
- hash: 6e7484d3dabc2aeaa7cd471822d7019860cc4a5c
author: Benedict Aas
subject: "feat(GUI): display succeeded and failed devices on finish screen"
@@ -8058,7 +8438,8 @@
body: >-
This replaces shelling out to `diskpart` on Windows to clear
the partition table with `win-drive-clean`, which does so via DeviceIoControl.
the partition table with `win-drive-clean`, which does so via
DeviceIoControl.
- hash: abf2dc3efcf214a68c0b0e329d57a3f66bb5d342
author: Benedict Aas
footers:
@@ -8165,15 +8546,18 @@
This updates the instructions to open the Developer Tools in the issue
template,
as the keyboard shortcuts have changed to their defaults on Linux & Windows
as the keyboard shortcuts have changed to their defaults on Linux &
Windows
from [Ctrl]+[Alt]+[I] to [Ctrl]+[Shift]+[I].
Further, the editor config is updated to allow trailing spaces in Markdown
Further, the editor config is updated to allow trailing spaces in
Markdown
files to add trailing spaces to the list items in the issue template, in
order to avoid people not putting whitespace in between, causing the formatting
order to avoid people not putting whitespace in between, causing the
formatting
to not be parsed properly.
- hash: 3dd646485fa34437ac3adb3caa5a594d439f1f68
@@ -8257,7 +8641,8 @@
This replaces use of `electron.app.getName()` with the package.json's
`.displayName`
property to ensure the correct application name is displayed when packaged.
property to ensure the correct application name is displayed when
packaged.
- hash: cf340f48c3582f3e96f7b2dc16c11f44b7661363
author: Jonas Hermsmeier
footers:
@@ -8433,7 +8818,8 @@
body: >-
This updates `resin-cli-visuals` in order to fix drive selection in
the CLI, which was caused by incompatibility of two different `drivelist` versions
the CLI, which was caused by incompatibility of two different
`drivelist` versions
- hash: bde1e32e29ae75ccecf7fc3bc1b03efd6e4f67b8
author: Jonas Hermsmeier
footers:
@@ -8731,9 +9117,11 @@
changelog-entry: Remove stale `invalidKey` check in store.
subject: "refactor: remove stale invalid key check in store"
body: >-
We remove a piece of code checking whether `_.keys` returns any non-string
We remove a piece of code checking whether `_.keys` returns any
non-string
values in its array, but per the Lodash documentation `_.keys` always returns an
values in its array, but per the Lodash documentation `_.keys` always
returns an
array of strings.
- hash: 83528df18be32bfe62d3e9e4578101077769a7cf
@@ -8759,7 +9147,8 @@
changelog-entry: Make the drive-selector button orange on warnings.
subject: "feat(GUI): warning makes drive-selector button orange"
body: >-
We make the drive-selector button orange when there is a warning attached
We make the drive-selector button orange when there is a warning
attached
to the image-drive pair.
- hash: 4ce89f97fe02d714ce7f247a6a03ad6d326c3a8a
@@ -8988,7 +9377,8 @@
body: >-
Due to some Windows systems missing certain C runtime libraries
(Visual C/C++ 2012 / 2015 Redistributables), we ignore errors when loading
(Visual C/C++ 2012 / 2015 Redistributables), we ignore errors when
loading
this module until we can ensure distribution of those along with it.
- hash: 21e595466d5d950d7c38b2411791f756ec6ebdca
@@ -9073,7 +9463,8 @@
body: >-
This updates the `postshrinkwrap` script to traverse the dependency tree
and remove all `from` fields to avoid inconsistent diffs across platforms,
and remove all `from` fields to avoid inconsistent diffs across
platforms,
environments and installs when shrinkwrapping anew.
- hash: 619051a4b0cd8995e31838f221386b9b44e6ffc8
@@ -9485,7 +9876,8 @@
This works around the "Cannot fetch index base URL
http://pypi.python.org/simple/"
error by installing pip==9.0.1 directly from the pypi.python.org/packages/
error by installing pip==9.0.1 directly from the
pypi.python.org/packages/
- hash: c8b2b652354029cedceda2637bed13fee65f8587
author: Juan Cruz Viotti
footers:
@@ -9537,9 +9929,11 @@
WARNING: Binary file: lib/blobs/usbboot/raspberrypi/bootcode.bin
WARNING: Binary file: tests/image-stream/data/unrecognized/xz-without-extension
WARNING: Binary file:
tests/image-stream/data/unrecognized/xz-without-extension
WARNING: Binary file: tests/image-stream/data/unrecognized/xz-with-invalid-extension.foo
WARNING: Binary file:
tests/image-stream/data/unrecognized/xz-with-invalid-extension.foo
```
- hash: f4e0121639d8f2cbcc15b6577ec15d7ecbab7e71
@@ -11272,7 +11666,8 @@
https://developer.apple.com/library/mac/technotes/tn2206/_index.html
> Code signing uses extended attributes to store signatures in non-Mach-O
> Code signing uses extended attributes to store signatures in
non-Mach-O
> executables such as script files. If the extended attributes are lost
@@ -11284,7 +11679,8 @@
> One way to guarantee preservation of extended attributes is by packing
> up your signed code in a read-write disk image (DMG) file before signing
> up your signed code in a read-write disk image (DMG) file before
signing
> and then, after signing, converting to read-only. You probably don't
@@ -11354,23 +11750,32 @@
Unhandled rejection TypeError: Cannot read property '0' of undefined
at Number.indexedGetter (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/call_get.js:106:15)
at Number.indexedGetter
(/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 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._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._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._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 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._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 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 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
@@ -11386,7 +11791,8 @@
body: >-
From the documentation:
> `useContentSize` Boolean - The `width` and `height` would be used as web
> `useContentSize` Boolean - The `width` and `height` would be used as
web
> pages size, which means the actual windows size will include window
@@ -12203,7 +12609,8 @@
]);
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:
@@ -12455,7 +12862,8 @@
body: >-
Electron no longer supports 10.8.
See http://electron.atom.io/docs/v0.37.5/tutorial/supported-platforms/#os-x
See
http://electron.atom.io/docs/v0.37.5/tutorial/supported-platforms/#os-x
- hash: 097c9a4aa37029154c3efe8564edbeef048926ad
author: Juan Cruz Viotti
subject: Add subtle hover styling to footer links

View File

@@ -3,6 +3,99 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
# v1.19.8
## (2024-04-22)
* patch: replace deprecated pkg with yao-pkg and bump etcher-util node v to 20.10 [Edwin Joassart]
# v1.19.7
## (2024-04-22)
* patch: fix formating [Edwin Joassart]
* patch: configure prettier in the project to use balena-lint configuration [Edwin Joassart]
# v1.19.6
## (2024-04-19)
* patch: fix win signature process [Edwin Joassart]
# v1.19.5
## (2024-02-14)
* Replace deprecated flowzone input tests_run_on [Kyle Harding]
# v1.19.4
## (2024-01-26)
* patch: remove screensaver error when not on etcher-pro [Edwin Joassart]
* patch: fix typo in IPC server id [Edwin Joassart]
# v1.19.3
## (2023-12-22)
* Update dependencies [Edwin Joassart]
# v1.19.2
## (2023-12-22)
* fix: typos [Rotzbua]
# v1.19.1
## (2023-12-22)
* patch: update winget-releaser v2 [Vedant]
# v1.19.0
## (2023-12-21)
* Use native ARM runner for Apple Silicon builds [Akis Kesoglou]
* Calculate and upload build artifact sha256 checksums [Akis Kesoglou]
* Migrate build pipeline to Electron Forge [Akis Kesoglou]
# v1.18.14
## (2023-12-20)
* Remove repo config from flowzone.yml [Kyle Harding]
* Update actions/upload-artifact to v4 [Kyle Harding]
# v1.18.13
## (2023-10-16)
* patch: upgrade to electron 25 [Edwin Joassart]
* patch: refactor scanner, loader and flasher out of gui + upgrade to electron 25 [Edwin Joassart]
# v1.18.12
## (2023-07-19)
* Update instructions for installing deb file [Jorge Capona]
# v1.18.11
## (2023-07-13)
* fix: prevent stealing window focus from auth dialog [leadpogrommer]
# v1.18.10
## (2023-07-12)
* spelling: validates [Josh Soref]
* spelling: undefined [Josh Soref]
* spelling: except if [Josh Soref]
# v1.18.9
## (2023-07-12)
* Fix opening links from within SafeWebView [Akis Kesoglou]
# v1.18.8
## (2023-04-26)
* Patch: Fix Support link [Oliver Plummer]
# v1.18.7
## (2023-04-25)
* patch: update docs to remove cloudsmith install instructions for linux [Edwin Joassart]
# v1.18.6
## (2023-03-21)

152
Makefile
View File

@@ -1,152 +0,0 @@
# ---------------------------------------------------------------------
# Build configuration
# ---------------------------------------------------------------------
RESIN_SCRIPTS ?= ./scripts/resin
export NPM_VERSION ?= 6.14.8
S3_BUCKET = artifacts.ci.balena-cloud.com
# This directory will be completely deleted by the `clean` rule
BUILD_DIRECTORY ?= dist
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
$(BUILD_DIRECTORY):
mkdir $@
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
mkdir $@
SHELL := /bin/bash
# ---------------------------------------------------------------------
# Operating system and architecture detection
# ---------------------------------------------------------------------
# http://stackoverflow.com/a/12099167
ifeq ($(OS),Windows_NT)
PLATFORM = win32
ifeq ($(PROCESSOR_ARCHITEW6432),AMD64)
HOST_ARCH = x64
else
ifeq ($(PROCESSOR_ARCHITECTURE),AMD64)
HOST_ARCH = x64
endif
ifeq ($(PROCESSOR_ARCHITECTURE),x86)
HOST_ARCH = x86
endif
endif
else
ifeq ($(shell uname -s),Linux)
PLATFORM = linux
ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64
endif
ifneq ($(filter %86,$(shell uname -m)),)
HOST_ARCH = x86
endif
ifeq ($(shell uname -m),armv7l)
HOST_ARCH = armv7hf
endif
ifeq ($(shell uname -m),aarch64)
HOST_ARCH = aarch64
endif
ifeq ($(shell uname -m),armv8)
HOST_ARCH = aarch64
endif
ifeq ($(shell uname -m),arm64)
HOST_ARCH = aarch64
endif
endif
ifeq ($(shell uname -s),Darwin)
PLATFORM = darwin
ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64
endif
ifeq ($(shell uname -m),arm64)
HOST_ARCH = aarch64
endif
endif
endif
ifndef PLATFORM
$(error We could not detect your host platform)
endif
ifndef HOST_ARCH
$(error We could not detect your host architecture)
endif
# Default to host architecture. You can override by doing:
#
# make <target> TARGET_ARCH=<arch>
#
TARGET_ARCH ?= $(HOST_ARCH)
# ---------------------------------------------------------------------
# Electron
# ---------------------------------------------------------------------
electron-develop:
git submodule update --init && \
npm ci && \
npm run webpack
electron-test:
$(RESIN_SCRIPTS)/electron/test.sh \
-b $(shell pwd) \
-s $(PLATFORM)
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
tiffutil -cathidpicheck $^ -out $@
electron-build: assets/dmg/background.tiff | $(BUILD_TEMPORARY_DIRECTORY)
$(RESIN_SCRIPTS)/electron/build.sh \
-b $(shell pwd) \
-r $(TARGET_ARCH) \
-s $(PLATFORM) \
-v production \
-n $(BUILD_TEMPORARY_DIRECTORY)/npm
# ---------------------------------------------------------------------
# Phony targets
# ---------------------------------------------------------------------
TARGETS = \
help \
info \
lint \
test \
clean \
distclean \
electron-develop \
electron-test \
electron-build
.PHONY: $(TARGETS)
lint:
npm run lint
test:
npm run test
help:
@echo "Available targets: $(TARGETS)"
info:
@echo "Platform : $(PLATFORM)"
@echo "Host arch : $(HOST_ARCH)"
@echo "Target arch : $(TARGET_ARCH)"
clean:
rm -rf $(BUILD_DIRECTORY)
distclean: clean
rm -rf node_modules
rm -rf dist
rm -rf generated
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
.DEFAULT_GOAL = help

133
README.md
View File

@@ -17,13 +17,9 @@ was written correctly, and much more. It can also directly flash Raspberry Pi de
## Supported Operating Systems
- Linux (most distros)
- macOS 10.10 (Yosemite) and later
- Microsoft Windows 7 and later
**Note**: Etcher will run on any platform officially supported by
[Electron][electron]. Read more in their
[documentation][electron-supported-platforms].
- Linux; most distros; Intel 64-bit.
- Windows 10 and later; Intel 64-bit.
- macOS 10.13 (High Sierra) and later; both Intel and Apple Silicon.
## Installers
@@ -32,132 +28,32 @@ installers for all supported operating systems.
## Packages
> [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](https://cloudsmith.com) \
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
enables your organization to create, store and share packages in any format, to any place, with total
confidence.
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
Package for Debian and Ubuntu can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
1. Add Etcher Debian repository:
##### Install .deb file using apt
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
| sudo -E bash
sudo apt install ./balena-etcher_******_amd64.deb
```
2. Update and install:
```sh
sudo apt-get update #you can use apt instead of apt-get as well
sudo apt-get install balena-etcher-electron
```
>Note: after v1.7.9 the package name changed to `balena-etcher` (no electron at the end)
##### Uninstall
```sh
sudo apt-get remove balena-etcher-electron
rm /etc/apt/sources.list.d/balena-etcher.list
apt-get clean
rm -rf /var/lib/apt/lists/*
apt-get update
```
```sh
sudo apt remove balena-etcher
```
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
##### DNF
1. Add Etcher rpm repository:
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install:
```sh
sudo dnf install -y balena-etcher-electron
```
>Note: after v1.7.9 the package name changed to `balena-etcher` (no electron at the end)
###### Uninstall
```sh
rm /etc/yum.repos.d/balena-etcher.repo
rm /etc/yum.repos.d/balena-etcher-source.repo
```
##### Yum
1. Add Etcher rpm repository:
Package for Fedora-based and Redhat can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install:
```sh
sudo yum install -y balena-etcher-electron
```
>Note: after v1.7.9 the package name changed to `balena-etcher` (no electron at the end)
###### Uninstall
1. Install using yum
```sh
sudo yum remove -y balena-etcher-electron
rm /etc/yum.repos.d/balena-etcher.repo
rm /etc/yum.repos.d/balena-etcher-source.repo
```
#### OpenSUSE LEAP & Tumbleweed install (zypper)
1. Add the repo
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install
```sh
sudo zypper up
sudo zypper install balena-etcher-electron
```
>Note: after v1.7.9 the package name changed to `balena-etcher` (no electron at the end)
##### Uninstall
```sh
sudo zypper rm balena-etcher-electron
# remove the repo
sudo zypper rr balena-etcher
sudo zypper rr balena-etcher-source
```
#### Solus (GNU/Linux x64)
```sh
sudo eopkg it etcher
```
##### Uninstall
```sh
sudo eopkg rm etcher
sudo yum localinstall balena-etcher-***.x86_64.rpm
```
#### Arch/Manjaro Linux (GNU/Linux x64)
@@ -173,6 +69,7 @@ yay -S balena-etcher
```sh
yay -R balena-etcher
```
#### WinGet (Windows)
This package is updated by [gh-action](https://github.com/vedantmgoyal2009/winget-releaser), and is kept up to date automatically.
@@ -215,11 +112,9 @@ the [license].
[etcher]: https://balena.io/etcher
[electron]: https://electronjs.org/
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
[support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
[support]: https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[milestones]: https://github.com/balena-io/etcher/milestones
[newissue]: https://github.com/balena-io/etcher/issues/new
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE

View File

@@ -1,31 +0,0 @@
'use strict'
const cp = require('child_process')
const fs = require('fs')
const outdent = require('outdent')
const path = require('path')
exports.default = function(context) {
if (context.packager.platform.name !== 'linux') {
return
}
const scriptPath = path.join(context.appOutDir, context.packager.executableName)
const binPath = scriptPath + '.bin'
cp.execFileSync('mv', [scriptPath, binPath])
fs.writeFileSync(
scriptPath,
outdent({trimTrailingNewline: false})`
#!/bin/bash
# Resolve symlinks. Warning, readlink -f doesn't work on MacOS/BSD
script_dir="$(dirname "$(readlink -f "\${BASH_SOURCE[0]}")")"
if [[ $EUID -ne 0 ]] || [[ $ELECTRON_RUN_AS_NODE ]]; then
"\${script_dir}"/${context.packager.executableName}.bin "$@"
else
"\${script_dir}"/${context.packager.executableName}.bin "$@" --no-sandbox
fi
`
)
cp.execFileSync('chmod', ['+x', scriptPath])
}

View File

@@ -1,25 +0,0 @@
'use strict'
const { notarize } = require('electron-notarize')
const { ELECTRON_SKIP_NOTARIZATION } = process.env
async function main(context) {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin' || ELECTRON_SKIP_NOTARIZATION === 'true') {
return
}
const appName = context.packager.appInfo.productFilename
const appleId = process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io'
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD
// https://github.com/electron/notarize/blob/main/README.md
await notarize({
appBundleId: 'io.balena.etcher',
appPath: `${appOutDir}/${appName}.app`,
appleId,
appleIdPassword
})
}
exports.default = main

View File

@@ -1,4 +0,0 @@
owner: balena-io
repo: etcher
provider: github
updaterCacheDirName: balena-etcher-updater

View File

@@ -1,9 +0,0 @@
boolen->boolean
aknowledge->acknowledge
seleted->selected
reming->remind
locl->local
subsribe->subscribe
unsubsribe->unsubscribe
calcluate->calculate
dictionaty->dictionary

View File

@@ -75,9 +75,7 @@ cd etcher
#### GUI
```sh
# Build the GUI
npm run webpack #or npm run build
# Start Electron
# Build and start application
npm start
```
@@ -104,7 +102,6 @@ systems as they can before sending a pull request.
*The test suite is run automatically by CI servers when you send a pull
request.*
We make use of [EditorConfig] to communicate indentation, line endings and
other text editing default. We encourage you to install the relevant plugin in
your text editor of choice to avoid having to fix any issues during the review
@@ -113,7 +110,8 @@ process.
Updating a dependency
---------------------
- Commit *both* `package.json` and `package-lock.json`.
- Install new version of dependency using npm
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
Diffing Binaries
----------------

View File

@@ -58,30 +58,23 @@ export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
##### Clean dist folder
**NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
Delete `.webpack` and `out/`.
##### Generating artifacts
The artifacts are generated by the CI and published as draft-release or pre-release.
`electron-builder` is used to create the packaged application.
Etcher is built with electron-forge. Run:
#### Mac OS
```
npm run make
```
**ATTENTION:** For production releases you'll need the code-signing key,
and set `CSC_NAME` to generate signed binaries on Mac OS.
#### Windows
**ATTENTION:** For production releases you'll need the code-signing key,
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
**NOTE:**
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
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.
Log in to cloudfront and upload the `rpm` and `deb` files.
### Dealing with a Problematic Release

View File

@@ -36,14 +36,17 @@ employee by asking for it from the relevant people.
Packaging
---------
The resulting installers will be saved to `dist/out`.
Run the following commands on all platforms with the right arguments:
Run the following command on each platform:
```sh
./node_modules/electron-builder build <...>
npm run make
```
This will produce all targets (eg. zip, dmg) specified in forge.config.ts for the
host platform and architecture.
The resulting artifacts can be found in `out/make`.
Publishing to Cloudfront
---------------------

View File

@@ -1,110 +0,0 @@
# https://www.electron.build/configuration/configuration
appId: io.balena.etcher
copyright: Copyright 2016-2023 Balena Ltd
productName: balenaEtcher
afterPack: ./afterPack.js
afterSign: ./afterSignHook.js
asar: false
files:
- generated
- lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
- lib/shared/catalina-sudo/sudo-askpass.osascript-en.js
mac:
icon: assets/icon.icns
category: public.app-category.developer-tools
hardenedRuntime: true
entitlements: "entitlements.mac.plist"
entitlementsInherit: "entitlements.mac.plist"
artifactName: "${productName}-${version}.${ext}"
target:
- dmg
dmg:
background: assets/dmg/background.tiff
icon: assets/icon.icns
iconSize: 110
contents:
- x: 140
y: 225
- x: 415
y: 225
type: link
path: /Applications
window:
width: 540
height: 405
win:
icon: assets/icon.ico
target:
- zip
- nsis
- portable
nsis:
oneClick: true
runAfterFinish: true
installerIcon: assets/icon.ico
uninstallerIcon: assets/icon.ico
deleteAppDataOnUninstall: true
license: LICENSE
artifactName: "${productName}-Setup-${version}.${ext}"
portable:
artifactName: "${productName}-Portable-${version}.${ext}"
requestExecutionLevel: user
linux:
icon: assets/iconset
target:
- AppImage
- rpm
- deb
category: Utility
packageCategory: utils
executableName: balena-etcher
synopsis: balenaEtcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.
appImage:
artifactName: ${productName}-${version}-${env.ELECTRON_BUILDER_ARCHITECTURE}.${ext}
deb:
priority: optional
compression: bzip2
depends:
- gconf-service
- gconf2
- libasound2
- libatk1.0-0
- libc6
- libcairo2
- libcups2
- libdbus-1-3
- libexpat1
- libfontconfig1
- libfreetype6
- libgbm1
- libgcc1
- libgconf-2-4
- libgdk-pixbuf2.0-0
- libglib2.0-0
- libgtk-3-0
- liblzma5
- libnotify4
- libnspr4
- libnss3
- libpango1.0-0 | libpango-1.0-0
- libstdc++6
- libx11-6
- libxcomposite1
- libxcursor1
- libxdamage1
- libxext6
- libxfixes3
- libxi6
- libxrandr2
- libxrender1
- libxss1
- libxtst6
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
afterInstall: "./after-install.tpl"
rpm:
depends:
- util-linux
protocols:
name: etcher
schemes:
- etcher

View File

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

157
forge.config.ts Normal file
View File

@@ -0,0 +1,157 @@
import type { ForgeConfig } from '@electron-forge/shared-types';
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
import { MakerDMG } from '@electron-forge/maker-dmg';
import { MakerAppImage } from '@reforged/maker-appimage';
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
import { mainConfig, rendererConfig } from './webpack.config';
import * as sidecar from './forge.sidecar';
import { hostDependencies, productDescription } from './package.json';
const osxSigningConfig: any = {};
let winSigningConfig: any = {};
if (process.env.NODE_ENV === 'production') {
osxSigningConfig.osxNotarize = {
tool: 'notarytool',
appleId: process.env.XCODE_APP_LOADER_EMAIL,
appleIdPassword: process.env.XCODE_APP_LOADER_PASSWORD,
teamId: process.env.XCODE_APP_LOADER_TEAM_ID,
};
winSigningConfig = {
signWithParams: `-sha1 ${process.env.SM_CODE_SIGNING_CERT_SHA1_HASH} -tr ${process.env.TIMESTAMP_SERVER} -td sha256 -fd sha256 -d balena-etcher`,
};
}
const config: ForgeConfig = {
packagerConfig: {
asar: true,
icon: './assets/icon',
executableName:
process.platform === 'linux' ? 'balena-etcher' : 'balenaEtcher',
appBundleId: 'io.balena.etcher',
appCategoryType: 'public.app-category.developer-tools',
appCopyright: 'Copyright 2016-2023 Balena Ltd',
darwinDarkModeSupport: true,
protocols: [{ name: 'etcher', schemes: ['etcher'] }],
extraResource: [
'lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js',
'lib/shared/catalina-sudo/sudo-askpass.osascript-en.js',
],
osxSign: {
optionsForFile: () => ({
entitlements: './entitlements.mac.plist',
hardenedRuntime: true,
}),
},
...osxSigningConfig,
},
rebuildConfig: {},
makers: [
new MakerZIP(),
new MakerSquirrel({
setupIcon: 'assets/icon.ico',
...winSigningConfig,
}),
new MakerDMG({
background: './assets/dmg/background.tiff',
icon: './assets/icon.icns',
iconSize: 110,
contents: ((opts: { appPath: string }) => {
return [
{ x: 140, y: 250, type: 'file', path: opts.appPath },
{ x: 415, y: 250, type: 'link', path: '/Applications' },
];
}) as any, // type of MakerDMGConfig omits `appPath`
additionalDMGOptions: {
window: {
size: {
width: 540,
height: 425,
},
position: {
x: 400,
y: 500,
},
},
},
}),
new MakerAppImage({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
},
}),
new MakerRpm({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
productDescription,
requires: ['util-linux'],
},
}),
new MakerDeb({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
section: 'utils',
priority: 'optional',
productDescription,
scripts: {
postinst: './after-install.tpl',
},
depends: hostDependencies['debian'],
},
}),
],
plugins: [
new AutoUnpackNativesPlugin({}),
new WebpackPlugin({
mainConfig,
renderer: {
config: rendererConfig,
nodeIntegration: true,
entryPoints: [
{
html: './lib/gui/app/index.html',
js: './lib/gui/app/renderer.ts',
name: 'main_window',
preload: {
js: './lib/gui/app/preload.ts',
},
},
],
},
}),
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;
},
},
};
export default config;

168
forge.sidecar.ts Normal file
View File

@@ -0,0 +1,168 @@
import { PluginBase } from '@electron-forge/plugin-base';
import {
ForgeHookMap,
ResolvedForgeConfig,
} from '@electron-forge/shared-types';
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
import { DefinePlugin } from 'webpack';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as d from 'debug';
const debug = d('sidecar');
function isStartScrpt(): boolean {
return process.env.npm_lifecycle_event === 'start';
}
function addWebpackDefine(
config: ResolvedForgeConfig,
defineName: string,
binDir: string,
binName: string,
): ResolvedForgeConfig {
config.plugins.forEach((plugin) => {
if (plugin.name !== 'webpack' || !(plugin instanceof WebpackPlugin)) {
return;
}
const { mainConfig } = plugin.config as any;
if (mainConfig.plugins == null) {
mainConfig.plugins = [];
}
const value = isStartScrpt()
? // on `npm start`, point directly to the binary
path.resolve(binDir, binName)
: // otherwise point relative to the resources folder of the bundled app
binName;
debug(`define '${defineName}'='${value}'`);
mainConfig.plugins.push(
new DefinePlugin({
// expose path to helper via this webpack define
[defineName]: JSON.stringify(value),
}),
);
});
return config;
}
function build(
sourcesDir: string,
buildForArchs: string,
binDir: string,
binName: string,
) {
const commands: Array<[string, string[], object?]> = [
['tsc', ['--project', 'tsconfig.sidecar.json', '--outDir', sourcesDir]],
];
buildForArchs.split(',').forEach((arch) => {
const binPath = isStartScrpt()
? // on `npm start`, we don't know the arch we're building for at the time we're
// adding the webpack define, so we just build under binDir
path.resolve(binDir, binName)
: // otherwise build in arch-specific directory within binDir
path.resolve(binDir, arch, binName);
// FIXME: rebuilding mountutils shouldn't be necessary, but it is.
// It's coming from etcher-sdk, a fix has been upstreamed but to use
// the latest etcher-sdk we need to upgrade axios at the same time.
commands.push(['npm', ['rebuild', 'mountutils', `--arch=${arch}`]]);
commands.push([
'pkg',
[
path.join(sourcesDir, 'util', 'api.js'),
'-c',
'pkg-sidecar.json',
// `--no-bytecode` so that we can cross-compile for arm64 on x64
'--no-bytecode',
'--public',
'--public-packages',
'"*"',
// always build for host platform and node version
// https://github.com/vercel/pkg-fetch/releases
'--target',
`node20-${arch}`,
'--output',
binPath,
],
]);
});
commands.forEach(([cmd, args, opt]) => {
debug('running command:', cmd, args.join(' '));
execFileSync(cmd, args, { shell: true, stdio: 'inherit', ...opt });
});
}
function copyArtifact(
buildPath: string,
arch: string,
binDir: string,
binName: string,
) {
const binPath = isStartScrpt()
? // on `npm start`, we don't know the arch we're building for at the time we're
// adding the webpack define, so look for the binary directly under binDir
path.resolve(binDir, binName)
: // otherwise look into arch-specific directory within binDir
path.resolve(binDir, arch, binName);
// buildPath points to appPath, which is inside resources dir which is the one we actually want
const resourcesPath = path.dirname(buildPath);
const dest = path.resolve(resourcesPath, path.basename(binPath));
debug(`copying '${binPath}' to '${dest}'`);
fs.copyFileSync(binPath, dest);
}
export class SidecarPlugin extends PluginBase<void> {
name = 'sidecar';
constructor() {
super();
this.getHooks = this.getHooks.bind(this);
debug('isStartScript:', isStartScrpt());
}
getHooks(): ForgeHookMap {
const DEFINE_NAME = 'ETCHER_UTIL_BIN_PATH';
const BASE_DIR = path.join('out', 'sidecar');
const SRC_DIR = path.join(BASE_DIR, 'src');
const BIN_DIR = path.join(BASE_DIR, 'bin');
const BIN_NAME = `etcher-util${process.platform === 'win32' ? '.exe' : ''}`;
return {
resolveForgeConfig: async (currentConfig) => {
debug('resolveForgeConfig');
return addWebpackDefine(currentConfig, DEFINE_NAME, BIN_DIR, BIN_NAME);
},
generateAssets: async (_config, platform, arch) => {
debug('generateAssets', { platform, arch });
build(SRC_DIR, arch, BIN_DIR, BIN_NAME);
},
packageAfterCopy: async (
_config,
buildPath,
electronVersion,
platform,
arch,
) => {
debug('packageAfterCopy', {
buildPath,
electronVersion,
platform,
arch,
});
copyArtifact(buildPath, arch, BIN_DIR, BIN_NAME);
},
};
}
}

View File

@@ -16,30 +16,29 @@
import * as electron from 'electron';
import * as remote from '@electron/remote';
import * as sdk from 'etcher-sdk';
import * as _ 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 { DrivelistDrive, isSourceDrive } from '../../shared/drive-constraints';
import { DrivelistDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state';
import { deselectImage, getImage } from './models/selection-state';
import * as settings from './models/settings';
import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics';
import { scanner as driveScanner } from './modules/drive-scanner';
import { startApiAndSpawnChild } from './modules/api';
import * as exceptionReporter from './modules/exception-reporter';
import * as osDialog from './os/dialog';
import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage';
import './css/main.css';
import * as i18next from 'i18next';
import { SourceMetadata } from '../../shared/typings/source-selector';
window.addEventListener(
'unhandledrejection',
@@ -89,7 +88,7 @@ analytics.logEvent('Application start', {
version: currentVersion,
});
const debouncedLog = _.debounce(console.log, 1000, { maxWait: 1000 });
const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 });
function pluralize(word: string, quantity: number) {
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
@@ -115,7 +114,7 @@ observe(() => {
// might cause some non-sense flashing state logs including
// `undefined` values.
debouncedLog(outdent({ newline: ' ' })`
${_.capitalize(currentFlashState.type)}
${capitalize(currentFlashState.type)}
${active},
${currentFlashState.percentage}%
at
@@ -128,173 +127,40 @@ observe(() => {
`);
});
/**
* @summary The radix used by USB ID numbers
*/
const USB_ID_RADIX = 16;
/**
* @summary The expected length of a USB ID number
*/
const USB_ID_LENGTH = 4;
/**
* @summary Convert a USB id (e.g. product/vendor) to a string
*
* @example
* console.log(usbIdToString(2652))
* > '0x0a5c'
*/
function usbIdToString(id: number): string {
return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`;
}
/**
* @summary Product ID of BCM2708
*/
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763;
/**
* @summary Product ID of BCM2710
*/
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764;
/**
* @summary Compute module descriptions
*/
const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary<string> = {
[USB_PRODUCT_ID_BCM2708_BOOT]: 'Compute Module 1',
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3',
};
async function driveIsAllowed(drive: {
devicePath: string;
device: string;
raw: string;
}) {
const driveBlacklist = (await settings.get('driveBlacklist')) || [];
return !(
driveBlacklist.includes(drive.devicePath) ||
driveBlacklist.includes(drive.device) ||
driveBlacklist.includes(drive.raw)
);
}
type Drive =
| sdk.sourceDestination.BlockDevice
| sdk.sourceDestination.UsbbootDrive
| sdk.sourceDestination.DriverlessDevice;
function prepareDrive(drive: Drive) {
if (drive instanceof sdk.sourceDestination.BlockDevice) {
// @ts-ignore (BlockDevice.drive is private)
return drive.drive;
} else if (drive instanceof sdk.sourceDestination.UsbbootDrive) {
// This is a workaround etcher expecting a device string and a size
// @ts-ignore
drive.device = drive.usbDevice.portId;
drive.size = null;
// @ts-ignore
drive.progress = 0;
drive.disabled = true;
drive.on('progress', (progress) => {
updateDriveProgress(drive, progress);
});
return drive;
} else if (drive instanceof sdk.sourceDestination.DriverlessDevice) {
const description =
COMPUTE_MODULE_DESCRIPTIONS[
drive.deviceDescriptor.idProduct.toString()
] || 'Compute Module';
return {
device: `${usbIdToString(
drive.deviceDescriptor.idVendor,
)}:${usbIdToString(drive.deviceDescriptor.idProduct)}`,
displayName: 'Missing drivers',
description,
mountpoints: [],
isReadOnly: false,
isSystem: false,
disabled: true,
icon: 'warning',
size: null,
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
linkCTA: 'Install',
linkTitle: 'Install missing drivers',
linkMessage: outdent`
Would you like to download the necessary drivers from the Raspberry Pi Foundation?
This will open your browser.
Once opened, download and run the installer from the "Windows Installer" section to install the drivers
`,
};
function setDrives(drives: Dictionary<DrivelistDrive>) {
// prevent setting drives while flashing otherwise we might lose some while we unmount them
if (!flashState.isFlashing()) {
availableDrives.setDrives(values(drives));
}
}
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
availableDrives.setDrives(_.values(drives));
}
// Spawning the child process without privileges to get the drives list
// TODO: clean up this mess of exports
export let requestMetadata: any;
function getDrives() {
return _.keyBy(availableDrives.getDrives(), 'device');
}
// start the api and spawn the child process
startApiAndSpawnChild({
withPrivileges: false,
}).then(({ emit, registerHandler }) => {
// start scanning
emit('scan');
async function addDrive(drive: Drive) {
const preparedDrive = prepareDrive(drive);
if (!(await driveIsAllowed(preparedDrive))) {
return;
}
const drives = getDrives();
drives[preparedDrive.device] = preparedDrive;
setDrives(drives);
}
// make the sourceMetada awaitable to be used on source selection
requestMetadata = async (params: any): Promise<SourceMetadata> => {
emit('sourceMetadata', JSON.stringify(params));
function removeDrive(drive: Drive) {
if (
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isSourceDrive(drive.drive, getImage())
) {
// Deselect the image if it was on the drive that was removed.
// This will also deselect the image if the drive mountpoints change.
deselectImage();
}
const preparedDrive = prepareDrive(drive);
const drives = getDrives();
delete drives[preparedDrive.device];
setDrives(drives);
}
return new Promise((resolve) =>
registerHandler('sourceMetadata', (data: any) => {
resolve(JSON.parse(data));
}),
);
};
function updateDriveProgress(
drive: sdk.sourceDestination.UsbbootDrive,
progress: number,
) {
const drives = getDrives();
// @ts-ignore
const driveInMap = drives[drive.device];
if (driveInMap) {
// @ts-ignore
drives[drive.device] = { ...driveInMap, progress };
setDrives(drives);
}
}
driveScanner.on('attach', addDrive);
driveScanner.on('detach', removeDrive);
driveScanner.on('error', (error) => {
// Stop the drive scanning loop in case of errors,
// otherwise we risk presenting the same error over
// and over again to the user, while also heavily
// spamming our error reporting service.
driveScanner.stop();
return exceptionReporter.report(error);
registerHandler('drives', (data: any) => {
setDrives(JSON.parse(data));
});
});
driveScanner.start();
let popupExists = false;
analytics.initAnalytics();

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
import * as React from 'react';
@@ -42,7 +42,7 @@ import {
Table,
} from '../../styled-components';
import { SourceMetadata } from '../source-selector/source-selector';
import { SourceMetadata } from '../../../../shared/typings/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next';
@@ -310,9 +310,17 @@ export class DriveSelector extends React.Component<
case compatibility.system():
return warning.systemDrive();
case compatibility.tooSmall():
const size =
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
return warning.tooSmall({ size }, drive);
return warning.tooSmall(
{
size:
this.state.image?.recommendedDriveSize ||
this.state.image?.size ||
0,
},
drive,
);
default:
return '';
}
}
@@ -428,11 +436,10 @@ export class DriveSelector extends React.Component<
) : (
<>
<DrivesTable
refFn={(t) => {
if (t !== null) {
t.setRowSelection(selectedList);
}
refFn={() => {
// noop
}}
checkedItems={selectedList}
checkedRowsNumber={selectedList.length}
multipleSelection={this.props.multipleSelection}
columns={this.tableColumns}
@@ -442,7 +449,10 @@ export class DriveSelector extends React.Component<
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
}
rowKey="displayName"
onCheck={(rows: Drive[]) => {
onCheck={(rows) => {
if (rows == null) {
rows = [];
}
let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) {
if (rows.length === 0) {

View File

@@ -1,11 +1,10 @@
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import * as _ from 'lodash';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import * as React from 'react';
import { Badge, Flex, Txt, ModalProps } from 'rendition';
import { Modal, ScrollableFlex } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as prettyBytes from 'pretty-bytes';
import prettyBytes from 'pretty-bytes';
import { DriveWithWarnings } from '../../pages/main/Flash';
import * as i18next from 'i18next';

View File

@@ -15,9 +15,8 @@
*/
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
import outdent from 'outdent';
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 { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
import styled from 'styled-components';
@@ -139,8 +138,9 @@ export function FlashResults({
};
} & FlexProps) {
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = !skip && results.devices.successful === 0;
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const allFailed = !skip && results?.devices?.successful === 0;
const someFailed = results?.devices?.failed !== 0 || errors?.length !== 0;
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
1,
);

View File

@@ -95,6 +95,7 @@ export class SafeWebview extends React.PureComponent<
);
this.entryHref = url.href;
// Events steal 'this'
this.handleDomReady = _.bind(this.handleDomReady, this);
this.didFailLoad = _.bind(this.didFailLoad, this);
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
// Make a persistent electron session for the webview
@@ -121,6 +122,8 @@ export class SafeWebview extends React.PureComponent<
ref={this.webviewRef}
partition={ELECTRON_SESSION}
style={style}
// @ts-ignore
allowpopups="true"
/>
);
}
@@ -134,8 +137,8 @@ export class SafeWebview extends React.PureComponent<
this.didFailLoad,
);
this.webviewRef.current.addEventListener(
'new-window',
SafeWebview.newWindow,
'dom-ready',
this.handleDomReady,
);
this.webviewRef.current.addEventListener(
'console-message',
@@ -157,8 +160,8 @@ export class SafeWebview extends React.PureComponent<
this.didFailLoad,
);
this.webviewRef.current.removeEventListener(
'new-window',
SafeWebview.newWindow,
'dom-ready',
this.handleDomReady,
);
this.webviewRef.current.removeEventListener(
'console-message',
@@ -168,6 +171,15 @@ export class SafeWebview extends React.PureComponent<
this.session.webRequest.onCompleted(null);
}
handleDomReady() {
const webview = this.webviewRef.current;
if (webview == null) {
return;
}
const id = webview.getWebContentsId();
electron.ipcRenderer.send('webview-dom-ready', id);
}
// Set the element state to hidden
public didFailLoad() {
this.setState({
@@ -196,17 +208,4 @@ export class SafeWebview extends React.PureComponent<
}
}
}
// Open link in browser if it's opened as a 'foreground-tab'
public static async newWindow(event: electron.NewWindowEvent) {
const url = new window.URL(event.url);
if (
(url.protocol === 'http:' || url.protocol === 'https:') &&
event.disposition === 'foreground-tab' &&
// Don't open links if they're disabled by the env var
!(await settings.get('disableExternalLinks'))
) {
electron.shell.openExternal(url.href);
}
}
}

View File

@@ -17,7 +17,7 @@
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import * as _ from 'lodash';
import * as React from 'react';
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
import { Box, Checkbox, Flex, Txt } from 'rendition';
import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings';
@@ -61,7 +61,9 @@ const EPInfo = etcherProInfo();
const InfoBox = (props: any) => (
<Box fontSize={14}>
<Txt>{props.label}</Txt>
<TextWithCopy code text={props.value} copy={props.value} />
<Txt code copy={props.value}>
{props.value}{' '}
</Txt>
</Box>
);

View File

@@ -17,16 +17,16 @@
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import 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 { sourceDestination } from 'etcher-sdk';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as _ from 'lodash';
import { GPTPartition, MBRPartition } from 'partitioninfo';
import { uniqBy, isNil } from 'lodash';
import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react';
import { requestMetadata } from '../../app';
import {
Flex,
ButtonProps,
@@ -47,7 +47,7 @@ import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import * as exceptionReporter from '../../modules/exception-reporter';
import * as osDialog from '../../os/dialog';
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';
import {
ChangeButton,
DetailsText,
@@ -64,8 +64,12 @@ import ImageSvg from '../../../assets/image.svg';
import SrcSvg from '../../../assets/src.svg';
import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
import axios, { AxiosRequestConfig } from 'axios';
import { isJson } from '../../../../shared/utils';
import {
SourceMetadata,
Authentication,
Source,
} from '../../../../shared/typings/source-selector';
import * as i18next from 'i18next';
const recentUrlImagesKey = 'recentUrlImages';
@@ -83,7 +87,7 @@ function normalizeRecentUrlImages(urls: any[]): URL[] {
}
})
.filter((url) => url !== undefined);
urls = _.uniqBy(urls, (url) => url.href);
urls = uniqBy(urls, (url) => url.href);
return urls.slice(urls.length - 5);
}
@@ -301,24 +305,6 @@ const FlowSelector = styled(
}
`;
export type Source =
| typeof sourceDestination.File
| typeof sourceDestination.BlockDevice
| typeof sourceDestination.Http;
export interface SourceMetadata extends sourceDestination.Metadata {
hasMBR?: boolean;
partitions?: MBRPartition[] | GPTPartition[];
path: string;
displayName: string;
description: string;
SourceType: Source;
drive?: DrivelistDrive;
extension?: string;
archiveExtension?: string;
auth?: Authentication;
}
interface SourceSelectorProps {
flashing: boolean;
}
@@ -336,11 +322,6 @@ interface SourceSelectorState {
imageLoading: boolean;
}
interface Authentication {
username: string;
password: string;
}
export class SourceSelector extends React.Component<
SourceSelectorProps,
SourceSelectorState
@@ -381,43 +362,11 @@ export class SourceSelector extends React.Component<
this.setState({ imageLoading: true });
await this.selectSource(
imagePath,
isURL(this.normalizeImagePath(imagePath))
? sourceDestination.Http
: sourceDestination.File,
isURL(this.normalizeImagePath(imagePath)) ? 'Http' : 'File',
).promise;
this.setState({ imageLoading: false });
}
private async createSource(
selected: string,
SourceType: Source,
auth?: Authentication,
) {
try {
selected = await replaceWindowsNetworkDriveLetter(selected);
} catch (error: any) {
analytics.logException(error);
}
if (isJson(decodeURIComponent(selected))) {
const config: AxiosRequestConfig = JSON.parse(
decodeURIComponent(selected),
);
return new sourceDestination.Http({
url: config.url!,
axiosInstance: axios.create(_.omit(config, ['url'])),
});
}
if (SourceType === sourceDestination.File) {
return new sourceDestination.File({
path: selected,
});
}
return new sourceDestination.Http({ url: selected, auth });
}
public normalizeImagePath(imgPath: string) {
const decodedPath = decodeURIComponent(imgPath);
if (isJson(decodedPath)) {
@@ -439,18 +388,16 @@ export class SourceSelector extends React.Component<
SourceType: Source,
auth?: Authentication,
): { promise: Promise<void>; cancel: () => void } {
let cancelled = false;
return {
cancel: () => {
cancelled = true;
// noop
},
promise: (async () => {
const sourcePath = isString(selected) ? selected : selected.device;
let source;
let metadata: SourceMetadata | undefined;
if (isString(selected)) {
if (
SourceType === sourceDestination.Http &&
SourceType === 'Http' &&
!isURL(this.normalizeImagePath(selected))
) {
this.handleError(
@@ -470,24 +417,14 @@ export class SourceSelector extends React.Component<
},
});
}
source = await this.createSource(selected, SourceType, auth);
if (cancelled) {
return;
}
try {
const innerSource = await source.getInnerSource();
if (cancelled) {
return;
}
metadata = await this.getMetadata(innerSource, selected);
if (cancelled) {
return;
}
metadata.SourceType = SourceType;
// this will send an event down the ipcMain asking for metadata
// we'll get the response through an event
if (!metadata.hasMBR && this.state.warning === null) {
metadata = await requestMetadata({ selected, SourceType, auth });
if (!metadata?.hasMBR && this.state.warning === null) {
analytics.logEvent('Missing partition table', { metadata });
this.setState({
warning: {
@@ -503,12 +440,6 @@ export class SourceSelector extends React.Component<
messages.error.openSource(sourcePath, error.message),
error,
);
} finally {
try {
await source.close();
} catch (error: any) {
// Noop
}
}
} else {
if (selected.partitionTableType === null) {
@@ -525,13 +456,14 @@ export class SourceSelector extends React.Component<
displayName: selected.displayName,
description: selected.displayName,
size: selected.size as SourceMetadata['size'],
SourceType: sourceDestination.BlockDevice,
SourceType: 'BlockDevice',
drive: selected,
};
}
if (metadata !== undefined) {
metadata.auth = auth;
metadata.SourceType = SourceType;
selectionState.selectSource(metadata);
analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of
@@ -565,25 +497,6 @@ export class SourceSelector extends React.Component<
analytics.logEvent(title, { path: sourcePath });
}
private async getMetadata(
source: sourceDestination.SourceDestination,
selected: string | DrivelistDrive,
) {
const metadata = (await source.getMetadata()) as SourceMetadata;
const partitionTable = await source.getPartitionTable();
if (partitionTable) {
metadata.hasMBR = true;
metadata.partitions = partitionTable.partitions;
} else {
metadata.hasMBR = false;
}
if (isString(selected)) {
metadata.extension = path.extname(selected).slice(1);
metadata.path = selected;
}
return metadata;
}
private async openImageSelector() {
analytics.logEvent('Open image selector');
this.setState({ imageSelectorOpen: true });
@@ -596,7 +509,7 @@ export class SourceSelector extends React.Component<
analytics.logEvent('Image selector closed');
return;
}
await this.selectSource(imagePath, sourceDestination.File).promise;
await this.selectSource(imagePath, 'File').promise;
} catch (error: any) {
exceptionReporter.report(error);
} finally {
@@ -605,9 +518,9 @@ export class SourceSelector extends React.Component<
}
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
const [file] = event.dataTransfer.files;
if (file) {
await this.selectSource(file.path, sourceDestination.File).promise;
const file = event.dataTransfer.files.item(0);
if (file != null) {
await this.selectSource(file.path, 'File').promise;
}
}
@@ -667,7 +580,7 @@ export class SourceSelector extends React.Component<
imageLoading,
} = this.state;
const selectionImage = selectionState.getImage();
let image: SourceMetadata | DrivelistDrive =
let image =
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
image = image.drive ?? image;
@@ -723,7 +636,7 @@ export class SourceSelector extends React.Component<
{i18next.t('cancel')}
</ChangeButton>
)}
{!_.isNil(imageSize) && !imageLoading && (
{!isNil(imageSize) && !imageLoading && (
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
)}
</>
@@ -770,7 +683,7 @@ export class SourceSelector extends React.Component<
style={{
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
}}
titleElement={
title={
<span>
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
<span>{this.state.warning.title}</span>
@@ -827,7 +740,7 @@ export class SourceSelector extends React.Component<
let promise;
({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL,
sourceDestination.Http,
'Http',
auth,
));
await promise;
@@ -850,10 +763,7 @@ export class SourceSelector extends React.Component<
if (originalList.length) {
const originalSource = originalList[0];
if (selectionImage?.drive?.device !== originalSource.device) {
this.selectSource(
originalSource,
sourceDestination.BlockDevice,
);
this.selectSource(originalSource, 'BlockDevice');
}
} else {
selectionState.deselectImage();
@@ -868,7 +778,7 @@ export class SourceSelector extends React.Component<
) {
return selectionState.deselectImage();
}
this.selectSource(drive, sourceDestination.BlockDevice);
this.selectSource(drive, 'BlockDevice');
}
}}
/>

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import * as React from 'react';
import { Flex, FlexProps, Txt } from 'rendition';

View File

@@ -15,36 +15,36 @@
*/
@font-face {
font-family: "SourceSansPro";
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-family: 'SourceSansPro';
src: url('./fonts/SourceSansPro-Regular.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: "SourceSansPro";
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-family: 'SourceSansPro';
src: url('./fonts/SourceSansPro-SemiBold.ttf') format('truetype');
font-weight: 600;
font-style: normal;
}
html,
body {
margin: 0;
overflow: hidden;
margin: 0;
overflow: hidden;
/* Prevent white flash when running application */
background-color: #4d5057;
/* Prevent white flash when running application */
background-color: #4d5057;
/* Prevent WebView bounce effect in OS X */
height: 100%;
width: 100%;
/* Prevent WebView bounce effect in OS X */
height: 100%;
width: 100%;
}
/* Prevent text selection */
body {
-webkit-user-select: none;
-webkit-overflow-scrolling: touch;
-webkit-user-select: none;
-webkit-overflow-scrolling: touch;
}
/* Prevent blue outline */
@@ -52,15 +52,15 @@ a:focus,
input:focus,
button:focus,
[tabindex]:focus,
input[type="checkbox"] + div {
outline: none !important;
box-shadow: none !important;
input[type='checkbox'] + div {
outline: none !important;
box-shadow: none !important;
}
.disabled {
opacity: 0.4;
opacity: 0.4;
}
#rendition-tooltip-root > div {
font-family: "SourceSansPro", sans-serif;
font-family: 'SourceSansPro', sans-serif;
}

View File

@@ -46,7 +46,8 @@ const translation = {
drive: '磁碟',
missingPartitionTable:
'看起來這不是一個可啟動的{{type}}。\n\n這個{{type}}似乎不包含分割表,因此您的設備可能無法識別或無法正確啟動。',
largeDriveSize: '這是個很大容量的磁碟!請檢查並確認它不包含對您來說存放很重要的資料',
largeDriveSize:
'這是個很大容量的磁碟!請檢查並確認它不包含對您來說存放很重要的資料',
systemDrive: '選擇系統分割區很危險,因為這將會刪除你的系統',
sourceDrive: '來源映像檔位於這個分割區中',
noSpace: '磁碟空間不足。請插入另一個較大的磁碟並重試。',
@@ -130,8 +131,7 @@ const translation = {
autoUpdate: '自動更新',
settings: '軟體設定',
systemInformation: '系統資訊',
trimExtPartitions:
'修改原始映像檔上未分配的空間(在 ext 類型分割區中)',
trimExtPartitions: '修改原始映像檔上未分配的空間(在 ext 類型分割區中)',
},
menu: {
edit: '編輯',

View File

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

View File

@@ -3,10 +3,8 @@
<head>
<meta charset="UTF-8">
<title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<main id="main"></main>
<script src="gui.js"></script>
</body>
</html>

View File

@@ -47,13 +47,7 @@ export function isFlashing(): boolean {
*/
export function setFlashingFlag() {
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
try {
electron.ipcRenderer.invoke('disable-screensaver');
} catch (error) {
console.log(
"Can't disable-screensaver, we're probably not running on a balena-electron env",
);
}
electron.ipcRenderer.send('disable-screensaver');
store.dispatch({
type: Actions.SET_FLASHING_FLAG,
data: {},
@@ -76,7 +70,8 @@ export function unsetFlashingFlag(results: {
data: results,
});
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.invoke('enable-screensaver');
electron.ipcRenderer.send('enable-screensaver');
}
export function setDevicePaths(devicePaths: string[]) {

View File

@@ -43,7 +43,7 @@ function blink(t: number) {
return Math.floor(t) % 2;
}
function one(_t: number) {
function one() {
return 1;
}

View File

@@ -200,7 +200,7 @@ function storeReducer(
constraints.isDriveValid(drive, image) &&
!drive.isReadOnly &&
constraints.isDriveSizeRecommended(drive, image) &&
// We don't want to auto-select large drives execpt is autoSelectAllDrives is true
// We don't want to auto-select large drives except if autoSelectAllDrives is true
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
// We don't want to auto-select system drives
!constraints.isSystemDrive(drive)

View File

@@ -52,7 +52,7 @@ export const anonymizeSentryData = (
return event;
};
const extractPathRegex = /(.*)(^|\s)(file\:\/\/)?(\w\:)?([\\\/].+)/;
const extractPathRegex = /(.*)(^|\s)(file:\/\/)?(\w:)?([\\/].+)/;
const etcherSegmentMarkers = ['app.asar', 'Resources'];
export const anonymizePath = (input: string) => {
@@ -156,7 +156,7 @@ function flattenObject(obj: any) {
const toReturn: AnalyticsPayload = {};
for (const i in obj) {
if (!obj.hasOwnProperty(i)) {
if (!Object.prototype.hasOwnProperty.call(obj, i)) {
continue;
}
@@ -168,7 +168,7 @@ function flattenObject(obj: any) {
if (typeof obj[i] === 'object' && obj[i] !== null) {
const flatObject = flattenObject(obj[i]);
for (const x in flatObject) {
if (!flatObject.hasOwnProperty(x)) {
if (!Object.prototype.hasOwnProperty.call(flatObject, x)) {
continue;
}

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

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

View File

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

View File

@@ -27,7 +27,6 @@ import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state';
import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer';
import * as notification from '../../os/notification';
import {
@@ -95,10 +94,6 @@ async function flashImageToDrive(
return '';
}
// Stop scanning drives when flashing
// otherwise Windows throws EPERM
driveScanner.stop();
const iconPath = path.join('media', 'icon.png');
const basename = path.basename(image.path);
try {
@@ -110,7 +105,7 @@ async function flashImageToDrive(
cancelled,
} = flashState.getFlashResults();
if (!skip && !cancelled) {
if (results.devices.successful > 0) {
if (results?.devices?.successful > 0) {
notifySuccess(iconPath, basename, drives, results.devices);
} else {
notifyFailure(iconPath, basename, drives);
@@ -129,7 +124,6 @@ async function flashImageToDrive(
return errorMessage;
} finally {
availableDrives.setDrives([]);
driveScanner.start();
}
return '';

View File

@@ -14,8 +14,8 @@
* limitations under the License.
*/
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg';
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg';
import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
@@ -26,10 +26,8 @@ import styled from 'styled-components';
import FinishPage from '../../components/finish/finish';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
import { SettingsModal } from '../../components/settings/settings';
import {
SourceMetadata,
SourceSelector,
} from '../../components/source-selector/source-selector';
import { SourceSelector } from '../../components/source-selector/source-selector';
import { SourceMetadata } from '../../../../shared/typings/source-selector';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
@@ -118,10 +116,10 @@ interface MainPageState {
}
export class MainPage extends React.Component<
{},
object,
MainPageState & MainPageStateFromStore
> {
constructor(props: {}) {
constructor(props: object) {
super(props);
this.state = {
current: 'main',
@@ -313,7 +311,7 @@ export class MainPage extends React.Component<
onClick={() =>
openExternal(
selectionState.getImage()?.supportUrl ||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
'https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md',
)
}
tabIndex={6}

12
lib/gui/app/preload.ts Normal file
View File

@@ -0,0 +1,12 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
import * as webapi from '../webapi';
declare global {
interface Window {
etcher: typeof webapi;
}
}
window['etcher'] = webapi;

View File

@@ -6,10 +6,4 @@ import { ipcRenderer } from 'electron';
ipcRenderer.send('change-lng', langParser());
if (module.hot) {
module.hot.accept('./app', () => {
main();
});
}
main();

View File

@@ -14,7 +14,6 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import * as React from 'react';
import {
Alert as AlertBase,
@@ -113,14 +112,25 @@ export const DetailsText = (props: FlexProps) => (
const modalFooterShadowCss = css`
overflow: auto;
background: 0, linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, 0,
background:
0,
linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%,
0,
linear-gradient(rgba(255, 255, 255, 0), rgba(221, 225, 240, 0.5) 70%) 0 100%;
background-repeat: no-repeat;
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
background-size:
100% 40px,
100% 40px,
100% 8px,
100% 8px;
background-repeat: no-repeat;
background-color: white;
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
background-size:
100% 40px,
100% 40px,
100% 8px,
100% 8px;
background-attachment: local, local, scroll, scroll;
`;
@@ -236,16 +246,15 @@ export interface GenericTableProps<T> extends BaseTableProps<T> {
showWarnings?: boolean;
}
const GenericTable: <T>(
function GenericTable<T>(
props: GenericTableProps<T>,
) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({
refFn,
...props
}: GenericTableProps<T>) => (
<div>
<BaseTable<T> ref={refFn} {...props} />
</div>
);
): React.ReactElement<GenericTableProps<T>> {
return (
<div>
<BaseTable<T> ref={props.refFn} {...props} />
</div>
);
}
function StyledTable<T>() {
return styled((props: GenericTableProps<T>) => (
@@ -284,7 +293,6 @@ function StyledTable<T>() {
[data-display='table-body'] > [data-display='table-row'] {
> [data-display='table-cell']:first-child {
padding-left: 15px;
width: 6%;
}
> [data-display='table-cell']:last-child {
@@ -319,7 +327,7 @@ function StyledTable<T>() {
`;
}
export const Table = <T extends {}>(props: GenericTableProps<T>) => {
export const Table = <T extends object>(props: GenericTableProps<T>) => {
const TypedStyledFunctional = StyledTable<T>();
return <TypedStyledFunctional {...props} />;
};

View File

@@ -14,6 +14,12 @@
* limitations under the License.
*/
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
// whether you're running in development or production).
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
import * as electron from 'electron';
import * as remoteMain from '@electron/remote/main';
import { autoUpdater } from 'electron-updater';
@@ -21,7 +27,7 @@ import { promises as fs } from 'fs';
import { platform } from 'os';
import * as path from 'path';
import * as semver from 'semver';
import * as _ from 'lodash';
import * as lodash from 'lodash';
import './app/i18n';
@@ -34,6 +40,8 @@ import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json';
import { anonymizeSentryData } from './app/modules/analytics';
import { delay } from '../shared/utils';
const customProtocol = 'etcher';
const scheme = `${customProtocol}://`;
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
@@ -52,7 +60,7 @@ async function checkForUpdates(interval: number) {
const release = await autoUpdater.checkForUpdates();
const isOutdated =
semver.compare(release!.updateInfo.version, version) > 0;
const shouldUpdate = release!.updateInfo.stagingPercentage !== 0; // undefinded (default) means 100%
const shouldUpdate = release!.updateInfo.stagingPercentage !== 0; // undefined (default) means 100%
if (shouldUpdate && isOutdated) {
await autoUpdater.downloadUpdate();
packageUpdated = true;
@@ -107,10 +115,10 @@ async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
}
}
const initSentryMain = _.once(() => {
const initSentryMain = lodash.once(() => {
const dsn =
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
lodash.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryMain.init({ dsn, beforeSend: anonymizeSentryData });
});
@@ -138,14 +146,6 @@ electron.app.on('open-url', async (event, data) => {
await selectImageURL(data);
});
interface AutoUpdaterConfig {
autoDownload?: boolean;
autoInstallOnAppQuit?: boolean;
allowPrerelease?: boolean;
fullChangelog?: boolean;
allowDowngrade?: boolean;
}
async function createMainWindow() {
const fullscreen = Boolean(await settings.get('fullscreen'));
const defaultWidth = settings.DEFAULT_WIDTH;
@@ -176,15 +176,16 @@ async function createMainWindow() {
contextIsolation: false,
webviewTag: true,
zoomFactor: width / defaultWidth,
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
electron.app.setAsDefaultProtocolClient(customProtocol);
mainWindow.setFullScreen(true);
// mainWindow.setFullScreen(true);
// Prevent flash of white when starting the application
mainWindow.on('ready-to-show', () => {
mainWindow.once('ready-to-show', () => {
console.timeEnd('ready-to-show');
// Electron sometimes caches the zoomFactor
// making it obnoxious to switch back-and-forth
@@ -195,17 +196,11 @@ async function createMainWindow() {
// Prevent external resources from being loaded (like images)
// when dropping them on the WebView.
// See https://github.com/electron/electron/issues/5919
mainWindow.webContents.on('will-navigate', (event) => {
mainWindow.webContents.on('will-navigate', (event: any) => {
event.preventDefault();
});
mainWindow.loadURL(
`file://${path.join(
'/',
...__dirname.split(path.sep).map(encodeURIComponent),
'index.html',
)}`,
);
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
const page = mainWindow.webContents;
remoteMain.enable(page);
@@ -241,6 +236,20 @@ electron.app.on('before-quit', () => {
process.exit(EXIT_CODES.SUCCESS);
});
// this is replaced at build-time with the path to helper binary,
// relative to the app resources directory.
declare const ETCHER_UTIL_BIN_PATH: string;
electron.ipcMain.handle('get-util-path', () => {
if (process.env.NODE_ENV === 'development') {
// In development there is no "app bundle" and we're working directly with
// artifacts from the "out" directory, where this value point to.
return ETCHER_UTIL_BIN_PATH;
}
// In any other case, resolve the helper relative to resources path.
return path.resolve(process.resourcesPath, ETCHER_UTIL_BIN_PATH);
});
async function main(): Promise<void> {
if (!electron.app.requestSingleInstanceLock()) {
electron.app.quit();
@@ -267,8 +276,33 @@ async function main(): Promise<void> {
console.log('Build menu failed. ');
}
});
electron.ipcMain.on('webview-dom-ready', (_, id) => {
const webview = electron.webContents.fromId(id);
// Open link in browser if it's opened as a 'foreground-tab'
webview!.setWindowOpenHandler((event) => {
const url = new URL(event.url);
if (
(url.protocol === 'http:' || url.protocol === 'https:') &&
event.disposition === 'foreground-tab' &&
// Don't open links if they're disabled by the env var
!settings.getSync('disableExternalLinks')
) {
electron.shell.openExternal(url.href);
}
return { action: 'deny' };
});
});
}
}
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
// tslint:disable-next-line:no-var-requires
if (require('electron-squirrel-startup')) {
app.quit();
}
main();
console.time('ready-to-show');

View File

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

15
lib/gui/webapi.ts Normal file
View File

@@ -0,0 +1,15 @@
//
// Anything exported from this module will become available to the
// renderer process via preload. They're accessible as `window.etcher.foo()`.
//
import { ipcRenderer } from 'electron';
// FIXME: this is a workaround for the renderer to be able to find the etcher-util
// binary. We should instead export a function that asks the main process to launch
// the binary itself.
export async function getEtcherUtilPath(): Promise<string> {
const utilPath = await ipcRenderer.invoke('get-util-path');
console.log(utilPath);
return utilPath;
}

View File

@@ -19,7 +19,6 @@ import { join } from 'path';
import { env } from 'process';
import { promisify } from 'util';
import { getAppPath } from '../utils';
import { supportedLocales } from '../../gui/app/i18n';
const execFileAsync = promisify(execFile);
@@ -27,6 +26,15 @@ const execFileAsync = promisify(execFile);
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`;
function getAskPassScriptPath(lang: string): string {
if (process.env.NODE_ENV === 'development') {
// Force webpack's hand to bundle the script.
return require.resolve(`./sudo-askpass.osascript-${lang}.js`);
}
// Otherwise resolve the script relative to resources path.
return join(process.resourcesPath, `sudo-askpass.osascript-${lang}.js`);
}
export async function sudo(
command: string,
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
@@ -47,11 +55,7 @@ export async function sudo(
encoding: 'utf8',
env: {
PATH: env.PATH,
SUDO_ASKPASS: join(
getAppPath(),
__dirname,
`sudo-askpass.osascript-${lang}.js`,
),
SUDO_ASKPASS: getAskPassScriptPath(lang),
},
},
);

View File

@@ -15,11 +15,11 @@
*/
import { Drive } from 'drivelist';
import * as _ from 'lodash';
import { isNil } from 'lodash';
import * as pathIsInside from 'path-is-inside';
import * as messages from './messages';
import { SourceMetadata } from '../gui/app/components/source-selector/source-selector';
import { SourceMetadata } from './typings/source-selector';
/**
* @summary The default unknown size for things such as images and drives
@@ -210,8 +210,8 @@ export function getDriveImageCompatibilityStatuses(
});
}
if (
!_.isNil(drive) &&
!_.isNil(drive.size) &&
!isNil(drive) &&
!isNil(drive.size) &&
!isDriveLargeEnough(drive, image)
) {
statusList.push(statuses.small);
@@ -229,7 +229,7 @@ export function getDriveImageCompatibilityStatuses(
if (
image !== undefined &&
!_.isNil(drive) &&
!isNil(drive) &&
!isDriveSizeRecommended(drive, image)
) {
statusList.push(statuses.sizeNotRecommended);

View File

@@ -16,7 +16,7 @@
import { Dictionary } from 'lodash';
import { outdent } from 'outdent';
import * as prettyBytes from 'pretty-bytes';
import prettyBytes from 'pretty-bytes';
import '../gui/app/i18n';
import * as i18next from 'i18next';
@@ -164,11 +164,11 @@ export const error = {
? i18next.t('message.toDrive', {
description: drives[0].description,
name: drives[0].displayName,
})
})
: i18next.t('message.toTarget', {
count: drives.length,
num: drives.length,
});
});
return i18next.t('message.flashError', {
image: imageBasename,
targets: target,

View File

@@ -70,14 +70,14 @@ export async function isElevated(): Promise<boolean> {
}
return true;
}
return process.geteuid() === UNIX_SUPERUSER_USER_ID;
return process.geteuid!() === UNIX_SUPERUSER_USER_ID;
}
/**
* @summary Check if the current process is running with elevated permissions
*/
export function isElevatedUnixSync(): boolean {
return process.geteuid() === UNIX_SUPERUSER_USER_ID;
return process.geteuid!() === UNIX_SUPERUSER_USER_ID;
}
function escapeSh(value: any): string {

View File

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

View File

@@ -14,9 +14,6 @@
* limitations under the License.
*/
import axios from 'axios';
import { Dictionary } from 'lodash';
import * as errors from './errors';
export function isValidPercentage(percentage: any): boolean {
@@ -38,19 +35,6 @@ export async function delay(duration: number): Promise<void> {
});
}
export function getAppPath(): string {
return (
(require('electron').app || require('@electron/remote').app)
.getAppPath()
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
// include the app-x64 or app-arm64 folder depending on the arch.
// We don't care about the app.asar file, we want the actual folder.
.replace(/\.asar$/, () =>
process.platform === 'darwin' ? '-' + process.arch : '',
)
);
}
export function isJson(jsonString: string) {
try {
JSON.parse(jsonString);

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

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

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

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

View File

@@ -30,14 +30,13 @@ const adapters: Adapter[] = [
// Can't use permissions.isElevated() here as it returns a promise and we need to set
// module.exports = scanner right now.
if (platform !== 'linux' || geteuid() === 0) {
if (platform !== 'linux' || (geteuid && geteuid() === 0)) {
adapters.push(new UsbbootDeviceAdapter());
}
if (platform === 'win32') {
const {
DriverlessDeviceAdapter: driverless,
// tslint:disable-next-line:no-var-requires
} = require('etcher-sdk/build/scanner/adapters/driverless');
adapters.push(new driverless());
}

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

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

View File

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

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

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

38989
npm-shrinkwrap.json generated Normal file

File diff suppressed because it is too large Load Diff

36145
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,10 @@
"name": "balena-etcher",
"private": true,
"displayName": "balenaEtcher",
"version": "1.18.6",
"productName": "balenaEtcher",
"version": "1.19.8",
"packageType": "local",
"main": "generated/etcher.js",
"main": ".webpack/main",
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
"productDescription": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.",
"homepage": "https://github.com/balena-io/etcher",
@@ -13,119 +14,139 @@
"url": "git@github.com:balena-io/etcher.git"
},
"scripts": {
"build": "npm run webpack",
"flowzone-preinstall-linux": "sudo apt-get update && sudo apt-get install -y xvfb libudev-dev && cat < electron-builder.yml | yq e .deb.depends[] - | xargs -L1 echo | sed 's/|//g' | xargs -L1 sudo apt-get --ignore-missing install || true",
"flowzone-preinstall-macos": "true",
"flowzone-preinstall-windows": "npx node-gyp install",
"flowzone-preinstall": "npm run flowzone-preinstall-linux",
"lint-css": "prettier --write lib/**/*.css",
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
"lint": "npm run lint-ts && npm run lint-css",
"postinstall": "electron-rebuild -t prod,dev,optional",
"sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh",
"start": "./node_modules/.bin/electron .",
"test-gui": "electron-mocha --recursive --reporter spec --window-config tests/gui/window-config.json --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
"test-shared": "electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
"test-macos": "npm run lint && npm run test-gui && npm run test-shared && npm run sanity-checks",
"test-linux": "npm run lint && xvfb-run --auto-servernum npm run test-gui && xvfb-run --auto-servernum npm run test-shared && npm run sanity-checks",
"test-windows": "npm run lint && npm run test-gui && npm run test-shared && npm run sanity-checks",
"test": "echo npm run test-{linux,windows,macos}",
"watch": "webpack serve --no-optimization-minimize --config ./webpack.dev.config.ts",
"webpack": "webpack"
"prettify": "prettier --write lib/**/*.css && balena-lint --fix --typescript typings lib tests forge.config.ts forge.sidecar.ts webpack.config.ts",
"lint": "npm run prettify && catch-uncommitted",
"test-gui": "xvfb-maybe electron-mocha --recursive --reporter spec --window-config tests/gui/window-config.json --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
"test-shared": "xvfb-maybe electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
"test": "npm run test-gui && npm run test-shared",
"package": "electron-forge package",
"start": "electron-forge start",
"make": "electron-forge make"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
"pre-commit": "npm run prettify"
}
},
"lint-staged": {
"./**/*.{ts,tsx}": [
"npm run lint-ts"
],
"./**/*.css": [
"npm run lint-css"
]
},
"author": "Balena Ltd. <hello@balena.io>",
"license": "Apache-2.0",
"devDependencies": {
"@balena/lint": "5.4.2",
"dependencies": {
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
"@electron/remote": "^2.0.9",
"@fortawesome/fontawesome-free": "5.15.4",
"@sentry/electron": "^4.1.2",
"@svgr/webpack": "5.5.0",
"@types/chai": "4.3.4",
"@types/copy-webpack-plugin": "6.4.3",
"@types/mime-types": "2.1.1",
"@types/mini-css-extract-plugin": "1.4.3",
"@types/mocha": "^9.1.1",
"@types/node": "^16.18.12",
"@types/node-ipc": "9.2.0",
"@types/react": "16.14.34",
"@types/react-dom": "16.9.17",
"@types/semver": "7.3.13",
"@types/sinon": "9.0.11",
"@types/terser-webpack-plugin": "5.0.4",
"@types/tmp": "0.2.3",
"@types/webpack-node-externals": "2.5.3",
"@electron/remote": "^2.1.0",
"@fortawesome/fontawesome-free": "6.5.1",
"@sentry/electron": "^4.15.1",
"analytics-client": "^2.0.1",
"axios": "^0.27.2",
"chai": "4.3.7",
"copy-webpack-plugin": "7.0.0",
"css-loader": "5.2.7",
"d3": "4.13.0",
"axios": "^1.6.0",
"debug": "4.3.4",
"electron": "^19.1.9",
"electron-builder": "^23.6.0",
"electron-mocha": "^11.0.2",
"electron-notarize": "1.2.2",
"electron-rebuild": "^3.2.9",
"electron-updater": "5.3.0",
"esbuild-loader": "2.20.0",
"etcher-sdk": "8.3.1",
"file-loader": "6.2.0",
"husky": "4.3.8",
"i18next": "21.10.0",
"electron-squirrel-startup": "^1.0.0",
"electron-updater": "6.1.7",
"etcher-sdk": "9.0.0",
"i18next": "23.7.8",
"immutable": "3.8.2",
"lint-staged": "10.5.4",
"lodash": "4.17.21",
"mini-css-extract-plugin": "1.6.2",
"mocha": "^9.1.1",
"native-addon-loader": "2.0.1",
"node-ipc": "9.2.1",
"omit-deep-lodash": "1.1.7",
"outdent": "0.8.0",
"path-is-inside": "1.0.2",
"pnp-webpack-plugin": "1.7.0",
"pretty-bytes": "5.6.0",
"react": "16.8.5",
"react-dom": "16.8.5",
"react-i18next": "11.18.6",
"redux": "4.2.0",
"rendition": "19.3.2",
"semver": "7.3.8",
"simple-progress-webpack-plugin": "1.1.2",
"sinon": "9.2.4",
"string-replace-loader": "3.1.0",
"style-loader": "2.0.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-i18next": "13.5.0",
"redux": "4.2.1",
"rendition": "35.1.2",
"semver": "7.5.4",
"styled-components": "5.3.6",
"sys-class-rgb-led": "3.0.1",
"terser-webpack-plugin": "5.3.6",
"ts-loader": "8.4.0",
"ts-node": "9.1.1",
"tslib": "2.4.1",
"typescript": "4.4.4",
"uuid": "9.0.1"
},
"devDependencies": {
"@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.11",
"@types/debug": "^4.1.12",
"@types/mime-types": "2.1.4",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.6",
"@types/node-ipc": "9.2.3",
"@types/react": "17.0.2",
"@types/react-dom": "17.0.2",
"@types/semver": "7.5.6",
"@types/sinon": "17.0.2",
"@types/tmp": "0.2.6",
"@vercel/webpack-asset-relocator-loader": "1.7.3",
"catch-uncommitted": "^2.0.0",
"chai": "4.3.10",
"css-loader": "5.2.7",
"electron": "27.1.3",
"electron-mocha": "^12.2.0",
"file-loader": "6.2.0",
"husky": "8.0.3",
"mocha": "^10.2.0",
"native-addon-loader": "2.0.1",
"node-loader": "^2.0.0",
"omit-deep-lodash": "1.1.7",
"@yao-pkg/pkg": "^5.11.1",
"sinon": "17.0.1",
"string-replace-loader": "3.1.0",
"style-loader": "3.3.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tslib": "2.6.2",
"typescript": "^5.3.3",
"url-loader": "4.1.1",
"uuid": "8.3.2",
"webpack": "5.75.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.11.1"
"xvfb-maybe": "^0.2.1"
},
"hostDependencies": {
"debian": [
"gconf-service",
"gconf2",
"libasound2",
"libatk1.0-0",
"libc6",
"libcairo2",
"libcups2",
"libdbus-1-3",
"libexpat1",
"libfontconfig1",
"libfreetype6",
"libgbm1",
"libgcc1",
"libgconf-2-4",
"libgdk-pixbuf2.0-0",
"libglib2.0-0",
"libgtk-3-0",
"liblzma5",
"libnotify4",
"libnspr4",
"libnss3",
"libpango1.0-0 | libpango-1.0-0",
"libstdc++6",
"libx11-6",
"libxcomposite1",
"libxcursor1",
"libxdamage1",
"libxext6",
"libxfixes3",
"libxi6",
"libxrandr2",
"libxrender1",
"libxss1",
"libxtst6",
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
]
},
"engines": {
"node": ">=16"
"node": ">=18 <20"
},
"versionist": {
"publishedAt": "2023-03-21T13:24:18.905Z"
"publishedAt": "2024-04-22T09:37:38.023Z"
}
}

12
pkg-sidecar.json Normal file
View File

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

View File

@@ -1,2 +0,0 @@
awscli==1.27.28
shyaml==0.6.2

View File

@@ -1,52 +0,0 @@
#!/bin/bash
###
# Copyright 2017 balena.io
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###
set -u
set -e
# Read list of wildcards from .gitattributes
wildcards=()
while IFS='' read -r line || [[ -n "$line" ]]; do
if [[ -n "$line" ]]; then
if [[ ! "$line" =~ "^#" ]]; then
filetype=$(echo "$line" | cut -d ' ' -f 2)
if [[ "$filetype" == "text" ]] || [[ "$filetype" == "binary" ]]; then
wildcards+=("$(echo "$line" | cut -d ' ' -f 1)")
fi
fi
fi
done < .gitattributes
# Verify those wildcards against all files stored in the repo
git ls-tree -r HEAD | while IFS='' read line; do
if [[ "$(echo $line | cut -d ' ' -f 2)" == "blob" ]]; then
# the cut delimiter in the line below is actually a tab character, not a space
filename=$(basename $(echo "$line" | cut -d ' ' -f 2))
found_match=0
for wildcard in "${wildcards[@]}"; do
if [[ "$filename" = $wildcard ]]; then
found_match=1
break
fi
done
if [[ $found_match -eq 0 ]]; then
echo "No wildcards match $filename"
exit 1
fi
fi
done

View File

@@ -1,52 +0,0 @@
/**
* This script is in charge of cleaning the `shrinkwrap` file.
*
* `npm shrinkwrap` has a bug where it will add optional dependencies
* to `npm-shrinkwrap.json`, therefore causing errors if these optional
* dependendencies are platform dependent and you then try to build
* the project in another platform.
*
* As a workaround, we keep a list of platform dependent dependencies in
* the `platformSpecificDependencies` property of `package.json`,
* and manually remove them from `npm-shrinkwrap.json` if they exist.
*
* See: https://github.com/npm/npm/issues/2679
*/
import { writeFile } from 'fs';
import * as omit from 'omit-deep-lodash';
import * as path from 'path';
import { promisify } from 'util';
import * as shrinkwrap from '../npm-shrinkwrap.json';
import * as packageInfo from '../package.json';
const writeFileAsync = promisify(writeFile);
const JSON_INDENT = 2;
const SHRINKWRAP_FILENAME = path.join(__dirname, '..', 'npm-shrinkwrap.json');
async function main() {
try {
const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies);
for (const item of Object.values(cleaned.dependencies)) {
// @ts-ignore
item.dev = true;
}
await writeFileAsync(
SHRINKWRAP_FILENAME,
JSON.stringify(cleaned, null, JSON_INDENT),
);
} catch (error: any) {
console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`);
process.exitCode = 1;
}
console.log(
`[OK] Wrote shrinkwrap file to ${path.relative(
__dirname,
SHRINKWRAP_FILENAME,
)}`,
);
}
main();

Submodule scripts/resin deleted from 8dfa21cfc2

182
test-wrapper.ts Normal file
View File

@@ -0,0 +1,182 @@
/*
* This is a test wrapper for etcher-utils.
* The only use for this file is debugging while developing etcher-utils.
* It will create a IPC server, spawn the cli version of etcher-writer, and wait for it to connect.
* Requires elevated privileges to work (launch with sudo)
* Note that you'll need to to edit `ipc.server.on('ready', ...` function based on what you want to test.
*/
import * as ipc from 'node-ipc';
import * as os from 'os';
import * as path from 'path';
import * as packageJSON from './package.json';
import * as permissions from './lib/shared/permissions';
// if (process.argv.length !== 3) {
// console.error('Expects an image to flash as only arg!');
// process.exit(1);
// }
const THREADS_PER_CPU = 16;
// There might be multiple Etcher instances running at
// the same time, therefore we must ensure each IPC
// server/client has a different name.
const IPC_SERVER_ID = `etcher-server-${process.pid}`;
const IPC_CLIENT_ID = `etcher-client-${process.pid}`;
ipc.config.id = IPC_SERVER_ID;
ipc.config.socketRoot = path.join(
process.env.XDG_RUNTIME_DIR || os.tmpdir(),
path.sep,
);
// NOTE: Ensure this isn't disabled, as it will cause
// the stdout maxBuffer size to be exceeded when flashing
ipc.config.silent = true;
function writerArgv(): string[] {
const entryPoint = path.join('./generated/etcher-util');
return [entryPoint];
}
function writerEnv() {
return {
IPC_SERVER_ID,
IPC_CLIENT_ID,
IPC_SOCKET_ROOT: ipc.config.socketRoot,
UV_THREADPOOL_SIZE: (os.cpus().length * THREADS_PER_CPU).toString(),
// This environment variable prevents the AppImages
// desktop integration script from presenting the
// "installation" dialog
SKIP: '1',
...(process.platform === 'win32' ? {} : process.env),
};
}
async function start(): Promise<any> {
ipc.serve();
return await new Promise((resolve, reject) => {
ipc.server.on('error', (message) => {
console.log('IPC server error', message);
});
ipc.server.on('log', (message) => {
console.log('log', message);
});
ipc.server.on('fail', ({ device, error }) => {
console.log('failure', error, device);
});
ipc.server.on('done', (event) => {
console.log('done', event);
});
ipc.server.on('abort', () => {
console.log('abort');
});
ipc.server.on('skip', () => {
console.log('skip');
});
ipc.server.on('state', (progress) => {
console.log('progress', progress);
});
ipc.server.on('drives', (drives) => {
console.log('drives', drives);
});
ipc.server.on('ready', (_data, socket) => {
console.log('ready');
ipc.server.emit(socket, 'scan', {});
// ipc.server.emit(socket, "hello", { message: "world" });
// ipc.server.emit(socket, "write", {
// image: {
// path: process.argv[2],
// displayName: "Random image for test",
// description: "Random image for test",
// SourceType: "File",
// },
// destinations: [
// {
// size: 15938355200,
// isVirtual: false,
// enumerator: "DiskArbitration",
// logicalBlockSize: 512,
// raw: "/dev/rdisk4",
// error: null,
// isReadOnly: false,
// displayName: "/dev/disk4",
// blockSize: 512,
// isSCSI: false,
// isRemovable: true,
// device: "/dev/disk4",
// busVersion: null,
// isSystem: false,
// busType: "USB",
// isCard: false,
// isUSB: true,
// devicePath:
// "IODeviceTree:/arm-io@10F00000/usb-drd1@2280000/usb-drd1-port-hs@01100000",
// mountpoints: [
// {
// path: "/Volumes/flash-rootB",
// label: "flash-rootB",
// },
// {
// path: "/Volumes/flash-rootA",
// label: "flash-rootA",
// },
// {
// path: "/Volumes/flash-boot",
// label: "flash-boot",
// },
// ],
// description: "Generic Flash Disk Media",
// isUAS: null,
// partitionTableType: "mbr",
// },
// ],
// SourceType: "File",
// autoBlockmapping: true,
// decompressFirst: true,
// });
});
const argv = writerArgv();
ipc.server.on('start', async () => {
console.log(`Elevating command: ${argv.join(' ')}`);
const env = writerEnv();
try {
await permissions.elevateCommand(argv, {
applicationName: packageJSON.displayName,
environment: env,
});
} catch (error: any) {
console.log('error', error);
// This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137;
if (error.code === SIGKILL_EXIT_CODE) {
error.code = 'ECHILDDIED';
}
reject(error);
} finally {
console.log('Terminating IPC server');
}
resolve(true);
});
// Clear the update lock timer to prevent longer
// flashing timing it out, and releasing the lock
ipc.server.start();
});
}
start();

View File

@@ -1,8 +1,6 @@
// tslint:disable-next-line:no-var-requires
const { app } = require('electron');
if (app !== undefined) {
// tslint:disable-next-line:no-var-requires
const remoteMain = require('@electron/remote/main');
remoteMain.initialize();

View File

@@ -15,7 +15,6 @@
*/
import { expect } from 'chai';
import * as _ from 'lodash';
import { stub } from 'sinon';
import * as settings from '../../../lib/gui/app/models/settings';
@@ -47,8 +46,8 @@ describe('Browser: settings', () => {
const writeConfigFileStub = stub();
writeConfigFileStub.returns(Promise.reject(new Error('settings error')));
const p = settings.set('foo', 'baz', writeConfigFileStub);
await checkError(p, async (error) => {
const promise = settings.set('foo', 'baz', writeConfigFileStub);
await checkError(promise, async (error) => {
expect(error).to.be.an.instanceof(Error);
expect(error.message).to.equal('settings error');
expect(await settings.get('foo')).to.equal('bar');

View File

@@ -1156,7 +1156,7 @@ describe('Shared: DriveConstraints', function () {
'/dev/disk4',
'/dev/disk5',
'/dev/disk6',
];
];
const drives = [
{
device: drivePaths[0],

17
tsconfig.sidecar.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"typeRoots": ["./node_modules/@types", "./typings"],
"module": "commonjs",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["lib/util"]
}

View File

@@ -1,28 +0,0 @@
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"module": "es2015",
"target": "es2019",
"jsx": "react",
"typeRoots": ["./node_modules/@types", "./typings"],
"importHelpers": true,
"allowSyntheticDefaultImports": true,
"lib": ["dom", "esnext"],
"declaration": true,
"declarationMap": true,
"pretty": true,
"sourceMap": true,
"baseUrl": "./src",
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowJs": true
},
"include": [
"lib/**/*.ts",
"node_modules/electron/**/*.d.ts"
]
}

View File

@@ -1 +0,0 @@
declare module 'pnp-webpack-plugin';

View File

@@ -1 +0,0 @@
declare module 'resin-corvus/browser';

View File

@@ -1 +0,0 @@
declare module 'simple-progress-webpack-plugin';

View File

@@ -14,137 +14,13 @@
* limitations under the License.
*/
import * as CopyPlugin from 'copy-webpack-plugin';
import { readdirSync } from 'fs';
import * as _ from 'lodash';
import * as os from 'os';
import outdent from 'outdent';
import * as path from 'path';
import { env } from 'process';
import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin';
import * as TerserPlugin from 'terser-webpack-plugin';
import type { Configuration, ModuleOptions } from 'webpack';
import {
BannerPlugin,
IgnorePlugin,
NormalModuleReplacementPlugin,
} from 'webpack';
import * as PnpWebpackPlugin from 'pnp-webpack-plugin';
import * as tsconfigRaw from './tsconfig.webpack.json';
/**
* Don't webpack package.json as sentry tokens
* will be inserted in it after webpacking
*/
function externalPackageJson(packageJsonPath: string) {
return (
{ request }: { context: string; request: string },
callback: (error?: Error | null, result?: string) => void,
) => {
if (_.endsWith(request, 'package.json')) {
return callback(null, `commonjs ${packageJsonPath}`);
}
return callback();
};
}
function platformSpecificModule(
platform: string,
module: string,
replacement = '{}',
) {
// Resolves module on platform, otherwise resolves the replacement
return (
{ request }: { context: string; request: string },
callback: (error?: Error, result?: string, type?: string) => void,
) => {
if (request === module && os.platform() !== platform) {
callback(undefined, replacement);
return;
}
callback();
};
}
function renameNodeModules(resourcePath: string) {
// electron-builder excludes the node_modules folder even if you specifically include it
// Work around by renaming it to "modules"
// See https://github.com/electron-userland/electron-builder/issues/4545
return (
path
.relative(__dirname, resourcePath)
.replace('node_modules', 'modules')
// use the same name on all architectures so electron-builder can build a universal dmg on mac
.replace(LZMA_BINDINGS_FOLDER, LZMA_BINDINGS_FOLDER_RENAMED)
// file-loader expects posix paths, even on Windows
.replace(/\\/g, '/')
);
}
function findUsbPrebuild(): string[] {
const usbPrebuildsFolder = path.join('node_modules', 'usb', 'prebuilds');
const prebuildFolders = readdirSync(usbPrebuildsFolder);
let bindingFile: string | undefined = 'node.napi.node';
const platformFolder = prebuildFolders.find(
(f) => f.startsWith(os.platform()) && f.indexOf(os.arch()) > -1,
);
if (platformFolder === undefined) {
throw new Error(
'Could not find usb prebuild. Should try fallback to node-gyp and use /build/Release instead of /prebuilds',
);
}
const bindingFiles = readdirSync(
path.join(usbPrebuildsFolder, platformFolder),
);
if (!bindingFiles.length) {
throw new Error('Could not find usb prebuild for platform');
}
if (bindingFiles.length === 1) {
bindingFile = bindingFiles[0];
}
// armv6 vs v7 in linux-arm and
// glibc vs musl in linux-x64
if (bindingFiles.length > 1) {
bindingFile = bindingFiles.find((file) => {
if (bindingFiles.indexOf('arm') > -1) {
const process = require('process');
return file.indexOf(process.config.variables.arm_version) > -1;
} else {
return file.indexOf('glibc') > -1;
}
});
}
if (bindingFile === undefined) {
throw new Error('Could not find usb prebuild for platform');
}
return [platformFolder, bindingFile];
}
const [USB_BINDINGS_FOLDER, USB_BINDINGS_FILE] = findUsbPrebuild();
function findLzmaNativeBindingsFolder(): string {
const files = readdirSync(
path.join('node_modules', 'lzma-native', 'prebuilds'),
);
const bindingsFolder = files.find(
(f) =>
f.startsWith(os.platform()) &&
f.endsWith(env.npm_config_target_arch || os.arch()),
);
if (bindingsFolder === undefined) {
throw new Error('Could not find lzma_native binding');
}
return bindingsFolder;
}
const LZMA_BINDINGS_FOLDER = findLzmaNativeBindingsFolder();
const LZMA_BINDINGS_FOLDER_RENAMED = 'binding';
interface ReplacementRule {
search: string;
@@ -164,158 +40,58 @@ function replace(test: RegExp, ...replacements: ReplacementRule[]) {
};
}
const commonConfig = {
mode: 'production',
optimization: {
moduleIds: 'natural',
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: false,
mangle: false,
format: {
comments: false,
ecma: 2020,
},
},
extractComments: false,
}),
],
const rules: Required<ModuleOptions>['rules'] = [
// Add support for native node modules
{
// We're specifying native_modules in the test because the asset relocator loader generates a
// "fake" .node file which is really a cjs file.
test: /native_modules[/\\].+\.node$/,
use: 'node-loader',
},
{
test: /[/\\]node_modules[/\\].+\.(m?js|node)$/,
parser: { amd: false },
use: {
loader: '@vercel/webpack-asset-relocator-loader',
options: {
outputAssetBase: 'native_modules',
},
},
},
{
test: /\.tsx?$/,
exclude: /(node_modules|\.webpack)/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
loader: 'file-loader',
},
{
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',
}),
];
export const rendererConfig: Configuration = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
loader: 'file-loader',
options: { name: renameNodeModules },
},
{
test: /\.svg$/,
use: '@svgr/webpack',
},
{
test: /\.tsx?$/,
use: [
{
loader: 'esbuild-loader',
options: {
loader: 'tsx',
target: 'es2021',
tsconfigRaw,
},
},
],
},
// don't import WeakMap polyfill in deep-map-keys (required in corvus)
replace(/node_modules\/deep-map-keys\/lib\/deep-map-keys\.js$/, {
search: "var WeakMap = require('es6-weak-map');",
replace: '',
}),
// force axios to use http backend (not xhr) to support streams
replace(/node_modules\/axios\/lib\/defaults\.js$/, {
search: './adapters/xhr',
replace: './adapters/http',
}),
// remove bindings magic from drivelist
replace(
/node_modules\/drivelist\/js\/index\.js$/,
{
search: 'require("bindings");',
replace: "require('../build/Release/drivelist.node')",
},
{
search: "bindings('drivelist')",
replace: 'bindings',
},
),
replace(
/node_modules\/lzma-native\/index\.js$/,
// remove node-pre-gyp magic from lzma-native
{
search: `require('node-gyp-build')(__dirname);`,
replace: `require('./prebuilds/${LZMA_BINDINGS_FOLDER}/electron.napi.node')`,
},
// use regular stream module instead of readable-stream
{
search: "var stream = require('readable-stream');",
replace: "var stream = require('stream');",
},
),
// remove node-pre-gyp magic from usb
replace(/node_modules\/usb\/dist\/usb\/bindings\.js$/, {
search: `require('node-gyp-build')(path_1.join(__dirname, '..', '..'));`,
replace: `require('../../prebuilds/${USB_BINDINGS_FOLDER}/${USB_BINDINGS_FILE}')`,
}),
// remove bindings magic from mountutils
replace(/node_modules\/mountutils\/index\.js$/, {
search: outdent`
require('bindings')({
bindings: 'MountUtils',
/* eslint-disable camelcase */
module_root: __dirname
/* eslint-enable camelcase */
})
`,
replace: "require('./build/Release/MountUtils.node')",
}),
// remove bindings magic from winusb-driver-generator
replace(/node_modules\/winusb-driver-generator\/index\.js$/, {
search: outdent`
require('bindings')({
bindings: 'Generator',
/* eslint-disable camelcase */
module_root: __dirname
/* eslint-enable camelcase */
});
`,
replace: "require('./build/Release/Generator.node')",
}),
replace(/node_modules\/node-raspberrypi-usbboot\/build\/index\.js$/, {
search:
"return await readFile(Path.join(__dirname, '..', 'blobs', filename));",
replace: outdent`
const remote = require('@electron/remote');
return await readFile(
Path.join(
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
// include the app-x64 or app-arm64 folder depending on the arch.
// We don't care about the app.asar file, we want the actual folder.
remote.app.getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
'generated',
__dirname.replace('node_modules', 'modules'),
'..',
'blobs',
filename
)
);
`,
}),
// Copy native modules to generated folder
{
test: /\.node$/,
use: [
{
loader: 'native-addon-loader',
options: { name: renameNodeModules },
},
],
},
],
},
resolve: {
extensions: ['.node', '.js', '.json', '.ts', '.tsx'],
rules,
},
plugins: [
PnpWebpackPlugin,
new SimpleProgressWebpackPlugin({
format: process.env.WEBPACK_PROGRESS || 'verbose',
}),
// 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(
@@ -330,100 +106,25 @@ const commonConfig = {
new IgnorePlugin({
resourceRegExp: /^aws-crt$/,
}),
],
resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)],
},
output: {
path: path.join(__dirname, 'generated'),
filename: '[name].js',
},
externals: [
// '../package.json' because we are in 'generated'
externalPackageJson('../package.json'),
// Only exists on windows
platformSpecificModule('win32', 'winusb-driver-generator'),
// Not needed but required by resin-corvus > os-locale > execa > cross-spawn
platformSpecificModule('none', 'spawn-sync'),
// Not needed as we replace all requires for it
platformSpecificModule('none', 'node-pre-gyp', '{ find: () => {} }'),
// Not needed as we replace all requires for it
platformSpecificModule('none', 'bindings'),
],
};
const guiConfigCopyPatterns = [
{
from: 'node_modules/node-raspberrypi-usbboot/blobs',
to: 'modules/node-raspberrypi-usbboot/blobs',
},
];
if (os.platform() === 'win32') {
// liblzma.dll is required on Windows for lzma-native
guiConfigCopyPatterns.push({
from: `node_modules/lzma-native/prebuilds/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
to: `modules/lzma-native/prebuilds/${LZMA_BINDINGS_FOLDER_RENAMED}/liblzma.dll`,
});
}
const guiConfig = {
...commonConfig,
target: 'electron-renderer',
node: {
__dirname: true,
__filename: true,
},
entry: {
gui: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
},
// entry: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
plugins: [
...commonConfig.plugins,
new CopyPlugin({
patterns: [
{ from: 'lib/gui/app/index.html', to: 'index.html' },
// electron-builder doesn't bundle folders named "assets"
// See https://github.com/electron-userland/electron-builder/issues/4545
{ from: 'assets/icon.png', to: 'media/icon.png' },
],
}),
// Remove "Download the React DevTools for a better development experience" message
new BannerPlugin({
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
raw: true,
}),
new CopyPlugin({ patterns: guiConfigCopyPatterns }),
],
};
const mainConfig = {
...commonConfig,
target: 'electron-main',
node: {
__dirname: false,
__filename: true,
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
},
};
const etcherConfig = {
...mainConfig,
export const mainConfig: Configuration = {
entry: {
etcher: path.join(__dirname, 'lib', 'gui', 'etcher.ts'),
etcher: './lib/gui/etcher.ts',
},
module: {
rules,
},
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
},
};
const childWriterConfig = {
...mainConfig,
entry: {
'child-writer': path.join(
__dirname,
'lib',
'gui',
'modules',
'child-writer.ts',
),
},
};
export default [guiConfig, etcherConfig, childWriterConfig];

View File

@@ -1,24 +0,0 @@
import configs from './webpack.config';
import { WebpackOptionsNormalized } from 'webpack';
import * as fs from 'fs';
const [
guiConfig,
etcherConfig,
childWriterConfig,
] = (configs as unknown) as WebpackOptionsNormalized[];
configs.forEach((config) => {
config.mode = 'development';
// @ts-ignore
config.devtool = 'source-map';
});
guiConfig.devServer = {
hot: true,
port: 3030,
};
fs.copyFileSync('./lib/gui/app/index.dev.html', './generated/index.html');
export default [guiConfig, etcherConfig, childWriterConfig];