Timeout promise while exporting flipper plugin

Summary:
Added a promiseTimeout util so that we limit the time taken to export flipper plugin. I had received a feedback from the user stating that that the loader for the export stays for ages and never finishes. So this diff adds a timeout to the promise which is returned by `exportPersistedState` function. This function is currently implemented just by Layout inspector to fetch the entire view hierarchy. I suspect the the former mentioned bug may be due to the unresponsive client.

This diff also shows the plugin and the client information for which `exportPersistedState` timed out. Also this will hopefully solve the problem surfaced recently stating that the bug report gets stuck, the reason for which I suspect would be the same as the above mentioned reason, because we export flipper data when we create a bug report.

Reviewed By: danielbuechele

Differential Revision: D14712633

fbshipit-source-id: 35f8cfa722ec3b7ff94ebda943d618834ac3207d
This commit is contained in:
Pritesh Nandgaonkar
2019-04-04 04:17:20 -07:00
committed by Facebook Github Bot
parent 825ecb8e23
commit 830c8067e4
6 changed files with 166 additions and 37 deletions

View File

@@ -101,8 +101,8 @@ function startFlipper({
// current eventloop task here. // current eventloop task here.
setTimeout(() => { setTimeout(() => {
exportStore(store) exportStore(store)
.then(output => { .then(({serializedString}) => {
originalConsole.log(output); originalConsole.log(serializedString);
process.exit(); process.exit();
}) })
.catch(console.error); .catch(console.error);
@@ -122,7 +122,9 @@ function startFlipper({
if (exit == 'sigint') { if (exit == 'sigint') {
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
try { try {
originalConsole.log(await exportStore(store)); const {serializedString, errorArray} = await exportStore(store);
errorArray.forEach(console.error);
originalConsole.log(serializedString);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@@ -59,10 +59,20 @@ const InfoText = styled(Text)({
marginBottom: 15, marginBottom: 15,
}); });
const Padder = styled('div')(
({paddingLeft, paddingRight, paddingBottom, paddingTop}) => ({
paddingLeft: paddingLeft || 0,
paddingRight: paddingRight || 0,
paddingBottom: paddingBottom || 0,
paddingTop: paddingTop || 0,
}),
);
type Props = { type Props = {
onHide: () => mixed, onHide: () => mixed,
}; };
type State = { type State = {
errorArray: Array<Error>,
result: result:
| ?{ | ?{
error_class: string, error_class: string,
@@ -79,20 +89,32 @@ export default class ShareSheet extends Component<Props, State> {
}; };
state = { state = {
errorArray: [],
result: null, result: null,
}; };
async componentDidMount() { async componentDidMount() {
const storeData = await exportStore(this.context.store); try {
const result = await shareFlipperData(storeData); const {serializedString, errorArray} = await exportStore(
this.setState({result}); this.context.store,
);
if (result.flipperUrl) { const result = await shareFlipperData(serializedString);
clipboard.writeText(String(result.flipperUrl)); this.setState({errorArray, result});
new window.Notification('Sharable Flipper trace created', { if (result.flipperUrl) {
body: 'URL copied to clipboard', clipboard.writeText(String(result.flipperUrl));
requireInteraction: true, new window.Notification('Sharable Flipper trace created', {
body: 'URL copied to clipboard',
requireInteraction: true,
});
}
} catch (e) {
this.setState({
result: {
error_class: 'EXPORT_ERROR',
error: e,
},
}); });
return;
} }
} }
@@ -116,6 +138,21 @@ export default class ShareSheet extends Component<Props, State> {
data might contain sensitve information like access tokens data might contain sensitve information like access tokens
used in network requests. used in network requests.
</InfoText> </InfoText>
{this.state.errorArray.length > 0 && (
<Padder paddingBottom={8}>
<FlexColumn>
<Title bold>
The following errors occurred while exporting your
data
</Title>
{this.state.errorArray.map((e: Error) => {
return (
<ErrorMessage code>{e.toString()}</ErrorMessage>
);
})}
</FlexColumn>
</Padder>
)}
</> </>
) : ( ) : (
<> <>

View File

@@ -54,11 +54,21 @@ const InfoText = styled(Text)({
marginBottom: 15, marginBottom: 15,
}); });
const Padder = styled('div')(
({paddingLeft, paddingRight, paddingBottom, paddingTop}) => ({
paddingLeft: paddingLeft || 0,
paddingRight: paddingRight || 0,
paddingBottom: paddingBottom || 0,
paddingTop: paddingTop || 0,
}),
);
type Props = { type Props = {
onHide: () => mixed, onHide: () => mixed,
file: string, file: string,
}; };
type State = { type State = {
errorArray: Array<Error>,
result: ?{ result: ?{
success: boolean, success: boolean,
error: ?Error, error: ?Error,
@@ -71,18 +81,19 @@ export default class ShareSheetExportFile extends Component<Props, State> {
}; };
state = { state = {
errorArray: [],
result: null, result: null,
}; };
async componentDidMount() { async componentDidMount() {
try { try {
await reportPlatformFailures( const {errorArray} = await reportPlatformFailures(
exportStoreToFile(this.props.file, this.context.store), exportStoreToFile(this.props.file, this.context.store),
`${EXPORT_FLIPPER_TRACE_EVENT}:UI`, `${EXPORT_FLIPPER_TRACE_EVENT}:UI`,
); );
this.setState({result: {success: true, error: null}}); this.setState({errorArray, result: {success: true, error: null}});
} catch (err) { } catch (err) {
this.setState({result: {success: false, error: err}}); this.setState({errorArray: [], result: {success: false, error: err}});
} }
} }
@@ -100,6 +111,16 @@ export default class ShareSheetExportFile extends Component<Props, State> {
might contain sensitive information like access tokens used in might contain sensitive information like access tokens used in
network requests. network requests.
</InfoText> </InfoText>
{this.state.errorArray.length > 0 && (
<Padder paddingBottom={8}>
<FlexColumn>
<Title bold>Errors: </Title>
{this.state.errorArray.map((e: Error) => {
return <ErrorMessage code>{e.toString()}</ErrorMessage>;
})}
</FlexColumn>
</Padder>
)}
</FlexColumn> </FlexColumn>
<FlexRow> <FlexRow>
<Spacer /> <Spacer />

View File

@@ -0,0 +1,37 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import promiseTimeout from '../promiseTimeout';
test('test promiseTimeout for timeout to happen', () => {
let promise = promiseTimeout(
200,
new Promise((resolve, reject) => {
let id = setTimeout(() => {
clearTimeout(id);
resolve();
}, 500);
return 'Executed';
}),
'Timed out',
);
return expect(promise).rejects.toThrow('Timed out');
});
test('test promiseTimeout for timeout not to happen', () => {
let promise = promiseTimeout(
200,
new Promise((resolve, reject) => {
let id = setTimeout(() => {
clearTimeout(id);
resolve();
}, 100);
resolve('Executed');
}),
'Timed out',
);
return expect(promise).resolves.toBe('Executed');
});

View File

@@ -23,7 +23,8 @@ import {remote} from 'electron';
import {serialize, deserialize} from './serialization'; import {serialize, deserialize} from './serialization';
import {readCurrentRevision} from './packageMetadata.js'; import {readCurrentRevision} from './packageMetadata.js';
import {tryCatchReportPlatformFailures} from './metrics'; import {tryCatchReportPlatformFailures} from './metrics';
import {promisify} from 'util';
import promiseTimeout from './promiseTimeout';
export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace'; export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace';
export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace'; export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace';
@@ -185,7 +186,7 @@ export const processStore = async (
export async function getStoreExport( export async function getStoreExport(
store: MiddlewareAPI, store: MiddlewareAPI,
): Promise<?ExportType> { ): Promise<{exportData: ?ExportType, errorArray: Array<Error>}> {
const state = store.getState(); const state = store.getState();
const {clients} = state.connections; const {clients} = state.connections;
const {pluginStates} = state; const {pluginStates} = state;
@@ -204,6 +205,7 @@ export async function getStoreExport(
plugins.devicePlugins.forEach((val, key) => { plugins.devicePlugins.forEach((val, key) => {
pluginsMap.set(key, val); pluginsMap.set(key, val);
}); });
const errorArray: Array<Error> = [];
for (let client of clients) { for (let client of clients) {
for (let plugin of client.plugins) { for (let plugin of client.plugins) {
const pluginClass: ?Class< const pluginClass: ?Class<
@@ -212,12 +214,17 @@ export async function getStoreExport(
const exportState = pluginClass ? pluginClass.exportPersistedState : null; const exportState = pluginClass ? pluginClass.exportPersistedState : null;
if (exportState) { if (exportState) {
const key = pluginKey(client.id, plugin); const key = pluginKey(client.id, plugin);
const data = await exportState( try {
callClient(client, plugin), const data = await promiseTimeout(
newPluginState[key], 120000, // Timeout in 2 mins
store, exportState(callClient(client, plugin), newPluginState[key], store),
); `Timed out while collecting data for ${plugin}`,
newPluginState[key] = data; );
newPluginState[key] = data;
} catch (e) {
errorArray.push(e);
continue;
}
} }
} }
} }
@@ -226,7 +233,7 @@ export async function getStoreExport(
const {selectedDevice} = store.getState().connections; const {selectedDevice} = store.getState().connections;
const {devicePlugins} = store.getState().plugins; const {devicePlugins} = store.getState().plugins;
return processStore( const exportData = await processStore(
activeNotifications, activeNotifications,
selectedDevice, selectedDevice,
newPluginState, newPluginState,
@@ -234,35 +241,37 @@ export async function getStoreExport(
devicePlugins, devicePlugins,
uuid.v4(), uuid.v4(),
); );
return {exportData, errorArray};
} }
export function exportStore(store: MiddlewareAPI): Promise<string> { export function exportStore(
store: MiddlewareAPI,
): Promise<{serializedString: string, errorArray: Array<Error>}> {
getLogger().track('usage', EXPORT_FLIPPER_TRACE_EVENT); getLogger().track('usage', EXPORT_FLIPPER_TRACE_EVENT);
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const storeExport = await getStoreExport(store); const {exportData, errorArray} = await getStoreExport(store);
if (!storeExport) { if (!exportData) {
console.error('Make sure a device is connected'); console.error('Make sure a device is connected');
reject('No device is selected'); reject('No device is selected');
} }
const serializedString = serialize(storeExport); const serializedString = serialize(exportData);
if (serializedString.length <= 0) { if (serializedString.length <= 0) {
reject('Serialize function returned empty string'); reject('Serialize function returned empty string');
} }
resolve(serializedString); resolve({serializedString, errorArray});
}); });
} }
export const exportStoreToFile = ( export const exportStoreToFile = (
exportFilePath: string, exportFilePath: string,
store: Store, store: Store,
): Promise<void> => { ): Promise<{errorArray: Array<Error>}> => {
return exportStore(store).then(storeString => { return exportStore(store).then(({serializedString, errorArray}) => {
fs.writeFile(exportFilePath, storeString, err => { return promisify(fs.writeFile)(exportFilePath, serializedString).then(
if (err) { () => {
throw new Error(err); return {errorArray};
} },
return; );
});
}); });
}; };

View File

@@ -0,0 +1,23 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
export default function promiseTimeout<T>(
ms: number,
promise: Promise<T>,
timeoutMessage: ?string,
): Promise<T> | Promise<void> {
// Create a promise that rejects in <ms> milliseconds
let timeout = new Promise((resolve, reject) => {
let id = setTimeout(() => {
clearTimeout(id);
reject(new Error(timeoutMessage || `Timed out in ${ms} ms.`));
}, ms);
});
// Returns a race between our timeout and the passed in promise
return Promise.race([promise, timeout]);
}