@hippy/debug-server-next
Version:
Debug server for hippy.
390 lines (389 loc) • 17.3 kB
JavaScript
;
/*
* 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);
};