mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +00:00
Convert child-writer.js to typescript
Change-type: patch
This commit is contained in:
parent
1c46ee2988
commit
97aff2eb4c
@ -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', {})
|
||||
})
|
||||
})
|
260
lib/gui/modules/child-writer.ts
Normal file
260
lib/gui/modules/child-writer.ts
Normal 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', {});
|
||||
});
|
||||
});
|
@ -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
9
npm-shrinkwrap.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user