Make sure disconnected devices / apps can be imported and exported
Summary: It should be possible to exported disconnected devices, so that flipper traces / support form reports can be created from them. This diff introduces this functionality. Support for plugins with custom export logic is introduced in a later diff. Issues fixed in this diff: - don't try to take a screenshot for a disconnected device (this would hang forever) - device plugins were always exported, regardless whether the user did select them or not - sandy plugins were never part of exported disconnected clients - increased the amount of data exported for device logs to ~10 MB. This makes more sense now as the logs will no longer be included in all cases - fixed issue where are plugins would appear to be enabled after the client disconnected (this bug is the result of some unfortunate naming of `isArchived` vs `isConnected` semantics. Will clean up those names in a later diff. Changelog: It is now possible to create a Flipper trace for disconnected devices and apps Reviewed By: nikoant Differential Revision: D26250894 fbshipit-source-id: 4dd0ec0cb152b1a8f649c31913e80efc25bcc5dd
This commit is contained in:
committed by
Facebook GitHub Bot
parent
8bc1b953c2
commit
ff7997b3fa
@@ -93,7 +93,10 @@ export default class AndroidDevice extends BaseDevice {
|
|||||||
this.adb.shell(this.serial, shellCommand);
|
this.adb.shell(this.serial, shellCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
screenshot(): Promise<Buffer> {
|
async screenshot(): Promise<Buffer> {
|
||||||
|
if (this.isArchived) {
|
||||||
|
return Buffer.from([]);
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.adb.screencap(this.serial).then((stream) => {
|
this.adb.screencap(this.serial).then((stream) => {
|
||||||
const chunks: Array<Buffer> = [];
|
const chunks: Array<Buffer> = [];
|
||||||
@@ -108,6 +111,9 @@ export default class AndroidDevice extends BaseDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async screenCaptureAvailable(): Promise<boolean> {
|
async screenCaptureAvailable(): Promise<boolean> {
|
||||||
|
if (this.isArchived) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await this.executeShell(
|
await this.executeShell(
|
||||||
`[ ! -f /system/bin/screenrecord ] && echo "File does not exist"`,
|
`[ ! -f /system/bin/screenrecord ] && echo "File does not exist"`,
|
||||||
|
|||||||
@@ -99,11 +99,15 @@ export default class BaseDevice {
|
|||||||
async exportState(
|
async exportState(
|
||||||
idler: Idler,
|
idler: Idler,
|
||||||
onStatusMessage: (msg: string) => void,
|
onStatusMessage: (msg: string) => void,
|
||||||
|
selectedPlugins: string[],
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
const pluginStates: Record<string, any> = {};
|
const pluginStates: Record<string, any> = {};
|
||||||
|
|
||||||
for (const instance of this.sandyPluginStates.values()) {
|
for (const instance of this.sandyPluginStates.values()) {
|
||||||
if (instance.isPersistable()) {
|
if (
|
||||||
|
selectedPlugins.includes(instance.definition.id) &&
|
||||||
|
instance.isPersistable()
|
||||||
|
) {
|
||||||
pluginStates[instance.definition.id] = await instance.exportState(
|
pluginStates[instance.definition.id] = await instance.exportState(
|
||||||
idler,
|
idler,
|
||||||
onStatusMessage,
|
onStatusMessage,
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ export default class IOSDevice extends BaseDevice {
|
|||||||
this.startLogListener();
|
this.startLogListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
screenshot(): Promise<Buffer> {
|
async screenshot(): Promise<Buffer> {
|
||||||
|
if (this.isArchived) {
|
||||||
|
return Buffer.from([]);
|
||||||
|
}
|
||||||
const tmpImageName = uuid() + '.png';
|
const tmpImageName = uuid() + '.png';
|
||||||
const tmpDirectory = (electron.app || electron.remote.app).getPath('temp');
|
const tmpDirectory = (electron.app || electron.remote.app).getPath('temp');
|
||||||
const tmpFilePath = path.join(tmpDirectory, tmpImageName);
|
const tmpFilePath = path.join(tmpDirectory, tmpImageName);
|
||||||
@@ -189,7 +192,7 @@ export default class IOSDevice extends BaseDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async screenCaptureAvailable() {
|
async screenCaptureAvailable() {
|
||||||
return this.deviceType === 'emulator';
|
return this.deviceType === 'emulator' && !this.isArchived;
|
||||||
}
|
}
|
||||||
|
|
||||||
async startScreenCapture(destination: string) {
|
async startScreenCapture(destination: string) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
DownloadOutlined,
|
DownloadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {Glyph, Layout, styled} from '../../ui';
|
import {Glyph, Layout, styled} from '../../ui';
|
||||||
import {theme, NUX, Tracked} from 'flipper-plugin';
|
import {theme, NUX, Tracked, useValue} from 'flipper-plugin';
|
||||||
import {useDispatch, useStore} from '../../utils/useStore';
|
import {useDispatch, useStore} from '../../utils/useStore';
|
||||||
import {
|
import {
|
||||||
computePluginLists,
|
computePluginLists,
|
||||||
@@ -85,7 +85,7 @@ export const PluginList = memo(function PluginList({
|
|||||||
connections.userStarredPlugins,
|
connections.userStarredPlugins,
|
||||||
pluginsChanged,
|
pluginsChanged,
|
||||||
]);
|
]);
|
||||||
const isArchived = !!activeDevice?.isArchived;
|
const isArchived = useValue(activeDevice?.archivedState, false);
|
||||||
|
|
||||||
const annotatedDownloadablePlugins = useMemoize<
|
const annotatedDownloadablePlugins = useMemoize<
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
|
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
|
||||||
import {Notification} from '../../plugin';
|
import {Notification} from '../../plugin';
|
||||||
import {default as Client, ClientExport} from '../../Client';
|
import {default as Client, ClientExport} from '../../Client';
|
||||||
import {State as PluginsState} from '../../reducers/plugins';
|
import {selectedPlugins, State as PluginsState} from '../../reducers/plugins';
|
||||||
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
|
||||||
import {
|
import {
|
||||||
TestUtils,
|
TestUtils,
|
||||||
@@ -1321,13 +1321,19 @@ test('Sandy device plugins are exported / imported properly', async () => {
|
|||||||
const device2 = store.getState().connections.devices[1];
|
const device2 = store.getState().connections.devices[1];
|
||||||
expect(device2).not.toBeFalsy();
|
expect(device2).not.toBeFalsy();
|
||||||
expect(device2).not.toBe(device);
|
expect(device2).not.toBe(device);
|
||||||
expect(device2.devicePlugins).toEqual([TestPlugin.id]);
|
expect(device2.devicePlugins).toEqual([sandyDeviceTestPlugin.id]);
|
||||||
|
|
||||||
const {counter} = device2.sandyPluginStates.get(TestPlugin.id)?.instanceApi;
|
const {counter} = device2.sandyPluginStates.get(
|
||||||
|
sandyDeviceTestPlugin.id,
|
||||||
|
)?.instanceApi;
|
||||||
counter.set(counter.get() + 1);
|
counter.set(counter.get() + 1);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
(await device.exportState(testIdler, testOnStatusMessage))[TestPlugin.id],
|
(
|
||||||
|
await device.exportState(testIdler, testOnStatusMessage, [
|
||||||
|
sandyDeviceTestPlugin.id,
|
||||||
|
])
|
||||||
|
)[sandyDeviceTestPlugin.id],
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"counter": 0,
|
"counter": 0,
|
||||||
@@ -1336,8 +1342,11 @@ test('Sandy device plugins are exported / imported properly', async () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect(await device2.exportState(testIdler, testOnStatusMessage))
|
expect(
|
||||||
.toMatchInlineSnapshot(`
|
await device2.exportState(testIdler, testOnStatusMessage, [
|
||||||
|
sandyDeviceTestPlugin.id,
|
||||||
|
]),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"TestPlugin": Object {
|
"TestPlugin": Object {
|
||||||
"counter": 4,
|
"counter": 4,
|
||||||
@@ -1358,6 +1367,7 @@ test('Sandy device plugins with custom export are export properly', async () =>
|
|||||||
.get(sandyDeviceTestPlugin.id)
|
.get(sandyDeviceTestPlugin.id)
|
||||||
?.instanceApi.enableCustomExport();
|
?.instanceApi.enableCustomExport();
|
||||||
|
|
||||||
|
store.dispatch(selectedPlugins([sandyDeviceTestPlugin.id]));
|
||||||
const storeExport = await exportStore(store);
|
const storeExport = await exportStore(store);
|
||||||
expect(storeExport.exportStoreData.device!.pluginStates).toEqual({
|
expect(storeExport.exportStoreData.device!.pluginStates).toEqual({
|
||||||
[sandyDeviceTestPlugin.id]: {customExport: true},
|
[sandyDeviceTestPlugin.id]: {customExport: true},
|
||||||
@@ -1522,8 +1532,9 @@ test('Sandy plugins with complex data are imported / exported correctly', async
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const {store} = await createMockFlipperWithPlugin(plugin);
|
const {store, client} = await createMockFlipperWithPlugin(plugin);
|
||||||
|
|
||||||
|
client.disconnect(); // lets make sure we can still export disconnected clients
|
||||||
const data = await exportStore(store);
|
const data = await exportStore(store);
|
||||||
expect(Object.values(data.exportStoreData.pluginStates2)).toMatchObject([
|
expect(Object.values(data.exportStoreData.pluginStates2)).toMatchObject([
|
||||||
{
|
{
|
||||||
@@ -1591,6 +1602,7 @@ test('Sandy device plugins with complex data are imported / exported correctly'
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {store} = await createMockFlipperWithPlugin(deviceplugin);
|
const {store} = await createMockFlipperWithPlugin(deviceplugin);
|
||||||
|
store.dispatch(selectedPlugins([deviceplugin.id]));
|
||||||
|
|
||||||
const data = await exportStore(store);
|
const data = await exportStore(store);
|
||||||
expect(data.exportStoreData.device?.pluginStates).toMatchObject({
|
expect(data.exportStoreData.device?.pluginStates).toMatchObject({
|
||||||
|
|||||||
@@ -431,10 +431,14 @@ export async function processStore(
|
|||||||
statusUpdate = () => {};
|
statusUpdate = () => {};
|
||||||
}
|
}
|
||||||
statusUpdate('Capturing screenshot...');
|
statusUpdate('Capturing screenshot...');
|
||||||
const deviceScreenshot = await capture(device).catch((e) => {
|
const deviceScreenshot = device.isArchived
|
||||||
console.warn('Failed to capture device screenshot when exporting. ' + e);
|
? null
|
||||||
return null;
|
: await capture(device).catch((e) => {
|
||||||
});
|
console.warn(
|
||||||
|
'Failed to capture device screenshot when exporting. ' + e,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
const processedClients = processClients(clients, serial, statusUpdate);
|
const processedClients = processClients(clients, serial, statusUpdate);
|
||||||
const processedPluginStates = processPluginStates({
|
const processedPluginStates = processPluginStates({
|
||||||
clients: processedClients,
|
clients: processedClients,
|
||||||
@@ -461,7 +465,7 @@ export async function processStore(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const devicePluginStates = await makeObjectSerializable(
|
const devicePluginStates = await makeObjectSerializable(
|
||||||
await device.exportState(idler, statusUpdate),
|
await device.exportState(idler, statusUpdate, selectedPlugins),
|
||||||
idler,
|
idler,
|
||||||
statusUpdate,
|
statusUpdate,
|
||||||
'Serializing device plugins',
|
'Serializing device plugins',
|
||||||
@@ -610,11 +614,7 @@ export function determinePluginsToProcess(
|
|||||||
const selectedPlugins = plugins.selectedPlugins;
|
const selectedPlugins = plugins.selectedPlugins;
|
||||||
|
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
if (
|
if (!selectedDevice || client.query.device_id !== selectedDevice.serial) {
|
||||||
!selectedDevice ||
|
|
||||||
selectedDevice.isArchived ||
|
|
||||||
client.query.device_id !== selectedDevice.serial
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const selectedFilteredPlugins = client
|
const selectedFilteredPlugins = client
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import type {
|
|||||||
PluginDetails,
|
PluginDetails,
|
||||||
} from 'flipper-plugin-lib';
|
} from 'flipper-plugin-lib';
|
||||||
import {filterNewestVersionOfEachPlugin} from '../dispatcher/plugins';
|
import {filterNewestVersionOfEachPlugin} from '../dispatcher/plugins';
|
||||||
|
import ArchivedDevice from '../devices/ArchivedDevice';
|
||||||
|
|
||||||
export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation plugin is enabled always, to make sure the navigation features works
|
export const defaultEnabledBackgroundPlugins = ['Navigation']; // The navigation plugin is enabled always, to make sure the navigation features works
|
||||||
|
|
||||||
@@ -300,11 +301,11 @@ function getFavoritePlugins(
|
|||||||
starredPlugins: undefined | string[],
|
starredPlugins: undefined | string[],
|
||||||
returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned
|
returnFavoredPlugins: boolean, // if false, unfavoried plugins are returned
|
||||||
): PluginDefinition[] {
|
): PluginDefinition[] {
|
||||||
if (device.isArchived) {
|
if (device instanceof ArchivedDevice) {
|
||||||
if (!returnFavoredPlugins) {
|
if (!returnFavoredPlugins) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
// for archived plugins, all stored plugins are enabled
|
// for *imported* devices, all stored plugins are enabled
|
||||||
return allPlugins.filter(
|
return allPlugins.filter(
|
||||||
(plugin) => client.plugins.indexOf(plugin.id) !== -1,
|
(plugin) => client.plugins.indexOf(plugin.id) !== -1,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ export function getFileName(extension: 'png' | 'mp4'): string {
|
|||||||
return `screencap-${new Date().toISOString().replace(/:/g, '')}.${extension}`;
|
return `screencap-${new Date().toISOString().replace(/:/g, '')}.${extension}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function capture(device: BaseDevice): Promise<string> {
|
export async function capture(device: BaseDevice): Promise<string> {
|
||||||
|
if (device.isArchived) {
|
||||||
|
console.log('Skipping screenshot for archived device');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const pngPath = path.join(CAPTURE_LOCATION, getFileName('png'));
|
const pngPath = path.join(CAPTURE_LOCATION, getFileName('png'));
|
||||||
return reportPlatformFailures(
|
return reportPlatformFailures(
|
||||||
device.screenshot().then((buffer) => writeBufferToFile(pngPath, buffer)),
|
device.screenshot().then((buffer) => writeBufferToFile(pngPath, buffer)),
|
||||||
|
|||||||
@@ -276,7 +276,13 @@ export class SandyPluginInstance extends BasePluginInstance {
|
|||||||
|
|
||||||
private assertConnected() {
|
private assertConnected() {
|
||||||
this.assertNotDestroyed();
|
this.assertNotDestroyed();
|
||||||
if (!this.connected.get()) {
|
if (
|
||||||
|
// This is a better-safe-than-sorry; just the first condition should suffice
|
||||||
|
!this.connected.get() ||
|
||||||
|
!this.realClient.connected.get() ||
|
||||||
|
!this.device.isConnected ||
|
||||||
|
this.device.isArchived
|
||||||
|
) {
|
||||||
throw new Error('Plugin is not connected');
|
throw new Error('Plugin is not connected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export function startPlugin<Module extends FlipperPluginModule<any>>(
|
|||||||
isBackgroundPlugin(_pluginId: string) {
|
isBackgroundPlugin(_pluginId: string) {
|
||||||
return !!options?.isBackgroundPlugin;
|
return !!options?.isBackgroundPlugin;
|
||||||
},
|
},
|
||||||
connected: createState(false),
|
connected: createState(true),
|
||||||
initPlugin() {
|
initPlugin() {
|
||||||
this.connected.set(true);
|
this.connected.set(true);
|
||||||
pluginInstance.connect();
|
pluginInstance.connect();
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ export function devicePlugin(client: DevicePluginClient) {
|
|||||||
return {
|
return {
|
||||||
logs: entries
|
logs: entries
|
||||||
.get()
|
.get()
|
||||||
.slice(-10000)
|
.slice(-100 * 1000)
|
||||||
.map((e) => e.entry),
|
.map((e) => e.entry),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user