├── .dockerignore ├── .editorconfig ├── .eslintrc.yml ├── .gitattributes ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE.md ├── .gitignore ├── .gitmodules ├── .gitsecret ├── keys │ ├── pubring.kbx │ ├── pubring.kbx~ │ └── trustdb.gpg └── paths │ └── mapping.cfg ├── .nvmrc ├── CHANGELOG.md ├── FAQ.md ├── LICENSE.md ├── Logo.svg ├── Makefile ├── README.md ├── SUPPORT.md ├── TODO.md ├── after-install.tpl ├── afterPack.js ├── afterSignHook.js ├── assets ├── dmg │ ├── background.png │ ├── background.tiff │ └── background@2x.png ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── iconset │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 144x144.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ └── 96x96.png └── install-spinner.gif ├── build.sh ├── build_linux.sh ├── dev-app-update.yml ├── dictionary.txt ├── docs ├── ARCHITECTURE.md ├── COMMIT-GUIDELINES.md ├── CONTRIBUTING.md ├── FAQ.md ├── MAINTAINERS.md ├── MANUAL-TESTING.md ├── PUBLISHING.md ├── SUPPORT.md └── USER-DOCUMENTATION.md ├── electron-builder.json ├── icon.png ├── lib ├── gui │ ├── app │ │ ├── app.ts │ │ ├── components │ │ │ ├── drive-selector │ │ │ │ └── drive-selector.tsx │ │ │ ├── drive-status-warning-modal │ │ │ │ └── drive-status-warning-modal.tsx │ │ │ ├── finish │ │ │ │ └── finish.tsx │ │ │ ├── flash-another │ │ │ │ └── flash-another.tsx │ │ │ ├── flash-results │ │ │ │ └── flash-results.tsx │ │ │ ├── progress-button │ │ │ │ └── progress-button.tsx │ │ │ ├── reduced-flashing-infos │ │ │ │ └── reduced-flashing-infos.tsx │ │ │ ├── safe-webview │ │ │ │ └── safe-webview.tsx │ │ │ ├── settings │ │ │ │ └── settings.tsx │ │ │ ├── source-selector │ │ │ │ └── source-selector.tsx │ │ │ ├── svg-icon │ │ │ │ └── svg-icon.tsx │ │ │ └── target-selector │ │ │ │ ├── target-selector-button.tsx │ │ │ │ └── target-selector.tsx │ │ ├── css │ │ │ ├── fonts │ │ │ │ ├── SourceSansPro-Regular.ttf │ │ │ │ └── SourceSansPro-SemiBold.ttf │ │ │ └── main.css │ │ ├── i18n.ts │ │ ├── i18n │ │ │ ├── README.md │ │ │ ├── en.ts │ │ │ ├── zh-CN.ts │ │ │ └── zh-TW.ts │ │ ├── index.css │ │ ├── index.dev.html │ │ ├── index.html │ │ ├── models │ │ │ ├── available-drives.ts │ │ │ ├── flash-state.ts │ │ │ ├── leds.ts │ │ │ ├── selection-state.ts │ │ │ ├── settings.ts │ │ │ └── store.ts │ │ ├── modules │ │ │ ├── analytics.ts │ │ │ ├── drive-scanner.ts │ │ │ ├── exception-reporter.ts │ │ │ ├── image-writer.ts │ │ │ └── progress-status.ts │ │ ├── os │ │ │ ├── dialog.ts │ │ │ ├── notification.ts │ │ │ ├── open-external │ │ │ │ └── services │ │ │ │ │ └── open-external.ts │ │ │ ├── open-internal-remote │ │ │ │ └── services │ │ │ │ │ └── open-internal-remote.ts │ │ │ ├── open-internal │ │ │ │ └── services │ │ │ │ │ └── open-internal.ts │ │ │ ├── window-progress.ts │ │ │ └── windows-network-drives.ts │ │ ├── pages │ │ │ └── main │ │ │ │ ├── Flash.tsx │ │ │ │ └── MainPage.tsx │ │ ├── renderer.ts │ │ ├── styled-components.tsx │ │ ├── theme.ts │ │ └── utils │ │ │ ├── etcher-pro-specific.ts │ │ │ └── middle-ellipsis.ts │ ├── assets │ │ ├── balena.svg │ │ ├── drive.svg │ │ ├── etcher.svg │ │ ├── flash.svg │ │ ├── icon.ico │ │ ├── icon48.png │ │ ├── icon64.png │ │ ├── image.svg │ │ ├── loading.svg │ │ ├── love.svg │ │ ├── raspberrypi.svg │ │ ├── resin.svg │ │ ├── src.svg │ │ ├── tgt.svg │ │ └── warning.svg │ ├── etcher.ts │ ├── menu.ts │ └── modules │ │ └── child-writer.ts └── shared │ ├── catalina-sudo │ ├── sudo-askpass.osascript-en.js │ ├── sudo-askpass.osascript-zh.js │ └── sudo.ts │ ├── drive-constraints.ts │ ├── errors.ts │ ├── exit-codes.ts │ ├── messages.ts │ ├── permissions.ts │ ├── supported-formats.ts │ ├── units.ts │ └── utils.ts ├── npm-shrinkwrap.json ├── package.json ├── promo.png ├── repo.yml ├── requirements.txt ├── scripts ├── ci │ └── ensure-all-file-extensions-in-gitattributes.sh └── clean-shrinkwrap.ts ├── secrets ├── APPLE_SIGNING.p12.secret ├── APPLE_SIGNING_PASSWORD.txt.secret ├── WINDOWS_SIGNING.pfx.secret ├── WINDOWS_SIGNING_PASSWORD.txt.secret └── XCODE_APP_LOADER_PASSWORD.txt.secret ├── tests ├── .eslintrc.yml ├── data │ └── wmic-output.txt ├── gui │ ├── allow-renderer-process-reuse.ts │ ├── models │ │ ├── available-drives.spec.ts │ │ ├── flash-state.spec.ts │ │ ├── selection-state.spec.ts │ │ └── settings.spec.ts │ ├── modules │ │ ├── child-writer.spec.ts │ │ ├── image-writer.spec.ts │ │ └── progress-status.spec.ts │ ├── os │ │ ├── window-progress.spec.ts │ │ └── windows-network-drives.spec.ts │ ├── utils │ │ └── middle-ellipsis.spec.ts │ └── window-config.json ├── shared │ ├── drive-constraints.spec.ts │ ├── errors.spec.ts │ ├── messages.spec.ts │ ├── permissions.spec.ts │ ├── supported-formats.spec.ts │ ├── units.spec.ts │ └── utils.spec.ts └── spectron │ └── runner.spec.ts ├── tsconfig.json ├── tsconfig.webpack.json ├── tslint.json ├── typings ├── omit-deep-lodash │ └── index.d.ts ├── path-is-inside │ └── index.d.ts ├── pnp-webpack-plugin │ └── index.d.ts ├── resin-corvus │ └── index.d.ts ├── simple-progress-webpack-plugin │ └── index.d.ts ├── sudo-prompt │ └── index.d.ts └── svg │ └── index.d.ts ├── webpack.config.ts ├── webpack.dev.config.bak └── webpack.dev.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !requirements.txt 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [*.ts] 18 | indent_style = tab 19 | 20 | [*.tsx] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # default 2 | * text 3 | 4 | # Javascript files must retain LF line-endings (to keep eslint happy) 5 | *.js text eol=lf 6 | *.jsx text eol=lf 7 | *.ts text eol=lf 8 | *.tsx text eol=lf 9 | # CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy) 10 | *.css text eol=lf 11 | *.scss text eol=lf 12 | 13 | # Text files 14 | Dockerfile* text 15 | .dockerignore text 16 | .editorconfig text 17 | etcher text 18 | .git* text 19 | *.html text 20 | *.json text eol=lf 21 | *.cpp text 22 | *.h text 23 | *.gyp text 24 | LICENSE text 25 | Makefile text 26 | *.md text 27 | *.sh text 28 | *.bat text 29 | *.svg text 30 | *.yml text 31 | *.patch text 32 | *.txt text 33 | *.tpl text 34 | CODEOWNERS text 35 | *.plist text 36 | 37 | # Binary files (no line-ending conversions) 38 | *.bz2 binary diff=hex 39 | *.gz binary diff=hex 40 | *.icns binary diff=hex 41 | *.ico binary diff=hex 42 | *.tiff binary diff=hex 43 | *.img binary diff=hex 44 | *.iso binary diff=hex 45 | *.png binary diff=hex 46 | *.bin binary diff=hex 47 | *.elf binary diff=hex 48 | *.xz binary diff=hex 49 | *.zip binary diff=hex 50 | *.dtb binary diff=hex 51 | *.dtbo binary diff=hex 52 | *.dat binary diff=hex 53 | *.bin binary diff=hex 54 | *.dmg binary diff=hex 55 | *.rpi-sdcard binary diff=hex 56 | *.wic binary diff=hex 57 | *.foo binary diff=hex 58 | *.eot binary diff=hex 59 | *.otf binary diff=hex 60 | *.woff binary diff=hex 61 | *.woff2 binary diff=hex 62 | *.ttf binary diff=hex 63 | xz-without-extension binary diff=hex 64 | wmic-output.txt binary diff=hex 65 | 66 | # gitsecret 67 | *.secret binary 68 | .gitsecret/** binary 69 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ['Alex313031'] 4 | custom: ['https://paypal.me/alex313031?country.x=US&locale.x=en_US'] 5 | patreon: ThoriumDeveloper 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **Etcher version:** 2 | - **Operating system and architecture:** 3 | - **Image flashed:** 4 | - **What do you think should have happened:** 5 | - **What happened:** 6 | - **Do you see any meaningful error information in the DevTools?** 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | /logs 3 | *.log 4 | npm-debug.log* 5 | .eslintcache 6 | Thumbs.db 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | /lib-cov 15 | 16 | # Image stream output directory 17 | /tests/image-stream/output 18 | 19 | # Coverage directory used by tools like istanbul 20 | /coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | /build 30 | /release/ 31 | 32 | # Electron binaries 33 | /electron/ 34 | 35 | # Generated files 36 | /generated 37 | 38 | # Dependency directory 39 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 40 | node_modules 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | !/typings/ 45 | !/lib/shared/typings 46 | 47 | # Compiled Etcher releases 48 | /dist 49 | 50 | #local development 51 | .yalc 52 | yarn.lock 53 | package-lock.json 54 | 55 | # Certificates 56 | *.spc 57 | *.pvk 58 | *.p12 59 | *.cer 60 | *.crt 61 | *.pem 62 | 63 | # OSX files 64 | 65 | .DS_Store 66 | 67 | # VSCode files 68 | 69 | .vscode 70 | .gitsecret/keys/random_seed 71 | !*.secret 72 | secrets/APPLE_SIGNING_PASSWORD.txt 73 | secrets/WINDOWS_SIGNING_PASSWORD.txt 74 | secrets/XCODE_APP_LOADER_PASSWORD.txt 75 | secrets/WINDOWS_SIGNING.pfx 76 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "scripts/resin"] 2 | path = scripts/resin 3 | url = https://github.com/balena-io/scripts.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.gitsecret/keys/pubring.kbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/.gitsecret/keys/pubring.kbx -------------------------------------------------------------------------------- /.gitsecret/keys/pubring.kbx~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/.gitsecret/keys/pubring.kbx~ -------------------------------------------------------------------------------- /.gitsecret/keys/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/.gitsecret/keys/trustdb.gpg -------------------------------------------------------------------------------- /.gitsecret/paths/mapping.cfg: -------------------------------------------------------------------------------- 1 | secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d 2 | secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568 3 | secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1 4 | secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714 5 | secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/gallium 2 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | ## Why is my drive not bootable? 2 | 3 | Etcher copies images to drives byte by byte, without doing any transformation to the final device, which means images that require special treatment to be made bootable, like Windows images, will not work out of the box. In these cases, the general advice is to use software specific to those kind of images, usually available from the image publishers themselves. You can find more information [here](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#why-is-my-drive-not-bootable). 4 | 5 | ## How can I configure persistent storage? 6 | 7 | Some programs, usually oriented at making GNU/Linux live USB drives, include an option to set persistent storage. This is currently not supported by Etcher, so if you require this functionality, we advise to fallback to [UNetbootin](https://unetbootin.github.io/). 8 | 9 | ## How do I flash Ubuntu ISOs 10 | 11 | Ubuntu images (and potentially some other related GNU/Linux distributions) have a peculiar format that allows the image to boot without any further modification from both CDs and USB drives. 12 | A consequence of this enhancement is that some programs, like parted get confused about the drive's format and partition table, printing warnings such as: 13 | 14 | > /dev/xxx contains GPT signatures, indicating that it has a GPT table. However, it does not have a valid fake msdos partition table, as it should. Perhaps it was corrupted -- possibly by a program that doesn't understand GPT partition tables. Or perhaps you deleted the GPT table, and are now using an msdos partition table. Is this a GPT partition table? Both the primary and backup GPT tables are corrupt. Try making a fresh table, and using Parted's rescue feature to recover partitions. 15 | 16 | > Warning: The driver descriptor says the physical block size is 2048 bytes, but Linux says it is 512 bytes. 17 | 18 | All these warnings are safe to ignore, and your drive should be able to boot without any problems. 19 | Refer to [the following message from Ubuntu's mailing list](https://lists.ubuntu.com/archives/ubuntu-devel/2011-June/033495.html) if you want to learn more. 20 | 21 | ## How do I run Etcher on Wayland? 22 | 23 | The XWayland Server provides backwards compatibility to run any X client on Wayland, including Etcher. 24 | This usually works out of the box on mainstream GNU/Linux distributions that properly support Wayland. If it doesn't, make sure the xwayland.so module is being loaded by declaring it in your [weston.ini](http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html): 25 | 26 | ``` 27 | [core] 28 | modules=xwayland.so 29 | ``` 30 | 31 | ## What are the runtime GNU/LINUX dependencies? 32 | 33 | [This entry](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#runtime-gnulinux-dependencies) aims to provide an up to date list of runtime dependencies needed to run Etcher on a GNU/Linux system. 34 | 35 | ## How can I recover the broken drive? 36 | 37 | Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state. 38 | To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems. 39 | 40 | ## I receive "No polkit authentication agent found" error in GNU/Linux 41 | 42 | Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice. 43 | 44 | ## May I run Etcher in older macOS versions? 45 | 46 | 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). 47 | -------------------------------------------------------------------------------- /Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------------------------- 2 | # Build configuration 3 | # --------------------------------------------------------------------- 4 | 5 | RESIN_SCRIPTS ?= ./scripts/resin 6 | export NPM_VERSION ?= 8.19.4 7 | S3_BUCKET = artifacts.ci.balena-cloud.com 8 | 9 | # This directory will be completely deleted by the `clean` rule 10 | BUILD_DIRECTORY ?= dist 11 | 12 | BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp 13 | 14 | $(BUILD_DIRECTORY): 15 | mkdir $@ 16 | 17 | $(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY) 18 | mkdir $@ 19 | 20 | SHELL := /bin/bash 21 | 22 | # --------------------------------------------------------------------- 23 | # Operating system and architecture detection 24 | # --------------------------------------------------------------------- 25 | 26 | # http://stackoverflow.com/a/12099167 27 | ifeq ($(OS),Windows_NT) 28 | PLATFORM = win32 29 | 30 | ifeq ($(PROCESSOR_ARCHITEW6432),AMD64) 31 | HOST_ARCH = x64 32 | else 33 | ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) 34 | HOST_ARCH = x64 35 | endif 36 | ifeq ($(PROCESSOR_ARCHITECTURE),x86) 37 | HOST_ARCH = x86 38 | endif 39 | endif 40 | else 41 | ifeq ($(shell uname -s),Linux) 42 | PLATFORM = linux 43 | 44 | ifeq ($(shell uname -m),x86_64) 45 | HOST_ARCH = x64 46 | endif 47 | ifneq ($(filter %86,$(shell uname -m)),) 48 | HOST_ARCH = x86 49 | endif 50 | ifeq ($(shell uname -m),armv7l) 51 | HOST_ARCH = armv7hf 52 | endif 53 | ifeq ($(shell uname -m),aarch64) 54 | HOST_ARCH = aarch64 55 | endif 56 | ifeq ($(shell uname -m),armv8) 57 | HOST_ARCH = aarch64 58 | endif 59 | ifeq ($(shell uname -m),arm64) 60 | HOST_ARCH = aarch64 61 | endif 62 | endif 63 | ifeq ($(shell uname -s),Darwin) 64 | PLATFORM = darwin 65 | 66 | ifeq ($(shell uname -m),x86_64) 67 | HOST_ARCH = x64 68 | endif 69 | ifeq ($(shell uname -m),arm64) 70 | HOST_ARCH = aarch64 71 | endif 72 | endif 73 | endif 74 | 75 | ifndef PLATFORM 76 | $(error We could not detect your host platform) 77 | endif 78 | ifndef HOST_ARCH 79 | $(error We could not detect your host architecture) 80 | endif 81 | 82 | # Default to host architecture. You can override by doing: 83 | # 84 | # make TARGET_ARCH= 85 | # 86 | TARGET_ARCH ?= $(HOST_ARCH) 87 | 88 | # --------------------------------------------------------------------- 89 | # Electron 90 | # --------------------------------------------------------------------- 91 | electron-develop: 92 | git submodule update --init && \ 93 | npm ci && \ 94 | npm run webpack 95 | 96 | electron-test: 97 | $(RESIN_SCRIPTS)/electron/test.sh \ 98 | -b $(shell pwd) \ 99 | -s $(PLATFORM) 100 | 101 | assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png 102 | tiffutil -cathidpicheck $^ -out $@ 103 | 104 | electron-build: assets/dmg/background.tiff | $(BUILD_TEMPORARY_DIRECTORY) 105 | $(RESIN_SCRIPTS)/electron/build.sh \ 106 | -b $(shell pwd) \ 107 | -r $(TARGET_ARCH) \ 108 | -s $(PLATFORM) \ 109 | -v production \ 110 | -n $(BUILD_TEMPORARY_DIRECTORY)/npm 111 | 112 | # --------------------------------------------------------------------- 113 | # Phony targets 114 | # --------------------------------------------------------------------- 115 | 116 | TARGETS = \ 117 | help \ 118 | info \ 119 | lint \ 120 | test \ 121 | clean \ 122 | distclean \ 123 | electron-develop \ 124 | electron-test \ 125 | electron-build 126 | 127 | .PHONY: $(TARGETS) 128 | 129 | lint: 130 | npm run lint 131 | 132 | test: 133 | npm run test 134 | 135 | help: 136 | @echo "Available targets: $(TARGETS)" 137 | 138 | info: 139 | @echo "Platform : $(PLATFORM)" 140 | @echo "Host arch : $(HOST_ARCH)" 141 | @echo "Target arch : $(TARGET_ARCH)" 142 | 143 | clean: 144 | rm -rf $(BUILD_DIRECTORY) 145 | 146 | distclean: clean 147 | rm -rf node_modules 148 | rm -rf dist 149 | rm -rf generated 150 | rm -rf $(BUILD_TEMPORARY_DIRECTORY) 151 | 152 | .DEFAULT_GOAL = help 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Etcher 2 | 3 | > Flash OS images to SD cards & USB drives, safely and easily. 4 | 5 | Etcher is a powerful OS image flasher built with web technologies to ensure 6 | flashing an SDCard or USB drive is a pleasant and safe experience. It protects 7 | you from accidentally writing to your hard-drives, ensures every byte of data 8 | was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode). 9 | 10 | [![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher) 11 | [![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE) 12 | [![Balena.io Forums](https://img.shields.io/discourse/https/forums.balena.io/topics.svg?style=flat-square&label=balena.io%20forums)](https://forums.balena.io/c/etcher) 13 | 14 | --- 15 | 16 | [**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones] 17 | 18 | ## Supported Operating Systems 19 | 20 | - Linux (most distros) 21 | - macOS 10.10 (Yosemite) and later 22 | - Microsoft Windows 7 and later 23 | 24 | **Note**: Etcher will run on any platform officially supported by 25 | [Electron][electron]. Read more in their 26 | [documentation][electron-supported-platforms]. 27 | 28 | ## Installers 29 | 30 | Refer to the [downloads page][etcher] for the latest pre-made 31 | installers for all supported operating systems. 32 | 33 | ## Packages 34 | 35 | #### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64) 36 | 37 | Package for Debian and Ubuntu can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/) 38 | 39 | ##### Install .deb file using apt 40 | 41 | ```sh 42 | sudo apt install ./balena-etcher_******_amd64.deb 43 | ``` 44 | 45 | ##### Uninstall 46 | 47 | ```sh 48 | sudo apt remove balena-etcher 49 | ``` 50 | 51 | #### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64) 52 | 53 | ##### Yum 54 | 55 | Package for Fedora-based and Redhat can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/) 56 | 57 | 1. Install using yum 58 | 59 | ```sh 60 | sudo yum localinstall balena-etcher-***.x86_64.rpm 61 | ``` 62 | 63 | #### Arch/Manjaro Linux (GNU/Linux x64) 64 | 65 | Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release: 66 | 67 | ```sh 68 | yay -S balena-etcher 69 | ``` 70 | 71 | ##### Uninstall 72 | 73 | ```sh 74 | yay -R balena-etcher 75 | ``` 76 | 77 | #### WinGet (Windows) 78 | 79 | This package is updated by [gh-action](https://github.com/vedantmgoyal2009/winget-releaser), and is kept up to date automatically. 80 | 81 | ```sh 82 | winget install balenaEtcher #or Balena.Etcher 83 | ``` 84 | 85 | ##### Uninstall 86 | 87 | ```sh 88 | winget uninstall balenaEtcher 89 | ``` 90 | 91 | #### Chocolatey (Windows) 92 | 93 | This package is maintained by [@majkinetor](https://github.com/majkinetor), and 94 | is kept up to date automatically. 95 | 96 | ```sh 97 | choco install etcher 98 | ``` 99 | 100 | ##### Uninstall 101 | 102 | ```sh 103 | choco uninstall etcher 104 | ``` 105 | 106 | ## Support 107 | 108 | If you're having any problem, please [raise an issue][newissue] on GitHub, and 109 | the balena.io team will be happy to help. 110 | 111 | ## License 112 | 113 | Etcher is free software and may be redistributed under the terms specified in 114 | the [license]. 115 | 116 | [etcher]: https://balena.io/etcher 117 | [electron]: https://electronjs.org/ 118 | [electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms 119 | [support]: https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md 120 | [contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md 121 | [user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md 122 | [milestones]: https://github.com/balena-io/etcher/milestones 123 | [newissue]: https://github.com/balena-io/etcher/issues/new 124 | [license]: https://github.com/balena-io/etcher/blob/master/LICENSE 125 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | Getting help with BalenaEtcher 2 | =============================== 3 | 4 | There are various ways to get support for Etcher if you experience an issue or 5 | have an idea you'd like to share with us. 6 | 7 | Documentation 8 | ------ 9 | 10 | We have answers to a variety of frequently asked questions in the [user 11 | documentation][documentation] and also in the [FAQs][faq] on the Etcher website. 12 | 13 | 14 | Forums 15 | ------ 16 | 17 | We have a [Discourse forum][discourse] which is open to everyone, so please 18 | come join us :). Drop us a line there and the balena.io staff and community 19 | users will be happy to assist. Your question might already be answered, so take 20 | a look at the existing threads before opening a new one! 21 | 22 | Make sure to mention the following information to help us provide better 23 | support: 24 | 25 | - The BalenaEtcher version you're running. 26 | 27 | - The operating system you're running Etcher in. 28 | 29 | - Relevant logging output, if any, from DevTools, which you can open by 30 | pressing `Ctrl+Shift+I` or `Cmd+Alt+I` depending on your platform. 31 | 32 | GitHub 33 | ------ 34 | 35 | If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue 36 | tracker][issues] and if there isn't a ticket covering it, [create 37 | one][new-issue]. 38 | 39 | [discourse]: https://forums.balena.io/c/etcher 40 | [issues]: https://github.com/balena-io/etcher/issues 41 | [new-issue]: https://github.com/balena-io/etcher/issues/new 42 | [documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md 43 | [faq]: https://etcher.io 44 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Make openInternal work for everything 2 | - Use builtin logger instead of electron-log, and make it output to console for everything 3 | -------------------------------------------------------------------------------- /after-install.tpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Link to the binary 4 | # Must hardcode Etcher-ng directory; no variable available 5 | ln -sf '/opt/Etcher-ng/${executable}' '/usr/bin/${executable}' 6 | 7 | # SUID chrome-sandbox for Electron 5+ 8 | chmod 4755 '/opt/Etcher-ng/chrome-sandbox' || true 9 | 10 | update-mime-database /usr/share/mime || true 11 | update-desktop-database /usr/share/applications || true 12 | -------------------------------------------------------------------------------- /afterPack.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const cp = require('child_process') 4 | const fs = require('fs') 5 | const outdent = require('outdent') 6 | const path = require('path') 7 | 8 | exports.default = function(context) { 9 | if (context.packager.platform.name !== 'linux') { 10 | return 11 | } 12 | const scriptPath = path.join(context.appOutDir, context.packager.executableName) 13 | const binPath = scriptPath + '.bin' 14 | cp.execFileSync('mv', [scriptPath, binPath]) 15 | fs.writeFileSync( 16 | scriptPath, 17 | outdent({trimTrailingNewline: false})` 18 | #!/bin/bash 19 | 20 | # Resolve symlinks. Warning, readlink -f doesn't work on MacOS/BSD 21 | script_dir="$(dirname "$(readlink -f "\${BASH_SOURCE[0]}")")" 22 | 23 | if [[ $EUID -ne 0 ]] || [[ $ELECTRON_RUN_AS_NODE ]]; then 24 | "\${script_dir}"/${context.packager.executableName}.bin "$@" 25 | else 26 | "\${script_dir}"/${context.packager.executableName}.bin "$@" --no-sandbox 27 | fi 28 | ` 29 | ) 30 | cp.execFileSync('chmod', ['+x', scriptPath]) 31 | } 32 | -------------------------------------------------------------------------------- /afterSignHook.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { notarize } = require('electron-notarize') 4 | const { ELECTRON_SKIP_NOTARIZATION } = process.env 5 | 6 | async function main(context) { 7 | const { electronPlatformName, appOutDir } = context 8 | if (electronPlatformName !== 'darwin' || ELECTRON_SKIP_NOTARIZATION === 'true') { 9 | return 10 | } 11 | 12 | const appName = context.packager.appInfo.productFilename 13 | const appleId = process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io' 14 | const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD 15 | 16 | // https://github.com/electron/notarize/blob/main/README.md 17 | await notarize({ 18 | appBundleId: 'com.alex313031.etcher-ng', 19 | appPath: `${appOutDir}/${appName}.app`, 20 | appleId, 21 | appleIdPassword 22 | }) 23 | } 24 | 25 | exports.default = main 26 | -------------------------------------------------------------------------------- /assets/dmg/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/dmg/background.png -------------------------------------------------------------------------------- /assets/dmg/background.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/dmg/background.tiff -------------------------------------------------------------------------------- /assets/dmg/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/dmg/background@2x.png -------------------------------------------------------------------------------- /assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | com.apple.security.device.usb 12 | 13 | com.apple.security.files.user-selected.read-only 14 | 15 | com.apple.security.network.client 16 | 17 | com.apple.security.cs.disable-library-validation 18 | 19 | com.apple.security.get-task-allow 20 | 21 | com.apple.security.cs.disable-executable-page-protection 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/icon.ico -------------------------------------------------------------------------------- /assets/iconset/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/1024x1024.png -------------------------------------------------------------------------------- /assets/iconset/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/128x128.png -------------------------------------------------------------------------------- /assets/iconset/144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/144x144.png -------------------------------------------------------------------------------- /assets/iconset/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/16x16.png -------------------------------------------------------------------------------- /assets/iconset/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/24x24.png -------------------------------------------------------------------------------- /assets/iconset/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/256x256.png -------------------------------------------------------------------------------- /assets/iconset/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/32x32.png -------------------------------------------------------------------------------- /assets/iconset/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/48x48.png -------------------------------------------------------------------------------- /assets/iconset/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/512x512.png -------------------------------------------------------------------------------- /assets/iconset/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/64x64.png -------------------------------------------------------------------------------- /assets/iconset/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/iconset/96x96.png -------------------------------------------------------------------------------- /assets/install-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/assets/install-spinner.gif -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | owner: Alex313031 2 | repo: etcher-ng-win7 3 | provider: github 4 | updaterCacheDirName: etcher-ng-updater 5 | -------------------------------------------------------------------------------- /dictionary.txt: -------------------------------------------------------------------------------- 1 | boolen->boolean 2 | aknowledge->acknowledge 3 | seleted->selected 4 | reming->remind 5 | locl->local 6 | subsribe->subscribe 7 | unsubsribe->unsubscribe 8 | calcluate->calculate 9 | dictionaty->dictionary 10 | -------------------------------------------------------------------------------- /docs/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | Etcher Architecture 2 | =================== 3 | 4 | This document aims to serve as a high-level overview of how Etcher works, 5 | specially oriented for contributors who want to understand the big picture. 6 | 7 | Technologies 8 | ------------ 9 | 10 | This is a non exhaustive list of the major frameworks, libraries, and other 11 | technologies used in Etcher that you should become familiar with: 12 | 13 | - [Electron][electron] 14 | - [NodeJS][nodejs] 15 | - [Redux][redux] 16 | - [ImmutableJS][immutablejs] 17 | - [Sass][sass] 18 | - [Mocha][mocha] 19 | - [JSDoc][jsdoc] 20 | 21 | Module architecture 22 | ------------------- 23 | 24 | Instead of embedding all the functionality required to create a full-featured 25 | image writer as a monolithic project, we try to hard to follow the ["lego block 26 | approach"][lego-blocks]. 27 | 28 | This has the advantage of allowing other applications to re-use logic we 29 | implemented for Etcher in their own project, even for things we didn't expect, 30 | which leads to users benefitting from what we've built, and we benefitting from 31 | user's bug reports, suggestions, etc, as an indirect way to make Etcher better. 32 | 33 | The fact that low-level details are scattered around many different modules can 34 | make it challenging for a new contributor to wrap their heads around the 35 | project as a whole, and get a clear high level view of how things work or where 36 | to submit their work or bug reports. 37 | 38 | These are the main Etcher components, in a nutshell: 39 | 40 | - [Drivelist](https://github.com/balena-io-modules/drivelist) 41 | 42 | As the name implies, this module's duty is to detect the connected drives 43 | uniformly in all major operating systems, along with valuable metadata, like if 44 | a drive is removable or not, to prevent users from trying to write an image to 45 | a system drive. 46 | 47 | - [Etcher](https://github.com/balena-io/etcher) 48 | 49 | This is the *"main repository"*, from which you're reading this from, which is 50 | basically the front-end and glue for all previously listed projects. 51 | 52 | Summary 53 | ------- 54 | 55 | We always welcome contributions to Etcher as well as our documentation. If you 56 | want to give back, but feel that your knowledge on how Etcher works is not 57 | enough to tackle a bug report or feature request, use that as your advantage, 58 | since fresh eyes could help unveil things that we take for granted, but should 59 | be documented instead! 60 | 61 | [lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328 62 | [exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js 63 | [gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui 64 | [electron]: http://electron.atom.io 65 | [nodejs]: https://nodejs.org 66 | [redux]: http://redux.js.org 67 | [immutablejs]: http://facebook.github.io/immutable-js/ 68 | [sass]: http://sass-lang.com 69 | [mocha]: http://mochajs.org 70 | [jsdoc]: http://usejsdoc.org 71 | -------------------------------------------------------------------------------- /docs/COMMIT-GUIDELINES.md: -------------------------------------------------------------------------------- 1 | Commit Guidelines 2 | ================= 3 | 4 | We enforce certain rules on commits with the following goals in mind: 5 | 6 | - Be able to reliably auto-generate the `CHANGELOG.md` *without* any human 7 | intervention. 8 | - Be able to automatically and correctly increment the semver version number 9 | based on what was done since the last release. 10 | - Be able to get a quick overview of what happened to the project by glancing 11 | over the commit history. 12 | - Be able to automatically reference relevant changes from a dependency 13 | upgrade. 14 | 15 | 16 | Commit structure 17 | ---------------- 18 | 19 | Each commit message needs to specify the semver-type. Which can be `patch|minor|major`. 20 | See the [Semantic Versioning][semver] specification for a more detailed explanation of the meaning of these types. 21 | See balena commit guidelines for more info about the whole commit structure. 22 | 23 | ``` 24 | : 25 | ``` 26 | or 27 | ``` 28 | 29 | 30 |
31 | 32 | Change-Type: 33 | ``` 34 | 35 | The subject should not contain more than 70 characters, including the type and 36 | scope, and the body should be wrapped at 72 characters. 37 | 38 | Tags 39 | ---- 40 | 41 | ### `See: `/`Link: ` 42 | 43 | This tag can be used to reference a resource that is relevant to the commit, 44 | and can be repeated multiple times in the same commit. 45 | 46 | Resource examples include: 47 | 48 | - A link to pull requests. 49 | - A link to a GitHub issue. 50 | - A link to a website providing useful information. 51 | - A commit hash. 52 | 53 | Its recommended that you avoid relative URLs, and that you include the whole 54 | commit hash to avoid any potential ambiguity issues in the future. 55 | 56 | If the commit type equals `upgrade`, this tag should be present, and should 57 | link to the CHANGELOG section of the dependency describing the changes 58 | introduced from the previously used version. 59 | 60 | Examples: 61 | 62 | ``` 63 | See: https://github.com/xxx/yyy/ 64 | See: 49d89b4acebd80838303b011d30517cd6229fdbe 65 | Link: https://github.com/xxx/yyy/issues/zzz 66 | ``` 67 | 68 | ### `Closes: `/`Fixes: ` 69 | 70 | This tag is used to make GitHub close the referenced issue automatically when 71 | the commit is merged. 72 | 73 | Its recommended that you provide the absolute URL to the GitHub issue rather 74 | than simply writing the ID prefixed by a hash tag for convenience when browsing 75 | the commit history outside the GitHub web interface. 76 | 77 | A commit can include multiple instances of this tag. 78 | 79 | Examples: 80 | 81 | ``` 82 | Closes: https://github.com/balena-io/etcher/issues/XXX 83 | Fixes: https://github.com/balena-io/etcher/issues/XXX 84 | ``` 85 | 86 | [semver]: http://semver.org 87 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | ## Why is my drive not bootable? 2 | 3 | Etcher copies images to drives byte by byte, without doing any transformation to the final device, which means images that require special treatment to be made bootable, like Windows images, will not work out of the box. In these cases, the general advice is to use software specific to those kind of images, usually available from the image publishers themselves. You can find more information [here](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#why-is-my-drive-not-bootable). 4 | 5 | ## How can I configure persistent storage? 6 | 7 | Some programs, usually oriented at making GNU/Linux live USB drives, include an option to set persistent storage. This is currently not supported by Etcher, so if you require this functionality, we advise to fallback to [UNetbootin](https://unetbootin.github.io/). 8 | 9 | ## How do I flash Ubuntu ISOs 10 | 11 | Ubuntu images (and potentially some other related GNU/Linux distributions) have a peculiar format that allows the image to boot without any further modification from both CDs and USB drives. 12 | A consequence of this enhancement is that some programs, like parted get confused about the drive's format and partition table, printing warnings such as: 13 | 14 | > /dev/xxx contains GPT signatures, indicating that it has a GPT table. However, it does not have a valid fake msdos partition table, as it should. Perhaps it was corrupted -- possibly by a program that doesn't understand GPT partition tables. Or perhaps you deleted the GPT table, and are now using an msdos partition table. Is this a GPT partition table? Both the primary and backup GPT tables are corrupt. Try making a fresh table, and using Parted's rescue feature to recover partitions. 15 | 16 | > Warning: The driver descriptor says the physical block size is 2048 bytes, but Linux says it is 512 bytes. 17 | 18 | All these warnings are safe to ignore, and your drive should be able to boot without any problems. 19 | Refer to [the following message from Ubuntu's mailing list](https://lists.ubuntu.com/archives/ubuntu-devel/2011-June/033495.html) if you want to learn more. 20 | 21 | ## How do I run Etcher on Wayland? 22 | 23 | The XWayland Server provides backwards compatibility to run any X client on Wayland, including Etcher. 24 | This usually works out of the box on mainstream GNU/Linux distributions that properly support Wayland. If it doesn't, make sure the xwayland.so module is being loaded by declaring it in your [weston.ini](http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html): 25 | 26 | ``` 27 | [core] 28 | modules=xwayland.so 29 | ``` 30 | 31 | ## What are the runtime GNU/LINUX dependencies? 32 | 33 | [This entry](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#runtime-gnulinux-dependencies) aims to provide an up to date list of runtime dependencies needed to run Etcher on a GNU/Linux system. 34 | 35 | ## How can I recover the broken drive? 36 | 37 | Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state. 38 | To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems. 39 | 40 | ## I receive "No polkit authentication agent found" error in GNU/Linux 41 | 42 | Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice. 43 | 44 | ## May I run Etcher in older macOS versions? 45 | 46 | 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). 47 | 48 | ## Can I use the Flash With Etcher button on my site? 49 | 50 | 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: 51 | 52 | `` -------------------------------------------------------------------------------- /docs/MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Maintaining Etcher 2 | ================== 3 | 4 | This document is meant to serve as a guide for maintainers to perform common tasks. 5 | 6 | Releasing 7 | --------- 8 | 9 | ### Release Types 10 | 11 | - **draft**: A continues snapshot of current master, made by the CI services 12 | - **pre-release** (default): A continues snapshot of current master, made by the CI services 13 | - **release**: Full releases 14 | 15 | Draft release is created from each PR, tagged with the branch name. 16 | All merged PR will generate a new tag/version as a *pre-release*. 17 | Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary. 18 | 19 | 20 | #### Preparation 21 | 22 | - [Prepare the new version](#preparing-a-new-version) 23 | - [Generate build artifacts](#generating-binaries) (binaries, archives, etc.) 24 | - [Draft a release on GitHub](https://github.com/balena-io/etcher/releases) 25 | - Upload build artifacts to GitHub release draft 26 | 27 | #### Testing 28 | 29 | - Test the prepared release and build artifacts properly on **all supported operating systems** to prevent regressions that went uncaught by the CI tests (see [MANUAL-TESTING.md](MANUAL-TESTING.md)) 30 | - If regressions or other issues arise, create issues on the repository for each one, and decide whether to fix them in this release (meaning repeating the process up until this point), or to follow up with a patch release 31 | 32 | #### Publishing 33 | 34 | - [Publish release draft on GitHub](https://github.com/balena-io/etcher/releases) 35 | - [Post release note to forums](https://forums.balena.io/c/etcher) 36 | - [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec) 37 | - [Update the website](https://github.com/balena-io/etcher-homepage) 38 | - Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions 39 | - If regressions arise; pull the release, and release a patched version, else: 40 | - [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront) 41 | - Post changelog with `#release-notes` tag on internal chat 42 | - If this release packs noteworthy major changes: 43 | - Write a blog post about it, and / or 44 | - Write about it to the Etcher mailing list 45 | 46 | ### Generating binaries 47 | 48 | **Environment** 49 | 50 | Make sure to set the analytics tokens when generating production release binaries: 51 | 52 | ```bash 53 | export ANALYTICS_SENTRY_TOKEN="xxxxxx" 54 | export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx" 55 | ``` 56 | 57 | #### Linux 58 | 59 | ##### Clean dist folder 60 | 61 | **NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`) 62 | 63 | ##### Generating artifacts 64 | 65 | The artifacts are generated by the CI and published as draft-release or pre-release. 66 | `electron-builder` is used to create the packaged application. 67 | 68 | #### Mac OS 69 | 70 | **ATTENTION:** For production releases you'll need the code-signing key, 71 | and set `CSC_NAME` to generate signed binaries on Mac OS. 72 | 73 | #### Windows 74 | 75 | **ATTENTION:** For production releases you'll need the code-signing key, 76 | and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows. 77 | 78 | **NOTE:** 79 | - Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`. 80 | 81 | 82 | ### Uploading packages to Cloudfront 83 | 84 | Log in to cloudfront and upload the `rpm` and `deb` files. 85 | 86 | ### Dealing with a Problematic Release 87 | 88 | There can be times where a release is accidentally plagued with bugs. If you 89 | released a new version and notice the error rates are higher than normal, then 90 | revert the problematic release as soon as possible, until the bugs are fixed. 91 | 92 | You can revert a version by deleting its builds from the S3 bucket and Bintray. 93 | Refer to the `Makefile` for the up to date information about the S3 bucket 94 | where we push builds to, and get in touch with the balena.io operations team to 95 | get write access to it. 96 | 97 | The Etcher update notifier dialog and the website only show the a certain 98 | version if all the expected files have been uploaded to it, so deleting a 99 | single package or two is enough to bring down the whole version. 100 | 101 | Use the following command to delete files from S3: 102 | 103 | ```bash 104 | aws s3api delete-object --bucket --key 105 | ``` 106 | 107 | The Bintray dashboard provides an easy way to delete a version's files. 108 | 109 | 110 | ### Submitting binaries to Symantec 111 | 112 | - [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/) 113 | - Fill out form: 114 | - **Select Submission Type:** "Provide a direct download URL" 115 | - **Name of the software being detected:** Etcher 116 | - **Name of detection given by Symantec product:** WS.Reputation.1 117 | - **Contact name:** Balena.io Ltd 118 | - **E-mail address:** hello@etcher.io 119 | - **Are you the creator or distributor of the software in question?** Yes 120 | -------------------------------------------------------------------------------- /docs/MANUAL-TESTING.md: -------------------------------------------------------------------------------- 1 | Manual Testing 2 | ============== 3 | 4 | This document describes a high-level script of manual tests to check for. We 5 | should aim to replace items on this list with automated Spectron test cases. 6 | 7 | Image Selection 8 | --------------- 9 | 10 | - [ ] Cancel image selection dialog 11 | - [ ] Select an unbootable image (without a partition table), and expect a 12 | sensible warning 13 | - [ ] Attempt to select a ZIP archive with more than one image 14 | - [ ] Attempt to select a tar archive (with any compression method) 15 | - [ ] Change image selection 16 | - [ ] Select a Windows image, and expect a sensible warning 17 | 18 | Drive Selection 19 | --------------- 20 | 21 | - [ ] Open the drive selection modal 22 | - [ ] Switch drive selection 23 | - [ ] Insert a single drive, and expect auto-selection 24 | - [ ] Insert more than one drive, and don't expect auto-selection 25 | - [ ] Insert a locked SD Card and expect a warning 26 | - [ ] Insert a too small drive and expect a warning 27 | - [ ] Put an image into a drive and attempt to flash the image to the drive 28 | that contains it 29 | - [ ] Attempt to flash a compressed image (for which we can get the 30 | uncompressed size) into a drive that is big enough to hold the compressed 31 | image, but not big enough to hold the uncompressed version 32 | - [ ] Enable "Unsafe Mode" and attempt to select a system drive 33 | - [ ] Enable "Unsafe Mode", and if there is only one system drive (and no 34 | removable ones), don't expect autoselection 35 | 36 | Image Support 37 | ------------- 38 | 39 | Run the following tests with and without validation enabled: 40 | 41 | - [ ] Flash an uncompressed image 42 | - [ ] Flash a Bzip2 image 43 | - [ ] Flash a XZ image 44 | - [ ] Flash a ZIP image 45 | - [ ] Flash a GZ image 46 | - [ ] Flash a DMG image 47 | - [ ] Flash an image whose size is not a multiple of 512 bytes 48 | - [ ] Flash a compressed image whose size is not a multiple of 512 bytes 49 | - [ ] Flash an archive whose image size is not a multiple of 512 bytes 50 | - [ ] Flash an archive image containing a logo 51 | - [ ] Flash an archive image containing a blockmap file 52 | - [ ] Flash an archive image containing a manifest metadata file 53 | 54 | Flashing Process 55 | ---------------- 56 | 57 | - [ ] Unplug the drive during flash or validation 58 | - [ ] Click "Flash", cancel elevation dialog, and click "Flash" again 59 | - [ ] Start flashing an image, try to close Etcher, cancel the application 60 | close warning dialog, and check that Etcher continues to flash the image 61 | 62 | ### Child Writer 63 | 64 | - [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and 65 | check that the UI reacts appropriately 66 | - [ ] Close the application while flashing using the window manager close icon 67 | - [ ] Close the application while flashing using the OS keyboard shortcut 68 | - [ ] Close the application from the terminal using Ctrl-C while flashing 69 | - [ ] Force kill the application (using a process monitor tool, etc) 70 | 71 | In all these cases, the child writer process should not remain alive. Note that 72 | in some systems you need to open your process monitor tool of choice with extra 73 | permissions to see the elevated child writer process. 74 | 75 | GUI 76 | ---- 77 | 78 | - [ ] Close application from the terminal using Ctrl-C while the application is 79 | idle 80 | - [ ] Click footer links that take you to an external website 81 | - [ ] Attempt to change image or drive selection while flashing 82 | - [ ] Go to the settings page while flashing and come back 83 | - [ ] Flash consecutive images without closing the application 84 | - [ ] Remove the selected drive right before clicking "Flash" 85 | - [ ] Minimize the application 86 | - [ ] Start the application given no internet connection 87 | 88 | Success Banner 89 | -------------- 90 | 91 | - [ ] Click an external link on the success banner (with and without internet 92 | connection) 93 | 94 | Elevation Prompt 95 | ---------------- 96 | 97 | - [ ] Flash an image as `root`/administrator 98 | - [ ] Reject elevation prompt 99 | - [ ] Put incorrect elevation prompt password 100 | - [ ] Unplug the drive during elevation 101 | 102 | Unmounting 103 | ---------- 104 | 105 | - [ ] Disable unmounting and flash an image 106 | - [ ] Flash an image with a file system that is readable by the host OS, and 107 | check that is unmounted correctly 108 | 109 | Analytics 110 | --------- 111 | 112 | - [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and 113 | check that no request is sent 114 | - [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or 115 | F5), and check that initial events are not sent to Amplitude** 116 | -------------------------------------------------------------------------------- /docs/PUBLISHING.md: -------------------------------------------------------------------------------- 1 | Publishing Etcher 2 | ================= 3 | 4 | This is a small guide to package and publish Etcher to all supported operating 5 | systems. 6 | 7 | Release Types 8 | ------------- 9 | 10 | Etcher supports **pre-release** and **final** release types as does Github. Each is 11 | published to Github releases. 12 | The release version is generated automatically from the commit messasges. 13 | 14 | Signing 15 | ------- 16 | 17 | ### OS X 18 | 19 | 1. Get our Apple Developer ID certificate for signing applications distributed 20 | outside the Mac App Store from the balena.io Apple account. 21 | 22 | 2. Install the Developer ID certificate to your Mac's Keychain by double 23 | clicking on the certificate file. 24 | 25 | The application will be signed automatically using this certificate when 26 | packaging for OS X. 27 | 28 | ### Windows 29 | 30 | 1. Get access to our code signing certificate and decryption key as a balena.io 31 | employee by asking for it from the relevant people. 32 | 33 | 2. Place the certificate in the root of the Etcher repository naming it 34 | `certificate.p12`. 35 | 36 | Packaging 37 | --------- 38 | 39 | The resulting installers will be saved to `dist/out`. 40 | 41 | Run the following commands on all platforms with the right arguments: 42 | 43 | ```sh 44 | ./node_modules/electron-builder build <...> 45 | ``` 46 | 47 | 48 | Publishing to Cloudfront 49 | --------------------- 50 | 51 | We publish GNU/Linux Debian packages to [Cloudfront][cloudfront]. 52 | 53 | Log in to cloudfront and upload the `rpm` and `deb` files. 54 | 55 | Publishing to Homebrew Cask 56 | --------------------------- 57 | 58 | 1. Update [`Casks/etcher.rb`][etcher-cask-file] with the new version and 59 | `sha256` 60 | 61 | 2. Send a PR with the changes above to 62 | [`caskroom/homebrew-cask`][homebrew-cask] 63 | 64 | Announcing 65 | ---------- 66 | 67 | Post messages to the [Etcher forum][balena-forum-etcher] announcing the new version 68 | of Etcher, and including the relevant section of the Changelog. 69 | 70 | [aws-cli]: https://aws.amazon.com/cli 71 | [cloudfront]: https://cloudfront.com 72 | [etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb 73 | [homebrew-cask]: https://github.com/caskroom/homebrew-cask 74 | [balena-forum-etcher]: https://forums.balena.io/c/etcher 75 | [github-releases]: https://github.com/balena-io/etcher/releases 76 | 77 | Updating EFP / Success-Banner 78 | ----------------------------- 79 | 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) 80 | -------------------------------------------------------------------------------- /docs/SUPPORT.md: -------------------------------------------------------------------------------- 1 | Getting help with BalenaEtcher 2 | =============================== 3 | 4 | There are various ways to get support for Etcher if you experience an issue or 5 | have an idea you'd like to share with us. 6 | 7 | Documentation 8 | ------ 9 | 10 | We have answers to a variety of frequently asked questions in the [user 11 | documentation][documentation] and also in the [FAQs][faq] on the Etcher website. 12 | 13 | 14 | Forums 15 | ------ 16 | 17 | We have a [Discourse forum][discourse] which is open to everyone, so please 18 | come join us :). Drop us a line there and the balena.io staff and community 19 | users will be happy to assist. Your question might already be answered, so take 20 | a look at the existing threads before opening a new one! 21 | 22 | Make sure to mention the following information to help us provide better 23 | support: 24 | 25 | - The BalenaEtcher version you're running. 26 | 27 | - The operating system you're running Etcher in. 28 | 29 | - Relevant logging output, if any, from DevTools, which you can open by 30 | pressing `Ctrl+Shift+I` or `Cmd+Alt+I` depending on your platform. 31 | 32 | GitHub 33 | ------ 34 | 35 | If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue 36 | tracker][issues] and if there isn't a ticket covering it, [create 37 | one][new-issue]. 38 | 39 | [discourse]: https://forums.balena.io/c/etcher 40 | [issues]: https://github.com/balena-io/etcher/issues 41 | [new-issue]: https://github.com/balena-io/etcher/issues/new 42 | [documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md 43 | [faq]: https://etcher.io 44 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "electronVersion": "22.3.27", 3 | "asar": false, 4 | "npmRebuild": true, 5 | "nodeGypRebuild": false, 6 | "afterPack": "./afterPack.js", 7 | "afterSign": "./afterSignHook.js", 8 | "productName": "Etcher-ng", 9 | "appId": "com.alex313031.etcher-ng", 10 | "copyright": "Copyright © 2024 Alex313031", 11 | "generateUpdatesFilesForAllChannels": false, 12 | "directories": { 13 | "app": ".", 14 | "buildResources": "./assets", 15 | "output": "dist" 16 | }, 17 | "files": [ 18 | "./generated", 19 | "./lib/shared/catalina-sudo/sudo-askpass.osascript.js" 20 | ], 21 | "linux": { 22 | "target": [ 23 | "zip", 24 | "deb", 25 | "rpm", 26 | "appimage" 27 | ], 28 | "icon": "./assets/iconset", 29 | "maintainer": "Alex313031", 30 | "vendor": "Alex313031", 31 | "synopsis": "Etcher-ng 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.", 32 | "category": "Utility;", 33 | "executableName": "etcher-ng", 34 | "artifactName": "etcher-ng_${version}_${arch}.${ext}", 35 | "desktop": { 36 | "Name": "Etcher-ng", 37 | "StartupWMClass": "etcher-ng", 38 | "Keywords": "etcher;flash;utils;", 39 | "Icon": "etcher-ng" 40 | } 41 | }, 42 | "deb": { 43 | "priority": "optional", 44 | "compression": "bzip2", 45 | "afterInstall": "./after-install.tpl", 46 | "depends": [ 47 | "gconf-service", 48 | "gconf2", 49 | "libasound2", 50 | "libatk1.0-0", 51 | "libc6", 52 | "libcairo2", 53 | "libcups2", 54 | "libdbus-1-3", 55 | "libexpat1", 56 | "libfontconfig1", 57 | "libfreetype6", 58 | "libgbm1", 59 | "libgcc1", 60 | "libgconf-2-4", 61 | "libgdk-pixbuf2.0-0", 62 | "libglib2.0-0", 63 | "libgtk-3-0", 64 | "libkrb5-3", 65 | "liblzma5", 66 | "libnotify4", 67 | "libnspr4", 68 | "libnss3", 69 | "libpango1.0-0 | libpango-1.0-0", 70 | "libstdc++6", 71 | "libx11-6", 72 | "libxcomposite1", 73 | "libxcursor1", 74 | "libxdamage1", 75 | "libxext6", 76 | "libxfixes3", 77 | "libxi6", 78 | "libxrandr2", 79 | "libxrender1", 80 | "libxss1", 81 | "libxtst6", 82 | "polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1" 83 | ] 84 | }, 85 | "appImage": { 86 | "category": "Utility;" 87 | }, 88 | "rpm": { 89 | "afterInstall": "./after-install.tpl", 90 | "depends": [ "util-linux" ] 91 | }, 92 | "win": { 93 | "icon": "./assets/icon.ico", 94 | "target": [ 95 | "zip", 96 | "portable", 97 | "nsis" 98 | ], 99 | "publisherName": "Alex313031", 100 | "executableName": "etcher_ng", 101 | "artifactName": "etcher-ng_${version}_${arch}.${ext}" 102 | }, 103 | "nsis": { 104 | "shortcutName": "Etcher-ng", 105 | "artifactName": "etcher-ng_setup_${version}_${arch}.${ext}", 106 | "uninstallDisplayName": "Etcher-ng ${version}", 107 | "deleteAppDataOnUninstall": true, 108 | "oneClick": true, 109 | "license": "LICENSE.md" 110 | }, 111 | "portable": { 112 | "artifactName": "etcher-ng_portable_${version}_${arch}.${ext}" 113 | }, 114 | "mac": { 115 | "category": "public.app-category.developer-tools", 116 | "entitlements": "assets/entitlements.mac.plist", 117 | "entitlementsInherit": "assets/entitlements.mac.plist", 118 | "icon": "./assets/icon.icns", 119 | "darkModeSupport": true, 120 | "minimumSystemVersion": "10.12", 121 | "artifactName": "etcher-ng_macos_${version}_${arch}.${ext}", 122 | "target": [ 123 | "dmg", 124 | "zip" 125 | ] 126 | }, 127 | "dmg": { 128 | "background": "./assets/dmg/background.tiff", 129 | "icon": "./assets/icon.icns", 130 | "iconSize": "110", 131 | "window": { 132 | "width": 540, 133 | "height": 405 134 | }, 135 | "contents": [ 136 | { "x": 140, "y": 225 }, 137 | { 138 | "x": 415, "y": 225, 139 | "type": "link", 140 | "path": "/Applications" 141 | } 142 | ], 143 | "artifactName": "etcher-ng_macos_${version}_${arch}.${ext}", 144 | }, 145 | "protocols": { 146 | "name": "etcher", 147 | "schemes": [ "etcher" ] 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/icon.png -------------------------------------------------------------------------------- /lib/gui/app/components/drive-status-warning-modal/drive-status-warning-modal.tsx: -------------------------------------------------------------------------------- 1 | import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg'; 2 | import * as _ from 'lodash'; 3 | import * as React from 'react'; 4 | import { Badge, Flex, Txt, ModalProps } from 'rendition'; 5 | import { Modal, ScrollableFlex } from '../../styled-components'; 6 | import { middleEllipsis } from '../../utils/middle-ellipsis'; 7 | 8 | import * as prettyBytes from 'pretty-bytes'; 9 | import { DriveWithWarnings } from '../../pages/main/Flash'; 10 | import * as i18next from 'i18next'; 11 | 12 | const DriveStatusWarningModal = ({ 13 | done, 14 | cancel, 15 | isSystem, 16 | drivesWithWarnings, 17 | }: ModalProps & { 18 | isSystem: boolean; 19 | drivesWithWarnings: DriveWithWarnings[]; 20 | }) => { 21 | let warningSubtitle = i18next.t('drives.largeDriveWarning'); 22 | let warningCta = i18next.t('drives.largeDriveWarningMsg'); 23 | 24 | if (isSystem) { 25 | warningSubtitle = i18next.t('drives.systemDriveWarning'); 26 | warningCta = i18next.t('drives.systemDriveWarningMsg'); 27 | } 28 | return ( 29 | 45 | 51 | 52 | 53 | 54 | {i18next.t('warning')} 55 | 56 | 57 | {warningSubtitle} 58 | 66 | {drivesWithWarnings.map((drive, i, array) => ( 67 | <> 68 | 69 | {middleEllipsis(drive.description, 28)}{' '} 70 | {drive.size && prettyBytes(drive.size) + ' '} 71 | {drive.statuses[0].message} 72 | 73 | {i !== array.length - 1 ?
: null} 74 | 75 | ))} 76 |
77 | {warningCta} 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default DriveStatusWarningModal; 84 | -------------------------------------------------------------------------------- /lib/gui/app/components/finish/finish.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import { Flex } from 'rendition'; 19 | import { v4 as uuidV4 } from 'uuid'; 20 | 21 | import * as flashState from '../../models/flash-state'; 22 | import * as selectionState from '../../models/selection-state'; 23 | import * as settings from '../../models/settings'; 24 | import { Actions, store } from '../../models/store'; 25 | import * as analytics from '../../modules/analytics'; 26 | import { FlashAnother } from '../flash-another/flash-another'; 27 | import { FlashResults, FlashError } from '../flash-results/flash-results'; 28 | import { SafeWebview } from '../safe-webview/safe-webview'; 29 | 30 | function restart(goToMain: () => void) { 31 | selectionState.deselectAllDrives(); 32 | analytics.logEvent('Restart'); 33 | 34 | // Reset the flashing workflow uuid 35 | store.dispatch({ 36 | type: Actions.SET_FLASHING_WORKFLOW_UUID, 37 | data: uuidV4(), 38 | }); 39 | 40 | goToMain(); 41 | } 42 | 43 | async function getSuccessBannerURL() { 44 | return ( 45 | (await settings.get('successBannerURL')) ?? 46 | 'https://thorium.rocks/etcher-ng/win7' 47 | ); 48 | } 49 | 50 | function FinishPage({ goToMain }: { goToMain: () => void }) { 51 | const [webviewShowing, setWebviewShowing] = React.useState(false); 52 | const [successBannerURL, setSuccessBannerURL] = React.useState(''); 53 | (async () => { 54 | setSuccessBannerURL(await getSuccessBannerURL()); 55 | })(); 56 | const flashResults = flashState.getFlashResults(); 57 | const errors: FlashError[] = ( 58 | store.getState().toJS().failedDeviceErrors || [] 59 | ).map(([, error]: [string, FlashError]) => ({ 60 | ...error, 61 | })); 62 | const { averageSpeed, blockmappedSize, bytesWritten, failed, size } = 63 | flashState.getFlashState(); 64 | const { 65 | skip, 66 | results = { 67 | bytesWritten, 68 | sourceMetadata: { 69 | size, 70 | blockmappedSize, 71 | }, 72 | averageFlashingSpeed: averageSpeed, 73 | devices: { failed, successful: 0 }, 74 | }, 75 | } = flashResults; 76 | return ( 77 | 78 | 91 | 99 | 100 | { 102 | restart(goToMain); 103 | }} 104 | /> 105 | 106 | {successBannerURL.length && ( 107 | 119 | )} 120 | 121 | ); 122 | } 123 | 124 | export default FinishPage; 125 | -------------------------------------------------------------------------------- /lib/gui/app/components/flash-another/flash-another.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | 19 | import { BaseButton } from '../../styled-components'; 20 | import * as i18next from 'i18next'; 21 | 22 | export interface FlashAnotherProps { 23 | onClick: () => void; 24 | } 25 | 26 | export const FlashAnother = (props: FlashAnotherProps) => { 27 | return ( 28 | 29 | {i18next.t('flash.another')} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/gui/app/components/progress-button/progress-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import { Flex, Button, ProgressBar, Txt } from 'rendition'; 19 | import { default as styled } from 'styled-components'; 20 | 21 | import { fromFlashState } from '../../modules/progress-status'; 22 | import { StepButton } from '../../styled-components'; 23 | import * as i18next from 'i18next'; 24 | 25 | const FlashProgressBar = styled(ProgressBar)` 26 | > div { 27 | width: 100%; 28 | height: 12px; 29 | color: white !important; 30 | text-shadow: none !important; 31 | transition-duration: 0s; 32 | 33 | > div { 34 | transition-duration: 0s; 35 | } 36 | } 37 | 38 | width: 100%; 39 | height: 12px; 40 | margin-bottom: 6px; 41 | border-radius: 14px; 42 | font-size: 16px; 43 | line-height: 48px; 44 | 45 | background: #2f3033; 46 | `; 47 | 48 | interface ProgressButtonProps { 49 | type: 'decompressing' | 'flashing' | 'verifying'; 50 | active: boolean; 51 | percentage: number; 52 | position: number; 53 | disabled: boolean; 54 | cancel: (type: string) => void; 55 | callback: () => void; 56 | warning?: boolean; 57 | } 58 | 59 | const colors = { 60 | decompressing: '#00aeef', 61 | flashing: '#da60ff', 62 | verifying: '#1ac135', 63 | } as const; 64 | 65 | const CancelButton = styled(({ type, onClick, ...props }) => { 66 | const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel'); 67 | return ( 68 | 71 | ); 72 | })` 73 | font-weight: 600; 74 | 75 | &&& { 76 | width: auto; 77 | height: auto; 78 | font-size: 14px; 79 | } 80 | `; 81 | 82 | export class ProgressButton extends React.PureComponent { 83 | public render() { 84 | const percentage = this.props.percentage; 85 | const warning = this.props.warning; 86 | const { status, position } = fromFlashState({ 87 | type: this.props.type, 88 | percentage, 89 | position: this.props.position, 90 | }); 91 | const type = this.props.type || 'default'; 92 | if (this.props.active) { 93 | return ( 94 | <> 95 | 106 | 107 | {status}  108 | {position} 109 | 110 | {type && ( 111 | 116 | )} 117 | 118 | 119 | 120 | ); 121 | } 122 | return ( 123 | 133 | {i18next.t('flash.flashNow')} 134 | 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import { Flex, Txt } from 'rendition'; 19 | 20 | import DriveSvg from '../../../assets/drive.svg'; 21 | import ImageSvg from '../../../assets/image.svg'; 22 | import { SVGIcon } from '../svg-icon/svg-icon'; 23 | import { middleEllipsis } from '../../utils/middle-ellipsis'; 24 | 25 | interface ReducedFlashingInfosProps { 26 | imageLogo?: string; 27 | imageName?: string; 28 | imageSize: string; 29 | driveTitle: string; 30 | driveLabel: string; 31 | style?: React.CSSProperties; 32 | } 33 | 34 | export class ReducedFlashingInfos extends React.Component { 35 | constructor(props: ReducedFlashingInfosProps) { 36 | super(props); 37 | this.state = {}; 38 | } 39 | 40 | public render() { 41 | const { imageName = '' } = this.props; 42 | return ( 43 | 47 | 48 | 56 | 60 | {middleEllipsis(imageName, 16)} 61 | 62 | {this.props.imageSize} 63 | 64 | 65 | 66 | 67 | 68 | {middleEllipsis(this.props.driveTitle, 16)} 69 | 70 | 71 | 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/gui/app/components/settings/settings.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg'; 18 | import * as _ from 'lodash'; 19 | import * as React from 'react'; 20 | import { Box, Checkbox, Divider, Flex, TextWithCopy, Txt } from 'rendition'; 21 | 22 | import { version, packageType } from '../../../../../package.json'; 23 | import * as settings from '../../models/settings'; 24 | import * as analytics from '../../modules/analytics'; 25 | import { open as openInternalRemote } from '../../os/open-internal-remote/services/open-internal-remote'; 26 | import { open as openExternal } from '../../os/open-external/services/open-external'; 27 | import { Modal } from '../../styled-components'; 28 | import * as i18next from 'i18next'; 29 | import { etcherProInfo } from '../../utils/etcher-pro-specific'; 30 | 31 | interface Setting { 32 | name: string; 33 | label: string | JSX.Element; 34 | } 35 | 36 | async function getSettingsList(): Promise { 37 | const list: Setting[] = [ 38 | { 39 | name: 'verify', 40 | label: i18next.t('settings.verify'), 41 | tooltip: i18next.t('settings.verifyDesc'), 42 | }, 43 | { 44 | name: 'autoBlockmapping', 45 | label: i18next.t('settings.trimExtPartitions'), 46 | tooltip: i18next.t('settings.trimExtPartitions'), 47 | }, 48 | { 49 | name: 'decompressFirst', 50 | label: i18next.t('settings.decompressFirst'), 51 | tooltip: i18next.t('settings.decompressFirst'), 52 | }, 53 | ]; 54 | return list; 55 | } 56 | 57 | interface SettingsModalProps { 58 | toggleModal: (value: boolean) => void; 59 | } 60 | 61 | const EPInfo = etcherProInfo(); 62 | 63 | const InfoBox = (props: any) => ( 64 | 65 | {props.label} 66 | 67 | 68 | ); 69 | 70 | export function SettingsModal({ toggleModal }: SettingsModalProps) { 71 | const [settingsList, setCurrentSettingsList] = React.useState([]); 72 | React.useEffect(() => { 73 | (async () => { 74 | if (settingsList.length === 0) { 75 | setCurrentSettingsList(await getSettingsList()); 76 | } 77 | })(); 78 | }); 79 | const [currentSettings, setCurrentSettings] = React.useState< 80 | _.Dictionary 81 | >({}); 82 | React.useEffect(() => { 83 | (async () => { 84 | if (_.isEmpty(currentSettings)) { 85 | setCurrentSettings(await settings.getAll()); 86 | } 87 | })(); 88 | }); 89 | 90 | const toggleSetting = async (setting: string) => { 91 | const value = currentSettings[setting]; 92 | analytics.logEvent('Toggle setting', { setting, value }); 93 | await settings.set(setting, !value); 94 | setCurrentSettings({ 95 | ...currentSettings, 96 | [setting]: !value, 97 | }); 98 | }; 99 | 100 | return ( 101 | 104 | {i18next.t('settings.settings')} 105 | 106 | } 107 | done={() => toggleModal(false)} 108 | > 109 | 110 | {settingsList.map((setting: Setting, i: number) => { 111 | return ( 112 | 113 | toggleSetting(setting.name)} 120 | /> 121 | 122 | ); 123 | })} 124 | {EPInfo !== undefined && ( 125 | 126 | {i18next.t('settings.systemInformation')} 127 | {EPInfo.get_serial() === undefined ? ( 128 | 129 | ) : ( 130 | 131 | )} 132 | 133 | )} 134 | 135 | 146 | openInternalRemote( 147 | 'https://github.com/Alex313031/etcher-ng-win7/blob/main/CHANGELOG.md', 148 | ) 149 | } 150 | > 151 | 156 | {version} 157 | 158 | 159 | 160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /lib/gui/app/components/svg-icon/svg-icon.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | 19 | const domParser = new window.DOMParser(); 20 | 21 | const DEFAULT_SIZE = '40px'; 22 | 23 | /** 24 | * @summary Try to parse SVG contents and return it data encoded 25 | * 26 | */ 27 | function tryParseSVGContents(contents?: string): string | undefined { 28 | if (contents === undefined) { 29 | return; 30 | } 31 | const doc = domParser.parseFromString(contents, 'image/svg+xml'); 32 | const parserError = doc.querySelector('parsererror'); 33 | const svg = doc.querySelector('svg'); 34 | if (!parserError && svg) { 35 | return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`; 36 | } 37 | } 38 | 39 | interface SVGIconProps { 40 | // Optional string representing the SVG contents to be tried 41 | contents?: string; 42 | // Fallback SVG element to show if `contents` is invalid/undefined 43 | fallback: React.FunctionComponent>; 44 | // SVG image width unit 45 | width?: string; 46 | // SVG image height unit 47 | height?: string; 48 | // Should the element visually appear grayed out and disabled? 49 | disabled?: boolean; 50 | style?: React.CSSProperties; 51 | } 52 | 53 | /** 54 | * @summary SVG element that takes file contents 55 | */ 56 | export class SVGIcon extends React.PureComponent { 57 | public render() { 58 | const svgData = tryParseSVGContents(this.props.contents); 59 | const { width, height, style = {} } = this.props; 60 | style.width = width || DEFAULT_SIZE; 61 | style.height = height || DEFAULT_SIZE; 62 | if (svgData !== undefined) { 63 | return ( 64 | 69 | ); 70 | } 71 | const { fallback: FallbackSVG } = this.props; 72 | return ; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/gui/app/components/target-selector/target-selector-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg'; 18 | import * as React from 'react'; 19 | import { Flex, FlexProps, Txt } from 'rendition'; 20 | 21 | import { 22 | getDriveImageCompatibilityStatuses, 23 | DriveStatus, 24 | } from '../../../../shared/drive-constraints'; 25 | import { compatibility, warning } from '../../../../shared/messages'; 26 | import * as prettyBytes from 'pretty-bytes'; 27 | import { getImage, getSelectedDrives } from '../../models/selection-state'; 28 | import { 29 | ChangeButton, 30 | DetailsText, 31 | StepButton, 32 | StepNameButton, 33 | } from '../../styled-components'; 34 | import { middleEllipsis } from '../../utils/middle-ellipsis'; 35 | import * as i18next from 'i18next'; 36 | 37 | interface TargetSelectorProps { 38 | targets: any[]; 39 | disabled: boolean; 40 | openDriveSelector: () => void; 41 | reselectDrive: () => void; 42 | flashing: boolean; 43 | show: boolean; 44 | tooltip: string; 45 | } 46 | 47 | function getDriveWarning(status: DriveStatus) { 48 | switch (status.message) { 49 | case compatibility.containsImage(): 50 | return warning.sourceDrive(); 51 | case compatibility.largeDrive(): 52 | return warning.largeDriveSize(); 53 | case compatibility.system(): 54 | return warning.systemDrive(); 55 | default: 56 | return ''; 57 | } 58 | } 59 | 60 | const DriveCompatibilityWarning = ({ 61 | warnings, 62 | ...props 63 | }: { 64 | warnings: string[]; 65 | } & FlexProps) => { 66 | const systemDrive = warnings.find( 67 | (message) => message === warning.systemDrive(), 68 | ); 69 | return ( 70 | 71 | 75 | 76 | ); 77 | }; 78 | 79 | export function TargetSelectorButton(props: TargetSelectorProps) { 80 | const targets = getSelectedDrives(); 81 | 82 | if (targets.length === 1) { 83 | const target = targets[0]; 84 | const warnings = getDriveImageCompatibilityStatuses( 85 | target, 86 | getImage(), 87 | true, 88 | ).map(getDriveWarning); 89 | return ( 90 | <> 91 | 92 | {warnings.length > 0 && ( 93 | 94 | )} 95 | {middleEllipsis(target.description, 20)} 96 | 97 | {!props.flashing && ( 98 | 99 | {i18next.t('target.change')} 100 | 101 | )} 102 | {target.size != null && ( 103 | {prettyBytes(target.size)} 104 | )} 105 | 106 | ); 107 | } 108 | 109 | if (targets.length > 1) { 110 | const targetsTemplate = []; 111 | for (const target of targets) { 112 | const warnings = getDriveImageCompatibilityStatuses( 113 | target, 114 | getImage(), 115 | true, 116 | ).map(getDriveWarning); 117 | targetsTemplate.push( 118 | 125 | {warnings.length > 0 ? ( 126 | 127 | ) : null} 128 | {middleEllipsis(target.description, 14)} 129 | {target.size != null && {prettyBytes(target.size)}} 130 | , 131 | ); 132 | } 133 | return ( 134 | <> 135 | 136 | {targets.length} {i18next.t('target.targets')} 137 | 138 | {!props.flashing && ( 139 | 140 | {i18next.t('target.change')} 141 | 142 | )} 143 | {targetsTemplate} 144 | 145 | ); 146 | } 147 | 148 | return ( 149 | 0 ? -1 : 2} 152 | disabled={props.disabled} 153 | tooltip="Choose target drive(s)" 154 | onClick={props.openDriveSelector} 155 | > 156 | {i18next.t('target.selectTarget')} 157 | 158 | ); 159 | } 160 | -------------------------------------------------------------------------------- /lib/gui/app/css/fonts/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/lib/gui/app/css/fonts/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /lib/gui/app/css/fonts/SourceSansPro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/lib/gui/app/css/fonts/SourceSansPro-SemiBold.ttf -------------------------------------------------------------------------------- /lib/gui/app/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @font-face { 18 | font-family: "SourceSansPro"; 19 | src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype"); 20 | font-weight: 500; 21 | font-style: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: "SourceSansPro"; 26 | src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype"); 27 | font-weight: 600; 28 | font-style: normal; 29 | } 30 | 31 | html, 32 | body { 33 | margin: 0; 34 | overflow: hidden; 35 | 36 | /* Prevent white flash when running application */ 37 | background-color: #4d5057; 38 | 39 | /* Prevent WebView bounce effect in OS X */ 40 | height: 100%; 41 | width: 100%; 42 | } 43 | 44 | /* Prevent text selection */ 45 | body { 46 | -webkit-user-select: none; 47 | -webkit-overflow-scrolling: touch; 48 | } 49 | 50 | /* Prevent blue outline */ 51 | a:focus, 52 | input:focus, 53 | button:focus, 54 | [tabindex]:focus, 55 | input[type="checkbox"] + div { 56 | outline: none !important; 57 | box-shadow: none !important; 58 | } 59 | 60 | .disabled { 61 | opacity: 0.4; 62 | } 63 | 64 | #rendition-tooltip-root > div { 65 | font-family: "SourceSansPro", sans-serif; 66 | } 67 | -------------------------------------------------------------------------------- /lib/gui/app/i18n.ts: -------------------------------------------------------------------------------- 1 | import * as i18next from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import zh_CN_translation from './i18n/zh-CN'; 4 | import zh_TW_translation from './i18n/zh-TW'; 5 | import en_translation from './i18n/en'; 6 | 7 | export function langParser() { 8 | if (process.env.LANG !== undefined) { 9 | // Bypass mocha, where lang-detect don't works 10 | return 'en'; 11 | } 12 | 13 | const lang = Intl.DateTimeFormat().resolvedOptions().locale; 14 | 15 | switch (lang.substr(0, 2)) { 16 | case 'zh': 17 | if (lang === 'zh-CN' || lang === 'zh-SG') { 18 | return 'zh-CN'; 19 | } // Simplified Chinese 20 | else { 21 | return 'zh-TW'; 22 | } // Traditional Chinese 23 | default: 24 | return lang.substr(0, 2); 25 | } 26 | } 27 | 28 | i18next.use(initReactI18next).init({ 29 | lng: langParser(), 30 | fallbackLng: 'en', 31 | nonExplicitSupportedLngs: true, 32 | interpolation: { 33 | escapeValue: false, 34 | }, 35 | resources: { 36 | 'zh-CN': zh_CN_translation, 37 | 'zh-TW': zh_TW_translation, 38 | en: en_translation, 39 | }, 40 | }); 41 | 42 | export const supportedLocales = ['en', 'zh']; 43 | 44 | export default i18next; 45 | -------------------------------------------------------------------------------- /lib/gui/app/i18n/README.md: -------------------------------------------------------------------------------- 1 | # i18n 2 | 3 | ## How it was done 4 | 5 | Using the open-source lib [i18next](https://www.i18next.com/). 6 | 7 | ## How to add your own language 8 | 9 | 1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned 10 | in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES` 11 | and `pt-BR`) 12 | . 13 | 2. Copy the content from an existing translation and start to translate. 14 | 3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the 15 | already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function. 16 | 4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to 17 | be `sudo-askpass.osascript-xx.js` and edit 18 | the `'Etcher-ng needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and 19 | those `Ok`s and `Cancel`s to your own language. 20 | 5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW` 21 | , or `pt-BR` and `pt-PT`, edit 22 | the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need. 23 | 6. Make a commit, and then a pull request on GitHub. 24 | -------------------------------------------------------------------------------- /lib/gui/app/i18n/zh-CN.ts: -------------------------------------------------------------------------------- 1 | const translation = { 2 | translation: { 3 | ok: '好', 4 | cancel: '取消', 5 | continue: '继续', 6 | skip: '跳过', 7 | sure: '我确定', 8 | warning: '请注意!', 9 | attention: '请注意', 10 | failed: '失败', 11 | completed: '完毕', 12 | yesExit: '是的,可以退出', 13 | reallyExit: '真的要现在退出 Etcher-ng 吗?', 14 | yesContinue: '是的,继续', 15 | progress: { 16 | starting: '正在启动……', 17 | decompressing: '正在解压……', 18 | flashing: '正在烧录……', 19 | finishing: '正在结束……', 20 | verifying: '正在验证……', 21 | failing: '失败……', 22 | }, 23 | message: { 24 | sizeNotRecommended: '大小不推荐', 25 | tooSmall: '空间太小', 26 | locked: '被锁定', 27 | system: '系统盘', 28 | containsImage: '存放源镜像', 29 | largeDrive: '很大的磁盘', 30 | sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。', 31 | flashSucceed_one: '烧录成功', 32 | flashSucceed_other: '烧录成功', 33 | flashFail_one: '烧录失败', 34 | flashFail_other: '烧录失败', 35 | toDrive: '到 {{description}} ({{name}})', 36 | toTarget_one: '到 {{num}} 个目标', 37 | toTarget_other: '到 {{num}} 个目标', 38 | andFailTarget_one: '并烧录失败了 {{num}} 个目标', 39 | andFailTarget_other: '并烧录失败了 {{num}} 个目标', 40 | succeedTo: '{{name}} 被成功烧录 {{target}}', 41 | exitWhileFlashing: 42 | '您当前正在刷机。 关闭 Etcher-ng 可能会导致您的磁盘无法使用。', 43 | looksLikeWindowsImage: 44 | '看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同,Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 Rufus (Windows)、WoeUSB (Linux) 或 Boot Camp 助理 (macOS)。', 45 | image: '镜像', 46 | drive: '磁盘', 47 | missingPartitionTable: 48 | '看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。', 49 | largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息', 50 | systemDrive: '选择系统盘很危险,因为这将会删除你的系统', 51 | sourceDrive: '源镜像位于这个分区中', 52 | noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。', 53 | genericFlashError: 54 | '出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}', 55 | validation: 56 | '写入已成功完成,但 Etcher-ng 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。', 57 | openError: '打开 {{source}} 时出错。\n\n错误信息: {{error}}', 58 | flashError: '烧录 {{image}} {{targets}} 失败。', 59 | unplug: 60 | '看起来 Etcher-ng 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。', 61 | cannotWrite: 62 | '看起来 Etcher-ng 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。', 63 | childWriterDied: 64 | '写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher-ng 团队。', 65 | badProtocol: '仅支持 http:// 和 https:// 开头的网址。', 66 | }, 67 | target: { 68 | selectTarget: '选择目标磁盘', 69 | plugTarget: '请插入目标磁盘', 70 | targets: '个目标', 71 | change: '更改', 72 | }, 73 | menu: { 74 | edit: '编辑', 75 | view: '视图', 76 | devTool: '打开开发者工具', 77 | window: '窗口', 78 | help: '帮助', 79 | pro: 'Etcher 专业版', 80 | website: 'Etcher 的官网', 81 | issue: '提交一个 Issue', 82 | devmenu: '开发商', 83 | electrondevtools: '打开 Electron 开发工具', 84 | testwindow: '打开测试窗口', 85 | config: '编辑配置文件', 86 | restart: '重新启动应用程序', 87 | gpu: '打开 chrome://gpu', 88 | procinternals: '打开 chrome://process-internals', 89 | goback: '回去', 90 | goforward: '前进', 91 | about: '关于 Etcher-ng', 92 | hide: '隐藏 Etcher-ng', 93 | hideOthers: '隐藏其它窗口', 94 | unhide: '取消隐藏', 95 | quit: '退出 Etcher-ng', 96 | }, 97 | source: { 98 | useSourceURL: '使用镜像网络地址', 99 | auth: '验证', 100 | username: '输入用户名', 101 | password: '输入密码', 102 | unsupportedProtocol: '不支持的协议', 103 | windowsImage: '这可能是 Windows 系统镜像', 104 | partitionTable: '找不到分区表', 105 | errorOpen: '打开源镜像时出错', 106 | fromFile: '从文件烧录', 107 | fromURL: '从在线地址烧录', 108 | clone: '克隆磁盘', 109 | image: '镜像信息', 110 | name: '名称:', 111 | path: '路径:', 112 | selectSource: '选择源', 113 | plugSource: '请插入源磁盘', 114 | osImages: '系统镜像格式', 115 | allFiles: '任何文件格式', 116 | enterValidURL: '请输入一个正确的地址', 117 | }, 118 | drives: { 119 | name: '名称', 120 | size: '大小', 121 | location: '位置', 122 | find: '找到 {{length}} 个', 123 | select: '选定 {{select}}', 124 | showHidden: '显示 {{num}} 个隐藏的磁盘', 125 | systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!', 126 | openInBrowser: 'Etcher-ng 会在浏览器中打开 {{link}}', 127 | changeTarget: '改变目标', 128 | largeDriveWarning: '您即将擦除一个非常大的磁盘', 129 | largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?', 130 | systemDriveWarning: '您将要擦除系统盘', 131 | systemDriveWarningMsg: '您确定要烧录到系统盘吗?', 132 | }, 133 | flash: { 134 | another: '烧录另一目标', 135 | target: '目标', 136 | location: '位置', 137 | error: '错误', 138 | flash: '烧录', 139 | flashNow: '现在烧录!', 140 | skip: '跳过了验证', 141 | moreInfo: '更多信息', 142 | speedTip: 143 | '通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分,因此具有EXT分区的磁盘镜像烧录速度更快。', 144 | speed: '速度:{{speed}} MB/秒', 145 | speedShort: '{{speed}} MB/秒', 146 | eta: '预计还需要:{{eta}}', 147 | failedTarget: '失败的烧录目标', 148 | failedRetry: '重试烧录失败目标', 149 | flashFailed: '烧录失败。', 150 | flashCompleted: '烧录成功!', 151 | }, 152 | settings: { 153 | errorReporting: '匿名地向 balena.io 报告运行错误和使用统计', 154 | verify: '刷机后自动验证', 155 | verifyDesc: '验证驱动器写入是否正确', 156 | autoUpdate: '自动更新', 157 | settings: '软件设置', 158 | systemInformation: '系统信息', 159 | trimExtPartitions: '修剪原始图像上未分配的空间(在 ext 类型分区中)', 160 | decompressFirst: '在刷新之前解压缩压缩图像(即 .tar.xz)', 161 | }, 162 | }, 163 | }; 164 | 165 | export default translation; 166 | -------------------------------------------------------------------------------- /lib/gui/app/i18n/zh-TW.ts: -------------------------------------------------------------------------------- 1 | const translation = { 2 | translation: { 3 | continue: '繼續', 4 | ok: '好', 5 | cancel: '取消', 6 | skip: '跳過', 7 | sure: '我確定', 8 | warning: '請注意!', 9 | attention: '請注意', 10 | failed: '失敗', 11 | completed: '完成', 12 | yesContinue: '是的,繼續', 13 | reallyExit: '真的要現在結束 Etcher-ng 嗎?', 14 | yesExit: '是的,可以結束', 15 | progress: { 16 | starting: '正在啟動……', 17 | decompressing: '正在解壓縮……', 18 | flashing: '正在燒錄……', 19 | finishing: '正在結束……', 20 | verifying: '正在驗證……', 21 | failing: '失敗……', 22 | }, 23 | message: { 24 | sizeNotRecommended: '大小不建議', 25 | tooSmall: '空間太小', 26 | locked: '被鎖定', 27 | system: '系統', 28 | containsImage: '存放來源映像檔', 29 | largeDrive: '很大的磁碟', 30 | sourceLarger: '所選的映像檔比目標磁碟大了 {{byte}} 位元組。', 31 | flashSucceed_one: '燒錄成功', 32 | flashSucceed_other: '燒錄成功', 33 | flashFail_one: '燒錄失敗', 34 | flashFail_other: '燒錄失敗', 35 | toDrive: '到 {{description}} ({{name}})', 36 | toTarget_one: '到 {{num}} 個目標', 37 | toTarget_other: '到 {{num}} 個目標', 38 | andFailTarget_one: '並燒錄失敗了 {{num}} 個目標', 39 | andFailTarget_other: '並燒錄失敗了 {{num}} 個目標', 40 | succeedTo: '{{name}} 被成功燒錄 {{target}}', 41 | exitWhileFlashing: 42 | '您目前正在刷寫。關閉 Etcher-ng 可能會導致您的磁碟無法使用。', 43 | looksLikeWindowsImage: 44 | '看起來您正在嘗試燒錄 Windows 映像檔。\n\n與其他映像檔不同,Windows 映像檔需要特殊處理才能使其可啟動。我們建議您使用專門為此目的設計的工具,例如 Rufus (Windows)、WoeUSB (Linux) 或 Boot Camp 助理 (macOS)。', 45 | image: '映像檔', 46 | drive: '磁碟', 47 | missingPartitionTable: 48 | '看起來這不是一個可啟動的{{type}}。\n\n這個{{type}}似乎不包含分割表,因此您的設備可能無法識別或無法正確啟動。', 49 | largeDriveSize: 50 | '這是個很大容量的磁碟!請檢查並確認它不包含對您來說存放很重要的資料', 51 | systemDrive: '選擇系統分割區很危險,因為這將會刪除你的系統', 52 | sourceDrive: '來源映像檔位於這個分割區中', 53 | noSpace: '磁碟空間不足。請插入另一個較大的磁碟並重試。', 54 | genericFlashError: 55 | '出了點問題。如果來源映像檔曾被壓縮過,請檢查它是否已損壞。\n{{error}}', 56 | validation: 57 | '寫入已成功完成,但 Etcher-ng 在從磁碟讀取映像檔時檢測到潛在的損壞問題。\n\n請考慮將映像檔寫入其他磁碟。', 58 | openError: '打開 {{source}} 時發生錯誤。\n\n錯誤訊息: {{error}}', 59 | flashError: '燒錄 {{image}} {{targets}} 失敗。', 60 | unplug: 61 | '看起來 Etcher-ng 失去了對磁碟的連接。是不是被意外拔掉了?\n\n有時這個錯誤是因為讀卡器出了故障。', 62 | cannotWrite: 63 | '看起來 Etcher-ng 無法寫入磁碟的這個位置。此錯誤通常是由故障的磁碟、讀取器或連接埠引起的。\n\n請使用其他磁碟、讀卡器或連接埠重試。', 64 | childWriterDied: 65 | '寫入處理程序意外崩潰。請再試一次,如果問題仍然存在,請聯絡 Etcher-ng 團隊。', 66 | badProtocol: '僅支援 http:// 和 https:// 開頭的網址。', 67 | }, 68 | target: { 69 | selectTarget: '選擇目標磁碟', 70 | plugTarget: '請插入目標磁碟', 71 | targets: '個目標', 72 | change: '更改', 73 | }, 74 | source: { 75 | useSourceURL: '使用映像檔網址', 76 | auth: '驗證', 77 | username: '輸入使用者名稱', 78 | password: '輸入密碼', 79 | unsupportedProtocol: '不支持的通訊協定', 80 | windowsImage: '這可能是 Windows 系統映像檔', 81 | partitionTable: '找不到分割表', 82 | errorOpen: '打開來源映像檔時出錯', 83 | fromFile: '從檔案燒錄', 84 | fromURL: '從網址燒錄', 85 | clone: '再製磁碟', 86 | image: '映像檔訊息', 87 | name: '名稱:', 88 | path: '路徑:', 89 | selectSource: '選擇來源', 90 | plugSource: '請插入來源磁碟', 91 | osImages: '系統映像檔格式', 92 | allFiles: '任何檔案格式', 93 | enterValidURL: '請輸入正確的網址', 94 | }, 95 | drives: { 96 | name: '名稱', 97 | size: '大小', 98 | location: '位置', 99 | find: '找到 {{length}} 個', 100 | select: '選取 {{select}}', 101 | showHidden: '顯示 {{num}} 個隱藏的磁碟', 102 | systemDriveDanger: '選擇系統分割區很危險,因為這將會刪除你的系統!', 103 | openInBrowser: 'Etcher-ng 會在瀏覽器中打開 {{link}}', 104 | changeTarget: '更改目標', 105 | largeDriveWarning: '您即將格式化一個非常大的磁碟', 106 | largeDriveWarningMsg: '您確定所選磁碟不是儲存資料的磁碟嗎?', 107 | systemDriveWarning: '您將要格式化系統分割區', 108 | systemDriveWarningMsg: '您確定要燒錄到系統分割區嗎?', 109 | }, 110 | flash: { 111 | another: '燒錄另一目標', 112 | target: '目標', 113 | location: '位置', 114 | error: '錯誤', 115 | flash: '燒錄', 116 | flashNow: '現在燒錄!', 117 | skip: '跳過了驗證', 118 | moreInfo: '更多資訊', 119 | speedTip: 120 | '透過將映像檔大小除以燒錄時間來計算速度。\n由於我們能夠跳過未使用的部分,因此具有 ext 分割區的磁碟映像檔燒錄速度更快。', 121 | speed: '速度:{{speed}} MB/秒', 122 | speedShort: '{{speed}} MB/秒', 123 | eta: '預計還需要:{{eta}}', 124 | failedTarget: '目標燒錄失敗', 125 | failedRetry: '重試燒錄失敗的目標', 126 | flashFailed: '燒錄失敗。', 127 | flashCompleted: '燒錄成功!', 128 | }, 129 | settings: { 130 | errorReporting: '匿名向 balena.io 回報程式錯誤和使用統計資料', 131 | verify: '刷機後自動驗證', 132 | verifyDesc: '驗證驅動器寫入是否正確', 133 | autoUpdate: '自動更新', 134 | settings: '軟體設定', 135 | systemInformation: '系統資訊', 136 | trimExtPartitions: '修改原始映像檔上未分配的空間(在 ext 類型分割區中)', 137 | decompressFirst: '在刷新之前解壓縮壓縮影像(即 .tar.xz)', 138 | }, 139 | menu: { 140 | edit: '編輯', 141 | view: '預覽', 142 | devTool: '打開開發者工具', 143 | window: '視窗', 144 | help: '協助', 145 | pro: 'Etcher 專業版', 146 | website: 'Etcher 的官網', 147 | issue: '提交 Issue', 148 | devmenu: '開發商', 149 | electrondevtools: '開啟 Electron 開發工具', 150 | testwindow: '開啟測試視窗', 151 | config: '編輯設定檔', 152 | restart: '重新啟動應用程式', 153 | gpu: '打開 chrome://gpu', 154 | procinternals: '打開 chrome://process-internals', 155 | goback: '回去', 156 | goforward: '前進', 157 | about: '關於 Etcher-ng', 158 | hide: '隱藏 Etcher-ng', 159 | hideOthers: '隱藏其它視窗', 160 | unhide: '取消隱藏', 161 | quit: '結束 Etcher-ng', 162 | }, 163 | }, 164 | }; 165 | 166 | export default translation; 167 | -------------------------------------------------------------------------------- /lib/gui/app/index.css: -------------------------------------------------------------------------------- 1 | /* Prefer border-box */ 2 | * { 3 | box-sizing: border-box; 4 | } 5 | -------------------------------------------------------------------------------- /lib/gui/app/index.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Etcher-ng 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/gui/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Etcher-ng 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/gui/app/models/available-drives.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DrivelistDrive } from '../../../shared/drive-constraints'; 18 | import { Actions, store } from './store'; 19 | 20 | export function hasAvailableDrives() { 21 | return getDrives().length > 0; 22 | } 23 | 24 | export function setDrives(drives: any[]) { 25 | store.dispatch({ 26 | type: Actions.SET_AVAILABLE_TARGETS, 27 | data: drives, 28 | }); 29 | } 30 | 31 | export function getDrives(): DrivelistDrive[] { 32 | return store.getState().toJS().availableDrives; 33 | } 34 | -------------------------------------------------------------------------------- /lib/gui/app/models/flash-state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as electron from 'electron'; 18 | import * as sdk from 'etcher-sdk'; 19 | import * as _ from 'lodash'; 20 | import { DrivelistDrive } from '../../../shared/drive-constraints'; 21 | import { bytesToMegabytes } from '../../../shared/units'; 22 | import { Actions, store } from './store'; 23 | 24 | /** 25 | * @summary Reset flash state 26 | */ 27 | export function resetState() { 28 | store.dispatch({ 29 | type: Actions.RESET_FLASH_STATE, 30 | data: {}, 31 | }); 32 | } 33 | 34 | /** 35 | * @summary Check if currently flashing 36 | */ 37 | export function isFlashing(): boolean { 38 | return store.getState().toJS().isFlashing; 39 | } 40 | 41 | /** 42 | * @summary Set the flashing flag 43 | * 44 | * @description 45 | * The flag is used to signify that we're going to 46 | * start a flash process. 47 | */ 48 | export function setFlashingFlag() { 49 | // see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods 50 | try { 51 | electron.ipcRenderer.send('disable-screensaver'); 52 | } catch (error) { 53 | console.log( 54 | "Can't disable-screensaver, we're probably not running on a balena-electron env", 55 | ); 56 | } 57 | store.dispatch({ 58 | type: Actions.SET_FLASHING_FLAG, 59 | data: {}, 60 | }); 61 | } 62 | 63 | /** 64 | * @summary Unset the flashing flag 65 | * 66 | * @description 67 | * The flag is used to signify that the write process ended. 68 | */ 69 | export function unsetFlashingFlag(results: { 70 | cancelled?: boolean; 71 | sourceChecksum?: string; 72 | errorCode?: string | number; 73 | }) { 74 | store.dispatch({ 75 | type: Actions.UNSET_FLASHING_FLAG, 76 | data: results, 77 | }); 78 | // see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods 79 | electron.ipcRenderer.send('enable-screensaver'); 80 | } 81 | 82 | export function setDevicePaths(devicePaths: string[]) { 83 | store.dispatch({ 84 | type: Actions.SET_DEVICE_PATHS, 85 | data: devicePaths, 86 | }); 87 | } 88 | 89 | export function addFailedDeviceError({ 90 | device, 91 | error, 92 | }: { 93 | device: DrivelistDrive; 94 | error: Error; 95 | }) { 96 | const failedDeviceErrorsMap = new Map( 97 | store.getState().toJS().failedDeviceErrors, 98 | ); 99 | if (failedDeviceErrorsMap.has(device.device)) { 100 | // Only store the first error 101 | return; 102 | } 103 | failedDeviceErrorsMap.set(device.device, { 104 | description: device.description, 105 | device: device.device, 106 | devicePath: device.devicePath, 107 | ...error, 108 | }); 109 | store.dispatch({ 110 | type: Actions.SET_FAILED_DEVICE_ERRORS, 111 | data: Array.from(failedDeviceErrorsMap), 112 | }); 113 | } 114 | 115 | /** 116 | * @summary Set the flashing state 117 | */ 118 | export function setProgressState( 119 | state: sdk.multiWrite.MultiDestinationProgress, 120 | ) { 121 | // Preserve only one decimal place 122 | const PRECISION = 1; 123 | const data = { 124 | ...state, 125 | percentage: 126 | state.percentage !== undefined && _.isFinite(state.percentage) 127 | ? Math.floor(state.percentage) 128 | : undefined, 129 | 130 | speed: _.attempt(() => { 131 | if (_.isFinite(state.speed)) { 132 | return _.round(bytesToMegabytes(state.speed), PRECISION); 133 | } 134 | 135 | return null; 136 | }), 137 | }; 138 | 139 | store.dispatch({ 140 | type: Actions.SET_FLASH_STATE, 141 | data, 142 | }); 143 | } 144 | 145 | export function getFlashResults() { 146 | return store.getState().toJS().flashResults; 147 | } 148 | 149 | export function getFlashState() { 150 | return store.getState().get('flashState').toJS(); 151 | } 152 | 153 | export function wasLastFlashCancelled() { 154 | return _.get(getFlashResults(), ['cancelled'], false); 155 | } 156 | 157 | export function getLastFlashSourceChecksum(): string { 158 | return getFlashResults().sourceChecksum; 159 | } 160 | 161 | export function getLastFlashErrorCode() { 162 | return getFlashResults().errorCode; 163 | } 164 | 165 | export function getFlashUuid() { 166 | return store.getState().toJS().flashUuid; 167 | } 168 | -------------------------------------------------------------------------------- /lib/gui/app/models/selection-state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { DrivelistDrive } from '../../../shared/drive-constraints'; 18 | import { SourceMetadata } from '../components/source-selector/source-selector'; 19 | 20 | import * as availableDrives from './available-drives'; 21 | import { Actions, store } from './store'; 22 | 23 | /** 24 | * @summary Select a drive by its device path 25 | */ 26 | export function selectDrive(driveDevice: string) { 27 | store.dispatch({ 28 | type: Actions.SELECT_TARGET, 29 | data: driveDevice, 30 | }); 31 | } 32 | 33 | /** 34 | * @summary Toggle drive selection 35 | */ 36 | export function toggleDrive(driveDevice: string) { 37 | if (isDriveSelected(driveDevice)) { 38 | deselectDrive(driveDevice); 39 | } else { 40 | selectDrive(driveDevice); 41 | } 42 | } 43 | 44 | export function selectSource(source: SourceMetadata) { 45 | store.dispatch({ 46 | type: Actions.SELECT_SOURCE, 47 | data: source, 48 | }); 49 | } 50 | 51 | /** 52 | * @summary Get all selected drives' devices 53 | */ 54 | export function getSelectedDevices(): string[] { 55 | return store.getState().getIn(['selection', 'devices']).toJS(); 56 | } 57 | 58 | /** 59 | * @summary Get all selected drive objects 60 | */ 61 | export function getSelectedDrives(): DrivelistDrive[] { 62 | const selectedDevices = getSelectedDevices(); 63 | return availableDrives 64 | .getDrives() 65 | .filter((drive) => selectedDevices.includes(drive.device)); 66 | } 67 | 68 | /** 69 | * @summary Get the selected image 70 | */ 71 | export function getImage(): SourceMetadata | undefined { 72 | return store.getState().toJS().selection.image; 73 | } 74 | 75 | /** 76 | * @summary Check if there is a selected drive 77 | */ 78 | export function hasDrive(): boolean { 79 | return Boolean(getSelectedDevices().length); 80 | } 81 | 82 | /** 83 | * @summary Check if there is a selected image 84 | */ 85 | export function hasImage(): boolean { 86 | return getImage() !== undefined; 87 | } 88 | 89 | /** 90 | * @summary Remove drive from selection 91 | */ 92 | export function deselectDrive(driveDevice: string) { 93 | store.dispatch({ 94 | type: Actions.DESELECT_TARGET, 95 | data: driveDevice, 96 | }); 97 | } 98 | 99 | export function deselectImage() { 100 | store.dispatch({ 101 | type: Actions.DESELECT_SOURCE, 102 | data: {}, 103 | }); 104 | } 105 | 106 | export function deselectAllDrives() { 107 | getSelectedDevices().forEach(deselectDrive); 108 | } 109 | 110 | /** 111 | * @summary Clear selections 112 | */ 113 | export function clear() { 114 | deselectImage(); 115 | deselectAllDrives(); 116 | } 117 | 118 | /** 119 | * @summary Check whether a given device is selected. 120 | */ 121 | export function isDriveSelected(driveDevice: string) { 122 | if (!driveDevice) { 123 | return false; 124 | } 125 | 126 | const selectedDriveDevices = getSelectedDevices(); 127 | return selectedDriveDevices.includes(driveDevice); 128 | } 129 | -------------------------------------------------------------------------------- /lib/gui/app/models/settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as _debug from 'debug'; 18 | import * as electron from 'electron'; 19 | import * as _ from 'lodash'; 20 | import { promises as fs } from 'fs'; 21 | import { join } from 'path'; 22 | 23 | const debug = _debug('etcher:models:settings'); 24 | 25 | const JSON_INDENT = 2; 26 | 27 | export const DEFAULT_WIDTH = 800; 28 | export const DEFAULT_HEIGHT = 480; 29 | 30 | /** 31 | * @summary Userdata directory path 32 | * @description 33 | * Defaults to the following: 34 | * - `%APPDATA%/etcher` on Windows 35 | * - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux 36 | * - `~/Library/Application Support/etcher` on macOS 37 | * See https://electronjs.org/docs/api/app#appgetpathname 38 | * 39 | * NOTE: We use the remote property when this module 40 | * is loaded in the Electron's renderer process 41 | */ 42 | function getConfigPath() { 43 | const app = electron.app || require('@electron/remote').app; 44 | return join(app.getPath('userData'), 'config.json'); 45 | } 46 | 47 | async function readConfigFile(filename: string): Promise<_.Dictionary> { 48 | let contents = '{}'; 49 | try { 50 | contents = await fs.readFile(filename, { encoding: 'utf8' }); 51 | } catch (error: any) { 52 | // noop 53 | } 54 | try { 55 | return JSON.parse(contents); 56 | } catch (parseError) { 57 | console.error(parseError); 58 | return {}; 59 | } 60 | } 61 | 62 | // exported for tests 63 | export async function readAll() { 64 | return await readConfigFile(getConfigPath()); 65 | } 66 | 67 | // exported for tests 68 | export async function writeConfigFile( 69 | filename: string, 70 | data: _.Dictionary, 71 | ): Promise { 72 | await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT)); 73 | } 74 | 75 | const DEFAULT_SETTINGS: _.Dictionary = { 76 | errorReporting: false, 77 | updatesEnabled: false, 78 | desktopNotifications: true, 79 | verify: false, 80 | autoBlockmapping: true, 81 | decompressFirst: true, 82 | }; 83 | 84 | const settings = _.cloneDeep(DEFAULT_SETTINGS); 85 | 86 | async function load(): Promise { 87 | debug('load'); 88 | const loadedSettings = await readAll(); 89 | _.assign(settings, loadedSettings); 90 | } 91 | 92 | const loaded = load(); 93 | 94 | export async function set( 95 | key: string, 96 | value: any, 97 | writeConfigFileFn = writeConfigFile, 98 | ): Promise { 99 | debug('set', key, value); 100 | await loaded; 101 | const previousValue = settings[key]; 102 | settings[key] = value; 103 | try { 104 | await writeConfigFileFn(getConfigPath(), settings); 105 | } catch (error: any) { 106 | // Revert to previous value if persisting settings failed 107 | settings[key] = previousValue; 108 | throw error; 109 | } 110 | } 111 | 112 | export async function get(key: string): Promise { 113 | await loaded; 114 | return getSync(key); 115 | } 116 | 117 | export function getSync(key: string): any { 118 | return _.cloneDeep(settings[key]); 119 | } 120 | 121 | export async function getAll() { 122 | debug('getAll'); 123 | await loaded; 124 | return _.cloneDeep(settings); 125 | } 126 | -------------------------------------------------------------------------------- /lib/gui/app/modules/drive-scanner.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as sdk from 'etcher-sdk'; 18 | import { 19 | Adapter, 20 | BlockDeviceAdapter, 21 | UsbbootDeviceAdapter, 22 | } from 'etcher-sdk/build/scanner/adapters'; 23 | import { geteuid, platform } from 'process'; 24 | 25 | const adapters: Adapter[] = [ 26 | new BlockDeviceAdapter({ 27 | includeSystemDrives: () => true, 28 | }), 29 | ]; 30 | 31 | // Can't use permissions.isElevated() here as it returns a promise and we need to set 32 | // module.exports = scanner right now. 33 | if (platform !== 'linux' || geteuid() === 0) { 34 | adapters.push(new UsbbootDeviceAdapter()); 35 | } 36 | 37 | if (platform === 'win32') { 38 | const { 39 | DriverlessDeviceAdapter: driverless, 40 | // tslint:disable-next-line:no-var-requires 41 | } = require('etcher-sdk/build/scanner/adapters/driverless'); 42 | adapters.push(new driverless()); 43 | } 44 | 45 | export const scanner = new sdk.scanner.Scanner(adapters); 46 | -------------------------------------------------------------------------------- /lib/gui/app/modules/exception-reporter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { logException } from '../modules/analytics'; 18 | import { showError } from '../os/dialog'; 19 | 20 | /** 21 | * @summary Report an exception 22 | */ 23 | export function report(exception?: Error) { 24 | if (exception === undefined) { 25 | return; 26 | } 27 | showError(exception); 28 | logException(exception); 29 | } 30 | -------------------------------------------------------------------------------- /lib/gui/app/modules/progress-status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License") 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as prettyBytes from 'pretty-bytes'; 18 | import * as i18next from 'i18next'; 19 | 20 | export interface FlashState { 21 | active: number; 22 | failed: number; 23 | percentage?: number; 24 | speed: number; 25 | position: number; 26 | type?: 'decompressing' | 'flashing' | 'verifying'; 27 | } 28 | 29 | export function fromFlashState({ 30 | type, 31 | percentage, 32 | position, 33 | }: Pick): { 34 | status: string; 35 | position?: string; 36 | } { 37 | if (type === undefined) { 38 | return { status: i18next.t('progress.starting') }; 39 | } else if (type === 'decompressing') { 40 | if (percentage == null) { 41 | return { status: i18next.t('progress.decompressing') }; 42 | } else { 43 | return { 44 | position: `${percentage}%`, 45 | status: i18next.t('progress.decompressing'), 46 | }; 47 | } 48 | } else if (type === 'flashing') { 49 | if (percentage != null) { 50 | if (percentage < 100) { 51 | return { 52 | position: `${percentage}%`, 53 | status: i18next.t('progress.flashing'), 54 | }; 55 | } else { 56 | return { status: i18next.t('progress.finishing') }; 57 | } 58 | } else { 59 | return { 60 | status: i18next.t('progress.flashing'), 61 | position: `${position ? prettyBytes(position) : ''}`, 62 | }; 63 | } 64 | } else if (type === 'verifying') { 65 | if (percentage == null) { 66 | return { status: i18next.t('progress.verifying') }; 67 | } else if (percentage < 100) { 68 | return { 69 | position: `${percentage}%`, 70 | status: i18next.t('progress.verifying'), 71 | }; 72 | } else { 73 | return { status: i18next.t('progress.finishing') }; 74 | } 75 | } 76 | return { status: i18next.t('progress.failing') }; 77 | } 78 | 79 | export function titleFromFlashState( 80 | state: Pick, 81 | ): string { 82 | const { status, position } = fromFlashState(state); 83 | if (position !== undefined) { 84 | return `${position} ${status}`; 85 | } 86 | return status; 87 | } 88 | -------------------------------------------------------------------------------- /lib/gui/app/os/dialog.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as electron from 'electron'; 18 | import * as remote from '@electron/remote'; 19 | import * as _ from 'lodash'; 20 | 21 | import * as errors from '../../../shared/errors'; 22 | import * as settings from '../../../gui/app/models/settings'; 23 | import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats'; 24 | import * as i18next from 'i18next'; 25 | 26 | async function mountSourceDrive() { 27 | // sourceDrivePath is the name of the link in /dev/disk/by-path 28 | const sourceDrivePath = await settings.get('automountOnFileSelect'); 29 | if (sourceDrivePath) { 30 | try { 31 | await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath); 32 | } catch (error: any) { 33 | // noop 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * @summary Open an image selection dialog 40 | * 41 | * @description 42 | * Notice that by image, we mean *.img/*.iso/*.zip/etc files. 43 | */ 44 | export async function selectImage(): Promise { 45 | await mountSourceDrive(); 46 | const options: electron.OpenDialogOptions = { 47 | // This variable is set when running in GNU/Linux from 48 | // inside an AppImage, and represents the working directory 49 | // from where the AppImage was run (which might not be the 50 | // place where the AppImage is located). `OWD` stands for 51 | // "Original Working Directory". 52 | // 53 | // See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 54 | defaultPath: process.env.OWD, 55 | properties: ['openFile', 'treatPackageAsDirectory'], 56 | filters: [ 57 | { 58 | name: i18next.t('source.osImages'), 59 | extensions: SUPPORTED_EXTENSIONS, 60 | }, 61 | { 62 | name: i18next.t('source.allFiles'), 63 | extensions: ['*'], 64 | }, 65 | ], 66 | }; 67 | const currentWindow = remote.getCurrentWindow(); 68 | const [file] = (await remote.dialog.showOpenDialog(currentWindow, options)) 69 | .filePaths; 70 | return file; 71 | } 72 | 73 | /** 74 | * @summary Open a warning dialog 75 | */ 76 | export async function showWarning(options: { 77 | confirmationLabel: string; 78 | rejectionLabel: string; 79 | title: string; 80 | description: string; 81 | }): Promise { 82 | _.defaults(options, { 83 | confirmationLabel: i18next.t('ok'), 84 | rejectionLabel: i18next.t('cancel'), 85 | }); 86 | 87 | const BUTTONS = [options.confirmationLabel, options.rejectionLabel]; 88 | 89 | const BUTTON_CONFIRMATION_INDEX = _.indexOf( 90 | BUTTONS, 91 | options.confirmationLabel, 92 | ); 93 | const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel); 94 | 95 | const { response } = await remote.dialog.showMessageBox( 96 | remote.getCurrentWindow(), 97 | { 98 | type: 'warning', 99 | buttons: BUTTONS, 100 | defaultId: BUTTON_REJECTION_INDEX, 101 | cancelId: BUTTON_REJECTION_INDEX, 102 | title: i18next.t('attention'), 103 | message: options.title, 104 | detail: options.description, 105 | }, 106 | ); 107 | return response === BUTTON_CONFIRMATION_INDEX; 108 | } 109 | 110 | /** 111 | * @summary Show error dialog for an Error instance 112 | */ 113 | export function showError(error: Error) { 114 | const title = errors.getTitle(error); 115 | const message = errors.getDescription(error); 116 | remote.dialog.showErrorBox(title, message); 117 | } 118 | -------------------------------------------------------------------------------- /lib/gui/app/os/notification.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as remote from '@electron/remote'; 18 | 19 | import * as settings from '../models/settings'; 20 | 21 | /** 22 | * @summary Send a notification 23 | */ 24 | export async function send(title: string, body: string, icon: string) { 25 | // Bail out if desktop notifications are disabled 26 | if (!(await settings.get('desktopNotifications'))) { 27 | return; 28 | } 29 | 30 | // `app.dock` is only defined in OS X 31 | if (remote.app.dock) { 32 | remote.app.dock.bounce(); 33 | } 34 | 35 | return new window.Notification(title, { body, icon }); 36 | } 37 | -------------------------------------------------------------------------------- /lib/gui/app/os/open-external/services/open-external.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 balena.io and Alex313031 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as electron from 'electron'; 18 | import * as electronLog from 'electron-log'; 19 | import * as settings from '../../../models/settings'; 20 | import { logEvent } from '../../../modules/analytics'; 21 | 22 | /** 23 | * @summary Open an external resource 24 | */ 25 | export async function open(url: string) { 26 | // Don't open links if they're disabled by the env var 27 | if (await settings.get('disableExternalLinks')) { 28 | return; 29 | } 30 | 31 | logEvent('Open external link', { url }); 32 | 33 | if (url) { 34 | electronLog.info('Opening external browser to', url); 35 | electron.shell.openExternal(url); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/gui/app/os/open-internal-remote/services/open-internal-remote.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 balena.io and Alex313031 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as remote from '@electron/remote'; 18 | import * as electronLog from 'electron-log/renderer'; 19 | 20 | /** 21 | * @summary Open an external resource in a new BrowserWindow 22 | * using the remote module. 23 | */ 24 | export async function open(url: string) { 25 | if (url) { 26 | electronLog.info(`Opening remote internal window to ` + `'` + url + `'`); 27 | const remoteWin = new remote.BrowserWindow({ 28 | width: 1024, 29 | height: 768, 30 | useContentSize: true, 31 | webPreferences: { 32 | sandbox: true, 33 | }, 34 | }); 35 | remoteWin.loadURL(url); 36 | remoteWin.on('close', () => { 37 | electronLog.info('Closed a remote internal window'); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/gui/app/os/open-internal/services/open-internal.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 balena.io and Alex313031 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as electron from 'electron'; 18 | import * as electronLog from 'electron-log'; 19 | 20 | /** 21 | * @summary Open an external resource in a new BrowserWindow 22 | */ 23 | export async function open(url: string) { 24 | if (url) { 25 | electronLog.info(`Opening internal window to ` + `'` + url + `'`); 26 | const newWin = new electron.BrowserWindow({ 27 | width: 1024, 28 | height: 768, 29 | useContentSize: true, 30 | webPreferences: { 31 | sandbox: true, 32 | }, 33 | }); 34 | newWin.loadURL(url); 35 | newWin.on('close', () => { 36 | electronLog.info('Closed an internal window'); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/gui/app/os/window-progress.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as remote from '@electron/remote'; 18 | 19 | import { percentageToFloat } from '../../../shared/utils'; 20 | import { FlashState, titleFromFlashState } from '../modules/progress-status'; 21 | 22 | /** 23 | * @summary The title of the main window upon program launch 24 | */ 25 | const INITIAL_TITLE = document.title; 26 | 27 | /** 28 | * @summary Make the full window status title 29 | */ 30 | function getWindowTitle(state?: FlashState) { 31 | if (state) { 32 | return `${INITIAL_TITLE} – ${titleFromFlashState(state)}`; 33 | } 34 | return INITIAL_TITLE; 35 | } 36 | 37 | /** 38 | * @summary A reference to the current renderer Electron window 39 | * 40 | * @description 41 | * We expose this property to `this` for testability purposes. 42 | */ 43 | export const currentWindow = remote.getCurrentWindow(); 44 | 45 | /** 46 | * @summary Set operating system window progress 47 | * 48 | * @description 49 | * Show progress inline in operating system task bar 50 | */ 51 | export function set(state: FlashState) { 52 | if (state.percentage != null) { 53 | currentWindow.setProgressBar(percentageToFloat(state.percentage)); 54 | } 55 | currentWindow.setTitle(getWindowTitle(state)); 56 | } 57 | 58 | /** 59 | * @summary Clear the window progress bar 60 | */ 61 | export function clear() { 62 | // Passing 0 or null/undefined doesn't work. 63 | currentWindow.setProgressBar(-1); 64 | currentWindow.setTitle(getWindowTitle(undefined)); 65 | } 66 | -------------------------------------------------------------------------------- /lib/gui/app/os/windows-network-drives.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { exec } from 'child_process'; 18 | import { withTmpFile } from 'etcher-sdk/build/tmp'; 19 | import { readFile } from 'fs'; 20 | import { chain, trim } from 'lodash'; 21 | import { platform } from 'os'; 22 | import { join } from 'path'; 23 | import { env } from 'process'; 24 | import { promisify } from 'util'; 25 | 26 | const readFileAsync = promisify(readFile); 27 | 28 | const execAsync = promisify(exec); 29 | 30 | /** 31 | * @summary Returns wmic's output for network drives 32 | */ 33 | async function getWmicNetworkDrivesOutput(): Promise { 34 | // When trying to read wmic's stdout directly from node, it is encoded with the current 35 | // console codepage (depending on the computer). 36 | // Decoding this would require getting this codepage somehow and using iconv as node 37 | // doesn't know how to read cp850 directly for example. 38 | // We could also use wmic's "/output:" switch but it doesn't work when the filename 39 | // contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example). 40 | // So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded. 41 | const options = { 42 | // Close the file once it's created 43 | keepOpen: false, 44 | // Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-") 45 | prefix: 'tmp', 46 | }; 47 | return withTmpFile(options, async ({ path }) => { 48 | const command = [ 49 | join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'), 50 | 'path', 51 | 'Win32_LogicalDisk', 52 | 'Where', 53 | 'DriveType="4"', 54 | 'get', 55 | 'DeviceID,ProviderName', 56 | '>', 57 | `"${path}"`, 58 | ]; 59 | await execAsync(command.join(' '), { windowsHide: true }); 60 | return readFileAsync(path, 'ucs2'); 61 | }); 62 | } 63 | 64 | /** 65 | * @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public' 66 | */ 67 | async function getWindowsNetworkDrives( 68 | getWmicOutput: () => Promise, 69 | ): Promise> { 70 | const result = await getWmicOutput(); 71 | const couples: Array<[string, string]> = chain(result) 72 | .split('\n') 73 | // Remove header line 74 | .slice(1) 75 | // Remove extra spaces / tabs / carriage returns 76 | .invokeMap(String.prototype.trim) 77 | // Filter out empty lines 78 | .compact() 79 | .map((str: string): [string, string] => { 80 | const colonPosition = str.indexOf(':'); 81 | if (colonPosition === -1) { 82 | throw new Error(`Can't parse wmic output: ${result}`); 83 | } 84 | return [ 85 | str.slice(0, colonPosition + 1), 86 | trim(str.slice(colonPosition + 1)), 87 | ]; 88 | }) 89 | .filter((couple) => couple[1].length > 0) 90 | .value(); 91 | return new Map(couples); 92 | } 93 | 94 | /** 95 | * @summary Replaces network drive letter with network drive location in the provided filePath on Windows 96 | */ 97 | export async function replaceWindowsNetworkDriveLetter( 98 | filePath: string, 99 | // getWmicOutput is a parameter so it can be replaced in tests 100 | getWmicOutput = getWmicNetworkDrivesOutput, 101 | ): Promise { 102 | let result = filePath; 103 | if (platform() === 'win32') { 104 | const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath); 105 | if (matches !== null) { 106 | const [, drive, relativePath] = matches; 107 | const drives = await getWindowsNetworkDrives(getWmicOutput); 108 | const location = drives.get(drive); 109 | if (location !== undefined) { 110 | result = `${location}\\${relativePath}`; 111 | } 112 | } 113 | } 114 | return result; 115 | } 116 | -------------------------------------------------------------------------------- /lib/gui/app/renderer.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { main } from './app'; 3 | import './i18n'; 4 | import { langParser } from './i18n'; 5 | import { ipcRenderer } from 'electron'; 6 | 7 | ipcRenderer.send('change-lng', langParser()); 8 | 9 | if (module.hot) { 10 | module.hot.accept('./app', () => { 11 | main(); 12 | }); 13 | } 14 | 15 | main(); 16 | -------------------------------------------------------------------------------- /lib/gui/app/theme.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"), 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as _ from 'lodash'; 18 | import { Theme } from 'rendition'; 19 | 20 | export const colors = { 21 | dark: { 22 | foreground: '#fff', 23 | background: '#4d5057', 24 | soft: { 25 | foreground: '#ddd', 26 | background: '#64686a', 27 | }, 28 | disabled: { 29 | foreground: '#787c7f', 30 | background: '#3a3c41', 31 | }, 32 | }, 33 | light: { 34 | foreground: '#666', 35 | background: '#fff', 36 | soft: { 37 | foreground: '#b3b3b3', 38 | }, 39 | disabled: { 40 | foreground: '#787c7f', 41 | background: '#d5d5d5', 42 | }, 43 | }, 44 | default: { 45 | foreground: '#b3b3b3', 46 | background: '#ececec', 47 | }, 48 | primary: { 49 | foreground: '#fff', 50 | background: '#00aeef', 51 | }, 52 | secondary: { 53 | foreground: '#000', 54 | background: '#ddd', 55 | main: '#fff', 56 | }, 57 | warning: { 58 | foreground: '#fff', 59 | background: '#fca321', 60 | }, 61 | danger: { 62 | foreground: '#fff', 63 | background: '#d9534f', 64 | }, 65 | success: { 66 | foreground: '#fff', 67 | background: '#5fb835', 68 | }, 69 | }; 70 | 71 | const font = 'SourceSansPro'; 72 | 73 | export const theme = _.merge({}, Theme, { 74 | colors, 75 | font, 76 | header: { 77 | height: '40px', 78 | }, 79 | global: { 80 | font: { 81 | family: font, 82 | size: 16, 83 | }, 84 | text: { 85 | medium: { 86 | size: 16, 87 | }, 88 | }, 89 | }, 90 | button: { 91 | border: { 92 | width: '0', 93 | radius: '24px', 94 | }, 95 | disabled: { 96 | opacity: 1, 97 | }, 98 | extend: () => ` 99 | width: 200px; 100 | font-size: 16px; 101 | 102 | && { 103 | width: 200px; 104 | height: 48px; 105 | } 106 | 107 | :disabled { 108 | background-color: ${colors.dark.disabled.background}; 109 | color: ${colors.dark.disabled.foreground}; 110 | opacity: 1; 111 | 112 | :hover { 113 | background-color: ${colors.dark.disabled.background}; 114 | color: ${colors.dark.disabled.foreground}; 115 | } 116 | } 117 | `, 118 | }, 119 | layer: { 120 | extend: () => ` 121 | > div:first-child { 122 | background-color: transparent; 123 | } 124 | `, 125 | }, 126 | }); 127 | -------------------------------------------------------------------------------- /lib/gui/app/utils/etcher-pro-specific.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Dictionary } from 'lodash'; 18 | 19 | type BalenaTag = { 20 | id: number; 21 | name: string; 22 | value: string; 23 | }; 24 | 25 | export class EtcherPro { 26 | private supervisorAddr: string; 27 | private supervisorKey: string; 28 | private tags: Dictionary | undefined; 29 | public uuid: string; 30 | 31 | constructor(supervisorAddr: string, supervisorKey: string) { 32 | this.supervisorAddr = supervisorAddr; 33 | this.supervisorKey = supervisorKey; 34 | this.uuid = (process.env.BALENA_DEVICE_UUID ?? 'NO-UUID').substring(0, 7); 35 | this.tags = undefined; 36 | this.get_tags().then((tags) => (this.tags = tags)); 37 | } 38 | 39 | async get_tags(): Promise> { 40 | const result = await fetch( 41 | this.supervisorAddr + '/v2/device/tags?apikey=' + this.supervisorKey, 42 | ); 43 | const parsed = await result.json(); 44 | if (parsed['status'] === 'success') { 45 | return Object.assign( 46 | {}, 47 | ...parsed['tags'].map((tag: BalenaTag) => { 48 | return { [tag.name]: tag.value }; 49 | }), 50 | ); 51 | } else { 52 | return {}; 53 | } 54 | } 55 | 56 | public get_serial(): string | undefined { 57 | if (this.tags) { 58 | return this.tags['Serial']; 59 | } else { 60 | return undefined; 61 | } 62 | } 63 | } 64 | 65 | export function etcherProInfo(): EtcherPro | undefined { 66 | const BALENA_SUPERVISOR_ADDRESS = process.env.BALENA_SUPERVISOR_ADDRESS; 67 | const BALENA_SUPERVISOR_API_KEY = process.env.BALENA_SUPERVISOR_API_KEY; 68 | 69 | if (BALENA_SUPERVISOR_ADDRESS && BALENA_SUPERVISOR_API_KEY) { 70 | return new EtcherPro(BALENA_SUPERVISOR_ADDRESS, BALENA_SUPERVISOR_API_KEY); 71 | } 72 | return undefined; 73 | } 74 | -------------------------------------------------------------------------------- /lib/gui/app/utils/middle-ellipsis.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Juan Cruz Viotti. https://github.com/jviotti 3 | * Copyright 2018 balena.io 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @summary Truncate text from the middle with an ellipsis 20 | */ 21 | export function middleEllipsis(input: string, limit: number): string { 22 | // We can't provide a 100% expected result if the limit is less than 3. For example: 23 | // 24 | // If the limit == 2: 25 | // Should we display the first at last character without an ellipses in the middle? 26 | // Should we display just one character and an ellipses before or after? 27 | // Should we display nothing at all? 28 | // 29 | // If the limit == 1: 30 | // Should we display just one character? 31 | // Should we display just an ellipses? 32 | // Should we display nothing at all? 33 | // 34 | // Etc. 35 | if (limit < 3) { 36 | throw new Error('middleEllipsis: Ellipses Limit should be at least 3'); 37 | } 38 | 39 | // Do nothing, the string doesn't need truncation. 40 | if (input.length <= limit) { 41 | return input; 42 | } 43 | 44 | const lengthOfTheSidesAfterTruncation = Math.floor((limit - 1) / 2); 45 | const finalLeftPart = input.slice(0, lengthOfTheSidesAfterTruncation); 46 | const finalRightPart = input.slice( 47 | input.length - lengthOfTheSidesAfterTruncation, 48 | ); 49 | 50 | return finalLeftPart + '…' + finalRightPart; 51 | } 52 | -------------------------------------------------------------------------------- /lib/gui/assets/balena.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/drive.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/gui/assets/flash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/lib/gui/assets/icon.ico -------------------------------------------------------------------------------- /lib/gui/assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/lib/gui/assets/icon48.png -------------------------------------------------------------------------------- /lib/gui/assets/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/lib/gui/assets/icon64.png -------------------------------------------------------------------------------- /lib/gui/assets/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/gui/assets/love.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/src.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/gui/assets/tgt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/shared/catalina-sudo/sudo-askpass.osascript-en.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | 3 | ObjC.import('stdlib') 4 | 5 | const app = Application.currentApplication() 6 | app.includeStandardAdditions = true 7 | 8 | const result = app.displayDialog('Etcher-ng needs privileged access in order to flash disks.\n\nType your password to allow this.', { 9 | defaultAnswer: '', 10 | withIcon: 'caution', 11 | buttons: ['Cancel', 'Ok'], 12 | defaultButton: 'Ok', 13 | hiddenAnswer: true, 14 | }) 15 | 16 | if (result.buttonReturned === 'Ok') { 17 | result.textReturned 18 | } else { 19 | $.exit(255) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | 3 | ObjC.import('stdlib') 4 | 5 | const app = Application.currentApplication() 6 | app.includeStandardAdditions = true 7 | 8 | const result = app.displayDialog('Etcher-ng 需要来自管理员的权限才能烧录镜像到磁盘。\n\n输入您的密码以允许此操作。', { 9 | defaultAnswer: '', 10 | withIcon: 'caution', 11 | buttons: ['取消', '好'], 12 | defaultButton: '好', 13 | hiddenAnswer: true, 14 | }) 15 | 16 | if (result.buttonReturned === '好') { 17 | result.textReturned 18 | } else { 19 | $.exit(255) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /lib/shared/catalina-sudo/sudo.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { execFile } from 'child_process'; 18 | import { join } from 'path'; 19 | import { env } from 'process'; 20 | import { promisify } from 'util'; 21 | 22 | import { getAppPath } from '../utils'; 23 | import { supportedLocales } from '../../gui/app/i18n'; 24 | 25 | const execFileAsync = promisify(execFile); 26 | 27 | const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED'; 28 | const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`; 29 | 30 | export async function sudo( 31 | command: string, 32 | ): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> { 33 | try { 34 | let lang = Intl.DateTimeFormat().resolvedOptions().locale; 35 | lang = lang.substr(0, 2); 36 | if (supportedLocales.indexOf(lang) > -1) { 37 | // language should be present 38 | } else { 39 | // fallback to eng 40 | lang = 'en'; 41 | } 42 | 43 | const { stdout, stderr } = await execFileAsync( 44 | 'sudo', 45 | ['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`], 46 | { 47 | encoding: 'utf8', 48 | env: { 49 | PATH: env.PATH, 50 | SUDO_ASKPASS: join( 51 | getAppPath(), 52 | __dirname, 53 | `sudo-askpass.osascript-${lang}.js`, 54 | ), 55 | }, 56 | }, 57 | ); 58 | return { 59 | cancelled: false, 60 | stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length), 61 | stderr, 62 | }; 63 | } catch (error: any) { 64 | if (error.code === 1) { 65 | if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) { 66 | return { cancelled: true }; 67 | } 68 | error.stdout = error.stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length); 69 | } 70 | throw error; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/shared/exit-codes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const SUCCESS = 0; 18 | export const GENERAL_ERROR = 1; 19 | export const VALIDATION_ERROR = 2; 20 | export const CANCELLED = 3; 21 | -------------------------------------------------------------------------------- /lib/shared/supported-formats.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { basename } from 'path'; 18 | 19 | export const SUPPORTED_EXTENSIONS = [ 20 | 'bin', 21 | 'bz2', 22 | 'dmg', 23 | 'dsk', 24 | 'etch', 25 | 'gz', 26 | 'hddimg', 27 | 'img', 28 | 'iso', 29 | 'raw', 30 | 'rpi-sdimg', 31 | 'sdcard', 32 | 'vhd', 33 | 'wic', 34 | 'wim', 35 | 'xz', 36 | 'zip', 37 | ]; 38 | 39 | export function looksLikeWindowsImage(imagePath: string): boolean { 40 | const regex = /windows|win7|win8|win10|winxp|winvista/i; 41 | return regex.test(basename(imagePath)); 42 | } 43 | -------------------------------------------------------------------------------- /lib/shared/units.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const MEGABYTE_TO_BYTE_RATIO = 1000000; 18 | 19 | export function bytesToMegabytes(bytes: number): number { 20 | return bytes / MEGABYTE_TO_BYTE_RATIO; 21 | } 22 | -------------------------------------------------------------------------------- /lib/shared/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import axios from 'axios'; 18 | import { Dictionary } from 'lodash'; 19 | 20 | import * as errors from './errors'; 21 | 22 | export function isValidPercentage(percentage: any): boolean { 23 | return typeof percentage === 'number' && percentage >= 0 && percentage <= 100; 24 | } 25 | 26 | export function percentageToFloat(percentage: any) { 27 | if (!isValidPercentage(percentage)) { 28 | throw errors.createError({ 29 | title: `Invalid percentage: ${percentage}`, 30 | }); 31 | } 32 | return percentage / 100; 33 | } 34 | 35 | export async function delay(duration: number): Promise { 36 | await new Promise((resolve) => { 37 | setTimeout(resolve, duration); 38 | }); 39 | } 40 | 41 | export function getAppPath(): string { 42 | return ( 43 | (require('electron').app || require('@electron/remote').app) 44 | .getAppPath() 45 | // With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will 46 | // include the app-x64 or app-arm64 folder depending on the arch. 47 | // We don't care about the app.asar file, we want the actual folder. 48 | .replace(/\.asar$/, () => 49 | process.platform === 'darwin' ? '-' + process.arch : '', 50 | ) 51 | ); 52 | } 53 | 54 | export function isJson(jsonString: string) { 55 | try { 56 | JSON.parse(jsonString); 57 | } catch (e) { 58 | return false; 59 | } 60 | return true; 61 | } 62 | -------------------------------------------------------------------------------- /promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/promo.png -------------------------------------------------------------------------------- /repo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | type: electron 3 | release: github 4 | publishMetadata: true 5 | sentry: 6 | org: balenaetcher 7 | team: resinio 8 | type: electron 9 | triggerNotification: 10 | version: 1.7.9 11 | stagingPercentage: 100 12 | upstream: 13 | - repo: etcher-sdk 14 | url: https://github.com/balena-io-modules/etcher-sdk 15 | module: etcher-sdk 16 | - repo: sys-class-rgb-led 17 | url: https://github.com/balena-io-modules/sys-class-rgb-led 18 | module: sys-class-rgb-led 19 | - repo: rendition 20 | url: https://github.com/balena-io-modules/rendition 21 | module: rendition 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | awscli==1.19.112 2 | shyaml==0.6.2 3 | -------------------------------------------------------------------------------- /scripts/ci/ensure-all-file-extensions-in-gitattributes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### 4 | # Copyright 2017 balena.io 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ### 18 | 19 | set -u 20 | set -e 21 | 22 | # Read list of wildcards from .gitattributes 23 | wildcards=() 24 | while IFS='' read -r line || [[ -n "$line" ]]; do 25 | if [[ -n "$line" ]]; then 26 | if [[ ! "$line" =~ "^#" ]]; then 27 | filetype=$(echo "$line" | cut -d ' ' -f 2) 28 | if [[ "$filetype" == "text" ]] || [[ "$filetype" == "binary" ]]; then 29 | wildcards+=("$(echo "$line" | cut -d ' ' -f 1)") 30 | fi 31 | fi 32 | fi 33 | done < .gitattributes 34 | 35 | # Verify those wildcards against all files stored in the repo 36 | git ls-tree -r HEAD | while IFS='' read line; do 37 | if [[ "$(echo $line | cut -d ' ' -f 2)" == "blob" ]]; then 38 | # the cut delimiter in the line below is actually a tab character, not a space 39 | filename=$(basename $(echo "$line" | cut -d ' ' -f 2)) 40 | found_match=0 41 | for wildcard in "${wildcards[@]}"; do 42 | if [[ "$filename" = $wildcard ]]; then 43 | found_match=1 44 | break 45 | fi 46 | done 47 | if [[ $found_match -eq 0 ]]; then 48 | echo "No wildcards match $filename" 49 | exit 1 50 | fi 51 | fi 52 | done 53 | -------------------------------------------------------------------------------- /scripts/clean-shrinkwrap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This script is in charge of cleaning the `shrinkwrap` file. 3 | * 4 | * `npm shrinkwrap` has a bug where it will add optional dependencies 5 | * to `npm-shrinkwrap.json`, therefore causing errors if these optional 6 | * dependendencies are platform dependent and you then try to build 7 | * the project in another platform. 8 | * 9 | * As a workaround, we keep a list of platform dependent dependencies in 10 | * the `platformSpecificDependencies` property of `package.json`, 11 | * and manually remove them from `npm-shrinkwrap.json` if they exist. 12 | * 13 | * See: https://github.com/npm/npm/issues/2679 14 | */ 15 | 16 | import { writeFile } from 'fs'; 17 | import * as omit from 'omit-deep-lodash'; 18 | import * as path from 'path'; 19 | import { promisify } from 'util'; 20 | 21 | import * as shrinkwrap from '../npm-shrinkwrap.json'; 22 | import * as packageInfo from '../package.json'; 23 | 24 | const writeFileAsync = promisify(writeFile); 25 | 26 | const JSON_INDENT = 2; 27 | const SHRINKWRAP_FILENAME = path.join(__dirname, '..', 'npm-shrinkwrap.json'); 28 | 29 | async function main() { 30 | try { 31 | const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies); 32 | for (const item of Object.values(cleaned.dependencies)) { 33 | // @ts-ignore 34 | item.dev = true; 35 | } 36 | await writeFileAsync( 37 | SHRINKWRAP_FILENAME, 38 | JSON.stringify(cleaned, null, JSON_INDENT), 39 | ); 40 | } catch (error: any) { 41 | console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`); 42 | process.exitCode = 1; 43 | } 44 | console.log( 45 | `[OK] Wrote shrinkwrap file to ${path.relative( 46 | __dirname, 47 | SHRINKWRAP_FILENAME, 48 | )}`, 49 | ); 50 | } 51 | 52 | main(); 53 | -------------------------------------------------------------------------------- /secrets/APPLE_SIGNING.p12.secret: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/secrets/APPLE_SIGNING.p12.secret -------------------------------------------------------------------------------- /secrets/APPLE_SIGNING_PASSWORD.txt.secret: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/secrets/APPLE_SIGNING_PASSWORD.txt.secret -------------------------------------------------------------------------------- /secrets/WINDOWS_SIGNING.pfx.secret: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/secrets/WINDOWS_SIGNING.pfx.secret -------------------------------------------------------------------------------- /secrets/WINDOWS_SIGNING_PASSWORD.txt.secret: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/secrets/WINDOWS_SIGNING_PASSWORD.txt.secret -------------------------------------------------------------------------------- /secrets/XCODE_APP_LOADER_PASSWORD.txt.secret: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/secrets/XCODE_APP_LOADER_PASSWORD.txt.secret -------------------------------------------------------------------------------- /tests/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | require-jsdoc: 3 | - off 4 | no-undefined: 5 | - off 6 | init-declarations: 7 | - off 8 | no-unused-expressions: 9 | - off 10 | prefer-arrow-callback: 11 | - off 12 | no-magic-numbers: 13 | - off 14 | id-length: 15 | - error 16 | - min: 2 17 | exceptions: 18 | - "_" 19 | - "m" 20 | -------------------------------------------------------------------------------- /tests/data/wmic-output.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alex313031/etcher-ng-win7/0623959b965cf4a2c23e8cb74b2f57adcf24b5db/tests/data/wmic-output.txt -------------------------------------------------------------------------------- /tests/gui/allow-renderer-process-reuse.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:no-var-requires 2 | const { app } = require('electron'); 3 | 4 | if (app !== undefined) { 5 | // tslint:disable-next-line:no-var-requires 6 | const remoteMain = require('@electron/remote/main'); 7 | 8 | remoteMain.initialize(); 9 | 10 | app.on('browser-window-created', (_event, window) => 11 | remoteMain.enable(window.webContents), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /tests/gui/models/settings.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as _ from 'lodash'; 19 | import { stub } from 'sinon'; 20 | 21 | import * as settings from '../../../lib/gui/app/models/settings'; 22 | 23 | async function checkError(promise: Promise, fn: (err: Error) => any) { 24 | try { 25 | await promise; 26 | } catch (error: any) { 27 | await fn(error); 28 | return; 29 | } 30 | throw new Error('Expected error was not thrown'); 31 | } 32 | 33 | describe('Browser: settings', () => { 34 | it('should be able to set and read values', async () => { 35 | expect(await settings.get('foo')).to.be.undefined; 36 | await settings.set('foo', true); 37 | expect(await settings.get('foo')).to.be.true; 38 | await settings.set('foo', false); 39 | expect(await settings.get('foo')).to.be.false; 40 | }); 41 | 42 | describe('.set()', () => { 43 | it('should not change the application state if storing to the local machine results in an error', async () => { 44 | await settings.set('foo', 'bar'); 45 | expect(await settings.get('foo')).to.equal('bar'); 46 | 47 | const writeConfigFileStub = stub(); 48 | writeConfigFileStub.returns(Promise.reject(new Error('settings error'))); 49 | 50 | const p = settings.set('foo', 'baz', writeConfigFileStub); 51 | await checkError(p, async (error) => { 52 | expect(error).to.be.an.instanceof(Error); 53 | expect(error.message).to.equal('settings error'); 54 | expect(await settings.get('foo')).to.equal('bar'); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('.set()', () => { 60 | it('should set an unknown key', async () => { 61 | expect(await settings.get('foobar')).to.be.undefined; 62 | await settings.set('foobar', true); 63 | expect(await settings.get('foobar')).to.be.true; 64 | }); 65 | 66 | it('should set the key to undefined if no value', async () => { 67 | await settings.set('foo', 'bar'); 68 | expect(await settings.get('foo')).to.equal('bar'); 69 | await settings.set('foo', undefined); 70 | expect(await settings.get('foo')).to.be.undefined; 71 | }); 72 | 73 | it('should store the setting to the local machine', async () => { 74 | const data = await settings.readAll(); 75 | expect(data.foo).to.be.undefined; 76 | await settings.set('foo', 'bar'); 77 | const data1 = await settings.readAll(); 78 | expect(data1.foo).to.equal('bar'); 79 | }); 80 | 81 | it('should not change the application state if storing to the local machine results in an error', async () => { 82 | await settings.set('foo', 'bar'); 83 | expect(await settings.get('foo')).to.equal('bar'); 84 | 85 | const writeConfigFileStub = stub(); 86 | writeConfigFileStub.returns(Promise.reject(new Error('settings error'))); 87 | 88 | await checkError( 89 | settings.set('foo', 'baz', writeConfigFileStub), 90 | async (error) => { 91 | expect(error).to.be.an.instanceof(Error); 92 | expect(error.message).to.equal('settings error'); 93 | expect(await settings.get('foo')).to.equal('bar'); 94 | }, 95 | ); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /tests/gui/modules/child-writer.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as ipc from 'node-ipc'; 19 | 20 | import('../../../lib/gui/modules/child-writer'); 21 | 22 | describe('Browser: childWriter', function () { 23 | it('should have the ipc config set to silent', function () { 24 | expect(ipc.config.silent).to.be.true; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/gui/modules/image-writer.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import { Drive as DrivelistDrive } from 'drivelist'; 19 | import { sourceDestination } from 'etcher-sdk'; 20 | import * as ipc from 'node-ipc'; 21 | import { assert, SinonStub, stub } from 'sinon'; 22 | 23 | import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector'; 24 | import * as flashState from '../../../lib/gui/app/models/flash-state'; 25 | import * as imageWriter from '../../../lib/gui/app/modules/image-writer'; 26 | 27 | // @ts-ignore 28 | const fakeDrive: DrivelistDrive = {}; 29 | 30 | describe('Browser: imageWriter', () => { 31 | describe('.flash()', () => { 32 | const image: SourceMetadata = { 33 | hasMBR: false, 34 | partitions: [], 35 | description: 'foo.img', 36 | displayName: 'foo.img', 37 | path: 'foo.img', 38 | SourceType: sourceDestination.File, 39 | extension: 'img', 40 | }; 41 | 42 | describe('given a successful write', () => { 43 | let performWriteStub: SinonStub; 44 | 45 | beforeEach(() => { 46 | performWriteStub = stub(); 47 | performWriteStub.returns( 48 | Promise.resolve({ 49 | cancelled: false, 50 | sourceChecksum: '1234', 51 | }), 52 | ); 53 | }); 54 | 55 | afterEach(() => { 56 | performWriteStub.reset(); 57 | }); 58 | 59 | it('should set flashing to false when done', async () => { 60 | flashState.unsetFlashingFlag({ 61 | cancelled: false, 62 | sourceChecksum: '1234', 63 | }); 64 | 65 | try { 66 | await imageWriter.flash(image, [fakeDrive], performWriteStub); 67 | } catch { 68 | // noop 69 | } finally { 70 | expect(flashState.isFlashing()).to.be.false; 71 | } 72 | }); 73 | 74 | it('should prevent writing more than once', async () => { 75 | flashState.unsetFlashingFlag({ 76 | cancelled: false, 77 | sourceChecksum: '1234', 78 | }); 79 | 80 | try { 81 | await Promise.all([ 82 | imageWriter.flash(image, [fakeDrive], performWriteStub), 83 | imageWriter.flash(image, [fakeDrive], performWriteStub), 84 | ]); 85 | assert.fail('Writing twice should fail'); 86 | } catch (error: any) { 87 | expect(error.message).to.equal( 88 | 'There is already a flash in progress', 89 | ); 90 | } 91 | }); 92 | }); 93 | 94 | describe('given an unsuccessful write', () => { 95 | let performWriteStub: SinonStub; 96 | 97 | beforeEach(() => { 98 | performWriteStub = stub(); 99 | const error: Error & { code?: string } = new Error('write error'); 100 | error.code = 'FOO'; 101 | performWriteStub.returns(Promise.reject(error)); 102 | }); 103 | 104 | afterEach(() => { 105 | performWriteStub.reset(); 106 | }); 107 | 108 | it('should set flashing to false when done', async () => { 109 | try { 110 | await imageWriter.flash(image, [fakeDrive], performWriteStub); 111 | } catch { 112 | // noop 113 | } finally { 114 | expect(flashState.isFlashing()).to.be.false; 115 | } 116 | }); 117 | 118 | it('should set the error code in the flash results', async () => { 119 | try { 120 | await imageWriter.flash(image, [fakeDrive], performWriteStub); 121 | } catch { 122 | // noop 123 | } finally { 124 | const flashResults = flashState.getFlashResults(); 125 | expect(flashResults.errorCode).to.equal('FOO'); 126 | } 127 | }); 128 | 129 | it('should be rejected with the error', async () => { 130 | flashState.unsetFlashingFlag({ 131 | cancelled: false, 132 | sourceChecksum: '1234', 133 | }); 134 | try { 135 | await imageWriter.flash(image, [fakeDrive], performWriteStub); 136 | } catch (error: any) { 137 | expect(error).to.be.an.instanceof(Error); 138 | expect(error.message).to.equal('write error'); 139 | } 140 | }); 141 | }); 142 | }); 143 | 144 | describe('.performWrite()', function () { 145 | it('should set the ipc config to silent', function () { 146 | // Reset this value as it can persist from other tests 147 | expect(ipc.config.silent).to.be.true; 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /tests/gui/modules/progress-status.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | 19 | import * as progressStatus from '../../../lib/gui/app/modules/progress-status'; 20 | 21 | describe('Browser: progressStatus', function () { 22 | describe('.titleFromFlashState()', function () { 23 | beforeEach(function () { 24 | this.state = { 25 | active: 1, 26 | type: 'flashing', 27 | failed: 0, 28 | percentage: 0, 29 | eta: 15, 30 | speed: 100000000000000, 31 | }; 32 | }); 33 | 34 | it('should report 0% if percentage == 0 but speed != 0', function () { 35 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 36 | '0% Flashing...', 37 | ); 38 | }); 39 | 40 | it('should handle percentage == 0, flashing', function () { 41 | this.state.speed = 0; 42 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 43 | '0% Flashing...', 44 | ); 45 | }); 46 | 47 | it('should handle percentage == 0, verifying', function () { 48 | this.state.speed = 0; 49 | this.state.type = 'verifying'; 50 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 51 | '0% Validating...', 52 | ); 53 | }); 54 | 55 | it('should handle percentage == 50, flashing', function () { 56 | this.state.percentage = 50; 57 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 58 | '50% Flashing...', 59 | ); 60 | }); 61 | 62 | it('should handle percentage == 50, verifying', function () { 63 | this.state.percentage = 50; 64 | this.state.type = 'verifying'; 65 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 66 | '50% Validating...', 67 | ); 68 | }); 69 | 70 | it('should handle percentage == 100, flashing', function () { 71 | this.state.percentage = 100; 72 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 73 | 'Finishing...', 74 | ); 75 | }); 76 | 77 | it('should handle percentage == 100, verifying', function () { 78 | this.state.percentage = 100; 79 | this.state.type = 'verifying'; 80 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 81 | 'Finishing...', 82 | ); 83 | }); 84 | 85 | it('should handle percentage == 100, validating', function () { 86 | this.state.percentage = 100; 87 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 88 | 'Finishing...', 89 | ); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/gui/os/window-progress.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import { assert, spy } from 'sinon'; 19 | 20 | import * as windowProgress from '../../../lib/gui/app/os/window-progress'; 21 | 22 | describe('Browser: WindowProgress', function () { 23 | describe('windowProgress', function () { 24 | describe('given a stubbed current window', function () { 25 | beforeEach(function () { 26 | this.setProgressBarSpy = spy(); 27 | this.setTitleSpy = spy(); 28 | 29 | windowProgress.currentWindow.setProgressBar = this.setProgressBarSpy; 30 | windowProgress.currentWindow.setTitle = this.setTitleSpy; 31 | 32 | this.state = { 33 | active: 1, 34 | type: 'flashing', 35 | failed: 0, 36 | percentage: 85, 37 | speed: 100, 38 | }; 39 | }); 40 | 41 | describe('.set()', function () { 42 | it('should translate 0-100 percentages to 0-1 ranges', function () { 43 | windowProgress.set(this.state); 44 | assert.calledWith(this.setProgressBarSpy, 0.85); 45 | }); 46 | 47 | it('should set 0 given 0', function () { 48 | this.state.percentage = 0; 49 | windowProgress.set(this.state); 50 | assert.calledWith(this.setProgressBarSpy, 0); 51 | }); 52 | 53 | it('should set 1 given 100', function () { 54 | this.state.percentage = 100; 55 | windowProgress.set(this.state); 56 | assert.calledWith(this.setProgressBarSpy, 1); 57 | }); 58 | 59 | it('should throw if given a percentage higher than 100', function () { 60 | this.state.percentage = 101; 61 | const state = this.state; 62 | expect(function () { 63 | windowProgress.set(state); 64 | }).to.throw('Invalid percentage: 101'); 65 | }); 66 | 67 | it('should throw if given a percentage less than 0', function () { 68 | this.state.percentage = -1; 69 | const state = this.state; 70 | expect(function () { 71 | windowProgress.set(state); 72 | }).to.throw('Invalid percentage: -1'); 73 | }); 74 | 75 | it('should set the flashing title', function () { 76 | windowProgress.set(this.state); 77 | assert.calledWith(this.setTitleSpy, ' – 85% Flashing...'); 78 | }); 79 | 80 | it('should set the verifying title', function () { 81 | this.state.type = 'verifying'; 82 | windowProgress.set(this.state); 83 | assert.calledWith(this.setTitleSpy, ' – 85% Validating...'); 84 | }); 85 | 86 | it('should set the starting title', function () { 87 | this.state.percentage = 0; 88 | this.state.speed = 0; 89 | windowProgress.set(this.state); 90 | assert.calledWith(this.setTitleSpy, ' – 0% Flashing...'); 91 | }); 92 | 93 | it('should set the finishing title', function () { 94 | this.state.percentage = 100; 95 | windowProgress.set(this.state); 96 | assert.calledWith(this.setTitleSpy, ' – Finishing...'); 97 | }); 98 | }); 99 | 100 | describe('.clear()', function () { 101 | it('should set -1', function () { 102 | windowProgress.clear(); 103 | assert.calledWith(this.setProgressBarSpy, -1); 104 | }); 105 | 106 | it('should clear the window title', function () { 107 | windowProgress.clear(); 108 | assert.calledWith(this.setTitleSpy, ''); 109 | }); 110 | }); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /tests/gui/os/windows-network-drives.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import { promises as fs } from 'fs'; 19 | import * as os from 'os'; 20 | import { SinonStub, stub } from 'sinon'; 21 | 22 | import * as wnd from '../../../lib/gui/app/os/windows-network-drives'; 23 | 24 | function mockGetWmicOutput() { 25 | return fs.readFile('tests/data/wmic-output.txt', { 26 | encoding: 'ucs2', 27 | }); 28 | } 29 | 30 | describe('Network drives on Windows', () => { 31 | let osPlatformStub: SinonStub; 32 | 33 | before(async () => { 34 | osPlatformStub = stub(os, 'platform'); 35 | osPlatformStub.returns('win32'); 36 | }); 37 | 38 | it('should parse network drive mapping on Windows', async () => { 39 | expect( 40 | await wnd.replaceWindowsNetworkDriveLetter( 41 | 'Z:\\some-folder\\some-file', 42 | mockGetWmicOutput, 43 | ), 44 | ).to.equal('\\\\192.168.1.1\\Publicé\\some-folder\\some-file'); 45 | }); 46 | 47 | after(() => { 48 | osPlatformStub.restore(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/gui/utils/middle-ellipsis.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | 19 | import { middleEllipsis } from '../../../lib/gui/app/utils/middle-ellipsis'; 20 | 21 | describe('Browser: MiddleEllipsis', function () { 22 | describe('.middleEllipsis()', function () { 23 | it('should throw error if limit < 3', function () { 24 | expect(() => { 25 | middleEllipsis('No', 2); 26 | }).to.throw('middleEllipsis: Limit should be at least 3'); 27 | }); 28 | 29 | describe('given the input length is greater than the limit', function () { 30 | it('should always truncate input to an odd length', function () { 31 | const alphabet = 'abcdefghijklmnopqrstuvwxyz'; 32 | expect(middleEllipsis(alphabet, 3)).to.have.lengthOf(3); 33 | expect(middleEllipsis(alphabet, 4)).to.have.lengthOf(3); 34 | expect(middleEllipsis(alphabet, 5)).to.have.lengthOf(5); 35 | expect(middleEllipsis(alphabet, 6)).to.have.lengthOf(5); 36 | }); 37 | }); 38 | 39 | it('should return the input if it is within the bounds of limit', function () { 40 | expect(middleEllipsis('Hello', 10)).to.equal('Hello'); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/gui/window-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "webPreferences": { 3 | "enableRemoteModule": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/shared/messages.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as _ from 'lodash'; 19 | 20 | import * as messages from '../../lib/shared/messages'; 21 | 22 | describe('Shared: Messages', function () { 23 | beforeEach(function () { 24 | this.drives = [ 25 | { 26 | description: 'My Drive', 27 | displayName: '/dev/disk1', 28 | }, 29 | { 30 | description: 'Other Drive', 31 | displayName: '/dev/disk2', 32 | }, 33 | ]; 34 | }); 35 | 36 | it('should contain object properties', function () { 37 | expect(_.every(_.map(messages, _.isPlainObject))).to.be.true; 38 | }); 39 | 40 | it('should contain function properties in each category', function () { 41 | _.each(messages, (category) => { 42 | expect(_.every(_.map(category, _.isFunction))).to.be.true; 43 | }); 44 | }); 45 | 46 | describe('.info', function () { 47 | describe('.flashComplete()', function () { 48 | it('should use singular when there are single results', function () { 49 | const msg = messages.info.flashComplete('image.img', this.drives, { 50 | failed: 1, 51 | successful: 1, 52 | }); 53 | 54 | expect(msg).to.equal( 55 | 'image.img was successfully flashed to 1 target and failed to be flashed to 1 target', 56 | ); 57 | }); 58 | 59 | it('should use plural when there are multiple results', function () { 60 | const msg = messages.info.flashComplete('image.img', this.drives, { 61 | failed: 2, 62 | successful: 2, 63 | }); 64 | 65 | expect(msg).to.equal( 66 | 'image.img was successfully flashed to 2 targets and failed to be flashed to 2 targets', 67 | ); 68 | }); 69 | 70 | it('should not contain failed target part when there are none', function () { 71 | const msg = messages.info.flashComplete('image.img', this.drives, { 72 | failed: 0, 73 | successful: 2, 74 | }); 75 | 76 | expect(msg).to.equal('image.img was successfully flashed to 2 targets'); 77 | }); 78 | 79 | it('should show drive name and description when only target', function () { 80 | const msg = messages.info.flashComplete('image.img', this.drives, { 81 | failed: 0, 82 | successful: 1, 83 | }); 84 | 85 | expect(msg).to.equal( 86 | 'image.img was successfully flashed to My Drive (/dev/disk1)', 87 | ); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('.error', function () { 93 | describe('.flashFailure()', function () { 94 | it('should use plural when there are multiple drives', function () { 95 | const msg = messages.error.flashFailure('image.img', this.drives); 96 | 97 | expect(msg).to.equal( 98 | 'Something went wrong while writing image.img to 2 targets.', 99 | ); 100 | }); 101 | 102 | it('should use singular when there is one drive', function () { 103 | const msg = messages.error.flashFailure('image.img', [this.drives[0]]); 104 | 105 | expect(msg).to.equal( 106 | 'Something went wrong while writing image.img to My Drive (/dev/disk1).', 107 | ); 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /tests/shared/permissions.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as os from 'os'; 19 | import { stub } from 'sinon'; 20 | 21 | import * as permissions from '../../lib/shared/permissions'; 22 | 23 | describe('Shared: permissions', function () { 24 | describe('.createLaunchScript()', function () { 25 | describe('given windows', function () { 26 | beforeEach(function () { 27 | this.osPlatformStub = stub(os, 'platform'); 28 | this.osPlatformStub.returns('win32'); 29 | }); 30 | 31 | afterEach(function () { 32 | this.osPlatformStub.restore(); 33 | }); 34 | 35 | it('should escape environment variables and arguments', function () { 36 | expect( 37 | permissions.createLaunchScript( 38 | 'C:\\Users\\Alice & Bob\'s Laptop\\"what"\\Etcher-ng', 39 | ['"a Laser"', 'arg1', "'&/ ^ \\", '" $ % *'], 40 | { 41 | key: 'value', 42 | key2: ' " \' ^ & = + $ % / \\', 43 | key3: '8', 44 | }, 45 | ), 46 | ).to.equal( 47 | `chcp 65001${os.EOL}` + 48 | `set "key=value"${os.EOL}` + 49 | `set "key2= " ' ^ & = + $ % / \\"${os.EOL}` + 50 | `set "key3=8"${os.EOL}` + 51 | `"C:\\Users\\Alice & Bob's Laptop\\\\"what\\"\\Etcher-ng" "\\"a Laser\\"" "arg1" "'&/ ^ \\" "\\" $ % *"`, 52 | ); 53 | }); 54 | }); 55 | 56 | for (const platform of ['linux', 'darwin']) { 57 | describe(`given ${platform}`, function () { 58 | beforeEach(function () { 59 | this.osPlatformStub = stub(os, 'platform'); 60 | this.osPlatformStub.returns(platform); 61 | }); 62 | 63 | afterEach(function () { 64 | this.osPlatformStub.restore(); 65 | }); 66 | 67 | it('should escape environment variables and arguments', function () { 68 | expect( 69 | permissions.createLaunchScript( 70 | '/home/Alice & Bob\'s Laptop/"what"/Etcher-ng', 71 | ['arg1', "'&/ ^ \\", '" $ % *'], 72 | { 73 | key: 'value', 74 | key2: ' " \' ^ & = + $ % / \\', 75 | key3: '8', 76 | }, 77 | ), 78 | ).to.equal( 79 | `export key='value'${os.EOL}` + 80 | `export key2=' " '\\'' ^ & = + $ % / \\'${os.EOL}` + 81 | `export key3='8'${os.EOL}` + 82 | `'/home/Alice & Bob'\\''s Laptop/"what"/Etcher-ng' 'arg1' ''\\''&/ ^ \\' '" $ % *'`, 83 | ); 84 | }); 85 | }); 86 | } 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /tests/shared/supported-formats.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as _ from 'lodash'; 19 | 20 | import * as supportedFormats from '../../lib/shared/supported-formats'; 21 | 22 | describe('Shared: SupportedFormats', function () { 23 | describe('.looksLikeWindowsImage()', function () { 24 | _.each( 25 | [ 26 | 'C:\\path\\to\\en_windows_10_multiple_editions_version_1607_updated_jan_2017_x64_dvd_9714399.iso', 27 | '/path/to/en_windows_10_multiple_editions_version_1607_updated_jan_2017_x64_dvd_9714399.iso', 28 | '/path/to/Win10_1607_SingleLang_English_x32.iso', 29 | '/path/to/en_winxp_pro_x86_build2600_iso.img', 30 | ], 31 | (imagePath) => { 32 | it(`should return true if filename is ${imagePath}`, function () { 33 | const looksLikeWindowsImage = 34 | supportedFormats.looksLikeWindowsImage(imagePath); 35 | expect(looksLikeWindowsImage).to.be.true; 36 | }); 37 | }, 38 | ); 39 | 40 | _.each( 41 | [ 42 | 'C:\\path\\to\\2017-01-11-raspbian-jessie.img', 43 | '/path/to/2017-01-11-raspbian-jessie.img', 44 | ], 45 | (imagePath) => { 46 | it(`should return false if filename is ${imagePath}`, function () { 47 | const looksLikeWindowsImage = 48 | supportedFormats.looksLikeWindowsImage(imagePath); 49 | expect(looksLikeWindowsImage).to.be.false; 50 | }); 51 | }, 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/shared/units.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import { bytesToMegabytes } from '../../lib/shared/units'; 19 | 20 | describe('Shared: Units', function () { 21 | describe('.bytesToMegabytes()', function () { 22 | it('should convert bytes to megabytes', function () { 23 | expect(bytesToMegabytes(1.2e7)).to.equal(12); 24 | expect(bytesToMegabytes(332000)).to.equal(0.332); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/shared/utils.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | 19 | import * as utils from '../../lib/shared/utils'; 20 | 21 | describe('Shared: Utils', function () { 22 | describe('.isValidPercentage()', function () { 23 | it('should return false if percentage is not a number', function () { 24 | expect(utils.isValidPercentage('50')).to.be.false; 25 | }); 26 | 27 | it('should return false if percentage is null', function () { 28 | expect(utils.isValidPercentage(null)).to.be.false; 29 | }); 30 | 31 | it('should return false if percentage is undefined', function () { 32 | expect(utils.isValidPercentage(undefined)).to.be.false; 33 | }); 34 | 35 | it('should return false if percentage is an integer less than 0', function () { 36 | expect(utils.isValidPercentage(-1)).to.be.false; 37 | }); 38 | 39 | it('should return false if percentage is a float less than 0', function () { 40 | expect(utils.isValidPercentage(-0.1)).to.be.false; 41 | }); 42 | 43 | it('should return true if percentage is 0', function () { 44 | expect(utils.isValidPercentage(0)).to.be.true; 45 | }); 46 | 47 | it('should return true if percentage is an integer greater than 0, but less than 100', function () { 48 | expect(utils.isValidPercentage(50)).to.be.true; 49 | }); 50 | 51 | it('should return true if percentage is a float greater than 0, but less than 100', function () { 52 | expect(utils.isValidPercentage(49.55)).to.be.true; 53 | }); 54 | 55 | it('should return true if percentage is 100', function () { 56 | expect(utils.isValidPercentage(100)).to.be.true; 57 | }); 58 | 59 | it('should return false if percentage is an integer greater than 100', function () { 60 | expect(utils.isValidPercentage(101)).to.be.false; 61 | }); 62 | 63 | it('should return false if percentage is a float greater than 100', function () { 64 | expect(utils.isValidPercentage(100.001)).to.be.false; 65 | }); 66 | }); 67 | 68 | describe('.percentageToFloat()', function () { 69 | it('should throw an error if given a string percentage', function () { 70 | expect(function () { 71 | utils.percentageToFloat('50'); 72 | }).to.throw('Invalid percentage: 50'); 73 | }); 74 | 75 | it('should throw an error if given a null percentage', function () { 76 | expect(function () { 77 | utils.percentageToFloat(null); 78 | }).to.throw('Invalid percentage: null'); 79 | }); 80 | 81 | it('should throw an error if given an undefined percentage', function () { 82 | expect(function () { 83 | utils.percentageToFloat(undefined); 84 | }).to.throw('Invalid percentage: undefined'); 85 | }); 86 | 87 | it('should throw an error if given an integer percentage < 0', function () { 88 | expect(function () { 89 | utils.percentageToFloat(-1); 90 | }).to.throw('Invalid percentage: -1'); 91 | }); 92 | 93 | it('should throw an error if given a float percentage < 0', function () { 94 | expect(function () { 95 | utils.percentageToFloat(-0.1); 96 | }).to.throw('Invalid percentage: -0.1'); 97 | }); 98 | 99 | it('should covert a 0 percentage to 0', function () { 100 | expect(utils.percentageToFloat(0)).to.equal(0); 101 | }); 102 | 103 | it('should covert an integer percentage to a float', function () { 104 | expect(utils.percentageToFloat(50)).to.equal(0.5); 105 | }); 106 | 107 | it('should covert an float percentage to a float', function () { 108 | expect(utils.percentageToFloat(46.54)).to.equal(0.4654); 109 | }); 110 | 111 | it('should covert a 100 percentage to 1', function () { 112 | expect(utils.percentageToFloat(100)).to.equal(1); 113 | }); 114 | 115 | it('should throw an error if given an integer percentage > 100', function () { 116 | expect(function () { 117 | utils.percentageToFloat(101); 118 | }).to.throw('Invalid percentage: 101'); 119 | }); 120 | 121 | it('should throw an error if given a float percentage > 100', function () { 122 | expect(function () { 123 | utils.percentageToFloat(100.01); 124 | }).to.throw('Invalid percentage: 100.01'); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /tests/spectron/runner.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import { platform } from 'os'; 19 | import { Application } from 'spectron'; 20 | import * as electronPath from 'electron'; 21 | 22 | // TODO: spectron fails to start on the CI with: 23 | // Error: Failed to create session. 24 | // unknown error: Chrome failed to start: exited abnormally 25 | if (platform() !== 'darwin') { 26 | describe('Spectron', function () { 27 | // Mainly for CI jobs 28 | this.timeout(40000); 29 | 30 | const app = new Application({ 31 | path: electronPath as unknown as string, 32 | args: ['--no-sandbox', '.'], 33 | }); 34 | 35 | before('app:start', async () => { 36 | await app.start(); 37 | }); 38 | 39 | after('app:stop', async () => { 40 | if (app && app.isRunning()) { 41 | await app.stop(); 42 | } 43 | }); 44 | 45 | describe('Browser Window', () => { 46 | it('should open a browser window', async () => { 47 | // We can't use `isVisible()` here as it won't work inside 48 | // a Windows Docker container, but we can approximate it 49 | // with these set of checks: 50 | const bounds = await app.browserWindow.getBounds(); 51 | expect(bounds.height).to.be.above(0); 52 | expect(bounds.width).to.be.above(0); 53 | expect(await app.browserWindow.isMinimized()).to.be.false; 54 | expect( 55 | (await app.browserWindow.isVisible()) || 56 | (await app.browserWindow.isFocused()), 57 | ).to.be.true; 58 | }); 59 | 60 | it('should set a proper title', async () => { 61 | // @ts-ignore (SpectronClient.getTitle exists) 62 | return expect(await app.client.getTitle()).to.equal('Etcher-ng'); 63 | }); 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es2019", 5 | "typeRoots": ["./node_modules/@types", "./typings"], 6 | "module": "commonjs", 7 | "lib": ["dom", "esnext"], 8 | "declaration": true, 9 | "declarationMap": true, 10 | "jsx": "react", 11 | "pretty": true, 12 | "sourceMap": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "moduleResolution": "node", 18 | "allowSyntheticDefaultImports": true, 19 | "resolveJsonModule": true, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noUnusedLocals": true, 5 | "noUnusedParameters": true, 6 | "resolveJsonModule": true, 7 | "moduleResolution": "node", 8 | "module": "es2015", 9 | "target": "es2019", 10 | "jsx": "react", 11 | "typeRoots": ["./node_modules/@types", "./typings"], 12 | "importHelpers": true, 13 | "allowSyntheticDefaultImports": true, 14 | "lib": ["dom", "esnext"], 15 | "declaration": true, 16 | "declarationMap": true, 17 | "pretty": true, 18 | "sourceMap": true, 19 | "baseUrl": "./src", 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "allowJs": true 23 | }, 24 | "include": [ 25 | "lib/**/*.ts", 26 | "node_modules/electron/**/*.d.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@balena/lint/config/tslint-prettier.json", 3 | "rules": { 4 | "no-floating-promises": false, 5 | "ban-ts-ignore": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /typings/omit-deep-lodash/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'omit-deep-lodash'; 2 | -------------------------------------------------------------------------------- /typings/path-is-inside/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'path-is-inside'; 2 | -------------------------------------------------------------------------------- /typings/pnp-webpack-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pnp-webpack-plugin'; 2 | -------------------------------------------------------------------------------- /typings/resin-corvus/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'resin-corvus/browser'; 2 | -------------------------------------------------------------------------------- /typings/simple-progress-webpack-plugin/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'simple-progress-webpack-plugin'; 2 | -------------------------------------------------------------------------------- /typings/sudo-prompt/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@balena/sudo-prompt'; 2 | -------------------------------------------------------------------------------- /typings/svg/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import { FunctionComponent, SVGProps } from 'react'; 3 | const _: FunctionComponent>; 4 | export = _; 5 | } 6 | -------------------------------------------------------------------------------- /webpack.dev.config.bak: -------------------------------------------------------------------------------- 1 | import configs from './webpack.config'; 2 | import { WebpackOptionsNormalized } from 'webpack'; 3 | import * as fs from 'fs'; 4 | 5 | const [guiConfig, etcherConfig, childWriterConfig] = 6 | configs as unknown as WebpackOptionsNormalized[]; 7 | 8 | configs.forEach((config) => { 9 | config.mode = 'development'; 10 | // @ts-ignore 11 | config.devtool = 'source-map'; 12 | }); 13 | 14 | guiConfig.devServer = { 15 | hot: true, 16 | port: 3030, 17 | }; 18 | 19 | fs.copyFileSync('./lib/gui/app/index.dev.html', './generated/index.html'); 20 | fs.copyFileSync('./lib/gui/app/index.css', './generated/index.css'); 21 | 22 | export default [guiConfig, etcherConfig, childWriterConfig]; 23 | --------------------------------------------------------------------------------