etcher/lib/gui/app/modules/image-writer.ts
Lorenzo Alberto Maria Ambrosi 00e8f11913 patch: Fix linting errors
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:21:48 +02:00

356 lines
9.5 KiB
TypeScript

/*
* Copyright 2016 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 { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk';
import { Dictionary } from 'lodash';
import * as ipc from 'node-ipc';
import * as os from 'os';
import * as path from 'path';
import * as packageJSON from '../../../../package.json';
import * as errors from '../../../shared/errors';
import * as permissions from '../../../shared/permissions';
import { getAppPath } from '../../../shared/utils';
import { SourceMetadata } from '../components/source-selector/source-selector';
import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state';
import * as settings from '../models/settings';
import * as analytics from '../modules/analytics';
import * as windowProgress from '../os/window-progress';
const THREADS_PER_CPU = 16;
// 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;
/**
* @summary Handle a flash error and log it to analytics
*/
function handleErrorLogging(
error: Error & { code: string },
analyticsData: any,
) {
const eventData = {
...analyticsData,
flashInstanceUuid: flashState.getFlashUuid(),
};
if (error.code === 'EVALIDATION') {
analytics.logEvent('Validation error', eventData);
} else if (error.code === 'EUNPLUGGED') {
analytics.logEvent('Drive unplugged', eventData);
} else if (error.code === 'EIO') {
analytics.logEvent('Input/output error', eventData);
} else if (error.code === 'ENOSPC') {
analytics.logEvent('Out of space', eventData);
} else if (error.code === 'ECHILDDIED') {
analytics.logEvent('Child died unexpectedly', eventData);
} else {
analytics.logEvent('Flash error', {
...eventData,
error: errors.toJSON(error),
});
}
}
function 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.
// @ts-ignore (no Server.sockets in @types/node-ipc)
for (const socket of ipc.server.sockets) {
socket.destroy();
}
ipc.server.stop();
}
function writerArgv(): string[] {
let entryPoint = path.join(getAppPath(), 'generated', 'child-writer.js');
// 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) {
entryPoint = entryPoint.replace(process.env.APPDIR, '');
return [
process.env.APPIMAGE,
'-e',
`require(\`\${process.env.APPDIR}${entryPoint}\`)`,
];
} else {
return [process.argv[0], entryPoint];
}
}
function writerEnv() {
return {
IPC_SERVER_ID,
IPC_CLIENT_ID,
IPC_SOCKET_ROOT: ipc.config.socketRoot,
ELECTRON_RUN_AS_NODE: '1',
UV_THREADPOOL_SIZE: (os.cpus().length * THREADS_PER_CPU).toString(),
// This environment variable prevents the AppImages
// desktop integration script from presenting the
// "installation" dialog
SKIP: '1',
...(process.platform === 'win32' ? {} : process.env),
};
}
interface FlashResults {
skip?: boolean;
cancelled?: boolean;
results?: {
bytesWritten: number;
devices: {
failed: number;
successful: number;
};
errors: Error[];
};
}
async function performWrite(
image: SourceMetadata,
drives: DrivelistDrive[],
onProgress: sdk.multiWrite.OnProgressFunction,
): Promise<{ cancelled?: boolean }> {
let cancelled = false;
let skip = false;
ipc.serve();
const { autoBlockmapping, decompressFirst } = await settings.getAll();
return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => {
terminateServer();
const errorObject = errors.fromJSON(error);
reject(errorObject);
});
ipc.server.on('log', (message) => {
console.log(message);
});
const flashResults: FlashResults = {};
const analyticsData = {
image,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
};
ipc.server.on('fail', ({ device, error }) => {
if (device.devicePath) {
flashState.addFailedDeviceError({ device, error });
}
handleErrorLogging(error, analyticsData);
});
ipc.server.on('done', (event) => {
event.results.errors = event.results.errors.map(
(data: Dictionary<any> & { message: string }) => {
return errors.fromJSON(data);
},
);
flashResults.results = event.results;
});
ipc.server.on('abort', () => {
terminateServer();
cancelled = true;
});
ipc.server.on('skip', () => {
terminateServer();
skip = true;
});
ipc.server.on('state', onProgress);
ipc.server.on('ready', (_data, socket) => {
ipc.server.emit(socket, 'write', {
image,
destinations: drives,
SourceType: image.SourceType.name,
autoBlockmapping,
decompressFirst,
});
});
const argv = writerArgv();
ipc.server.on('start', async () => {
console.log(`Elevating command: ${argv.join(' ')}`);
const env = writerEnv();
try {
const results = await permissions.elevateCommand(argv, {
applicationName: packageJSON.displayName,
environment: env,
});
flashResults.cancelled = cancelled || results.cancelled;
flashResults.skip = skip;
} catch (error: any) {
// This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137;
if (error.code === SIGKILL_EXIT_CODE) {
error.code = 'ECHILDDIED';
}
reject(error);
} finally {
console.log('Terminating IPC server');
terminateServer();
}
console.log('Flash results', flashResults);
// The flash wasn't cancelled and we didn't get a 'done' event
if (
!flashResults.cancelled &&
!flashResults.skip &&
flashResults.results === undefined
) {
reject(
errors.createUserError({
title: 'The writer process ended unexpectedly',
description:
'Please try again, and contact the Etcher team if the problem persists',
}),
);
return;
}
resolve(flashResults);
});
// Clear the update lock timer to prevent longer
// flashing timing it out, and releasing the lock
ipc.server.start();
});
}
/**
* @summary Flash an image to drives
*/
export async function flash(
image: SourceMetadata,
drives: DrivelistDrive[],
// This function is a parameter so it can be mocked in tests
write = performWrite,
): Promise<void> {
if (flashState.isFlashing()) {
throw new Error('There is already a flash in progress');
}
await flashState.setFlashingFlag();
flashState.setDevicePaths(
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
);
const analyticsData = {
image,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
status: 'started',
flashInstanceUuid: flashState.getFlashUuid(),
};
analytics.logEvent('Flash', analyticsData);
try {
const result = await write(image, drives, flashState.setProgressState);
await flashState.unsetFlashingFlag(result);
} catch (error: any) {
await flashState.unsetFlashingFlag({
cancelled: false,
errorCode: error.code,
});
windowProgress.clear();
const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
devices: results.devices,
status: 'failed',
error,
};
analytics.logEvent('Write failed', eventData);
throw error;
}
windowProgress.clear();
if (flashState.wasLastFlashCancelled()) {
const eventData = {
...analyticsData,
status: 'cancel',
};
analytics.logEvent('Elevation cancelled', eventData);
} else {
const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
devices: results.devices,
status: 'finished',
bytesWritten: results.bytesWritten,
sourceMetadata: results.sourceMetadata,
};
analytics.logEvent('Done', eventData);
}
}
/**
* @summary Cancel write operation
*/
export async function cancel(type: string) {
const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices();
const analyticsData = {
image: selectionState.getImage()?.path,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
status,
};
analytics.logEvent('Cancel', analyticsData);
// Re-enable lock release on inactivity
try {
// @ts-ignore (no Server.sockets in @types/node-ipc)
const [socket] = ipc.server.sockets;
if (socket !== undefined) {
ipc.server.emit(socket, status);
}
} catch (error: any) {
analytics.logException(error);
}
}