Skip to content
This repository has been archived by the owner on Mar 22, 2024. It is now read-only.

Commit

Permalink
Retry read operations up to 10 times on EIO (#70)
Browse files Browse the repository at this point in the history
Some faulty SDCard, or SDCard readers might throw EIO when reading data.
Retrying a couple of times makes the issue go away.

See: #73
Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
  • Loading branch information
Juan Cruz Viotti committed Jan 5, 2017
1 parent 2d4b9b1 commit 81ab773
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 34 deletions.
13 changes: 5 additions & 8 deletions lib/checksum.js
Expand Up @@ -18,11 +18,11 @@

const _ = require('lodash');
const Bluebird = require('bluebird');
const fs = require('fs');
const sliceStream = require('slice-stream2');
const progressStream = require('progress-stream');
const CRC32Stream = require('crc32-stream');
const DevNullStream = require('dev-null-stream');
const ImageReadStream = require('./image-read-stream');
const utils = require('./utils');

/**
Expand All @@ -33,13 +33,15 @@ const utils = require('./utils');
* @param {Number} deviceFileDescriptor - device file descriptor
* @param {Object} options - options
* @param {Number} options.imageSize - image size in bytes
* @param {Number} options.chunkSize - chunk size
* @param {Function} options.progress - progress callback (state)
* @fulfil {String} - checksum
* @returns {Promise}
*
* const fd = fs.openSync('/dev/rdisk2', 'rs+');
* checksum.calculateDeviceChecksum(fd, {
* imageSize: 65536 * 128,
* chunkSize: 65536 * 16
* progress: (state) => {
* console.log(state);
* }
Expand All @@ -53,13 +55,8 @@ exports.calculateDeviceChecksum = (deviceFileDescriptor, options = {}) => {
return new Bluebird((resolve, reject) => {
utils.safePipe([
{
stream: fs.createReadStream(null, {
fd: deviceFileDescriptor,

// Make sure we start reading from the beginning,
// since the fd might have been modified before.
start: 0

stream: new ImageReadStream(deviceFileDescriptor, {
chunkSize: options.chunkSize
})
},
{
Expand Down
53 changes: 53 additions & 0 deletions lib/filesystem.js
Expand Up @@ -88,3 +88,56 @@ exports.writeChunk = (fd, chunk, position, callback, retries = 1) => {
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 = 1) => {
fs.read(fd, Buffer.allocUnsafe(size), 0, size, position, (error, bytesRead, buffer) => {

if (error) {

// 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 (retries > MAXIMUM_RETRIES) {
return callback(error);
}

return setTimeout(() => {
return exports.readChunk(fd, size, position, callback, retries + 1);
}, RETRY_BASE_TIMEOUT * retries);
}

return callback(error);
}

if (bytesRead === 0) {
return callback(null, null);
}

return callback(null, buffer);
});
};
71 changes: 71 additions & 0 deletions lib/image-read-stream.js
@@ -0,0 +1,71 @@
/*
* 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 stream = require('stream');
const filesystem = require('./filesystem');
const ImageStreamPosition = require('./image-stream-position');

module.exports = class ImageWriteStream extends stream.Readable {

/**
* @summary Create an instance of ImageReadStream
* @name ImageReadStream
* @class
* @public
*
* @param {Number} fileDescriptor - file descriptor
* @param {Object} options - options
* @param {Number} options.chunkSize - chunk size
*
* @example
* const fd = fs.openSync('/dev/rdisk2', 'rs+');
*
* const stream = new ImageReadStream(fd, {
* chunkSize: 65536 * 16
* });
*
* stream.pipe(fs.createWriteStream('path/to/output/img'))
* .pipe(new ImageWriteStream(fd));
*/
constructor(fileDescriptor, options) {
const position = new ImageStreamPosition();

super({
highWaterMark: options.chunkSize,
objectMode: false,
read: (size) => {
const streamPosition = position.getStreamPosition();

filesystem.readChunk(this.fileDescriptor, size, streamPosition, (error, buffer) => {
if (error) {
return this.emit('error', error);
}

if (buffer) {
position.incrementStreamPosition(buffer.length);
}

this.push(buffer);
});
}
});

this.fileDescriptor = fileDescriptor;
}

};
10 changes: 6 additions & 4 deletions lib/index.js
Expand Up @@ -126,16 +126,17 @@ exports.write = (drive, image, options) => {
throw new Error('Invalid drive size: ' + drive.size);
}

win32.prepare(drive.device).then(() => {
// 64K * 16 = 1024K = 1M
const CHUNK_SIZE = 65536 * 16;

win32.prepare(drive.device).then(function() {
return write.inferFromOptions(drive.fd, {
imageSize: image.size,
imageStream: image.stream,
transformStream: options.transform,
bmapContents: options.bmap,
bytesToZeroOutFromTheBeginning: options.bytesToZeroOutFromTheBeginning,

// 64K * 16 = 1024K = 1M
chunkSize: 65536 * 16,
chunkSize: CHUNK_SIZE,

// 64K * 8 = 512K
minimumChunkSize: 65536 * 8,
Expand All @@ -152,6 +153,7 @@ exports.write = (drive, image, options) => {
bmapContents: options.bmap,
imageSize: results.transferredBytes,
imageChecksum: results.checksum,
chunkSize: CHUNK_SIZE,
progress: (state) => {
state.type = 'check';
emitter.emit('progress', state);
Expand Down
35 changes: 18 additions & 17 deletions lib/validate.js
Expand Up @@ -71,6 +71,7 @@ exports.usingBmap = (deviceFileDescriptor, options = {}) => {
* @param {Object} options - options
* @param {Number} options.imageSize - image size
* @param {String} options.imageChecksum - image checksum
* @param {Number} options.chunkSize - chunk size
* @param {Function} options.progress - progress callback (state)
* @fulfil {Object} - results
* @returns {Promise}
Expand All @@ -80,6 +81,7 @@ exports.usingBmap = (deviceFileDescriptor, options = {}) => {
* validate.usingChecksum(fd, {
* imageSize: 65536 * 128,
* imageChecksum: '717a34c1',
* chunkSize: 65536 * 16,
* progress: (state) => {
* console.log(state);
* }
Expand All @@ -90,7 +92,8 @@ exports.usingBmap = (deviceFileDescriptor, options = {}) => {
exports.usingChecksum = (deviceFileDescriptor, options = {}) => {
return checksum.calculateDeviceChecksum(deviceFileDescriptor, {
imageSize: options.imageSize,
progress: options.progress
progress: options.progress,
chunkSize: options.chunkSize
}).then((deviceChecksum) => {
if (options.imageChecksum !== deviceChecksum) {
throw errors.ValidationError;
Expand Down Expand Up @@ -152,24 +155,22 @@ exports.mock = (deviceFileDescriptor, options = {}) => {
* });
*/
exports.inferFromOptions = (deviceFileDescriptor, options = {}) => {
return Bluebird.try(() => {
if (options.omitValidation) {
return exports.mock(deviceFileDescriptor, {
imageChecksum: options.imageChecksum
});
}

if (options.bmapContents) {
return exports.usingBmap(deviceFileDescriptor, {
bmapContents: options.bmapContents,
progress: options.progress
});
}
if (options.omitValidation) {
return exports.mock(deviceFileDescriptor, {
imageChecksum: options.imageChecksum
});
}

return exports.usingChecksum(deviceFileDescriptor, {
imageSize: options.imageSize,
imageChecksum: options.imageChecksum,
if (options.bmapContents) {
return exports.usingBmap(deviceFileDescriptor, {
bmapContents: options.bmapContents,
progress: options.progress
});
}

return exports.usingChecksum(deviceFileDescriptor, {
imageSize: options.imageSize,
imageChecksum: options.imageChecksum,
progress: options.progress
});
};
Binary file removed tests/evalidation/mock
Binary file not shown.
9 changes: 4 additions & 5 deletions tests/evalidation/test.js
Expand Up @@ -21,23 +21,22 @@ const path = require('path');
const Bluebird = require('bluebird');
const fs = Bluebird.promisifyAll(require('fs'));
const imageWrite = require('../../lib');
const checksum = require('../../lib/checksum');

module.exports = [

{
name: 'should throw EVALIDATION is check fails',
data: {
input: path.join(__dirname, 'input'),
mock: path.join(__dirname, 'mock'),
output: path.join(__dirname, 'output')
},
case: (data) => {
const outputFileDescriptor = fs.openSync(data.output, 'rs+');
const stream = fs.createReadStream(data.input);
const mockStream = fs.createReadStream(data.mock);

const createReadStreamStub = m.sinon.stub(fs, 'createReadStream');
createReadStreamStub.returns(mockStream);
const calculateDeviceChecksumStub = m.sinon.stub(checksum, 'calculateDeviceChecksum');
calculateDeviceChecksumStub.returns(Bluebird.resolve('xxxxxxx'));

return new Bluebird((resolve, reject) => {
const imageSize = fs.statSync(data.input).size;
Expand All @@ -64,7 +63,7 @@ module.exports = [
}).catch((error) => {
m.chai.expect(error.code).to.equal('EVALIDATION');
}).finally(() => {
createReadStreamStub.restore();
calculateDeviceChecksumStub.restore();
return fs.closeAsync(outputFileDescriptor);
});
}
Expand Down

0 comments on commit 81ab773

Please sign in to comment.