/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import React from 'react'; import {State, Store} from '../reducers/index'; import { FlipperServer, Logger, NoLongerConnectedToClientError, isTest, DeviceDescription, } from 'flipper-common'; import Client from '../Client'; import {notification} from 'antd'; import BaseDevice from '../devices/BaseDevice'; import {ClientDescription, timeout} from 'flipper-common'; import {reportPlatformFailures} from 'flipper-common'; import {sideEffect} from '../utils/sideEffect'; export function connectFlipperServerToStore( server: FlipperServer, store: Store, logger: Logger, ) { server.on('notification', ({type, title, description}) => { const text = `[$type] ${title}: ${description}`; console.warn(text); notification.open({ message: title, description: description, type: type, duration: 0, key: text, }); }); server.on('server-error', (err) => { notification.error({ message: 'Failed to start connection server', description: err.code === 'EADDRINUSE' ? ( <> Couldn't start connection server. Looks like you have multiple copies of Flipper running or another process is using the same port(s). As a result devices will not be able to connect to Flipper.

Please try to kill the offending process by running{' '} kill $(lsof -ti:PORTNUMBER) and restart flipper.

{'' + err} ) : ( <>Failed to start Flipper server: ${err.message} ), duration: null, }); }); server.on('device-connected', (deviceInfo) => { handleDeviceConnected(server, store, logger, deviceInfo); }); server.on('device-disconnected', (device) => { logger.track('usage', 'unregister-device', { os: device.os, serial: device.serial, }); // N.B.: note that we don't remove the device, we keep it in offline }); server.on('client-setup', (client) => { store.dispatch({ type: 'START_CLIENT_SETUP', payload: client, }); }); server.on('client-connected', (payload: ClientDescription) => handleClientConnected(server, store, logger, payload), ); server.on('client-disconnected', ({id}) => { const existingClient = store.getState().connections.clients.get(id); existingClient?.disconnect(); }); server.on('client-message', ({id, message}) => { const existingClient = store.getState().connections.clients.get(id); existingClient?.onMessage(message); }); if (typeof window !== 'undefined') { window.addEventListener('beforeunload', () => { server.close(); }); } let sideEffectDisposer: undefined | (() => void); if (!isTest()) { sideEffectDisposer = startSideEffects(store, server); } console.log( 'Flipper server started and accepting device / client connections', ); server .exec('device-list') .then((devices) => { // register all devices devices.forEach((device) => { handleDeviceConnected(server, store, logger, device); }); }) .then(() => { return server.exec('client-list'); }) .then((clients) => { clients.forEach((client) => { handleClientConnected(server, store, logger, client); }); }) .catch((e) => { console.error('Failed to get initial device/client list: ', e); }); return () => { sideEffectDisposer?.(); server.close(); }; } function startSideEffects(store: Store, server: FlipperServer) { const dispose1 = sideEffect( store, { name: 'settingsPersistor', throttleMs: 100, }, (state) => state.settingsState, (settings) => { server.exec('persist-settings', settings).catch((e) => { console.error('Failed to persist Flipper settings', e); }); }, ); const dispose2 = sideEffect( store, { name: 'launcherSettingsPersistor', throttleMs: 100, }, (state) => state.launcherSettingsState, (settings) => { server.exec('persist-launcher-settings', settings).catch((e) => { console.error('Failed to persist launcher settings', e); }); }, ); return () => { dispose1(); dispose2(); }; } function handleDeviceConnected( server: FlipperServer, store: Store, logger: Logger, deviceInfo: DeviceDescription, ) { logger.track('usage', 'register-device', { os: deviceInfo.os, name: deviceInfo.title, serial: deviceInfo.serial, }); const existing = store .getState() .connections.devices.find((device) => device.serial === deviceInfo.serial); // handled outside reducer, as it might emit new redux actions... if (existing) { if (existing.connected.get()) { console.warn( `Tried to replace still connected device '${existing.serial}' with a new instance.`, ); } existing.destroy(); } const device = new BaseDevice(server, deviceInfo); device.loadDevicePlugins( store.getState().plugins.devicePlugins, store.getState().connections.enabledDevicePlugins, ); store.dispatch({ type: 'REGISTER_DEVICE', payload: device, }); } export async function handleClientConnected( server: Pick, store: Store, logger: Logger, {id, query}: ClientDescription, ) { const {connections} = store.getState(); const existingClient = connections.clients.get(id); if (existingClient) { existingClient.destroy(); store.dispatch({ type: 'CLEAR_CLIENT_PLUGINS_STATE', payload: { clientId: id, devicePlugins: new Set(), }, }); store.dispatch({ type: 'CLIENT_REMOVED', payload: id, }); } console.log( `[conn] Searching matching device ${query.device_id} for client ${query.app}...`, ); const device = getDeviceBySerial(store.getState(), query.device_id) ?? (await findDeviceForConnection(store, query.app, query.device_id).catch( (e) => { console.error( `[conn] Failed to find device '${query.device_id}' while connection app '${query.app}'`, e, ); notification.error({ message: 'Connection failed', description: `Failed to find device '${query.device_id}' while trying to connect app '${query.app}'`, duration: 0, }); }, )); if (!device) { return; } const client = new Client( id, query, { send(data: any) { server.exec('client-request', id, data); }, async sendExpectResponse(data: any) { return await server.exec('client-request-response', id, data); }, }, logger, store, undefined, device, ); console.debug( `Device client initialized: ${client.id}. Supported plugins: ${Array.from( client.plugins, ).join(', ')}`, 'server', ); store.dispatch({ type: 'NEW_CLIENT', payload: client, }); try { await timeout( 30 * 1000, client.init(), `[conn] Failed to initialize client ${query.app} on ${query.device_id} in a timely manner`, ); console.log( `[conn] ${query.app} on ${query.device_id} connected and ready.`, ); } catch (e) { if (e instanceof NoLongerConnectedToClientError) { console.warn( `[conn] Client ${query.app} on ${query.device_id} disconnected while initialising`, ); return; } throw e; } } function getDeviceBySerial( state: State, serial: string, ): BaseDevice | undefined { return state.connections.devices.find((device) => device.serial === serial); } async function findDeviceForConnection( store: Store, clientId: string, serial: string, ): Promise { let lastSeenDeviceList: BaseDevice[] = []; /* All clients should have a corresponding Device in the store. However, clients can connect before a device is registered, so wait a while for the device to be registered if it isn't already. */ return reportPlatformFailures( new Promise((resolve, reject) => { let unsubscribe: () => void = () => {}; const timeout = setTimeout(() => { unsubscribe(); reject( new Error( `Timed out waiting for device ${serial} for client ${clientId}`, ), ); }, 15000); unsubscribe = sideEffect( store, {name: 'waitForDevice', throttleMs: 100}, (state) => state.connections.devices, (newDeviceList) => { if (newDeviceList === lastSeenDeviceList) { return; } lastSeenDeviceList = newDeviceList; const matchingDevice = newDeviceList.find( (device) => device.serial === serial, ); if (matchingDevice) { console.log(`[conn] Found device for: ${clientId} on ${serial}.`); clearTimeout(timeout); resolve(matchingDevice); unsubscribe(); } }, ); }), 'client-setMatchingDevice', ); }