Skip to content

Commit

Permalink
patch: extract the loader from gui and add it to utility (required so…
Browse files Browse the repository at this point in the history
…me api refactoring)
  • Loading branch information
aethernet committed Sep 26, 2023
1 parent 1aec7a3 commit 15f82c5
Show file tree
Hide file tree
Showing 13 changed files with 376 additions and 220 deletions.
40 changes: 25 additions & 15 deletions lib/gui/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage';
import './css/main.css';
import * as i18next from 'i18next';
import { promises } from 'dns';
import { SourceMetadata } from '../../shared/typings/source-selector';

window.addEventListener(
'unhandledrejection',
Expand Down Expand Up @@ -134,25 +136,33 @@ function setDrives(drives: Dictionary<DrivelistDrive>) {
}

// Spwaning the child process without privileges to get the drives list
const apiEventHandler = (event: any) => {
switch (event.type) {
case 'drives':
setDrives(JSON.parse(event.payload));
break;
default:
console.log('Unknown event type', event.type);
break;
}
};

const apiEvents = ['drives'];
// TODO: clean up this mess of exports
export let requestMetadata: any;
export let stopScanning: any;

// start the api and spawn the child process
startApiAndSpawnChild({
apiEventHandler,
apiEvents,
withPrivileges: false,
}).then(({ emit }) => {
}).then(({ emit, registerHandler, terminateServer }) => {
// start scanning
emit('scan');

// make the sourceMetada awaitable to be used on source selection
requestMetadata = async (params: any): Promise<SourceMetadata> => {
emit('sourceMetadata', JSON.stringify(params));

return new Promise((resolve) =>
registerHandler('sourceMetadata', (data: any) => {
resolve(JSON.parse(data));
}),
);
};

stopScanning = stopScanning;

registerHandler('drives', (data: any) => {
setDrives(JSON.parse(data));
});
});

let popupExists = false;
Expand Down
5 changes: 3 additions & 2 deletions lib/gui/app/components/flash-results/flash-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ export function FlashResults({
};
} & FlexProps) {
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = !skip && results.devices.successful === 0;
const someFailed = results.devices.failed !== 0 || errors.length !== 0;

const allFailed = !skip && results?.devices?.successful === 0;
const someFailed = results?.devices?.failed !== 0 || errors?.length !== 0;
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
1,
);
Expand Down
112 changes: 22 additions & 90 deletions lib/gui/app/components/source-selector/source-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
import { sourceDestination } from 'etcher-sdk';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as _ from 'lodash';
import { uniqBy, isNil } from 'lodash';
import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react';
import { requestMetadata } from '../../app';

import {
Flex,
ButtonProps,
Expand All @@ -46,7 +47,7 @@ import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import * as exceptionReporter from '../../modules/exception-reporter';
import * as osDialog from '../../os/dialog';
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';

import {
ChangeButton,
DetailsText,
Expand All @@ -63,7 +64,6 @@ import ImageSvg from '../../../assets/image.svg';
import SrcSvg from '../../../assets/src.svg';
import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
import axios, { AxiosRequestConfig } from 'axios';
import { isJson } from '../../../../shared/utils';
import {
SourceMetadata,
Expand All @@ -87,7 +87,7 @@ function normalizeRecentUrlImages(urls: any[]): URL[] {
}
})
.filter((url) => url !== undefined);
urls = _.uniqBy(urls, (url) => url.href);
urls = uniqBy(urls, (url) => url.href);
return urls.slice(urls.length - 5);
}

Expand Down Expand Up @@ -362,43 +362,11 @@ export class SourceSelector extends React.Component<
this.setState({ imageLoading: true });
await this.selectSource(
imagePath,
isURL(this.normalizeImagePath(imagePath))
? sourceDestination.Http
: sourceDestination.File,
isURL(this.normalizeImagePath(imagePath)) ? 'Http' : 'File',
).promise;
this.setState({ imageLoading: false });
}

private async createSource(
selected: string,
SourceType: Source,
auth?: Authentication,
) {
try {
selected = await replaceWindowsNetworkDriveLetter(selected);
} catch (error: any) {
analytics.logException(error);
}

if (isJson(decodeURIComponent(selected))) {
const config: AxiosRequestConfig = JSON.parse(
decodeURIComponent(selected),
);
return new sourceDestination.Http({
url: config.url!,
axiosInstance: axios.create(_.omit(config, ['url'])),
});
}

if (SourceType === sourceDestination.File) {
return new sourceDestination.File({
path: selected,
});
}

return new sourceDestination.Http({ url: selected, auth });
}

public normalizeImagePath(imgPath: string) {
const decodedPath = decodeURIComponent(imgPath);
if (isJson(decodedPath)) {
Expand Down Expand Up @@ -427,11 +395,10 @@ export class SourceSelector extends React.Component<
},
promise: (async () => {
const sourcePath = isString(selected) ? selected : selected.device;
let source;
let metadata: SourceMetadata | undefined;
if (isString(selected)) {
if (
SourceType === sourceDestination.Http &&
SourceType === 'Http' &&
!isURL(this.normalizeImagePath(selected))
) {
this.handleError(
Expand All @@ -451,24 +418,16 @@ export class SourceSelector extends React.Component<
},
});
}
source = await this.createSource(selected, SourceType, auth);

if (cancelled) {
return;
}

try {
const innerSource = await source.getInnerSource();
if (cancelled) {
return;
}
metadata = await this.getMetadata(innerSource, selected);
if (cancelled) {
return;
}
metadata.SourceType = SourceType;
// this will send an event down the ipcMain asking for metadata
// we'll get the response through an event

console.log('--> will request metadata');
metadata = await requestMetadata({ selected, SourceType, auth });
console.log('--> got metadata', metadata);

if (!metadata.hasMBR && this.state.warning === null) {
if (!metadata?.hasMBR && this.state.warning === null) {
analytics.logEvent('Missing partition table', { metadata });
this.setState({
warning: {
Expand All @@ -484,12 +443,6 @@ export class SourceSelector extends React.Component<
messages.error.openSource(sourcePath, error.message),
error,
);
} finally {
try {
await source.close();
} catch (error: any) {
// Noop
}
}
} else {
if (selected.partitionTableType === null) {
Expand All @@ -506,13 +459,14 @@ export class SourceSelector extends React.Component<
displayName: selected.displayName,
description: selected.displayName,
size: selected.size as SourceMetadata['size'],
SourceType: sourceDestination.BlockDevice,
SourceType: 'BlockDevice',
drive: selected,
};
}

if (metadata !== undefined) {
metadata.auth = auth;
metadata.SourceType = SourceType;
selectionState.selectSource(metadata);
analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of
Expand Down Expand Up @@ -546,25 +500,6 @@ export class SourceSelector extends React.Component<
analytics.logEvent(title, { path: sourcePath });
}

private async getMetadata(
source: sourceDestination.SourceDestination,
selected: string | DrivelistDrive,
) {
const metadata = (await source.getMetadata()) as SourceMetadata;
const partitionTable = await source.getPartitionTable();
if (partitionTable) {
metadata.hasMBR = true;
metadata.partitions = partitionTable.partitions;
} else {
metadata.hasMBR = false;
}
if (isString(selected)) {
metadata.extension = path.extname(selected).slice(1);
metadata.path = selected;
}
return metadata;
}

private async openImageSelector() {
analytics.logEvent('Open image selector');
this.setState({ imageSelectorOpen: true });
Expand All @@ -577,7 +512,7 @@ export class SourceSelector extends React.Component<
analytics.logEvent('Image selector closed');
return;
}
await this.selectSource(imagePath, sourceDestination.File).promise;
await this.selectSource(imagePath, 'File').promise;
} catch (error: any) {
exceptionReporter.report(error);
} finally {
Expand All @@ -588,7 +523,7 @@ export class SourceSelector extends React.Component<
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
const [file] = event.dataTransfer.files;
if (file) {
await this.selectSource(file.path, sourceDestination.File).promise;
await this.selectSource(file.path, 'File').promise;
}
}

Expand Down Expand Up @@ -704,7 +639,7 @@ export class SourceSelector extends React.Component<
{i18next.t('cancel')}
</ChangeButton>
)}
{!_.isNil(imageSize) && !imageLoading && (
{!isNil(imageSize) && !imageLoading && (
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
)}
</>
Expand Down Expand Up @@ -808,7 +743,7 @@ export class SourceSelector extends React.Component<
let promise;
({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL,
sourceDestination.Http,
'Http',
auth,
));
await promise;
Expand All @@ -831,10 +766,7 @@ export class SourceSelector extends React.Component<
if (originalList.length) {
const originalSource = originalList[0];
if (selectionImage?.drive?.device !== originalSource.device) {
this.selectSource(
originalSource,
sourceDestination.BlockDevice,
);
this.selectSource(originalSource, 'BlockDevice');
}
} else {
selectionState.deselectImage();
Expand All @@ -849,7 +781,7 @@ export class SourceSelector extends React.Component<
) {
return selectionState.deselectImage();
}
this.selectSource(drive, sourceDestination.BlockDevice);
this.selectSource(drive, 'BlockDevice');
}
}}
/>
Expand Down
1 change: 0 additions & 1 deletion lib/gui/app/models/selection-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export function selectSource(source: SourceMetadata) {
* @summary Get all selected drives' devices
*/
export function getSelectedDevices(): string[] {
console.log('fullState', store.getState());
return store.getState().getIn(['selection', 'devices']).toJS();
}

Expand Down
35 changes: 17 additions & 18 deletions lib/gui/app/modules/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,23 +109,19 @@ function terminateServer(server: any) {

// TODO: replace the custom ipc events by one generic "message" for all communication with the backend
function startApiAndSpawnChild({
apiEvents,
apiEventHandler,
withPrivileges,
}: {
apiEvents: string[];
apiEventHandler: any;
withPrivileges: boolean;
}): Promise<any> {
// There might be multiple Etcher instances running at
// the same time, also we might spawn multiple child and api so we must ensure each IPC
// server/client has a different name.
const IPC_SERVER_ID = `etcher-server-${
process.pid
}-${Date.now()}-${Math.random()}}`;
const IPC_CLIENT_ID = `etcher-client-${
process.pid
}-${Date.now()}-${Math.random()}}`;
const IPC_SERVER_ID = `etcher-server-${process.pid}-${Date.now()}-${
withPrivileges ? 'privileged' : 'unprivileged'
}}}`;
const IPC_CLIENT_ID = `etcher-client-${process.pid}-${Date.now()}-${
withPrivileges ? 'privileged' : 'unprivileged'
}}`;

const IPC_SOCKET_ROOT = path.join(
process.env.XDG_RUNTIME_DIR || os.tmpdir(),
Expand All @@ -143,19 +139,21 @@ function startApiAndSpawnChild({
console.log(message);
});

// api to register more handlers with callbacks
const registerHandler = (event: string, handler: any) => {
ipc.server.on(event, handler);
};

// once api is ready (means child process is connected) we pass the emit and terminate function to the caller
ipc.server.on('ready', (_: any, socket) => {
const emit = (channel: string, data: any) => {
ipc.server.emit(socket, channel, data);
};
resolve({ emit, terminateServer: () => terminateServer(ipc.server) });
});

// register all the api events we want to track, they will be handled by the apiEventHandler
apiEvents.forEach((event) => {
ipc.server.on(event, (payload) =>
apiEventHandler({ type: event, payload }),
);
resolve({
emit,
terminateServer: () => terminateServer(ipc.server),
registerHandler,
});
});

// on api error we terminate
Expand All @@ -174,6 +172,7 @@ function startApiAndSpawnChild({
IPC_SERVER_ID,
IPC_SOCKET_ROOT,
});
console.log('Child process started', IPC_SERVER_ID);
// this will happen if the child is spawned withPrivileges and privileges has been rejected
if (results.cancelled) {
reject();
Expand Down
Loading

0 comments on commit 15f82c5

Please sign in to comment.