UNPKG

@hippy/debug-server-next

Version:
390 lines (389 loc) 17.3 kB
"use strict"; /* * Tencent is pleased to support the open source community by making * Hippy available. * * Copyright (C) 2017-2019 THL A29 Limited, a Tencent company. * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.updateIWDPAppClient = exports.subscribeByIWDP = exports.cleanAllDebugTargets = exports.cleanDebugTarget = exports.subscribeCommand = void 0; const lodash_1 = require("lodash"); const enum_1 = require("@debug-server-next/@types/enum"); const db_1 = require("@debug-server-next/db"); const client_1 = require("@debug-server-next/client"); const middlewares_1 = require("@debug-server-next/middlewares"); const pub_sub_channel_1 = require("@debug-server-next/utils/pub-sub-channel"); const log_1 = require("@debug-server-next/utils/log"); const debug_target_1 = require("@debug-server-next/utils/debug-target"); const history_event_protocol_1 = require("@debug-server-next/utils/history-event-protocol"); const log = new log_1.Logger('pub-sub-manager', enum_1.WinstonColor.BrightGreen); // store the data of a DebugTarget, key: clientId const channelMap = new Map(); /** * subscribe to the upward command, trigger occasion: * 1. tunnel appConnect event * 2. app ws connection: maybe repeat subscribe because the iOS close event is later connect event * 3. get IWDP pages: should filter this situation, because frontend will request every 2s */ const subscribeCommand = async (debugTarget, ws) => { const { clientId, title, platform } = debugTarget; if (!channelMap.has(clientId)) addChannelItem(debugTarget); else if (isIWDPPage(clientId)) return; const { appClientList, upwardSubHandlerMap, upwardSubscriber, internalSubscriber } = channelMap.get(clientId); createAppClientList(debugTarget, ws); /** * subscribe upward message. * because there maybe multiple devtools client, such as multiple chrome extensions, * we need use batch subscribe (pSubscribe) */ if (!upwardSubHandlerMap.has(clientId)) { const upwardSubHandler = createUpwardSubHandler(clientId); const internalHandler = createInternalHandler(clientId); upwardSubHandlerMap.set(clientId, { upwardSubHandler, internalHandler }); } const { upwardSubHandler, internalHandler } = upwardSubHandlerMap.get(clientId); // must unsubscribe first to avoid subscribe multiple times upwardSubscriber.pUnsubscribe(upwardSubHandler); upwardSubscriber.pSubscribe(upwardSubHandler); // publish downward message to devtools frontend appClientList.forEach((appClient) => { appClient.removeAllListeners("message" /* AppClientEvent.Message */); appClient.on("message" /* AppClientEvent.Message */, (msg) => { const handler = getAppClientMessageHandler(debugTarget); if (handler) handler(msg); }); }); // publish history logs await internalSubscriber.unsubscribe(internalHandler); await internalSubscriber.subscribe(internalHandler); }; exports.subscribeCommand = subscribeCommand; /** * clean cache of one DebugTarget * should invoke when tunnel appDisconnect event, or WSAppClient ws close event. */ const cleanDebugTarget = async (clientId, closeDevtools, cleanCache = false) => { if (cleanCache) { await (0, debug_target_1.removeDebugTarget)(clientId); log.verbose('removeDebugTarget %s', clientId); return; } const debugTarget = await (0, debug_target_1.decreaseRefAndSave)(clientId); if (debugTarget) return; const channelInfo = channelMap.get(clientId); if (!channelInfo) return; const { publisherMap, upwardSubscriber, internalPublisher, internalSubscriber } = channelInfo; if (closeDevtools) { await internalPublisher.publish(enum_1.InternalChannelEvent.AppWSClose); } process.nextTick(() => { Array.from(publisherMap.values()).forEach((publisher) => publisher.disconnect()); upwardSubscriber.disconnect(); internalPublisher.disconnect(); internalSubscriber.disconnect(); channelMap.delete(clientId); }); }; exports.cleanDebugTarget = cleanDebugTarget; /** * clean all cache of DebugTarget */ const cleanAllDebugTargets = async () => Promise.all(Array.from(channelMap.values()).map(({ debugTarget }) => (0, exports.cleanDebugTarget)(debugTarget.clientId, true, true))); exports.cleanAllDebugTargets = cleanAllDebugTargets; let oldIWDPDebugTargets = []; /** * subscribe upward message from IWDP, and clean the outdated IWDP page */ const subscribeByIWDP = (debugTargets) => { const outdatedDebugTargets = (0, lodash_1.differenceBy)(oldIWDPDebugTargets, debugTargets, 'clientId'); if (outdatedDebugTargets.length) log.verbose('outdatedDebugTargets %j', outdatedDebugTargets); outdatedDebugTargets.forEach(({ clientId }) => { (0, exports.cleanDebugTarget)(clientId, true); }); debugTargets.forEach((debugTarget) => { const oldDebugTarget = oldIWDPDebugTargets.find((item) => item.clientId === debugTarget.clientId); if (oldDebugTarget) debugTarget.ts = oldDebugTarget.ts; (0, exports.subscribeCommand)(debugTarget); }); oldIWDPDebugTargets = debugTargets; }; exports.subscribeByIWDP = subscribeByIWDP; const addChannelItem = (debugTarget) => { const { clientId } = debugTarget; const { Publisher, Subscriber } = (0, db_1.getDBOperator)(); const internalChannelId = (0, pub_sub_channel_1.createInternalChannel)(clientId, ''); const upwardSubscriber = createUpwardSubscriber(clientId); const internalPublisher = new Publisher(internalChannelId); const internalSubscriber = new Subscriber(internalChannelId); channelMap.set(clientId, { downwardChannelSet: new Set(), cmdIdChannelIdMap: new Map(), publisherMap: new Map(), upwardSubscriber, internalPublisher, internalSubscriber, debugTarget, appClientList: [], upwardSubHandlerMap: new Map(), }); }; const createUpwardSubscriber = (clientId) => { const { Subscriber } = (0, db_1.getDBOperator)(); const upwardChannelId = (0, pub_sub_channel_1.createUpwardChannel)(clientId, '*'); log.verbose('subscribe to redis channel %s', upwardChannelId); return new Subscriber(upwardChannelId); }; /** * create matched debug tunnels by DebugTarget */ const createAppClientList = (debugTarget, ws) => { const { clientId } = debugTarget; const { appClientList } = channelMap.get(clientId); const options = client_1.appClientManager.getAppClientOptions(debugTarget.platform); return options .map(({ Ctor: AppClientCtor, ...option }) => { try { const outdatedAppClientIndex = appClientList.findIndex((appClient) => appClient.constructor.name === AppClientCtor.name); if (outdatedAppClientIndex !== -1) { const outdatedAppClient = appClientList.splice(outdatedAppClientIndex, 1)[0]; outdatedAppClient.destroy(); log.verbose('%s is outdated, re-constructor now', outdatedAppClient.constructor.name); } const urlParsedContext = (0, middlewares_1.debugTargetToUrlParsedContext)(debugTarget); const newOption = { ...option, urlParsedContext, iWDPWsUrl: debugTarget.iWDPWsUrl, ws, }; if (AppClientCtor.name === "WSAppClient" /* AppClientType.WS */ && !ws) { log.verbose('WSAppClient constructor option need ws'); return; } if (AppClientCtor.name === "IWDPAppClient" /* AppClientType.IWDP */ && !debugTarget.iWDPWsUrl) { log.verbose('IWDPAppClient constructor option need iWDPWsUrl, if you are debug iOS without USB, please ignore this warning.'); return; } log.verbose(`create app client ${AppClientCtor.name}`); const appClient = new AppClientCtor(clientId, newOption); appClientList.push(appClient); return appClient; } catch (e) { log.error('create app client error: %s', e === null || e === void 0 ? void 0 : e.stack); return null; } }) .filter((v) => v); }; /** * when re-plug iOS device, IWDPAppClient ws url will change, so need update new IWDPAppClient */ const updateIWDPAppClient = (debugTarget) => { const { clientId } = debugTarget; if (debugTarget.platform !== enum_1.DevicePlatform.IOS || !channelMap.has(clientId)) return; const { appClientList } = channelMap.get(clientId); const options = client_1.appClientManager.getAppClientOptions(debugTarget.platform); const iWDPOption = options.find(({ Ctor: AppClientCtor }) => AppClientCtor.name === "IWDPAppClient" /* AppClientType.IWDP */); if (!iWDPOption) return; const { Ctor: AppClientCtor, ...option } = iWDPOption; const hasIWDPAppClient = debugTarget.appClientTypeList.indexOf("IWDPAppClient" /* AppClientType.IWDP */) !== -1; const outdatedIWDPAppClientIndex = appClientList.findIndex((item) => item.constructor.name === "IWDPAppClient" /* AppClientType.IWDP */); if (hasIWDPAppClient) { if (outdatedIWDPAppClientIndex === -1) { appendIWDPAppClient(); } else { updateIWDPAppClient(); } } else if (outdatedIWDPAppClientIndex !== -1) { removeIWDPAppClient(); } else { appendIWDPAppClient(); } function appendIWDPAppClient() { log.verbose('append IWDPAppClient'); if (!debugTarget.iWDPWsUrl) { return log.verbose('debugTarget.iWDPWsUrl is null, could not debug jscore'); } const {} = iWDPOption; const iWDPAppClient = new AppClientCtor(clientId, { ...option, urlParsedContext: (0, middlewares_1.debugTargetToUrlParsedContext)(debugTarget), iWDPWsUrl: debugTarget.iWDPWsUrl, }); appClientList.push(iWDPAppClient); iWDPAppClient.on("message" /* AppClientEvent.Message */, (msg) => getAppClientMessageHandler(debugTarget)(msg)); } function updateIWDPAppClient() { const iWDPAppClient = appClientList[outdatedIWDPAppClientIndex]; if (iWDPAppClient.url === debugTarget.iWDPWsUrl) return; const outdatedAppClient = appClientList.splice(outdatedIWDPAppClientIndex, 1)[0]; outdatedAppClient.destroy(); if (!debugTarget.iWDPWsUrl) { return log.verbose('debugTarget.iWDPWsUrl is null, could not debug jscore'); } const urlParsedContext = (0, middlewares_1.debugTargetToUrlParsedContext)(debugTarget); const newOption = { ...option, urlParsedContext, iWDPWsUrl: debugTarget.iWDPWsUrl, }; const appClient = new AppClientCtor(clientId, newOption); appClientList.push(appClient); appClient.removeAllListeners("message" /* AppClientEvent.Message */); appClient.on("message" /* AppClientEvent.Message */, (msg) => getAppClientMessageHandler(debugTarget)(msg)); log.verbose(`create app client ${AppClientCtor.name}, update iWDPWsUrl to %s`, debugTarget.iWDPWsUrl); } function removeIWDPAppClient() { const outdated = appClientList.splice(outdatedIWDPAppClientIndex, 1)[0]; outdated.destroy(); log.verbose('IWDPAppClient is outdated and destroyed!'); } }; exports.updateIWDPAppClient = updateIWDPAppClient; /** * downward message handler */ const getAppClientMessageHandler = (debugTarget) => async (msg) => { const { clientId, platform } = debugTarget; const channelInfo = channelMap.get(clientId); if (!channelInfo) { return log.verbose('channelInfo does not exist, maybe app page is closed!'); } const { downwardChannelSet, cmdIdChannelIdMap, publisherMap } = channelInfo; msg.ts = Date.now(); const msgStr = JSON.stringify(msg); const { Publisher } = (0, db_1.getDBOperator)(); if ('id' in msg) { // publish CommandRes to `downwardChannelId` const commandRes = msg; const downwardChannelId = cmdIdChannelIdMap.get(commandRes.id); if (!downwardChannelId) return; if (!publisherMap.has(downwardChannelId)) { const publisher = new Publisher(downwardChannelId); publisherMap.set(downwardChannelId, publisher); } const publisher = publisherMap.get(downwardChannelId); publisher.publish(msgStr); } else { if (downwardChannelSet.size === 0) { downwardChannelSet.add((0, pub_sub_channel_1.createDownwardChannel)(clientId)); } if (platform === enum_1.DevicePlatform.IOS && (0, history_event_protocol_1.isHistoryProtocol)(msg.method, platform)) { (0, history_event_protocol_1.saveHistoryProtocol)(clientId, msgStr); } // broadcast to all channel, because could'n determine the receiver of EventRes downwardChannelSet.forEach((channelId) => { if (!publisherMap.has(channelId)) { const publisher = new Publisher(channelId); publisherMap.set(channelId, publisher); } const publisher = publisherMap.get(channelId); publisher.publish(msgStr); }); } }; const isIWDPPage = (clientId) => { const { appClientList } = channelMap.get(clientId); return (appClientList === null || appClientList === void 0 ? void 0 : appClientList.length) === 1 && appClientList[0].constructor.name === "IWDPAppClient" /* AppClientType.IWDP */; }; const createUpwardSubHandler = (clientId) => (message, upwardChannelId) => { const { downwardChannelSet, cmdIdChannelIdMap } = channelMap.get(clientId); if (!upwardChannelId) return log.verbose('pSubscribe without channelId'); if (upwardChannelId.includes(pub_sub_channel_1.vueDevtoolsExtensionName) || upwardChannelId.includes(pub_sub_channel_1.reactDevtoolsExtensionName)) return log.verbose('ignore vue/react channel'); let msgObj; try { msgObj = JSON.parse(message); } catch (e) { log.error('%s channel message are invalid JSON, %s', upwardChannelId, message); } const downwardChannelId = (0, pub_sub_channel_1.upwardChannelToDownwardChannel)(upwardChannelId); cmdIdChannelIdMap.set(msgObj.id, downwardChannelId); downwardChannelSet.add(downwardChannelId); /** * when devtools publish resumeCommands, maybe app had closed */ const channelInfo = channelMap.get(clientId); if (!channelInfo) return; const { appClientList: latestAppClientList } = channelInfo; latestAppClientList.forEach((appClient) => { appClient.sendToApp(msgObj).catch((e) => { if (e !== 1 /* ErrorCode.DomainFiltered */) { return log.error('%s app client send error: %j', appClient.constructor.name, e); } }); }); }; const createInternalHandler = (clientId) => async (msg) => { const { downwardChannelSet, publisherMap, debugTarget } = channelMap.get(clientId); const { platform } = debugTarget; if (msg !== enum_1.InternalChannelEvent.DevtoolsConnected) return; const list = await (0, history_event_protocol_1.getHistoryProtocol)(clientId); if (!list.length) return; /** * delay to send history log after ConsoleModel of devtools is ready */ setTimeout(() => { downwardChannelSet.forEach(async (channelId) => { if (!publisherMap.has(channelId)) { const { Publisher } = (0, db_1.getDBOperator)(); const publisher = new Publisher(channelId); publisherMap.set(channelId, publisher); } const publisher = publisherMap.get(channelId); /** * mock a clear protocol to clear existed history logs in console panel */ if (platform !== enum_1.DevicePlatform.Android) { await publisher.publish({ method: 'Log.cleared', params: {}, }); } list.forEach((item) => { var _a; try { const cmd = JSON.parse(item); if (!((_a = cmd.method) === null || _a === void 0 ? void 0 : _a.startsWith('Network.'))) { publisher.publish(item); } } catch (e) { } }); }); }, 1500); };