diff --git a/headless/index.js b/headless/index.js index 5852737cc..f74614d2b 100644 --- a/headless/index.js +++ b/headless/index.js @@ -97,10 +97,8 @@ function startFlipper({ const logger = initLogger(store, {isHeadless: true}); dispatcher(store, logger); - process.on('SIGINT', () => { - originalConsole.log( - JSON.stringify(serializeStore(store.getState()), null, 2), - ); + process.on('SIGINT', async () => { + originalConsole.log(JSON.stringify(await serializeStore(store), null, 2)); process.exit(); }); } diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm index f7d35a3da..30af5dc7f 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm @@ -86,6 +86,10 @@ [connection receive:@"getRoot" withBlock:^(NSDictionary *params, id responder) { FlipperPerformBlockOnMainThread(^{ [weakSelf onCallGetRoot: responder]; }, responder); }]; + + [connection receive:@"getAllNodes" withBlock:^(NSDictionary *params, id responder) { + FlipperPerformBlockOnMainThread(^{ [weakSelf onCallGetAllNodesWithResponder: responder]; }, responder); + }]; [connection receive:@"getNodes" withBlock:^(NSDictionary *params, id responder) { FlipperPerformBlockOnMainThread(^{ [weakSelf onCallGetNodes: params[@"ids"] withResponder: responder]; }, responder); @@ -134,6 +138,29 @@ [responder success: rootNode]; } +- (void)populateAllNodesFromNode:(nonnull NSString *)identifier inDictionary:(nonnull NSMutableDictionary *)mutableDict { + NSDictionary *nodeDict = [self getNode:identifier]; + mutableDict[identifier] = nodeDict; + NSArray *arr = nodeDict[@"children"]; + for (NSString *childIdentifier in arr) { + [self populateAllNodesFromNode:childIdentifier inDictionary:mutableDict]; + } + return; +} + +- (void)onCallGetAllNodesWithResponder:(nonnull id)responder { + NSMutableArray *allNodes = @[].mutableCopy; + NSString *identifier = [self trackObject: _rootNode]; + NSDictionary *rootNode = [self getNode: identifier]; + if (!rootNode) { + return [responder error:@{@"error": [NSString stringWithFormat:@"getNode returned nil for the rootNode %@, while getting all the nodes", identifier]}]; + } + [allNodes addObject:rootNode]; + NSMutableDictionary *allNodesDict = @{}.mutableCopy; + [self populateAllNodesFromNode:identifier inDictionary:allNodesDict]; + [responder success:@{@"allNodes": @{@"rootElement": identifier, @"elements": allNodesDict}}]; +} + - (void)onCallGetNodes:(NSArray *)nodeIds withResponder:(id)responder { NSMutableArray *elements = [NSMutableArray new]; @@ -417,6 +444,10 @@ return objectIdentifier; } +- (BOOL)runInBackground { + return true; +} + @end #endif diff --git a/src/Client.js b/src/Client.js index 71081191d..19cc47139 100644 --- a/src/Client.js +++ b/src/Client.js @@ -171,7 +171,9 @@ export default class Client extends EventEmitter { // get the supported plugins async getPlugins(): Promise { - const plugins = await this.rawCall('getPlugins').then(data => data.plugins); + const plugins = await this.rawCall('getPlugins', false).then( + data => data.plugins, + ); this.plugins = plugins; return plugins; } @@ -345,7 +347,11 @@ export default class Client extends EventEmitter { methodCallbacks.delete(callback); } - rawCall(method: string, params?: Object): Promise { + rawCall( + method: string, + fromPlugin: boolean, + params?: Object, + ): Promise { return new Promise((resolve, reject) => { const id = this.messageIdCounter++; const metadata: RequestMetadata = { @@ -378,13 +384,13 @@ export default class Client extends EventEmitter { const mark = this.getPerformanceMark(metadata); performance.mark(mark); - if (this.isAcceptingMessagesFromPlugin(plugin)) { + if (!fromPlugin || this.isAcceptingMessagesFromPlugin(plugin)) { this.connection && this.connection .requestResponse({data: JSON.stringify(data)}) .subscribe({ onComplete: payload => { - if (this.isAcceptingMessagesFromPlugin(plugin)) { + if (!fromPlugin || this.isAcceptingMessagesFromPlugin(plugin)) { const logEventName = this.getLogEventName(data); this.logger.trackTimeSince(mark, logEventName); const response: {| @@ -452,9 +458,14 @@ export default class Client extends EventEmitter { } } - call(api: string, method: string, params?: Object): Promise { + call( + api: string, + method: string, + fromPlugin: boolean, + params?: Object, + ): Promise { return reportPluginFailures( - this.rawCall('execute', {api, method, params}), + this.rawCall('execute', fromPlugin, {api, method, params}), `Call-${method}`, api, ); @@ -474,7 +485,7 @@ export default class Client extends EventEmitter { if (this.sdkVersion < 2) { return Promise.resolve(false); } - return this.rawCall('isMethodSupported', {api, method}).then( + return this.rawCall('isMethodSupported', true, {api, method}).then( response => response.isSupported, ); } diff --git a/src/PluginContainer.js b/src/PluginContainer.js index 241c1d55c..7b5f8e82a 100644 --- a/src/PluginContainer.js +++ b/src/PluginContainer.js @@ -8,7 +8,7 @@ import type {FlipperPlugin, FlipperDevicePlugin} from './plugin.js'; import type {Logger} from './fb-interfaces/Logger'; import BaseDevice from './devices/BaseDevice.js'; import type {Props as PluginProps} from './plugin'; - +import {pluginKey as getPluginKey} from './reducers/pluginStates'; import Client from './Client.js'; import { ErrorBoundary, @@ -168,7 +168,7 @@ export default connect( } target = selectedDevice; if (activePlugin) { - pluginKey = `${selectedDevice.serial}#${activePlugin.id}`; + pluginKey = getPluginKey(selectedDevice.serial, activePlugin.id); } else { target = clients.find((client: Client) => client.id === selectedApp); activePlugin = clientPlugins.get(selectedPlugin); @@ -177,7 +177,7 @@ export default connect( `Plugin "${selectedPlugin || ''}" could not be found.`, ); } - pluginKey = `${target.id}#${activePlugin.id}`; + pluginKey = getPluginKey(target.id, activePlugin.id); } } diff --git a/src/index.js b/src/index.js index 172172e5a..dc8084338 100644 --- a/src/index.js +++ b/src/index.js @@ -14,8 +14,10 @@ export { FlipperBasePlugin, FlipperPlugin, FlipperDevicePlugin, + callClient, } from './plugin.js'; export type {PluginClient} from './plugin.js'; +export {default as Client} from './Client.js'; export {clipboard} from 'electron'; export * from './fb-stubs/constants.js'; export * from './fb-stubs/createPaste.js'; diff --git a/src/plugin.js b/src/plugin.js index 5e9978da4..aa062ed53 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -19,6 +19,15 @@ import IOSDevice from './devices/IOSDevice'; const invariant = require('invariant'); +// This function is intended to be called from outside of the plugin. +// If you want to `call` from the plugin use, this.client.call +export function callClient( + client: Client, + id: string, +): (string, ?Object) => Promise { + return (method, params) => client.call(id, method, false, params); +} + export type PluginClient = {| send: (method: string, params?: Object) => void, call: (method: string, params?: Object) => Promise, @@ -69,6 +78,11 @@ export class FlipperBasePlugin< method: string, data: Object, ) => $Shape; + static exportPersistedState: ?( + callClient: (string, ?Object) => Promise, + persistedState: ?PersistedState, + store: ?Store, + ) => Promise; static getActiveNotifications: ?( persistedState: PersistedState, ) => Array; @@ -160,7 +174,7 @@ export class FlipperPlugin extends FlipperBasePlugin< // $FlowFixMe props.target will be instance of Client this.realClient = props.target; this.client = { - call: (method, params) => this.realClient.call(id, method, params), + call: (method, params) => this.realClient.call(id, method, true, params), send: (method, params) => this.realClient.send(id, method, params), subscribe: (method, callback) => { this.subscriptions.push({ diff --git a/src/plugins/layout/layout2/index.js b/src/plugins/layout/layout2/index.js index 2a301e6be..f91c11e0e 100644 --- a/src/plugins/layout/layout2/index.js +++ b/src/plugins/layout/layout2/index.js @@ -5,7 +5,7 @@ * @format */ -import type {ElementID, Element, ElementSearchResultSet} from 'flipper'; +import type {ElementID, Element, ElementSearchResultSet, Store} from 'flipper'; import { FlexColumn, @@ -51,6 +51,22 @@ const BetaBar = styled(Toolbar)({ }); export default class Layout extends FlipperPlugin { + static exportPersistedState = ( + callClient: (string, ?Object) => Promise, + persistedState: ?PersistedState, + store: ?Store, + ): Promise => { + const defaultPromise = Promise.resolve(persistedState); + if (!store) { + return defaultPromise; + } + const selectedDevice = store.getState().connections.selectedDevice; + if (selectedDevice && selectedDevice.os === 'iOS') { + return callClient('getAllNodes').then(({allNodes}) => allNodes); + } + return defaultPromise; + }; + static defaultPersistedState = { rootElement: null, rootAXElement: null, @@ -68,11 +84,6 @@ export default class Layout extends FlipperPlugin { searchResults: null, }; - componentDidMount() { - // reset the data, as it might be outdated - this.props.setPersistedState(Layout.defaultPersistedState); - } - init() { // persist searchActive state when moving between plugins to prevent multiple // TouchOverlayViews since we can't edit the view heirarchy in onDisconnect diff --git a/src/reducers/pluginStates.js b/src/reducers/pluginStates.js index 3eb2bdbf5..c08ca0cfd 100644 --- a/src/reducers/pluginStates.js +++ b/src/reducers/pluginStates.js @@ -9,6 +9,10 @@ export type State = { [pluginKey: string]: Object, }; +export const pluginKey = (serial: string, pluginName: string): string => { + return `${serial}#${pluginName}`; +}; + export type Action = | { type: 'SET_PLUGIN_STATE', diff --git a/src/utils/exportData.js b/src/utils/exportData.js index 66d7458db..4ad9a25d7 100644 --- a/src/utils/exportData.js +++ b/src/utils/exportData.js @@ -10,8 +10,8 @@ import type {State as PluginStates} from '../reducers/pluginStates'; import type {PluginNotification} from '../reducers/notifications.js'; import type {ClientExport} from '../Client.js'; import type {State as PluginStatesState} from '../reducers/pluginStates'; -import type {State} from '../reducers/index'; -import {FlipperDevicePlugin} from '../plugin.js'; +import {pluginKey} from '../reducers/pluginStates'; +import {FlipperDevicePlugin, FlipperPlugin, callClient} from '../plugin.js'; import {default as BaseDevice} from '../devices/BaseDevice'; import {default as ArchivedDevice} from '../devices/ArchivedDevice'; import {default as Client} from '../Client'; @@ -174,17 +174,51 @@ export const processStore = ( return null; }; -export function serializeStore(state: State): ?ExportType { - const {activeNotifications} = state.notifications; - const {selectedDevice, clients} = state.connections; +export async function serializeStore(store: Store): Promise { + const state = store.getState(); + const {clients} = state.connections; const {pluginStates} = state; - const {devicePlugins} = state.plugins; + const {plugins} = state; + const newPluginState = {...pluginStates}; // TODO: T39612653 Make Client mockable. Currently rsocket logic is tightly coupled. // Not passing the entire state as currently Client is not mockable. + + const pluginsMap: Map< + string, + Class | FlipperPlugin<>>, + > = new Map([]); + plugins.clientPlugins.forEach((val, key) => { + pluginsMap.set(key, val); + }); + plugins.devicePlugins.forEach((val, key) => { + pluginsMap.set(key, val); + }); + for (let client of clients) { + for (let plugin of client.plugins) { + const pluginClass: ?Class< + FlipperDevicePlugin<> | FlipperPlugin<>, + > = plugin ? pluginsMap.get(plugin) : null; + 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; + } + } + } + + const {activeNotifications} = store.getState().notifications; + const {selectedDevice} = store.getState().connections; + const {devicePlugins} = store.getState().plugins; + return processStore( activeNotifications, selectedDevice, - pluginStates, + newPluginState, clients.map(client => client.toJSON()), devicePlugins, uuid.v4(), @@ -193,21 +227,21 @@ export function serializeStore(state: State): ?ExportType { export const exportStoreToFile = ( exportFilePath: string, - data: Store, + store: Store, ): Promise => { - const json = serializeStore(data.getState()); - if (json) { - return new Promise((resolve, reject) => { - fs.writeFile(exportFilePath, serialize(json), err => { - if (err) { - reject(err); - } - resolve(); - }); + return new Promise(async (resolve, reject) => { + const json = await serializeStore(store); + if (!json) { + console.error('Make sure a device is connected'); + reject('No device is selected'); + } + fs.writeFile(exportFilePath, serialize(json), err => { + if (err) { + reject(err); + } + resolve(); }); - } - console.error('Make sure a device is connected'); - return new Promise.reject(new Error('No device is selected')); + }); }; export const importFileToStore = (file: string, store: Store) => {