mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-22 14:27:18 +00:00
Compare commits
No commits in common. "master" and "v1.7.11" have entirely different histories.
10
.eslintrc.js
10
.eslintrc.js
@ -1,10 +0,0 @@
|
||||
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",
|
||||
},
|
||||
};
|
454
.eslintrc.yml
Normal file
454
.eslintrc.yml
Normal file
@ -0,0 +1,454 @@
|
||||
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
|
51
.github/actions/finalize/action.yml
vendored
Normal file
51
.github/actions/finalize/action.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
name: publish GitHub release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Get release version
|
||||
if: runner.os == 'Linux'
|
||||
id: get_release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Rename release
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
gh release edit '${{ github.event.pull_request.head.ref }}' \
|
||||
--title 'v${{ steps.get_release.outputs.version }}' \
|
||||
--tag 'v${{ steps.get_release.outputs.version }}'
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||
|
||||
- name: Finalize GitHub release
|
||||
if: runner.os == 'Linux'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: v${{ steps.get_release.outputs.version }}
|
||||
tag_name: v${{ steps.get_release.outputs.version }}
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
prerelease: false
|
||||
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
304
.github/actions/publish/action.yml
vendored
304
.github/actions/publish/action.yml
vendored
@ -10,12 +10,12 @@ inputs:
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
XCODE_APP_LOADER_EMAIL:
|
||||
type: string
|
||||
default: "accounts+apple@balena.io"
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
# Beware that native modules will be built for this version,
|
||||
# which might not be compatible with the one used by pkg (see forge.sidecar.ts)
|
||||
# https://github.com/vercel/pkg-fetch/releases
|
||||
default: "20.x"
|
||||
default: "14.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
@ -25,22 +25,22 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract custom source artifact
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
working-directory: .
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Extract custom source artifact
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
shell: powershell
|
||||
working-directory: .
|
||||
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -xf ${{ runner.temp }}\custom.tgz
|
||||
run: tar -xf ${{ runner.temp }}\custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
@ -48,158 +48,204 @@ runs:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install host dependencies
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: sudo apt-get install -y --no-install-recommends fakeroot dpkg rpm
|
||||
- name: Install yq
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: choco install yq
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
# rpmbuild will strip binaries by default, which breaks the sidecar.
|
||||
# Use a macro to override the "strip" to bypass stripping.
|
||||
- name: Configure rpmbuild to not strip executables
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: echo '%__strip /usr/bin/true' > ~/.rpmmacros
|
||||
|
||||
- name: Install host dependencies
|
||||
if: runner.os == 'macOS'
|
||||
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
|
||||
# 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
|
||||
# FIXME: resinci-deploy is not actively maintained
|
||||
# https://github.com/product-os/resinci-deploy
|
||||
- name: Checkout resinci-deploy
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
python-version: '3.11'
|
||||
repository: product-os/resinci-deploy
|
||||
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||
path: resinci-deploy
|
||||
|
||||
- name: Build and install resinci-deploy
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
rm -rf ../resinci-deploy && mv resinci-deploy ..
|
||||
|
||||
pushd ../resinci-deploy && npm ci && npm link && popd
|
||||
|
||||
if [ $runner_os =~ linux|macos ]]; then
|
||||
chmod +x "$(dirname "$(which node)")/resinci-deploy" && which resinci-deploy
|
||||
fi
|
||||
|
||||
# FIXME: store sentry workflow is not documented
|
||||
# https://github.com/product-os/resinci-deploy/blob/master/lib/sentry.ts
|
||||
# https://github.com/getsentry/sentry-cli
|
||||
# https://docs.sentry.io/api/projects/create-a-new-client-key/
|
||||
- name: Generate Sentry DSN
|
||||
id: sentry
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
branch="$(echo '${{ github.event.pull_request.head.ref }}' | sed 's/[^[:alnum:]]/-/g')"
|
||||
|
||||
stdout="$(resinci-deploy store sentry \
|
||||
--branch="${branch}" \
|
||||
--name="$(jq -r '.name' package.json)" \
|
||||
--team="$(yq e '.sentry.team' repo.yml)" \
|
||||
--org="$(yq e '.sentry.org' repo.yml)" \
|
||||
--type="$(yq e '.sentry.type' repo.yml)")"
|
||||
|
||||
echo "dsn=$(echo "${stdout}" | tail -n 1)" >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
SENTRY_TOKEN: ${{ fromJSON(inputs.secrets).SENTRY_AUTH_TOKEN }}
|
||||
|
||||
# https://www.electron.build/code-signing.html
|
||||
# https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
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 }}
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
|
||||
- name: Import Windows code signing certificate
|
||||
if: runner.os == 'Windows'
|
||||
id: import_win_signing_cert
|
||||
shell: powershell
|
||||
run: |
|
||||
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
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||
|
||||
echo "certFilePath=${{ runner.temp }}/Certificate_pkcs12.p12" >> $GITHUB_OUTPUT
|
||||
Import-PfxCertificate `
|
||||
-FilePath ${{ runner.temp }}/certificate.pfx `
|
||||
-CertStoreLocation Cert:\CurrentUser\My `
|
||||
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.pfx
|
||||
|
||||
env:
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_FILE_B64 }}
|
||||
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
|
||||
# ... or refactor (e.g.) https://github.com/samuelmeuli/action-electron-builder
|
||||
# https://github.com/product-os/scripts/tree/master/electron
|
||||
# https://github.com/product-os/scripts/tree/master/shared
|
||||
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
|
||||
- name: Package release
|
||||
shell: bash
|
||||
# IMPORTANT: before making changes to this step please consult @engineering in balena's chat.
|
||||
id: package_release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
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
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
ELECTRON_BUILDER_ARCHITECTURE="${runner_arch}"
|
||||
APPLICATION_VERSION="$(jq -r '.version' package.json)"
|
||||
HOST_ARCH="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||
ARCHITECTURE_FLAGS="--${ELECTRON_BUILDER_ARCHITECTURE}"
|
||||
|
||||
if [[ "${RUNNER_OS}" == Linux ]]; then
|
||||
PLATFORM=Linux
|
||||
SHA256SUM_BIN=sha256sum
|
||||
if [[ $runner_os =~ linux ]]; then
|
||||
ELECTRON_BUILDER_OS='--linux'
|
||||
TARGETS="$(yq e .linux.target[] electron-builder.yml)"
|
||||
|
||||
elif [[ "${RUNNER_OS}" == macOS ]]; then
|
||||
PLATFORM=Darwin
|
||||
SHA256SUM_BIN='shasum -a 256'
|
||||
elif [[ $runner_os =~ darwin|macos|osx ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
CSC_KEYCHAIN=signing_temp
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
ELECTRON_BUILDER_OS='--mac'
|
||||
TARGETS="$(yq e .mac.target[] electron-builder.yml)"
|
||||
|
||||
elif [[ "${RUNNER_OS}" == Windows ]]; 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}"
|
||||
elif [[ $runner_os =~ windows|win ]]; then
|
||||
ARCHITECTURE_FLAGS="--ia32 ${ARCHITECTURE_FLAGS}"
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
ELECTRON_BUILDER_OS='--win'
|
||||
TARGETS="$(yq e .win.target[] electron-builder.yml)"
|
||||
|
||||
else
|
||||
echo "ERROR: unexpected runner OS: ${RUNNER_OS}"
|
||||
exit 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Currently, we can only build for the host architecture.
|
||||
npx electron-forge make
|
||||
npm link electron-builder
|
||||
|
||||
for target in ${TARGETS}; do
|
||||
electron-builder ${ELECTRON_BUILDER_OS} ${ARCHITECTURE_FLAGS} \
|
||||
--c.extraMetadata.analytics.sentry.token='${{ steps.sentry.outputs.dsn }}' \
|
||||
--c.extraMetadata.packageType="${target}"
|
||||
|
||||
find dist -type f -maxdepth 1
|
||||
done
|
||||
|
||||
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:
|
||||
# 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 }}
|
||||
# Apple notarization (afterSignHook.js)
|
||||
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
|
||||
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||
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
|
||||
# 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
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
# 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\" {} \;
|
||||
|
||||
# https://github.com/softprops/action-gh-release#-customizing
|
||||
- name: Create draft GitHub (pre)release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: dist
|
||||
# use PR branch name for draft releases
|
||||
name: ${{ github.event.pull_request.head.ref }}
|
||||
tag_name: ${{ github.event.pull_request.head.ref }}
|
||||
draft: true
|
||||
generate_release_notes: false
|
||||
prerelease: true
|
||||
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||
files: |
|
||||
dist/*.AppImage
|
||||
dist/*.blockmap
|
||||
dist/*.deb
|
||||
dist/*.dmg
|
||||
dist/*.exe
|
||||
dist/*.rpm
|
||||
dist/*.zip
|
||||
dist/latest*.yml
|
||||
|
||||
- name: Compress custom source
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Compress custom source
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: tar -acf ${{ runner.temp }}\custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
75
.github/actions/test/action.yml
vendored
75
.github/actions/test/action.yml
vendored
@ -3,24 +3,36 @@ name: test release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: 'JSON stringified object containing all the inputs from the calling workflow'
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: 'JSON stringified object containing all the secrets from the calling workflow'
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.10'
|
||||
default: "14.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: 'true'
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: 'composite'
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Delete previous draft release
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
gh release delete --yes '${{ github.event.pull_request.head.ref }}' || true
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
|
||||
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
@ -28,60 +40,37 @@ runs:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install host dependencies
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
|
||||
- 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'
|
||||
|
||||
- name: Test release
|
||||
shell: bash
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
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
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
npm run flowzone-preinstall-${runner_os}
|
||||
npm ci
|
||||
|
||||
# as the shrinkwrap might have been done on mac/linux, this is ensure the package is there for windows
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
npm i -D winusb-driver-generator
|
||||
fi
|
||||
|
||||
npm run lint
|
||||
npm run package
|
||||
npm run wdio # test stage, note that it requires the package to be done first
|
||||
npm run build
|
||||
npm run test-${runner_os}
|
||||
|
||||
env:
|
||||
# https://www.electronjs.org/docs/latest/api/environment-variables
|
||||
ELECTRON_NO_ATTACH_CONSOLE: 'true'
|
||||
ELECTRON_NO_ATTACH_CONSOLE: true
|
||||
|
||||
- name: Compress custom source
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
shell: bash --noprofile --norc -eo pipefail -x {0}
|
||||
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 .
|
||||
shell: powershell
|
||||
run: tar -acf ${{ runner.temp }}\custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
||||
|
37
.github/workflows/flowzone.yml
vendored
37
.github/workflows/flowzone.yml
vendored
@ -1,41 +1,16 @@
|
||||
name: Flowzone
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
# allow external contributions to use secrets within trusted code
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
branches:
|
||||
- "main"
|
||||
- "master"
|
||||
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||
# prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for
|
||||
# internal or external contributions respectively
|
||||
if: |
|
||||
(github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') ||
|
||||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
|
||||
secrets: inherit
|
||||
with:
|
||||
custom_test_matrix: >
|
||||
{
|
||||
"os": [
|
||||
["ubuntu-22.04"],
|
||||
["windows-2019"],
|
||||
["macos-13"],
|
||||
["macos-latest-xlarge"]
|
||||
]
|
||||
}
|
||||
custom_publish_matrix: >
|
||||
{
|
||||
"os": [
|
||||
["ubuntu-22.04"],
|
||||
["windows-2019"],
|
||||
["macos-13"],
|
||||
["macos-latest-xlarge"]
|
||||
]
|
||||
}
|
||||
restrict_custom_actions: false
|
||||
github_prerelease: true
|
||||
cloudflare_website: "etcher"
|
||||
tests_run_on: '["ubuntu-latest","macos-latest","windows-2019"]'
|
||||
|
14
.github/workflows/winget.yml
vendored
14
.github/workflows/winget.yml
vendored
@ -1,14 +0,0 @@
|
||||
name: Publish to WinGet
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest # action can only be run on windows
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: Balena.Etcher
|
||||
# matches something like "balenaEtcher-1.19.0.Setup.exe"
|
||||
installers-regex: 'balenaEtcher-[\d.-]+\.Setup.exe$'
|
||||
token: ${{ secrets.WINGET_PAT }}
|
112
.gitignore
vendored
112
.gitignore
vendored
@ -1,103 +1,40 @@
|
||||
|
||||
# -- 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
|
||||
/lib-cov
|
||||
|
||||
# Image stream output directory
|
||||
/tests/image-stream/output
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
/coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
/build
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
# Generated files
|
||||
/generated
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
# Dependency directory
|
||||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# 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/
|
||||
# Compiled Etcher releases
|
||||
/dist
|
||||
|
||||
# Certificates
|
||||
*.spc
|
||||
@ -107,17 +44,16 @@ dist/
|
||||
*.crt
|
||||
*.pem
|
||||
|
||||
# Secrets
|
||||
# OSX files
|
||||
|
||||
.DS_Store
|
||||
|
||||
# VSCode files
|
||||
|
||||
.vscode
|
||||
.gitsecret/keys/random_seed
|
||||
!*.secret
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING.pfx
|
||||
|
||||
# Image stream output directory
|
||||
/tests/image-stream/output
|
||||
|
||||
#local development
|
||||
.yalc
|
||||
yalc.lock
|
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[submodule "scripts/resin"]
|
||||
path = scripts/resin
|
||||
url = https://github.com/balena-io/scripts.git
|
||||
branch = master
|
@ -1,6 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "node_modules", "@balena", "lint", "config", ".prettierrc"), "utf8"),
|
||||
);
|
File diff suppressed because it is too large
Load Diff
734
CHANGELOG.md
734
CHANGELOG.md
@ -3,740 +3,6 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# v2.1.0
|
||||
## (2025-02-27)
|
||||
|
||||
* Add informational notice about how to disable analytics collection [myarmolinsky]
|
||||
|
||||
# v2.0.0
|
||||
## (2025-02-20)
|
||||
|
||||
* major: build on ubuntu 22 and macos 13 [Edwin Joassart]
|
||||
|
||||
# v1.19.25
|
||||
## (2024-10-10)
|
||||
|
||||
* patch: bump etcher-sdk to 9.1.2 [Edwin Joassart]
|
||||
|
||||
# v1.19.24
|
||||
## (2024-10-09)
|
||||
|
||||
* patch: etcher-util is corrupted in RPM package [Richard Glidden]
|
||||
|
||||
# v1.19.23
|
||||
## (2024-10-09)
|
||||
|
||||
* patch: remove gconf2 libgconf-2-4 deps [Marc-Aurèle Brothier]
|
||||
|
||||
# v1.19.22
|
||||
## (2024-07-18)
|
||||
|
||||
* Replace deprecated Flowzone inputs [Kyle Harding]
|
||||
|
||||
# v1.19.21
|
||||
## (2024-05-30)
|
||||
|
||||
* patch: fix missing windows dependency [Edwin Joassart]
|
||||
* patch: fix missing windows dependency [Edwin Joassart]
|
||||
* patch: fix missing windows dependency [Edwin Joassart]
|
||||
|
||||
# v1.19.20
|
||||
## (2024-05-30)
|
||||
|
||||
* patch: fix missing windows dependency [Edwin Joassart]
|
||||
|
||||
# v1.19.19
|
||||
## (2024-05-28)
|
||||
|
||||
* patch: add sentry debug flag [Edwin Joassart]
|
||||
|
||||
# v1.19.18
|
||||
## (2024-05-22)
|
||||
|
||||
* patch: fix Sentry DSN for main process [Edwin Joassart]
|
||||
|
||||
# v1.19.17
|
||||
## (2024-05-09)
|
||||
|
||||
* patch: fix injection of analytics key at build time [JOASSART Edwin]
|
||||
|
||||
# v1.19.16
|
||||
## (2024-04-26)
|
||||
|
||||
* patch: hold request for metadata while waiting for flasher [Edwin Joassart]
|
||||
|
||||
# v1.19.15
|
||||
## (2024-04-26)
|
||||
|
||||
* patch: bump etcher-sdk to 9.0.11 to fix url loading using http/2 [Edwin Joassart]
|
||||
|
||||
# v1.19.14
|
||||
## (2024-04-25)
|
||||
|
||||
* patch: pretty-bytes to 6.1.1 [JOASSART Edwin]
|
||||
|
||||
# v1.19.13
|
||||
## (2024-04-25)
|
||||
|
||||
* patch: use etcher icon as loading for windows installer [Edwin Joassart]
|
||||
* patch: fix windows squirrel install [Edwin Joassart]
|
||||
|
||||
# v1.19.12
|
||||
## (2024-04-25)
|
||||
|
||||
* patch: bump minors & patch [Edwin Joassart]
|
||||
* patch: bump @electron-forge/* to 7.4.0 [Edwin Joassart]
|
||||
* patch: bump electron to 30.0.1 & @electron/remote to 2.1.2 [Edwin Joassart]
|
||||
* patch: npm upgrade [Edwin Joassart]
|
||||
* patch: bump @balena/lint to 8.0.2 and fix formating [Edwin Joassart]
|
||||
* patch: fix pretty-bytes imports [Edwin Joassart]
|
||||
* patch: bump etcher-sdk to 9.0.9 [Edwin Joassart]
|
||||
|
||||
# v1.19.11
|
||||
## (2024-04-25)
|
||||
|
||||
* patch: setup wdio and port (most) tests [Edwin Joassart]
|
||||
|
||||
# v1.19.10
|
||||
## (2024-04-23)
|
||||
|
||||
* patch: remove node-ipc and tests [Edwin Joassart]
|
||||
* patch: switch api; use ws; integrate sudo-prompt - switch api roles flow - use websocket instead of node-ipc - integrate; modernize; simplify and deprecate sudo-prompt [Edwin Joassart]
|
||||
* patch: refactor api to use a single topic [Edwin Joassart]
|
||||
* patch: set require node engine to 20 [Edwin Joassart]
|
||||
|
||||
# v1.19.9
|
||||
## (2024-04-22)
|
||||
|
||||
* patch: prevent rebuild of native deps by @electron/rebuild [Edwin Joassart]
|
||||
|
||||
# v1.19.8
|
||||
## (2024-04-22)
|
||||
|
||||
* patch: replace deprecated pkg with yao-pkg and bump etcher-util node v to 20.10 [Edwin Joassart]
|
||||
|
||||
# v1.19.7
|
||||
## (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)
|
||||
|
||||
* add-flash-with-etcher-to-docs [Lizzie Epton]
|
||||
|
||||
# v1.18.5
|
||||
## (2023-03-09)
|
||||
|
||||
* patch: add apt-get update in flowzone preinstall [Edwin Joassart]
|
||||
|
||||
# v1.18.4
|
||||
## (2023-03-02)
|
||||
|
||||
* patch: bump etcher-sdk to 8.3.1 [JOASSART Edwin]
|
||||
|
||||
# v1.18.3
|
||||
## (2023-02-22)
|
||||
|
||||
* fix-typo [Lizzie Epton]
|
||||
* edits-to-info-about-efp [Lizzie Epton]
|
||||
* Add reference to etcher-efp in publishing.md [Edwin Joassart]
|
||||
|
||||
# v1.18.2
|
||||
## (2023-02-21)
|
||||
|
||||
* patch: organize docs [mcraa]
|
||||
* patch: actualized develop guide [mcraa]
|
||||
* patch: updated commit message guide [mcraa]
|
||||
* add-item-from-FAQs [Lizzie Epton]
|
||||
* patch: removed gt characters from contributing guide [mcraa]
|
||||
* patch: added docosaurus site name [mcraa]
|
||||
|
||||
# v1.18.1
|
||||
## (2023-02-15)
|
||||
|
||||
* patch: use @electron/remote for locating rpiboot files [mcraa]
|
||||
|
||||
# v1.18.0
|
||||
## (2023-02-14)
|
||||
|
||||
* Update to Electron 19 [Akis Kesoglou]
|
||||
* Remove Spectron and related (low-value) tests [Akis Kesoglou]
|
||||
|
||||
# v1.17.0
|
||||
## (2023-02-14)
|
||||
|
||||
* Update to Electron 17 and Node 16 [Akis Kesoglou]
|
||||
|
||||
# v1.16.0
|
||||
## (2023-02-14)
|
||||
|
||||
* Update to Electron 14 [Akis Kesoglou]
|
||||
|
||||
# v1.15.6
|
||||
## (2023-02-13)
|
||||
|
||||
* patch: app: i18n: Translation: Update zh-TW strings * Improve translate. * Sync layout with English strings ts file. [Edward Wu]
|
||||
|
||||
# v1.15.5
|
||||
## (2023-02-03)
|
||||
|
||||
* revert auto-update feature [JOASSART Edwin]
|
||||
|
||||
# v1.15.4
|
||||
## (2023-02-02)
|
||||
|
||||
* Switch to `@electron/remote` [Akis Kesoglou]
|
||||
|
||||
# v1.15.3
|
||||
## (2023-02-02)
|
||||
|
||||
* move EFP & success-banner to efp.balena.io [Edwin Joassart]
|
||||
|
||||
# v1.15.2
|
||||
## (2023-02-02)
|
||||
|
||||
* Remove configuration remote update [Edwin Joassart]
|
||||
|
||||
# v1.15.1
|
||||
## (2023-02-01)
|
||||
|
||||
* Remove redundant resinci-deploy build step [Akis Kesoglou]
|
||||
* Lazily import Electron from child-writer process [Akis Kesoglou]
|
||||
|
||||
# v1.15.0
|
||||
## (2023-01-27)
|
||||
|
||||
* Add support for Node 18 [Akis Kesoglou]
|
||||
|
||||
# v1.14.3
|
||||
## (2023-01-19)
|
||||
|
||||
* patch: fixed mac sudo on other languages [Peter Makra]
|
||||
|
||||
# v1.14.2
|
||||
## (2023-01-17)
|
||||
|
||||
* patch: revert to lockfile v1 [Peter Makra]
|
||||
* patch: update etcher-sdk for cm4v5 [builder555]
|
||||
|
||||
# v1.14.1
|
||||
## (2023-01-16)
|
||||
|
||||
* fix disabled-screensaver unhandled exception outside balena-electron env [Edwin Joassart]
|
||||
|
||||
# v1.14.0
|
||||
## (2023-01-16)
|
||||
|
||||
* Anonymizes all paths before sending [Otávio Jacobi]
|
||||
* patch: Sentry fix path [Edwin Joassart]
|
||||
* Remove personal path on etcher [Otávio Jacobi]
|
||||
* Unifying sentry reports in a single project [Edwin Joassart]
|
||||
* Removes corvus in favor of sentry and analytics client [Otávio Jacobi]
|
||||
* Removes corvus in favor of sentry and analytics client [Otávio Jacobi]
|
||||
|
||||
# v1.13.4
|
||||
## (2023-01-12)
|
||||
|
||||
* Adding EtcherPro device serial number to the Settings modal [Aurelien VALADE]
|
||||
|
||||
# v1.13.3
|
||||
## (2023-01-11)
|
||||
|
||||
* patch: progress cm4 to second stage [Peter Makra]
|
||||
|
||||
# v1.13.2
|
||||
## (2023-01-02)
|
||||
|
||||
* patch: fixed winget parameter name [mcraa]
|
||||
|
||||
# v1.13.1
|
||||
## (2023-01-02)
|
||||
|
||||
* patch: updated sdk to fix bz2 issue [Peter Makra]
|
||||
* patch: update copyright in electron-builder [JOASSART Edwin]
|
||||
|
||||
# v1.13.0
|
||||
## (2022-12-28)
|
||||
|
||||
* minor: electron version bump [Peter Makra]
|
||||
* patch: handle ext2fs with webpack [Peter Makra]
|
||||
* Patch: update etcher-sdk version to fix CM4 issues [builder555]
|
||||
|
||||
# v1.12.7
|
||||
## (2022-12-20)
|
||||
|
||||
* Update dependency i18next to 21.10.0 [Renovate Bot]
|
||||
|
||||
# v1.12.6
|
||||
## (2022-12-20)
|
||||
|
||||
* Update dependency react-i18next to 11.18.6 [Renovate Bot]
|
||||
|
||||
# v1.12.5
|
||||
## (2022-12-20)
|
||||
|
||||
* Patch: made trim setting more readable [builder555]
|
||||
|
||||
# v1.12.4
|
||||
## (2022-12-19)
|
||||
|
||||
* patch: publish to winget with gh action [Begula]
|
||||
|
||||
# v1.12.3
|
||||
## (2022-12-19)
|
||||
|
||||
* Patch: replaced plain text with i18n in settings [builder555]
|
||||
|
||||
# v1.12.2
|
||||
## (2022-12-16)
|
||||
|
||||
* Update dependency webpack-dev-server to 4.11.1 [Renovate Bot]
|
||||
|
||||
# v1.12.1
|
||||
## (2022-12-16)
|
||||
|
||||
* Patch: expose trim ext{2,3,4} setting [builder555]
|
||||
|
||||
# v1.12.0
|
||||
## (2022-12-14)
|
||||
|
||||
* i18n support and Chinese translation [ab77]
|
||||
* minor: optimize i18n [r-q]
|
||||
|
||||
# v1.11.10
|
||||
## (2022-12-13)
|
||||
|
||||
* Update dependency webpack-cli to 4.10.0 [Renovate Bot]
|
||||
|
||||
# v1.11.9
|
||||
## (2022-12-12)
|
||||
|
||||
* Update dependency webpack to 5.75.0 [Renovate Bot]
|
||||
|
||||
# v1.11.8
|
||||
## (2022-12-12)
|
||||
|
||||
* Update dependency awscli to 1.27.28 [Renovate Bot]
|
||||
|
||||
# v1.11.7
|
||||
## (2022-12-12)
|
||||
|
||||
* Update dependency uuid to 8.3.2 [Renovate Bot]
|
||||
|
||||
# v1.11.6
|
||||
## (2022-12-12)
|
||||
|
||||
* Update dependency tslib to 2.4.1 [Renovate Bot]
|
||||
* Patch: run linux build on ubuntu-20.04 [Edwin Joassart]
|
||||
|
||||
# v1.11.5
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency ts-loader to 8.4.0 [Renovate Bot]
|
||||
|
||||
# v1.11.4
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency styled-components to 5.3.6 [Renovate Bot]
|
||||
|
||||
# v1.11.3
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency terser-webpack-plugin to 5.3.6 [Renovate Bot]
|
||||
|
||||
# v1.11.2
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency string-replace-loader to 3.1.0 [Renovate Bot]
|
||||
|
||||
# v1.11.1
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency sinon to 9.2.4 [Renovate Bot]
|
||||
|
||||
# v1.11.0
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency shyaml to 0.6.2 [Renovate Bot]
|
||||
|
||||
# v1.10.29
|
||||
## (2022-12-10)
|
||||
|
||||
* Update dependency awscli to 1.27.27 [Renovate Bot]
|
||||
|
||||
# v1.10.28
|
||||
## (2022-12-10)
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependency rendition to 19.3.2 [Renovate Bot] </summary>
|
||||
|
||||
> ## rendition-19.3.2
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Add Breadcrumbs component export [JSReds]
|
||||
>
|
||||
> ## rendition-19.3.1
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Fix max-width on breadcrumbs container [JSReds]
|
||||
>
|
||||
> ## rendition-19.3.0
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Add Breadcrumbs component [JSReds]
|
||||
>
|
||||
|
||||
</details>
|
||||
|
||||
# v1.10.27
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency redux to 4.2.0 [Renovate Bot]
|
||||
|
||||
# v1.10.26
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency pretty-bytes to 5.6.0 [Renovate Bot]
|
||||
|
||||
# v1.10.25
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency pnp-webpack-plugin to 1.7.0 [Renovate Bot]
|
||||
|
||||
# v1.10.24
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency node-ipc to 9.2.1 [Renovate Bot]
|
||||
|
||||
# v1.10.23
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency mocha to 8.4.0 [Renovate Bot]
|
||||
|
||||
# v1.10.22
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency mini-css-extract-plugin to 1.6.2 [Renovate Bot]
|
||||
|
||||
# v1.10.21
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency lint-staged to 10.5.4 [Renovate Bot]
|
||||
|
||||
# v1.10.20
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency husky to 4.3.8 [Renovate Bot]
|
||||
|
||||
# v1.10.19
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency esbuild-loader to 2.20.0 [Renovate Bot]
|
||||
|
||||
# v1.10.18
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency electron-updater to 4.6.5 [Renovate Bot]
|
||||
|
||||
# v1.10.17
|
||||
## (2022-12-09)
|
||||
|
||||
* Update dependency electron-notarize to 1.2.2 [Renovate Bot]
|
||||
|
||||
# v1.10.16
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency awscli to 1.27.26 [Renovate Bot]
|
||||
|
||||
# v1.10.15
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency electron-builder to 22.14.13 [Renovate Bot]
|
||||
|
||||
# v1.10.14
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency debug to 4.3.4 [Renovate Bot]
|
||||
|
||||
# v1.10.13
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency awscli to 1.27.25 [Renovate Bot]
|
||||
|
||||
# v1.10.12
|
||||
## (2022-12-08)
|
||||
|
||||
* Update dependency css-loader to 5.2.7 [Renovate Bot]
|
||||
|
||||
# v1.10.11
|
||||
## (2022-12-07)
|
||||
|
||||
* Update dependency awscli to 1.27.24 [Renovate Bot]
|
||||
|
||||
# v1.10.10
|
||||
## (2022-12-07)
|
||||
|
||||
* Update dependency @types/node to 14.18.34 [Renovate Bot]
|
||||
|
||||
# v1.10.9
|
||||
## (2022-12-06)
|
||||
|
||||
* Enable repository configuration [ab77]
|
||||
|
||||
# v1.10.8
|
||||
## (2022-12-05)
|
||||
|
||||
* Update dependency chai to 4.3.7 [Renovate Bot]
|
||||
|
||||
# v1.10.7
|
||||
## (2022-12-05)
|
||||
|
||||
* Use core workflow for GitHub publish [ab77]
|
||||
|
||||
# v1.10.6
|
||||
## (2022-12-02)
|
||||
|
||||
* Dummy update to fix asset version issue [Edwin Joassart]
|
||||
|
||||
# v1.10.5
|
||||
## (2022-12-02)
|
||||
|
||||
* Patch: run linux build on ubuntu-18.04 [Edwin Joassart]
|
||||
|
||||
# v1.10.4
|
||||
## (2022-12-01)
|
||||
|
||||
* patch: remove Homebrew instructions in README [Patrick Linnane]
|
||||
|
||||
# v1.10.3
|
||||
## (2022-12-01)
|
||||
|
||||
* Allow external contributors [ab77]
|
||||
|
||||
# v1.10.2
|
||||
## (2022-11-25)
|
||||
|
||||
* Fix missing analytics token [Edwin Joassart]
|
||||
|
||||
# v1.10.1
|
||||
## (2022-11-21)
|
||||
|
||||
* Fixing call to electron block screensaver methods invocation [Aurelien VALADE]
|
||||
|
||||
# v1.10.0
|
||||
## (2022-11-10)
|
||||
|
||||
* testing renovate [builder555]
|
||||
|
||||
# v1.9.0
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency awscli to 1.27.5 [Renovate Bot]
|
||||
|
||||
# v1.8.17
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/react-dom to 16.9.17 [Renovate Bot]
|
||||
|
||||
# v1.8.16
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/react to 16.14.34 [Renovate Bot]
|
||||
|
||||
# v1.8.15
|
||||
## (2022-11-08)
|
||||
|
||||
* CI: generalise artefact handling [ab77]
|
||||
|
||||
# v1.8.14
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/node to 14.18.33 [Renovate Bot]
|
||||
|
||||
# v1.8.13
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/copy-webpack-plugin to 6.4.3 [Renovate Bot]
|
||||
|
||||
# v1.8.12
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @fortawesome/fontawesome-free to 5.15.4 [Renovate Bot]
|
||||
|
||||
# v1.8.11
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @balena/lint to 5.4.2 [Renovate Bot]
|
||||
|
||||
# v1.8.10
|
||||
## (2022-11-08)
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update dependency sys-class-rgb-led to 3.0.1 [Renovate Bot] </summary>
|
||||
|
||||
> ## sys-class-rgb-led-3.0.1
|
||||
> ### (2021-07-01)
|
||||
>
|
||||
> * patch: Delete Codeowners [Vipul Gupta]
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.8.9
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency semver to 7.3.8 [Renovate Bot]
|
||||
|
||||
# v1.8.8
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency omit-deep-lodash to 1.1.7 [Renovate Bot]
|
||||
|
||||
# v1.8.7
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency immutable to 3.8.2 [Renovate Bot]
|
||||
|
||||
# v1.8.6
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency electron-rebuild to 3.2.9 [Renovate Bot]
|
||||
|
||||
# v1.8.5
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency electron-mocha to 9.3.3 [Renovate Bot]
|
||||
|
||||
# v1.8.4
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/webpack-node-externals to 2.5.3 [Renovate Bot]
|
||||
|
||||
# v1.8.3
|
||||
## (2022-11-08)
|
||||
|
||||
* Update dependency @types/tmp to 0.2.3 [Renovate Bot]
|
||||
|
||||
# v1.8.2
|
||||
## (2022-11-08)
|
||||
|
||||
* Generate release notes with git [ab77]
|
||||
|
||||
# v1.8.1
|
||||
## (2022-11-07)
|
||||
|
||||
* Update dependency @types/mime-types to 2.1.1 [Renovate Bot]
|
||||
|
||||
# v1.8.0
|
||||
## (2022-11-07)
|
||||
|
||||
* Update scripts/resin digest to 652fdd4 [Renovate Bot]
|
||||
|
||||
# v1.7.15
|
||||
## (2022-11-07)
|
||||
|
||||
* Build targets individually [ab77]
|
||||
|
||||
# v1.7.14
|
||||
## (2022-11-07)
|
||||
|
||||
* Update dependency lodash to 4.17.21 [SECURITY] [Renovate Bot]
|
||||
|
||||
# v1.7.13
|
||||
## (2022-11-07)
|
||||
|
||||
* Update release notes on finalize [ab77]
|
||||
|
||||
# v1.7.12
|
||||
## (2022-11-07)
|
||||
|
||||
* Avoid duplicate releases [ab77]
|
||||
|
||||
# v1.7.11
|
||||
## (2022-11-07)
|
||||
|
||||
|
@ -44,9 +44,3 @@ Etcher requires an available [polkit authentication agent](https://wiki.archlinu
|
||||
## May I run Etcher in older macOS versions?
|
||||
|
||||
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||
|
||||
## Can I use the Flash With Etcher button on my site?
|
||||
|
||||
You can use the Flash with Etcher button on your site or blog, if you have an OS that you want your users to be able to easily flash using Etcher, add the following code where you want to button to be:
|
||||
|
||||
`<a href="https://efp.balena.io/open-image-url?imageUrl=<your image URL>"><img src="http://balena.io/flash-with-etcher.png" /></a>`
|
152
Makefile
Normal file
152
Makefile
Normal file
@ -0,0 +1,152 @@
|
||||
# ---------------------------------------------------------------------
|
||||
# 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
|
138
README.md
138
README.md
@ -17,9 +17,13 @@ was written correctly, and much more. It can also directly flash Raspberry Pi de
|
||||
|
||||
## Supported Operating Systems
|
||||
|
||||
- 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.
|
||||
- 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].
|
||||
|
||||
## Installers
|
||||
|
||||
@ -28,32 +32,128 @@ installers for all supported operating systems.
|
||||
|
||||
## Packages
|
||||
|
||||
> [](https://cloudsmith.com) \
|
||||
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
|
||||
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
|
||||
enables your organization to create, store and share packages in any format, to any place, with total
|
||||
confidence.
|
||||
|
||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
Package for Debian and Ubuntu can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
|
||||
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
|
||||
|
||||
##### Install .deb file using apt
|
||||
1. Add Etcher Debian repository:
|
||||
|
||||
```sh
|
||||
sudo apt install ./balena-etcher_******_amd64.deb
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo apt remove balena-etcher
|
||||
```
|
||||
```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
|
||||
```
|
||||
|
||||
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
##### Yum
|
||||
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
|
||||
|
||||
Package for Fedora-based and Redhat can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
|
||||
|
||||
1. Install using yum
|
||||
##### DNF
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum localinstall balena-etcher-***.x86_64.rpm
|
||||
rm /etc/yum.repos.d/balena-etcher.repo
|
||||
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||
```
|
||||
|
||||
##### Yum
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
rm /etc/yum.repos.d/balena-etcher.repo
|
||||
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||
```
|
||||
|
||||
#### OpenSUSE LEAP & Tumbleweed install (zypper)
|
||||
|
||||
1. Add the repo
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
2. Update and install
|
||||
|
||||
```sh
|
||||
sudo zypper up
|
||||
sudo zypper install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
#### Arch/Manjaro Linux (GNU/Linux x64)
|
||||
@ -70,18 +170,20 @@ yay -S balena-etcher
|
||||
yay -R balena-etcher
|
||||
```
|
||||
|
||||
#### WinGet (Windows)
|
||||
#### Brew (macOS)
|
||||
|
||||
This package is updated by [gh-action](https://github.com/vedantmgoyal2009/winget-releaser), and is kept up to date automatically.
|
||||
**Note**: Etcher has to be updated manually to point to new versions,
|
||||
so it might not refer to the latest version immediately after an Etcher
|
||||
release.
|
||||
|
||||
```sh
|
||||
winget install balenaEtcher #or Balena.Etcher
|
||||
brew install balenaetcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
winget uninstall balenaEtcher
|
||||
brew uninstall balenaetcher
|
||||
```
|
||||
|
||||
#### Chocolatey (Windows)
|
||||
@ -112,7 +214,7 @@ 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/docs/SUPPORT.md
|
||||
[support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[milestones]: https://github.com/balena-io/etcher/milestones
|
||||
|
31
afterPack.js
Normal file
31
afterPack.js
Normal file
@ -0,0 +1,31 @@
|
||||
'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])
|
||||
}
|
25
afterSignHook.js
Normal file
25
afterSignHook.js
Normal file
@ -0,0 +1,25 @@
|
||||
'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
|
4
dev-app-update.yml
Normal file
4
dev-app-update.yml
Normal file
@ -0,0 +1,4 @@
|
||||
owner: balena-io
|
||||
repo: etcher
|
||||
provider: github
|
||||
updaterCacheDirName: balena-etcher-updater
|
9
dictionary.txt
Normal file
9
dictionary.txt
Normal file
@ -0,0 +1,9 @@
|
||||
boolen->boolean
|
||||
aknowledge->acknowledge
|
||||
seleted->selected
|
||||
reming->remind
|
||||
locl->local
|
||||
subsribe->subscribe
|
||||
unsubsribe->unsubscribe
|
||||
calcluate->calculate
|
||||
dictionaty->dictionary
|
@ -12,29 +12,67 @@ over the commit history.
|
||||
- Be able to automatically reference relevant changes from a dependency
|
||||
upgrade.
|
||||
|
||||
The guidelines are inspired by the [AngularJS git commit
|
||||
guidelines][angular-commit-guidelines].
|
||||
|
||||
Commit structure
|
||||
----------------
|
||||
|
||||
Each commit message needs to specify the semver-type. Which can be `patch|minor|major`.
|
||||
See the [Semantic Versioning][semver] specification for a more detailed explanation of the meaning of these types.
|
||||
See balena commit guidelines for more info about the whole commit structure.
|
||||
Each commit message consists of a header, a body and a footer. The header has a
|
||||
special format that includes a type, a scope and a subject.
|
||||
|
||||
```
|
||||
<semver-type>: <subject>
|
||||
```
|
||||
or
|
||||
```
|
||||
<subject>
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<details>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
Change-Type: <semver-type>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The subject should not contain more than 70 characters, including the type and
|
||||
scope, and the body should be wrapped at 72 characters.
|
||||
|
||||
Type
|
||||
----
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- `feat`: A new feature.
|
||||
- `fix`: A bug fix.
|
||||
- `minifix`: A minimal fix that doesn't warrant an entry in the CHANGELOG.
|
||||
- `docs`: Documentation only changes.
|
||||
- `style`: Changes that do not affect the meaning of the code (white-space,
|
||||
formatting, missing semi-colons, JSDoc annotations, comments, etc).
|
||||
- `refactor`: A code change that neither fixes a bug nor adds a feature.
|
||||
- `perf`: A code change that improves performance.
|
||||
- `test`: Adding missing tests.
|
||||
- `chore`: Changes to the build process or auxiliary tools and libraries.
|
||||
- `upgrade`: A version upgrade of a project dependency.
|
||||
|
||||
Scope
|
||||
-----
|
||||
|
||||
The scope is required for types that make sense, such as `feat`, `fix`,
|
||||
`test`, etc. Certain commit types, such as `chore` might not have a clearly
|
||||
defined scope, in which case its better to omit it.
|
||||
|
||||
Subject
|
||||
-------
|
||||
|
||||
The subject should contain a short description of the change:
|
||||
|
||||
- Use the imperative, present tense.
|
||||
- Don't capitalize the first letter.
|
||||
- No dot (.) at the end.
|
||||
|
||||
Footer
|
||||
------
|
||||
|
||||
The footer contains extra information about the commit, such as tags.
|
||||
|
||||
**Breaking Changes** should start with the word BREAKING CHANGE: with a space
|
||||
or two newlines. The rest of the commit message is then used for this.
|
||||
|
||||
Tags
|
||||
----
|
||||
|
||||
@ -83,4 +121,125 @@ Closes: https://github.com/balena-io/etcher/issues/XXX
|
||||
Fixes: https://github.com/balena-io/etcher/issues/XXX
|
||||
```
|
||||
|
||||
### `Change-Type: <type>`
|
||||
|
||||
This tag is used to determine the change type that a commit introduces. The
|
||||
following types are supported:
|
||||
|
||||
- `major`
|
||||
- `minor`
|
||||
- `patch`
|
||||
|
||||
This tag can be omitted for commits that don't change the application from the
|
||||
user's point of view, such as for refactoring commits.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
Change-Type: major
|
||||
Change-Type: minor
|
||||
Change-Type: patch
|
||||
```
|
||||
|
||||
See the [Semantic Versioning][semver] specification for a more detailed
|
||||
explanation of the meaning of these types.
|
||||
|
||||
### `Changelog-Entry: <message>`
|
||||
|
||||
This tag is used to describe the changes introduced by the commit in a more
|
||||
human style that would fit the `CHANGELOG.md` better.
|
||||
|
||||
If the commit type is either `fix` or `feat`, the commit will take part in the
|
||||
CHANGELOG. If this tag is not defined, then the commit subject will be used
|
||||
instead.
|
||||
|
||||
You explicitly can use this tag to make a commit whose type is not `fix` nor
|
||||
`feat` appear in the `CHANGELOG.md`.
|
||||
|
||||
Since whatever your write here will be shown *as it is* in the `CHANGELOG.md`,
|
||||
take some time to write a decent entry. Consider the following guidelines:
|
||||
|
||||
- Use the imperative, present tense.
|
||||
- Capitalize the first letter.
|
||||
|
||||
There is no fixed length limit for the contents of this tag, but always strive
|
||||
to make as short as possible without compromising its quality.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
Changelog-Entry: Fix EPERM errors when flashing to a GPT drive.
|
||||
```
|
||||
|
||||
Complete examples
|
||||
-----------------
|
||||
|
||||
```
|
||||
fix(GUI): ignore extensions before the first non-compressed extension
|
||||
|
||||
Currently, we extract all the extensions from an image path and report back
|
||||
that the image is invalid if *any* of the extensions is not valid , however
|
||||
this can cause trouble with images including information between dots that are
|
||||
not strictly extensions, for example:
|
||||
|
||||
elementaryos-0.3.2-stable-i386.20151209.iso
|
||||
|
||||
Etcher will consider `20151209` to be an invalid extension and therefore
|
||||
will prevent such image from being selected at all.
|
||||
|
||||
As a way to allow these corner cases but still make use of our enforced check
|
||||
controls, the validation routine now only consider extensions starting from the
|
||||
first non compressed extension.
|
||||
|
||||
Change-Type: patch
|
||||
Changelog-Entry: Don't interpret image file name information between dots as image extensions.
|
||||
Fixes: https://github.com/balena-io/etcher/issues/492
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
```
|
||||
upgrade: etcher-image-write to v5.0.2
|
||||
|
||||
This version contains a fix to an `EPERM` issue happening to some Windows user,
|
||||
triggered by the `write` system call during the first ~5% of a flash given that
|
||||
the operating system still thinks the drive has a file system.
|
||||
|
||||
Change-Type: patch
|
||||
Changelog-Entry: Upgrade `etcher-image-write` to v5.0.2.
|
||||
Link: https://github.com/balena-io-modules/etcher-image-write/blob/master/CHANGELOG.md#502---2016-06-27
|
||||
Fixes: https://github.com/balena-io/etcher/issues/531
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
```
|
||||
feat(GUI): implement update notifier functionality
|
||||
|
||||
Auto-update functionality is not ready for usage. As a workaround to
|
||||
prevent users staying with older versions, we now check for updates at
|
||||
startup, and if the user is not running the latest version, we present a
|
||||
modal informing the user of the availiblity of a new version, and
|
||||
provide a call to action to open the Etcher website in his web browser.
|
||||
|
||||
Extra features:
|
||||
|
||||
- The user can skip the update, and tell the program to delay the
|
||||
notification for 7 days.
|
||||
|
||||
Misc changes:
|
||||
|
||||
- Center modal with flexbox, to allow more flexibility on the modal height.
|
||||
interacting with the S3 server.
|
||||
- Implement `ManifestBindService`, which now serves as a backend for the
|
||||
`manifest-bind` directive to allow the directive's functionality to be
|
||||
re-used by other services.
|
||||
- Namespace checkbox styles that are specific to the settings page.
|
||||
|
||||
Change-Type: minor
|
||||
Changelog-Entry: Check for updates and show a modal prompting the user to download the latest version.
|
||||
Closes: https://github.com/balena-io/etcher/issues/396
|
||||
```
|
||||
|
||||
[angular-commit-guidelines]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit
|
||||
[semver]: http://semver.org
|
||||
|
@ -17,11 +17,11 @@ Developing
|
||||
|
||||
#### Common
|
||||
|
||||
- [NodeJS](https://nodejs.org) (at least v16.11)
|
||||
- [Python 3](https://www.python.org)
|
||||
- [NodeJS](https://nodejs.org) (at least v6.11)
|
||||
- [Python 2.7](https://www.python.org)
|
||||
- [jq](https://stedolan.github.io/jq/)
|
||||
- [curl](https://curl.haxx.se/)
|
||||
- [npm](https://www.npmjs.com/)
|
||||
- [npm](https://www.npmjs.com/) (version 6.7)
|
||||
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@ -33,16 +33,16 @@ You might need to run this with `sudo` or administrator permissions.
|
||||
|
||||
- [NSIS v2.51](http://nsis.sourceforge.net/Main_Page) (v3.x won't work)
|
||||
- Either one of the following:
|
||||
- [Visual C++ 2019 Build Tools](https://visualstudio.microsoft.com/vs/features/cplusplus/) containing standalone compilers, libraries and scripts
|
||||
- The [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools#windows-build-tools) should be installed along with NodeJS
|
||||
- [Visual Studio Community 2019](https://visualstudio.microsoft.com/vs/) (free) (other editions, like Professional and Enterprise, should work too)
|
||||
**NOTE:** Visual Studio doesn't install C++ by default. You have to rerun the
|
||||
- [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools) containing standalone compilers, libraries and scripts
|
||||
- Install the [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) via npm with `npm install --global windows-build-tools`
|
||||
- [Visual Studio Community 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48146) (free) (other editions, like Professional and Enterprise, should work too)
|
||||
**NOTE:** Visual Studio 2015 doesn't install C++ by default. You have to rerun the
|
||||
setup, select "Modify" and then check `Visual C++ -> Common Tools for Visual
|
||||
C++` (see http://stackoverflow.com/a/31955339)
|
||||
C++ 2015` (see http://stackoverflow.com/a/31955339)
|
||||
- [MinGW](http://www.mingw.org)
|
||||
|
||||
You might need to `npm config set msvs_version 2019` for node-gyp to correctly detect
|
||||
the version of Visual Studio you're using (in this example VS2019).
|
||||
You might need to `npm config set msvs_version 2015` for node-gyp to correctly detect
|
||||
the version of Visual Studio you're using (in this example VS2015).
|
||||
|
||||
The following MinGW packages are required:
|
||||
|
||||
@ -61,7 +61,7 @@ as well.
|
||||
|
||||
#### Linux
|
||||
|
||||
- `libudev-dev` for libusb (for example install with `sudo apt install libudev-dev`, or on fedora `systemd-devel` contains the required package)
|
||||
- `libudev-dev` for libusb (install with `sudo apt install libudev-dev` for example)
|
||||
|
||||
### Cloning the project
|
||||
|
||||
@ -70,12 +70,29 @@ git clone --recursive https://github.com/balena-io/etcher
|
||||
cd etcher
|
||||
```
|
||||
|
||||
### Installing npm dependencies
|
||||
|
||||
**NOTE:** Please make use of the following command to install npm dependencies rather
|
||||
than simply running `npm install` given that we need to do extra configuration
|
||||
to make sure native dependencies are correctly compiled for Electron, otherwise
|
||||
the application might not run successfully.
|
||||
|
||||
If you're on Windows, **run the command from the _Developer Command Prompt for
|
||||
VS2015_**, to ensure all Visual Studio command utilities are available in the
|
||||
`%PATH%`.
|
||||
|
||||
```sh
|
||||
make electron-develop
|
||||
```
|
||||
|
||||
### Running the application
|
||||
|
||||
#### GUI
|
||||
|
||||
```sh
|
||||
# Build and start application
|
||||
# Build the GUI
|
||||
npm run webpack
|
||||
# Start Electron
|
||||
npm start
|
||||
```
|
||||
|
||||
@ -102,6 +119,11 @@ systems as they can before sending a pull request.
|
||||
*The test suite is run automatically by CI servers when you send a pull
|
||||
request.*
|
||||
|
||||
We also rely on various `make` targets to perform some common tasks:
|
||||
|
||||
- `make lint`: Run the linter.
|
||||
- `make sass`: Compile SCSS files.
|
||||
|
||||
We make use of [EditorConfig] to communicate indentation, line endings and
|
||||
other text editing default. We encourage you to install the relevant plugin in
|
||||
your text editor of choice to avoid having to fix any issues during the review
|
||||
@ -110,7 +132,19 @@ process.
|
||||
Updating a dependency
|
||||
---------------------
|
||||
|
||||
- Install new version of dependency using npm
|
||||
Given we use [npm shrinkwrap][shrinkwrap], we have to take extra steps to make
|
||||
sure the `npm-shrinkwrap.json` file gets updated correctly when we update a
|
||||
dependency.
|
||||
|
||||
Use the following steps to ensure everything goes flawlessly:
|
||||
|
||||
- Run `make electron-develop` to ensure you don't have extraneous dependencies
|
||||
you might have brought during development, or you are running older
|
||||
dependencies because you come from another branch or reference.
|
||||
|
||||
- Install the new version of the dependency. For example: `npm install --save
|
||||
<package>@<version>`. This will update the `npm-shrinkwrap.json` file.
|
||||
|
||||
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
|
||||
|
||||
Diffing Binaries
|
||||
|
@ -8,14 +8,10 @@ Releasing
|
||||
|
||||
### Release Types
|
||||
|
||||
- **draft**: A continues snapshot of current master, made by the CI services
|
||||
- **pre-release** (default): A continues snapshot of current master, made by the CI services
|
||||
- **release**: Full releases
|
||||
|
||||
Draft release is created from each PR, tagged with the branch name.
|
||||
All merged PR will generate a new tag/version as a *pre-release*.
|
||||
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
|
||||
- **snapshot** (default): A continues snapshot of current master, made by the CI services
|
||||
- **production**: Full releases
|
||||
|
||||
### Flight Plan
|
||||
|
||||
#### Preparation
|
||||
|
||||
@ -35,10 +31,11 @@ Mark the pre-release as final when it is necessary, then distribute the packages
|
||||
- [Post release note to forums](https://forums.balena.io/c/etcher)
|
||||
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
|
||||
- [Update the website](https://github.com/balena-io/etcher-homepage)
|
||||
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions
|
||||
- Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
|
||||
- If regressions arise; pull the release, and release a patched version, else:
|
||||
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
|
||||
- Post changelog with `#release-notes` tag on internal chat
|
||||
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
|
||||
- [Upload build artifacts to Amazon S3](#uploading-binaries-to-amazon-s3)
|
||||
- Post changelog with `#release-notes` tag on Flowdock
|
||||
- If this release packs noteworthy major changes:
|
||||
- Write a blog post about it, and / or
|
||||
- Write about it to the Etcher mailing list
|
||||
@ -51,30 +48,95 @@ Make sure to set the analytics tokens when generating production release binarie
|
||||
|
||||
```bash
|
||||
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
|
||||
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
|
||||
export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
||||
```
|
||||
|
||||
#### Linux
|
||||
|
||||
##### Clean dist folder
|
||||
|
||||
Delete `.webpack` and `out/`.
|
||||
**NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
|
||||
|
||||
```bash
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make distclean"
|
||||
```
|
||||
|
||||
##### Generating artifacts
|
||||
|
||||
The artifacts are generated by the CI and published as draft-release or pre-release.
|
||||
Etcher is built with electron-forge. Run:
|
||||
```bash
|
||||
# x64
|
||||
|
||||
```
|
||||
npm run make
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||
# Build AppImages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||
|
||||
# x86
|
||||
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||
# Build AppImages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||
```
|
||||
|
||||
Our CI will appropriately sign artifacts for macOS and some Windows targets.
|
||||
#### Mac OS
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_NAME` to generate signed binaries on Mac OS.
|
||||
|
||||
### Uploading packages to Cloudfront
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
Log in to cloudfront and upload the `rpm` and `deb` files.
|
||||
# Build the zip
|
||||
make RELEASE_TYPE=production electron-installer-app-zip
|
||||
# Build the dmg
|
||||
make RELEASE_TYPE=production electron-installer-dmg
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
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`.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the Portable version
|
||||
make RELEASE_TYPE=production electron-installer-portable
|
||||
# Build the Installer
|
||||
make RELEASE_TYPE=production electron-installer-nsis
|
||||
```
|
||||
|
||||
### Uploading packages to Bintray
|
||||
|
||||
```bash
|
||||
export BINTRAY_USER="username@account"
|
||||
export BINTRAY_API_KEY="youruserapikey"
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "debian" -y "debian" -r "x64" -f "dist/etcher-electron_1.2.1_amd64.deb"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "debian" -y "debian" -r "x86" -f "dist/etcher-electron_1.2.1_i386.deb"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "redhat" -y "redhat" -r "x64" -f "dist/etcher-electron-1.2.1.x86_64.rpm"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "redhat" -y "redhat" -r "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
|
||||
```
|
||||
|
||||
### Uploading binaries to Amazon S3
|
||||
|
||||
```bash
|
||||
export S3_KEY="..."
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/aws-s3.sh -b "balena-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
|
||||
```
|
||||
|
||||
### Dealing with a Problematic Release
|
||||
|
||||
|
@ -112,4 +112,4 @@ Analytics
|
||||
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
|
||||
check that no request is sent
|
||||
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
|
||||
F5), and check that initial events are not sent to Amplitude**
|
||||
F5), and check that initial events are not sent to Mixpanel**
|
||||
|
@ -7,9 +7,44 @@ systems.
|
||||
Release Types
|
||||
-------------
|
||||
|
||||
Etcher supports **pre-release** and **final** release types as does Github. Each is
|
||||
published to Github releases.
|
||||
The release version is generated automatically from the commit messasges.
|
||||
Etcher supports **production** and **snapshot** release types. Each is
|
||||
published to a different S3 bucket, and production release types are code
|
||||
signed, while snapshot release types aren't and include a short git commit-hash
|
||||
as a build number. For example, `1.0.0-beta.19` is a production release type,
|
||||
while `1.0.0-beta.19+531ab82` is a snapshot release type.
|
||||
|
||||
In terms of comparison: `1.0.0-beta.19` (production) < `1.0.0-beta.19+531ab82`
|
||||
(snapshot) < `1.0.0-rc.1` (production) < `1.0.0-rc.1+7fde24a` (snapshot) <
|
||||
`1.0.0` (production) < `1.0.0+2201e5f` (snapshot). Keep in mind that if you're
|
||||
running a production release type, you'll only be prompted to update to
|
||||
production release types, and if you're running a snapshot release type, you'll
|
||||
only be prompted to update to other snapshot release types.
|
||||
|
||||
The build system creates (and publishes) snapshot release types by default, but
|
||||
you can build a specific release type by setting the `RELEASE_TYPE` make
|
||||
variable. For example:
|
||||
|
||||
```sh
|
||||
make <target> RELEASE_TYPE=snapshot
|
||||
make <target> RELEASE_TYPE=production
|
||||
```
|
||||
|
||||
We can control the version range a specific Etcher version will consider when
|
||||
showing the update notification dialog by tweaking the `updates.semverRange`
|
||||
property of `package.json`.
|
||||
|
||||
Update Channels
|
||||
---------------
|
||||
|
||||
Etcher has a setting to include the unstable update channel. If this option is
|
||||
set, Etcher will consider both stable and unstable versions when showing the
|
||||
update notifier dialog. Unstable versions are the ones that contain a `beta`
|
||||
pre-release tag. For example:
|
||||
|
||||
- Production unstable version: `1.4.0-beta.1`
|
||||
- Snapshot unstable version: `1.4.0-beta.1+7fde24a`
|
||||
- Production stable version: `1.4.0`
|
||||
- Snapshot stable version: `1.4.0+7fde24a`
|
||||
|
||||
Signing
|
||||
-------
|
||||
@ -36,24 +71,65 @@ employee by asking for it from the relevant people.
|
||||
Packaging
|
||||
---------
|
||||
|
||||
Run the following command on each platform:
|
||||
The resulting installers will be saved to `dist/out`.
|
||||
|
||||
Run the following commands:
|
||||
|
||||
### OS X
|
||||
|
||||
```sh
|
||||
npm run make
|
||||
make electron-installer-dmg
|
||||
make electron-installer-app-zip
|
||||
```
|
||||
|
||||
This will produce all targets (eg. zip, dmg) specified in forge.config.ts for the
|
||||
host platform and architecture.
|
||||
### GNU/Linux
|
||||
|
||||
The resulting artifacts can be found in `out/make`.
|
||||
```sh
|
||||
make electron-installer-appimage
|
||||
make electron-installer-debian
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Publishing to Cloudfront
|
||||
```sh
|
||||
make electron-installer-zip
|
||||
make electron-installer-nsis
|
||||
```
|
||||
|
||||
Publishing to Bintray
|
||||
---------------------
|
||||
|
||||
We publish GNU/Linux Debian packages to [Cloudfront][cloudfront].
|
||||
We publish GNU/Linux Debian packages to [Bintray][bintray].
|
||||
|
||||
Log in to cloudfront and upload the `rpm` and `deb` files.
|
||||
Make sure you set the following environment variables:
|
||||
|
||||
- `BINTRAY_USER`
|
||||
- `BINTRAY_API_KEY`
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
make publish-bintray-debian
|
||||
```
|
||||
|
||||
Publishing to S3
|
||||
----------------
|
||||
|
||||
- [AWS CLI][aws-cli]
|
||||
|
||||
Make sure you have the [AWS CLI tool][aws-cli] installed and configured to
|
||||
access balena.io's production or snapshot S3 bucket.
|
||||
|
||||
Run the following command to publish all files for the current combination of
|
||||
_platform_ and _arch_ (building them if necessary):
|
||||
|
||||
```sh
|
||||
make publish-aws-s3
|
||||
```
|
||||
|
||||
Also add links to each AWS S3 file in [GitHub Releases][github-releases]. See
|
||||
[`v1.0.0-beta.17`](https://github.com/balena-io/etcher/releases/tag/v1.0.0-beta.17)
|
||||
as an example.
|
||||
|
||||
Publishing to Homebrew Cask
|
||||
---------------------------
|
||||
@ -71,12 +147,8 @@ Post messages to the [Etcher forum][balena-forum-etcher] announcing the new vers
|
||||
of Etcher, and including the relevant section of the Changelog.
|
||||
|
||||
[aws-cli]: https://aws.amazon.com/cli
|
||||
[cloudfront]: https://cloudfront.com
|
||||
[bintray]: https://bintray.com
|
||||
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb
|
||||
[homebrew-cask]: https://github.com/caskroom/homebrew-cask
|
||||
[balena-forum-etcher]: https://forums.balena.io/c/etcher
|
||||
[github-releases]: https://github.com/balena-io/etcher/releases
|
||||
|
||||
Updating EFP / Success-Banner
|
||||
-----------------------------
|
||||
Etcher Featured Project is automatically run based on an algorithm which promoted projects from the balena marketplace which have been contributed by the community, the algorithm prioritises projects which give uses the best experience. Editing both EFP and the Etcher Success-Banner can only be done by someone from balena, instruction are on the [Etcher-EFP repo (private)](https://github.com/balena-io/etcher-efp)
|
||||
|
@ -3,11 +3,6 @@ Etcher User Documentation
|
||||
|
||||
This document contains how-tos and FAQs oriented to Etcher users.
|
||||
|
||||
Config
|
||||
------
|
||||
Etcher's configuration is saved to the `config.json` file in the apps folder.
|
||||
Not all the options are surfaced to the UI. You may edit this file to tweak settings even before launching the app.
|
||||
|
||||
Why is my drive not bootable?
|
||||
-----------------------------
|
||||
|
||||
@ -122,6 +117,7 @@ run Etcher on a GNU/Linux system.
|
||||
- xrender
|
||||
- xtst
|
||||
- xscrnsaver
|
||||
- gconf-2.0
|
||||
- gmodule-2.0
|
||||
- nss
|
||||
|
||||
@ -222,5 +218,3 @@ macOS 10.10 (Yosemite) and newer versions][electron-supported-platforms].
|
||||
[unetbootin]: https://unetbootin.github.io
|
||||
[windows-iot-dashboard]: https://developer.microsoft.com/en-us/windows/iot/downloads
|
||||
[woeusb]: https://github.com/slacka/WoeUSB
|
||||
|
||||
See [PUBLISHING](/docs/PUBLISHING.md) for more details about release types.
|
109
electron-builder.yml
Normal file
109
electron-builder.yml
Normal file
@ -0,0 +1,109 @@
|
||||
# https://www.electron.build/configuration/configuration
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2021 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
afterPack: ./afterPack.js
|
||||
afterSign: ./afterSignHook.js
|
||||
asar: false
|
||||
files:
|
||||
- generated
|
||||
- lib/shared/catalina-sudo/sudo-askpass.osascript.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
|
@ -14,11 +14,5 @@
|
||||
<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>
|
||||
|
159
forge.config.ts
159
forge.config.ts
@ -1,159 +0,0 @@
|
||||
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 { exec } from 'child_process';
|
||||
|
||||
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/sudo/sudo-askpass.osascript-zh.js',
|
||||
'lib/shared/sudo/sudo-askpass.osascript-en.js',
|
||||
],
|
||||
osxSign: {
|
||||
optionsForFile: () => ({
|
||||
entitlements: './entitlements.mac.plist',
|
||||
hardenedRuntime: true,
|
||||
}),
|
||||
},
|
||||
...osxSigningConfig,
|
||||
},
|
||||
rebuildConfig: {
|
||||
onlyModules: [], // prevent rebuilding *any* native modules as they won't be used by electron but by the sidecar
|
||||
},
|
||||
makers: [
|
||||
new MakerZIP(),
|
||||
new MakerSquirrel({
|
||||
setupIcon: 'assets/icon.ico',
|
||||
loadingGif: 'assets/icon.png',
|
||||
...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: {
|
||||
postPackage: async (_forgeConfig, options) => {
|
||||
if (options.platform === 'linux') {
|
||||
// symlink the etcher binary from balena-etcher to balenaEtcher to ensure compatibility with the wdio suite and the old name
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
exec(
|
||||
`ln -s "${options.outputPaths}/balena-etcher" "${options.outputPaths}/balenaEtcher"`,
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
168
forge.sidecar.ts
168
forge.sidecar.ts
@ -1,168 +0,0 @@
|
||||
import { PluginBase } from '@electron-forge/plugin-base';
|
||||
import type {
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
@ -15,31 +15,29 @@
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as remote from '@electron/remote';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { debounce, capitalize, values } from 'lodash';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as packageJSON from '../../../package.json';
|
||||
import type { DrivelistDrive } from '../../shared/drive-constraints';
|
||||
import { DrivelistDrive, isSourceDrive } 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 { spawnChildAndConnect } from './modules/api';
|
||||
import { scanner as driveScanner } from './modules/drive-scanner';
|
||||
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 type { SourceMetadata } from '../../shared/typings/source-selector';
|
||||
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
@ -89,7 +87,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 +113,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,48 +126,175 @@ observe(() => {
|
||||
`);
|
||||
});
|
||||
|
||||
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));
|
||||
/**
|
||||
* @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
|
||||
`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Spawning the child process without privileges to get the drives list
|
||||
// TODO: clean up this mess of exports
|
||||
export let requestMetadata: any;
|
||||
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
|
||||
availableDrives.setDrives(_.values(drives));
|
||||
}
|
||||
|
||||
// start the api and spawn the child process
|
||||
spawnChildAndConnect({
|
||||
withPrivileges: false,
|
||||
})
|
||||
.then(({ emit, registerHandler }) => {
|
||||
// start scanning
|
||||
emit('scan', {});
|
||||
function getDrives() {
|
||||
return _.keyBy(availableDrives.getDrives(), 'device');
|
||||
}
|
||||
|
||||
// make the sourceMetada awaitable to be used on source selection
|
||||
requestMetadata = async (params: any): Promise<SourceMetadata> => {
|
||||
emit('sourceMetadata', JSON.stringify(params));
|
||||
async function addDrive(drive: Drive) {
|
||||
const preparedDrive = prepareDrive(drive);
|
||||
if (!(await driveIsAllowed(preparedDrive))) {
|
||||
return;
|
||||
}
|
||||
const drives = getDrives();
|
||||
drives[preparedDrive.device] = preparedDrive;
|
||||
setDrives(drives);
|
||||
}
|
||||
|
||||
return new Promise((resolve) =>
|
||||
registerHandler('sourceMetadata', (data: any) => {
|
||||
resolve(JSON.parse(data));
|
||||
}),
|
||||
);
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
registerHandler('drives', (data: any) => {
|
||||
setDrives(JSON.parse(data));
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
throw new Error(`Failed to start the flasher process. error: ${error}`);
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
driveScanner.start();
|
||||
|
||||
let popupExists = false;
|
||||
|
||||
analytics.initAnalytics();
|
||||
|
||||
window.addEventListener('beforeunload', async (event) => {
|
||||
if (!flashState.isFlashing() || popupExists) {
|
||||
analytics.logEvent('Close application', {
|
||||
@ -188,9 +313,9 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
|
||||
try {
|
||||
const confirmed = await osDialog.showWarning({
|
||||
confirmationLabel: i18next.t('yesExit'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
title: i18next.t('reallyExit'),
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: 'Are you sure you want to close Etcher?',
|
||||
description: messages.warning.exitWhileFlashing(),
|
||||
});
|
||||
if (confirmed) {
|
||||
@ -199,8 +324,8 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
});
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
// remote.app.quit() which does not.
|
||||
remote.process.exit(EXIT_CODES.SUCCESS);
|
||||
// electron.remote.app.quit() which does not.
|
||||
electron.remote.process.exit(EXIT_CODES.SUCCESS);
|
||||
}
|
||||
|
||||
analytics.logEvent('Close rejected while flashing', {
|
||||
|
@ -14,36 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import type * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||
import * as React from 'react';
|
||||
import type { ModalProps, TableColumn } from 'rendition';
|
||||
import { Flex, Txt, Badge, Link } from 'rendition';
|
||||
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {
|
||||
DriveStatus,
|
||||
DrivelistDrive,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
isDriveValid,
|
||||
DriveStatus,
|
||||
DrivelistDrive,
|
||||
isDriveSizeLarge,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||
import { store } from '../../models/store';
|
||||
import { logEvent, logException } from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import type { GenericTableProps } from '../../styled-components';
|
||||
import { Alert, Modal, Table } from '../../styled-components';
|
||||
import {
|
||||
Alert,
|
||||
GenericTableProps,
|
||||
Modal,
|
||||
Table,
|
||||
} from '../../styled-components';
|
||||
|
||||
import type { SourceMetadata } from '../../../../shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../source-selector/source-selector';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
@ -189,7 +189,7 @@ export class DriveSelector extends React.Component<
|
||||
this.tableColumns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: i18next.t('drives.name'),
|
||||
label: 'Name',
|
||||
render: (description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive)) {
|
||||
const isLargeDrive = isDriveSizeLarge(drive);
|
||||
@ -215,7 +215,7 @@ export class DriveSelector extends React.Component<
|
||||
{
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: i18next.t('drives.size'),
|
||||
label: 'Size',
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return prettyBytes(drive.size);
|
||||
@ -225,7 +225,7 @@ export class DriveSelector extends React.Component<
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: i18next.t('drives.location'),
|
||||
label: 'Location',
|
||||
render: (_description: string, drive: Drive) => {
|
||||
return (
|
||||
<Txt>
|
||||
@ -309,17 +309,9 @@ export class DriveSelector extends React.Component<
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
case compatibility.tooSmall():
|
||||
return warning.tooSmall(
|
||||
{
|
||||
size:
|
||||
this.state.image?.recommendedDriveSize ||
|
||||
this.state.image?.size ||
|
||||
0,
|
||||
},
|
||||
drive,
|
||||
);
|
||||
default:
|
||||
return '';
|
||||
const size =
|
||||
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||
return warning.tooSmall({ size }, drive);
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,14 +399,14 @@ export class DriveSelector extends React.Component<
|
||||
color="#5b82a7"
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
{i18next.t('drives.find', { length: drives.length })}
|
||||
{drives.length} found
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={() => cancel(this.originalList)}
|
||||
done={() => done(selectedList)}
|
||||
action={i18next.t('drives.select', { select: selectedList.length })}
|
||||
action={`Select (${selectedList.length})`}
|
||||
primaryButtonProps={{
|
||||
primary: !showWarnings,
|
||||
warning: showWarnings,
|
||||
@ -435,10 +427,11 @@ export class DriveSelector extends React.Component<
|
||||
) : (
|
||||
<>
|
||||
<DrivesTable
|
||||
refFn={() => {
|
||||
// noop
|
||||
refFn={(t) => {
|
||||
if (t !== null) {
|
||||
t.setRowSelection(selectedList);
|
||||
}
|
||||
}}
|
||||
checkedItems={selectedList}
|
||||
checkedRowsNumber={selectedList.length}
|
||||
multipleSelection={this.props.multipleSelection}
|
||||
columns={this.tableColumns}
|
||||
@ -448,10 +441,7 @@ export class DriveSelector extends React.Component<
|
||||
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
||||
}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows) => {
|
||||
if (rows == null) {
|
||||
rows = [];
|
||||
}
|
||||
onCheck={(rows: Drive[]) => {
|
||||
let newSelection = rows.filter(isDrivelistDrive);
|
||||
if (this.props.multipleSelection) {
|
||||
if (rows.length === 0) {
|
||||
@ -522,11 +512,7 @@ export class DriveSelector extends React.Component<
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
<Txt ml={8}>
|
||||
{i18next.t('drives.showHidden', {
|
||||
num: numberOfHiddenSystemDrives,
|
||||
})}
|
||||
</Txt>
|
||||
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
@ -534,7 +520,7 @@ export class DriveSelector extends React.Component<
|
||||
)}
|
||||
{this.props.showWarnings && hasSystemDrives ? (
|
||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||
{i18next.t('drives.systemDriveDanger')}
|
||||
Selecting your system drive is dangerous and will erase your drive!
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
@ -554,15 +540,13 @@ export class DriveSelector extends React.Component<
|
||||
this.setState({ missingDriversModal: {} });
|
||||
}
|
||||
}}
|
||||
action={i18next.t('yesContinue')}
|
||||
action="Yes, continue"
|
||||
cancelButtonProps={{
|
||||
children: i18next.t('cancel'),
|
||||
children: 'Cancel',
|
||||
}}
|
||||
children={
|
||||
missingDriversModal.drive.linkMessage ||
|
||||
i18next.t('drives.openInBrowser', {
|
||||
link: missingDriversModal.drive.link,
|
||||
})
|
||||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import type { ModalProps } from 'rendition';
|
||||
import { Badge, Flex, Txt } from 'rendition';
|
||||
import { Badge, Flex, Txt, ModalProps } from 'rendition';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import type { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
import * as i18next from 'i18next';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
done,
|
||||
@ -18,12 +17,12 @@ const DriveStatusWarningModal = ({
|
||||
isSystem: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}) => {
|
||||
let warningSubtitle = i18next.t('drives.largeDriveWarning');
|
||||
let warningCta = i18next.t('drives.largeDriveWarningMsg');
|
||||
let warningSubtitle = 'You are about to erase an unusually large drive';
|
||||
let warningCta = 'Are you sure the selected drive is not a storage drive?';
|
||||
|
||||
if (isSystem) {
|
||||
warningSubtitle = i18next.t('drives.systemDriveWarning');
|
||||
warningCta = i18next.t('drives.systemDriveWarningMsg');
|
||||
warningSubtitle = "You are about to erase your computer's drives";
|
||||
warningCta = 'Are you sure you want to flash your system drive?';
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
@ -34,9 +33,9 @@ const DriveStatusWarningModal = ({
|
||||
cancelButtonProps={{
|
||||
primary: false,
|
||||
warning: true,
|
||||
children: i18next.t('drives.changeTarget'),
|
||||
children: 'Change target',
|
||||
}}
|
||||
action={i18next.t('sure')}
|
||||
action={"Yes, I'm sure"}
|
||||
primaryButtonProps={{
|
||||
primary: false,
|
||||
outline: true,
|
||||
@ -51,7 +50,7 @@ const DriveStatusWarningModal = ({
|
||||
<Flex flexDirection="column">
|
||||
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||
<Txt fontSize="24px" color="#fca321">
|
||||
{i18next.t('warning')}
|
||||
WARNING!
|
||||
</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||
|
@ -24,8 +24,7 @@ import * as settings from '../../models/settings';
|
||||
import { Actions, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { FlashAnother } from '../flash-another/flash-another';
|
||||
import type { FlashError } from '../flash-results/flash-results';
|
||||
import { FlashResults } from '../flash-results/flash-results';
|
||||
import { FlashResults, FlashError } from '../flash-results/flash-results';
|
||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||
|
||||
function restart(goToMain: () => void) {
|
||||
@ -44,7 +43,7 @@ function restart(goToMain: () => void) {
|
||||
async function getSuccessBannerURL() {
|
||||
return (
|
||||
(await settings.get('successBannerURL')) ??
|
||||
'https://efp.balena.io/success-banner?borderTop=false&darkBackground=true'
|
||||
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { BaseButton } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
export interface FlashAnotherProps {
|
||||
onClick: () => void;
|
||||
@ -26,7 +25,7 @@ export interface FlashAnotherProps {
|
||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||
return (
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
{i18next.t('flash.another')}
|
||||
Flash another
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
@ -15,11 +15,11 @@
|
||||
*/
|
||||
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg';
|
||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg';
|
||||
import 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 * as React from 'react';
|
||||
import type { FlexProps, TableColumn } from 'rendition';
|
||||
import { Flex, Link, Txt } from 'rendition';
|
||||
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { progress } from '../../../../shared/messages';
|
||||
@ -31,7 +31,6 @@ import { resetState } from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { Modal, Table } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||
&&& [data-display='table-head'],
|
||||
@ -89,15 +88,15 @@ function formattedErrors(errors: FlashError[]) {
|
||||
const columns: Array<TableColumn<FlashError>> = [
|
||||
{
|
||||
field: 'description',
|
||||
label: i18next.t('flash.target'),
|
||||
label: 'Target',
|
||||
},
|
||||
{
|
||||
field: 'device',
|
||||
label: i18next.t('flash.location'),
|
||||
label: 'Location',
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
label: i18next.t('flash.error'),
|
||||
label: 'Error',
|
||||
render: (message: string, { code }: FlashError) => {
|
||||
return message ?? code;
|
||||
},
|
||||
@ -139,9 +138,8 @@ 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,
|
||||
);
|
||||
@ -164,11 +162,9 @@ export function FlashResults({
|
||||
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize={24} color="#fff" mb="17px">
|
||||
{allFailed
|
||||
? i18next.t('flash.flashFailed')
|
||||
: i18next.t('flash.flashCompleted')}
|
||||
Flash {allFailed ? 'Failed' : 'Complete'}!
|
||||
</Txt>
|
||||
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
|
||||
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
|
||||
</Flex>
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{results.devices.successful !== 0 ? (
|
||||
@ -192,7 +188,7 @@ export function FlashResults({
|
||||
{progress.failed(errors.length)}
|
||||
</Txt>
|
||||
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||
{i18next.t('flash.moreInfo')}
|
||||
more info
|
||||
</Link>
|
||||
</Flex>
|
||||
) : null}
|
||||
@ -203,9 +199,12 @@ export function FlashResults({
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
tooltip={i18next.t('flash.speedTip')}
|
||||
tooltip={outdent({ newline: ' ' })`
|
||||
The speed is calculated by dividing the image size by the flashing time.
|
||||
Disk images with ext partitions flash faster as we are able to skip unused parts.
|
||||
`}
|
||||
>
|
||||
{i18next.t('flash.speed', { speed: effectiveSpeed })}
|
||||
Effective speed: {effectiveSpeed} MB/s
|
||||
</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
@ -215,11 +214,11 @@ export function FlashResults({
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
{i18next.t('failedTarget')}
|
||||
Failed targets
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
action={i18next.t('failedRetry')}
|
||||
action="Retry failed targets"
|
||||
cancel={() => setShowErrorsInfo(false)}
|
||||
done={() => {
|
||||
setShowErrorsInfo(false);
|
||||
|
@ -20,7 +20,6 @@ import { default as styled } from 'styled-components';
|
||||
|
||||
import { fromFlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
@ -29,7 +28,6 @@ const FlashProgressBar = styled(ProgressBar)`
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
transition-duration: 0s;
|
||||
|
||||
> div {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
@ -63,7 +61,7 @@ const colors = {
|
||||
} as const;
|
||||
|
||||
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
|
||||
const status = type === 'verifying' ? 'Skip' : 'Cancel';
|
||||
return (
|
||||
<Button plain onClick={() => onClick(status)} {...props}>
|
||||
{status}
|
||||
@ -71,7 +69,6 @@ const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
);
|
||||
})`
|
||||
font-weight: 600;
|
||||
|
||||
&&& {
|
||||
width: auto;
|
||||
height: auto;
|
||||
@ -129,7 +126,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
{i18next.t('flash.flashNow')}
|
||||
Flash!
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as remote from '@electron/remote';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
|
||||
@ -95,11 +94,10 @@ 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
|
||||
this.session = remote.session.fromPartition(ELECTRON_SESSION, {
|
||||
this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, {
|
||||
// Disable the cache for the session such that new content shows up when refreshing
|
||||
cache: false,
|
||||
});
|
||||
@ -122,8 +120,6 @@ export class SafeWebview extends React.PureComponent<
|
||||
ref={this.webviewRef}
|
||||
partition={ELECTRON_SESSION}
|
||||
style={style}
|
||||
// @ts-ignore
|
||||
allowpopups="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -137,8 +133,8 @@ export class SafeWebview extends React.PureComponent<
|
||||
this.didFailLoad,
|
||||
);
|
||||
this.webviewRef.current.addEventListener(
|
||||
'dom-ready',
|
||||
this.handleDomReady,
|
||||
'new-window',
|
||||
SafeWebview.newWindow,
|
||||
);
|
||||
this.webviewRef.current.addEventListener(
|
||||
'console-message',
|
||||
@ -160,8 +156,8 @@ export class SafeWebview extends React.PureComponent<
|
||||
this.didFailLoad,
|
||||
);
|
||||
this.webviewRef.current.removeEventListener(
|
||||
'dom-ready',
|
||||
this.handleDomReady,
|
||||
'new-window',
|
||||
SafeWebview.newWindow,
|
||||
);
|
||||
this.webviewRef.current.removeEventListener(
|
||||
'console-message',
|
||||
@ -171,15 +167,6 @@ 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,10 +183,7 @@ export class SafeWebview extends React.PureComponent<
|
||||
// only care about this event if it's a request for the main frame
|
||||
if (event.resourceType === 'mainFrame') {
|
||||
const HTTP_OK = 200;
|
||||
const { webContents, ...webviewEvent } = event;
|
||||
analytics.logEvent('SafeWebview loaded', {
|
||||
...webviewEvent,
|
||||
});
|
||||
analytics.logEvent('SafeWebview loaded', { event });
|
||||
this.setState({
|
||||
shouldShow: event.statusCode === HTTP_OK,
|
||||
});
|
||||
@ -208,4 +192,17 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,15 +17,13 @@
|
||||
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Box, Checkbox, Flex, Txt } from 'rendition';
|
||||
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
|
||||
|
||||
import { version, packageType } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { Modal } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
import { etcherProInfo } from '../../utils/etcher-pro-specific';
|
||||
|
||||
interface Setting {
|
||||
name: string;
|
||||
@ -36,17 +34,13 @@ async function getSettingsList(): Promise<Setting[]> {
|
||||
const list: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: i18next.t('settings.errorReporting'),
|
||||
},
|
||||
{
|
||||
name: 'autoBlockmapping',
|
||||
label: i18next.t('settings.trimExtPartitions'),
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
},
|
||||
];
|
||||
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||
list.push({
|
||||
name: 'updatesEnabled',
|
||||
label: i18next.t('settings.autoUpdate'),
|
||||
label: 'Auto-updates enabled',
|
||||
});
|
||||
}
|
||||
return list;
|
||||
@ -56,17 +50,14 @@ interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const EPInfo = etcherProInfo();
|
||||
const UUID = process.env.BALENA_DEVICE_UUID;
|
||||
|
||||
const InfoBox = (props: any) => (
|
||||
<Box fontSize={14}>
|
||||
<Txt>{props.label}</Txt>
|
||||
<Txt code copy={props.value}>
|
||||
{props.value}{' '}
|
||||
</Txt>
|
||||
<TextWithCopy code text={props.value} copy={props.value} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
||||
React.useEffect(() => {
|
||||
@ -101,7 +92,7 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
<Modal
|
||||
titleElement={
|
||||
<Txt fontSize={24} mb={24}>
|
||||
{i18next.t('settings.settings')}
|
||||
Settings
|
||||
</Txt>
|
||||
}
|
||||
done={() => toggleModal(false)}
|
||||
@ -120,14 +111,10 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
{EPInfo !== undefined && (
|
||||
{UUID !== undefined && (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
|
||||
{EPInfo.get_serial() === undefined ? (
|
||||
<InfoBox label="UUID" value={EPInfo.uuid} />
|
||||
) : (
|
||||
<InfoBox label="Serial" value={EPInfo.get_serial()} />
|
||||
)}
|
||||
<Txt fontSize={24}>System Information</Txt>
|
||||
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
|
@ -17,20 +17,19 @@
|
||||
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/triangle-exclamation.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
|
||||
import type { IpcRendererEvent } from 'electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { uniqBy, isNil } from 'lodash';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
import * as path from 'path';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import { requestMetadata } from '../../app';
|
||||
|
||||
import type { ButtonProps } from 'rendition';
|
||||
import {
|
||||
Flex,
|
||||
ButtonProps,
|
||||
Modal as SmallModal,
|
||||
Txt,
|
||||
Card as BaseCard,
|
||||
@ -48,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,14 +63,9 @@ import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import SrcSvg from '../../../assets/src.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import type { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { isJson } from '../../../../shared/utils';
|
||||
import type {
|
||||
SourceMetadata,
|
||||
Authentication,
|
||||
Source,
|
||||
} from '../../../../shared/typings/source-selector';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
@ -88,7 +82,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);
|
||||
}
|
||||
|
||||
@ -166,7 +160,7 @@ const URLSelector = ({
|
||||
primaryButtonProps={{
|
||||
disabled: loading || !imageURL,
|
||||
}}
|
||||
action={loading ? <Spinner /> : i18next.t('ok')}
|
||||
action={loading ? <Spinner /> : 'OK'}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const urlStrings = recentImages.map((url: URL) => url.href);
|
||||
@ -182,11 +176,11 @@ const URLSelector = ({
|
||||
<Flex flexDirection="column">
|
||||
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
{i18next.t('source.useSourceURL')}
|
||||
Use Image URL
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder={i18next.t('source.enterValidURL')}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
@ -211,7 +205,7 @@ const URLSelector = ({
|
||||
{!showBasicAuth && (
|
||||
<ChevronRightSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
<Txt ml={8}>{i18next.t('source.auth')}</Txt>
|
||||
<Txt ml={8}>Authentication</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
{showBasicAuth && (
|
||||
@ -219,7 +213,7 @@ const URLSelector = ({
|
||||
<Input
|
||||
mb={15}
|
||||
value={username}
|
||||
placeholder={i18next.t('source.username')}
|
||||
placeholder="Enter username"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setUsername(evt.target.value)
|
||||
@ -227,7 +221,7 @@ const URLSelector = ({
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
placeholder={i18next.t('source.password')}
|
||||
placeholder="Enter password"
|
||||
type="password"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(evt.target.value)
|
||||
@ -301,14 +295,31 @@ const FlowSelector = styled(
|
||||
font-weight: 600;
|
||||
|
||||
svg {
|
||||
color: ${colors.primary.foreground} !important;
|
||||
color: ${colors.primary.foreground}!important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type Source =
|
||||
| typeof sourceDestination.File
|
||||
| typeof sourceDestination.BlockDevice
|
||||
| typeof sourceDestination.Http;
|
||||
|
||||
export interface SourceMetadata extends sourceDestination.Metadata {
|
||||
hasMBR?: boolean;
|
||||
partitions?: MBRPartition[] | GPTPartition[];
|
||||
path: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
SourceType: Source;
|
||||
drive?: DrivelistDrive;
|
||||
extension?: string;
|
||||
archiveExtension?: string;
|
||||
auth?: Authentication;
|
||||
}
|
||||
|
||||
interface SourceSelectorProps {
|
||||
flashing: boolean;
|
||||
hideAnalyticsAlert: () => void;
|
||||
}
|
||||
|
||||
interface SourceSelectorState {
|
||||
@ -324,6 +335,11 @@ interface SourceSelectorState {
|
||||
imageLoading: boolean;
|
||||
}
|
||||
|
||||
interface Authentication {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class SourceSelector extends React.Component<
|
||||
SourceSelectorProps,
|
||||
SourceSelectorState
|
||||
@ -360,29 +376,47 @@ export class SourceSelector extends React.Component<
|
||||
ipcRenderer.removeListener('select-image', this.onSelectImage);
|
||||
}
|
||||
|
||||
public componentDidUpdate(
|
||||
_prevProps: Readonly<SourceSelectorProps>,
|
||||
prevState: Readonly<SourceSelectorState>,
|
||||
) {
|
||||
if (
|
||||
(!prevState.showDriveSelector && this.state.showDriveSelector) ||
|
||||
(!prevState.showURLSelector && this.state.showURLSelector) ||
|
||||
(!prevState.showImageDetails && this.state.showImageDetails) ||
|
||||
(!prevState.imageSelectorOpen && this.state.imageSelectorOpen)
|
||||
) {
|
||||
this.props.hideAnalyticsAlert();
|
||||
}
|
||||
}
|
||||
|
||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||
this.setState({ imageLoading: true });
|
||||
await this.selectSource(
|
||||
imagePath,
|
||||
isURL(this.normalizeImagePath(imagePath)) ? 'Http' : 'File',
|
||||
isURL(this.normalizeImagePath(imagePath))
|
||||
? sourceDestination.Http
|
||||
: sourceDestination.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)) {
|
||||
@ -397,7 +431,6 @@ export class SourceSelector extends React.Component<
|
||||
});
|
||||
|
||||
selectionState.deselectImage();
|
||||
this.props.hideAnalyticsAlert();
|
||||
}
|
||||
|
||||
private selectSource(
|
||||
@ -405,20 +438,22 @@ export class SourceSelector extends React.Component<
|
||||
SourceType: Source,
|
||||
auth?: Authentication,
|
||||
): { promise: Promise<void>; cancel: () => void } {
|
||||
let cancelled = false;
|
||||
return {
|
||||
cancel: () => {
|
||||
// noop
|
||||
cancelled = true;
|
||||
},
|
||||
promise: (async () => {
|
||||
const sourcePath = isString(selected) ? selected : selected.device;
|
||||
let source;
|
||||
let metadata: SourceMetadata | undefined;
|
||||
if (isString(selected)) {
|
||||
if (
|
||||
SourceType === 'Http' &&
|
||||
SourceType === sourceDestination.Http &&
|
||||
!isURL(this.normalizeImagePath(selected))
|
||||
) {
|
||||
this.handleError(
|
||||
i18next.t('source.unsupportedProtocol'),
|
||||
'Unsupported protocol',
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
@ -430,41 +465,49 @@ export class SourceSelector extends React.Component<
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
title: i18next.t('source.windowsImage'),
|
||||
title: 'Possible Windows image detected',
|
||||
},
|
||||
});
|
||||
}
|
||||
source = await this.createSource(selected, SourceType, auth);
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// this will send an event down the ipcMain asking for metadata
|
||||
// we'll get the response through an event
|
||||
|
||||
// FIXME: This is a poor man wait while loading to prevent a potential race condition without completely blocking the interface
|
||||
// This should be addressed when refactoring the GUI
|
||||
let retriesLeft = 10;
|
||||
while (requestMetadata === undefined && retriesLeft > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1050)); // api is trying to connect every 1000, this is offset to make sure we fall between retries
|
||||
retriesLeft--;
|
||||
const innerSource = await source.getInnerSource();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
metadata = await this.getMetadata(innerSource, selected);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
metadata.SourceType = SourceType;
|
||||
|
||||
metadata = await requestMetadata({ selected, SourceType, auth });
|
||||
|
||||
if (!metadata?.hasMBR && this.state.warning === null) {
|
||||
if (!metadata.hasMBR && this.state.warning === null) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
title: i18next.t('source.partitionTable'),
|
||||
title: 'Missing partition table',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.handleError(
|
||||
i18next.t('source.errorOpen'),
|
||||
'Error opening source',
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error: any) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (selected.partitionTableType === null) {
|
||||
@ -472,7 +515,7 @@ export class SourceSelector extends React.Component<
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.driveMissingPartitionTable(),
|
||||
title: i18next.t('source.partitionTable'),
|
||||
title: 'Missing partition table',
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -481,14 +524,13 @@ export class SourceSelector extends React.Component<
|
||||
displayName: selected.displayName,
|
||||
description: selected.displayName,
|
||||
size: selected.size as SourceMetadata['size'],
|
||||
SourceType: 'BlockDevice',
|
||||
SourceType: sourceDestination.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
|
||||
@ -522,6 +564,25 @@ 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 });
|
||||
@ -534,7 +595,7 @@ export class SourceSelector extends React.Component<
|
||||
analytics.logEvent('Image selector closed');
|
||||
return;
|
||||
}
|
||||
await this.selectSource(imagePath, 'File').promise;
|
||||
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
} finally {
|
||||
@ -543,9 +604,9 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
|
||||
const file = event.dataTransfer.files.item(0);
|
||||
if (file != null) {
|
||||
await this.selectSource(file.path, 'File').promise;
|
||||
const [file] = event.dataTransfer.files;
|
||||
if (file) {
|
||||
await this.selectSource(file.path, sourceDestination.File).promise;
|
||||
}
|
||||
}
|
||||
|
||||
@ -605,7 +666,7 @@ export class SourceSelector extends React.Component<
|
||||
imageLoading,
|
||||
} = this.state;
|
||||
const selectionImage = selectionState.getImage();
|
||||
let image =
|
||||
let image: SourceMetadata | DrivelistDrive =
|
||||
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||
|
||||
image = image.drive ?? image;
|
||||
@ -658,10 +719,10 @@ export class SourceSelector extends React.Component<
|
||||
mb={14}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
{i18next.t('cancel')}
|
||||
Remove
|
||||
</ChangeButton>
|
||||
)}
|
||||
{!isNil(imageSize) && !imageLoading && (
|
||||
{!_.isNil(imageSize) && !imageLoading && (
|
||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
@ -673,7 +734,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Flash from file"
|
||||
flow={{
|
||||
onClick: () => this.openImageSelector(),
|
||||
label: i18next.t('source.fromFile'),
|
||||
label: 'Flash from file',
|
||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@ -683,7 +744,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Flash from URL"
|
||||
flow={{
|
||||
onClick: () => this.openURLSelector(),
|
||||
label: i18next.t('source.fromURL'),
|
||||
label: 'Flash from URL',
|
||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@ -693,7 +754,7 @@ export class SourceSelector extends React.Component<
|
||||
key="Clone drive"
|
||||
flow={{
|
||||
onClick: () => this.openDriveSelector(),
|
||||
label: i18next.t('source.clone'),
|
||||
label: 'Clone drive',
|
||||
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
@ -708,13 +769,13 @@ export class SourceSelector extends React.Component<
|
||||
style={{
|
||||
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
title={
|
||||
titleElement={
|
||||
<span>
|
||||
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
||||
<span>{this.state.warning.title}</span>
|
||||
</span>
|
||||
}
|
||||
action={i18next.t('continue')}
|
||||
action="Continue"
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectSource();
|
||||
@ -732,17 +793,17 @@ export class SourceSelector extends React.Component<
|
||||
|
||||
{showImageDetails && (
|
||||
<SmallModal
|
||||
title={i18next.t('source.image')}
|
||||
title="Image"
|
||||
done={() => {
|
||||
this.setState({ showImageDetails: false });
|
||||
}}
|
||||
>
|
||||
<Txt.p>
|
||||
<Txt.span bold>{i18next.t('source.name')}</Txt.span>
|
||||
<Txt.span bold>Name: </Txt.span>
|
||||
<Txt.span>{imageName || imageBasename}</Txt.span>
|
||||
</Txt.p>
|
||||
<Txt.p>
|
||||
<Txt.span bold>{i18next.t('source.path')}</Txt.span>
|
||||
<Txt.span bold>Path: </Txt.span>
|
||||
<Txt.span>{imagePath}</Txt.span>
|
||||
</Txt.p>
|
||||
</SmallModal>
|
||||
@ -765,7 +826,7 @@ export class SourceSelector extends React.Component<
|
||||
let promise;
|
||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||
imageURL,
|
||||
'Http',
|
||||
sourceDestination.Http,
|
||||
auth,
|
||||
));
|
||||
await promise;
|
||||
@ -781,14 +842,17 @@ export class SourceSelector extends React.Component<
|
||||
<DriveSelector
|
||||
write={false}
|
||||
multipleSelection={false}
|
||||
titleLabel={i18next.t('source.selectSource')}
|
||||
emptyListLabel={i18next.t('source.plugSource')}
|
||||
titleLabel="Select source"
|
||||
emptyListLabel="Plug a source drive"
|
||||
emptyListIcon={<SrcSvg width="40px" />}
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
const originalSource = originalList[0];
|
||||
if (selectionImage?.drive?.device !== originalSource.device) {
|
||||
this.selectSource(originalSource, 'BlockDevice');
|
||||
this.selectSource(
|
||||
originalSource,
|
||||
sourceDestination.BlockDevice,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
selectionState.deselectImage();
|
||||
@ -803,7 +867,7 @@ export class SourceSelector extends React.Component<
|
||||
) {
|
||||
return selectionState.deselectImage();
|
||||
}
|
||||
this.selectSource(drive, 'BlockDevice');
|
||||
this.selectSource(drive, sourceDestination.BlockDevice);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -14,15 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import * as React from 'react';
|
||||
import type { FlexProps } from 'rendition';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
import { Flex, FlexProps, Txt } from 'rendition';
|
||||
|
||||
import type { DriveStatus } from '../../../../shared/drive-constraints';
|
||||
import { getDriveImageCompatibilityStatuses } from '../../../../shared/drive-constraints';
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
DriveStatus,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||
import {
|
||||
ChangeButton,
|
||||
@ -31,7 +32,6 @@ import {
|
||||
StepNameButton,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface TargetSelectorProps {
|
||||
targets: any[];
|
||||
@ -95,7 +95,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
|
||||
{i18next.t('target.change')}
|
||||
Change
|
||||
</ChangeButton>
|
||||
)}
|
||||
{target.size != null && (
|
||||
@ -132,11 +132,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{targets.length} {i18next.t('target.targets')}
|
||||
{targets.length} Targets
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
|
||||
{i18next.t('target.change')}
|
||||
Change
|
||||
</ChangeButton>
|
||||
)}
|
||||
{targetsTemplate}
|
||||
@ -151,7 +151,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
{i18next.t('target.selectTarget')}
|
||||
Select target
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
|
@ -17,8 +17,10 @@
|
||||
import * as React from 'react';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import type { DriveSelectorProps } from '../drive-selector/drive-selector';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import {
|
||||
DriveSelector,
|
||||
DriveSelectorProps,
|
||||
} from '../drive-selector/drive-selector';
|
||||
import {
|
||||
isDriveSelected,
|
||||
getImage,
|
||||
@ -34,8 +36,7 @@ import { TargetSelectorButton } from './target-selector-button';
|
||||
import TgtSvg from '../../../assets/tgt.svg';
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
import type { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import * as i18next from 'i18next';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
|
||||
export const getDriveListLabel = () => {
|
||||
return getSelectedDrives()
|
||||
@ -59,8 +60,8 @@ export const TargetSelectorModal = (
|
||||
) => (
|
||||
<DriveSelector
|
||||
multipleSelection={true}
|
||||
titleLabel={i18next.t('target.selectTarget')}
|
||||
emptyListLabel={i18next.t('target.plugTarget')}
|
||||
titleLabel="Select target"
|
||||
emptyListLabel="Plug a target drive"
|
||||
emptyListIcon={<TgtSvg width="40px" />}
|
||||
showWarnings={true}
|
||||
selectedList={getSelectedDrives()}
|
||||
@ -100,14 +101,12 @@ interface TargetSelectorProps {
|
||||
disabled: boolean;
|
||||
hasDrive: boolean;
|
||||
flashing: boolean;
|
||||
hideAnalyticsAlert: () => void;
|
||||
}
|
||||
|
||||
export const TargetSelector = ({
|
||||
disabled,
|
||||
hasDrive,
|
||||
flashing,
|
||||
hideAnalyticsAlert,
|
||||
}: TargetSelectorProps) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
|
||||
@ -139,7 +138,6 @@ export const TargetSelector = ({
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowTargetSelectorModal(true);
|
||||
hideAnalyticsAlert();
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive');
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,44 +0,0 @@
|
||||
import * as i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import zh_CN_translation from './i18n/zh-CN';
|
||||
import zh_TW_translation from './i18n/zh-TW';
|
||||
import en_translation from './i18n/en';
|
||||
|
||||
export function langParser() {
|
||||
if (process.env.LANG !== undefined) {
|
||||
// Bypass mocha, where lang-detect don't works
|
||||
return 'en';
|
||||
}
|
||||
|
||||
const lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
|
||||
switch (lang.substr(0, 2)) {
|
||||
case 'zh':
|
||||
if (lang === 'zh-CN' || lang === 'zh-SG') {
|
||||
return 'zh-CN';
|
||||
} // Simplified Chinese
|
||||
else {
|
||||
return 'zh-TW';
|
||||
} // Traditional Chinese
|
||||
default:
|
||||
return lang.substr(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
i18next.use(initReactI18next).init({
|
||||
lng: langParser(),
|
||||
fallbackLng: 'en',
|
||||
nonExplicitSupportedLngs: true,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
resources: {
|
||||
'zh-CN': zh_CN_translation,
|
||||
'zh-TW': zh_TW_translation,
|
||||
en: en_translation,
|
||||
},
|
||||
});
|
||||
|
||||
export const supportedLocales = ['en', 'zh'];
|
||||
|
||||
export default i18next;
|
@ -1,23 +0,0 @@
|
||||
# i18n
|
||||
|
||||
## How it was done
|
||||
|
||||
Using the open-source lib [i18next](https://www.i18next.com/).
|
||||
|
||||
## How to add your own language
|
||||
|
||||
1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned
|
||||
in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES`
|
||||
and `pt-BR`)
|
||||
.
|
||||
2. Copy the content from an existing translation and start to translate.
|
||||
3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the
|
||||
already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function.
|
||||
4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to
|
||||
be `sudo-askpass.osascript-xx.js` and edit
|
||||
the `'balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and
|
||||
those `Ok`s and `Cancel`s to your own language.
|
||||
5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW`
|
||||
, or `pt-BR` and `pt-PT`, edit
|
||||
the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need.
|
||||
6. Make a commit, and then a pull request on GitHub.
|
@ -1,162 +0,0 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
continue: 'Continue',
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
skip: 'Skip',
|
||||
sure: "Yes, I'm sure",
|
||||
warning: 'WARNING! ',
|
||||
attention: 'Attention',
|
||||
failed: 'Failed',
|
||||
completed: 'Completed',
|
||||
yesContinue: 'Yes, continue',
|
||||
reallyExit: 'Are you sure you want to close Etcher?',
|
||||
yesExit: 'Yes, quit',
|
||||
progress: {
|
||||
starting: 'Starting...',
|
||||
decompressing: 'Decompressing...',
|
||||
flashing: 'Flashing...',
|
||||
finishing: 'Finishing...',
|
||||
verifying: 'Validating...',
|
||||
failing: 'Failed',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: 'Not recommended',
|
||||
tooSmall: 'Too small',
|
||||
locked: 'Locked',
|
||||
system: 'System drive',
|
||||
containsImage: 'Source drive',
|
||||
largeDrive: 'Large drive',
|
||||
sourceLarger: 'The selected source is {{byte}} larger than this drive.',
|
||||
flashSucceed_one: 'Successful target',
|
||||
flashSucceed_other: 'Successful targets',
|
||||
flashFail_one: 'Failed target',
|
||||
flashFail_other: 'Failed targets',
|
||||
toDrive: 'to {{description}} ({{name}})',
|
||||
toTarget_one: 'to {{num}} target',
|
||||
toTarget_other: 'to {{num}} targets',
|
||||
andFailTarget_one: 'and failed to be flashed to {{num}} target',
|
||||
andFailTarget_other: 'and failed to be flashed to {{num}} targets',
|
||||
succeedTo: '{{name}} was successfully flashed {{target}}',
|
||||
exitWhileFlashing:
|
||||
'You are currently flashing a drive. Closing Etcher may leave your drive in an unusable state.',
|
||||
looksLikeWindowsImage:
|
||||
'It looks like you are trying to burn a Windows image.\n\nUnlike other images, Windows images require special processing to be made bootable. We suggest you use a tool specially designed for this purpose, such as <a href="https://rufus.akeo.ie">Rufus</a> (Windows), <a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux), or Boot Camp Assistant (macOS).',
|
||||
image: 'image',
|
||||
drive: 'drive',
|
||||
missingPartitionTable:
|
||||
'It looks like this is not a bootable {{type}}.\n\nThe {{type}} does not appear to contain a partition table, and might not be recognized or bootable by your device.',
|
||||
largeDriveSize:
|
||||
"This is a large drive! Make sure it doesn't contain files that you want to keep.",
|
||||
systemDrive:
|
||||
'Selecting your system drive is dangerous and will erase your drive!',
|
||||
sourceDrive: 'Contains the image you chose to flash',
|
||||
noSpace:
|
||||
'Not enough space on the drive. Please insert larger one and try again.',
|
||||
genericFlashError:
|
||||
'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n{{error}}',
|
||||
validation:
|
||||
'The write has been completed successfully but Etcher detected potential corruption issues when reading the image back from the drive. \n\nPlease consider writing the image to a different drive.',
|
||||
openError:
|
||||
'Something went wrong while opening {{source}}.\n\nError: {{error}}',
|
||||
flashError: 'Something went wrong while writing {{image}} {{targets}}.',
|
||||
unplug:
|
||||
"Looks like Etcher lost access to the drive. Did it get unplugged accidentally?\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
|
||||
cannotWrite:
|
||||
'Looks like Etcher is not able to write to this location of the drive. This error is usually caused by a faulty drive, reader, or port. \n\nPlease try again with another drive, reader, or port.',
|
||||
childWriterDied:
|
||||
'The writer process ended unexpectedly. Please try again, and contact the Etcher team if the problem persists.',
|
||||
badProtocol: 'Only http:// and https:// URLs are supported.',
|
||||
},
|
||||
target: {
|
||||
selectTarget: 'Select target',
|
||||
plugTarget: 'Plug a target drive',
|
||||
targets: 'Targets',
|
||||
change: 'Change',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: 'Use Image URL',
|
||||
auth: 'Authentication',
|
||||
username: 'Enter username',
|
||||
password: 'Enter password',
|
||||
unsupportedProtocol: 'Unsupported protocol',
|
||||
windowsImage: 'Possible Windows image detected',
|
||||
partitionTable: 'Missing partition table',
|
||||
errorOpen: 'Error opening source',
|
||||
fromFile: 'Flash from file',
|
||||
fromURL: 'Flash from URL',
|
||||
clone: 'Clone drive',
|
||||
image: 'Image',
|
||||
name: 'Name: ',
|
||||
path: 'Path: ',
|
||||
selectSource: 'Select source',
|
||||
plugSource: 'Plug a source drive',
|
||||
osImages: 'OS Images',
|
||||
allFiles: 'All',
|
||||
enterValidURL: 'Enter a valid URL',
|
||||
},
|
||||
drives: {
|
||||
name: 'Name',
|
||||
size: 'Size',
|
||||
location: 'Location',
|
||||
find: '{{length}} found',
|
||||
select: 'Select {{select}}',
|
||||
showHidden: 'Show {{num}} hidden',
|
||||
systemDriveDanger:
|
||||
'Selecting your system drive is dangerous and will erase your drive!',
|
||||
openInBrowser: '`Etcher will open {{link}} in your browser`',
|
||||
changeTarget: 'Change target',
|
||||
largeDriveWarning: 'You are about to erase an unusually large drive',
|
||||
largeDriveWarningMsg:
|
||||
'Are you sure the selected drive is not a storage drive?',
|
||||
systemDriveWarning: "You are about to erase your computer's drives",
|
||||
systemDriveWarningMsg:
|
||||
'Are you sure you want to flash your system drive?',
|
||||
},
|
||||
flash: {
|
||||
another: 'Flash another',
|
||||
target: 'Target',
|
||||
location: 'Location',
|
||||
error: 'Error',
|
||||
flash: 'Flash',
|
||||
flashNow: 'Flash!',
|
||||
skip: 'Validation has been skipped',
|
||||
moreInfo: 'more info',
|
||||
speedTip:
|
||||
'The speed is calculated by dividing the image size by the flashing time.\nDisk images with ext partitions flash faster as we are able to skip unused parts.',
|
||||
speed: 'Effective speed: {{speed}} MB/s',
|
||||
speedShort: '{{speed}} MB/s',
|
||||
eta: 'ETA: {{eta}}',
|
||||
failedTarget: 'Failed targets',
|
||||
failedRetry: 'Retry failed targets',
|
||||
flashFailed: 'Flash Failed.',
|
||||
flashCompleted: 'Flash Completed!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting:
|
||||
'Anonymously report errors and usage statistics to balena.io',
|
||||
autoUpdate: 'Auto-updates enabled',
|
||||
settings: 'Settings',
|
||||
systemInformation: 'System Information',
|
||||
trimExtPartitions:
|
||||
'Trim unallocated space on raw images (in ext-type partitions)',
|
||||
},
|
||||
menu: {
|
||||
edit: 'Edit',
|
||||
view: 'View',
|
||||
devTool: 'Toggle Developer Tools',
|
||||
window: 'Window',
|
||||
help: 'Help',
|
||||
pro: 'Etcher Pro',
|
||||
website: 'Etcher Website',
|
||||
issue: 'Report an issue',
|
||||
about: 'About Etcher',
|
||||
hide: 'Hide Etcher',
|
||||
hideOthers: 'Hide Others',
|
||||
unhide: 'Unhide All',
|
||||
quit: 'Quit Etcher',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
@ -1,152 +0,0 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
ok: '好',
|
||||
cancel: '取消',
|
||||
continue: '继续',
|
||||
skip: '跳过',
|
||||
sure: '我确定',
|
||||
warning: '请注意!',
|
||||
attention: '请注意',
|
||||
failed: '失败',
|
||||
completed: '完毕',
|
||||
yesExit: '是的,可以退出',
|
||||
reallyExit: '真的要现在退出 Etcher 吗?',
|
||||
yesContinue: '是的,继续',
|
||||
progress: {
|
||||
starting: '正在启动……',
|
||||
decompressing: '正在解压……',
|
||||
flashing: '正在烧录……',
|
||||
finishing: '正在结束……',
|
||||
verifying: '正在验证……',
|
||||
failing: '失败……',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: '大小不推荐',
|
||||
tooSmall: '空间太小',
|
||||
locked: '被锁定',
|
||||
system: '系统盘',
|
||||
containsImage: '存放源镜像',
|
||||
largeDrive: '很大的磁盘',
|
||||
sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。',
|
||||
flashSucceed_one: '烧录成功',
|
||||
flashSucceed_other: '烧录成功',
|
||||
flashFail_one: '烧录失败',
|
||||
flashFail_other: '烧录失败',
|
||||
toDrive: '到 {{description}} ({{name}})',
|
||||
toTarget_one: '到 {{num}} 个目标',
|
||||
toTarget_other: '到 {{num}} 个目标',
|
||||
andFailTarget_one: '并烧录失败了 {{num}} 个目标',
|
||||
andFailTarget_other: '并烧录失败了 {{num}} 个目标',
|
||||
succeedTo: '{{name}} 被成功烧录 {{target}}',
|
||||
exitWhileFlashing:
|
||||
'您当前正在刷机。 关闭 Etcher 可能会导致您的磁盘无法使用。',
|
||||
looksLikeWindowsImage:
|
||||
'看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同,Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
|
||||
image: '镜像',
|
||||
drive: '磁盘',
|
||||
missingPartitionTable:
|
||||
'看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。',
|
||||
largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息',
|
||||
systemDrive: '选择系统盘很危险,因为这将会删除你的系统',
|
||||
sourceDrive: '源镜像位于这个分区中',
|
||||
noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。',
|
||||
genericFlashError:
|
||||
'出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}',
|
||||
validation:
|
||||
'写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。',
|
||||
openError: '打开 {{source}} 时出错。\n\n错误信息: {{error}}',
|
||||
flashError: '烧录 {{image}} {{targets}} 失败。',
|
||||
unplug:
|
||||
'看起来 Etcher 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。',
|
||||
cannotWrite:
|
||||
'看起来 Etcher 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。',
|
||||
childWriterDied:
|
||||
'写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher 团队。',
|
||||
badProtocol: '仅支持 http:// 和 https:// 开头的网址。',
|
||||
},
|
||||
target: {
|
||||
selectTarget: '选择目标磁盘',
|
||||
plugTarget: '请插入目标磁盘',
|
||||
targets: '个目标',
|
||||
change: '更改',
|
||||
},
|
||||
menu: {
|
||||
edit: '编辑',
|
||||
view: '视图',
|
||||
devTool: '打开开发者工具',
|
||||
window: '窗口',
|
||||
help: '帮助',
|
||||
pro: 'Etcher 专业版',
|
||||
website: 'Etcher 的官网',
|
||||
issue: '提交一个 issue',
|
||||
about: '关于 Etcher',
|
||||
hide: '隐藏 Etcher',
|
||||
hideOthers: '隐藏其它窗口',
|
||||
unhide: '取消隐藏',
|
||||
quit: '退出 Etcher',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: '使用镜像网络地址',
|
||||
auth: '验证',
|
||||
username: '输入用户名',
|
||||
password: '输入密码',
|
||||
unsupportedProtocol: '不支持的协议',
|
||||
windowsImage: '这可能是 Windows 系统镜像',
|
||||
partitionTable: '找不到分区表',
|
||||
errorOpen: '打开源镜像时出错',
|
||||
fromFile: '从文件烧录',
|
||||
fromURL: '从在线地址烧录',
|
||||
clone: '克隆磁盘',
|
||||
image: '镜像信息',
|
||||
name: '名称:',
|
||||
path: '路径:',
|
||||
selectSource: '选择源',
|
||||
plugSource: '请插入源磁盘',
|
||||
osImages: '系统镜像格式',
|
||||
allFiles: '任何文件格式',
|
||||
enterValidURL: '请输入一个正确的地址',
|
||||
},
|
||||
drives: {
|
||||
name: '名称',
|
||||
size: '大小',
|
||||
location: '位置',
|
||||
find: '找到 {{length}} 个',
|
||||
select: '选定 {{select}}',
|
||||
showHidden: '显示 {{num}} 个隐藏的磁盘',
|
||||
systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!',
|
||||
openInBrowser: 'Etcher 会在浏览器中打开 {{link}}',
|
||||
changeTarget: '改变目标',
|
||||
largeDriveWarning: '您即将擦除一个非常大的磁盘',
|
||||
largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?',
|
||||
systemDriveWarning: '您将要擦除系统盘',
|
||||
systemDriveWarningMsg: '您确定要烧录到系统盘吗?',
|
||||
},
|
||||
flash: {
|
||||
another: '烧录另一目标',
|
||||
target: '目标',
|
||||
location: '位置',
|
||||
error: '错误',
|
||||
flash: '烧录',
|
||||
flashNow: '现在烧录!',
|
||||
skip: '跳过了验证',
|
||||
moreInfo: '更多信息',
|
||||
speedTip:
|
||||
'通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分,因此具有EXT分区的磁盘镜像烧录速度更快。',
|
||||
speed: '速度:{{speed}} MB/秒',
|
||||
speedShort: '{{speed}} MB/秒',
|
||||
eta: '预计还需要:{{eta}}',
|
||||
failedTarget: '失败的烧录目标',
|
||||
failedRetry: '重试烧录失败目标',
|
||||
flashFailed: '烧录失败。',
|
||||
flashCompleted: '烧录成功!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: '匿名地向 balena.io 报告运行错误和使用统计',
|
||||
autoUpdate: '自动更新',
|
||||
settings: '软件设置',
|
||||
systemInformation: '系统信息',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
@ -1,154 +0,0 @@
|
||||
const translation = {
|
||||
translation: {
|
||||
continue: '繼續',
|
||||
ok: '好',
|
||||
cancel: '取消',
|
||||
skip: '跳過',
|
||||
sure: '我確定',
|
||||
warning: '請注意!',
|
||||
attention: '請注意',
|
||||
failed: '失敗',
|
||||
completed: '完成',
|
||||
yesContinue: '是的,繼續',
|
||||
reallyExit: '真的要現在結束 Etcher 嗎?',
|
||||
yesExit: '是的,可以結束',
|
||||
progress: {
|
||||
starting: '正在啟動……',
|
||||
decompressing: '正在解壓縮……',
|
||||
flashing: '正在燒錄……',
|
||||
finishing: '正在結束……',
|
||||
verifying: '正在驗證……',
|
||||
failing: '失敗……',
|
||||
},
|
||||
message: {
|
||||
sizeNotRecommended: '大小不建議',
|
||||
tooSmall: '空間太小',
|
||||
locked: '被鎖定',
|
||||
system: '系統',
|
||||
containsImage: '存放來源映像檔',
|
||||
largeDrive: '很大的磁碟',
|
||||
sourceLarger: '所選的映像檔比目標磁碟大了 {{byte}} 位元組。',
|
||||
flashSucceed_one: '燒錄成功',
|
||||
flashSucceed_other: '燒錄成功',
|
||||
flashFail_one: '燒錄失敗',
|
||||
flashFail_other: '燒錄失敗',
|
||||
toDrive: '到 {{description}} ({{name}})',
|
||||
toTarget_one: '到 {{num}} 個目標',
|
||||
toTarget_other: '到 {{num}} 個目標',
|
||||
andFailTarget_one: '並燒錄失敗了 {{num}} 個目標',
|
||||
andFailTarget_other: '並燒錄失敗了 {{num}} 個目標',
|
||||
succeedTo: '{{name}} 被成功燒錄 {{target}}',
|
||||
exitWhileFlashing:
|
||||
'您目前正在刷寫。關閉 Etcher 可能會導致您的磁碟無法使用。',
|
||||
looksLikeWindowsImage:
|
||||
'看起來您正在嘗試燒錄 Windows 映像檔。\n\n與其他映像檔不同,Windows 映像檔需要特殊處理才能使其可啟動。我們建議您使用專門為此目的設計的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
|
||||
image: '映像檔',
|
||||
drive: '磁碟',
|
||||
missingPartitionTable:
|
||||
'看起來這不是一個可啟動的{{type}}。\n\n這個{{type}}似乎不包含分割表,因此您的設備可能無法識別或無法正確啟動。',
|
||||
largeDriveSize:
|
||||
'這是個很大容量的磁碟!請檢查並確認它不包含對您來說存放很重要的資料',
|
||||
systemDrive: '選擇系統分割區很危險,因為這將會刪除你的系統',
|
||||
sourceDrive: '來源映像檔位於這個分割區中',
|
||||
noSpace: '磁碟空間不足。請插入另一個較大的磁碟並重試。',
|
||||
genericFlashError:
|
||||
'出了點問題。如果來源映像檔曾被壓縮過,請檢查它是否已損壞。\n{{error}}',
|
||||
validation:
|
||||
'寫入已成功完成,但 Etcher 在從磁碟讀取映像檔時檢測到潛在的損壞問題。\n\n請考慮將映像檔寫入其他磁碟。',
|
||||
openError: '打開 {{source}} 時發生錯誤。\n\n錯誤訊息: {{error}}',
|
||||
flashError: '燒錄 {{image}} {{targets}} 失敗。',
|
||||
unplug:
|
||||
'看起來 Etcher 失去了對磁碟的連接。是不是被意外拔掉了?\n\n有時這個錯誤是因為讀卡器出了故障。',
|
||||
cannotWrite:
|
||||
'看起來 Etcher 無法寫入磁碟的這個位置。此錯誤通常是由故障的磁碟、讀取器或連接埠引起的。\n\n請使用其他磁碟、讀卡器或連接埠重試。',
|
||||
childWriterDied:
|
||||
'寫入處理程序意外崩潰。請再試一次,如果問題仍然存在,請聯絡 Etcher 團隊。',
|
||||
badProtocol: '僅支援 http:// 和 https:// 開頭的網址。',
|
||||
},
|
||||
target: {
|
||||
selectTarget: '選擇目標磁碟',
|
||||
plugTarget: '請插入目標磁碟',
|
||||
targets: '個目標',
|
||||
change: '更改',
|
||||
},
|
||||
source: {
|
||||
useSourceURL: '使用映像檔網址',
|
||||
auth: '驗證',
|
||||
username: '輸入使用者名稱',
|
||||
password: '輸入密碼',
|
||||
unsupportedProtocol: '不支持的通訊協定',
|
||||
windowsImage: '這可能是 Windows 系統映像檔',
|
||||
partitionTable: '找不到分割表',
|
||||
errorOpen: '打開來源映像檔時出錯',
|
||||
fromFile: '從檔案燒錄',
|
||||
fromURL: '從網址燒錄',
|
||||
clone: '再製磁碟',
|
||||
image: '映像檔訊息',
|
||||
name: '名稱:',
|
||||
path: '路徑:',
|
||||
selectSource: '選擇來源',
|
||||
plugSource: '請插入來源磁碟',
|
||||
osImages: '系統映像檔格式',
|
||||
allFiles: '任何檔案格式',
|
||||
enterValidURL: '請輸入正確的網址',
|
||||
},
|
||||
drives: {
|
||||
name: '名稱',
|
||||
size: '大小',
|
||||
location: '位置',
|
||||
find: '找到 {{length}} 個',
|
||||
select: '選取 {{select}}',
|
||||
showHidden: '顯示 {{num}} 個隱藏的磁碟',
|
||||
systemDriveDanger: '選擇系統分割區很危險,因為這將會刪除你的系統!',
|
||||
openInBrowser: 'Etcher 會在瀏覽器中打開 {{link}}',
|
||||
changeTarget: '更改目標',
|
||||
largeDriveWarning: '您即將格式化一個非常大的磁碟',
|
||||
largeDriveWarningMsg: '您確定所選磁碟不是儲存資料的磁碟嗎?',
|
||||
systemDriveWarning: '您將要格式化系統分割區',
|
||||
systemDriveWarningMsg: '您確定要燒錄到系統分割區嗎?',
|
||||
},
|
||||
flash: {
|
||||
another: '燒錄另一目標',
|
||||
target: '目標',
|
||||
location: '位置',
|
||||
error: '錯誤',
|
||||
flash: '燒錄',
|
||||
flashNow: '現在燒錄!',
|
||||
skip: '跳過了驗證',
|
||||
moreInfo: '更多資訊',
|
||||
speedTip:
|
||||
'透過將映像檔大小除以燒錄時間來計算速度。\n由於我們能夠跳過未使用的部分,因此具有 ext 分割區的磁碟映像檔燒錄速度更快。',
|
||||
speed: '速度:{{speed}} MB/秒',
|
||||
speedShort: '{{speed}} MB/秒',
|
||||
eta: '預計還需要:{{eta}}',
|
||||
failedTarget: '目標燒錄失敗',
|
||||
failedRetry: '重試燒錄失敗的目標',
|
||||
flashFailed: '燒錄失敗。',
|
||||
flashCompleted: '燒錄成功!',
|
||||
},
|
||||
settings: {
|
||||
errorReporting: '匿名向 balena.io 回報程式錯誤和使用統計資料',
|
||||
autoUpdate: '自動更新',
|
||||
settings: '軟體設定',
|
||||
systemInformation: '系統資訊',
|
||||
trimExtPartitions: '修改原始映像檔上未分配的空間(在 ext 類型分割區中)',
|
||||
},
|
||||
menu: {
|
||||
edit: '編輯',
|
||||
view: '預覽',
|
||||
devTool: '打開開發者工具',
|
||||
window: '視窗',
|
||||
help: '協助',
|
||||
pro: 'Etcher 專業版',
|
||||
website: 'Etcher 的官網',
|
||||
issue: '提交 issue',
|
||||
about: '關於 Etcher',
|
||||
hide: '隱藏 Etcher',
|
||||
hideOthers: '隱藏其它視窗',
|
||||
unhide: '取消隱藏',
|
||||
quit: '結束 Etcher',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default translation;
|
12
lib/gui/app/index.dev.html
Normal file
12
lib/gui/app/index.dev.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>balenaEtcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="main"></main>
|
||||
<script src="http://localhost:3030/gui.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -3,8 +3,10 @@
|
||||
<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>
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
export function hasAvailableDrives() {
|
||||
|
@ -15,9 +15,10 @@
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import type * as sdk from 'etcher-sdk';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
|
||||
import { bytesToMegabytes } from '../../../shared/units';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
@ -70,7 +71,6 @@ export function unsetFlashingFlag(results: {
|
||||
data: results,
|
||||
});
|
||||
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||
|
||||
electron.ipcRenderer.send('enable-screensaver');
|
||||
}
|
||||
|
||||
|
@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import type { AnimationFunction, Color } from 'sys-class-rgb-led';
|
||||
import { Animator, RGBLed } from 'sys-class-rgb-led';
|
||||
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { isSourceDrive } from '../../../shared/drive-constraints';
|
||||
import {
|
||||
DrivelistDrive,
|
||||
isSourceDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import { getDrives } from './available-drives';
|
||||
import { getSelectedDrives } from './selection-state';
|
||||
import * as settings from './settings';
|
||||
@ -42,7 +43,7 @@ function blink(t: number) {
|
||||
return Math.floor(t) % 2;
|
||||
}
|
||||
|
||||
function one() {
|
||||
function one(_t: number) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
@ -15,7 +15,7 @@ import type { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { SourceMetadata } from '../../../shared/typings/source-selector';
|
||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
|
||||
import * as availableDrives from './available-drives';
|
||||
import { Actions, store } from './store';
|
||||
|
@ -41,10 +41,11 @@ export const DEFAULT_HEIGHT = 480;
|
||||
* NOTE: We use the remote property when this module
|
||||
* is loaded in the Electron's renderer process
|
||||
*/
|
||||
function getConfigPath() {
|
||||
const app = electron.app || require('@electron/remote').app;
|
||||
return join(app.getPath('userData'), 'config.json');
|
||||
}
|
||||
const app = electron.app || electron.remote.app;
|
||||
|
||||
const USER_DATA_DIR = app.getPath('userData');
|
||||
|
||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
let contents = '{}';
|
||||
@ -63,7 +64,7 @@ async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
|
||||
// exported for tests
|
||||
export async function readAll() {
|
||||
return await readConfigFile(getConfigPath());
|
||||
return await readConfigFile(CONFIG_PATH);
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
@ -102,7 +103,7 @@ export async function set(
|
||||
const previousValue = settings[key];
|
||||
settings[key] = value;
|
||||
try {
|
||||
await writeConfigFileFn(getConfigPath(), settings);
|
||||
await writeConfigFileFn(CONFIG_PATH, settings);
|
||||
} catch (error: any) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
|
@ -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 except if autoSelectAllDrives is true
|
||||
// We don't want to auto-select large drives execpt is autoSelectAllDrives is true
|
||||
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
|
||||
// We don't want to auto-select system drives
|
||||
!constraints.isSystemDrive(drive)
|
||||
|
@ -14,192 +14,85 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { findLastIndex, once } from 'lodash';
|
||||
import type { Client } from 'analytics-client';
|
||||
import { createClient, createNoopClient } from 'analytics-client';
|
||||
import * as SentryRenderer from '@sentry/electron/renderer';
|
||||
import * as _ from 'lodash';
|
||||
import * as resinCorvus from 'resin-corvus/browser';
|
||||
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import { getConfig } from '../../../shared/utils';
|
||||
import * as settings from '../models/settings';
|
||||
import { store } from '../models/store';
|
||||
import { version } from '../../../../package.json';
|
||||
|
||||
type AnalyticsPayload = _.Dictionary<any>;
|
||||
const DEFAULT_PROBABILITY = 0.1;
|
||||
|
||||
const clearUserPath = (filename: string): string => {
|
||||
const generatedFile = filename.split('generated').reverse()[0];
|
||||
return generatedFile !== filename ? `generated${generatedFile}` : filename;
|
||||
};
|
||||
|
||||
export const anonymizeSentryData = (
|
||||
event: SentryRenderer.Event,
|
||||
): SentryRenderer.Event => {
|
||||
event.exception?.values?.forEach((exception) => {
|
||||
exception.stacktrace?.frames?.forEach((frame) => {
|
||||
if (frame.filename) {
|
||||
frame.filename = clearUserPath(frame.filename);
|
||||
}
|
||||
});
|
||||
async function installCorvus(): Promise<void> {
|
||||
const sentryToken =
|
||||
(await settings.get('analyticsSentryToken')) ||
|
||||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
|
||||
const mixpanelToken =
|
||||
(await settings.get('analyticsMixpanelToken')) ||
|
||||
_.get(packageJSON, ['analytics', 'mixpanel', 'token']);
|
||||
resinCorvus.install({
|
||||
services: {
|
||||
sentry: sentryToken,
|
||||
mixpanel: mixpanelToken,
|
||||
},
|
||||
options: {
|
||||
release: packageJSON.version,
|
||||
shouldReport: () => {
|
||||
return settings.getSync('errorReporting');
|
||||
},
|
||||
mixpanelDeferred: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
event.breadcrumbs?.forEach((breadcrumb) => {
|
||||
if (breadcrumb.data?.url) {
|
||||
breadcrumb.data.url = clearUserPath(breadcrumb.data.url);
|
||||
}
|
||||
});
|
||||
let mixpanelSample = DEFAULT_PROBABILITY;
|
||||
|
||||
if (event.request?.url) {
|
||||
event.request.url = clearUserPath(event.request.url);
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
const extractPathRegex = /(.*)(^|\s)(file:\/\/)?(\w:)?([\\/].+)/;
|
||||
const etcherSegmentMarkers = ['app.asar', 'Resources'];
|
||||
|
||||
export const anonymizePath = (input: string) => {
|
||||
// First, extract a part of the value that matches a path pattern.
|
||||
const match = extractPathRegex.exec(input);
|
||||
if (match === null) {
|
||||
return input;
|
||||
}
|
||||
const mainPart = match[5];
|
||||
const space = match[2];
|
||||
const beginning = match[1];
|
||||
const uriPrefix = match[3] || '';
|
||||
|
||||
// We have to deal with both Windows and POSIX here.
|
||||
// The path starts with its separator (we work with absolute paths).
|
||||
const sep = mainPart[0];
|
||||
const segments = mainPart.split(sep);
|
||||
|
||||
// Moving from the end, find the first marker and cut the path from there.
|
||||
const startCutIndex = findLastIndex(segments, (segment) =>
|
||||
etcherSegmentMarkers.includes(segment),
|
||||
);
|
||||
return (
|
||||
beginning +
|
||||
space +
|
||||
uriPrefix +
|
||||
'[PERSONAL PATH]' +
|
||||
sep +
|
||||
segments.splice(startCutIndex).join(sep)
|
||||
);
|
||||
};
|
||||
|
||||
const safeAnonymizePath = (input: string) => {
|
||||
try {
|
||||
return anonymizePath(input);
|
||||
} catch (e) {
|
||||
return '[ANONYMIZE PATH FAILED]';
|
||||
}
|
||||
};
|
||||
|
||||
const sensitiveEtcherProperties = [
|
||||
'error.description',
|
||||
'error.message',
|
||||
'error.stack',
|
||||
'image',
|
||||
'image.path',
|
||||
'path',
|
||||
];
|
||||
|
||||
export const anonymizeAnalyticsPayload = (
|
||||
data: AnalyticsPayload,
|
||||
): AnalyticsPayload => {
|
||||
for (const prop of sensitiveEtcherProperties) {
|
||||
const value = data[prop];
|
||||
if (value != null) {
|
||||
data[prop] = safeAnonymizePath(value.toString());
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
let analyticsClient: Client;
|
||||
/**
|
||||
* @summary Init analytics configurations
|
||||
*/
|
||||
export const initAnalytics = once(() => {
|
||||
const dsn =
|
||||
settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
|
||||
SentryRenderer.init({
|
||||
dsn,
|
||||
beforeSend: anonymizeSentryData,
|
||||
debug: process.env.ETCHER_SENTRY_DEBUG === 'true',
|
||||
});
|
||||
|
||||
const projectName =
|
||||
settings.getSync('analyticsAmplitudeToken') || process.env.AMPLITUDE_TOKEN;
|
||||
|
||||
const clientConfig = {
|
||||
projectName,
|
||||
endpoint: 'data.balena-cloud.com',
|
||||
componentName: 'etcher',
|
||||
componentVersion: version,
|
||||
};
|
||||
analyticsClient = projectName
|
||||
? createClient(clientConfig)
|
||||
: createNoopClient();
|
||||
});
|
||||
|
||||
const getCircularReplacer = () => {
|
||||
const seen = new WeakSet();
|
||||
return (_key: any, value: any) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return;
|
||||
}
|
||||
seen.add(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
function flattenObject(obj: any) {
|
||||
const toReturn: AnalyticsPayload = {};
|
||||
|
||||
for (const i in obj) {
|
||||
if (!Object.prototype.hasOwnProperty.call(obj, i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj[i])) {
|
||||
toReturn[i] = obj[i];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof obj[i] === 'object' && obj[i] !== null) {
|
||||
const flatObject = flattenObject(obj[i]);
|
||||
for (const x in flatObject) {
|
||||
if (!Object.prototype.hasOwnProperty.call(flatObject, x)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
toReturn[i.toLowerCase() + '.' + x.toLowerCase()] = flatObject[x];
|
||||
}
|
||||
} else {
|
||||
toReturn[i] = obj[i];
|
||||
async function initConfig() {
|
||||
await installCorvus();
|
||||
let validatedConfig = null;
|
||||
try {
|
||||
const configUrl = await settings.get('configUrl');
|
||||
const config = await getConfig(configUrl);
|
||||
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
|
||||
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
|
||||
if (isClientEligible(mixpanelSample)) {
|
||||
validatedConfig = validateMixpanelConfig(mixpanel);
|
||||
}
|
||||
} catch (err) {
|
||||
resinCorvus.logException(err);
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
function formatEvent(data: any): AnalyticsPayload {
|
||||
const event = JSON.parse(JSON.stringify(data, getCircularReplacer()));
|
||||
return anonymizeAnalyticsPayload(flattenObject(event));
|
||||
}
|
||||
|
||||
function reportAnalytics(message: string, data: AnalyticsPayload = {}) {
|
||||
const { applicationSessionUuid, flashingWorkflowUuid } = store
|
||||
.getState()
|
||||
.toJS();
|
||||
|
||||
const event = formatEvent({
|
||||
...data,
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
resinCorvus.setConfigs({
|
||||
mixpanel: validatedConfig,
|
||||
});
|
||||
analyticsClient.track(message, event);
|
||||
}
|
||||
|
||||
initConfig();
|
||||
|
||||
/**
|
||||
* @summary Check that the client is eligible for analytics
|
||||
*/
|
||||
function isClientEligible(probability: number) {
|
||||
return Math.random() < probability;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check that config has at least HTTP_PROTOCOL and api_host
|
||||
*/
|
||||
function validateMixpanelConfig(config: {
|
||||
api_host?: string;
|
||||
HTTP_PROTOCOL?: string;
|
||||
}) {
|
||||
const mixpanelConfig = {
|
||||
api_host: 'https://api.mixpanel.com',
|
||||
};
|
||||
if (config.HTTP_PROTOCOL !== undefined && config.api_host !== undefined) {
|
||||
mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`;
|
||||
}
|
||||
return mixpanelConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,12 +101,16 @@ function reportAnalytics(message: string, data: AnalyticsPayload = {}) {
|
||||
* @description
|
||||
* This function sends the debug message to product analytics services.
|
||||
*/
|
||||
export async function logEvent(message: string, data: AnalyticsPayload = {}) {
|
||||
const shouldReportAnalytics = await settings.get('errorReporting');
|
||||
if (shouldReportAnalytics) {
|
||||
initAnalytics();
|
||||
reportAnalytics(message, data);
|
||||
}
|
||||
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
|
||||
const { applicationSessionUuid, flashingWorkflowUuid } = store
|
||||
.getState()
|
||||
.toJS();
|
||||
resinCorvus.logEvent(message, {
|
||||
...data,
|
||||
sample: mixpanelSample,
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -222,11 +119,4 @@ export async function logEvent(message: string, data: AnalyticsPayload = {}) {
|
||||
* @description
|
||||
* This function logs an exception to error reporting services.
|
||||
*/
|
||||
export function logException(error: any) {
|
||||
const shouldReportErrors = settings.getSync('errorReporting');
|
||||
console.error(error);
|
||||
if (shouldReportErrors) {
|
||||
initAnalytics();
|
||||
SentryRenderer.captureException(error);
|
||||
}
|
||||
}
|
||||
export const logException = resinCorvus.logException;
|
||||
|
@ -1,251 +0,0 @@
|
||||
/** 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 WebSocket from 'ws'; // (no types for wrapper, this is expected)
|
||||
import { spawn, exec } from 'child_process';
|
||||
import * as os from 'os';
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import * as permissions from '../../../shared/permissions';
|
||||
import * as errors from '../../../shared/errors';
|
||||
|
||||
const THREADS_PER_CPU = 16;
|
||||
const connectionRetryDelay = 1000;
|
||||
const connectionRetryAttempts = 10;
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnChild(
|
||||
withPrivileges: boolean,
|
||||
etcherServerId: string,
|
||||
etcherServerAddress: string,
|
||||
etcherServerPort: string,
|
||||
) {
|
||||
const argv = await writerArgv();
|
||||
const env: any = {
|
||||
ETCHER_SERVER_ADDRESS: etcherServerAddress,
|
||||
ETCHER_SERVER_ID: etcherServerId,
|
||||
ETCHER_SERVER_PORT: etcherServerPort,
|
||||
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),
|
||||
};
|
||||
|
||||
if (withPrivileges) {
|
||||
console.log('... with privileges ...');
|
||||
return permissions.elevateCommand(argv, {
|
||||
applicationName: packageJSON.displayName,
|
||||
env,
|
||||
});
|
||||
} else {
|
||||
if (process.platform === 'win32') {
|
||||
// we need to ensure we reset the env as a previous elevation process might have kept them in a wrong state
|
||||
const envCommand = [];
|
||||
for (const key in env) {
|
||||
if (Object.prototype.hasOwnProperty.call(env, key)) {
|
||||
envCommand.push(`set ${key}=${env[key]}`);
|
||||
}
|
||||
}
|
||||
await exec(envCommand.join(' && '));
|
||||
}
|
||||
const spawned = await spawn(argv[0], argv.slice(1), {
|
||||
env,
|
||||
});
|
||||
return { cancelled: false, spawned };
|
||||
}
|
||||
}
|
||||
|
||||
type ChildApi = {
|
||||
emit: (type: string, payload: any) => void;
|
||||
registerHandler: (event: string, handler: any) => void;
|
||||
failed: boolean;
|
||||
};
|
||||
|
||||
async function connectToChildProcess(
|
||||
etcherServerAddress: string,
|
||||
etcherServerPort: string,
|
||||
etcherServerId: string,
|
||||
): Promise<ChildApi | { failed: boolean }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// TODO: default to IPC connections https://github.com/websockets/ws/blob/master/doc/ws.md#ipc-connections
|
||||
// TOOD: use the path as cheap authentication
|
||||
console.log(etcherServerId);
|
||||
|
||||
const url = `ws://${etcherServerAddress}:${etcherServerPort}`;
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
let heartbeat: any;
|
||||
|
||||
const startHeartbeat = (emit: any) => {
|
||||
console.log('start heartbeat');
|
||||
heartbeat = setInterval(() => {
|
||||
emit('heartbeat', {});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const stopHeartbeat = () => {
|
||||
console.log('stop heartbeat');
|
||||
clearInterval(heartbeat);
|
||||
};
|
||||
|
||||
ws.on('error', (error: any) => {
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
resolve({
|
||||
failed: true,
|
||||
});
|
||||
} else {
|
||||
stopHeartbeat();
|
||||
reject({
|
||||
failed: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('open', () => {
|
||||
const emit = (type: string, payload: any) => {
|
||||
ws.send(JSON.stringify({ type, payload }));
|
||||
};
|
||||
|
||||
emit('ready', {});
|
||||
|
||||
// parse and route messages
|
||||
const messagesHandler: any = {
|
||||
log: (message: any) => {
|
||||
console.log(`CHILD LOG: ${message}`);
|
||||
},
|
||||
|
||||
error: (error: any) => {
|
||||
const errorObject = errors.fromJSON(error);
|
||||
console.error('CHILD ERROR', errorObject);
|
||||
stopHeartbeat();
|
||||
},
|
||||
|
||||
// once api is ready (means child process is connected) we pass the emit function to the caller
|
||||
ready: () => {
|
||||
console.log('CHILD READY');
|
||||
|
||||
startHeartbeat(emit);
|
||||
|
||||
resolve({
|
||||
failed: false,
|
||||
emit,
|
||||
registerHandler,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
ws.on('message', (jsonData: any) => {
|
||||
const data = JSON.parse(jsonData);
|
||||
const message = messagesHandler[data.type];
|
||||
if (message) {
|
||||
message(data.payload);
|
||||
} else {
|
||||
throw new Error(`Unknown message type: ${data.type}`);
|
||||
}
|
||||
});
|
||||
|
||||
// api to register more handlers with callbacks
|
||||
const registerHandler = (event: string, handler: any) => {
|
||||
messagesHandler[event] = handler;
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function spawnChildAndConnect({
|
||||
withPrivileges,
|
||||
}: {
|
||||
withPrivileges: boolean;
|
||||
}): Promise<ChildApi> {
|
||||
const etcherServerAddress = process.env.ETCHER_SERVER_ADDRESS ?? '127.0.0.1'; // localhost
|
||||
const etcherServerPort =
|
||||
process.env.ETCHER_SERVER_PORT ?? withPrivileges ? '3435' : '3434';
|
||||
const etcherServerId =
|
||||
process.env.ETCHER_SERVER_ID ??
|
||||
`etcher-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
console.log(
|
||||
`Spawning ${
|
||||
withPrivileges ? 'priviledged' : 'unpriviledged'
|
||||
} sidecar on port ${etcherServerPort}`,
|
||||
);
|
||||
|
||||
// spawn the child process, which will act as the ws server
|
||||
// ETCHER_NO_SPAWN_UTIL can be set to launch a GUI only version of etcher, in that case you'll probably want to set other ENV to match your setup
|
||||
if (!process.env.ETCHER_NO_SPAWN_UTIL) {
|
||||
try {
|
||||
const result = await spawnChild(
|
||||
withPrivileges,
|
||||
etcherServerId,
|
||||
etcherServerAddress,
|
||||
etcherServerPort,
|
||||
);
|
||||
if (result.cancelled) {
|
||||
throw new Error('Spwaning the child process was cancelled');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error spawning child process', error);
|
||||
throw new Error('Error spawning the child process');
|
||||
}
|
||||
}
|
||||
|
||||
// try to connect to the ws server, retrying if necessary, until the connection is established
|
||||
try {
|
||||
let retry = 0;
|
||||
while (retry < connectionRetryAttempts) {
|
||||
const { emit, registerHandler, failed } = await connectToChildProcess(
|
||||
etcherServerAddress,
|
||||
etcherServerPort,
|
||||
etcherServerId,
|
||||
);
|
||||
if (failed) {
|
||||
retry++;
|
||||
console.log(
|
||||
`Retrying to connect to child process in ${connectionRetryDelay}... ${retry} / ${connectionRetryAttempts}`,
|
||||
);
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, connectionRetryDelay),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
return { failed, emit, registerHandler };
|
||||
}
|
||||
throw new Error('Connection to etcher-util timed out');
|
||||
} catch (error) {
|
||||
console.error('Error connecting to child process', error);
|
||||
throw new Error('Connection to etcher-util failed');
|
||||
}
|
||||
}
|
||||
|
||||
export { spawnChildAndConnect };
|
@ -15,8 +15,8 @@
|
||||
*/
|
||||
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import type { Adapter } from 'etcher-sdk/build/scanner/adapters';
|
||||
import {
|
||||
Adapter,
|
||||
BlockDeviceAdapter,
|
||||
UsbbootDeviceAdapter,
|
||||
} from 'etcher-sdk/build/scanner/adapters';
|
||||
@ -30,13 +30,14 @@ 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 && geteuid() === 0)) {
|
||||
if (platform !== 'linux' || 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());
|
||||
}
|
@ -14,17 +14,41 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Drive as DrivelistDrive } from 'drivelist';
|
||||
import type * as sdk from 'etcher-sdk';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import { Dictionary } from 'lodash';
|
||||
import * as 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 type { SourceMetadata } from '../../../shared/typings/source-selector';
|
||||
import * as permissions from '../../../shared/permissions';
|
||||
import { getAppPath } from '../../../shared/utils';
|
||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
import * as flashState from '../models/flash-state';
|
||||
import * as selectionState from '../models/selection-state';
|
||||
import * as settings from '../models/settings';
|
||||
import * as analytics from '../modules/analytics';
|
||||
import * as windowProgress from '../os/window-progress';
|
||||
import { spawnChildAndConnect } from './api';
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @summary Handle a flash error and log it to analytics
|
||||
@ -56,7 +80,51 @@ function handleErrorLogging(
|
||||
}
|
||||
}
|
||||
|
||||
let cancelEmitter: (type: string) => void | undefined;
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
interface FlashResults {
|
||||
skip?: boolean;
|
||||
@ -76,18 +144,22 @@ 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();
|
||||
|
||||
// Spawn the child process with privileges and wait for the connection to be made
|
||||
const { emit, registerHandler } = await spawnChildAndConnect({
|
||||
withPrivileges: true,
|
||||
});
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
// if the connection failed, reject the promise
|
||||
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,
|
||||
@ -96,51 +168,75 @@ async function performWrite(
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
};
|
||||
|
||||
const onFail = ({ device, error }: { device: any; error: any }) => {
|
||||
console.log('fail event');
|
||||
console.log(device);
|
||||
console.log(error);
|
||||
ipc.server.on('fail', ({ device, error }) => {
|
||||
if (device.devicePath) {
|
||||
flashState.addFailedDeviceError({ device, error });
|
||||
}
|
||||
handleErrorLogging(error, analyticsData);
|
||||
finish();
|
||||
};
|
||||
});
|
||||
|
||||
const onDone = (payload: any) => {
|
||||
console.log('CHILD: flash done', payload);
|
||||
payload.results.errors = payload.results.errors.map(
|
||||
ipc.server.on('done', (event) => {
|
||||
event.results.errors = event.results.errors.map(
|
||||
(data: Dictionary<any> & { message: string }) => {
|
||||
return errors.fromJSON(data);
|
||||
},
|
||||
);
|
||||
flashResults.results = payload.results;
|
||||
finish();
|
||||
};
|
||||
flashResults.results = event.results;
|
||||
});
|
||||
|
||||
const onAbort = () => {
|
||||
console.log('CHILD: flash aborted');
|
||||
flashResults.cancelled = true;
|
||||
finish();
|
||||
};
|
||||
ipc.server.on('abort', () => {
|
||||
terminateServer();
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
const onSkip = () => {
|
||||
console.log('CHILD: validation skipped');
|
||||
flashResults.skip = true;
|
||||
finish();
|
||||
};
|
||||
ipc.server.on('skip', () => {
|
||||
terminateServer();
|
||||
skip = true;
|
||||
});
|
||||
|
||||
const 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();
|
||||
}
|
||||
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',
|
||||
@ -148,32 +244,15 @@ async function performWrite(
|
||||
'Please try again, and contact the Etcher team if the problem persists',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(flashResults);
|
||||
};
|
||||
});
|
||||
|
||||
registerHandler('state', onProgress);
|
||||
registerHandler('fail', onFail);
|
||||
registerHandler('done', onDone);
|
||||
registerHandler('abort', onAbort);
|
||||
registerHandler('skip', onSkip);
|
||||
|
||||
cancelEmitter = (cancelStatus: string) => emit('cancel', 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);
|
||||
// Clear the update lock timer to prevent longer
|
||||
// flashing timing it out, and releasing the lock
|
||||
ipc.server.start();
|
||||
});
|
||||
|
||||
// The process continue in the event handler
|
||||
}
|
||||
|
||||
/**
|
||||
@ -190,7 +269,6 @@ export async function flash(
|
||||
}
|
||||
|
||||
await flashState.setFlashingFlag();
|
||||
|
||||
flashState.setDevicePaths(
|
||||
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
||||
);
|
||||
@ -206,22 +284,16 @@ export async function flash(
|
||||
|
||||
analytics.logEvent('Flash', analyticsData);
|
||||
|
||||
// start api and call the flasher
|
||||
try {
|
||||
const result = await write(image, drives, flashState.setProgressState);
|
||||
console.log('got results', result);
|
||||
await flashState.unsetFlashingFlag(result);
|
||||
console.log('removed flashing flag');
|
||||
} catch (error: any) {
|
||||
await flashState.unsetFlashingFlag({
|
||||
cancelled: false,
|
||||
errorCode: error.code,
|
||||
});
|
||||
|
||||
windowProgress.clear();
|
||||
|
||||
const { results = {} } = flashState.getFlashResults();
|
||||
|
||||
const eventData = {
|
||||
...analyticsData,
|
||||
errors: results.errors,
|
||||
@ -232,9 +304,7 @@ export async function flash(
|
||||
analytics.logEvent('Write failed', eventData);
|
||||
throw error;
|
||||
}
|
||||
|
||||
windowProgress.clear();
|
||||
|
||||
if (flashState.wasLastFlashCancelled()) {
|
||||
const eventData = {
|
||||
...analyticsData,
|
||||
@ -257,7 +327,6 @@ 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();
|
||||
@ -272,7 +341,15 @@ export async function cancel(type: string) {
|
||||
};
|
||||
analytics.logEvent('Cancel', analyticsData);
|
||||
|
||||
if (cancelEmitter) {
|
||||
cancelEmitter(status);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as i18next from 'i18next';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
export interface FlashState {
|
||||
active: number;
|
||||
@ -34,48 +33,37 @@ export function fromFlashState({
|
||||
status: string;
|
||||
position?: string;
|
||||
} {
|
||||
console.log(i18next.t('progress.starting'));
|
||||
|
||||
if (type === undefined) {
|
||||
return { status: i18next.t('progress.starting') };
|
||||
return { status: 'Starting...' };
|
||||
} else if (type === 'decompressing') {
|
||||
if (percentage == null) {
|
||||
return { status: i18next.t('progress.decompressing') };
|
||||
return { status: 'Decompressing...' };
|
||||
} else {
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.decompressing'),
|
||||
};
|
||||
return { position: `${percentage}%`, status: 'Decompressing...' };
|
||||
}
|
||||
} else if (type === 'flashing') {
|
||||
if (percentage != null) {
|
||||
if (percentage < 100) {
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.flashing'),
|
||||
};
|
||||
return { position: `${percentage}%`, status: 'Flashing...' };
|
||||
} else {
|
||||
return { status: i18next.t('progress.finishing') };
|
||||
return { status: 'Finishing...' };
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
status: i18next.t('progress.flashing'),
|
||||
status: 'Flashing...',
|
||||
position: `${position ? prettyBytes(position) : ''}`,
|
||||
};
|
||||
}
|
||||
} else if (type === 'verifying') {
|
||||
if (percentage == null) {
|
||||
return { status: i18next.t('progress.verifying') };
|
||||
return { status: 'Validating...' };
|
||||
} else if (percentage < 100) {
|
||||
return {
|
||||
position: `${percentage}%`,
|
||||
status: i18next.t('progress.verifying'),
|
||||
};
|
||||
return { position: `${percentage}%`, status: 'Validating...' };
|
||||
} else {
|
||||
return { status: i18next.t('progress.finishing') };
|
||||
return { status: 'Finishing...' };
|
||||
}
|
||||
}
|
||||
return { status: i18next.t('progress.failing') };
|
||||
return { status: 'Failed' };
|
||||
}
|
||||
|
||||
export function titleFromFlashState(
|
||||
|
@ -15,13 +15,11 @@
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as remote from '@electron/remote';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as settings from '../../../gui/app/models/settings';
|
||||
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
async function mountSourceDrive() {
|
||||
// sourceDrivePath is the name of the link in /dev/disk/by-path
|
||||
@ -55,18 +53,19 @@ export async function selectImage(): Promise<string | undefined> {
|
||||
properties: ['openFile', 'treatPackageAsDirectory'],
|
||||
filters: [
|
||||
{
|
||||
name: i18next.t('source.osImages'),
|
||||
name: 'OS Images',
|
||||
extensions: SUPPORTED_EXTENSIONS,
|
||||
},
|
||||
{
|
||||
name: i18next.t('source.allFiles'),
|
||||
name: 'All',
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
};
|
||||
const currentWindow = remote.getCurrentWindow();
|
||||
const [file] = (await remote.dialog.showOpenDialog(currentWindow, options))
|
||||
.filePaths;
|
||||
const currentWindow = electron.remote.getCurrentWindow();
|
||||
const [file] = (
|
||||
await electron.remote.dialog.showOpenDialog(currentWindow, options)
|
||||
).filePaths;
|
||||
return file;
|
||||
}
|
||||
|
||||
@ -80,8 +79,8 @@ export async function showWarning(options: {
|
||||
description: string;
|
||||
}): Promise<boolean> {
|
||||
_.defaults(options, {
|
||||
confirmationLabel: i18next.t('ok'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
confirmationLabel: 'OK',
|
||||
rejectionLabel: 'Cancel',
|
||||
});
|
||||
|
||||
const BUTTONS = [options.confirmationLabel, options.rejectionLabel];
|
||||
@ -92,14 +91,14 @@ export async function showWarning(options: {
|
||||
);
|
||||
const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel);
|
||||
|
||||
const { response } = await remote.dialog.showMessageBox(
|
||||
remote.getCurrentWindow(),
|
||||
const { response } = await electron.remote.dialog.showMessageBox(
|
||||
electron.remote.getCurrentWindow(),
|
||||
{
|
||||
type: 'warning',
|
||||
buttons: BUTTONS,
|
||||
defaultId: BUTTON_REJECTION_INDEX,
|
||||
cancelId: BUTTON_REJECTION_INDEX,
|
||||
title: i18next.t('attention'),
|
||||
title: 'Attention',
|
||||
message: options.title,
|
||||
detail: options.description,
|
||||
},
|
||||
@ -113,5 +112,5 @@ export async function showWarning(options: {
|
||||
export function showError(error: Error) {
|
||||
const title = errors.getTitle(error);
|
||||
const message = errors.getDescription(error);
|
||||
remote.dialog.showErrorBox(title, message);
|
||||
electron.remote.dialog.showErrorBox(title, message);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as remote from '@electron/remote';
|
||||
import * as electron from 'electron';
|
||||
|
||||
import * as settings from '../models/settings';
|
||||
|
||||
@ -28,8 +28,8 @@ export async function send(title: string, body: string, icon: string) {
|
||||
}
|
||||
|
||||
// `app.dock` is only defined in OS X
|
||||
if (remote.app.dock) {
|
||||
remote.app.dock.bounce();
|
||||
if (electron.remote.app.dock) {
|
||||
electron.remote.app.dock.bounce();
|
||||
}
|
||||
|
||||
return new window.Notification(title, { body, icon });
|
||||
|
@ -14,11 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as remote from '@electron/remote';
|
||||
import * as electron from 'electron';
|
||||
|
||||
import { percentageToFloat } from '../../../shared/utils';
|
||||
import type { FlashState } from '../modules/progress-status';
|
||||
import { titleFromFlashState } from '../modules/progress-status';
|
||||
import { FlashState, titleFromFlashState } from '../modules/progress-status';
|
||||
|
||||
/**
|
||||
* @summary The title of the main window upon program launch
|
||||
@ -41,7 +40,7 @@ function getWindowTitle(state?: FlashState) {
|
||||
* @description
|
||||
* We expose this property to `this` for testability purposes.
|
||||
*/
|
||||
export const currentWindow = remote.getCurrentWindow();
|
||||
export const currentWindow = electron.remote.getCurrentWindow();
|
||||
|
||||
/**
|
||||
* @summary Set operating system window progress
|
||||
|
@ -27,6 +27,7 @@ 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 {
|
||||
@ -36,7 +37,6 @@ import {
|
||||
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const COMPLETED_PERCENTAGE = 100;
|
||||
const SPEED_PRECISION = 2;
|
||||
@ -94,6 +94,10 @@ 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 {
|
||||
@ -105,7 +109,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);
|
||||
@ -124,6 +128,7 @@ async function flashImageToDrive(
|
||||
return errorMessage;
|
||||
} finally {
|
||||
availableDrives.setDrives([]);
|
||||
driveScanner.start();
|
||||
}
|
||||
|
||||
return '';
|
||||
@ -288,17 +293,9 @@ export class FlashStep extends React.PureComponent<
|
||||
color="#7e8085"
|
||||
width="100%"
|
||||
>
|
||||
<Txt>
|
||||
{i18next.t('flash.speedShort', {
|
||||
speed: this.props.speed.toFixed(SPEED_PRECISION),
|
||||
})}
|
||||
</Txt>
|
||||
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
||||
{!_.isNil(this.props.eta) && (
|
||||
<Txt>
|
||||
{i18next.t('flash.eta', {
|
||||
eta: formatSeconds(this.props.eta),
|
||||
})}
|
||||
</Txt>
|
||||
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
@ -14,21 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg';
|
||||
import CloseSvg from '@fortawesome/fontawesome-free/svgs/solid/x.svg';
|
||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg';
|
||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
|
||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||
|
||||
import * as path from 'path';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import { Alert, Flex, Link } from 'rendition';
|
||||
import { Flex } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import FinishPage from '../../components/finish/finish';
|
||||
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
||||
import { SettingsModal } from '../../components/settings/settings';
|
||||
import { SourceSelector } from '../../components/source-selector/source-selector';
|
||||
import type { SourceMetadata } from '../../../../shared/typings/source-selector';
|
||||
import {
|
||||
SourceMetadata,
|
||||
SourceSelector,
|
||||
} from '../../components/source-selector/source-selector';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
@ -36,7 +37,6 @@ import { observe } from '../../models/store';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import {
|
||||
IconButton as BaseIcon,
|
||||
IconButton,
|
||||
ThemedProvider,
|
||||
} from '../../styled-components';
|
||||
|
||||
@ -48,7 +48,6 @@ import { FlashStep } from './Flash';
|
||||
|
||||
import EtcherSvg from '../../../assets/etcher.svg';
|
||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
import { theme } from '../../theme';
|
||||
|
||||
const Icon = styled(BaseIcon)`
|
||||
margin-right: 20px;
|
||||
@ -100,8 +99,6 @@ const StepBorder = styled.div<{
|
||||
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
|
||||
`;
|
||||
|
||||
const ANALYTICS_ALERT_VISIBILITY_KEY = 'analytics_alert_visible';
|
||||
|
||||
interface MainPageStateFromStore {
|
||||
isFlashing: boolean;
|
||||
hasImage: boolean;
|
||||
@ -118,21 +115,18 @@ interface MainPageState {
|
||||
isWebviewShowing: boolean;
|
||||
hideSettings: boolean;
|
||||
featuredProjectURL?: string;
|
||||
analyticsAlertIsVisible: boolean;
|
||||
}
|
||||
|
||||
export class MainPage extends React.Component<
|
||||
object,
|
||||
{},
|
||||
MainPageState & MainPageStateFromStore
|
||||
> {
|
||||
constructor(props: object) {
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
this.state = {
|
||||
current: 'main',
|
||||
isWebviewShowing: false,
|
||||
hideSettings: true,
|
||||
analyticsAlertIsVisible:
|
||||
localStorage.getItem(ANALYTICS_ALERT_VISIBILITY_KEY) !== 'false',
|
||||
...this.stateHelper(),
|
||||
};
|
||||
}
|
||||
@ -154,20 +148,13 @@ export class MainPage extends React.Component<
|
||||
private async getFeaturedProjectURL() {
|
||||
const url = new URL(
|
||||
(await settings.get('featuredProjectEndpoint')) ||
|
||||
'https://efp.balena.io/index.html',
|
||||
'https://assets.balena.io/etcher-featured/index.html',
|
||||
);
|
||||
url.searchParams.append('borderRight', 'false');
|
||||
url.searchParams.append('darkBackground', 'true');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private hideAnalyticsAlert = () => {
|
||||
if (this.state.analyticsAlertIsVisible) {
|
||||
localStorage.setItem(ANALYTICS_ALERT_VISIBILITY_KEY, 'false');
|
||||
this.setState({ analyticsAlertIsVisible: false });
|
||||
}
|
||||
};
|
||||
|
||||
public async componentDidMount() {
|
||||
observe(() => {
|
||||
this.setState(this.stateHelper());
|
||||
@ -175,17 +162,6 @@ export class MainPage extends React.Component<
|
||||
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
|
||||
}
|
||||
|
||||
public componentDidUpdate(
|
||||
_prevProps: object,
|
||||
prevState: Readonly<MainPageState & MainPageStateFromStore>,
|
||||
) {
|
||||
if (this.state.analyticsAlertIsVisible) {
|
||||
if (prevState.hideSettings !== this.state.hideSettings) {
|
||||
this.setState({ analyticsAlertIsVisible: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private renderMain() {
|
||||
const state = flashState.getFlashState();
|
||||
const shouldDriveStepBeDisabled = !this.state.hasImage;
|
||||
@ -195,127 +171,86 @@ export class MainPage extends React.Component<
|
||||
!this.state.isFlashing || !this.state.isWebviewShowing;
|
||||
return (
|
||||
<Flex
|
||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px 18px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||
flexDirection="column"
|
||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
mb={this.state.analyticsAlertIsVisible ? '0px' : '92px'}
|
||||
>
|
||||
{notFlashingOrSplitView && (
|
||||
<>
|
||||
<SourceSelector
|
||||
flashing={this.state.isFlashing}
|
||||
hideAnalyticsAlert={this.hideAnalyticsAlert}
|
||||
/>
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</Flex>
|
||||
<TargetSelector
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
hideAnalyticsAlert={this.hideAnalyticsAlert}
|
||||
/>
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||
<Flex
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '36.2vw',
|
||||
height: '100vh',
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<ReducedFlashingInfos
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={this.state.imageName}
|
||||
imageSize={
|
||||
typeof this.state.imageSize === 'number'
|
||||
? prettyBytes(this.state.imageSize)
|
||||
: ''
|
||||
}
|
||||
driveTitle={this.state.driveTitle}
|
||||
driveLabel={this.state.driveLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
color: '#fff',
|
||||
left: 35,
|
||||
top: 72,
|
||||
}}
|
||||
/>
|
||||
{notFlashingOrSplitView && (
|
||||
<>
|
||||
<SourceSelector flashing={this.state.isFlashing} />
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</Flex>
|
||||
)}
|
||||
{this.state.isFlashing && this.state.featuredProjectURL && (
|
||||
<SafeWebview
|
||||
src={this.state.featuredProjectURL}
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
<TargetSelector
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
/>
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||
<Flex
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '36.2vw',
|
||||
height: '100vh',
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<ReducedFlashingInfos
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={this.state.imageName}
|
||||
imageSize={
|
||||
typeof this.state.imageSize === 'number'
|
||||
? prettyBytes(this.state.imageSize)
|
||||
: ''
|
||||
}
|
||||
driveTitle={this.state.driveTitle}
|
||||
driveLabel={this.state.driveLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
color: '#fff',
|
||||
left: 35,
|
||||
top: 72,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlashStep
|
||||
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
isFlashing={this.state.isFlashing}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
position={state.position}
|
||||
failed={state.failed}
|
||||
speed={state.speed}
|
||||
eta={state.eta}
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
</Flex>
|
||||
{this.state.analyticsAlertIsVisible && (
|
||||
<Alert mt="18px" style={{ boxShadow: 'none', fontSize: '12px' }}>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<Flex flexDirection="column">
|
||||
<div>
|
||||
Etcher collects a limited amount of anonymous data to help us
|
||||
improve user experience. You can opt out in the{' '}
|
||||
<Link onClick={() => this.setState({ hideSettings: false })}>
|
||||
settings
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
<div>
|
||||
For more information about how we use this data, see our{' '}
|
||||
<Link
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openExternal('https://www.balena.io/privacy-policy');
|
||||
}}
|
||||
>
|
||||
privacy policy
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
</Flex>
|
||||
{/* TODO: can we use onDismiss instead? */}
|
||||
<IconButton onClick={this.hideAnalyticsAlert}>
|
||||
<CloseSvg height="0.75rem" fill={theme.colors.text.main} />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Alert>
|
||||
</Flex>
|
||||
)}
|
||||
{this.state.isFlashing && this.state.featuredProjectURL && (
|
||||
<SafeWebview
|
||||
src={this.state.featuredProjectURL}
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlashStep
|
||||
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
isFlashing={this.state.isFlashing}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
position={state.position}
|
||||
failed={state.failed}
|
||||
speed={state.speed}
|
||||
eta={state.eta}
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@ -378,7 +313,7 @@ export class MainPage extends React.Component<
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
selectionState.getImage()?.supportUrl ||
|
||||
'https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md',
|
||||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
|
||||
)
|
||||
}
|
||||
tabIndex={6}
|
||||
|
@ -1,12 +0,0 @@
|
||||
// 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;
|
@ -1,9 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import { main } from './app';
|
||||
import './i18n';
|
||||
import { langParser } from './i18n';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
ipcRenderer.send('change-lng', langParser());
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', () => {
|
||||
main();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -14,19 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import type {
|
||||
FlexProps,
|
||||
ButtonProps,
|
||||
TableProps as BaseTableProps,
|
||||
} from 'rendition';
|
||||
import {
|
||||
Alert as AlertBase,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Button,
|
||||
ButtonProps,
|
||||
Modal as ModalBase,
|
||||
Provider,
|
||||
Table as BaseTable,
|
||||
TableProps as BaseTableProps,
|
||||
Txt,
|
||||
} from 'rendition';
|
||||
import styled, { css } from 'styled-components';
|
||||
@ -114,25 +113,14 @@ 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;
|
||||
`;
|
||||
|
||||
@ -154,7 +142,7 @@ export const Modal = styled(({ style, children, ...props }) => {
|
||||
{...props}
|
||||
>
|
||||
<ScrollableFlex flexDirection="column" width="100%" height="90%">
|
||||
{children.length ? children.map((c: any) => <>{c}</>) : children}
|
||||
{...children}
|
||||
</ScrollableFlex>
|
||||
</ModalBase>
|
||||
);
|
||||
@ -248,15 +236,16 @@ export interface GenericTableProps<T> extends BaseTableProps<T> {
|
||||
showWarnings?: boolean;
|
||||
}
|
||||
|
||||
function GenericTable<T>(
|
||||
const GenericTable: <T>(
|
||||
props: GenericTableProps<T>,
|
||||
): React.ReactElement<GenericTableProps<T>> {
|
||||
return (
|
||||
<div>
|
||||
<BaseTable<T> ref={props.refFn} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({
|
||||
refFn,
|
||||
...props
|
||||
}: GenericTableProps<T>) => (
|
||||
<div>
|
||||
<BaseTable<T> ref={refFn} {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
function StyledTable<T>() {
|
||||
return styled((props: GenericTableProps<T>) => (
|
||||
@ -295,6 +284,7 @@ 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 {
|
||||
@ -329,7 +319,7 @@ function StyledTable<T>() {
|
||||
`;
|
||||
}
|
||||
|
||||
export const Table = <T extends object>(props: GenericTableProps<T>) => {
|
||||
export const Table = <T extends {}>(props: GenericTableProps<T>) => {
|
||||
const TypedStyledFunctional = StyledTable<T>();
|
||||
return <TypedStyledFunctional {...props} />;
|
||||
};
|
||||
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2022 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dictionary } from 'lodash';
|
||||
|
||||
type BalenaTag = {
|
||||
id: number;
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export class EtcherPro {
|
||||
private supervisorAddr: string;
|
||||
private supervisorKey: string;
|
||||
private tags: Dictionary<string> | undefined;
|
||||
public uuid: string;
|
||||
|
||||
constructor(supervisorAddr: string, supervisorKey: string) {
|
||||
this.supervisorAddr = supervisorAddr;
|
||||
this.supervisorKey = supervisorKey;
|
||||
this.uuid = (process.env.BALENA_DEVICE_UUID ?? 'NO-UUID').substring(0, 7);
|
||||
this.tags = undefined;
|
||||
this.get_tags().then((tags) => (this.tags = tags));
|
||||
}
|
||||
|
||||
async get_tags(): Promise<Dictionary<string>> {
|
||||
const result = await fetch(
|
||||
this.supervisorAddr + '/v2/device/tags?apikey=' + this.supervisorKey,
|
||||
);
|
||||
const parsed = await result.json();
|
||||
if (parsed['status'] === 'success') {
|
||||
return Object.assign(
|
||||
{},
|
||||
...parsed['tags'].map((tag: BalenaTag) => {
|
||||
return { [tag.name]: tag.value };
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public get_serial(): string | undefined {
|
||||
if (this.tags) {
|
||||
return this.tags['Serial'];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function etcherProInfo(): EtcherPro | undefined {
|
||||
const BALENA_SUPERVISOR_ADDRESS = process.env.BALENA_SUPERVISOR_ADDRESS;
|
||||
const BALENA_SUPERVISOR_API_KEY = process.env.BALENA_SUPERVISOR_API_KEY;
|
||||
|
||||
if (BALENA_SUPERVISOR_ADDRESS && BALENA_SUPERVISOR_API_KEY) {
|
||||
return new EtcherPro(BALENA_SUPERVISOR_ADDRESS, BALENA_SUPERVISOR_API_KEY);
|
||||
}
|
||||
return undefined;
|
||||
}
|
@ -14,41 +14,25 @@
|
||||
* 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';
|
||||
import { promises as fs } from 'fs';
|
||||
import { platform } from 'os';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
import { once } from 'lodash';
|
||||
|
||||
import './app/i18n';
|
||||
|
||||
import { packageType, version } from '../../package.json';
|
||||
import * as EXIT_CODES from '../shared/exit-codes';
|
||||
import { delay, getConfig } from '../shared/utils';
|
||||
import * as settings from './app/models/settings';
|
||||
import { logException } from './app/modules/analytics';
|
||||
import { buildWindowMenu } from './menu';
|
||||
import * as i18n from 'i18next';
|
||||
import * as SentryMain from '@sentry/electron/main';
|
||||
import { anonymizeSentryData } from './app/modules/analytics';
|
||||
|
||||
import { delay } from '../shared/utils';
|
||||
|
||||
const customProtocol = 'etcher';
|
||||
const scheme = `${customProtocol}://`;
|
||||
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
|
||||
const packageUpdatable = updatablePackageTypes.includes(packageType);
|
||||
let packageUpdated = false;
|
||||
let mainWindow: any = null;
|
||||
|
||||
remoteMain.initialize();
|
||||
|
||||
async function checkForUpdates(interval: number) {
|
||||
// We use a while loop instead of a setInterval to preserve
|
||||
@ -58,28 +42,20 @@ async function checkForUpdates(interval: number) {
|
||||
try {
|
||||
const release = await autoUpdater.checkForUpdates();
|
||||
const isOutdated =
|
||||
semver.compare(release!.updateInfo.version, version) > 0;
|
||||
const shouldUpdate = release!.updateInfo.stagingPercentage !== 0; // undefined (default) means 100%
|
||||
semver.compare(release.updateInfo.version, version) > 0;
|
||||
const shouldUpdate = release.updateInfo.stagingPercentage !== 0; // undefinded (default) means 100%
|
||||
if (shouldUpdate && isOutdated) {
|
||||
await autoUpdater.downloadUpdate();
|
||||
packageUpdated = true;
|
||||
}
|
||||
} catch (err) {
|
||||
logMainProcessException(err);
|
||||
logException(err);
|
||||
}
|
||||
}
|
||||
await delay(interval);
|
||||
}
|
||||
}
|
||||
|
||||
function logMainProcessException(error: any) {
|
||||
const shouldReportErrors = settings.getSync('errorReporting');
|
||||
console.error(error);
|
||||
if (shouldReportErrors) {
|
||||
SentryMain.captureException(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function isFile(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
@ -114,18 +90,6 @@ async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
|
||||
}
|
||||
}
|
||||
|
||||
const initSentryMain = once(() => {
|
||||
const dsn =
|
||||
settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
|
||||
|
||||
SentryMain.init({
|
||||
dsn,
|
||||
beforeSend: anonymizeSentryData,
|
||||
debug: process.env.ETCHER_SENTRY_DEBUG === 'true',
|
||||
});
|
||||
console.log(SentryMain.getCurrentScope());
|
||||
});
|
||||
|
||||
const sourceSelectorReady = new Promise((resolve) => {
|
||||
electron.ipcMain.on('source-selector-ready', resolve);
|
||||
});
|
||||
@ -149,6 +113,14 @@ 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;
|
||||
@ -158,7 +130,7 @@ async function createMainWindow() {
|
||||
if (fullscreen) {
|
||||
({ width, height } = electron.screen.getPrimaryDisplay().bounds);
|
||||
}
|
||||
mainWindow = new electron.BrowserWindow({
|
||||
const mainWindow = new electron.BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
frame: !fullscreen,
|
||||
@ -179,16 +151,17 @@ async function createMainWindow() {
|
||||
contextIsolation: false,
|
||||
webviewTag: true,
|
||||
zoomFactor: width / defaultWidth,
|
||||
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
|
||||
electron.app.setAsDefaultProtocolClient(customProtocol);
|
||||
|
||||
// mainWindow.setFullScreen(true);
|
||||
buildWindowMenu(mainWindow);
|
||||
mainWindow.setFullScreen(true);
|
||||
|
||||
// Prevent flash of white when starting the application
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
console.timeEnd('ready-to-show');
|
||||
// Electron sometimes caches the zoomFactor
|
||||
// making it obnoxious to switch back-and-forth
|
||||
@ -199,33 +172,47 @@ 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: any) => {
|
||||
mainWindow.webContents.on('will-navigate', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);
|
||||
mainWindow.loadURL(
|
||||
`file://${path.join(
|
||||
'/',
|
||||
...__dirname.split(path.sep).map(encodeURIComponent),
|
||||
'index.html',
|
||||
)}`,
|
||||
);
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
remoteMain.enable(page);
|
||||
|
||||
page.once('did-frame-finish-load', async () => {
|
||||
console.log('packageUpdatable', packageUpdatable);
|
||||
autoUpdater.on('error', (err) => {
|
||||
logMainProcessException(err);
|
||||
logException(err);
|
||||
});
|
||||
if (packageUpdatable) {
|
||||
try {
|
||||
const checkForUpdatesTimer = 300000;
|
||||
const configUrl = await settings.get('configUrl');
|
||||
const onlineConfig = await getConfig(configUrl);
|
||||
const autoUpdaterConfig: AutoUpdaterConfig = onlineConfig?.autoUpdates
|
||||
?.autoUpdaterConfig ?? {
|
||||
autoDownload: false,
|
||||
};
|
||||
for (const [key, value] of Object.entries(autoUpdaterConfig)) {
|
||||
autoUpdater[key as keyof AutoUpdaterConfig] = value;
|
||||
}
|
||||
const checkForUpdatesTimer =
|
||||
onlineConfig?.autoUpdates?.checkForUpdatesTimer ?? 300000;
|
||||
checkForUpdates(checkForUpdatesTimer);
|
||||
} catch (err) {
|
||||
logMainProcessException(err);
|
||||
logException(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
electron.app.allowRendererProcessReuse = false;
|
||||
electron.app.on('window-all-closed', electron.app.quit);
|
||||
|
||||
// Sending a `SIGINT` (e.g: Ctrl-C) to an Electron app that registers
|
||||
@ -239,25 +226,10 @@ 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();
|
||||
} else {
|
||||
initSentryMain();
|
||||
await electron.app.whenReady();
|
||||
const window = await createMainWindow();
|
||||
electron.app.on('second-instance', async (_event, argv) => {
|
||||
@ -268,44 +240,9 @@ async function main(): Promise<void> {
|
||||
await selectImageURL(await getCommandLineURL(argv));
|
||||
});
|
||||
await selectImageURL(await getCommandLineURL(process.argv));
|
||||
|
||||
electron.ipcMain.on('change-lng', function (event, args) {
|
||||
i18n.changeLanguage(args, () => {
|
||||
console.log('Language changed to: ' + args);
|
||||
});
|
||||
if (mainWindow != null) {
|
||||
buildWindowMenu(mainWindow);
|
||||
} else {
|
||||
console.log('Build menu failed. ');
|
||||
}
|
||||
});
|
||||
|
||||
electron.ipcMain.on('webview-dom-ready', (_, id) => {
|
||||
const webview = electron.webContents.fromId(id);
|
||||
|
||||
// Open link in browser if it's opened as a 'foreground-tab'
|
||||
webview!.setWindowOpenHandler((event) => {
|
||||
const url = new URL(event.url);
|
||||
if (
|
||||
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
||||
event.disposition === 'foreground-tab' &&
|
||||
// Don't open links if they're disabled by the env var
|
||||
!settings.getSync('disableExternalLinks')
|
||||
) {
|
||||
electron.shell.openExternal(url.href);
|
||||
}
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
if (require('electron-squirrel-startup')) {
|
||||
electron.app.quit();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
console.time('ready-to-show');
|
||||
|
@ -17,8 +17,6 @@
|
||||
import * as electron from 'electron';
|
||||
import { displayName } from '../../package.json';
|
||||
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
/**
|
||||
* @summary Builds a native application menu for a given window
|
||||
*/
|
||||
@ -44,13 +42,12 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
const menuTemplate: electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
role: 'editMenu',
|
||||
label: i18next.t('menu.edit'),
|
||||
},
|
||||
{
|
||||
label: i18next.t('menu.view'),
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
label: i18next.t('menu.devTool'),
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator:
|
||||
process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I',
|
||||
click: toggleDevTools,
|
||||
@ -59,14 +56,12 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
},
|
||||
{
|
||||
role: 'windowMenu',
|
||||
label: i18next.t('menu.window'),
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
label: i18next.t('menu.help'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18next.t('menu.pro'),
|
||||
label: 'Etcher Pro',
|
||||
click() {
|
||||
electron.shell.openExternal(
|
||||
'https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu',
|
||||
@ -74,13 +69,13 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18next.t('menu.website'),
|
||||
label: 'Etcher Website',
|
||||
click() {
|
||||
electron.shell.openExternal('https://etcher.io?ref=etcher_menu');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18next.t('menu.issue'),
|
||||
label: 'Report an issue',
|
||||
click() {
|
||||
electron.shell.openExternal(
|
||||
'https://github.com/balena-io/etcher/issues',
|
||||
@ -97,29 +92,25 @@ export function buildWindowMenu(window: electron.BrowserWindow) {
|
||||
submenu: [
|
||||
{
|
||||
role: 'about' as const,
|
||||
label: i18next.t('menu.about'),
|
||||
label: 'About Etcher',
|
||||
},
|
||||
{
|
||||
type: 'separator' as const,
|
||||
},
|
||||
{
|
||||
role: 'hide' as const,
|
||||
label: i18next.t('menu.hide'),
|
||||
},
|
||||
{
|
||||
role: 'hideOthers' as const,
|
||||
label: i18next.t('menu.hideOthers'),
|
||||
},
|
||||
{
|
||||
role: 'unhide' as const,
|
||||
label: i18next.t('menu.unhide'),
|
||||
},
|
||||
{
|
||||
type: 'separator' as const,
|
||||
},
|
||||
{
|
||||
role: 'quit' as const,
|
||||
label: i18next.t('menu.quit'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
333
lib/gui/modules/child-writer.ts
Normal file
333
lib/gui/modules/child-writer.ts
Normal file
@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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', {});
|
||||
});
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
//
|
||||
// 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;
|
||||
}
|
62
lib/shared/catalina-sudo/sudo.ts
Normal file
62
lib/shared/catalina-sudo/sudo.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2019 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 { execFile } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { env } from 'process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { getAppPath } from '../utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||
const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`;
|
||||
|
||||
export async function sudo(
|
||||
command: string,
|
||||
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
'sudo',
|
||||
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
PATH: env.PATH,
|
||||
SUDO_ASKPASS: join(
|
||||
getAppPath(),
|
||||
__dirname,
|
||||
'sudo-askpass.osascript.js',
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
return {
|
||||
cancelled: false,
|
||||
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
|
||||
stderr,
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error.code === 1) {
|
||||
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
|
||||
return { cancelled: true };
|
||||
}
|
||||
error.stdout = error.stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
@ -14,12 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Drive } from 'drivelist';
|
||||
import { isNil } from 'lodash';
|
||||
import { Drive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import * as pathIsInside from 'path-is-inside';
|
||||
|
||||
import * as messages from './messages';
|
||||
import type { SourceMetadata } from './typings/source-selector';
|
||||
import { SourceMetadata } from '../gui/app/components/source-selector/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);
|
||||
|
@ -14,19 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { Dictionary } from 'lodash';
|
||||
import { outdent } from 'outdent';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import '../gui/app/i18n';
|
||||
import * as i18next from 'i18next';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
export const progress: Dictionary<(quantity: number) => string> = {
|
||||
successful: (quantity: number) => {
|
||||
return i18next.t('message.flashSucceed', { count: quantity });
|
||||
const plural = quantity === 1 ? '' : 's';
|
||||
return `Successful target${plural}`;
|
||||
},
|
||||
|
||||
failed: (quantity: number) => {
|
||||
return i18next.t('message.flashFail', { count: quantity });
|
||||
const plural = quantity === 1 ? '' : 's';
|
||||
return `Failed target${plural}`;
|
||||
},
|
||||
};
|
||||
|
||||
@ -38,121 +38,129 @@ export const info = {
|
||||
) => {
|
||||
const targets = [];
|
||||
if (failed + successful === 1) {
|
||||
targets.push(
|
||||
i18next.t('message.toDrive', {
|
||||
description: drive.description,
|
||||
name: drive.displayName,
|
||||
}),
|
||||
);
|
||||
targets.push(`to ${drive.description} (${drive.displayName})`);
|
||||
} else {
|
||||
if (successful) {
|
||||
targets.push(
|
||||
i18next.t('message.toTarget', {
|
||||
count: successful,
|
||||
num: successful,
|
||||
}),
|
||||
);
|
||||
const plural = successful === 1 ? '' : 's';
|
||||
targets.push(`to ${successful} target${plural}`);
|
||||
}
|
||||
if (failed) {
|
||||
targets.push(
|
||||
i18next.t('message.andFailTarget', { count: failed, num: failed }),
|
||||
);
|
||||
const plural = failed === 1 ? '' : 's';
|
||||
targets.push(`and failed to be flashed to ${failed} target${plural}`);
|
||||
}
|
||||
}
|
||||
return i18next.t('message.succeedTo', {
|
||||
name: imageBasename,
|
||||
target: targets.join(' '),
|
||||
});
|
||||
return `${imageBasename} was successfully flashed ${targets.join(' ')}`;
|
||||
},
|
||||
};
|
||||
|
||||
export const compatibility = {
|
||||
sizeNotRecommended: () => {
|
||||
return i18next.t('message.sizeNotRecommended');
|
||||
return 'Not recommended';
|
||||
},
|
||||
|
||||
tooSmall: () => {
|
||||
return i18next.t('message.tooSmall');
|
||||
return 'Too small';
|
||||
},
|
||||
|
||||
locked: () => {
|
||||
return i18next.t('message.locked');
|
||||
return 'Locked';
|
||||
},
|
||||
|
||||
system: () => {
|
||||
return i18next.t('message.system');
|
||||
return 'System drive';
|
||||
},
|
||||
|
||||
containsImage: () => {
|
||||
return i18next.t('message.containsImage');
|
||||
return 'Source drive';
|
||||
},
|
||||
|
||||
// The drive is large and therefore likely not a medium you want to write to.
|
||||
largeDrive: () => {
|
||||
return i18next.t('message.largeDrive');
|
||||
return 'Large drive';
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const warning = {
|
||||
tooSmall: (source: { size: number }, target: { size: number }) => {
|
||||
return outdent({ newline: ' ' })`
|
||||
${i18next.t('message.sourceLarger', {
|
||||
byte: prettyBytes(source.size - target.size),
|
||||
})}
|
||||
The selected source is ${prettyBytes(source.size - target.size)}
|
||||
larger than this drive.
|
||||
`;
|
||||
},
|
||||
|
||||
exitWhileFlashing: () => {
|
||||
return i18next.t('message.exitWhileFlashing');
|
||||
return [
|
||||
'You are currently flashing a drive.',
|
||||
'Closing Etcher may leave your drive in an unusable state.',
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
looksLikeWindowsImage: () => {
|
||||
return i18next.t('message.looksLikeWindowsImage');
|
||||
return [
|
||||
'It looks like you are trying to burn a Windows image.\n\n',
|
||||
'Unlike other images, Windows images require special processing to be made bootable.',
|
||||
'We suggest you use a tool specially designed for this purpose, such as',
|
||||
'<a href="https://rufus.akeo.ie">Rufus</a> (Windows),',
|
||||
'<a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux),',
|
||||
'or Boot Camp Assistant (macOS).',
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
missingPartitionTable: () => {
|
||||
return i18next.t('message.missingPartitionTable', {
|
||||
type: i18next.t('message.image'),
|
||||
});
|
||||
return [
|
||||
'It looks like this is not a bootable image.\n\n',
|
||||
'The image does not appear to contain a partition table,',
|
||||
'and might not be recognized or bootable by your device.',
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
driveMissingPartitionTable: () => {
|
||||
return i18next.t('message.missingPartitionTable', {
|
||||
type: i18next.t('message.drive'),
|
||||
});
|
||||
return outdent({ newline: ' ' })`
|
||||
It looks like this is not a bootable drive.
|
||||
The drive does not appear to contain a partition table,
|
||||
and might not be recognized or bootable by your device.
|
||||
`;
|
||||
},
|
||||
|
||||
largeDriveSize: () => {
|
||||
return i18next.t('message.largeDriveSize');
|
||||
return "This is a large drive! Make sure it doesn't contain files that you want to keep.";
|
||||
},
|
||||
|
||||
systemDrive: () => {
|
||||
return i18next.t('message.systemDrive');
|
||||
return 'Selecting your system drive is dangerous and will erase your drive!';
|
||||
},
|
||||
|
||||
sourceDrive: () => {
|
||||
return i18next.t('message.sourceDrive');
|
||||
return 'Contains the image you chose to flash';
|
||||
},
|
||||
};
|
||||
|
||||
export const error = {
|
||||
notEnoughSpaceInDrive: () => {
|
||||
return i18next.t('message.noSpace');
|
||||
return [
|
||||
'Not enough space on the drive.',
|
||||
'Please insert larger one and try again.',
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
genericFlashError: (err: Error) => {
|
||||
return i18next.t('message.genericFlashError', { error: err.message });
|
||||
return `Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n${err.message}`;
|
||||
},
|
||||
|
||||
validation: () => {
|
||||
return i18next.t('message.validation');
|
||||
return [
|
||||
'The write has been completed successfully but Etcher detected potential',
|
||||
'corruption issues when reading the image back from the drive.',
|
||||
'\n\nPlease consider writing the image to a different drive.',
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
openSource: (sourceName: string, errorMessage: string) => {
|
||||
return i18next.t('message.openError', {
|
||||
source: sourceName,
|
||||
error: errorMessage,
|
||||
});
|
||||
return outdent`
|
||||
Something went wrong while opening ${sourceName}
|
||||
|
||||
Error: ${errorMessage}
|
||||
`;
|
||||
},
|
||||
|
||||
flashFailure: (
|
||||
@ -161,33 +169,35 @@ export const error = {
|
||||
) => {
|
||||
const target =
|
||||
drives.length === 1
|
||||
? 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,
|
||||
});
|
||||
? `${drives[0].description} (${drives[0].displayName})`
|
||||
: `${drives.length} targets`;
|
||||
return `Something went wrong while writing ${imageBasename} to ${target}.`;
|
||||
},
|
||||
|
||||
driveUnplugged: () => {
|
||||
return i18next.t('message.unplug');
|
||||
return [
|
||||
'Looks like Etcher lost access to the drive.',
|
||||
'Did it get unplugged accidentally?',
|
||||
"\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
inputOutput: () => {
|
||||
return i18next.t('message.cannotWrite');
|
||||
return [
|
||||
'Looks like Etcher is not able to write to this location of the drive.',
|
||||
'This error is usually caused by a faulty drive, reader, or port.',
|
||||
'\n\nPlease try again with another drive, reader, or port.',
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
childWriterDied: () => {
|
||||
return i18next.t('message.childWriterDied');
|
||||
return [
|
||||
'The writer process ended unexpectedly.',
|
||||
'Please try again, and contact the Etcher team if the problem persists.',
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
unsupportedProtocol: () => {
|
||||
return i18next.t('message.badProtocol');
|
||||
return 'Only http:// and https:// URLs are supported.';
|
||||
},
|
||||
};
|
||||
|
@ -14,27 +14,41 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* This is convoluted and needlessly complex. It should be simplified and modernized.
|
||||
* The environment variable setting and escaping should be greatly simplified by letting {linux|catalina}-sudo handle that.
|
||||
* We shouldn't need to write a script to a file and then execute it. We should be able to forwatd the command to the sudo code directly.
|
||||
*/
|
||||
|
||||
import { spawn, exec } from 'child_process';
|
||||
import * as childProcess from 'child_process';
|
||||
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
||||
import { promises as fs } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as semver from 'semver';
|
||||
import * as sudoPrompt from '@balena/sudo-prompt';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { sudo as darwinSudo } from './sudo/darwin';
|
||||
import { sudo as linuxSudo } from './sudo/linux';
|
||||
import { sudo as winSudo } from './sudo/windows';
|
||||
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
||||
import * as errors from './errors';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const execAsync = promisify(childProcess.exec);
|
||||
const execFileAsync = promisify(childProcess.execFile);
|
||||
|
||||
type Std = string | Buffer | undefined;
|
||||
|
||||
function sudoExecAsync(
|
||||
cmd: string,
|
||||
options: { name: string },
|
||||
): Promise<{ stdout: Std; stderr: Std }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sudoPrompt.exec(
|
||||
cmd,
|
||||
options,
|
||||
(error: Error | undefined, stdout: Std, stderr: Std) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary The user id of the UNIX "superuser"
|
||||
@ -56,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 {
|
||||
@ -111,11 +125,10 @@ export function createLaunchScript(
|
||||
async function elevateScriptWindows(
|
||||
path: string,
|
||||
name: string,
|
||||
env: any,
|
||||
): Promise<{ cancelled: false }> {
|
||||
// '&' needs to be escaped here (but not when written to a .cmd file)
|
||||
const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')].join(' ');
|
||||
await winSudo(cmd, name, env);
|
||||
await sudoExecAsync(cmd, { name });
|
||||
return { cancelled: false };
|
||||
}
|
||||
|
||||
@ -124,7 +137,7 @@ async function elevateScriptUnix(
|
||||
name: string,
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
const cmd = ['bash', escapeSh(path)].join(' ');
|
||||
await linuxSudo(cmd, { name });
|
||||
await sudoExecAsync(cmd, { name });
|
||||
return { cancelled: false };
|
||||
}
|
||||
|
||||
@ -133,7 +146,7 @@ async function elevateScriptCatalina(
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
const cmd = ['bash', escapeSh(path)].join(' ');
|
||||
try {
|
||||
const { cancelled } = await darwinSudo(cmd);
|
||||
const { cancelled } = await catalinaSudo(cmd);
|
||||
return { cancelled };
|
||||
} catch (error: any) {
|
||||
throw errors.createError({ title: error.stderr });
|
||||
@ -143,13 +156,13 @@ async function elevateScriptCatalina(
|
||||
export async function elevateCommand(
|
||||
command: string[],
|
||||
options: {
|
||||
env: _.Dictionary<string | undefined>;
|
||||
environment: _.Dictionary<string | undefined>;
|
||||
applicationName: string;
|
||||
},
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
if (await isElevated()) {
|
||||
spawn(command[0], command.slice(1), {
|
||||
env: options.env,
|
||||
await execFileAsync(command[0], command.slice(1), {
|
||||
env: options.environment,
|
||||
});
|
||||
return { cancelled: false };
|
||||
}
|
||||
@ -157,7 +170,7 @@ export async function elevateCommand(
|
||||
const launchScript = createLaunchScript(
|
||||
command[0],
|
||||
command.slice(1),
|
||||
options.env,
|
||||
options.environment,
|
||||
);
|
||||
return await withTmpFile(
|
||||
{
|
||||
@ -168,7 +181,7 @@ export async function elevateCommand(
|
||||
async ({ path }) => {
|
||||
await fs.writeFile(path, launchScript);
|
||||
if (isWindows) {
|
||||
return elevateScriptWindows(path, options.applicationName, options.env);
|
||||
return elevateScriptWindows(path, options.applicationName);
|
||||
}
|
||||
if (
|
||||
os.platform() === 'darwin' &&
|
||||
@ -178,7 +191,7 @@ export async function elevateCommand(
|
||||
return elevateScriptCatalina(path);
|
||||
}
|
||||
try {
|
||||
return elevateScriptUnix(path, options.applicationName);
|
||||
return await elevateScriptUnix(path, options.applicationName);
|
||||
} catch (error: any) {
|
||||
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
||||
// There doesn't seem to be a better way to handle these errors, so
|
||||
|
@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 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 { spawn } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { env } from 'process';
|
||||
// import { promisify } from "util";
|
||||
|
||||
import { supportedLocales } from '../../gui/app/i18n';
|
||||
|
||||
// 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 }> {
|
||||
try {
|
||||
let lang = Intl.DateTimeFormat().resolvedOptions().locale;
|
||||
lang = lang.substr(0, 2);
|
||||
if (supportedLocales.indexOf(lang) > -1) {
|
||||
// language should be present
|
||||
} else {
|
||||
// fallback to eng
|
||||
lang = 'en';
|
||||
}
|
||||
|
||||
const elevateProcess = spawn(
|
||||
'sudo',
|
||||
['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`],
|
||||
{
|
||||
// encoding: "utf8",
|
||||
env: {
|
||||
PATH: env.PATH,
|
||||
SUDO_ASKPASS: getAskPassScriptPath(lang),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let elevated = 'pending';
|
||||
|
||||
elevateProcess.stdout.on('data', (data) => {
|
||||
if (data.toString().includes(SUCCESSFUL_AUTH_MARKER)) {
|
||||
// if the first data comming out of the sudo command is the expected marker we resolve the promise
|
||||
elevated = 'granted';
|
||||
} else {
|
||||
// if the first data comming out of the sudo command is not the expected marker we reject the promise
|
||||
elevated = 'rejected';
|
||||
}
|
||||
});
|
||||
|
||||
// we don't spawn or read stdout in the promise otherwise resolving stop the process
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkElevation = setInterval(() => {
|
||||
if (elevated === 'granted') {
|
||||
clearInterval(checkElevation);
|
||||
resolve({ cancelled: false });
|
||||
} else if (elevated === 'rejected') {
|
||||
clearInterval(checkElevation);
|
||||
resolve({ cancelled: true });
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// if the elevation didn't occured in 30 seconds we reject the promise
|
||||
setTimeout(() => {
|
||||
clearInterval(checkElevation);
|
||||
reject(new Error('Elevation timeout'));
|
||||
}, 30000);
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.code === 1) {
|
||||
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
|
||||
return { cancelled: true };
|
||||
}
|
||||
error.stdout = error.stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
/*
|
||||
* This is heavily inspired (read: a ripof) https://github.com/balena-io-modules/sudo-prompt
|
||||
* Which was a fork of https://github.com/jorangreef/sudo-prompt
|
||||
*
|
||||
* This and the original code was released under The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2015 Joran Dirk Greef
|
||||
* Copyright (c) 2024 Balena
|
||||
*
|
||||
The MIT License (MIT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { access, constants } from 'fs/promises';
|
||||
import { env } from 'process';
|
||||
|
||||
// const execFileAsync = promisify(execFile);
|
||||
|
||||
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||
|
||||
/** Check for kdesudo or pkexec */
|
||||
function checkLinuxBinary() {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// We used to prefer gksudo over pkexec since it enabled a better prompt.
|
||||
// However, gksudo cannot run multiple commands concurrently.
|
||||
|
||||
const paths = ['/usr/bin/kdesudo', '/usr/bin/pkexec'];
|
||||
for (const path of paths) {
|
||||
try {
|
||||
// check if the file exist and is executable
|
||||
await access(path, constants.X_OK);
|
||||
resolve(path);
|
||||
} catch (error: any) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
reject('Unable to find pkexec or kdesudo.');
|
||||
});
|
||||
}
|
||||
|
||||
function escapeDoubleQuotes(escapeString: string) {
|
||||
return escapeString.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
export async function sudo(
|
||||
command: string,
|
||||
{ name }: { name: string },
|
||||
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
||||
const linuxBinary: string = (await checkLinuxBinary()) as string;
|
||||
if (!linuxBinary) {
|
||||
throw new Error('Unable to find pkexec or kdesudo.');
|
||||
}
|
||||
|
||||
const parameters = [];
|
||||
|
||||
if (/kdesudo/i.test(linuxBinary)) {
|
||||
parameters.push(
|
||||
'--comment',
|
||||
`"${name} wants to make changes.
|
||||
Enter your password to allow this."`,
|
||||
);
|
||||
parameters.push('-d'); // Do not show the command to be run in the dialog.
|
||||
parameters.push('--');
|
||||
} else if (/pkexec/i.test(linuxBinary)) {
|
||||
parameters.push('--disable-internal-agent');
|
||||
}
|
||||
|
||||
parameters.push('/bin/bash');
|
||||
parameters.push('-c');
|
||||
parameters.push(
|
||||
`echo ${SUCCESSFUL_AUTH_MARKER} && ${escapeDoubleQuotes(command)}`,
|
||||
);
|
||||
|
||||
const elevateProcess = spawn(linuxBinary, parameters, {
|
||||
// encoding: "utf8",
|
||||
env: {
|
||||
PATH: env.PATH,
|
||||
},
|
||||
});
|
||||
|
||||
let elevated = '';
|
||||
|
||||
elevateProcess.stdout.on('data', (data) => {
|
||||
// console.log(`stdout: ${data.toString()}`);
|
||||
if (data.toString().includes(SUCCESSFUL_AUTH_MARKER)) {
|
||||
// if the first data comming out of the sudo command is the expected marker we resolve the promise
|
||||
elevated = 'granted';
|
||||
} else {
|
||||
// if the first data comming out of the sudo command is not the expected marker we reject the promise
|
||||
elevated = 'refused';
|
||||
}
|
||||
});
|
||||
|
||||
// elevateProcess.stderr.on('data', (data) => {
|
||||
// // console.log(`stderr: ${data.toString()}`);
|
||||
// // if (data.toString().includes(SUCCESSFUL_AUTH_MARKER)) {
|
||||
// // // if the first data comming out of the sudo command is the expected marker we resolve the promise
|
||||
// // elevated = 'granted';
|
||||
// // } else {
|
||||
// // // if the first data comming out of the sudo command is not the expected marker we reject the promise
|
||||
// // elevated = 'refused';
|
||||
// // }
|
||||
// });
|
||||
|
||||
// we don't spawn or read stdout in the promise otherwise resolving stop the process
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkElevation = setInterval(() => {
|
||||
if (elevated === 'granted') {
|
||||
clearInterval(checkElevation);
|
||||
resolve({ cancelled: false });
|
||||
} else if (elevated === 'refused') {
|
||||
clearInterval(checkElevation);
|
||||
resolve({ cancelled: true });
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// if the elevation didn't occured in 30 seconds we reject the promise
|
||||
setTimeout(() => {
|
||||
clearInterval(checkElevation);
|
||||
reject(new Error('Elevation timeout'));
|
||||
}, 30000);
|
||||
});
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env osascript -l JavaScript
|
||||
|
||||
ObjC.import('stdlib')
|
||||
|
||||
const app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
const result = app.displayDialog('balenaEtcher 需要来自管理员的权限才能烧录镜像到磁盘。\n\n输入您的密码以允许此操作。', {
|
||||
defaultAnswer: '',
|
||||
withIcon: 'caution',
|
||||
buttons: ['取消', '好'],
|
||||
defaultButton: '好',
|
||||
hiddenAnswer: true,
|
||||
})
|
||||
|
||||
if (result.buttonReturned === '好') {
|
||||
result.textReturned
|
||||
} else {
|
||||
$.exit(255)
|
||||
}
|
||||
|
@ -1,218 +0,0 @@
|
||||
/*
|
||||
* This is heavily inspired (read: a ripof) https://github.com/balena-io-modules/sudo-prompt
|
||||
* Which was a fork of https://github.com/jorangreef/sudo-prompt
|
||||
*
|
||||
* This and the original code was released under The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2015 Joran Dirk Greef
|
||||
* Copyright (c) 2024 Balena
|
||||
*
|
||||
The MIT License (MIT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
// import { env } from 'process';
|
||||
import { tmpdir } from 'os';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { join, sep } from 'path';
|
||||
import { mkdir, writeFile, copyFile, readFile } from 'fs/promises';
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* Migrate, modernize and clenup the windows elevation code from the old @balena/sudo-prompt package in a similar way to linux-sudo.ts and catalina-sudo files.
|
||||
*/
|
||||
|
||||
export async function sudo(
|
||||
command: string,
|
||||
_name: string,
|
||||
env: any,
|
||||
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
|
||||
const uuid = uuidv4();
|
||||
|
||||
const temp = tmpdir();
|
||||
if (!temp) {
|
||||
throw new Error('os.tmpdir() not defined.');
|
||||
}
|
||||
|
||||
const tmpFolder = join(temp, uuid);
|
||||
|
||||
if (/"/.test(tmpFolder)) {
|
||||
// We expect double quotes to be reserved on Windows.
|
||||
// Even so, we test for this and abort if they are present.
|
||||
throw new Error('instance.path cannot contain double-quotes.');
|
||||
}
|
||||
|
||||
const executeScriptPath = join(tmpFolder, 'execute.bat');
|
||||
const commandScriptPath = join(tmpFolder, 'command.bat');
|
||||
const stdoutPath = join(tmpFolder, 'stdout');
|
||||
const stderrPath = join(tmpFolder, 'stderr');
|
||||
const statusPath = join(tmpFolder, 'status');
|
||||
|
||||
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||
|
||||
try {
|
||||
await mkdir(tmpFolder);
|
||||
|
||||
// WindowsWriteExecuteScript(instance, end)
|
||||
const executeScript = `
|
||||
@echo off\r\n
|
||||
call "${commandScriptPath}" > "${stdoutPath}" 2> "${stderrPath}"\r\n
|
||||
(echo %ERRORLEVEL%) > "${statusPath}"
|
||||
`;
|
||||
|
||||
await writeFile(executeScriptPath, executeScript, 'utf-8');
|
||||
|
||||
// WindowsWriteCommandScript(instance, end)
|
||||
const cwd = process.cwd();
|
||||
if (/"/.test(cwd)) {
|
||||
// We expect double quotes to be reserved on Windows.
|
||||
// Even so, we test for this and abort if they are present.
|
||||
throw new Error('process.cwd() cannot contain double-quotes.');
|
||||
}
|
||||
|
||||
const commandScriptArray = [];
|
||||
commandScriptArray.push('@echo off');
|
||||
// Set code page to UTF-8:
|
||||
commandScriptArray.push('chcp 65001>nul');
|
||||
// Preserve current working directory:
|
||||
// We pass /d as an option in case the cwd is on another drive (issue 70).
|
||||
commandScriptArray.push(`cd /d "${cwd}"`);
|
||||
// Export environment variables:
|
||||
for (const key in env) {
|
||||
// "The characters <, >, |, &, ^ are special command shell characters, and
|
||||
// they must be preceded by the escape character (^) or enclosed in
|
||||
// quotation marks. If you use quotation marks to enclose a string that
|
||||
// contains one of the special characters, the quotation marks are set as
|
||||
// part of the environment variable value."
|
||||
// In other words, Windows assigns everything that follows the equals sign
|
||||
// to the value of the variable, whereas Unix systems ignore double quotes.
|
||||
if (Object.prototype.hasOwnProperty.call(env, key)) {
|
||||
const value = env[key];
|
||||
commandScriptArray.push(
|
||||
`set ${key}=${value!.replace(/([<>\\|&^])/g, '^$1')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
commandScriptArray.push(`echo ${SUCCESSFUL_AUTH_MARKER}`);
|
||||
commandScriptArray.push(command);
|
||||
await writeFile(
|
||||
commandScriptPath,
|
||||
commandScriptArray.join('\r\n'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// WindowsCopyCmd(instance, end)
|
||||
if (windowsNeedsCopyCmd(tmpFolder)) {
|
||||
// Work around https://github.com/jorangreef/sudo-prompt/issues/97
|
||||
// Powershell can't properly escape amperstands in paths.
|
||||
// We work around this by copying cmd.exe in our temporary folder and running
|
||||
// it from here (see WindowsElevate below).
|
||||
// That way, we don't have to pass the path containing the amperstand at all.
|
||||
// A symlink would probably work too but you have to be an administrator in
|
||||
// order to create symlinks on Windows.
|
||||
await copyFile(
|
||||
join(process.env.SystemRoot!, 'System32', 'cmd.exe'),
|
||||
join(tmpFolder, 'cmd.exe'),
|
||||
);
|
||||
}
|
||||
|
||||
// WindowsElevate(instance, end)
|
||||
// We used to use this for executing elevate.vbs:
|
||||
// var command = 'cscript.exe //NoLogo "' + instance.pathElevate + '"';
|
||||
const spawnCommand = [];
|
||||
// spawnCommand.push("powershell.exe") // as we use spawn this one is out of the array
|
||||
spawnCommand.push('Start-Process');
|
||||
spawnCommand.push('-FilePath');
|
||||
const options: any = { encoding: 'utf8' };
|
||||
if (windowsNeedsCopyCmd(tmpFolder)) {
|
||||
// Node.path.join('.', 'cmd.exe') would return 'cmd.exe'
|
||||
spawnCommand.push(['.', 'cmd.exe'].join(sep));
|
||||
spawnCommand.push('-ArgumentList');
|
||||
spawnCommand.push('"/C","execute.bat"');
|
||||
options.cwd = tmpFolder;
|
||||
} else {
|
||||
// Escape characters for cmd using double quotes:
|
||||
// Escape characters for PowerShell using single quotes:
|
||||
// Escape single quotes for PowerShell using backtick:
|
||||
// See: https://ss64.com/ps/syntax-esc.html
|
||||
spawnCommand.push(`'${executeScriptPath.replace(/'/g, "`'")}'`);
|
||||
}
|
||||
spawnCommand.push('-WindowStyle hidden');
|
||||
spawnCommand.push('-Verb runAs');
|
||||
|
||||
spawn('powershell.exe', spawnCommand);
|
||||
|
||||
// setTimeout(() => {elevated = "granted"}, 5000)
|
||||
|
||||
// we don't spawn or read stdout in the promise otherwise resolving stop the process
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkElevation = setInterval(async () => {
|
||||
try {
|
||||
const result = await readFile(stdoutPath, 'utf-8');
|
||||
const error = await readFile(stderrPath, 'utf-8');
|
||||
|
||||
if (error && error !== '') {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
// TODO: should track something more generic
|
||||
if (result.includes(SUCCESSFUL_AUTH_MARKER)) {
|
||||
clearInterval(checkElevation);
|
||||
resolve({ cancelled: false });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'Error while reading flasher elevation script output',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// if the elevation didn't occured in 30 seconds we reject the promise
|
||||
setTimeout(() => {
|
||||
clearInterval(checkElevation);
|
||||
reject(new Error('Elevation timeout'));
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// WindowsWaitForStatus(instance, end)
|
||||
|
||||
// WindowsResult(instance, end)
|
||||
} catch (error) {
|
||||
throw new Error(`Can't elevate process ${error}`);
|
||||
} finally {
|
||||
// TODO: cleanup
|
||||
// // Remove(instance.path, function (errorRemove) {
|
||||
// // if (error) return callback(error)
|
||||
// // if (errorRemove) return callback(errorRemove)
|
||||
// // callback(undefined, stdout, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
function windowsNeedsCopyCmd(path: string) {
|
||||
const specialChars = ['&', '`', "'", '"', '<', '>', '|', '^'];
|
||||
for (const specialChar of specialChars) {
|
||||
if (path.includes(specialChar)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import type { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
import type { sourceDestination } from 'etcher-sdk';
|
||||
import type { 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;
|
||||
}
|
@ -14,6 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { app, remote } from 'electron';
|
||||
import { Dictionary } from 'lodash';
|
||||
|
||||
import * as errors from './errors';
|
||||
|
||||
export function isValidPercentage(percentage: any): boolean {
|
||||
@ -29,12 +33,35 @@ export function percentageToFloat(percentage: any) {
|
||||
return percentage / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get etcher configs stored online
|
||||
* @param {String} - url where config.json is stored
|
||||
*/
|
||||
export async function getConfig(configUrl?: string): Promise<Dictionary<any>> {
|
||||
configUrl = configUrl ?? 'https://balena.io/etcher/static/config.json';
|
||||
const response = await axios.get(configUrl, { responseType: 'json' });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function delay(duration: number): Promise<void> {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, duration);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppPath(): string {
|
||||
return (
|
||||
(app || remote.app)
|
||||
.getAppPath()
|
||||
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
|
||||
// include the app-x64 or app-arm64 folder depending on the arch.
|
||||
// We don't care about the app.asar file, we want the actual folder.
|
||||
.replace(/\.asar$/, () =>
|
||||
process.platform === 'darwin' ? '-' + process.arch : '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isJson(jsonString: string) {
|
||||
try {
|
||||
JSON.parse(jsonString);
|
||||
|
292
lib/util/api.ts
292
lib/util/api.ts
@ -1,292 +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 { WebSocketServer } from 'ws';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { values } from 'lodash';
|
||||
|
||||
import type { MultiDestinationProgress } from 'etcher-sdk/build/multi-write';
|
||||
|
||||
import { toJSON } from '../shared/errors';
|
||||
import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes';
|
||||
import type { WriteOptions } from './types/types';
|
||||
import { write, cleanup } from './child-writer';
|
||||
import { startScanning } from './scanner';
|
||||
import { getSourceMetadata } from './source-metadata';
|
||||
import type { DrivelistDrive } from '../shared/drive-constraints';
|
||||
import type { SourceMetadata } from '../shared/typings/source-selector';
|
||||
|
||||
const ETCHER_SERVER_ADDRESS = process.env.ETCHER_SERVER_ADDRESS as string;
|
||||
const ETCHER_SERVER_PORT = process.env.ETCHER_SERVER_PORT as string;
|
||||
// const ETCHER_SERVER_ID = process.env.ETCHER_SERVER_ID as string;
|
||||
|
||||
const ETCHER_TERMINATE_TIMEOUT: number = parseInt(
|
||||
process.env.ETCHER_TERMINATE_TIMEOUT ?? '10000',
|
||||
10,
|
||||
);
|
||||
|
||||
const host = ETCHER_SERVER_ADDRESS ?? '127.0.0.1';
|
||||
const port = parseInt(ETCHER_SERVER_PORT || '3434', 10);
|
||||
// const path = ETCHER_SERVER_ID || "etcher";
|
||||
|
||||
// TODO: use the path as cheap authentication
|
||||
|
||||
const wss = new WebSocketServer({ host, port });
|
||||
|
||||
// hold emit functions
|
||||
let emitLog: (message: string) => void | undefined;
|
||||
let emitState: (state: MultiDestinationProgress) => void | undefined;
|
||||
let emitFail: (data: any) => void | undefined;
|
||||
let emitDrives: (drives: Dictionary<DrivelistDrive>) => void | undefined;
|
||||
let emitSourceMetadata: (
|
||||
sourceMetadata: SourceMetadata | Record<string, never>,
|
||||
) => void | undefined; // Record<string, never> means an empty object
|
||||
|
||||
// Terminate the child process
|
||||
async function terminate(exitCode?: number) {
|
||||
await cleanup(Date.now());
|
||||
process.nextTick(() => {
|
||||
process.exit(exitCode || SUCCESS);
|
||||
});
|
||||
}
|
||||
|
||||
// kill the process if no initila connections or heartbeat for X sec (default 10)
|
||||
function setTerminateTimeout() {
|
||||
if (ETCHER_TERMINATE_TIMEOUT > 0) {
|
||||
return setTimeout(() => {
|
||||
console.log(
|
||||
`no connections or heartbeat for ${ETCHER_TERMINATE_TIMEOUT} ms, terminating`,
|
||||
);
|
||||
terminate();
|
||||
}, ETCHER_TERMINATE_TIMEOUT);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// terminate the process cleanly on SIGINT
|
||||
process.once('SIGINT', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
// terminate the process cleanly on SIGTERM
|
||||
process.once('SIGTERM', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
let terminateInterval = setTerminateTimeout();
|
||||
|
||||
interface EmitLog {
|
||||
emit: (channel: string, message: object | string) => void;
|
||||
log: (message: string) => void;
|
||||
}
|
||||
|
||||
function setup(): Promise<EmitLog> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('connection established... setting up');
|
||||
|
||||
/**
|
||||
* @summary Send a message to the IPC server
|
||||
*/
|
||||
function emit(type: string, payload?: object | string) {
|
||||
ws.send(JSON.stringify({ type, payload }));
|
||||
// ipc.of[IPC_SERVER_ID].emit("message", { type, payload });
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Print logs and send them back to client
|
||||
*/
|
||||
function log(message: string) {
|
||||
console.log(message);
|
||||
emit('log', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Handle `errors`
|
||||
*/
|
||||
async function handleError(error: Error) {
|
||||
emit('error', toJSON(error));
|
||||
await terminate(GENERAL_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Handle `abort` from client
|
||||
*/
|
||||
const onAbort = async (exitCode: number) => {
|
||||
log('Abort');
|
||||
emit('abort');
|
||||
await terminate(exitCode);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Handle `skip` from client; skip validation
|
||||
*/
|
||||
const onSkip = async (exitCode: number) => {
|
||||
log('Skip validation');
|
||||
emit('skip');
|
||||
await terminate(exitCode);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Handle `write` from client; start writing to the drives
|
||||
*/
|
||||
const onWrite = async (options: WriteOptions) => {
|
||||
log('write requested');
|
||||
|
||||
// Remove leftover tmp files older than 1 hour
|
||||
cleanup(Date.now() - 60 * 60 * 1000);
|
||||
|
||||
let exitCode = SUCCESS;
|
||||
|
||||
// Write to the drives
|
||||
const results = await write(options);
|
||||
|
||||
// handle potential errors from the write process
|
||||
if (results.errors.length > 0) {
|
||||
results.errors = results.errors.map(toJSON);
|
||||
exitCode = GENERAL_ERROR;
|
||||
}
|
||||
|
||||
// send the results back to the client
|
||||
emit('done', { results });
|
||||
|
||||
// terminate this process
|
||||
await terminate(exitCode);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Handle `sourceMetadata` from client; get source metadata
|
||||
*/
|
||||
const onSourceMetadata = async (params: any) => {
|
||||
log('sourceMetadata requested');
|
||||
const { selected, SourceType, auth } = JSON.parse(params);
|
||||
try {
|
||||
const sourceMatadata = await getSourceMetadata(
|
||||
selected,
|
||||
SourceType,
|
||||
auth,
|
||||
);
|
||||
emitSourceMetadata(sourceMatadata);
|
||||
} catch (error: any) {
|
||||
emitFail(error);
|
||||
}
|
||||
};
|
||||
|
||||
// handle uncaught exceptions
|
||||
process.once('uncaughtException', handleError);
|
||||
|
||||
// terminate the process if the connection is closed
|
||||
ws.on('error', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
// route messages from the client by `type`
|
||||
const messagesHandler: any = {
|
||||
// terminate the process
|
||||
terminate: () => terminate(SUCCESS),
|
||||
|
||||
/*
|
||||
receive a `heartbeat`, reset the terminate timeout
|
||||
this mechanism ensure the process will be terminated if the client is disconnected
|
||||
*/
|
||||
heartbeat: () => {
|
||||
if (terminateInterval) {
|
||||
clearTimeout(terminateInterval);
|
||||
}
|
||||
terminateInterval = setTerminateTimeout();
|
||||
},
|
||||
|
||||
// resolve the setup promise when the client is ready
|
||||
ready: () => {
|
||||
log('Ready ...');
|
||||
resolve({ emit, log });
|
||||
},
|
||||
|
||||
// start scanning for drives
|
||||
scan: () => {
|
||||
log('Scan requested');
|
||||
startScanning();
|
||||
},
|
||||
|
||||
// route `cancel` from client
|
||||
cancel: () => onAbort(GENERAL_ERROR),
|
||||
|
||||
// route `skip` from client
|
||||
skip: () => onSkip(GENERAL_ERROR),
|
||||
|
||||
// route `write` from client
|
||||
write: async (options: WriteOptions) => onWrite(options),
|
||||
|
||||
// route `sourceMetadata` from client
|
||||
sourceMetadata: async (params: any) => onSourceMetadata(params),
|
||||
};
|
||||
|
||||
// message handler, parse and route messages coming on WS
|
||||
ws.on('message', async (jsonData: any) => {
|
||||
const data = JSON.parse(jsonData);
|
||||
const message = messagesHandler[data.type];
|
||||
if (message) {
|
||||
await message(data.payload);
|
||||
} else {
|
||||
throw new Error(`Unknown message type: ${data.type}`);
|
||||
}
|
||||
});
|
||||
|
||||
// inform the client that the server is ready to receive messages
|
||||
emit('ready', {});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// setTimeout(() => console.log('wss', wss.address()), 1000);
|
||||
console.log('waiting for connection...');
|
||||
|
||||
setup().then(({ emit, log }: EmitLog) => {
|
||||
// connection is established, clear initial terminate timeout
|
||||
if (terminateInterval) {
|
||||
clearInterval(terminateInterval);
|
||||
}
|
||||
|
||||
console.log('waiting for instruction...');
|
||||
|
||||
// set the exportable emit functions
|
||||
emitLog = (message) => {
|
||||
log(message);
|
||||
};
|
||||
|
||||
emitState = (state) => {
|
||||
emit('state', state);
|
||||
};
|
||||
|
||||
emitFail = (data) => {
|
||||
emit('fail', data);
|
||||
};
|
||||
|
||||
emitDrives = (drives) => {
|
||||
emit('drives', JSON.stringify(values(drives)));
|
||||
};
|
||||
|
||||
emitSourceMetadata = (sourceMetadata) => {
|
||||
emit('sourceMetadata', JSON.stringify(sourceMetadata));
|
||||
};
|
||||
});
|
||||
|
||||
export { emitLog, emitState, emitFail, emitDrives, emitSourceMetadata };
|
@ -1,199 +0,0 @@
|
||||
/*
|
||||
* 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 type {
|
||||
OnProgressFunction,
|
||||
OnFailFunction,
|
||||
MultiDestinationProgress,
|
||||
} from 'etcher-sdk/build/multi-write';
|
||||
import {
|
||||
decompressThenFlash,
|
||||
DECOMPRESSED_IMAGE_PREFIX,
|
||||
} from 'etcher-sdk/build/multi-write';
|
||||
|
||||
import { totalmem } from 'os';
|
||||
|
||||
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||
|
||||
import type { SourceDestination } from 'etcher-sdk/build/source-destination';
|
||||
import { File, Http, BlockDevice } from 'etcher-sdk/build/source-destination';
|
||||
|
||||
import type { 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 };
|
@ -1,184 +0,0 @@
|
||||
import { scanner as driveScanner } from './drive-scanner';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import type { DrivelistDrive } from '../shared/drive-constraints';
|
||||
import outdent from 'outdent';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { 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 };
|
@ -1,94 +0,0 @@
|
||||
/** Get metadata for a source */
|
||||
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { replaceWindowsNetworkDriveLetter } from '../gui/app/os/windows-network-drives';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { isJson } from '../shared/utils';
|
||||
import * as path from 'path';
|
||||
import type {
|
||||
SourceMetadata,
|
||||
Authentication,
|
||||
Source,
|
||||
} from '../shared/typings/source-selector';
|
||||
import type { 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,
|
||||
): Promise<SourceMetadata | Record<string, never>> {
|
||||
// `Record<string, never>` means an empty object
|
||||
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
|
||||
return {};
|
||||
} finally {
|
||||
await source.close();
|
||||
}
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export { getSourceMetadata };
|
33
lib/util/types/types.d.ts
vendored
33
lib/util/types/types.d.ts
vendored
@ -1,33 +0,0 @@
|
||||
import type { Metadata } from 'etcher-sdk/build/source-destination';
|
||||
import type { SourceMetadata } from '../../shared/typings/source-selector';
|
||||
import type { 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;
|
||||
}
|
28196
npm-shrinkwrap.json
generated
28196
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
18153
package-lock.json
generated
Normal file
18153
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
233
package.json
233
package.json
@ -2,10 +2,9 @@
|
||||
"name": "balena-etcher",
|
||||
"private": true,
|
||||
"displayName": "balenaEtcher",
|
||||
"productName": "balenaEtcher",
|
||||
"version": "2.1.0",
|
||||
"version": "1.7.11",
|
||||
"packageType": "local",
|
||||
"main": ".webpack/main",
|
||||
"main": "generated/etcher.js",
|
||||
"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",
|
||||
@ -14,141 +13,117 @@
|
||||
"url": "git@github.com:balena-io/etcher.git"
|
||||
},
|
||||
"scripts": {
|
||||
"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": "echo 'Only use custom tests; if you want to test locally, use `npm run wdio`' && exit 0",
|
||||
"package": "electron-forge package",
|
||||
"start": "electron-forge start",
|
||||
"make": "electron-forge make",
|
||||
"wdio": "xvfb-maybe wdio run ./wdio.conf.ts"
|
||||
"build": "npm run webpack",
|
||||
"flowzone-preinstall-linux": "sudo apt-get install -y xvfb libudev-dev && cat < electron-builder.yml | yq e .deb.depends[] - | xargs -L1 echo | sed 's/|//g' | xargs -L1 sudo apt-get --ignore-missing install || true",
|
||||
"flowzone-preinstall-macos": "true",
|
||||
"flowzone-preinstall-windows": "true",
|
||||
"flowzone-preinstall": "npm run flowzone-preinstall-linux",
|
||||
"lint-css": "prettier --write lib/**/*.css",
|
||||
"lint-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-macos": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks",
|
||||
"test-gui": "electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
|
||||
"test-linux": "npm run lint && xvfb-run --auto-servernum npm run test-gui && xvfb-run --auto-servernum npm run test-shared && xvfb-run --auto-servernum npm run test-spectron && npm run sanity-checks",
|
||||
"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-spectron": "mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts",
|
||||
"test-windows": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks",
|
||||
"test": "echo npm run test-{linux,windows,macos}",
|
||||
"watch": "webpack serve --no-optimization-minimize --config ./webpack.dev.config.ts",
|
||||
"webpack": "webpack"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run prettify"
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"author": "Balena Ltd. <hello@balena.io>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.1.2",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@ronomon/direct-io": "^3.0.1",
|
||||
"@sentry/electron": "^4.24.0",
|
||||
"analytics-client": "^2.0.1",
|
||||
"axios": "^1.6.8",
|
||||
"debug": "4.3.4",
|
||||
"drivelist": "^12.0.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-updater": "6.1.8",
|
||||
"etcher-sdk": "9.1.2",
|
||||
"i18next": "23.11.2",
|
||||
"immutable": "3.8.2",
|
||||
"lodash": "4.17.21",
|
||||
"outdent": "0.8.0",
|
||||
"path-is-inside": "1.0.2",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-i18next": "13.5.0",
|
||||
"redux": "4.2.1",
|
||||
"rendition": "35.2.0",
|
||||
"semver": "7.6.0",
|
||||
"styled-components": "5.3.6",
|
||||
"sys-class-rgb-led": "3.0.1",
|
||||
"uuid": "9.0.1",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@balena/lint": "8.0.2",
|
||||
"@electron-forge/cli": "7.4.0",
|
||||
"@electron-forge/maker-deb": "7.4.0",
|
||||
"@electron-forge/maker-dmg": "7.4.0",
|
||||
"@electron-forge/maker-rpm": "7.4.0",
|
||||
"@electron-forge/maker-squirrel": "7.4.0",
|
||||
"@electron-forge/maker-zip": "7.4.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.4.0",
|
||||
"@electron-forge/plugin-webpack": "7.4.0",
|
||||
"@reforged/maker-appimage": "3.3.2",
|
||||
"@svgr/webpack": "8.1.0",
|
||||
"@types/chai": "4.3.14",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/node": "^20.11.6",
|
||||
"@types/react": "17.0.2",
|
||||
"@types/react-dom": "17.0.2",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/sinon": "17.0.3",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@vercel/webpack-asset-relocator-loader": "1.7.3",
|
||||
"@wdio/cli": "^8.36.1",
|
||||
"@wdio/local-runner": "^8.36.1",
|
||||
"@wdio/mocha-framework": "^8.36.1",
|
||||
"@wdio/spec-reporter": "^8.36.1",
|
||||
"@yao-pkg/pkg": "^5.11.5",
|
||||
"catch-uncommitted": "^2.0.0",
|
||||
"chai": "4.3.10",
|
||||
"css-loader": "5.2.7",
|
||||
"electron": "30.0.1",
|
||||
"file-loader": "6.2.0",
|
||||
"husky": "8.0.3",
|
||||
"native-addon-loader": "2.0.1",
|
||||
"node-loader": "^2.0.0",
|
||||
"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",
|
||||
"wdio-electron-service": "^6.4.1",
|
||||
"xvfb-maybe": "^0.2.1"
|
||||
},
|
||||
"hostDependencies": {
|
||||
"debian": [
|
||||
"libasound2",
|
||||
"libatk1.0-0",
|
||||
"libc6",
|
||||
"libcairo2",
|
||||
"libcups2",
|
||||
"libdbus-1-3",
|
||||
"libexpat1",
|
||||
"libfontconfig1",
|
||||
"libfreetype6",
|
||||
"libgbm1",
|
||||
"libgcc1",
|
||||
"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"
|
||||
"lint-staged": {
|
||||
"./**/*.{ts,tsx}": [
|
||||
"npm run lint-ts"
|
||||
],
|
||||
"./**/*.css": [
|
||||
"npm run lint-css"
|
||||
]
|
||||
},
|
||||
"author": "Balena Inc. <hello@etcher.io>",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@balena/lint": "5.3.0",
|
||||
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
|
||||
"@fortawesome/fontawesome-free": "5.13.1",
|
||||
"@svgr/webpack": "5.5.0",
|
||||
"@types/chai": "4.2.7",
|
||||
"@types/copy-webpack-plugin": "6.0.0",
|
||||
"@types/mime-types": "2.1.0",
|
||||
"@types/mini-css-extract-plugin": "1.2.2",
|
||||
"@types/mocha": "8.0.3",
|
||||
"@types/node": "14.14.41",
|
||||
"@types/node-ipc": "9.1.2",
|
||||
"@types/react": "16.8.5",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/semver": "7.1.0",
|
||||
"@types/sinon": "9.0.0",
|
||||
"@types/terser-webpack-plugin": "5.0.2",
|
||||
"@types/tmp": "0.2.0",
|
||||
"@types/webpack-node-externals": "2.5.0",
|
||||
"aws4-axios": "2.4.9",
|
||||
"chai": "4.2.0",
|
||||
"copy-webpack-plugin": "7.0.0",
|
||||
"css-loader": "5.0.1",
|
||||
"d3": "4.13.0",
|
||||
"debug": "4.2.0",
|
||||
"electron": "12.2.3",
|
||||
"electron-builder": "22.10.5",
|
||||
"electron-mocha": "9.3.2",
|
||||
"electron-notarize": "1.0.0",
|
||||
"electron-rebuild": "3.2.5",
|
||||
"electron-updater": "4.3.5",
|
||||
"esbuild-loader": "2.16.0",
|
||||
"etcher-sdk": "7.4.0",
|
||||
"file-loader": "6.2.0",
|
||||
"husky": "4.2.5",
|
||||
"immutable": "3.8.1",
|
||||
"lint-staged": "10.2.2",
|
||||
"lodash": "4.17.10",
|
||||
"mini-css-extract-plugin": "1.3.3",
|
||||
"mocha": "8.0.1",
|
||||
"native-addon-loader": "2.0.1",
|
||||
"node-ipc": "9.1.1",
|
||||
"omit-deep-lodash": "1.1.4",
|
||||
"outdent": "0.8.0",
|
||||
"path-is-inside": "1.0.2",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
"pretty-bytes": "5.3.0",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5",
|
||||
"redux": "4.0.5",
|
||||
"rendition": "19.2.0",
|
||||
"resin-corvus": "2.0.5",
|
||||
"semver": "7.3.2",
|
||||
"simple-progress-webpack-plugin": "1.1.2",
|
||||
"sinon": "9.0.2",
|
||||
"spectron": "14.0.0",
|
||||
"string-replace-loader": "3.0.1",
|
||||
"style-loader": "2.0.0",
|
||||
"styled-components": "5.1.0",
|
||||
"sys-class-rgb-led": "3.0.0",
|
||||
"terser-webpack-plugin": "5.2.5",
|
||||
"ts-loader": "8.0.12",
|
||||
"ts-node": "9.1.1",
|
||||
"tslib": "2.0.0",
|
||||
"typescript": "4.4.4",
|
||||
"url-loader": "4.1.1",
|
||||
"uuid": "8.1.0",
|
||||
"webpack": "5.11.0",
|
||||
"webpack-cli": "4.2.0",
|
||||
"webpack-dev-server": "4.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20 <21"
|
||||
"node": ">=14 < 16"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2025-02-27T16:16:57.534Z"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"utf-8-validate": "^5.0.10",
|
||||
"winusb-driver-generator": "2.1.2"
|
||||
"publishedAt": "2022-11-07T17:32:26.446Z"
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"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/**"
|
||||
]
|
||||
}
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
awscli==1.11.87
|
||||
shyaml==0.5.0
|
52
scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
Executable file
52
scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
Executable file
@ -0,0 +1,52 @@
|
||||
#!/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
|
52
scripts/clean-shrinkwrap.ts
Normal file
52
scripts/clean-shrinkwrap.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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();
|
1
scripts/resin
Submodule
1
scripts/resin
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 8dfa21cfc23b1dbc0eaa22b5dbdf1f5c796b0c2c
|
@ -1,11 +1,5 @@
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const { app } = require('electron');
|
||||
|
||||
if (app !== undefined) {
|
||||
const remoteMain = require('@electron/remote/main');
|
||||
|
||||
remoteMain.initialize();
|
||||
|
||||
app.on('browser-window-created', (_event, window) =>
|
||||
remoteMain.enable(window.webContents),
|
||||
);
|
||||
app.allowRendererProcessReuse = false;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user