Merge branch 'master' into patch-1

This commit is contained in:
Jonas Hermsmeier 2018-02-08 09:52:12 -08:00 committed by GitHub
commit 974315868d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
207 changed files with 8040 additions and 7757 deletions

View File

@ -9,5 +9,8 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

View File

@ -222,8 +222,7 @@ rules:
- error
- max: 1
multiline-ternary:
- error
- never
- off
newline-per-chained-call:
- off
no-bitwise:

7
.gitattributes vendored
View File

@ -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

View File

@ -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
View 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"
]
}
}
}
}

View File

@ -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

View File

@ -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.

View File

@ -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)"

View File

@ -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:

View File

@ -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:

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/iconset/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/iconset/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

BIN
assets/iconset/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
assets/iconset/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
assets/iconset/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
assets/iconset/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -1,5 +0,0 @@
boolen->boolean
aknowledge->acknowledge
seleted->selected
reming->remind
locl->local

9
dictionary.txt Normal file
View File

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

View File

@ -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

View File

@ -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*

View File

@ -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.

View File

@ -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
View 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**

View File

@ -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
View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,4 @@
gpu_mem=16
dtoverlay=dwc2,dr_mode=peripheral
dtparam=act_led_trigger=none
dtparam=act_led_activelow=off

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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!

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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)
})

View File

@ -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
View 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}"`))
}
}

View File

@ -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(() => {

View File

@ -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'
})
}

View File

@ -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)
})
})
})
})

View File

@ -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)
})

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -1,6 +1,6 @@
<div class="modal-header">
<h4 class="modal-title">Select a Drive</h4>
<button class="close" ng-click="modal.closeModal()">&times;</button>
<button tabindex="14" class="close" ng-click="modal.closeModal()">&times;</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>

View File

@ -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) {

View File

@ -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;

View File

@ -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, [])

View File

@ -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

View File

@ -3,7 +3,9 @@
<span class="glyphicon glyphicon-exclamation-sign"></span>
<span>Attention</span>
</h4>
<button class="close" ng-click="modal.reject()">&times;</button>
<button class="close"
tabindex="11"
ng-click="modal.reject()">&times;</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>

View File

@ -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>

View File

@ -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

View File

@ -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({

View 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

View 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()
})
}

View 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)}`)
}

View File

@ -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: [
{

View File

@ -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)
}
}
})
}
}
}

View File

@ -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))
}

View File

@ -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) {

Some files were not shown because too many files have changed in this diff Show More