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:
committed by
Facebook Github Bot
parent
825ecb8e23
commit
830c8067e4
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
37
src/utils/__tests__/promiseTimeout.node.js
Normal file
37
src/utils/__tests__/promiseTimeout.node.js
Normal 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');
|
||||||
|
});
|
||||||
@@ -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;
|
);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
23
src/utils/promiseTimeout.js
Normal file
23
src/utils/promiseTimeout.js
Normal 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]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user