Convert child-writer.js to typescript

Change-type: patch
This commit is contained in:
Alexis Svinartchouk 2020-01-10 14:58:05 +01:00 committed by Lorenzo Alberto Maria Ambrosi
parent 1c46ee2988
commit 97aff2eb4c
6 changed files with 272 additions and 244 deletions

View File

@ -1,244 +0,0 @@
/*
* Copyright 2017 balena.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 ipc = require('node-ipc')
const sdk = require('etcher-sdk')
// eslint-disable-next-line node/no-missing-require
const EXIT_CODES = require('../../shared/exit-codes')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../shared/errors')
ipc.config.id = process.env.IPC_CLIENT_ID
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT
// NOTE: Ensure this isn't disabled, as it will cause
// the stdout maxBuffer size to be exceeded when flashing
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 this process as well.
ipc.config.stopRetrying = 0
const DISCONNECT_DELAY = 100
const IPC_SERVER_ID = process.env.IPC_SERVER_ID
/**
* @summary Send a log debug message to the IPC server
* @function
* @private
*
* @param {String} message - message
*
* @example
* log('Hello world!')
*/
const log = (message) => {
ipc.of[IPC_SERVER_ID].emit('log', message)
}
/**
* @summary Terminate the child writer process
* @function
* @private
*
* @param {Number} [code=0] - exit code
*
* @example
* terminate(1)
*/
const terminate = (code) => {
ipc.disconnect(IPC_SERVER_ID)
process.nextTick(() => {
process.exit(code || EXIT_CODES.SUCCESS)
})
}
/**
* @summary Handle a child writer error
* @function
* @private
*
* @param {Error} error - error
*
* @example
* handleError(new Error('Something bad happened!'))
*/
const handleError = async (error) => {
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
await Bluebird.delay(DISCONNECT_DELAY)
terminate(EXIT_CODES.GENERAL_ERROR)
}
/**
* @summary writes the source to the destinations and valiates the writes
* @param {SourceDestination} source - source
* @param {SourceDestination[]} destinations - destinations
* @param {Boolean} verify - whether to validate the writes or not
* @param {Boolean} trim - whether to trim ext partitions before writing
* @param {Function} onProgress - function to call on progress
* @param {Function} onFail - function to call on fail
* @returns {Promise<{ bytesWritten, devices, errors} >}
*
* @example
* writeAndValidate(source, destinations, verify, onProgress, onFail, onFinish, onError)
*/
const writeAndValidate = async (source, destinations, verify, trim, onProgress, onFail) => {
let innerSource = await source.getInnerSource()
if (trim && (await innerSource.canRead())) {
innerSource = new sdk.sourceDestination.ConfiguredSource(
innerSource,
trim,
// Create stream from file-disk (not source stream)
true
)
}
const { failures, bytesWritten } = await sdk.multiWrite.pipeSourceToDestinations(
innerSource,
destinations,
onFail,
onProgress,
verify
)
const result = {
bytesWritten,
devices: {
failed: failures.size,
successful: destinations.length - failures.size
},
errors: []
}
for (const [ destination, error ] of failures) {
error.device = destination.drive.device
result.errors.push(error)
}
return result
}
ipc.connectTo(IPC_SERVER_ID, () => {
process.once('uncaughtException', handleError)
// Gracefully exit on the following cases. If the parent
// process detects that child exit successfully but
// no flashing information is available, then it will
// assume that the child died halfway through.
process.once('SIGINT', () => {
terminate(EXIT_CODES.SUCCESS)
})
process.once('SIGTERM', () => {
terminate(EXIT_CODES.SUCCESS)
})
// The IPC server failed. Abort.
ipc.of[IPC_SERVER_ID].on('error', () => {
terminate(EXIT_CODES.SUCCESS)
})
// The IPC server was disconnected. Abort.
ipc.of[IPC_SERVER_ID].on('disconnect', () => {
terminate(EXIT_CODES.SUCCESS)
})
ipc.of[IPC_SERVER_ID].on('write', async (options) => {
/**
* @summary Progress handler
* @param {Object} state - progress state
* @example
* writer.on('progress', onProgress)
*/
const onProgress = (state) => {
ipc.of[IPC_SERVER_ID].emit('state', state)
}
let exitCode = EXIT_CODES.SUCCESS
/**
* @summary Abort handler
* @example
* writer.on('abort', onAbort)
*/
const onAbort = async () => {
log('Abort')
ipc.of[IPC_SERVER_ID].emit('abort')
await Bluebird.delay(DISCONNECT_DELAY)
terminate(exitCode)
}
ipc.of[IPC_SERVER_ID].on('cancel', onAbort)
/**
* @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination
* @param {Error} error - error
* @example
* writer.on('fail', onFail)
*/
const onFail = (destination, error) => {
ipc.of[IPC_SERVER_ID].emit('fail', {
// TODO: device should be destination
device: destination.drive,
error: errors.toJSON(error)
})
}
const destinations = _.map(options.destinations, 'device')
log(`Image: ${options.imagePath}`)
log(`Devices: ${destinations.join(', ')}`)
log(`Umount on success: ${options.unmountOnSuccess}`)
log(`Validate on success: ${options.validateWriteOnSuccess}`)
log(`Trim: ${options.trim}`)
const dests = _.map(options.destinations, (destination) => {
return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess)
})
const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)
try {
const results = await writeAndValidate(
source,
dests,
options.validateWriteOnSuccess,
options.trim,
onProgress,
onFail
)
log(`Finish: ${results.bytesWritten}`)
results.errors = _.map(results.errors, (error) => {
return errors.toJSON(error)
})
ipc.of[IPC_SERVER_ID].emit('done', { results })
await Bluebird.delay(DISCONNECT_DELAY)
terminate(exitCode)
} catch (error) {
log(`Error: ${error.message}`)
exitCode = EXIT_CODES.GENERAL_ERROR
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
}
})
ipc.of[IPC_SERVER_ID].on('connect', () => {
log(`Successfully connected to IPC server: ${IPC_SERVER_ID}, socket root ${ipc.config.socketRoot}`)
ipc.of[IPC_SERVER_ID].emit('ready', {})
})
})

View File

@ -0,0 +1,260 @@
/*
* Copyright 2017 balena.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.
*/
import { delay } from 'bluebird';
import { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import * as ipc from 'node-ipc';
import { toJSON } from '../../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
ipc.config.id = process.env.IPC_CLIENT_ID as string;
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
// NOTE: Ensure this isn't disabled, as it will cause
// the stdout maxBuffer size to be exceeded when flashing
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 this process as well.
// @ts-ignore (0 is a valid value for stopRetrying and is not the same as false)
ipc.config.stopRetrying = 0;
const DISCONNECT_DELAY = 100;
const IPC_SERVER_ID = process.env.IPC_SERVER_ID as string;
/**
* @summary Send a log debug message to the IPC server
*/
function log(message: string) {
ipc.of[IPC_SERVER_ID].emit('log', message);
}
/**
* @summary Terminate the child writer process
*/
function terminate(exitCode: number) {
ipc.disconnect(IPC_SERVER_ID);
process.nextTick(() => {
process.exit(exitCode || SUCCESS);
});
}
/**
* @summary Handle a child writer error
*/
async function handleError(error: Error) {
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
await delay(DISCONNECT_DELAY);
terminate(GENERAL_ERROR);
}
interface WriteResult {
bytesWritten: number;
devices: {
failed: number;
successful: number;
};
errors: Array<Error & { device: string }>;
}
/**
* @summary writes the source to the destinations and valiates the writes
* @param {SourceDestination} source - source
* @param {SourceDestination[]} destinations - destinations
* @param {Boolean} verify - whether to validate the writes or not
* @param {Boolean} trim - whether to trim ext partitions before writing
* @param {Function} onProgress - function to call on progress
* @param {Function} onFail - function to call on fail
* @returns {Promise<{ bytesWritten, devices, errors} >}
*/
async function writeAndValidate(
source: sdk.sourceDestination.SourceDestination,
destinations: sdk.sourceDestination.BlockDevice[],
verify: boolean,
trim: boolean,
onProgress: sdk.multiWrite.OnProgressFunction,
onFail: sdk.multiWrite.OnFailFunction,
): Promise<WriteResult> {
let innerSource: sdk.sourceDestination.SourceDestination = await source.getInnerSource();
if (trim && (await innerSource.canRead())) {
// @ts-ignore FIXME: ts thinks that SparseReadStream can't be assigned to SparseReadable (which it implements)
innerSource = new sdk.sourceDestination.ConfiguredSource(
innerSource,
trim,
// Create stream from file-disk (not source stream)
true,
);
}
const {
failures,
bytesWritten,
} = await sdk.multiWrite.pipeSourceToDestinations(
innerSource,
// @ts-ignore FIXME: ts thinks that BlockWriteStream can't be assigned to WritableStream (which it implements)
destinations,
onFail,
onProgress,
verify,
);
const result: WriteResult = {
bytesWritten,
devices: {
failed: failures.size,
successful: destinations.length - failures.size,
},
errors: [],
};
for (const [destination, error] of failures) {
(error as (Error & { device: string })).device = destination.drive.device;
result.errors.push(error);
}
return result;
}
interface WriteOptions {
imagePath: string;
destinations: DrivelistDrive[];
unmountOnSuccess: boolean;
validateWriteOnSuccess: boolean;
trim: boolean;
}
ipc.connectTo(IPC_SERVER_ID, () => {
process.once('uncaughtException', handleError);
// Gracefully exit on the following cases. If the parent
// process detects that child exit successfully but
// no flashing information is available, then it will
// assume that the child died halfway through.
process.once('SIGINT', () => {
terminate(SUCCESS);
});
process.once('SIGTERM', () => {
terminate(SUCCESS);
});
// The IPC server failed. Abort.
ipc.of[IPC_SERVER_ID].on('error', () => {
terminate(SUCCESS);
});
// The IPC server was disconnected. Abort.
ipc.of[IPC_SERVER_ID].on('disconnect', () => {
terminate(SUCCESS);
});
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
/**
* @summary Progress handler
* @param {Object} state - progress state
* @example
* writer.on('progress', onProgress)
*/
const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => {
ipc.of[IPC_SERVER_ID].emit('state', state);
};
let exitCode = SUCCESS;
/**
* @summary Abort handler
* @example
* writer.on('abort', onAbort)
*/
const onAbort = async () => {
log('Abort');
ipc.of[IPC_SERVER_ID].emit('abort');
await delay(DISCONNECT_DELAY);
terminate(exitCode);
};
ipc.of[IPC_SERVER_ID].on('cancel', onAbort);
/**
* @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination
* @param {Error} error - error
* @example
* writer.on('fail', onFail)
*/
const onFail = (
destination: sdk.sourceDestination.BlockDevice,
error: Error,
) => {
ipc.of[IPC_SERVER_ID].emit('fail', {
// TODO: device should be destination
// @ts-ignore (destination.drive is private)
device: destination.drive,
error: toJSON(error),
});
};
const destinations = _.map(options.destinations, 'device');
log(`Image: ${options.imagePath}`);
log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Validate on success: ${options.validateWriteOnSuccess}`);
log(`Trim: ${options.trim}`);
const dests = _.map(options.destinations, destination => {
return new sdk.sourceDestination.BlockDevice(
destination,
options.unmountOnSuccess,
);
});
const source = new sdk.sourceDestination.File(
options.imagePath,
sdk.sourceDestination.File.OpenFlags.Read,
);
try {
const results = await writeAndValidate(
// @ts-ignore FIXME: ts thinks that SparseWriteStream can't be assigned to SparseWritable (which it implements)
source,
dests,
options.validateWriteOnSuccess,
options.trim,
onProgress,
onFail,
);
log(`Finish: ${results.bytesWritten}`);
results.errors = _.map(results.errors, error => {
return toJSON(error);
});
ipc.of[IPC_SERVER_ID].emit('done', { results });
await delay(DISCONNECT_DELAY);
terminate(exitCode);
} catch (error) {
log(`Error: ${error.message}`);
exitCode = GENERAL_ERROR;
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
}
});
ipc.of[IPC_SERVER_ID].on('connect', () => {
log(
`Successfully connected to IPC server: ${IPC_SERVER_ID}, socket root ${ipc.config.socketRoot}`,
);
ipc.of[IPC_SERVER_ID].emit('ready', {});
});
});

View File

@ -24,6 +24,7 @@
// or the entry point file (this file) manually as an argument.
if (process.env.ELECTRON_RUN_AS_NODE) {
// eslint-disable-next-line node/no-missing-require
require('./gui/modules/child-writer')
} else {
require('./gui/etcher')

9
npm-shrinkwrap.json generated
View File

@ -1215,6 +1215,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.24.tgz",
"integrity": "sha512-1Ciqv9pqwVtW6FsIUKSZNB82E5Cu1I2bBTj1xuIHXLe/1zYLl3956Nbhg2MzSYHVfl9/rmanjbQIb7LibfCnug=="
},
"@types/node-ipc": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.1.2.tgz",
"integrity": "sha512-140YlGizUg2Dbbmypc97RZ2iaWOEdcwec6QPJ9C5AWy8H/Hus6co4MeEF2lRPmOTBY3GJu+Xaxyr4FfyE6Hjew==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/normalize-package-data": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",

View File

@ -97,6 +97,7 @@
"@types/bindings": "^1.3.0",
"@types/mime-types": "^2.1.0",
"@types/node": "^12.12.24",
"@types/node-ipc": "^9.1.2",
"@types/react-dom": "^16.8.4",
"@types/request": "^2.48.4",
"@types/semver": "^6.2.0",

View File

@ -18,6 +18,7 @@
const m = require('mochainon')
const ipc = require('node-ipc')
// eslint-disable-next-line node/no-missing-require
require('../../../lib/gui/modules/child-writer')
describe('Browser: childWriter', function () {