Compare commits

..

1 Commits

Author SHA1 Message Date
Edwin Joassart
ec7e0b745e patch: send sourcemap to sentry at build 2023-01-12 17:46:00 +01:00
18 changed files with 2026 additions and 1604 deletions

View File

@@ -46,6 +46,55 @@ runs:
run: choco install yq run: choco install yq
if: runner.os == 'Windows' if: runner.os == 'Windows'
# FIXME: resinci-deploy is not actively maintained
# https://github.com/product-os/resinci-deploy
- name: Checkout resinci-deploy
uses: actions/checkout@v3
with:
repository: product-os/resinci-deploy
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
path: resinci-deploy
- name: Build and install resinci-deploy
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
rm -rf ../resinci-deploy && mv resinci-deploy ..
pushd ../resinci-deploy && npm ci && npm link && popd
if [[ $runner_os =~ linux|macos ]]; then
chmod +x "$(dirname "$(which node)")/resinci-deploy" && which resinci-deploy
fi
# Upload sourcemaps to sentry
- 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://www.electron.build/code-signing.html
# https://github.com/Apple-Actions/import-codesign-certs # https://github.com/Apple-Actions/import-codesign-certs
- name: Import Apple code signing certificate - name: Import Apple code signing certificate
@@ -119,8 +168,8 @@ runs:
for target in ${TARGETS}; do for target in ${TARGETS}; do
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \ electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
--c.extraMetadata.analytics.sentry.token='https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632' \ --c.extraMetadata.analytics.sentry.token='${{ steps.sentry.outputs.dsn }}' \
--c.extraMetadata.analytics.amplitude.token='balena-etcher' \ --c.extraMetadata.analytics.mixpanel.token='balena-etcher' \
--c.extraMetadata.packageType="${target}" --c.extraMetadata.packageType="${target}"
find dist -type f -maxdepth 1 find dist -type f -maxdepth 1
@@ -154,6 +203,16 @@ runs:
-name "latest*.yml" \ -name "latest*.yml" \
-exec yq -i e .stagingPercentage=\"$percentage\" {} \; -exec yq -i e .stagingPercentage=\"$percentage\" {} \;
- name: Upload sourcemap to Sentry
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
VERSION=${{ steps.package_release.outputs.version }} npm run uploadSourcemap
env:
SENTRY_AUTH_TOKEN: ${{ fromJSON(inputs.secrets).SENTRY_AUTH_TOKEN }}
npm_config_SENTRY_ORG: balenaEtcher
npm_config_SENTRY_PROJECT: balenaetcher
npm_config_SENTRY_VERSION: ${{ steps.package_release.outputs.version }}
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:

View File

@@ -1,161 +1,3 @@
- commits:
- subject: Remove configuration remote update
hash: 85a49a221fa7fc9b1943dc8ed43b29995f9d8260
body: ""
footer:
Change-type: patch
change-type: patch
author: Edwin Joassart
nested: []
version: 1.15.2
title: ""
date: 2023-02-02T13:05:01.310Z
- commits:
- subject: Remove redundant resinci-deploy build step
hash: 48ddafd120cc9cd4fb94c0d6f7530a14be46f28d
body: ""
footer:
Change-type: patch
change-type: patch
author: Akis Kesoglou
nested: []
- subject: Lazily import Electron from child-writer process
hash: 851219f835ed037d9fd970f538095e4b339c5342
body: >
No idea how this *used* to work, but it doesnt since 887ec428 and this is
fixing it properly.
footer:
Change-type: patch
change-type: patch
author: Akis Kesoglou
nested: []
version: 1.15.1
title: ""
date: 2023-02-01T12:18:55.922Z
- commits:
- subject: Add support for Node 18
hash: 887ec42847acbd4a935b4e9ed6abb2b8d87058ce
body: >
The Electron version were currently using is on Node 14 but this is a
step forward to upgrading to a newer Electron and Node version.
Updates etcher-sdk and switches the redundant aws4-axios dependency to just axios.
Also changed bundler to stop trying to bundle wasm files — they must be included inline with JS code as data — and removed some now redundant code.
The crucial changes that enable support are:
1. The update to etcher-sdk@8 where some dependency fixes and updates took place
2. The downgrade and pinning of "electron-rebuild" to v3.2.3 until were able to update to Electron >= 14.2. The patch we need to avoid is https://github.com/electron/rebuild/pull/907. Also see: https://github.com/nodejs/node-gyp/issues/2673 and https://github.com/electron/rebuild/issues/913
3. A rule in webpack.config to ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used by etcher-sdk and does a runtime check to its availability. Were not currently using the “assume role” functionality (AFAIU) of aws4-axios and we dont care that its not found, so force webpack to ignore the import. See https://github.com/aws/aws-sdk-js-v3/issues/3025
footer:
Change-type: minor
change-type: minor
author: Akis Kesoglou
nested: []
version: 1.15.0
title: ""
date: 2023-01-27T11:36:32.980Z
- commits:
- subject: "patch: fixed mac sudo on other languages"
hash: 19d1e093fc2b1588492c9868f7604ee15ab3fd5b
body: ""
footer: {}
author: Peter Makra
nested: []
version: 1.14.3
title: ""
date: 2023-01-19T12:21:02.651Z
- commits:
- subject: "patch: revert to lockfile v1"
hash: 72af77860bee3685635c9f4db602c2a07e825037
body: ""
footer: {}
author: Peter Makra
nested: []
- subject: "patch: update etcher-sdk for cm4v5"
hash: 8e63be2efecada2ad6abd9d9d7728859e2c30ebc
body: ""
footer:
Change-type: patch
change-type: patch
author: builder555
nested: []
version: 1.14.2
title: ""
date: 2023-01-17T14:37:41.555Z
- commits:
- subject: fix disabled-screensaver unhandled exception outside balena-electron env
hash: 46c406e8c1e3b5e41890aff7f65b1711e4426782
body: ""
footer:
Change-type: patch
change-type: patch
author: Edwin Joassart
nested: []
version: 1.14.1
title: ""
date: 2023-01-16T13:22:36.972Z
- commits:
- subject: Anonymizes all paths before sending
hash: 86d43a536f7c9aa6b450a9f2f90341e07364208e
body: ""
footer:
Change-type: patch
change-type: patch
author: Otávio Jacobi
nested: []
- subject: "patch: Sentry fix path"
hash: 6c417e35a13873cd95d25f42a819de3750cdf65d
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: Remove personal path on etcher
hash: 2b728d3c521b76177a2c019b4891627272f35aac
body: ""
footer:
Change-type: minor
change-type: minor
author: Otávio Jacobi
nested: []
- subject: Unifying sentry reports in a single project
hash: f3f7ecb852503d4d97dbe6a78bf920ca177bddd1
body: ""
footer:
Change-type: patch
change-type: patch
author: Edwin Joassart
nested: []
- subject: Removes corvus in favor of sentry and analytics client
hash: 41fca03c98d4a72bd8c3842d7e6b9d41f65336f9
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Otavio Jacobi
signed-off-by: Otavio Jacobi
author: Otávio Jacobi
nested: []
- subject: Removes corvus in favor of sentry and analytics client
hash: 10caf8f1b6a174762192b13ce7bb4eaa71e90fcc
body: ""
footer:
Change-type: patch
change-type: patch
Signed-off-by: Otavio Jacobi
signed-off-by: Otavio Jacobi
author: Otávio Jacobi
nested: []
version: 1.14.0
title: ""
date: 2023-01-16T11:23:54.866Z
- commits: - commits:
- subject: Adding EtcherPro device serial number to the Settings modal - subject: Adding EtcherPro device serial number to the Settings modal
hash: d25eda9a7d6bf89284b630b2d55cbb0a7e3a9432 hash: d25eda9a7d6bf89284b630b2d55cbb0a7e3a9432

View File

@@ -3,48 +3,6 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
# 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 # v1.13.4
## (2023-01-12) ## (2023-01-12)

View File

@@ -31,7 +31,7 @@ Releasing
- [Post release note to forums](https://forums.balena.io/c/etcher) - [Post release note to forums](https://forums.balena.io/c/etcher)
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec) - [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
- [Update the website](https://github.com/balena-io/etcher-homepage) - [Update the website](https://github.com/balena-io/etcher-homepage)
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions - Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
- If regressions arise; pull the release, and release a patched version, else: - If regressions arise; pull the release, and release a patched version, else:
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray) - [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
- [Upload build artifacts to Amazon S3](#uploading-binaries-to-amazon-s3) - [Upload build artifacts to Amazon S3](#uploading-binaries-to-amazon-s3)
@@ -48,7 +48,7 @@ Make sure to set the analytics tokens when generating production release binarie
```bash ```bash
export ANALYTICS_SENTRY_TOKEN="xxxxxx" export ANALYTICS_SENTRY_TOKEN="xxxxxx"
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx" export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
``` ```
#### Linux #### Linux

View File

@@ -112,4 +112,4 @@ Analytics
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and - [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
check that no request is sent check that no request is sent
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or - [ ] **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**

View File

@@ -296,8 +296,6 @@ driveScanner.start();
let popupExists = false; let popupExists = false;
analytics.initAnalytics();
window.addEventListener('beforeunload', async (event) => { window.addEventListener('beforeunload', async (event) => {
if (!flashState.isFlashing() || popupExists) { if (!flashState.isFlashing() || popupExists) {
analytics.logEvent('Close application', { analytics.logEvent('Close application', {

View File

@@ -183,12 +183,7 @@ export class SafeWebview extends React.PureComponent<
// only care about this event if it's a request for the main frame // only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') { if (event.resourceType === 'mainFrame') {
const HTTP_OK = 200; const HTTP_OK = 200;
const { webContents, ...webviewEvent } = event; analytics.logEvent('SafeWebview loaded', { event });
analytics.logEvent('SafeWebview loaded', {
...webviewEvent,
screen_height: webContents?.hostWebContents.browserWindowOptions.height,
screen_width: webContents?.hostWebContents.browserWindowOptions.width,
});
this.setState({ this.setState({
shouldShow: event.statusCode === HTTP_OK, shouldShow: event.statusCode === HTTP_OK,
}); });

View File

@@ -39,6 +39,4 @@ i18next.use(initReactI18next).init({
}, },
}); });
export const supportedLocales = ['en', 'zh'];
export default i18next; export default i18next;

View File

@@ -138,8 +138,7 @@ const translation = {
autoUpdate: 'Auto-updates enabled', autoUpdate: 'Auto-updates enabled',
settings: 'Settings', settings: 'Settings',
systemInformation: 'System Information', systemInformation: 'System Information',
trimExtPartitions: trimExtPartitions: 'Trim unallocated space on raw images (in ext-type partitions)',
'Trim unallocated space on raw images (in ext-type partitions)',
}, },
menu: { menu: {
edit: 'Edit', edit: 'Edit',

View File

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

View File

@@ -15,188 +15,84 @@
*/ */
import * as _ from 'lodash'; import * as _ from 'lodash';
import { Client, createClient, createNoopClient } from 'analytics-client'; import * as resinCorvus from 'resin-corvus/browser';
import * as SentryRenderer from '@sentry/electron/renderer';
import * as packageJSON from '../../../../package.json';
import { getConfig } from '../../../shared/utils';
import * as settings from '../models/settings'; import * as settings from '../models/settings';
import { store } from '../models/store'; import { store } from '../models/store';
import * as packageJSON from '../../../../package.json';
type AnalyticsPayload = _.Dictionary<any>; const DEFAULT_PROBABILITY = 0.1;
const clearUserPath = (filename: string): string => { async function installCorvus(): Promise<void> {
const generatedFile = filename.split('generated').reverse()[0]; const sentryToken =
return generatedFile !== filename ? `generated${generatedFile}` : filename; (await settings.get('analyticsSentryToken')) ||
}; _.get(packageJSON, ['analytics', 'sentry', 'token']);
const mixpanelToken =
export const anonymizeSentryData = ( (await settings.get('analyticsMixpanelToken')) ||
event: SentryRenderer.Event, _.get(packageJSON, ['analytics', 'mixpanel', 'token']);
): SentryRenderer.Event => { resinCorvus.install({
event.exception?.values?.forEach((exception) => { services: {
exception.stacktrace?.frames?.forEach((frame) => { sentry: sentryToken,
if (frame.filename) { mixpanel: mixpanelToken,
frame.filename = clearUserPath(frame.filename); },
} options: {
}); release: packageJSON.version,
shouldReport: () => {
return settings.getSync('errorReporting');
},
mixpanelDeferred: true,
},
}); });
}
event.breadcrumbs?.forEach((breadcrumb) => { let mixpanelSample = DEFAULT_PROBABILITY;
if (breadcrumb.data?.url) {
breadcrumb.data.url = clearUserPath(breadcrumb.data.url);
}
});
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 * @summary Init analytics configurations
*/ */
export const initAnalytics = _.once(() => { async function initConfig() {
const dsn = await installCorvus();
settings.getSync('analyticsSentryToken') || let validatedConfig = null;
_.get(packageJSON, ['analytics', 'sentry', 'token']); try {
SentryRenderer.init({ dsn, beforeSend: anonymizeSentryData }); const configUrl = await settings.get('configUrl');
const config = await getConfig(configUrl);
const projectName = const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
settings.getSync('analyticsAmplitudeToken') || mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
_.get(packageJSON, ['analytics', 'amplitude', 'token']); if (isClientEligible(mixpanelSample)) {
validatedConfig = validateMixpanelConfig(mixpanel);
const clientConfig = {
projectName,
endpoint: 'data.balena-cloud.com',
componentName: 'etcher',
componentVersion: packageJSON.version,
};
analyticsClient = projectName
? createClient(clientConfig)
: createNoopClient();
});
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key: any, value: any) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
function flattenObject(obj: any) {
const toReturn: AnalyticsPayload = {};
for (const i in obj) {
if (!obj.hasOwnProperty(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 (!flatObject.hasOwnProperty(x)) {
continue;
}
toReturn[i.toLowerCase() + '.' + x.toLowerCase()] = flatObject[x];
}
} else {
toReturn[i] = obj[i];
} }
} catch (err) {
resinCorvus.logException(err);
} }
return toReturn; resinCorvus.setConfigs({
} mixpanel: validatedConfig,
function formatEvent(data: any): AnalyticsPayload {
const event = JSON.parse(JSON.stringify(data, getCircularReplacer()));
return anonymizeAnalyticsPayload(flattenObject(event));
}
function reportAnalytics(message: string, data: AnalyticsPayload = {}) {
const { applicationSessionUuid, flashingWorkflowUuid } = store
.getState()
.toJS();
const event = formatEvent({
...data,
applicationSessionUuid,
flashingWorkflowUuid,
}); });
analyticsClient.track(message, event); }
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;
} }
/** /**
@@ -205,12 +101,16 @@ function reportAnalytics(message: string, data: AnalyticsPayload = {}) {
* @description * @description
* This function sends the debug message to product analytics services. * This function sends the debug message to product analytics services.
*/ */
export async function logEvent(message: string, data: AnalyticsPayload = {}) { export function logEvent(message: string, data: _.Dictionary<any> = {}) {
const shouldReportAnalytics = await settings.get('errorReporting'); const { applicationSessionUuid, flashingWorkflowUuid } = store
if (shouldReportAnalytics) { .getState()
initAnalytics(); .toJS();
reportAnalytics(message, data); resinCorvus.logEvent(message, {
} ...data,
sample: mixpanelSample,
applicationSessionUuid,
flashingWorkflowUuid,
});
} }
/** /**
@@ -219,11 +119,4 @@ export async function logEvent(message: string, data: AnalyticsPayload = {}) {
* @description * @description
* This function logs an exception to error reporting services. * This function logs an exception to error reporting services.
*/ */
export function logException(error: any) { export const logException = resinCorvus.logException;
const shouldReportErrors = settings.getSync('errorReporting');
console.error(error);
if (shouldReportErrors) {
initAnalytics();
SentryRenderer.captureException(error);
}
}

View File

@@ -17,10 +17,10 @@
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
type BalenaTag = { type BalenaTag = {
id: number; id: number,
name: string; name: string,
value: string; value: string
}; }
export class EtcherPro { export class EtcherPro {
private supervisorAddr: string; private supervisorAddr: string;

View File

@@ -20,18 +20,16 @@ import { promises as fs } from 'fs';
import { platform } from 'os'; import { platform } from 'os';
import * as path from 'path'; import * as path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import * as _ from 'lodash';
import './app/i18n'; import './app/i18n';
import { packageType, version } from '../../package.json'; import { packageType, version } from '../../package.json';
import * as EXIT_CODES from '../shared/exit-codes'; import * as EXIT_CODES from '../shared/exit-codes';
import { delay, getConfig } from '../shared/utils';
import * as settings from './app/models/settings'; import * as settings from './app/models/settings';
import { logException } from './app/modules/analytics';
import { buildWindowMenu } from './menu'; import { buildWindowMenu } from './menu';
import * as i18n from 'i18next'; import * as i18n from 'i18next';
import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json';
import { anonymizeSentryData } from './app/modules/analytics';
const customProtocol = 'etcher'; const customProtocol = 'etcher';
const scheme = `${customProtocol}://`; const scheme = `${customProtocol}://`;
@@ -55,21 +53,13 @@ async function checkForUpdates(interval: number) {
packageUpdated = true; packageUpdated = true;
} }
} catch (err) { } catch (err) {
logMainProcessException(err); logException(err);
} }
} }
await delay(interval); 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> { async function isFile(filePath: string): Promise<boolean> {
try { try {
const stat = await fs.stat(filePath); const stat = await fs.stat(filePath);
@@ -104,14 +94,6 @@ async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
} }
} }
const initSentryMain = _.once(() => {
const dsn =
settings.getSync('analyticsSentryToken') ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryMain.init({ dsn, beforeSend: anonymizeSentryData });
});
const sourceSelectorReady = new Promise((resolve) => { const sourceSelectorReady = new Promise((resolve) => {
electron.ipcMain.on('source-selector-ready', resolve); electron.ipcMain.on('source-selector-ready', resolve);
}); });
@@ -205,6 +187,31 @@ async function createMainWindow() {
)}`, )}`,
); );
const page = mainWindow.webContents;
page.once('did-frame-finish-load', async () => {
autoUpdater.on('error', (err) => {
logException(err);
});
if (packageUpdatable) {
try {
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) {
logException(err);
}
}
});
return mainWindow; return mainWindow;
} }
@@ -226,7 +233,6 @@ async function main(): Promise<void> {
if (!electron.app.requestSingleInstanceLock()) { if (!electron.app.requestSingleInstanceLock()) {
electron.app.quit(); electron.app.quit();
} else { } else {
initSentryMain();
await electron.app.whenReady(); await electron.app.whenReady();
const window = await createMainWindow(); const window = await createMainWindow();
electron.app.on('second-instance', async (_event, argv) => { electron.app.on('second-instance', async (_event, argv) => {
@@ -250,6 +256,7 @@ async function main(): Promise<void> {
}); });
} }
} }
main(); main();
console.time('ready-to-show'); console.time('ready-to-show');

View File

@@ -20,7 +20,6 @@ import { env } from 'process';
import { promisify } from 'util'; import { promisify } from 'util';
import { getAppPath } from '../utils'; import { getAppPath } from '../utils';
import { supportedLocales } from '../../gui/app/i18n';
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
@@ -33,12 +32,6 @@ export async function sudo(
try { try {
let lang = Intl.DateTimeFormat().resolvedOptions().locale; let lang = Intl.DateTimeFormat().resolvedOptions().locale;
lang = lang.substr(0, 2); lang = lang.substr(0, 2);
if (supportedLocales.indexOf(lang) > -1) {
// language should be present
} else {
// fallback to eng
lang = 'en';
}
const { stdout, stderr } = await execFileAsync( const { stdout, stderr } = await execFileAsync(
'sudo', 'sudo',
@@ -50,7 +43,7 @@ export async function sudo(
SUDO_ASKPASS: join( SUDO_ASKPASS: join(
getAppPath(), getAppPath(),
__dirname, __dirname,
`sudo-askpass.osascript-${lang}.js`, 'sudo-askpass.osascript-' + lang + '.js',
), ),
}, },
}, },

View File

@@ -15,6 +15,7 @@
*/ */
import axios from 'axios'; import axios from 'axios';
import { app, remote } from 'electron';
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import * as errors from './errors'; import * as errors from './errors';
@@ -32,6 +33,16 @@ export function percentageToFloat(percentage: any) {
return percentage / 100; 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> { export async function delay(duration: number): Promise<void> {
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, duration); setTimeout(resolve, duration);
@@ -39,7 +50,6 @@ export async function delay(duration: number): Promise<void> {
} }
export function getAppPath(): string { export function getAppPath(): string {
const { app, remote } = require('electron');
return ( return (
(app || remote.app) (app || remote.app)
.getAppPath() .getAppPath()

2893
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "balena-etcher", "name": "balena-etcher",
"private": true, "private": true,
"displayName": "balenaEtcher", "displayName": "balenaEtcher",
"version": "1.15.2", "version": "1.13.4",
"packageType": "local", "packageType": "local",
"main": "generated/etcher.js", "main": "generated/etcher.js",
"description": "Flash OS images to SD cards and USB drives, safely and easily.", "description": "Flash OS images to SD cards and USB drives, safely and easily.",
@@ -31,6 +31,7 @@
"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-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-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}", "test": "echo npm run test-{linux,windows,macos}",
"uploadSourcemap": "sentry-cli releases files $npm_config_SENTRY_VERSION upload-sourcemaps ./generated/*.js.map --org $npm_config_SENTRY_ORG --project $npm_config_SENTRY_PROJECT",
"watch": "webpack serve --no-optimization-minimize --config ./webpack.dev.config.ts", "watch": "webpack serve --no-optimization-minimize --config ./webpack.dev.config.ts",
"webpack": "webpack" "webpack": "webpack"
}, },
@@ -47,13 +48,13 @@
"npm run lint-css" "npm run lint-css"
] ]
}, },
"author": "Balena Ltd. <hello@balena.io>", "author": "Balena Inc. <hello@etcher.io>",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"@balena/lint": "5.4.2", "@balena/lint": "5.4.2",
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534", "@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
"@fortawesome/fontawesome-free": "5.15.4", "@fortawesome/fontawesome-free": "5.15.4",
"@sentry/electron": "^4.1.2", "@sentry/cli": "^2.11.0",
"@svgr/webpack": "5.5.0", "@svgr/webpack": "5.5.0",
"@types/chai": "4.3.4", "@types/chai": "4.3.4",
"@types/copy-webpack-plugin": "6.4.3", "@types/copy-webpack-plugin": "6.4.3",
@@ -69,21 +70,20 @@
"@types/terser-webpack-plugin": "5.0.4", "@types/terser-webpack-plugin": "5.0.4",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"@types/webpack-node-externals": "2.5.3", "@types/webpack-node-externals": "2.5.3",
"analytics-client": "^2.0.1", "aws4-axios": "2.4.9",
"axios": "^0.27.2",
"chai": "4.3.7", "chai": "4.3.7",
"copy-webpack-plugin": "7.0.0", "copy-webpack-plugin": "7.0.0",
"css-loader": "5.2.7", "css-loader": "5.2.7",
"d3": "4.13.0", "d3": "4.13.0",
"debug": "4.3.4", "debug": "4.3.4",
"electron": "^13.6.9", "electron": "^13.5.0",
"electron-builder": "^23.6.0", "electron-builder": "^23.0.9",
"electron-mocha": "9.3.3", "electron-mocha": "9.3.3",
"electron-notarize": "1.2.2", "electron-notarize": "1.2.2",
"electron-rebuild": "3.2.3", "electron-rebuild": "3.2.9",
"electron-updater": "5.3.0", "electron-updater": "5.3.0",
"esbuild-loader": "2.20.0", "esbuild-loader": "2.20.0",
"etcher-sdk": "^8.2.0", "etcher-sdk": "^7.4.7",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"husky": "4.3.8", "husky": "4.3.8",
"i18next": "21.10.0", "i18next": "21.10.0",
@@ -104,6 +104,7 @@
"react-i18next": "11.18.6", "react-i18next": "11.18.6",
"redux": "4.2.0", "redux": "4.2.0",
"rendition": "19.3.2", "rendition": "19.3.2",
"resin-corvus": "2.0.5",
"semver": "7.3.8", "semver": "7.3.8",
"simple-progress-webpack-plugin": "1.1.2", "simple-progress-webpack-plugin": "1.1.2",
"sinon": "9.2.4", "sinon": "9.2.4",
@@ -124,9 +125,9 @@
"webpack-dev-server": "4.11.1" "webpack-dev-server": "4.11.1"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=14 < 16"
}, },
"versionist": { "versionist": {
"publishedAt": "2023-02-02T13:05:02.262Z" "publishedAt": "2023-01-12T15:10:50.986Z"
} }
} }

View File

@@ -15,7 +15,7 @@
*/ */
import * as CopyPlugin from 'copy-webpack-plugin'; import * as CopyPlugin from 'copy-webpack-plugin';
import { readdirSync } from 'fs'; import { readdirSync, existsSync } from 'fs';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as os from 'os'; import * as os from 'os';
import outdent from 'outdent'; import outdent from 'outdent';
@@ -23,17 +23,13 @@ import * as path from 'path';
import { env } from 'process'; import { env } from 'process';
import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin'; import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin';
import * as TerserPlugin from 'terser-webpack-plugin'; import * as TerserPlugin from 'terser-webpack-plugin';
import { import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
BannerPlugin,
IgnorePlugin,
NormalModuleReplacementPlugin,
} from 'webpack';
import * as PnpWebpackPlugin from 'pnp-webpack-plugin'; import * as PnpWebpackPlugin from 'pnp-webpack-plugin';
import * as tsconfigRaw from './tsconfig.webpack.json'; import * as tsconfigRaw from './tsconfig.webpack.json';
/** /**
* Don't webpack package.json as sentry tokens * Don't webpack package.json as mixpanel & sentry tokens
* will be inserted in it after webpacking * will be inserted in it after webpacking
*/ */
function externalPackageJson(packageJsonPath: string) { function externalPackageJson(packageJsonPath: string) {
@@ -81,6 +77,26 @@ function renameNodeModules(resourcePath: string) {
); );
} }
function findExt2fsFolder(): string {
const ext2fs = 'node_modules/ext2fs';
const biFsExt2fs = 'node_modules/balena-image-fs/node_modules/ext2fs';
if (existsSync(ext2fs)) {
return ext2fs;
} else if (existsSync(biFsExt2fs)) {
return biFsExt2fs;
} else {
throw Error('ext2fs not found');
}
}
function makeExt2FsRegex(): RegExp {
const folder = findExt2fsFolder();
const libpath = '/lib/libext2fs\\.js$';
return new RegExp(folder.concat(libpath));
}
function findUsbPrebuild(): string[] { function findUsbPrebuild(): string[] {
const usbPrebuildsFolder = path.join('node_modules', 'usb', 'prebuilds'); const usbPrebuildsFolder = path.join('node_modules', 'usb', 'prebuilds');
const prebuildFolders = readdirSync(usbPrebuildsFolder); const prebuildFolders = readdirSync(usbPrebuildsFolder);
@@ -164,6 +180,31 @@ function replace(test: RegExp, ...replacements: ReplacementRule[]) {
}; };
} }
function fetchWasm(...where: string[]) {
const whereStr = where.map((x) => `'${x}'`).join(', ');
return outdent`
const Path = require('path');
let electron;
try {
// This doesn't exist in the child-writer
electron = require('electron');
} catch {
}
function appPath() {
return Path.isAbsolute(__dirname) ?
__dirname :
Path.join(
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
// include the app-x64 or app-arm64 folder depending on the arch.
// We don't care about the app.asar file, we want the actual folder.
electron.remote.app.getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
'generated'
);
}
scriptDirectory = Path.join(appPath(), 'modules', ${whereStr}, '/');
`;
}
const commonConfig = { const commonConfig = {
mode: 'production', mode: 'production',
optimization: { optimization: {
@@ -298,6 +339,18 @@ const commonConfig = {
); );
`, `,
}), }),
// Use the libext2fs.wasm file in the generated folder
// The way to find the app directory depends on whether we run in the renderer or in the child-writer
// We use __dirname in the child-writer and electron.remote.app.getAppPath() in the renderer
replace(makeExt2FsRegex(), {
search: 'scriptDirectory = __dirname + "/";',
replace: fetchWasm('ext2fs', 'lib'),
}),
// Same for node-crc-utils
replace(/node_modules\/@balena\/node-crc-utils\/crc32\.js$/, {
search: 'scriptDirectory=__dirname+"/"',
replace: fetchWasm('@balena', 'node-crc-utils'),
}),
// Copy native modules to generated folder // Copy native modules to generated folder
{ {
test: /\.node$/, test: /\.node$/,
@@ -324,14 +377,6 @@ const commonConfig = {
slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/), slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/),
'./http.js', './http.js',
), ),
// Ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used
// by etcher-sdk and does a runtime check to its availability. Were not currently
// using the “assume role” functionality (AFAIU) of aws4-axios and we dont care that
// its not found, so force webpack to ignore the import.
// See https://github.com/aws/aws-sdk-js-v3/issues/3025
new IgnorePlugin({
resourceRegExp: /^aws-crt$/,
}),
], ],
resolveLoader: { resolveLoader: {
plugins: [PnpWebpackPlugin.moduleLoader(module)], plugins: [PnpWebpackPlugin.moduleLoader(module)],
@@ -359,6 +404,14 @@ const guiConfigCopyPatterns = [
from: 'node_modules/node-raspberrypi-usbboot/blobs', from: 'node_modules/node-raspberrypi-usbboot/blobs',
to: 'modules/node-raspberrypi-usbboot/blobs', to: 'modules/node-raspberrypi-usbboot/blobs',
}, },
{
from: `${findExt2fsFolder()}/lib/libext2fs.wasm`,
to: 'modules/ext2fs/lib/libext2fs.wasm',
},
{
from: 'node_modules/@balena/node-crc-utils/crc32.wasm',
to: 'modules/@balena/node-crc-utils/crc32.wasm',
},
]; ];
if (os.platform() === 'win32') { if (os.platform() === 'win32') {
@@ -402,6 +455,7 @@ const guiConfig = {
const mainConfig = { const mainConfig = {
...commonConfig, ...commonConfig,
target: 'electron-main', target: 'electron-main',
devtool: 'source-map',
node: { node: {
__dirname: false, __dirname: false,
__filename: true, __filename: true,