etcher-image-write
Version:
The cross-platform way to stream an OS image to a device
191 lines (171 loc) • 5.17 kB
JavaScript
/*
* 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.
*/
;
const fs = require('fs');
const _ = require('lodash');
const os = require('os');
const errors = require('./errors');
// The functions to perform common I/O operations defined in this file
// contains certain workarounds to compensate SDCard errors and similar
// problems, therefore every I/O operation from this module should use
// the filesystem functions exposed in this file.
/**
* @summary Maximum I/O retries
* @constant
* @type {Number}
*/
const MAXIMUM_RETRIES = 10;
/**
* @summary I/O retry base timeout, in milliseconds
* @constant
* @type {Number}
*/
const RETRY_BASE_TIMEOUT = 100;
/**
* @summary Handle read/write errors
* @function
* @private
*
* @param {Error} error - error
* @param {Object} options - options
* @param {Number} options.currentRetries - current number of retries
* @param {Function} options.retry - retry function (currentRetries)
* @param {Function} callback - callback (error)
* @returns {Undefined}
*
* @example
* const foo = (callback, retries = 1) => {
* bar((error) => {
* if (error) {
* handleError(error, {
* currentRetries: retries,
* retry: (currentRetries) => {
* foo(callback, currentRetries + 1);
* }
* }, callback);
* }
*
* callback(null, 'Success!');
* });
* };
*/
const handleError = (error, options, callback) => {
const platform = os.platform();
if (_.some([
platform === 'darwin' && (error.code === 'ENXIO' || error.code === 'EBUSY'),
platform === 'win32' && (error.code === 'ENOENT' || error.code === 'UNKNOWN')
])) {
return callback(errors.unpluggedError());
}
// In some faulty SDCards or SDCard readers you might randomly
// get EIO errors, but they usually go away after some retries.
if (error.code === 'EIO') {
if (options.currentRetries > MAXIMUM_RETRIES) {
if (platform === 'linux') {
return callback(errors.unpluggedError());
}
return callback(error);
}
return setTimeout(() => {
options.retry(options.currentRetries);
}, RETRY_BASE_TIMEOUT * options.currentRetries);
}
callback(error);
};
/**
* @summary Write a chunk to a file descriptor
* @function
* @public
*
* @description
* This function is simply a convenient wrapper around `fs.write()`.
*
* @param {Number} fd - file descriptor
* @param {Buffer} chunk - buffer chunk
* @param {Number} position - file position to write to
* @param {Function} callback - callback (error)
* @param {Number} [retries=1] - number of pending retries
*
* @example
* const fd = fs.openSync('/dev/rdisk2', 'rs+');
*
* filesystem.writeChunk(fd, Buffer.allocUnsafe(65536), 0, (error) => {
* if (error) {
* throw error;
* }
*
* console.log('We wrote a 65536 bytes chunk at position 0');
* });
*/
exports.writeChunk = (fd, chunk, position, callback, retries) => {
retries = retries || 1;
fs.write(fd, chunk, 0, chunk.length, position, (error, bytesWritten) => {
if (error) {
return handleError(error, {
currentRetries: retries,
retry: (currentRetries) => {
return exports.writeChunk(fd, chunk, position, callback, currentRetries + 1);
}
}, callback);
}
if (bytesWritten !== chunk.length) {
return callback(new Error(`Write ${bytesWritten} bytes, ${chunk.length} expected`));
}
return callback();
});
};
/**
* @summary Read a chunk from a file descriptor
* @function
* @public
*
* @description
* This function is simply a convenient wrapper around `fs.read()`.
*
* @param {Number} fd - file descriptor
* @param {Number} size - chunk size to read
* @param {Number} position - position to read from
* @param {Function} callback - callback (error, buffer)
* @param {Number} [retries=1] - number of pending retries
*
* @example
* const fd = fs.openSync('/dev/rdisk2', 'rs+');
*
* filesystem.readChunk(fd, 512, 0, (error, buffer) => {
* if (error) {
* throw error;
* }
*
* console.log(buffer);
* });
*/
exports.readChunk = (fd, size, position, callback, retries) => {
retries = retries || 1;
fs.read(fd, Buffer.allocUnsafe(size), 0, size, position, (error, bytesRead, buffer) => {
if (error) {
return handleError(error, {
currentRetries: retries,
retry: (currentRetries) => {
return exports.readChunk(fd, size, position, callback, currentRetries + 1);
}
}, callback);
}
if (bytesRead === 0) {
return callback(null, null);
}
return callback(null, buffer);
});
};