Merge branch 'master' into patch-1
@ -9,5 +9,8 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
@ -222,8 +222,7 @@ rules:
|
||||
- error
|
||||
- max: 1
|
||||
multiline-ternary:
|
||||
- error
|
||||
- never
|
||||
- off
|
||||
newline-per-chained-call:
|
||||
- off
|
||||
no-bitwise:
|
||||
|
7
.gitattributes
vendored
@ -5,7 +5,6 @@
|
||||
*.scss text eol=lf
|
||||
|
||||
# Text files
|
||||
dictionary text
|
||||
Dockerfile* text
|
||||
.dockerignore text
|
||||
.editorconfig text
|
||||
@ -35,8 +34,14 @@ Makefile text
|
||||
*.img binary diff=hex
|
||||
*.iso binary diff=hex
|
||||
*.png binary diff=hex
|
||||
*.bin binary diff=hex
|
||||
*.elf binary diff=hex
|
||||
*.xz binary diff=hex
|
||||
*.zip binary diff=hex
|
||||
*.dtb binary diff=hex
|
||||
*.dtbo binary diff=hex
|
||||
*.dat binary diff=hex
|
||||
*.bin binary diff=hex
|
||||
*.dmg binary diff=hex
|
||||
*.rpi-sdcard binary diff=hex
|
||||
*.foo binary diff=hex
|
||||
|
10
.github/ISSUE_TEMPLATE.md
vendored
@ -1,6 +1,6 @@
|
||||
- **Etcher version:**
|
||||
- **Operating system and architecture:**
|
||||
- **Image flashed:**
|
||||
- **Do you see any meaningful error information in the DevTools?**
|
||||
- **Etcher version:**
|
||||
- **Operating system and architecture:**
|
||||
- **Image flashed:**
|
||||
- **Do you see any meaningful error information in the DevTools?**
|
||||
|
||||
<!-- You can open DevTools by pressing `Ctrl+Alt+I`, or `Cmd+Alt+I` if you're running OS X. -->
|
||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Alt+I` if you're on Mac OS. -->
|
||||
|
59
.resinci.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"node-cli": {
|
||||
"node": "6.1.0",
|
||||
"main": "lib/cli/etcher.js",
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev"
|
||||
]
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev",
|
||||
"libyaml-dev"
|
||||
]
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.resin.etcher",
|
||||
"copyright": "Copyright 2016 Resinio Ltd",
|
||||
"productName": "Etcher",
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools"
|
||||
},
|
||||
"dmg": {
|
||||
"iconSize": 110,
|
||||
"contents": [
|
||||
{
|
||||
"x": 140,
|
||||
"y": 225
|
||||
},
|
||||
{
|
||||
"x": 415,
|
||||
"y": 225,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"width": 540,
|
||||
"height": 405
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
"packageCategory": "utils",
|
||||
"synopsis": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more."
|
||||
},
|
||||
"deb": {
|
||||
"priority": "optional",
|
||||
"depends": [
|
||||
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
.travis.yml
@ -47,18 +47,12 @@ matrix:
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then PATH=/usr/local/opt/ccache/libexec:$PATH; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
|
||||
export HOST_OS="darwin";
|
||||
else
|
||||
export HOST_OS="$TRAVIS_OS_NAME";
|
||||
fi
|
||||
- export HOST_OS="$TRAVIS_OS_NAME";
|
||||
|
||||
install:
|
||||
- travis_wait ./scripts/ci/install.sh -o $HOST_OS -r $TARGET_ARCH
|
||||
- ./scripts/ci/install.sh -o $HOST_OS -r $TARGET_ARCH
|
||||
|
||||
script:
|
||||
- ./scripts/ci/test.sh -o $HOST_OS -r $TARGET_ARCH
|
||||
|
80
CHANGELOG.md
@ -3,6 +3,84 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## v1.3.1 - 2018-01-23
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix "stdout maxBuffer" error on Linux
|
||||
- Fix Etcher not working / crashing on older Windows systems
|
||||
- Fix not all partitions being unmounted after flashing on Linux
|
||||
- Fix selection of images in folders with file extension on Mac OS
|
||||
|
||||
### Misc
|
||||
|
||||
- Update Electron to v1.7.11
|
||||
|
||||
## v1.3.0 - 2018-01-04
|
||||
|
||||
### Features
|
||||
|
||||
- Display connected Compute Modules even if Windows doesn't have the necessary drivers to act on them
|
||||
- Add read/write retry delays with backoff to ...
|
||||
- Add native application menu (which fixes OS native window management shortcuts not working)
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix "Couldn't scan drives" error
|
||||
- Ensure the writer process dies when the GUI application is killed
|
||||
- Run elevated writing process asynchronously on Windows
|
||||
- Fix trailing space in environment variables during Windows elevation
|
||||
- Don't send analytics events when attempting to toggle a disabled drive
|
||||
- Fix handling of transient write errors on Linux (EBUSY)
|
||||
- Fix runaway perl process in drivelist on Mac OS
|
||||
|
||||
### Misc
|
||||
|
||||
- Update Electron from v1.7.9 to v1.7.10
|
||||
- Remove Angular dependency from image-writer
|
||||
|
||||
## v1.2.1 - 2017-12-06
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix handling of temporary read/write errors
|
||||
- Don't send initial Mixpanel events before "Anonymous Tracking" settings are loaded
|
||||
- Fix verification step reading from the cache
|
||||
|
||||
## v1.2.0 - 2017-11-22
|
||||
|
||||
### Features
|
||||
|
||||
- Display actual write speed
|
||||
- Add the progress and status to the window title.
|
||||
- Add a sudo-prompt upon launch on Linux-based systems.
|
||||
- Add optional progress bars to drive-selector drives.
|
||||
- Increase the flashing speed of usbboot discovered devices.
|
||||
- Add eye candy to usbboot initialized devices.
|
||||
- Integrate Raspberry Pi's usbboot technology.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fix bzip2 streaming with the new pipelines
|
||||
- Remove Linux elevation meant for usbboot.
|
||||
- Fix `LIBUSB_ERROR_NO_DEVICE` error at the end of usbboot.
|
||||
- Gracefully handle scenarios where a USB drive is disconnected halfway through the usbboot procedure.
|
||||
- Make sure the progress button is always rounded.
|
||||
- Fix permission denied issues when XDG_RUNTIME_DIR is mounted with the `noexec` option.
|
||||
- Fix Etcher being unable to read certain zip files
|
||||
- Fix "Couldn't scan the drives: An unknown error occurred" error when there is a drive locked with BitLocker.
|
||||
- Fix "Missing state eta" error when speed is zero
|
||||
- Fix "Stuck on Starting..." error
|
||||
- Fix situations where the process would get stuck while flashing
|
||||
|
||||
### Misc
|
||||
|
||||
- Add the Python version (2.7) to the CONTRIBUTING doc.
|
||||
- Remove duplicate debug enabling in usbboot module.
|
||||
- Update Electron to v1.7.9
|
||||
- Retry ejection various times before giving up on Windows.
|
||||
- Try to use `$XDG_RUNTIME_DIR` to extract temporary scripts on GNU/Linux.
|
||||
|
||||
## v1.1.2 - 2017-08-07
|
||||
|
||||
### Features
|
||||
@ -412,7 +490,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
- Fix broken image drag and drop functionality.
|
||||
- Prevent global shortcuts from interferring with another applications.
|
||||
- Prevent re-activating the "Flash" button with the keybaord shortcuts when a flash is already in process.
|
||||
- Prevent re-activating the "Flash" button with the keyboard shortcuts when a flash is already in process.
|
||||
- Fix certain non-removable Windows devices not being filtered out.
|
||||
- Display non-mountable Windows drives in the drive selector.
|
||||
|
||||
|
94
Makefile
@ -2,8 +2,6 @@
|
||||
# Build configuration
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
NPX = ./node_modules/.bin/npx
|
||||
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
BUILD_DIRECTORY ?= dist
|
||||
|
||||
@ -15,6 +13,13 @@ endif
|
||||
|
||||
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
|
||||
|
||||
# See https://github.com/electron/spectron/issues/127
|
||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
||||
|
||||
# See https://stackoverflow.com/a/13468229/1641422
|
||||
SHELL := /bin/bash
|
||||
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Operating system and architecture detection
|
||||
# ---------------------------------------------------------------------
|
||||
@ -162,17 +167,6 @@ endif
|
||||
|
||||
ELECTRON_BUILDER_OPTIONS = --$(TARGET_ARCH_ELECTRON_BUILDER)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Updates
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
DISABLE_UPDATES_ELECTRON_BUILDER_OPTIONS = --extraMetadata.analytics.updates.enabled=false
|
||||
|
||||
ifdef DISABLE_UPDATES
|
||||
$(warning Update notification dialog has been disabled (DISABLE_UPDATES is set))
|
||||
ELECTRON_BUILDER_OPTIONS += $(DISABLE_UPDATES_ELECTRON_BUILDER_OPTIONS)
|
||||
endif
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Analytics
|
||||
# ---------------------------------------------------------------------
|
||||
@ -220,7 +214,7 @@ $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(
|
||||
-x $@ \
|
||||
-t node \
|
||||
-s "$(PLATFORM)"
|
||||
git apply --directory $@/node_modules/lzma-native patches/cli/lzma-native-index-static-addon-require.patch
|
||||
patch --directory=$@ --force --strip=1 --ignore-whitespace < patches/lzma-native-index-static-addon-require.patch
|
||||
cp -r lib $@
|
||||
cp package.json $@
|
||||
|
||||
@ -228,7 +222,7 @@ $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)-app \
|
||||
| $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
cd $< && ../../$(NPX) pkg --output ../../$@/$(ETCHER_CLI_BINARY) -t node6-$(PLATFORM_PKG)-$(TARGET_ARCH) $(ENTRY_POINT_CLI)
|
||||
cd $< && pkg --output ../../$@/$(ETCHER_CLI_BINARY) -t node6-$(PLATFORM_PKG)-$(TARGET_ARCH) $(ENTRY_POINT_CLI)
|
||||
./scripts/build/dependencies-npm-extract-addons.sh \
|
||||
-d $</node_modules \
|
||||
-o $@/node_modules
|
||||
@ -279,18 +273,18 @@ $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(
|
||||
# GUI
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
assets/osx/installer.tiff: assets/osx/installer.png assets/osx/installer@2x.png
|
||||
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
|
||||
tiffutil -cathidpicheck $^ -out $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg: assets/osx/installer.tiff \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg: assets/dmg/background.tiff \
|
||||
| $(BUILD_DIRECTORY)
|
||||
TARGET_ARCH=$(TARGET_ARCH) $(NPX) build --mac dmg $(ELECTRON_BUILDER_OPTIONS) \
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --mac dmg $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=dmg
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip: assets/osx/installer.tiff \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip: assets/dmg/background.tiff \
|
||||
| $(BUILD_DIRECTORY)
|
||||
TARGET_ARCH=$(TARGET_ARCH) $(NPX) build --mac zip $(ELECTRON_BUILDER_OPTIONS) \
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --mac zip $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=zip
|
||||
|
||||
@ -298,19 +292,17 @@ APPLICATION_NAME_ELECTRON = $(APPLICATION_NAME_LOWERCASE)-electron
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
$(NPX) build --linux rpm $(ELECTRON_BUILDER_OPTIONS) \
|
||||
build --linux rpm $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION_REDHAT) \
|
||||
--extraMetadata.packageType=rpm \
|
||||
$(DISABLE_UPDATES_ELECTRON_BUILDER_OPTIONS)
|
||||
--extraMetadata.packageType=rpm
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
$(NPX) build --linux deb $(ELECTRON_BUILDER_OPTIONS) \
|
||||
build --linux deb $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION_DEBIAN) \
|
||||
--extraMetadata.packageType=deb \
|
||||
$(DISABLE_UPDATES_ELECTRON_BUILDER_OPTIONS)
|
||||
--extraMetadata.packageType=deb
|
||||
|
||||
ifeq ($(TARGET_ARCH),x64)
|
||||
ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY = linux-unpacked
|
||||
@ -319,7 +311,7 @@ ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY = linux-$(TARGET_ARCH_ELECTRON_BUILDER
|
||||
endif
|
||||
|
||||
$(BUILD_DIRECTORY)/$(ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY)/$(APPLICATION_NAME_ELECTRON): | $(BUILD_DIRECTORY)
|
||||
$(NPX) build --dir --linux $(ELECTRON_BUILDER_OPTIONS) \
|
||||
build --dir --linux $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=AppImage
|
||||
@ -346,15 +338,20 @@ $(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET
|
||||
-w $(BUILD_TEMPORARY_DIRECTORY) \
|
||||
-o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip: \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage \
|
||||
| $(BUILD_DIRECTORY)
|
||||
./scripts/build/zip-file.sh -f $< -s $(PLATFORM) -o $@
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
TARGET_ARCH=$(TARGET_ARCH) $(NPX) build --win portable $(ELECTRON_BUILDER_OPTIONS) \
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --win portable $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=portable
|
||||
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe: \
|
||||
| $(BUILD_DIRECTORY)
|
||||
TARGET_ARCH=$(TARGET_ARCH) $(NPX) build --win nsis $(ELECTRON_BUILDER_OPTIONS) \
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --win nsis $(ELECTRON_BUILDER_OPTIONS) \
|
||||
--extraMetadata.version=$(APPLICATION_VERSION) \
|
||||
--extraMetadata.packageType=nsis
|
||||
|
||||
@ -371,8 +368,10 @@ TARGETS = \
|
||||
lint-cpp \
|
||||
lint-html \
|
||||
lint-spell \
|
||||
test-spectron \
|
||||
test-gui \
|
||||
test-sdk \
|
||||
test-cli \
|
||||
test \
|
||||
sanity-checks \
|
||||
clean \
|
||||
@ -386,10 +385,10 @@ TARGETS = \
|
||||
electron-develop
|
||||
|
||||
changelog:
|
||||
$(NPX) versionist
|
||||
versionist
|
||||
|
||||
package-electron:
|
||||
TARGET_ARCH=$(TARGET_ARCH) $(NPX) build --dir $(ELECTRON_BUILDER_OPTIONS)
|
||||
TARGET_ARCH=$(TARGET_ARCH) build --dir $(ELECTRON_BUILDER_OPTIONS)
|
||||
|
||||
package-cli: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
|
||||
|
||||
@ -408,7 +407,7 @@ PUBLISH_AWS_S3 += \
|
||||
endif
|
||||
|
||||
ifeq ($(PLATFORM),linux)
|
||||
electron-installer-appimage: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage
|
||||
electron-installer-appimage: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip
|
||||
electron-installer-debian: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
|
||||
electron-installer-redhat: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm
|
||||
cli-installer-tar-gz: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
||||
@ -418,7 +417,7 @@ TARGETS += \
|
||||
electron-installer-redhat \
|
||||
cli-installer-tar-gz
|
||||
PUBLISH_AWS_S3 += \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
|
||||
PUBLISH_BINTRAY_DEBIAN += \
|
||||
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
|
||||
@ -515,13 +514,13 @@ electron-develop:
|
||||
-s "$(PLATFORM)"
|
||||
|
||||
sass:
|
||||
$(NPX) node-sass lib/gui/scss/main.scss > lib/gui/css/main.css
|
||||
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
|
||||
|
||||
lint-js:
|
||||
$(NPX) eslint lib tests scripts bin versionist.conf.js
|
||||
eslint lib tests scripts bin versionist.conf.js
|
||||
|
||||
lint-sass:
|
||||
$(NPX) sass-lint lib/gui/scss
|
||||
sass-lint lib/gui/scss
|
||||
|
||||
lint-cpp:
|
||||
cpplint --recursive src
|
||||
@ -530,24 +529,33 @@ lint-html:
|
||||
node scripts/html-lint.js
|
||||
|
||||
lint-spell:
|
||||
codespell.py \
|
||||
--skip *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,.DS_Store \
|
||||
codespell \
|
||||
--dictionary - \
|
||||
--dictionary dictionary.txt \
|
||||
--skip *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
|
||||
lib tests docs scripts Makefile *.md LICENSE
|
||||
|
||||
lint: lint-js lint-sass lint-cpp lint-html lint-spell
|
||||
|
||||
ELECTRON_MOCHA_OPTIONS=--recursive --reporter spec
|
||||
MOCHA_OPTIONS=--recursive --reporter spec
|
||||
|
||||
test-spectron:
|
||||
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron
|
||||
|
||||
test-gui:
|
||||
$(NPX) electron-mocha $(ELECTRON_MOCHA_OPTIONS) --renderer tests/gui
|
||||
electron-mocha $(MOCHA_OPTIONS) --renderer tests/gui
|
||||
|
||||
test-sdk:
|
||||
$(NPX) electron-mocha $(ELECTRON_MOCHA_OPTIONS) \
|
||||
electron-mocha $(MOCHA_OPTIONS) \
|
||||
tests/shared \
|
||||
tests/child-writer \
|
||||
tests/image-stream
|
||||
|
||||
test: test-gui test-sdk
|
||||
test-cli:
|
||||
mocha $(MOCHA_OPTIONS) \
|
||||
tests/shared \
|
||||
tests/image-stream
|
||||
|
||||
test: test-gui test-sdk test-spectron
|
||||
|
||||
help:
|
||||
@echo "Available targets: $(TARGETS)"
|
||||
|
@ -39,10 +39,6 @@ Installers
|
||||
Refer to the [downloads page][etcher] for the latest pre-made
|
||||
installers for all supported operating systems.
|
||||
|
||||
#### AppImage
|
||||
|
||||
Set executable permissions on the `.AppImage` file and double-click it.
|
||||
|
||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher debian repository:
|
||||
|
20
appveyor.yml
@ -3,6 +3,9 @@
|
||||
|
||||
image: Visual Studio 2015
|
||||
|
||||
# See https://github.com/electron/spectron#on-appveyor
|
||||
os: unstable
|
||||
|
||||
cache:
|
||||
- C:\Users\appveyor\.node-gyp
|
||||
- '%LOCALAPPDATA%\electron\Cache'
|
||||
@ -19,31 +22,32 @@ environment:
|
||||
global:
|
||||
ELECTRON_NO_ATTACH_CONSOLE: true
|
||||
nodejs_version: "6.10.3"
|
||||
matrix:
|
||||
- TARGET_ARCH: x64
|
||||
- TARGET_ARCH: x86
|
||||
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
install:
|
||||
- ps: Update-NodeJsInstallation $env:nodejs_version $env:TARGET_ARCH
|
||||
- ps: Update-NodeJsInstallation $env:nodejs_version $env:Platform
|
||||
- set PATH=C:\Program Files (x86)\Windows Kits\8.1\bin\x86;%PATH%
|
||||
- set PATH=C:\Program Files (x86)\NSIS;%PATH%
|
||||
- set PATH=C:\MinGW\bin;%PATH%
|
||||
- set PATH=C:\MinGW\msys\1.0\bin;%PATH%
|
||||
- bash .\scripts\ci\install.sh -o win32 -r %TARGET_ARCH%
|
||||
- bash .\scripts\ci\install.sh -o win32 -r %Platform%
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- bash .\scripts\ci\test.sh -o win32 -r %TARGET_ARCH%
|
||||
- bash .\scripts\ci\build-installers.sh -o win32 -r %TARGET_ARCH%
|
||||
- bash .\scripts\ci\test.sh -o win32 -r %Platform%
|
||||
- bash .\scripts\ci\build-installers.sh -o win32 -r %Platform%
|
||||
|
||||
deploy_script:
|
||||
- if %APPVEYOR_REPO_BRANCH%==master (bash .\scripts\ci\deploy.sh -o win32 -r %TARGET_ARCH%)
|
||||
- if %APPVEYOR_REPO_BRANCH%==master (bash .\scripts\ci\deploy.sh -o win32 -r %Platform%)
|
||||
|
||||
notifications:
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
BIN
assets/iconset/128x128.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
assets/iconset/16x16.png
Normal file
After Width: | Height: | Size: 881 B |
BIN
assets/iconset/256x256.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
assets/iconset/32x32.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/iconset/48x48.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
assets/iconset/512x512.png
Normal file
After Width: | Height: | Size: 146 KiB |
@ -1,5 +0,0 @@
|
||||
boolen->boolean
|
||||
aknowledge->acknowledge
|
||||
seleted->selected
|
||||
reming->remind
|
||||
locl->local
|
9
dictionary.txt
Normal file
@ -0,0 +1,9 @@
|
||||
boolen->boolean
|
||||
aknowledge->acknowledge
|
||||
seleted->selected
|
||||
reming->remind
|
||||
locl->local
|
||||
subsribe->subscribe
|
||||
unsubsribe->unsubscribe
|
||||
calcluate->calculate
|
||||
dictionaty->dictionary
|
@ -47,9 +47,11 @@ to a raw device and the place where image validation resides. Its main purpose
|
||||
is to abstract the messy details of interacting with raw devices in all major
|
||||
operating systems.
|
||||
|
||||
- [Etcher Image Stream](https://github.com/resin-io-modules/etcher-image-stream)
|
||||
- [Etcher Image Stream](../lib/image-stream)
|
||||
|
||||
The goal of this project is to convert any kind of input into a readable stream
|
||||
> (Moved from a separate repository into the main Etcher codebase)
|
||||
|
||||
This module converts any kind of input into a readable stream
|
||||
representing the image so it can be plugged to [etcher-image-write]. Inputs
|
||||
that this module might handle could be, for example: a simple image file, a URL
|
||||
to an image, a compressed image, an image inside a ZIP archive, etc. Together
|
||||
|
@ -25,7 +25,7 @@ export PATH="$PATH:/opt/etcher-cli"
|
||||
|
||||
- On Windows 10 and Windows 8
|
||||
- Open *Control Panel*
|
||||
- Open *System
|
||||
- Open *System*
|
||||
- Click the *Advanced system settings* link
|
||||
- Click *Environment Variables*
|
||||
- Find the `PATH` environment variable, and click *Edit*
|
||||
|
@ -18,7 +18,7 @@ Developing
|
||||
#### Common
|
||||
|
||||
- [NodeJS](https://nodejs.org) (at least v6)
|
||||
- [Python](https://www.python.org)
|
||||
- [Python 2.7](https://www.python.org)
|
||||
- [jq](https://stedolan.github.io/jq/)
|
||||
- [curl](https://curl.haxx.se/)
|
||||
|
||||
@ -40,6 +40,9 @@ You might need to run this with `sudo` or administrator permissions.
|
||||
C++ 2015` (see http://stackoverflow.com/a/31955339)
|
||||
- [MinGW](http://www.mingw.org)
|
||||
|
||||
You might need to `npm config set msvs_version 2015` for node-gyp to correctly detect
|
||||
the version of Visual Studio you're using (in this example VS2015).
|
||||
|
||||
The following MinGW packages are required:
|
||||
|
||||
- `msys-make`
|
||||
@ -53,6 +56,10 @@ The following MinGW packages are required:
|
||||
- [XCode](https://developer.apple.com/xcode/) or [XCode Command Line Tools],
|
||||
which can be installed by running `xcode-select --install`.
|
||||
|
||||
#### Linux
|
||||
|
||||
- `libudev-dev` for libusb (install with `sudo apt install libudev-dev` for example)
|
||||
|
||||
### Cloning the project
|
||||
|
||||
```sh
|
||||
@ -62,10 +69,7 @@ cd etcher
|
||||
|
||||
### Installing npm dependencies
|
||||
|
||||
**Make sure you have all the pre-requisites listed above installed in your
|
||||
system before running the `install` script.**
|
||||
|
||||
**NOTE:** Please make use of the following scripts to install npm dependencies rather
|
||||
**NOTE:** Please make use of the following command to install npm dependencies rather
|
||||
than simply running `npm install` given that we need to do extra configuration
|
||||
to make sure native dependencies are correctly compiled for Electron, otherwise
|
||||
the application might not run successfully.
|
||||
|
@ -1,52 +1,187 @@
|
||||
Maintaining Etcher
|
||||
==================
|
||||
|
||||
This document is meant to serve as a guide for maintainers to perform common
|
||||
tasks.
|
||||
This document is meant to serve as a guide for maintainers to perform common tasks.
|
||||
|
||||
Preparing a new version
|
||||
-----------------------
|
||||
Releasing
|
||||
---------
|
||||
|
||||
### Release Types
|
||||
|
||||
- **snapshot** (default): A continues snapshot of current master, made by the CI services
|
||||
- **production**: Full releases
|
||||
|
||||
### Flight Plan
|
||||
|
||||
#### Preparation
|
||||
|
||||
- [Prepare the new version](#preparing-a-new-version)
|
||||
- [Generate build artefacts](#generating-binaries) (binaries, archives, etc.)
|
||||
- [Draft a release on GitHub](https://github.com/resin-io/etcher/releases)
|
||||
- Upload build artefacts to GitHub release draft
|
||||
|
||||
#### Testing
|
||||
|
||||
- Test the prepared release and build artefacts properly on **all supported operating systems** to prevent regressions that went uncaught by the CI tests (see [MANUAL-TESTING.md](MANUAL-TESTING.md))
|
||||
- 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
|
||||
|
||||
#### Publishing
|
||||
|
||||
- [Publish release draft on GitHub](https://github.com/resin-io/etcher/releases)
|
||||
- [Post release note to forums](https://forums.resin.io/c/etcher)
|
||||
- [Update the website](https://github.com/resin-io/etcher-homepage)
|
||||
- Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
|
||||
- If regressions arise; pull the release, and release a patched version, else:
|
||||
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
|
||||
- [Upload build artefacts to Amazon S3](#uploading-binaries-to-amazon-s3)
|
||||
- Post changelog with `#release-notes` tag on Flowdock
|
||||
- If this release packs noteworthy major changes:
|
||||
- Write a blog post about it, and / or
|
||||
- Write about it to the Etcher mailing list
|
||||
|
||||
### Preparing a New Version
|
||||
|
||||
- Create & hop onto a new release branch, i.e. `release-1.0.0`
|
||||
- Bump the version number in the `package.json`'s `version` property.
|
||||
|
||||
- Bump the version number in the `npm-shrinkwrap.json`'s `version` property.
|
||||
|
||||
- Add a new entry to `CHANGELOG.md` by running `make CHANGELOG.md`.
|
||||
|
||||
- Re-take `screenshot.png` so it displays the latest version in the bottom
|
||||
right corner.
|
||||
|
||||
- Bump the version number in the `npm-shrinkwrap.json`'s `version` property
|
||||
- Add a new entry to `CHANGELOG.md` by running `make changelog`
|
||||
- Manually revise the `CHANGELOG.md` versionist output
|
||||
- Update `screenshot.png` so it displays the latest version in the bottom
|
||||
right corner
|
||||
- Revise the `updates.semverRange` version in `package.json`
|
||||
- Commit the changes with the version number as the commit title, including the `v` prefix, to `master`. For example:
|
||||
|
||||
- Commit the changes with the version number as the commit title, including the
|
||||
`v` prefix, to `master`. For example:
|
||||
**NOTE:** The version **MUST** be prefixed with a "v"
|
||||
|
||||
```sh
|
||||
```bash
|
||||
git commit -m "v1.0.0" # not 1.0.0
|
||||
```
|
||||
|
||||
- Create an annotated tag for the new version. The commit title should equal
|
||||
the annotated tag name. For example:
|
||||
- Create an annotated tag for the new version. The commit title should equal the annotated tag name. For example:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
git tag -a v1.0.0 -m "v1.0.0"
|
||||
```
|
||||
|
||||
- Push the commit and the annotated tag.
|
||||
|
||||
```sh
|
||||
```bash
|
||||
git push
|
||||
git push --tags
|
||||
```
|
||||
|
||||
Upgrading Electron
|
||||
------------------
|
||||
- Open a pull request against `master` titled "Release v1.0.0"
|
||||
|
||||
- Upgrade the `electron` dependency version in `package.json` to an *exact
|
||||
version* (no `~`, `^`, etc).
|
||||
### Generating binaries
|
||||
|
||||
Dealing with a problematic release
|
||||
----------------------------------
|
||||
**Environment**
|
||||
|
||||
Make sure to set the analytics tokens when generating production release binaries:
|
||||
|
||||
```bash
|
||||
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
|
||||
export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
||||
```
|
||||
|
||||
#### Linux
|
||||
|
||||
##### Clean dist folder
|
||||
|
||||
**NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
|
||||
|
||||
```bash
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make distclean"
|
||||
```
|
||||
|
||||
##### Generating artefacts
|
||||
|
||||
```bash
|
||||
# x64
|
||||
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||
# Build AppImages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||
# Build CLI
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production cli-installer-tar-gz"
|
||||
|
||||
# x86
|
||||
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||
# Build AppImages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||
# Build CLI
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production cli-installer-tar-gz"
|
||||
```
|
||||
|
||||
#### Mac OS
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_NAME` to generate signed binaries on Mac OS.
|
||||
|
||||
**NOTE:** The CLI is not code-signed for either at this time.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the CLI
|
||||
make RELEASE_TYPE=production cli-installer-tar-gz
|
||||
# Build the zip
|
||||
make RELEASE_TYPE=production electron-installer-app-zip
|
||||
# Build the dmg
|
||||
make RELEASE_TYPE=production electron-installer-dmg
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
|
||||
|
||||
**NOTE:**
|
||||
- The CLI is not code-signed for either at this time.
|
||||
- Keep in mind to also generate artefacts for x86, with `TARGET_ARCH=x86`.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the CLI
|
||||
make RELEASE_TYPE=production cli-installer-zip
|
||||
# Build the Portable version
|
||||
make RELEASE_TYPE=production electron-installer-portable
|
||||
# Build the Installer
|
||||
make RELEASE_TYPE=production electron-installer-nsis
|
||||
```
|
||||
|
||||
### Uploading packages to Bintray
|
||||
|
||||
```bash
|
||||
export BINTRAY_USER="username@account"
|
||||
export BINTRAY_API_KEY="youruserapikey"
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "debian" -y "debian" -r "x64" -f "dist/etcher-electron_1.2.1_amd64.deb"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "debian" -y "debian" -r "x86" -f "dist/etcher-electron_1.2.1_i386.deb"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "redhat" -y "redhat" -r "x64" -f "dist/etcher-electron-1.2.1.x86_64.rpm"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "redhat" -y "redhat" -r "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
|
||||
```
|
||||
|
||||
### Uploading binaries to Amazon S3
|
||||
|
||||
```bash
|
||||
export S3_KEY="..."
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/aws-s3.sh -b "resin-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
|
||||
```
|
||||
|
||||
### Dealing with a Problematic Release
|
||||
|
||||
There can be times where a release is accidentally plagued with bugs. If you
|
||||
released a new version and notice the error rates are higher than normal, then
|
||||
@ -63,7 +198,7 @@ single package or two is enough to bring down the whole version.
|
||||
|
||||
Use the following command to delete files from S3:
|
||||
|
||||
```sh
|
||||
```bash
|
||||
aws s3api delete-object --bucket <bucket name> --key <file name>
|
||||
```
|
||||
|
||||
|
115
docs/MANUAL-TESTING.md
Normal file
@ -0,0 +1,115 @@
|
||||
Manual Testing
|
||||
==============
|
||||
|
||||
This document describes a high-level script of manual tests to check for. We
|
||||
should aim to replace items on this list with automated Spectron test cases.
|
||||
|
||||
Image Selection
|
||||
---------------
|
||||
|
||||
- [ ] Cancel image selection dialog
|
||||
- [ ] Select an unbootable image (without a partition table), and expect a
|
||||
sensible warning
|
||||
- [ ] Attempt to select a ZIP archive with more than one image
|
||||
- [ ] Attempt to select a tar archive (with any compression method)
|
||||
- [ ] Change image selection
|
||||
- [ ] Select a Windows image, and expect a sensible warning
|
||||
|
||||
Drive Selection
|
||||
---------------
|
||||
|
||||
- [ ] Open the drive selection modal
|
||||
- [ ] Switch drive selection
|
||||
- [ ] Insert a single drive, and expect auto-selection
|
||||
- [ ] Insert more than one drive, and don't expect auto-selection
|
||||
- [ ] Insert a locked SD Card and expect a warning
|
||||
- [ ] Insert a too small drive and expect a warning
|
||||
- [ ] Put an image into a drive and attempt to flash the image to the drive
|
||||
that contains it
|
||||
- [ ] Attempt to flash a compressed image (for which we can get the
|
||||
uncompressed size) into a drive that is big enough to hold the compressed
|
||||
image, but not big enough to hold the uncompressed version
|
||||
- [ ] Enable "Unsafe Mode" and attempt to select a system drive
|
||||
- [ ] Enable "Unsafe Mode", and if there is only one system drive (and no
|
||||
removable ones), don't expect autoselection
|
||||
|
||||
Image Support
|
||||
-------------
|
||||
|
||||
Run the following tests with and without validation enabled:
|
||||
|
||||
- [ ] Flash an uncompressed image
|
||||
- [ ] Flash a Bzip2 image
|
||||
- [ ] Flash a XZ image
|
||||
- [ ] Flash a ZIP image
|
||||
- [ ] Flash a GZ image
|
||||
- [ ] Flash a DMG image
|
||||
- [ ] Flash an image whose size is not a multiple of 512 bytes
|
||||
- [ ] Flash a compressed image whose size is not a multiple of 512 bytes
|
||||
- [ ] Flash an archive whose image size is not a multiple of 512 bytes
|
||||
- [ ] Flash an archive image containing a logo
|
||||
- [ ] Flash an archive image containing a blockmap file
|
||||
- [ ] Flash an archive image containing a manifest metadata file
|
||||
|
||||
Flashing Process
|
||||
----------------
|
||||
|
||||
- [ ] Unplug the drive during flash or validation
|
||||
- [ ] Click "Flash", cancel elevation dialog, and click "Flash" again
|
||||
- [ ] Start flashing an image, try to close Etcher, cancel the application
|
||||
close warning dialog, and check that Etcher continues to flash the image
|
||||
|
||||
### Child Writer
|
||||
|
||||
- [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and
|
||||
check that the UI reacts appropriately
|
||||
- [ ] Close the application while flashing using the window manager close icon
|
||||
- [ ] Close the application while flashing using the OS keyboard shortcut
|
||||
- [ ] Close the application from the terminal using Ctrl-C while flashing
|
||||
- [ ] Force kill the application (using a process monitor tool, etc)
|
||||
|
||||
In all these cases, the child writer process should not remain alive. Note that
|
||||
in some systems you need to open your process monitor tool of choice with extra
|
||||
permissions to see the elevated child writer process.
|
||||
|
||||
GUI
|
||||
----
|
||||
|
||||
- [ ] Close application from the terminal using Ctrl-C while the application is
|
||||
idle
|
||||
- [ ] Click footer links that take you to an external website
|
||||
- [ ] Attempt to change image or drive selection while flashing
|
||||
- [ ] Go to the settings page while flashing and come back
|
||||
- [ ] Flash consecutive images without closing the application
|
||||
- [ ] Remove the selected drive right before clicking "Flash"
|
||||
- [ ] Minimize the application
|
||||
- [ ] Start the application given no internet connection
|
||||
|
||||
Success Banner
|
||||
--------------
|
||||
|
||||
- [ ] Click an external link on the success banner (with and without internet
|
||||
connection)
|
||||
|
||||
Elevation Prompt
|
||||
----------------
|
||||
|
||||
- [ ] Flash an image as `root`/administrator
|
||||
- [ ] Reject elevation prompt
|
||||
- [ ] Put incorrect elevation prompt password
|
||||
- [ ] Unplug the drive during elevation
|
||||
|
||||
Unmounting
|
||||
----------
|
||||
|
||||
- [ ] Disable unmounting and flash an image
|
||||
- [ ] Flash an image with a file system that is readable by the host OS, and
|
||||
check that is unmounted correctly
|
||||
|
||||
Analytics
|
||||
---------
|
||||
|
||||
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
|
||||
check that no request is sent
|
||||
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
|
||||
F5), and check that initial events are not sent to Mixpanel**
|
@ -13,7 +13,7 @@ mac:
|
||||
icon: assets/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
dmg:
|
||||
background: assets/osx/installer.tiff
|
||||
background: assets/dmg/background.tiff
|
||||
icon: assets/icon.icns
|
||||
iconSize: 110
|
||||
contents:
|
||||
@ -44,8 +44,8 @@ linux:
|
||||
packageCategory: utils
|
||||
executableName: etcher-electron
|
||||
synopsis: Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.
|
||||
icon: assets/iconset
|
||||
deb:
|
||||
icon: assets/icon.png
|
||||
priority: optional
|
||||
depends:
|
||||
- gconf2
|
||||
@ -84,9 +84,6 @@ deb:
|
||||
- libxtst6
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
rpm:
|
||||
icon: assets/icon.png
|
||||
depends:
|
||||
- lsb
|
||||
- libXScrnSaver
|
||||
appImage:
|
||||
icon: assets/icon.png
|
||||
|
339
lib/blobs/usbboot/LICENSE
Normal file
@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
BIN
lib/blobs/usbboot/bcm2708-rpi-0-w.dtb
Normal file
BIN
lib/blobs/usbboot/bcm2708-rpi-b-plus.dtb
Normal file
BIN
lib/blobs/usbboot/bcm2708-rpi-b.dtb
Normal file
BIN
lib/blobs/usbboot/bcm2708-rpi-cm.dtb
Normal file
BIN
lib/blobs/usbboot/bcm2709-rpi-2-b.dtb
Normal file
BIN
lib/blobs/usbboot/bcm2710-rpi-3-b.dtb
Normal file
BIN
lib/blobs/usbboot/bcm2710-rpi-cm3.dtb
Normal file
4
lib/blobs/usbboot/config.txt
Normal file
@ -0,0 +1,4 @@
|
||||
gpu_mem=16
|
||||
dtoverlay=dwc2,dr_mode=peripheral
|
||||
dtparam=act_led_trigger=none
|
||||
dtparam=act_led_activelow=off
|
BIN
lib/blobs/usbboot/kernel.img
Normal file
BIN
lib/blobs/usbboot/kernel7.img
Normal file
BIN
lib/blobs/usbboot/overlays/dwc2.dtbo
Normal file
30
lib/blobs/usbboot/raspberrypi/LICENSE
Normal file
@ -0,0 +1,30 @@
|
||||
Copyright (c) 2006, Broadcom Corporation.
|
||||
Copyright (c) 2015, Raspberry Pi (Trading) Ltd
|
||||
All rights reserved.
|
||||
|
||||
Redistribution. Redistribution and use in binary form, without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* This software may only be used for the purposes of developing for,
|
||||
running or using a Raspberry Pi device.
|
||||
* Redistributions must reproduce the above copyright notice and the
|
||||
following disclaimer in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
* Neither the name of Broadcom Corporation nor the names of its suppliers
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
|
||||
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
BIN
lib/blobs/usbboot/raspberrypi/bootcode.bin
Normal file
BIN
lib/blobs/usbboot/raspberrypi/fixup_cd.dat
Normal file
BIN
lib/blobs/usbboot/raspberrypi/start_cd.elf
Normal file
@ -1,69 +0,0 @@
|
||||
Etcher Child Writer
|
||||
===================
|
||||
|
||||
This module is in charge of dealing with the gory details of elevating and
|
||||
managing the child writer process. As a word of warning, it contains tons of
|
||||
workarounds and "hacks" to deal with platform differences, packaging, and
|
||||
inter-process communication. This empowers us to write this small guide to
|
||||
explain how it works in a more high level manner, hoping to make it easier to
|
||||
grok for contributors.
|
||||
|
||||
The problem
|
||||
-----------
|
||||
|
||||
Elevating a forked process is an easy task. Thanks to the widely available NPM
|
||||
modules to display nice GUI prompt dialogs, elevation is just a matter of
|
||||
executing the process with one of those modules instead of with `child_process`
|
||||
directly.
|
||||
|
||||
The main problems we faced are:
|
||||
|
||||
- The modules that implement elevation provide "execution" support, but don't
|
||||
allow us to fork/spawn the process and consume its `stdout` and `stderr` in a
|
||||
stream fashion. This also means that we can't use the nice `process.send` IPC
|
||||
communication channel directly that `child_process.fork` gives us to send
|
||||
messages back to the parent.
|
||||
|
||||
- Since we can't assume anything from the environment Etcher is running on, we
|
||||
must make use of the same application entry point to execute both the GUI and
|
||||
the CLI code, which starts to get messy once we throw `asar` packaging into
|
||||
the mix.
|
||||
|
||||
- Each elevation mechanism has its quirks, mainly on GNU/Linux. Making sure
|
||||
that the forked process was elevated correctly and could work without issues
|
||||
required various workarounds targeting `pkexec` or `kdesudo`.
|
||||
|
||||
How it works
|
||||
------------
|
||||
|
||||
The Etcher binary runs in CLI or GUI mode depending on an environment variable
|
||||
called `ELECTRON_RUN_AS_NODE`. When this variable is set, it instructs Electron
|
||||
to run as a normal NodeJS process (without Chromium, etc), but still keep any
|
||||
patches applied by Electron, like `asar` support.
|
||||
|
||||
When the Etcher GUI is ran, and the user presses the "Flash!" button, the GUI
|
||||
creates an IPC server, and forks a process called the "writer proxy", passing
|
||||
it all the required information to perform the flashing, such as the image
|
||||
path, the device path, the current settings, etc.
|
||||
|
||||
The writer proxy then checks if its currently elevated, and if not, prompts the
|
||||
user for elevation and re-spawns itself.
|
||||
|
||||
Once the writer proxy has enough permissions to directly access devices, it
|
||||
spawns the Etcher CLI passing the `--robot` option along with all the
|
||||
information gathered before. The `--robot` option basically tells the Etcher
|
||||
CLI to output state information in a way that can be very easily parsed by the
|
||||
parent process.
|
||||
|
||||
The output of the Etcher CLI is then sent to the IPC server that was opened by
|
||||
the GUI, which nicely displays them in the progress bar the user sees.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
There are lots of details we're omitting for the sake of clarity. Feel free to
|
||||
dive in inside the child writer code, which is heavily commented to explain the
|
||||
reasons behind each decision or workaround.
|
||||
|
||||
Don't hesitate in getting in touch if you have any suggestion, or just want to
|
||||
know more!
|
@ -1,100 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* @summary Get the explicit boolean form of an argument
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @description
|
||||
* We refer as "explicit boolean form of an argument" to a boolean
|
||||
* argument in either normal or negated form.
|
||||
*
|
||||
* For example: `--check` and `--no-check`;
|
||||
*
|
||||
* @param {String} argumentName - argument name
|
||||
* @param {Boolean} value - argument value
|
||||
* @returns {String} argument
|
||||
*
|
||||
* @example
|
||||
* console.log(cli.getBooleanArgumentForm('check', true));
|
||||
* > '--check'
|
||||
*
|
||||
* @example
|
||||
* console.log(cli.getBooleanArgumentForm('check', false));
|
||||
* > '--no-check'
|
||||
*/
|
||||
exports.getBooleanArgumentForm = (argumentName, value) => {
|
||||
const prefix = _.attempt(() => {
|
||||
if (!value) {
|
||||
return '--no-'
|
||||
}
|
||||
|
||||
const SHORT_OPTION_LENGTH = 1
|
||||
if (_.size(argumentName) === SHORT_OPTION_LENGTH) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return '--'
|
||||
})
|
||||
|
||||
return prefix + argumentName
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get CLI writer arguments
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.image - image
|
||||
* @param {String} options.device - device
|
||||
* @param {String} options.entryPoint - entry point
|
||||
* @param {Boolean} [options.validateWriteOnSuccess] - validate write on success
|
||||
* @param {Boolean} [options.unmountOnSuccess] - unmount on success
|
||||
* @returns {String[]} arguments
|
||||
*
|
||||
* @example
|
||||
* const argv = cli.getArguments({
|
||||
* image: 'path/to/rpi.img',
|
||||
* device: '/dev/disk2'
|
||||
* entryPoint: 'path/to/app.asar',
|
||||
* validateWriteOnSuccess: true,
|
||||
* unmountOnSuccess: true
|
||||
* });
|
||||
*/
|
||||
exports.getArguments = (options) => {
|
||||
const argv = [
|
||||
options.entryPoint,
|
||||
options.image,
|
||||
'--drive',
|
||||
options.device,
|
||||
|
||||
// Explicitly set the boolean flag in positive
|
||||
// or negative way in order to be on the safe
|
||||
// side in case the Etcher CLI changes the
|
||||
// default value of these options.
|
||||
exports.getBooleanArgumentForm('unmount', options.unmountOnSuccess),
|
||||
exports.getBooleanArgumentForm('check', options.validateWriteOnSuccess)
|
||||
|
||||
]
|
||||
|
||||
return argv
|
||||
}
|
@ -1,217 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const _ = require('lodash')
|
||||
const childProcess = require('child_process')
|
||||
const ipc = require('node-ipc')
|
||||
const rendererUtils = require('./renderer-utils')
|
||||
const cli = require('./cli')
|
||||
const CONSTANTS = require('./constants')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const robot = require('../shared/robot')
|
||||
|
||||
/**
|
||||
* @summary Perform a write
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} image - image
|
||||
* @param {Object} drive - drive
|
||||
* @param {Object} options - options
|
||||
* @returns {EventEmitter} event emitter
|
||||
*
|
||||
* @example
|
||||
* const child = childWriter.write('path/to/rpi.img', {
|
||||
* device: '/dev/disk2'
|
||||
* }, {
|
||||
* validateWriteOnSuccess: true,
|
||||
* unmountOnSuccess: true
|
||||
* });
|
||||
*
|
||||
* child.on('progress', (state) => {
|
||||
* console.log(state);
|
||||
* });
|
||||
*
|
||||
* child.on('error', (error) => {
|
||||
* throw error;
|
||||
* });
|
||||
*
|
||||
* child.on('done', () => {
|
||||
* console.log('Validation was successful!');
|
||||
* });
|
||||
*/
|
||||
exports.write = (image, drive, options) => {
|
||||
const emitter = new EventEmitter()
|
||||
|
||||
const argv = cli.getArguments({
|
||||
entryPoint: rendererUtils.getApplicationEntryPoint(),
|
||||
image,
|
||||
device: drive.device,
|
||||
validateWriteOnSuccess: options.validateWriteOnSuccess,
|
||||
unmountOnSuccess: options.unmountOnSuccess
|
||||
})
|
||||
|
||||
// There might be multiple Etcher instances running at
|
||||
// the same time, therefore we must ensure each IPC
|
||||
// server/client has a different name.
|
||||
process.env.IPC_SERVER_ID = `etcher-server-${process.pid}`
|
||||
process.env.IPC_CLIENT_ID = `etcher-client-${process.pid}`
|
||||
|
||||
ipc.config.id = process.env.IPC_SERVER_ID
|
||||
ipc.config.silent = true
|
||||
ipc.serve()
|
||||
|
||||
/**
|
||||
* @summary Safely terminate the IPC server
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @example
|
||||
* terminateServer();
|
||||
*/
|
||||
const terminateServer = () => {
|
||||
// Turns out we need to destroy all sockets for
|
||||
// the server to actually close. Otherwise, it
|
||||
// just stops receiving any further connections,
|
||||
// but remains open if there are active ones.
|
||||
_.each(ipc.server.sockets, (socket) => {
|
||||
socket.destroy()
|
||||
})
|
||||
|
||||
ipc.server.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Emit an error to the client
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Error} error - error
|
||||
*
|
||||
* @example
|
||||
* emitError(new Error('foo bar'));
|
||||
*/
|
||||
const emitError = (error) => {
|
||||
terminateServer()
|
||||
emitter.emit('error', error)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Bridge robot message to the child writer caller
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {String} message - robot message
|
||||
*
|
||||
* @example
|
||||
* bridgeRobotMessage(robot.buildMessage('foo', {
|
||||
* bar: 'baz'
|
||||
* }));
|
||||
*/
|
||||
const bridgeRobotMessage = (message) => {
|
||||
const parsedMessage = _.attempt(() => {
|
||||
if (robot.isMessage(message)) {
|
||||
return robot.parseMessage(message)
|
||||
}
|
||||
|
||||
// Don't be so strict. If a message doesn't look like
|
||||
// a robot message, then make the child writer log it
|
||||
// for debugging purposes.
|
||||
return robot.parseMessage(robot.buildMessage(robot.COMMAND.LOG, {
|
||||
message
|
||||
}))
|
||||
})
|
||||
|
||||
if (_.isError(parsedMessage)) {
|
||||
emitError(parsedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// These are lighweight accessor methods for
|
||||
// the properties of the parsed message
|
||||
const messageCommand = robot.getCommand(parsedMessage)
|
||||
const messageData = robot.getData(parsedMessage)
|
||||
|
||||
// The error object is decomposed by the CLI for serialisation
|
||||
// purposes. We compose it back to an `Error` here in order
|
||||
// to provide better encapsulation.
|
||||
if (messageCommand === robot.COMMAND.ERROR) {
|
||||
emitError(robot.recomposeErrorMessage(parsedMessage))
|
||||
} else if (messageCommand === robot.COMMAND.LOG) {
|
||||
// If the message data is an object and it contains a
|
||||
// message string then log the message string only.
|
||||
if (_.isPlainObject(messageData) && _.isString(messageData.message)) {
|
||||
console.log(messageData.message)
|
||||
} else {
|
||||
console.log(messageData)
|
||||
}
|
||||
} else {
|
||||
emitter.emit(messageCommand, messageData)
|
||||
}
|
||||
} catch (error) {
|
||||
emitError(error)
|
||||
}
|
||||
}
|
||||
|
||||
ipc.server.on('error', emitError)
|
||||
ipc.server.on('message', bridgeRobotMessage)
|
||||
|
||||
ipc.server.on('start', () => {
|
||||
const child = childProcess.fork(CONSTANTS.WRITER_PROXY_SCRIPT, argv, {
|
||||
silent: true,
|
||||
env: process.env
|
||||
})
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
console.info(`WRITER: ${data.toString()}`)
|
||||
})
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
bridgeRobotMessage(data.toString())
|
||||
|
||||
// This function causes the `close` event to be emitted
|
||||
child.kill()
|
||||
})
|
||||
|
||||
child.on('error', emitError)
|
||||
|
||||
child.on('close', (code) => {
|
||||
terminateServer()
|
||||
|
||||
if (code === EXIT_CODES.CANCELLED) {
|
||||
return emitter.emit('done', {
|
||||
cancelled: true
|
||||
})
|
||||
}
|
||||
|
||||
// We shouldn't emit the `done` event manually here
|
||||
// since the writer process will take care of it.
|
||||
if (code === EXIT_CODES.SUCCESS || code === EXIT_CODES.VALIDATION_ERROR) {
|
||||
return null
|
||||
}
|
||||
|
||||
return emitError(new Error(`Child process exited with error code: ${code}`))
|
||||
})
|
||||
})
|
||||
|
||||
ipc.server.start()
|
||||
|
||||
return emitter
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* This file is only meant to be loaded by the renderer process.
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
const isRunningInAsar = require('electron-is-running-in-asar')
|
||||
const electron = require('electron')
|
||||
const CONSTANTS = require('./constants')
|
||||
|
||||
/**
|
||||
* @summary Get application entry point
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} entry point
|
||||
*
|
||||
* @example
|
||||
* const entryPoint = rendererUtils.getApplicationEntryPoint();
|
||||
*/
|
||||
exports.getApplicationEntryPoint = () => {
|
||||
if (isRunningInAsar()) {
|
||||
return path.join(process.resourcesPath, 'app.asar')
|
||||
}
|
||||
|
||||
const ENTRY_POINT_ARGV_INDEX = 1
|
||||
const relativeEntryPoint = electron.remote.process.argv[ENTRY_POINT_ARGV_INDEX]
|
||||
|
||||
// On GNU/Linux, `pkexec` resolves relative paths
|
||||
// from `/root`, therefore we pass an absolute path,
|
||||
// in order to be on the safe side.
|
||||
return path.join(CONSTANTS.PROJECT_ROOT, relativeEntryPoint)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* @summary Split stringified object lines
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* This function takes special care to not consider new lines
|
||||
* inside the object properties.
|
||||
*
|
||||
* @param {String} lines - lines
|
||||
* @returns {String[]} split lines
|
||||
*
|
||||
* @example
|
||||
* const result = utils.splitObjectLines('{"foo":"bar"}\n{"hello":"Hello\nWorld"}');
|
||||
* console.log(result);
|
||||
*
|
||||
* > [ '{"foo":"bar"}', '{"hello":"Hello\nWorld"}' ]
|
||||
*/
|
||||
exports.splitObjectLines = (lines) => {
|
||||
return _.chain(lines)
|
||||
.split(/((?:[^\n"']|"[^"]*"|'[^']*')+)/)
|
||||
.map(_.trim)
|
||||
.reject(_.isEmpty)
|
||||
.value()
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const childProcess = require('child_process')
|
||||
const ipc = require('node-ipc')
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const utils = require('./utils')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const robot = require('../shared/robot')
|
||||
const permissions = require('../shared/permissions')
|
||||
const packageJSON = require('../../package.json')
|
||||
|
||||
// This script is in charge of spawning the writer process and
|
||||
// ensuring it has the necessary privileges. It might look a bit
|
||||
// complex at first sight, but this is only because elevation
|
||||
// modules don't work in a spawn/fork fashion.
|
||||
//
|
||||
// This script spawns the writer process and redirects its `stdout`
|
||||
// and `stderr` to the parent process using IPC communication,
|
||||
// taking care of the writer elevation as needed.
|
||||
|
||||
/**
|
||||
* @summary The Etcher executable file path
|
||||
* @constant
|
||||
* @private
|
||||
* @type {String}
|
||||
*/
|
||||
const executable = _.first(process.argv)
|
||||
|
||||
/**
|
||||
* @summary The first index that represents an actual option argument
|
||||
* @constant
|
||||
* @private
|
||||
* @type {Number}
|
||||
*
|
||||
* @description
|
||||
* The first arguments are usually the program executable itself, etc.
|
||||
*/
|
||||
const OPTIONS_INDEX_START = 2
|
||||
|
||||
/**
|
||||
* @summary The list of Etcher argument options
|
||||
* @constant
|
||||
* @private
|
||||
* @type {String[]}
|
||||
*/
|
||||
const etcherArguments = process.argv.slice(OPTIONS_INDEX_START)
|
||||
|
||||
permissions.isElevated().then((elevated) => {
|
||||
if (!elevated) {
|
||||
console.log('Attempting to elevate')
|
||||
|
||||
const commandArguments = _.attempt(() => {
|
||||
if (os.platform() === 'linux' && process.env.APPIMAGE && process.env.APPDIR) {
|
||||
// Translate the current arguments to point to the AppImage
|
||||
// Relative paths are resolved from `/tmp/.mount_XXXXXX/usr`
|
||||
const translatedArguments = _.chain(process.argv)
|
||||
.tail()
|
||||
.invokeMap('replace', path.join(process.env.APPDIR, 'usr/'), '')
|
||||
.value()
|
||||
|
||||
return _.concat([ process.env.APPIMAGE ], translatedArguments)
|
||||
}
|
||||
|
||||
return process.argv
|
||||
})
|
||||
|
||||
// For debugging purposes
|
||||
console.log(`Running: ${commandArguments.join(' ')}`)
|
||||
|
||||
return permissions.elevateCommand(commandArguments, {
|
||||
applicationName: packageJSON.displayName,
|
||||
environment: {
|
||||
ELECTRON_RUN_AS_NODE: 1,
|
||||
IPC_SERVER_ID: process.env.IPC_SERVER_ID,
|
||||
IPC_CLIENT_ID: process.env.IPC_CLIENT_ID,
|
||||
|
||||
// This environment variable prevents the AppImages
|
||||
// desktop integration script from presenting the
|
||||
// "installation" dialog.
|
||||
SKIP: 1
|
||||
|
||||
}
|
||||
}).then((results) => {
|
||||
if (results.cancelled) {
|
||||
process.exit(EXIT_CODES.CANCELLED)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Re-spawning with elevation')
|
||||
|
||||
return new Bluebird((resolve, reject) => {
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID
|
||||
ipc.config.silent = true
|
||||
|
||||
// > If set to 0, the client will NOT try to reconnect.
|
||||
// See https://github.com/RIAEvangelist/node-ipc/
|
||||
//
|
||||
// The purpose behind this change is for this process
|
||||
// to emit a "disconnect" event as soon as the GUI
|
||||
// process is closed, so we can kill the CLI as well.
|
||||
ipc.config.stopRetrying = 0
|
||||
|
||||
ipc.connectTo(process.env.IPC_SERVER_ID, () => {
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('error', reject)
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('connect', () => {
|
||||
const child = childProcess.spawn(executable, etcherArguments, {
|
||||
env: {
|
||||
|
||||
// The CLI might call operating system utilities (like `diskutil`),
|
||||
// so we must ensure the `PATH` is inherited.
|
||||
PATH: process.env.PATH,
|
||||
|
||||
ELECTRON_RUN_AS_NODE: 1,
|
||||
ETCHER_CLI_ROBOT: 1,
|
||||
|
||||
// Enable extra logging from mountutils
|
||||
// See https://github.com/resin-io-modules/mountutils
|
||||
MOUNTUTILS_DEBUG: 1
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
ipc.of[process.env.IPC_SERVER_ID].on('disconnect', _.bind(child.kill, child))
|
||||
child.on('error', reject)
|
||||
child.on('close', resolve)
|
||||
|
||||
/**
|
||||
* @summary Emit an object message to the IPC server
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Buffer} data - json message data
|
||||
*
|
||||
* @example
|
||||
* emitMessage(Buffer.from(JSON.stringify({
|
||||
* foo: 'bar'
|
||||
* })));
|
||||
*/
|
||||
const emitMessage = (data) => {
|
||||
// Output from stdout/stderr coming from the CLI might be buffered,
|
||||
// causing several progress lines to come up at once as single message.
|
||||
// Trying to parse multiple JSON objects separated by new lines will
|
||||
// of course make the parser confused, causing errors later on.
|
||||
_.each(utils.splitObjectLines(data.toString()), (object) => {
|
||||
ipc.of[process.env.IPC_SERVER_ID].emit('message', object)
|
||||
})
|
||||
}
|
||||
|
||||
child.stdout.on('data', emitMessage)
|
||||
child.stderr.on('data', emitMessage)
|
||||
})
|
||||
})
|
||||
}).then((exitCode) => {
|
||||
process.exit(exitCode)
|
||||
})
|
||||
}).catch((error) => {
|
||||
robot.printError(error)
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
})
|
@ -9,30 +9,7 @@ This module also has the task of unmounting the drives before and after
|
||||
flashing.
|
||||
|
||||
Notice the Etcher CLI is not worried about elevation, and assumes it has enough
|
||||
permissions to continue, throwing an error otherwise. Consult the
|
||||
[`lib/child-writer`][child-writer] module to understand how elevation works on
|
||||
Etcher.
|
||||
|
||||
The robot option
|
||||
----------------
|
||||
|
||||
Setting the `ETCHER_CLI_ROBOT` environment variable allows other applications
|
||||
to easily consume the output of the Etcher CLI in real-time. When using the
|
||||
`ETCHER_CLI_ROBOT` option, the `--yes` option is implicit, therefore you need
|
||||
to manually specify `--drive`.
|
||||
|
||||
When `ETCHER_CLI_ROBOT` is used, the program will output JSON lines containing
|
||||
the progress state and other useful information. For example:
|
||||
|
||||
```
|
||||
$ sudo ETCHER_CLI_ROBOT=1 etcher image.iso --drive /dev/disk2
|
||||
{"command":"progress","data":{"type":"write","percentage":1,"eta":130,"speed":1703936}}
|
||||
...
|
||||
{"command":"progress","data":{"type":"check","percentage":100,"eta":0,"speed":17180514}}
|
||||
{"command":"done","data":{"sourceChecksum":"27c39a5d"}}
|
||||
```
|
||||
|
||||
See documentation about the robot mode at [`lib/shared/robot`][robot].
|
||||
permissions to continue, throwing an error otherwise.
|
||||
|
||||
Exit codes
|
||||
----------
|
||||
@ -42,5 +19,3 @@ These are documented in [`lib/shared/exit-codes.js`][exit-codes] and are also
|
||||
printed on the Etcher CLI help page.
|
||||
|
||||
[exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/shared/exit-codes.js
|
||||
[robot]: https://github.com/resin-io/etcher/tree/master/lib/shared/robot
|
||||
[child-writer]: https://github.com/resin-io/etcher/tree/master/lib/child-writer
|
||||
|
124
lib/cli/diskpart.js
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2017 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const os = require('os')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const crypto = require('crypto')
|
||||
const childProcess = require('child_process')
|
||||
const debug = require('debug')('etcher:cli:diskpart')
|
||||
const Promise = require('bluebird')
|
||||
const retry = require('bluebird-retry')
|
||||
|
||||
const TMP_RANDOM_BYTES = 6
|
||||
const DISKPART_DELAY = 2000
|
||||
const DISKPART_RETRIES = 5
|
||||
|
||||
/**
|
||||
* @summary Generate a tmp filename with full path of OS' tmp dir
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {String} extension - temporary file extension
|
||||
* @returns {String} filename
|
||||
*
|
||||
* @example
|
||||
* const filename = tmpFilename('.sh');
|
||||
*/
|
||||
const tmpFilename = (extension) => {
|
||||
const random = crypto.randomBytes(TMP_RANDOM_BYTES).toString('hex')
|
||||
const filename = `etcher-diskpart-${random}${extension}`
|
||||
return path.join(os.tmpdir(), filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Run a diskpart script
|
||||
* @param {Array<String>} commands - list of commands to run
|
||||
* @param {Function} callback - callback(error)
|
||||
* @example
|
||||
* runDiskpart(['rescan'], (error) => {
|
||||
* ...
|
||||
* })
|
||||
*/
|
||||
const runDiskpart = (commands, callback) => {
|
||||
if (os.platform() !== 'win32') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
|
||||
const filename = tmpFilename('')
|
||||
const script = commands.join('\r\n')
|
||||
|
||||
fs.writeFile(filename, script, {
|
||||
mode: 0o755
|
||||
}, (writeError) => {
|
||||
debug('write %s:', filename, writeError || 'OK')
|
||||
|
||||
childProcess.exec(`diskpart /s ${filename}`, (execError, stdout, stderr) => {
|
||||
debug('stdout:', stdout)
|
||||
debug('stderr:', stderr)
|
||||
|
||||
fs.unlink(filename, (unlinkError) => {
|
||||
debug('unlink %s:', filename, unlinkError || 'OK')
|
||||
callback(execError)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
/**
|
||||
* @summary Clean a device's partition tables
|
||||
* @param {String} device - device path
|
||||
* @example
|
||||
* diskpart.clean('\\\\.\\PhysicalDrive2')
|
||||
* .then(...)
|
||||
* .catch(...)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
clean (device) {
|
||||
if (os.platform() !== 'win32') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
debug('clean', device)
|
||||
|
||||
const pattern = /PHYSICALDRIVE(\d+)/i
|
||||
|
||||
if (pattern.test(device)) {
|
||||
const deviceId = device.match(pattern).pop()
|
||||
return retry(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
runDiskpart([ `select disk ${deviceId}`, 'clean', 'rescan' ], (error) => {
|
||||
return error ? reject(error) : resolve()
|
||||
})
|
||||
}).delay(DISKPART_DELAY)
|
||||
}, {
|
||||
/* eslint-disable camelcase */
|
||||
max_tries: DISKPART_RETRIES
|
||||
/* eslint-enable camelcase */
|
||||
}).catch((error) => {
|
||||
throw new Error(`Couldn't clean the drive, ${error.failure.message} (code ${error.failure.code})`)
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Invalid device: "${device}"`))
|
||||
}
|
||||
|
||||
}
|
@ -16,16 +16,13 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const Bluebird = require('bluebird')
|
||||
const visuals = require('resin-cli-visuals')
|
||||
const form = require('resin-cli-form')
|
||||
const drivelist = Bluebird.promisifyAll(require('drivelist'))
|
||||
const writer = require('./writer')
|
||||
const utils = require('./utils')
|
||||
const options = require('./options')
|
||||
const robot = require('../shared/robot')
|
||||
const messages = require('../shared/messages')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const errors = require('../shared/errors')
|
||||
@ -61,7 +58,7 @@ permissions.isElevated().then((elevated) => {
|
||||
// If `options.yes` is `false`, pass `null`,
|
||||
// otherwise the question will not be asked because
|
||||
// `false` is a defined value.
|
||||
yes: robot.isEnabled(process.env) || options.yes || null
|
||||
yes: options.yes || null
|
||||
|
||||
}
|
||||
})
|
||||
@ -78,55 +75,23 @@ permissions.isElevated().then((elevated) => {
|
||||
check: new visuals.Progress('Validating')
|
||||
}
|
||||
|
||||
return drivelist.listAsync().then((drives) => {
|
||||
const selectedDrive = _.find(drives, {
|
||||
device: answers.drive
|
||||
})
|
||||
|
||||
if (!selectedDrive) {
|
||||
throw errors.createUserError({
|
||||
title: 'The selected drive was not found',
|
||||
description: `We can't find ${answers.drive} in your system. Did you unplug the drive?`
|
||||
})
|
||||
return writer.writeImage(imagePath, answers.drive, {
|
||||
unmountOnSuccess: options.unmount,
|
||||
validateWriteOnSuccess: options.check
|
||||
}, (state) => {
|
||||
progressBars[state.type].update(state)
|
||||
}).then((results) => {
|
||||
return {
|
||||
imagePath,
|
||||
flash: results
|
||||
}
|
||||
|
||||
return writer.writeImage(imagePath, selectedDrive, {
|
||||
unmountOnSuccess: options.unmount,
|
||||
validateWriteOnSuccess: options.check
|
||||
}, (state) => {
|
||||
if (robot.isEnabled(process.env)) {
|
||||
robot.printMessage('progress', {
|
||||
type: state.type,
|
||||
percentage: Math.floor(state.percentage),
|
||||
eta: state.eta,
|
||||
speed: Math.floor(state.speed)
|
||||
})
|
||||
} else {
|
||||
progressBars[state.type].update(state)
|
||||
}
|
||||
}).then((results) => {
|
||||
return {
|
||||
imagePath,
|
||||
flash: results,
|
||||
drive: selectedDrive
|
||||
}
|
||||
})
|
||||
})
|
||||
}).then((results) => {
|
||||
return Bluebird.try(() => {
|
||||
if (robot.isEnabled(process.env)) {
|
||||
return robot.printMessage('done', {
|
||||
sourceChecksum: results.flash.sourceChecksum
|
||||
})
|
||||
}
|
||||
console.log(messages.info.flashComplete(path.basename(results.imagePath), results.flash.drive))
|
||||
|
||||
console.log(messages.info.flashComplete({
|
||||
drive: results.drive,
|
||||
imageBasename: path.basename(results.imagePath)
|
||||
}))
|
||||
|
||||
if (results.flash.sourceChecksum) {
|
||||
console.log(`Checksum: ${results.flash.sourceChecksum}`)
|
||||
if (results.flash.checksum.crc32) {
|
||||
console.log(`Checksum: ${results.flash.checksum.crc32}`)
|
||||
}
|
||||
|
||||
return Bluebird.resolve()
|
||||
@ -135,10 +100,6 @@ permissions.isElevated().then((elevated) => {
|
||||
})
|
||||
}).catch((error) => {
|
||||
return Bluebird.try(() => {
|
||||
if (robot.isEnabled(process.env)) {
|
||||
return robot.printError(error)
|
||||
}
|
||||
|
||||
utils.printError(error)
|
||||
return Bluebird.resolve()
|
||||
}).then(() => {
|
||||
|
@ -20,7 +20,6 @@ const _ = require('lodash')
|
||||
const fs = require('fs')
|
||||
const yargs = require('yargs')
|
||||
const utils = require('./utils')
|
||||
const robot = require('../shared/robot')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const errors = require('../shared/errors')
|
||||
const packageJSON = require('../../package.json')
|
||||
@ -89,7 +88,7 @@ module.exports = yargs
|
||||
.help()
|
||||
|
||||
// Version option
|
||||
.version(_.constant(packageJSON.version))
|
||||
.version(packageJSON.version)
|
||||
|
||||
// Error reporting
|
||||
.fail((message, error) => {
|
||||
@ -97,13 +96,8 @@ module.exports = yargs
|
||||
title: message
|
||||
})
|
||||
|
||||
if (robot.isEnabled(process.env)) {
|
||||
robot.printError(errorObject)
|
||||
} else {
|
||||
yargs.showHelp()
|
||||
utils.printError(errorObject)
|
||||
}
|
||||
|
||||
yargs.showHelp()
|
||||
utils.printError(errorObject)
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR)
|
||||
})
|
||||
|
||||
@ -123,11 +117,12 @@ module.exports = yargs
|
||||
return true
|
||||
})
|
||||
|
||||
// Assert that if the `yes` flag is provided, the `drive` flag is also provided.
|
||||
.check((argv) => {
|
||||
if (robot.isEnabled(process.env) && !argv.drive) {
|
||||
if (argv.yes && !argv.drive) {
|
||||
throw errors.createUserError({
|
||||
title: 'Missing drive',
|
||||
description: 'You need to explicitly pass a drive when enabling robot mode'
|
||||
description: 'You need to explicitly pass a drive when disabling interactively'
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -16,14 +16,17 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const imageWrite = require('etcher-image-write')
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const fs = Bluebird.promisifyAll(require('fs'))
|
||||
const mountutils = Bluebird.promisifyAll(require('mountutils'))
|
||||
const drivelist = Bluebird.promisifyAll(require('drivelist'))
|
||||
const os = require('os')
|
||||
const imageStream = require('../image-stream')
|
||||
const errors = require('../shared/errors')
|
||||
const constraints = require('../shared/drive-constraints')
|
||||
const ImageWriter = require('../writer')
|
||||
const diskpart = require('./diskpart')
|
||||
|
||||
/**
|
||||
* @summary Timeout, in milliseconds, to wait before unmounting on success
|
||||
@ -37,12 +40,8 @@ const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* See https://github.com/resin-io-modules/etcher-image-write for information
|
||||
* about the `state` object passed to `onProgress` callback.
|
||||
*
|
||||
* @param {String} imagePath - path to image
|
||||
* @param {Object} drive - drive
|
||||
* @param {String} drive - drive
|
||||
* @param {Object} options - options
|
||||
* @param {Boolean} [options.unmountOnSuccess=false] - unmount on success
|
||||
* @param {Boolean} [options.validateWriteOnSuccess=false] - validate write on success
|
||||
@ -52,9 +51,7 @@ const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* writer.writeImage('path/to/image.img', {
|
||||
* device: '/dev/disk2'
|
||||
* }, {
|
||||
* writer.writeImage('path/to/image.img', '/dev/disk2', {
|
||||
* unmountOnSuccess: true,
|
||||
* validateWriteOnSuccess: true
|
||||
* }, (state) => {
|
||||
@ -64,61 +61,86 @@ const UNMOUNT_ON_SUCCESS_TIMEOUT_MS = 2000
|
||||
* });
|
||||
*/
|
||||
exports.writeImage = (imagePath, drive, options, onProgress) => {
|
||||
return Bluebird.try(() => {
|
||||
// Unmounting a drive in Windows means we can't write to it anymore
|
||||
if (os.platform() === 'win32') {
|
||||
return Bluebird.resolve()
|
||||
return drivelist.listAsync().then((drives) => {
|
||||
const selectedDrive = _.find(drives, {
|
||||
device: drive
|
||||
})
|
||||
|
||||
if (!selectedDrive) {
|
||||
throw errors.createUserError({
|
||||
title: 'The selected drive was not found',
|
||||
description: `We can't find ${drive} in your system. Did you unplug the drive?`,
|
||||
code: 'EUNPLUGGED'
|
||||
})
|
||||
}
|
||||
|
||||
return mountutils.unmountDiskAsync(drive.device)
|
||||
}).then(() => {
|
||||
return fs.openAsync(drive.raw, 'rs+')
|
||||
}).then((driveFileDescriptor) => {
|
||||
return imageStream.getFromFilePath(imagePath).then((image) => {
|
||||
if (!constraints.isDriveLargeEnough(drive, image)) {
|
||||
throw errors.createUserError({
|
||||
title: 'The image you selected is too big for this drive',
|
||||
description: 'Please connect a bigger drive and try again'
|
||||
})
|
||||
return selectedDrive
|
||||
}).then((driveObject) => {
|
||||
return Bluebird.try(() => {
|
||||
// Unmounting a drive in Windows means we can't write to it anymore
|
||||
if (os.platform() === 'win32') {
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
return imageWrite.write({
|
||||
fd: driveFileDescriptor,
|
||||
device: drive.raw,
|
||||
size: drive.size
|
||||
}, {
|
||||
stream: image.stream,
|
||||
size: image.size.original
|
||||
}, {
|
||||
check: options.validateWriteOnSuccess,
|
||||
transform: image.transform,
|
||||
bmap: image.bmap,
|
||||
bytesToZeroOutFromTheBeginning: image.bytesToZeroOutFromTheBeginning
|
||||
})
|
||||
}).then((writer) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
writer.on('progress', onProgress)
|
||||
writer.on('error', reject)
|
||||
writer.on('done', resolve)
|
||||
})
|
||||
}).tap(() => {
|
||||
// Make sure the device stream file descriptor is closed
|
||||
// before returning control the the caller. Not closing
|
||||
// the file descriptor (and waiting for it) results in
|
||||
// `EBUSY` errors when attempting to unmount the drive
|
||||
// right afterwards in some Windows 7 systems.
|
||||
return fs.closeAsync(driveFileDescriptor).then(() => {
|
||||
if (!options.unmountOnSuccess) {
|
||||
return Bluebird.resolve()
|
||||
return mountutils.unmountDiskAsync(driveObject.device)
|
||||
}).then(() => {
|
||||
return diskpart.clean(driveObject.device)
|
||||
}).then(() => {
|
||||
/* eslint-disable no-bitwise */
|
||||
const flags = fs.constants.O_RDWR |
|
||||
fs.constants.O_EXCL |
|
||||
fs.constants.O_NONBLOCK |
|
||||
fs.constants.O_SYNC |
|
||||
fs.constants.O_DIRECT
|
||||
/* eslint-enable no-bitwise */
|
||||
|
||||
return fs.openAsync(driveObject.raw, flags)
|
||||
}).then((driveFileDescriptor) => {
|
||||
return imageStream.getFromFilePath(imagePath).then((image) => {
|
||||
if (!constraints.isDriveLargeEnough(driveObject, image)) {
|
||||
throw errors.createUserError({
|
||||
title: 'The image you selected is too big for this drive',
|
||||
description: 'Please connect a bigger drive and try again'
|
||||
})
|
||||
}
|
||||
|
||||
// Closing a file descriptor on a drive containing mountable
|
||||
// partitions causes macOS to mount the drive. If we try to
|
||||
// unmount to quickly, then the drive might get re-mounted
|
||||
// right afterwards.
|
||||
return Bluebird.delay(UNMOUNT_ON_SUCCESS_TIMEOUT_MS)
|
||||
.return(drive.device)
|
||||
.then(mountutils.unmountDiskAsync)
|
||||
const writer = new ImageWriter({
|
||||
image,
|
||||
fd: driveFileDescriptor,
|
||||
path: driveObject.raw,
|
||||
verify: options.validateWriteOnSuccess,
|
||||
checksumAlgorithms: [ 'crc32' ]
|
||||
})
|
||||
|
||||
return writer.write()
|
||||
}).then((writer) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
writer.on('progress', onProgress)
|
||||
writer.on('error', reject)
|
||||
writer.on('finish', (results) => {
|
||||
results.drive = driveObject
|
||||
resolve(results)
|
||||
})
|
||||
})
|
||||
}).tap(() => {
|
||||
// Make sure the device stream file descriptor is closed
|
||||
// before returning control the the caller. Not closing
|
||||
// the file descriptor (and waiting for it) results in
|
||||
// `EBUSY` errors when attempting to unmount the drive
|
||||
// right afterwards in some Windows 7 systems.
|
||||
return fs.closeAsync(driveFileDescriptor).then(() => {
|
||||
if (!options.unmountOnSuccess) {
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
// Closing a file descriptor on a drive containing mountable
|
||||
// partitions causes macOS to mount the drive. If we try to
|
||||
// unmount to quickly, then the drive might get re-mounted
|
||||
// right afterwards.
|
||||
return Bluebird.delay(UNMOUNT_ON_SUCCESS_TIMEOUT_MS)
|
||||
.return(driveObject.device)
|
||||
.then(mountutils.unmountDiskAsync)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -29,24 +29,32 @@ var angular = require('angular')
|
||||
const electron = require('electron')
|
||||
const Bluebird = require('bluebird')
|
||||
const semver = require('semver')
|
||||
const EXIT_CODES = require('../shared/exit-codes')
|
||||
const messages = require('../shared/messages')
|
||||
const s3Packages = require('../shared/s3-packages')
|
||||
const release = require('../shared/release')
|
||||
const store = require('../shared/store')
|
||||
const errors = require('../shared/errors')
|
||||
const packageJSON = require('../../package.json')
|
||||
const flashState = require('../shared/models/flash-state')
|
||||
const EXIT_CODES = require('../../shared/exit-codes')
|
||||
const messages = require('../../shared/messages')
|
||||
const s3Packages = require('../../shared/s3-packages')
|
||||
const release = require('../../shared/release')
|
||||
const store = require('../../shared/store')
|
||||
const errors = require('../../shared/errors')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const flashState = require('../../shared/models/flash-state')
|
||||
const settings = require('./models/settings')
|
||||
const windowProgress = require('./os/window-progress')
|
||||
const analytics = require('./modules/analytics')
|
||||
const updateNotifier = require('./components/update-notifier')
|
||||
const availableDrives = require('../shared/models/available-drives')
|
||||
const selectionState = require('../shared/models/selection-state')
|
||||
const availableDrives = require('../../shared/models/available-drives')
|
||||
const selectionState = require('../../shared/models/selection-state')
|
||||
const driveScanner = require('./modules/drive-scanner')
|
||||
const osDialog = require('./os/dialog')
|
||||
const exceptionReporter = require('./modules/exception-reporter')
|
||||
|
||||
// Enable debug information from all modules that use `debug`
|
||||
// See https://github.com/visionmedia/debug#browser-support
|
||||
//
|
||||
// Enable drivelist debugging information
|
||||
// See https://github.com/resin-io-modules/drivelist
|
||||
electron.remote.process.env.DRIVELIST_DEBUG = /drivelist|^\*$/i.test(electron.remote.process.env.DEBUG) ? '1' : ''
|
||||
window.localStorage.debug = electron.remote.process.env.DEBUG
|
||||
|
||||
const app = angular.module('Etcher', [
|
||||
require('angular-ui-router'),
|
||||
require('angular-ui-bootstrap'),
|
||||
@ -94,108 +102,103 @@ app.run(() => {
|
||||
version: currentVersion
|
||||
})
|
||||
|
||||
settings.load().then(() => {
|
||||
const shouldCheckForUpdates = updateNotifier.shouldCheckForUpdates({
|
||||
currentVersion,
|
||||
lastSleptUpdateNotifier: settings.get('lastSleptUpdateNotifier'),
|
||||
lastSleptUpdateNotifierVersion: settings.get('lastSleptUpdateNotifierVersion')
|
||||
const shouldCheckForUpdates = updateNotifier.shouldCheckForUpdates({
|
||||
currentVersion,
|
||||
lastSleptUpdateNotifier: settings.get('lastSleptUpdateNotifier'),
|
||||
lastSleptUpdateNotifierVersion: settings.get('lastSleptUpdateNotifierVersion')
|
||||
})
|
||||
|
||||
const isStableRelease = release.isStableRelease(currentVersion)
|
||||
const updatesEnabled = settings.get('updatesEnabled')
|
||||
|
||||
if (!shouldCheckForUpdates || !updatesEnabled) {
|
||||
analytics.logEvent('Not checking for updates', {
|
||||
shouldCheckForUpdates,
|
||||
updatesEnabled,
|
||||
stable: isStableRelease
|
||||
})
|
||||
|
||||
const isStableRelease = release.isStableRelease(currentVersion)
|
||||
const updatesEnabled = settings.get('updatesEnabled')
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
if (!shouldCheckForUpdates || !updatesEnabled) {
|
||||
analytics.logEvent('Not checking for updates', {
|
||||
shouldCheckForUpdates,
|
||||
updatesEnabled,
|
||||
stable: isStableRelease
|
||||
const updateSemverRange = packageJSON.updates.semverRange
|
||||
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel')
|
||||
|
||||
analytics.logEvent('Checking for updates', {
|
||||
currentVersion,
|
||||
stable: isStableRelease,
|
||||
updateSemverRange,
|
||||
includeUnstableChannel
|
||||
})
|
||||
|
||||
return s3Packages.getLatestVersion(release.getReleaseType(currentVersion), {
|
||||
range: updateSemverRange,
|
||||
includeUnstableChannel
|
||||
}).then((latestVersion) => {
|
||||
if (semver.gte(currentVersion, latestVersion || '0.0.0')) {
|
||||
analytics.logEvent('Update notification skipped', {
|
||||
reason: 'Latest version'
|
||||
})
|
||||
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
const updateSemverRange = packageJSON.updates.semverRange
|
||||
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel')
|
||||
// In case the internet connection is not good and checking the
|
||||
// latest published version takes too long, only show notify
|
||||
// the user about the new version if he didn't start the flash
|
||||
// process (e.g: selected an image), otherwise such interruption
|
||||
// might be annoying.
|
||||
if (selectionState.hasImage()) {
|
||||
analytics.logEvent('Update notification skipped', {
|
||||
reason: 'Image selected'
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
analytics.logEvent('Checking for updates', {
|
||||
currentVersion,
|
||||
stable: isStableRelease,
|
||||
updateSemverRange,
|
||||
includeUnstableChannel
|
||||
analytics.logEvent('Notifying update', {
|
||||
latestVersion
|
||||
})
|
||||
|
||||
return s3Packages.getLatestVersion(release.getReleaseType(currentVersion), {
|
||||
range: updateSemverRange,
|
||||
includeUnstableChannel
|
||||
}).then((latestVersion) => {
|
||||
if (semver.gte(currentVersion, latestVersion || '0.0.0')) {
|
||||
analytics.logEvent('Update notification skipped', {
|
||||
reason: 'Latest version'
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
return updateNotifier.notify(latestVersion, {
|
||||
allowSleepUpdateCheck: isStableRelease
|
||||
})
|
||||
|
||||
// In case the internet connection is not good and checking the
|
||||
// latest published version takes too long, only show notify
|
||||
// the user about the new version if he didn't start the flash
|
||||
// process (e.g: selected an image), otherwise such interruption
|
||||
// might be annoying.
|
||||
if (selectionState.hasImage()) {
|
||||
analytics.logEvent('Update notification skipped', {
|
||||
reason: 'Image selected'
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
analytics.logEvent('Notifying update', {
|
||||
latestVersion
|
||||
})
|
||||
|
||||
return updateNotifier.notify(latestVersion, {
|
||||
allowSleepUpdateCheck: isStableRelease
|
||||
})
|
||||
|
||||
// If the error is an update user error, then we don't want
|
||||
// to bother users each time they open the app.
|
||||
// See: https://github.com/resin-io/etcher/issues/1525
|
||||
}).catch((error) => {
|
||||
return errors.isUserError(error) && error.code === 'UPDATE_USER_ERROR'
|
||||
}, (error) => {
|
||||
analytics.logEvent('Update check user error', {
|
||||
title: errors.getTitle(error),
|
||||
description: errors.getDescription(error)
|
||||
})
|
||||
// If the error is an update user error, then we don't want
|
||||
// to bother users each time they open the app.
|
||||
// See: https://github.com/resin-io/etcher/issues/1525
|
||||
}).catch((error) => {
|
||||
return errors.isUserError(error) && error.code === 'UPDATE_USER_ERROR'
|
||||
}, (error) => {
|
||||
analytics.logEvent('Update check user error', {
|
||||
title: errors.getTitle(error),
|
||||
description: errors.getDescription(error)
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
||||
|
||||
app.run(() => {
|
||||
store.subscribe(() => {
|
||||
const currentFlashState = flashState.getFlashState()
|
||||
|
||||
// There is usually a short time period between the `isFlashing()`
|
||||
// property being set, and the flashing actually starting, which
|
||||
// might cause some non-sense flashing state logs including
|
||||
// `undefined` values.
|
||||
//
|
||||
// We use the presence of `.eta` to determine that the actual
|
||||
// writing started.
|
||||
if (!flashState.isFlashing() || !currentFlashState.eta) {
|
||||
if (!flashState.isFlashing()) {
|
||||
return
|
||||
}
|
||||
|
||||
analytics.logDebug([
|
||||
`Progress (${currentFlashState.type}):`,
|
||||
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s`,
|
||||
`(eta ${currentFlashState.eta}s)`
|
||||
].join(' '))
|
||||
const currentFlashState = flashState.getFlashState()
|
||||
|
||||
windowProgress.set(currentFlashState.percentage)
|
||||
// NOTE: There is usually a short time period between the `isFlashing()`
|
||||
// property being set, and the flashing actually starting, which
|
||||
// might cause some non-sense flashing state logs including
|
||||
// `undefined` values.
|
||||
analytics.logDebug(
|
||||
`Progress (${currentFlashState.type}): ` +
|
||||
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` +
|
||||
`(eta ${currentFlashState.eta}s)`
|
||||
)
|
||||
|
||||
windowProgress.set(currentFlashState)
|
||||
})
|
||||
})
|
||||
|
||||
app.run(($timeout) => {
|
||||
driveScanner.on('drives', (drives) => {
|
||||
driveScanner.on('devices', (drives) => {
|
||||
// Safely trigger a digest cycle.
|
||||
// In some cases, AngularJS doesn't acknowledge that the
|
||||
// available drives list has changed, and incorrectly
|
||||
@ -342,3 +345,18 @@ app.controller('StateController', function ($rootScope, $scope) {
|
||||
*/
|
||||
this.currentName = null
|
||||
})
|
||||
|
||||
// Handle keyboard shortcut to open the settings
|
||||
app.run(($state) => {
|
||||
electron.ipcRenderer.on('menu:preferences', () => {
|
||||
$state.go('settings')
|
||||
})
|
||||
})
|
||||
|
||||
// Ensure user settings are loaded before
|
||||
// we bootstrap the Angular.js application
|
||||
angular.element(document).ready(() => {
|
||||
settings.load().then(() => {
|
||||
angular.bootstrap(document, [ 'Etcher' ])
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
@ -18,11 +18,11 @@
|
||||
|
||||
const angular = require('angular')
|
||||
const _ = require('lodash')
|
||||
const messages = require('../../../../shared/messages')
|
||||
const constraints = require('../../../../shared/drive-constraints')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const constraints = require('../../../../../shared/drive-constraints')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const availableDrives = require('../../../../shared/models/available-drives')
|
||||
const selectionState = require('../../../../shared/models/selection-state')
|
||||
const availableDrives = require('../../../../../shared/models/available-drives')
|
||||
const selectionState = require('../../../../../shared/models/selection-state')
|
||||
|
||||
module.exports = function (
|
||||
$q,
|
||||
@ -82,10 +82,7 @@ module.exports = function (
|
||||
return WarningModalService.display({
|
||||
confirmationLabel: 'Yes, continue',
|
||||
description: [
|
||||
messages.warning.unrecommendedDriveSize({
|
||||
image: selectionState.getImage(),
|
||||
drive
|
||||
}),
|
||||
messages.warning.unrecommendedDriveSize(selectionState.getImage(), drive),
|
||||
'Are you sure you want to continue?'
|
||||
].join(' ')
|
||||
})
|
||||
@ -107,13 +104,13 @@ module.exports = function (
|
||||
* });
|
||||
*/
|
||||
this.toggleDrive = (drive) => {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: selectionState.isCurrentDrive(drive.device)
|
||||
})
|
||||
|
||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
||||
if (canChangeDriveSelectionState) {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: selectionState.isCurrentDrive(drive.device)
|
||||
})
|
||||
|
||||
selectionState.toggleSetDrive(drive.device)
|
||||
}
|
||||
})
|
||||
@ -251,4 +248,29 @@ module.exports = function (
|
||||
this.getDriveStatuses = this.memoizeImmutableListReference((drive) => {
|
||||
return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage())
|
||||
})
|
||||
|
||||
/**
|
||||
* @summary Keyboard event drive toggling
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Keyboard-event specific entry to the toggleDrive function.
|
||||
*
|
||||
* @param {Object} drive - drive
|
||||
* @param {Object} $event - event
|
||||
*
|
||||
* @example
|
||||
* <div tabindex="1" ng-keypress="this.keyboardToggleDrive(drive, $event)">
|
||||
* Tab-select me and press enter or space!
|
||||
* </div>
|
||||
*/
|
||||
this.keyboardToggleDrive = (drive, $event) => {
|
||||
console.log($event.keyCode)
|
||||
const ENTER = 13
|
||||
const SPACE = 32
|
||||
if (_.includes([ ENTER, SPACE ], $event.keyCode)) {
|
||||
this.toggleDrive(drive)
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
.modal-drive-selector-modal .modal-content {
|
||||
width: 300px;
|
||||
width: 315px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
.modal-drive-selector-modal {
|
||||
|
||||
.list-group-item-footer {
|
||||
.list-group-item-footer:has(span) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@ -52,12 +52,16 @@
|
||||
border-color: darken($palette-theme-light-background, 7%);
|
||||
padding: 12px 0;
|
||||
|
||||
> .tick {
|
||||
font-size: 11px;
|
||||
.list-group-item-section-expanded {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
:first-child {
|
||||
flex-grow: 1;
|
||||
.list-group-item-section + .list-group-item-section {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
> .tick {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@ -67,6 +71,26 @@
|
||||
&[disabled] .list-group-item-heading {
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
}
|
||||
|
||||
progress {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 2.5px;
|
||||
border: none;
|
||||
border-radius: 50% 50%;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
background-color: $palette-theme-default-background;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
||||
background-color: $palette-theme-primary-background;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.list-group-item-heading {
|
@ -1,6 +1,6 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Select a Drive</h4>
|
||||
<button class="close" ng-click="modal.closeModal()">×</button>
|
||||
<button tabindex="14" class="close" ng-click="modal.closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
@ -9,9 +9,19 @@
|
||||
ng-disabled="!modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
||||
ng-dblclick="modal.selectDriveAndClose(drive)"
|
||||
ng-click="modal.toggleDrive(drive)">
|
||||
<div>
|
||||
<h4 class="list-group-item-heading">{{ drive.description }} -
|
||||
<span class="word-keep">{{ drive.size | closestUnit }}</span>
|
||||
<img class="list-group-item-section" alt="Drive device type logo"
|
||||
ng-if="drive.icon"
|
||||
ng-src="./assets/{{drive.icon}}.svg"
|
||||
width="25"
|
||||
height="30">
|
||||
<div
|
||||
class="list-group-item-section list-group-item-section-expanded"
|
||||
tabindex="{{ 15 + $index }}"
|
||||
ng-keypress="modal.keyboardToggleDrive(drive, $event)">
|
||||
|
||||
<h4 class="list-group-item-heading">{{ drive.description }}
|
||||
<span class="word-keep"
|
||||
ng-show="drive.size"> - {{ drive.size | closestUnit }}</span>
|
||||
</h4>
|
||||
<p class="list-group-item-text">{{ drive.displayName }}</p>
|
||||
|
||||
@ -26,8 +36,9 @@
|
||||
</span>
|
||||
|
||||
</footer>
|
||||
<progress ng-if="drive.progress" value="{{ drive.progress }}" max="100"></progress>
|
||||
</div>
|
||||
<span class="tick tick--success"
|
||||
<span class="list-group-item-section tick tick--success"
|
||||
ng-show="modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
||||
ng-disabled="!modal.state.isCurrentDrive(drive.device)"></span>
|
||||
</li>
|
||||
@ -43,6 +54,7 @@
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="button button-primary button-block"
|
||||
tabindex="{{ 15 + modal.getDrives().length }}"
|
||||
ng-click="modal.closeModal()"
|
||||
ng-disabled="!modal.state.hasDrive()">Continue</button>
|
||||
</div>
|
@ -16,8 +16,8 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const flashState = require('../../../../shared/models/flash-state')
|
||||
const selectionState = require('../../../../shared/models/selection-state')
|
||||
const flashState = require('../../../../../shared/models/flash-state')
|
||||
const selectionState = require('../../../../../shared/models/selection-state')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
module.exports = function (WarningModalService) {
|
@ -47,6 +47,8 @@ $progress-button-stripes-animation-duration: 1s;
|
||||
.progress-button {
|
||||
@extend .button;
|
||||
@extend .button-primary;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&[active="true"] {
|
||||
background-color: $palette-theme-warning-background;
|
@ -25,8 +25,8 @@ const react = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const react2angular = require('react2angular').react2angular
|
||||
const analytics = require('../modules/analytics')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const robot = require('../../shared/robot')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
const robot = require('../../../shared/robot')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.SafeWebview'
|
||||
const angularSafeWebview = angular.module(MODULE_NAME, [])
|
@ -21,9 +21,9 @@ const Bluebird = require('bluebird')
|
||||
const _ = require('lodash')
|
||||
const settings = require('../models/settings')
|
||||
const analytics = require('../modules/analytics')
|
||||
const units = require('../../shared/units')
|
||||
const release = require('../../shared/release')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const units = require('../../../shared/units')
|
||||
const release = require('../../../shared/release')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
|
||||
/**
|
||||
* @summary The number of days the update notifier can be put to sleep
|
@ -3,7 +3,9 @@
|
||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||
<span>Attention</span>
|
||||
</h4>
|
||||
<button class="close" ng-click="modal.reject()">×</button>
|
||||
<button class="close"
|
||||
tabindex="11"
|
||||
ng-click="modal.reject()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
@ -13,8 +15,10 @@
|
||||
<div class="modal-footer">
|
||||
<div class="modal-menu">
|
||||
<button class="button button-danger button-block"
|
||||
tabindex="13"
|
||||
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
||||
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
||||
tabindex="12"
|
||||
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
||||
</div>
|
||||
</div>
|
@ -1,34 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Etcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../node_modules/flexboxgrid/dist/flexboxgrid.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/desktop.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/angular.css">
|
||||
|
||||
<!-- Enable debug information from all modules that use `debug` -->
|
||||
<!-- See https://github.com/visionmedia/debug#browser-support -->
|
||||
<script>
|
||||
// Enable drivelist debugging information
|
||||
// See https://github.com/resin-io-modules/drivelist
|
||||
process.env['DRIVELIST_DEBUG'] = /drivelist|^\*$/i.test(process.env['DEBUG']) ? '1' : '';
|
||||
window.localStorage.debug = process.env['DEBUG'];
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="../../../node_modules/flexboxgrid/dist/flexboxgrid.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/main.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/desktop.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/angular.css">
|
||||
<script src="./app.js"></script>
|
||||
</head>
|
||||
<body ng-app="Etcher">
|
||||
<body>
|
||||
<header class="section-header" ng-controller="HeaderController as header">
|
||||
<button class="button button-link" ng-click="header.openHelpPage()">
|
||||
<button class="button button-link"
|
||||
ng-click="header.openHelpPage()"
|
||||
tabindex="4">
|
||||
<span class="glyphicon glyphicon-question-sign"></span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link" ui-sref="settings" hide-if-state="settings">
|
||||
<button class="button button-link"
|
||||
ui-sref="settings"
|
||||
hide-if-state="settings"
|
||||
tabindex="5">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link" ui-sref="main" show-if-state="settings">
|
||||
<button class="button button-link"
|
||||
tabindex="5"
|
||||
ui-sref="main"
|
||||
show-if-state="settings">
|
||||
<span class="glyphicon glyphicon-chevron-left"></span> Back
|
||||
</button>
|
||||
</header>
|
||||
@ -37,23 +36,28 @@
|
||||
|
||||
<footer class="section-footer" ng-controller="StateController as state"
|
||||
ng-hide="state.currentName === 'success'">
|
||||
<span os-open-external="https://etcher.io?ref=etcher_footer">
|
||||
<svg-icon path="'../assets/etcher.svg'"
|
||||
<span os-open-external="https://etcher.io?ref=etcher_footer"
|
||||
tabindex="100">
|
||||
<svg-icon path="'../../assets/etcher.svg'"
|
||||
width="'83px'"
|
||||
height="'13px'"></svg-icon>
|
||||
</span>
|
||||
|
||||
<span class="caption">
|
||||
is <span class="caption" os-open-external="https://github.com/resin-io/etcher">an open source project</span> by
|
||||
is <span class="caption"
|
||||
tabindex="101"
|
||||
os-open-external="https://github.com/resin-io/etcher">an open source project</span> by
|
||||
</span>
|
||||
|
||||
<span os-open-external="https://resin.io?ref=etcher">
|
||||
<svg-icon path="'../assets/resin.svg'"
|
||||
<span os-open-external="https://resin.io?ref=etcher"
|
||||
tabindex="102">
|
||||
<svg-icon path="'../../assets/resin.svg'"
|
||||
width="'79px'"
|
||||
height="'23px'"></svg-icon>
|
||||
</span>
|
||||
|
||||
<span class="caption footer-right"
|
||||
tabindex="103"
|
||||
manifest-bind="version"
|
||||
os-open-external="https://github.com/resin-io/etcher/blob/master/CHANGELOG.md"></span>
|
||||
</footer>
|
@ -23,8 +23,8 @@
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const localSettings = require('./local-settings')
|
||||
const store = require('../../shared/store')
|
||||
const errors = require('../../shared/errors')
|
||||
const store = require('../../../shared/store')
|
||||
const errors = require('../../../shared/errors')
|
||||
|
||||
/**
|
||||
* @summary Set a settings object
|
@ -18,7 +18,7 @@
|
||||
|
||||
const _ = require('lodash')
|
||||
const resinCorvus = require('resin-corvus/browser')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
const settings = require('../models/settings')
|
||||
|
||||
resinCorvus.install({
|
54
lib/gui/app/modules/drive-scanner.js
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const fs = Bluebird.promisifyAll(require('fs'))
|
||||
const path = require('path')
|
||||
const settings = require('../models/settings')
|
||||
const SDK = require('../../../shared/sdk')
|
||||
|
||||
/**
|
||||
* @summary The Etcher "blobs" directory path
|
||||
* @type {String}
|
||||
* @constant
|
||||
*/
|
||||
const BLOBS_DIRECTORY = path.join(__dirname, '..', '..', 'blobs')
|
||||
|
||||
const scanner = SDK.createScanner({
|
||||
standard: {
|
||||
get includeSystemDrives () {
|
||||
return settings.get('unsafeMode')
|
||||
}
|
||||
},
|
||||
usbboot: {
|
||||
readFile: (name) => {
|
||||
const isRaspberryPi = _.includes([
|
||||
'bootcode.bin',
|
||||
'start_cd.elf',
|
||||
'fixup_cd.dat'
|
||||
], name)
|
||||
|
||||
const blobPath = isRaspberryPi ? path.join('raspberrypi', name) : name
|
||||
|
||||
return fs.readFileAsync(path.join(BLOBS_DIRECTORY, 'usbboot', blobPath))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = scanner
|
282
lib/gui/app/modules/image-writer.js
Normal file
@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
const ipc = require('node-ipc')
|
||||
const isRunningInAsar = require('electron-is-running-in-asar')
|
||||
const electron = require('electron')
|
||||
const settings = require('../models/settings')
|
||||
const flashState = require('../../../shared/models/flash-state')
|
||||
const errors = require('../../../shared/errors')
|
||||
const permissions = require('../../../shared/permissions')
|
||||
const windowProgress = require('../os/window-progress')
|
||||
const analytics = require('../modules/analytics')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
|
||||
/**
|
||||
* @summary Get application entry point
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @returns {String} entry point
|
||||
*
|
||||
* @example
|
||||
* const entryPoint = imageWriter.getApplicationEntryPoint()
|
||||
*/
|
||||
const getApplicationEntryPoint = () => {
|
||||
if (isRunningInAsar()) {
|
||||
return path.join(process.resourcesPath, 'app.asar')
|
||||
}
|
||||
|
||||
const ENTRY_POINT_ARGV_INDEX = 1
|
||||
const relativeEntryPoint = electron.remote.process.argv[ENTRY_POINT_ARGV_INDEX]
|
||||
|
||||
const PROJECT_ROOT = path.join(__dirname, '..', '..', '..', '..')
|
||||
return path.resolve(PROJECT_ROOT, relativeEntryPoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Perform write operation
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @description
|
||||
* This function is extracted for testing purposes.
|
||||
*
|
||||
* @param {String} image - image path
|
||||
* @param {Object} drive - drive
|
||||
* @param {Function} onProgress - in progress callback (state)
|
||||
*
|
||||
* @fulfil {Object} - flash results
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* imageWriter.performWrite('path/to/image.img', {
|
||||
* device: '/dev/disk2'
|
||||
* }, (state) => {
|
||||
* console.log(state.percentage)
|
||||
* })
|
||||
*/
|
||||
exports.performWrite = (image, drive, onProgress) => {
|
||||
// There might be multiple Etcher instances running at
|
||||
// the same time, therefore we must ensure each IPC
|
||||
// server/client has a different name.
|
||||
const IPC_SERVER_ID = `etcher-server-${process.pid}`
|
||||
const IPC_CLIENT_ID = `etcher-client-${process.pid}`
|
||||
|
||||
ipc.config.id = IPC_SERVER_ID
|
||||
ipc.config.socketRoot = path.join(process.env.XDG_RUNTIME_DIR || os.tmpdir(), path.sep)
|
||||
|
||||
// NOTE: Ensure this isn't disabled, as it will cause
|
||||
// the stdout maxBuffer size to be exceeded when flashing
|
||||
ipc.config.silent = true
|
||||
ipc.serve()
|
||||
|
||||
/**
|
||||
* @summary Safely terminate the IPC server
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @example
|
||||
* terminateServer()
|
||||
*/
|
||||
const terminateServer = () => {
|
||||
// Turns out we need to destroy all sockets for
|
||||
// the server to actually close. Otherwise, it
|
||||
// just stops receiving any further connections,
|
||||
// but remains open if there are active ones.
|
||||
_.each(ipc.server.sockets, (socket) => {
|
||||
socket.destroy()
|
||||
})
|
||||
|
||||
ipc.server.stop()
|
||||
}
|
||||
|
||||
return new Bluebird((resolve, reject) => {
|
||||
ipc.server.on('error', (error) => {
|
||||
terminateServer()
|
||||
const errorObject = _.isError(error) ? error : errors.fromJSON(error)
|
||||
reject(errorObject)
|
||||
})
|
||||
|
||||
ipc.server.on('log', (message) => {
|
||||
console.log(message)
|
||||
})
|
||||
|
||||
const flashResults = {}
|
||||
ipc.server.on('done', (results) => {
|
||||
_.merge(flashResults, results)
|
||||
})
|
||||
|
||||
ipc.server.on('state', onProgress)
|
||||
|
||||
const argv = _.attempt(() => {
|
||||
const entryPoint = getApplicationEntryPoint()
|
||||
|
||||
// AppImages run over FUSE, so the files inside the mount point
|
||||
// can only be accessed by the user that mounted the AppImage.
|
||||
// This means we can't re-spawn Etcher as root from the same
|
||||
// mount-point, and as a workaround, we re-mount the original
|
||||
// AppImage as root.
|
||||
if (os.platform() === 'linux' && process.env.APPIMAGE && process.env.APPDIR) {
|
||||
return [
|
||||
process.env.APPIMAGE,
|
||||
|
||||
// Executing the AppImage with ELECTRON_RUN_AS_NODE opens
|
||||
// the Node.js REPL without loading the default entry point.
|
||||
// As a workaround, we can pass the path to the file we want
|
||||
// to load, relative to the usr/ directory of the mounted
|
||||
// AppImage.
|
||||
_.replace(entryPoint, path.join(process.env.APPDIR, 'usr/'), '')
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
_.first(process.argv),
|
||||
entryPoint
|
||||
]
|
||||
})
|
||||
|
||||
ipc.server.on('start', () => {
|
||||
console.log(`Elevating command: ${_.join(argv, ' ')}`)
|
||||
|
||||
permissions.elevateCommand(argv, {
|
||||
applicationName: packageJSON.displayName,
|
||||
environment: {
|
||||
IPC_SERVER_ID,
|
||||
IPC_CLIENT_ID,
|
||||
IPC_SOCKET_ROOT: ipc.config.socketRoot,
|
||||
ELECTRON_RUN_AS_NODE: 1,
|
||||
|
||||
// Casting to Number nicely converts booleans to 0 or 1.
|
||||
OPTION_VALIDATE: Number(settings.get('validateWriteOnSuccess')),
|
||||
OPTION_UNMOUNT: Number(settings.get('unmountOnSuccess')),
|
||||
|
||||
OPTION_IMAGE: image,
|
||||
OPTION_DEVICE: drive.device,
|
||||
|
||||
// This environment variable prevents the AppImages
|
||||
// desktop integration script from presenting the
|
||||
// "installation" dialog
|
||||
SKIP: 1
|
||||
}
|
||||
}).then((results) => {
|
||||
flashResults.cancelled = results.cancelled
|
||||
console.log('Flash results', flashResults)
|
||||
|
||||
// This likely means the child died halfway through
|
||||
if (!flashResults.cancelled && !flashResults.bytesWritten) {
|
||||
throw errors.createUserError({
|
||||
title: 'The writer process ended unexpectedly',
|
||||
description: 'Please try again, and contact the Etcher team if the problem persists',
|
||||
code: 'ECHILDDIED'
|
||||
})
|
||||
}
|
||||
|
||||
return resolve(flashResults)
|
||||
}).catch((error) => {
|
||||
// This happens when the child is killed using SIGKILL
|
||||
const SIGKILL_EXIT_CODE = 137
|
||||
if (error.code === SIGKILL_EXIT_CODE) {
|
||||
error.code = 'ECHILDDIED'
|
||||
}
|
||||
|
||||
return reject(error)
|
||||
}).finally(() => {
|
||||
console.log('Terminating IPC server')
|
||||
terminateServer()
|
||||
})
|
||||
})
|
||||
|
||||
ipc.server.start()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Flash an image to a drive
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* This function will update `imageWriter.state` with the current writing state.
|
||||
*
|
||||
* @param {String} image - image path
|
||||
* @param {Object} drive - drive
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* imageWriter.flash('foo.img', {
|
||||
* device: '/dev/disk2'
|
||||
* }).then(() => {
|
||||
* console.log('Write completed!')
|
||||
* })
|
||||
*/
|
||||
exports.flash = (image, drive) => {
|
||||
if (flashState.isFlashing()) {
|
||||
return Bluebird.reject(new Error('There is already a flash in progress'))
|
||||
}
|
||||
|
||||
flashState.setFlashingFlag()
|
||||
|
||||
const analyticsData = {
|
||||
image,
|
||||
drive,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess')
|
||||
}
|
||||
|
||||
analytics.logEvent('Flash', analyticsData)
|
||||
|
||||
return exports.performWrite(image, drive, (state) => {
|
||||
flashState.setProgressState(state)
|
||||
}).then(flashState.unsetFlashingFlag).then(() => {
|
||||
if (flashState.wasLastFlashCancelled()) {
|
||||
analytics.logEvent('Elevation cancelled', analyticsData)
|
||||
} else {
|
||||
analytics.logEvent('Done', analyticsData)
|
||||
}
|
||||
}).catch((error) => {
|
||||
flashState.unsetFlashingFlag({
|
||||
errorCode: error.code
|
||||
})
|
||||
|
||||
if (error.code === 'EVALIDATION') {
|
||||
analytics.logEvent('Validation error', analyticsData)
|
||||
} else if (error.code === 'EUNPLUGGED') {
|
||||
analytics.logEvent('Drive unplugged', analyticsData)
|
||||
} else if (error.code === 'EIO') {
|
||||
analytics.logEvent('Input/output error', analyticsData)
|
||||
} else if (error.code === 'ENOSPC') {
|
||||
analytics.logEvent('Out of space', analyticsData)
|
||||
} else if (error.code === 'ECHILDDIED') {
|
||||
analytics.logEvent('Child died unexpectedly', analyticsData)
|
||||
} else {
|
||||
analytics.logEvent('Flash error', _.merge({
|
||||
error: errors.toJSON(error)
|
||||
}, analyticsData))
|
||||
}
|
||||
|
||||
return Bluebird.reject(error)
|
||||
}).finally(() => {
|
||||
windowProgress.clear()
|
||||
})
|
||||
}
|
63
lib/gui/app/modules/progress-status.js
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2017 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const settings = require('../models/settings')
|
||||
const utils = require('../../../shared/utils')
|
||||
|
||||
/**
|
||||
* @summary Make the progress status subtitle string
|
||||
*
|
||||
* @param {Object} state - flashing metadata
|
||||
*
|
||||
* @returns {String}
|
||||
*
|
||||
* @example
|
||||
* const status = progressStatus.fromFlashState({
|
||||
* type: 'write',
|
||||
* percentage: 55,
|
||||
* speed: 2049
|
||||
* })
|
||||
*
|
||||
* console.log(status)
|
||||
* // '55% Flashing'
|
||||
*/
|
||||
exports.fromFlashState = (state) => {
|
||||
const isChecking = state.type === 'check'
|
||||
const shouldValidate = settings.get('validateWriteOnSuccess')
|
||||
const shouldUnmount = settings.get('unmountOnSuccess')
|
||||
|
||||
if (state.percentage === utils.PERCENTAGE_MINIMUM && !state.speed) {
|
||||
if (isChecking) {
|
||||
return 'Validating...'
|
||||
}
|
||||
|
||||
return 'Starting...'
|
||||
} else if (state.percentage === utils.PERCENTAGE_MAXIMUM) {
|
||||
if ((isChecking || !shouldValidate) && shouldUnmount) {
|
||||
return 'Unmounting...'
|
||||
}
|
||||
|
||||
return 'Finishing...'
|
||||
} else if (state.type === 'write') {
|
||||
return `${state.percentage}% Flashing`
|
||||
} else if (state.type === 'check') {
|
||||
return `${state.percentage}% Validating`
|
||||
}
|
||||
|
||||
throw new Error(`Invalid state: ${JSON.stringify(state)}`)
|
||||
}
|
@ -19,8 +19,8 @@
|
||||
const _ = require('lodash')
|
||||
const electron = require('electron')
|
||||
const Bluebird = require('bluebird')
|
||||
const errors = require('../../shared/errors')
|
||||
const supportedFormats = require('../../shared/supported-formats')
|
||||
const errors = require('../../../shared/errors')
|
||||
const supportedFormats = require('../../../shared/supported-formats')
|
||||
|
||||
/**
|
||||
* @summary Current renderer BrowserWindow instance
|
||||
@ -57,9 +57,9 @@ exports.selectImage = () => {
|
||||
//
|
||||
// See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7
|
||||
defaultPath: process.env.OWD,
|
||||
|
||||
properties: [
|
||||
'openFile'
|
||||
'openFile',
|
||||
'treatPackageAsDirectory'
|
||||
],
|
||||
filters: [
|
||||
{
|
@ -16,6 +16,8 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* @summary OsOpenExternal directive
|
||||
* @function
|
||||
@ -43,6 +45,17 @@ module.exports = (OSOpenExternalService) => {
|
||||
element.on('click', () => {
|
||||
OSOpenExternalService.open(attributes.osOpenExternal)
|
||||
})
|
||||
|
||||
const ENTER_KEY = 13
|
||||
const SPACE_KEY = 32
|
||||
element.on('keypress', (event) => {
|
||||
if (_.includes([ ENTER_KEY, SPACE_KEY ], event.keyCode)) {
|
||||
// Don't spam the user with several tabs if the key is being held
|
||||
if (!event.repeat) {
|
||||
OSOpenExternalService.open(attributes.osOpenExternal)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,44 @@
|
||||
'use strict'
|
||||
|
||||
const electron = require('electron')
|
||||
const utils = require('../../shared/utils')
|
||||
const utils = require('../../../shared/utils')
|
||||
const progressStatus = require('../modules/progress-status')
|
||||
|
||||
/**
|
||||
* @summary The title of the main window upon program launch
|
||||
* @type {String}
|
||||
* @private
|
||||
* @constant
|
||||
*/
|
||||
const INITIAL_TITLE = document.title
|
||||
|
||||
/**
|
||||
* @summary Make the full window status title
|
||||
* @private
|
||||
*
|
||||
* @param {Object} state - flash state object
|
||||
*
|
||||
* @returns {String}
|
||||
*
|
||||
* @example
|
||||
* const title = getWindowTitle({
|
||||
* type: 'write',
|
||||
* percentage: 55,
|
||||
* speed: 2049
|
||||
* });
|
||||
*
|
||||
* console.log(title);
|
||||
* // 'Etcher \u2013 55% Flashing'
|
||||
*/
|
||||
const getWindowTitle = (state) => {
|
||||
if (state) {
|
||||
const subtitle = progressStatus.fromFlashState(state)
|
||||
const DASH_UNICODE_CHAR = '\u2013'
|
||||
return `${INITIAL_TITLE} ${DASH_UNICODE_CHAR} ${subtitle}`
|
||||
}
|
||||
|
||||
return INITIAL_TITLE
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary A reference to the current renderer Electron window
|
||||
@ -37,13 +74,18 @@ exports.currentWindow = electron.remote.getCurrentWindow()
|
||||
* @description
|
||||
* Show progress inline in operating system task bar
|
||||
*
|
||||
* @param {Number} percentage - percentage
|
||||
* @param {Number} state - flash state object
|
||||
*
|
||||
* @example
|
||||
* windowProgress.set(85);
|
||||
* windowProgress.set({
|
||||
* type: 'write',
|
||||
* percentage: 55,
|
||||
* speed: 2049
|
||||
* })
|
||||
*/
|
||||
exports.set = (percentage) => {
|
||||
exports.currentWindow.setProgressBar(utils.percentageToFloat(percentage))
|
||||
exports.set = (state) => {
|
||||
exports.currentWindow.setProgressBar(utils.percentageToFloat(state.percentage))
|
||||
exports.currentWindow.setTitle(getWindowTitle(state))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,4 +101,5 @@ exports.clear = () => {
|
||||
const ELECTRON_PROGRESS_BAR_RESET_VALUE = -1
|
||||
|
||||
exports.currentWindow.setProgressBar(ELECTRON_PROGRESS_BAR_RESET_VALUE)
|
||||
exports.currentWindow.setTitle(getWindowTitle(null))
|
||||
}
|
@ -17,8 +17,8 @@
|
||||
'use strict'
|
||||
|
||||
const settings = require('../../../models/settings')
|
||||
const flashState = require('../../../../shared/models/flash-state')
|
||||
const selectionState = require('../../../../shared/models/selection-state')
|
||||
const flashState = require('../../../../../shared/models/flash-state')
|
||||
const selectionState = require('../../../../../shared/models/selection-state')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
module.exports = function ($state) {
|