diff --git a/headless/index.js b/headless/index.js index 5245943e7..7f6de350a 100644 --- a/headless/index.js +++ b/headless/index.js @@ -101,8 +101,8 @@ function startFlipper({ // current eventloop task here. setTimeout(() => { exportStore(store) - .then(output => { - originalConsole.log(output); + .then(({serializedString}) => { + originalConsole.log(serializedString); process.exit(); }) .catch(console.error); @@ -122,7 +122,9 @@ function startFlipper({ if (exit == 'sigint') { process.on('SIGINT', async () => { try { - originalConsole.log(await exportStore(store)); + const {serializedString, errorArray} = await exportStore(store); + errorArray.forEach(console.error); + originalConsole.log(serializedString); } catch (e) { console.error(e); } diff --git a/src/chrome/ShareSheet.js b/src/chrome/ShareSheet.js index 376ce9d44..3797a9375 100644 --- a/src/chrome/ShareSheet.js +++ b/src/chrome/ShareSheet.js @@ -59,10 +59,20 @@ const InfoText = styled(Text)({ marginBottom: 15, }); +const Padder = styled('div')( + ({paddingLeft, paddingRight, paddingBottom, paddingTop}) => ({ + paddingLeft: paddingLeft || 0, + paddingRight: paddingRight || 0, + paddingBottom: paddingBottom || 0, + paddingTop: paddingTop || 0, + }), +); + type Props = { onHide: () => mixed, }; type State = { + errorArray: Array, result: | ?{ error_class: string, @@ -79,20 +89,32 @@ export default class ShareSheet extends Component { }; state = { + errorArray: [], result: null, }; async componentDidMount() { - const storeData = await exportStore(this.context.store); - const result = await shareFlipperData(storeData); - this.setState({result}); - - if (result.flipperUrl) { - clipboard.writeText(String(result.flipperUrl)); - new window.Notification('Sharable Flipper trace created', { - body: 'URL copied to clipboard', - requireInteraction: true, + try { + const {serializedString, errorArray} = await exportStore( + this.context.store, + ); + const result = await shareFlipperData(serializedString); + this.setState({errorArray, result}); + if (result.flipperUrl) { + clipboard.writeText(String(result.flipperUrl)); + 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 { data might contain sensitve information like access tokens used in network requests. + {this.state.errorArray.length > 0 && ( + + + + The following errors occurred while exporting your + data + + {this.state.errorArray.map((e: Error) => { + return ( + {e.toString()} + ); + })} + + + )} ) : ( <> diff --git a/src/chrome/ShareSheetExportFile.js b/src/chrome/ShareSheetExportFile.js index 231313fd4..b255690c7 100644 --- a/src/chrome/ShareSheetExportFile.js +++ b/src/chrome/ShareSheetExportFile.js @@ -54,11 +54,21 @@ const InfoText = styled(Text)({ marginBottom: 15, }); +const Padder = styled('div')( + ({paddingLeft, paddingRight, paddingBottom, paddingTop}) => ({ + paddingLeft: paddingLeft || 0, + paddingRight: paddingRight || 0, + paddingBottom: paddingBottom || 0, + paddingTop: paddingTop || 0, + }), +); + type Props = { onHide: () => mixed, file: string, }; type State = { + errorArray: Array, result: ?{ success: boolean, error: ?Error, @@ -71,18 +81,19 @@ export default class ShareSheetExportFile extends Component { }; state = { + errorArray: [], result: null, }; async componentDidMount() { try { - await reportPlatformFailures( + const {errorArray} = await reportPlatformFailures( exportStoreToFile(this.props.file, this.context.store), `${EXPORT_FLIPPER_TRACE_EVENT}:UI`, ); - this.setState({result: {success: true, error: null}}); + this.setState({errorArray, result: {success: true, error: null}}); } 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 { might contain sensitive information like access tokens used in network requests. + {this.state.errorArray.length > 0 && ( + + + Errors: + {this.state.errorArray.map((e: Error) => { + return {e.toString()}; + })} + + + )} diff --git a/src/utils/__tests__/promiseTimeout.node.js b/src/utils/__tests__/promiseTimeout.node.js new file mode 100644 index 000000000..bc9017493 --- /dev/null +++ b/src/utils/__tests__/promiseTimeout.node.js @@ -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'); +}); diff --git a/src/utils/exportData.js b/src/utils/exportData.js index 167d3059d..4c3378789 100644 --- a/src/utils/exportData.js +++ b/src/utils/exportData.js @@ -23,7 +23,8 @@ import {remote} from 'electron'; import {serialize, deserialize} from './serialization'; import {readCurrentRevision} from './packageMetadata.js'; import {tryCatchReportPlatformFailures} from './metrics'; - +import {promisify} from 'util'; +import promiseTimeout from './promiseTimeout'; export const IMPORT_FLIPPER_TRACE_EVENT = 'import-flipper-trace'; export const EXPORT_FLIPPER_TRACE_EVENT = 'export-flipper-trace'; @@ -185,7 +186,7 @@ export const processStore = async ( export async function getStoreExport( store: MiddlewareAPI, -): Promise { +): Promise<{exportData: ?ExportType, errorArray: Array}> { const state = store.getState(); const {clients} = state.connections; const {pluginStates} = state; @@ -204,6 +205,7 @@ export async function getStoreExport( plugins.devicePlugins.forEach((val, key) => { pluginsMap.set(key, val); }); + const errorArray: Array = []; for (let client of clients) { for (let plugin of client.plugins) { const pluginClass: ?Class< @@ -212,12 +214,17 @@ export async function getStoreExport( const exportState = pluginClass ? pluginClass.exportPersistedState : null; if (exportState) { const key = pluginKey(client.id, plugin); - const data = await exportState( - callClient(client, plugin), - newPluginState[key], - store, - ); - newPluginState[key] = data; + try { + const data = await promiseTimeout( + 120000, // Timeout in 2 mins + exportState(callClient(client, plugin), newPluginState[key], store), + `Timed out while collecting data for ${plugin}`, + ); + newPluginState[key] = data; + } catch (e) { + errorArray.push(e); + continue; + } } } } @@ -226,7 +233,7 @@ export async function getStoreExport( const {selectedDevice} = store.getState().connections; const {devicePlugins} = store.getState().plugins; - return processStore( + const exportData = await processStore( activeNotifications, selectedDevice, newPluginState, @@ -234,35 +241,37 @@ export async function getStoreExport( devicePlugins, uuid.v4(), ); + return {exportData, errorArray}; } -export function exportStore(store: MiddlewareAPI): Promise { +export function exportStore( + store: MiddlewareAPI, +): Promise<{serializedString: string, errorArray: Array}> { getLogger().track('usage', EXPORT_FLIPPER_TRACE_EVENT); return new Promise(async (resolve, reject) => { - const storeExport = await getStoreExport(store); - if (!storeExport) { + const {exportData, errorArray} = await getStoreExport(store); + if (!exportData) { console.error('Make sure a device is connected'); reject('No device is selected'); } - const serializedString = serialize(storeExport); + const serializedString = serialize(exportData); if (serializedString.length <= 0) { reject('Serialize function returned empty string'); } - resolve(serializedString); + resolve({serializedString, errorArray}); }); } export const exportStoreToFile = ( exportFilePath: string, store: Store, -): Promise => { - return exportStore(store).then(storeString => { - fs.writeFile(exportFilePath, storeString, err => { - if (err) { - throw new Error(err); - } - return; - }); +): Promise<{errorArray: Array}> => { + return exportStore(store).then(({serializedString, errorArray}) => { + return promisify(fs.writeFile)(exportFilePath, serializedString).then( + () => { + return {errorArray}; + }, + ); }); }; diff --git a/src/utils/promiseTimeout.js b/src/utils/promiseTimeout.js new file mode 100644 index 000000000..71ea0b2c5 --- /dev/null +++ b/src/utils/promiseTimeout.js @@ -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( + ms: number, + promise: Promise, + timeoutMessage: ?string, +): Promise | Promise { + // Create a promise that rejects in 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]); +}