refactor: update node ws channel code (#3241)

This commit is contained in:
野声 2024-01-16 10:59:06 +08:00 committed by GitHub
parent 32419c9fca
commit c89984983a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 2481 additions and 1599 deletions

View File

@ -3,11 +3,14 @@ const { pathsToModuleNameMapper } = require('ts-jest');
const tsconfig = require('./configs/ts/tsconfig.resolve.json');
const tsModuleNameMapper = pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: '<rootDir>/configs/' });
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
const baseConfig = {
preset: 'ts-jest',
testRunner: 'jest-jasmine2',
resolver: '<rootDir>/tools/dev-tool/src/jest-resolver.js',
coverageProvider: process.env.JEST_COVERAGE_PROVIDER || 'babel',
// https://dev.to/vantanev/make-your-jest-tests-up-to-20-faster-by-changing-a-single-setting-i36
maxWorkers: 2,
collectCoverageFrom: [
@ -58,6 +61,11 @@ const baseConfig = {
},
},
};
if (process.env.JEST_COVERAGE_PROVIDER) {
baseConfig.coverageProvider = process.env.JEST_COVERAGE_PROVIDER;
}
/**
* @type {import('@jest/types').Config.InitialOptions}
*/

View File

@ -1,3 +1,5 @@
const { TextEncoder, TextDecoder } = require('util');
// Do not log message on GitHub Actions.
// Because these logs will affect the detection of real problems.
const _console = global.console;
@ -13,6 +15,9 @@ global.console = process.env.CI
}
: _console;
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
process.on('unhandledRejection', (error) => {
_console.error('unhandledRejection', error);
if (process.env.EXIT_ON_UNHANDLED_REJECTION) {

View File

@ -54,8 +54,6 @@ global.document.queryCommandSupported = () => {};
global.document.execCommand = () => {};
global.HTMLElement = jsdom.window.HTMLElement;
global.self = global;
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
global.ElectronIpcRenderer = {
send: () => {},

View File

@ -95,7 +95,7 @@
"@types/react-is": "^16.7.1",
"@types/socket.io-client": "^1.4.32",
"@types/temp": "^0.9.1",
"@types/ws": "^6.0.1",
"@types/ws": "^8.5.10",
"@typescript-eslint/eslint-plugin": "^5.12.1",
"@typescript-eslint/parser": "^5.12.1",
"async-retry": "^1.3.1",

View File

@ -1,34 +1,39 @@
import { WebSocket, Server } from 'mock-socket';
import { ReconnectingWebSocketConnection } from '@opensumi/ide-connection/lib/common/connection/drivers/reconnecting-websocket';
import { WebSocket, Server } from '@opensumi/mock-socket';
import { WSChannelHandler } from '../../src/browser/ws-channel-handler';
import { stringify, parse } from '../../src/common/utils';
import { stringify, parse } from '../../src/common/ws-channel';
(global as any).WebSocket = WebSocket;
const randomPortFn = () => Math.floor(Math.random() * 10000) + 10000;
const randomPort = randomPortFn();
describe('connection browser', () => {
it('init connection', async () => {
jest.setTimeout(20000);
const fakeWSURL = 'ws://localhost:8089';
const fakeWSURL = `ws://localhost:${randomPort}`;
const mockServer = new Server(fakeWSURL);
let receivedHeartbeat = false;
mockServer.on('connection', (socket) => {
socket.on('message', (msg) => {
const msgObj = parse(msg as string);
const msgObj = parse(msg as Uint8Array);
if (msgObj.kind === 'open') {
socket.send(
stringify({
id: msgObj.id,
kind: 'ready',
kind: 'server-ready',
}),
);
} else if (msgObj.kind === 'heartbeat') {
} else if (msgObj.kind === 'ping') {
receivedHeartbeat = true;
}
});
});
const wsChannelHandler = new WSChannelHandler(fakeWSURL, console);
const wsChannelHandler = new WSChannelHandler(ReconnectingWebSocketConnection.forURL(fakeWSURL), console);
await wsChannelHandler.initHandler();
await new Promise<void>((resolve) => {

View File

@ -0,0 +1,76 @@
import { Buffers } from '../../src/common/connection/buffers';
describe('Buffers', () => {
it('can append and slice', () => {
const list = new Buffers();
list.push(new Uint8Array([1, 2, 3]));
list.push(new Uint8Array([4, 5, 6]));
expect(list.slice(0, 0)).toEqual(new Uint8Array(0));
expect(list.slice(0, 2)).toEqual(new Uint8Array([1, 2]));
expect(list.slice(0, 7)).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6]));
expect(list.slice(1, 1)).toEqual(new Uint8Array(0));
expect(list.slice(1, 5)).toEqual(new Uint8Array([2, 3, 4, 5]));
expect(list.slice(1, 7)).toEqual(new Uint8Array([2, 3, 4, 5, 6]));
expect(list.slice(2, 2)).toEqual(new Uint8Array(0));
expect(list.slice(2, 6)).toEqual(new Uint8Array([3, 4, 5, 6]));
});
it('can splice', () => {
const list = new Buffers();
list.push(new Uint8Array([1, 2, 3]));
list.push(new Uint8Array([4, 5, 6]));
expect(list.splice(0, 0)).toEqual({ buffers: [], size: 0 });
expect(list.byteLength).toEqual(6);
expect(list.splice(0, 1)).toEqual({ buffers: [new Uint8Array([1])], size: 1 });
expect(list.byteLength).toEqual(5);
expect(list.splice(0, 2)).toEqual({ buffers: [new Uint8Array([2, 3])], size: 2 });
expect(list.byteLength).toEqual(3);
expect(list.splice(0, 0, new Uint8Array([1]))).toEqual({ buffers: [], size: 0 });
expect(list.byteLength).toEqual(4);
expect(list.splice(0, 0, new Uint8Array([2, 3]))).toEqual({ buffers: [], size: 0 });
expect(list.byteLength).toEqual(6);
expect(list.splice(0, 0, new Uint8Array([4, 5, 6]))).toEqual({ buffers: [], size: 0 });
expect(list.byteLength).toEqual(9);
expect(list.buffers).toEqual([
new Uint8Array([4, 5, 6]),
new Uint8Array([2, 3]),
new Uint8Array([1]),
new Uint8Array([4, 5, 6]),
]);
});
it('can copy', () => {
const list = new Buffers();
list.push(new Uint8Array([1, 2, 3]));
list.push(new Uint8Array([4, 5, 6]));
const target = new Uint8Array(7);
list.copy(target, 0);
expect(target).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 0]));
});
it('other', () => {
const list = new Buffers();
list.push(new Uint8Array([1, 2, 3]));
list.push(new Uint8Array([4, 5, 6]));
expect(list.byteLength).toEqual(6);
expect(list.pos(0)).toEqual({ buf: 0, offset: 0 });
expect(list.pos(1)).toEqual({ buf: 0, offset: 1 });
expect(list.pos(2)).toEqual({ buf: 0, offset: 2 });
expect(list.get(0)).toEqual(1);
expect(list.get(1)).toEqual(2);
list.set(0, 9);
list.set(1, 10);
expect(list.get(0)).toEqual(9);
expect(list.get(1)).toEqual(10);
});
});

View File

@ -0,0 +1,105 @@
/* eslint-disable no-console */
import { BinaryReader } from '@furyjs/fury/dist/lib/reader';
import {
StreamPacketDecoder,
createStreamPacket,
kMagicNumber,
} from '../../src/common/connection/drivers/stream-decoder';
const reader = BinaryReader({});
function round(x: number, count: number) {
return Math.round(x * 10 ** count) / 10 ** count;
}
function createPayload(size: number) {
const payload = new Uint8Array(size);
for (let i = 0; i < size; i++) {
payload[i] = i % 256;
}
return payload;
}
console.time('createPayload');
const p1k = createPayload(1024);
const p64k = createPayload(64 * 1024);
const p128k = createPayload(128 * 1024);
const p1m = createPayload(1024 * 1024);
const p5m = createPayload(5 * 1024 * 1024);
const p10m = createPayload(10 * 1024 * 1024);
const h1m = createPayload(1024 + p1m.byteLength);
h1m.set(p1m, 1024);
const h5m = createPayload(1024 + p5m.byteLength + 233);
h5m.set(p5m, 1024);
console.timeEnd('createPayload');
// 1m
const pressure = 1024 * 1024;
const purePackets = [p1k, p64k, p128k, p5m, p10m].map((v) => [createStreamPacket(v), v] as const);
const mixedPackets = [p1m, p5m].map((v) => {
const sumiPacket = createStreamPacket(v);
const newPacket = createPayload(1024 + sumiPacket.byteLength);
newPacket.set(sumiPacket, 1024);
return [newPacket, v] as const;
});
const packets = [...purePackets, ...mixedPackets];
describe('stream-packet', () => {
it('can create sumi stream packet', () => {
const content = new Uint8Array([1, 2, 3]);
const packet = createStreamPacket(content);
reader.reset(packet);
expect(reader.uint32()).toBe(kMagicNumber);
expect(reader.varUInt32()).toBe(content.byteLength);
expect(Uint8Array.from(reader.buffer(content.byteLength))).toEqual(content);
});
packets.forEach(([packet, expected]) => {
it(`can decode stream packet: ${round(packet.byteLength / 1024 / 1024, 2)}m`, (done) => {
const decoder = new StreamPacketDecoder();
decoder.onData((data) => {
expect(data.byteLength).toEqual(expected.byteLength);
for (let i = 0; i < 10; i++) {
// 随机选一些数据(<= 100字节),对比是否正确,对比整个数组的话,超大 buffer 会很耗时
const start = Math.floor(Math.random() * data.byteLength);
const end = Math.floor(Math.random() * 1024);
expect(data.subarray(start, end)).toEqual(expected.subarray(start, end));
}
decoder.dispose();
done();
});
console.log('write chunk', packet.byteLength);
// write chunk by ${pressure} bytes
for (let i = 0; i < packet.byteLength; i += pressure) {
decoder.push(packet.subarray(i, i + pressure));
logMemoryUsage();
}
logMemoryUsage();
});
});
});
function logMemoryUsage() {
const used = process.memoryUsage();
let text = new Date().toLocaleString('zh') + ' Memory usage:\n';
// eslint-disable-next-line guard-for-in
for (const key in used) {
text += `${key} ${Math.round((used[key] / 1024 / 1024) * 100) / 100} MB\n`;
}
console.log(text);
}

View File

@ -1,18 +1,17 @@
import http from 'http';
import ws from 'ws';
import WebSocket from 'ws';
import { WSWebSocketConnection } from '@opensumi/ide-connection/lib/common/connection';
import { Deferred, Emitter, Uri } from '@opensumi/ide-core-common';
import { RPCService } from '../../src';
import { RPCServiceCenter, initRPCService, RPCMessageConnection } from '../../src/common';
import { createWebSocketConnection } from '../../src/common/message';
import { RPCProtocol, createMainContextProxyIdentifier } from '../../src/common/rpcProtocol';
import { parse } from '../../src/common/utils';
import { WSChannel } from '../../src/common/ws-channel';
import { RPCServiceCenter, initRPCService } from '../../src/common';
import { RPCProtocol, createMainContextProxyIdentifier } from '../../src/common/ext-rpc-protocol';
import { WSChannel, parse } from '../../src/common/ws-channel';
import { WebSocketServerRoute, CommonChannelHandler, commonChannelPathHandler } from '../../src/node';
const WebSocket = ws;
const wssPort = 7788;
class MockFileService extends RPCService {
getContent(filePath) {
@ -36,8 +35,8 @@ describe('connection', () => {
socketRoute.init();
await new Promise<void>((resolve) => {
server.listen(7788, () => {
resolve(undefined);
server.listen(wssPort, () => {
resolve();
});
});
@ -47,24 +46,24 @@ describe('connection', () => {
dispose: () => {},
});
const connection = new WebSocket('ws://0.0.0.0:7788/service');
const connection = new WebSocket(`ws://0.0.0.0:${wssPort}/service`);
connection.on('error', () => {
connection.close();
});
await new Promise<void>((resolve) => {
connection.on('open', () => {
resolve(undefined);
resolve();
});
});
const channelSend = (content) => {
connection.send(content, (err) => {});
};
const channel = new WSChannel(channelSend, 'TEST_CHANNEL_ID');
connection.on('message', (msg) => {
const msgObj = parse(msg as string);
if (msgObj.kind === 'ready') {
const clientId = 'TEST_CLIENT';
const wsConnection = new WSWebSocketConnection(connection);
const channel = new WSChannel(wsConnection, {
id: 'TEST_CHANNEL_ID',
});
connection.on('message', (msg: Uint8Array) => {
const msgObj = parse(msg);
if (msgObj.kind === 'server-ready') {
if (msgObj.id === 'TEST_CHANNEL_ID') {
channel.handleMessage(msgObj);
}
@ -73,9 +72,9 @@ describe('connection', () => {
await new Promise<void>((resolve) => {
channel.onOpen(() => {
resolve(undefined);
resolve();
});
channel.open('TEST_CHANNEL');
channel.open('TEST_CHANNEL', clientId);
});
expect(mockHandler.mock.calls.length).toBe(1);
@ -102,8 +101,8 @@ describe('connection', () => {
socketRoute.init();
await new Promise<void>((resolve) => {
server.listen(7788, () => {
resolve(undefined);
server.listen(wssPort, () => {
resolve();
});
});
@ -113,7 +112,7 @@ describe('connection', () => {
dispose: () => {},
});
const connection = new WebSocket('ws://0.0.0.0:7788/service');
const connection = new WebSocket(`ws://0.0.0.0:${wssPort}/service`);
connection.on('error', (e) => {
deferred.reject(e);
@ -124,32 +123,35 @@ describe('connection', () => {
});
it('RPCService', async () => {
const wss = new WebSocket.Server({ port: 7788 });
const wss = new WebSocket.Server({ port: wssPort });
const notificationMock = jest.fn();
let serviceCenter;
let clientConnection;
let serviceCenter: RPCServiceCenter;
let clientConnection: WebSocket;
await Promise.all([
new Promise<void>((resolve) => {
wss.on('connection', (connection) => {
serviceCenter = new RPCServiceCenter();
const serverConnection = createWebSocketConnection(connection);
serviceCenter.setConnection(serverConnection);
const channel = WSChannel.forWebSocket(connection, {
id: 'test-wss',
});
resolve(undefined);
serviceCenter.setChannel(channel);
resolve();
});
}),
new Promise<void>((resolve) => {
clientConnection = new WebSocket('ws://0.0.0.0:7788/service');
clientConnection = new WebSocket(`ws://0.0.0.0:${wssPort}/service`);
clientConnection.on('open', () => {
resolve(undefined);
resolve();
});
}),
]);
const { createRPCService } = initRPCService(serviceCenter);
const { createRPCService } = initRPCService(serviceCenter!);
createRPCService('MockFileServicePath', mockFileService);
createRPCService('MockNotificationService', {
@ -159,8 +161,14 @@ describe('connection', () => {
});
const clientCenter = new RPCServiceCenter();
clientCenter.setConnection(createWebSocketConnection(clientConnection) as RPCMessageConnection);
const channel = WSChannel.forWebSocket(clientConnection!, {
id: 'test',
});
const toDispose = clientCenter.setChannel(channel);
clientConnection!.once('close', () => {
toDispose.dispose();
});
const { getRPCService } = initRPCService<
MockFileService & {
onFileChange: (k: any) => void;
@ -193,6 +201,7 @@ describe('connection', () => {
expect(notificationMock.mock.calls.length).toBe(2);
wss.close();
clientConnection!.close();
});
it('RPCProtocol', async () => {

View File

@ -0,0 +1,88 @@
import net from 'net';
import { WSChannel } from '@opensumi/ide-connection';
import { normalizedIpcHandlerPathAsync } from '@opensumi/ide-core-common/lib/utils/ipc';
const total = 1000;
describe('ws channel node', () => {
it('works on net.Socket', async () => {
const ipcPath = await normalizedIpcHandlerPathAsync('test', true);
const server = new net.Server();
server.on('connection', (socket) => {
const channel1 = WSChannel.forNetSocket(socket, {
id: 'channel1',
});
channel1.send('hello');
});
server.listen(ipcPath);
const socket2 = net.createConnection(ipcPath);
const channel2 = WSChannel.forNetSocket(socket2, {
id: 'channel2',
});
const msg = await new Promise<string>((resolve) => {
channel2.onMessage((data) => {
resolve(data);
});
});
expect(msg).toEqual('hello');
server.close();
socket2.destroy();
socket2.end();
});
it(`互相通信 N 次(N = ${total})`, async () => {
jest.setTimeout(20 * 1000);
let count = 0;
const ipcPath = await normalizedIpcHandlerPathAsync('test', true);
const server = new net.Server();
server.on('connection', (socket) => {
const channel1 = WSChannel.forNetSocket(socket, {
id: 'channel1',
});
channel1.onMessage((d) => {
channel1.send(d + 'resp');
});
});
server.listen(ipcPath);
const socket2 = net.createConnection(ipcPath);
const channel2 = WSChannel.forNetSocket(socket2, {
id: 'channel2',
});
await Promise.all([
new Promise<void>((resolve) => {
channel2.onMessage(() => {
count++;
if (count === total) {
resolve();
}
});
}),
new Promise<void>((resolve) => {
for (let i = 0; i < total; i++) {
channel2.send('hello');
}
resolve();
}),
]);
server.close();
socket2.destroy();
socket2.end();
});
});

View File

@ -17,15 +17,17 @@
"url": "git@github.com:opensumi/core.git"
},
"dependencies": {
"@furyjs/fury": "0.5.6-beta",
"@opensumi/events": "^0.1.0",
"@opensumi/ide-core-common": "workspace:*",
"@opensumi/vscode-jsonrpc": "^8.0.0-next.2",
"path-match": "^1.2.4",
"reconnecting-websocket": "^4.2.0",
"ws": "^8.9.0"
"reconnecting-websocket": "^4.4.0",
"ws": "^8.15.1"
},
"devDependencies": {
"@opensumi/ide-components": "workspace:*",
"@opensumi/ide-dev-tool": "workspace:*",
"mock-socket": "^9.0.2"
"@opensumi/mock-socket": "^9.3.1"
}
}

View File

@ -1,25 +1,32 @@
import ReconnectingWebSocket from 'reconnecting-websocket';
import { uuid } from '@opensumi/ide-core-common';
import { IReporterService, REPORT_NAME, UrlProvider } from '@opensumi/ide-core-common';
import { IReporterService, REPORT_NAME } from '@opensumi/ide-core-common';
import { stringify, parse, WSCloseInfo, ConnectionInfo } from '../common/utils';
import { WSChannel, MessageString } from '../common/ws-channel';
import { NetSocketConnection } from '../common/connection';
import { ReconnectingWebSocketConnection } from '../common/connection/drivers/reconnecting-websocket';
import { WSCloseInfo, ConnectionInfo } from '../common/utils';
import { WSChannel, stringify, parse } from '../common/ws-channel';
// 前台链接管理类
/**
* Channel Handler in browser
*/
export class WSChannelHandler {
public connection: WebSocket;
private channelMap: Map<number | string, WSChannel> = new Map();
private channelCloseEventMap: Map<number | string, WSCloseInfo> = new Map();
private channelMap: Map<string, WSChannel> = new Map();
private channelCloseEventMap: Map<string, WSCloseInfo> = new Map();
private logger = console;
public clientId: string;
private heartbeatMessageTimer: NodeJS.Timer | null;
private reporterService: IReporterService;
constructor(public wsPath: UrlProvider, logger: any, public protocols?: string[], clientId?: string) {
LOG_TAG = '[WSChannelHandler]';
constructor(
public connection: ReconnectingWebSocketConnection | NetSocketConnection,
logger: any,
clientId?: string,
) {
this.logger = logger || this.logger;
this.clientId = clientId || `CLIENT_ID_${uuid()}`;
this.connection = new ReconnectingWebSocket(wsPath, protocols, {}) as WebSocket; // new WebSocket(wsPath, protocols);
this.LOG_TAG = `[WSChannelHandler] [client-id:${this.clientId}]`;
}
// 为解决建立连接之后,替换成可落盘的 logger
replaceLogger(logger: any) {
@ -30,21 +37,15 @@ export class WSChannelHandler {
setReporter(reporterService: IReporterService) {
this.reporterService = reporterService;
}
private clientMessage() {
const clientMsg: MessageString = stringify({
kind: 'client',
clientId: this.clientId,
});
this.connection.send(clientMsg);
}
private heartbeatMessage() {
if (this.heartbeatMessageTimer) {
clearTimeout(this.heartbeatMessageTimer);
}
this.heartbeatMessageTimer = global.setTimeout(() => {
const msg = stringify({
kind: 'heartbeat',
kind: 'ping',
clientId: this.clientId,
id: this.clientId,
});
this.connection.send(msg);
this.heartbeatMessage();
@ -52,74 +53,85 @@ export class WSChannelHandler {
}
public async initHandler() {
this.connection.onmessage = (e) => {
this.connection.onMessage((message) => {
// 一个心跳周期内如果有收到消息,则不需要再发送心跳
this.heartbeatMessage();
const msg = parse(e.data);
const msg = parse(message);
if (msg.id) {
const channel = this.channelMap.get(msg.id);
if (channel) {
if (msg.kind === 'data' && !(channel as any).fireMessage) {
// 要求前端发送初始化消息,但后端最先发送消息时,前端并未准备好
this.logger.error('channel not ready!', msg);
}
channel.handleMessage(msg);
} else {
this.logger.warn(`channel ${msg.id} not found`);
if (msg.kind === 'pong') {
// ignore server2client pong message
return;
}
if (!msg.id) {
// unknown message
this.logger.warn(this.LOG_TAG, 'unknown message', msg);
return;
}
const channel = this.channelMap.get(msg.id);
if (channel) {
if (!channel.hasMessageListener()) {
// 要求前端发送初始化消息,但后端最先发送消息时,前端并未准备好
this.logger.error(this.LOG_TAG, 'channel not ready!', msg);
}
channel.handleMessage(msg);
} else {
this.logger.warn(this.LOG_TAG, `channel ${msg.id} not found`);
}
});
const reopenExistsChannel = () => {
if (this.channelMap.size) {
this.channelMap.forEach((channel) => {
channel.onOpen(() => {
const closeInfo = this.channelCloseEventMap.get(channel.id);
this.reporterService &&
this.reporterService.point(REPORT_NAME.CHANNEL_RECONNECT, REPORT_NAME.CHANNEL_RECONNECT, closeInfo);
this.logger.log(this.LOG_TAG, `channel reconnect ${this.clientId}:${channel.channelPath}`);
});
channel.open(channel.channelPath, this.clientId);
// 针对前端需要重新设置下后台状态的情况
channel.fireReopen();
});
}
};
await new Promise((resolve) => {
this.connection.addEventListener('open', () => {
this.clientMessage();
await new Promise<void>((resolve) => {
if (this.connection.isOpen()) {
this.heartbeatMessage();
resolve(undefined);
// 重连 channel
resolve();
reopenExistsChannel();
} else {
this.connection.onOpen(() => {
this.heartbeatMessage();
resolve();
reopenExistsChannel();
});
}
this.connection.onceClose((code, reason) => {
if (this.channelMap.size) {
this.channelMap.forEach((channel) => {
channel.onOpen(() => {
const closeInfo = this.channelCloseEventMap.get(channel.id);
this.reporterService &&
this.reporterService.point(REPORT_NAME.CHANNEL_RECONNECT, REPORT_NAME.CHANNEL_RECONNECT, closeInfo);
this.logger && this.logger.log(`channel reconnect ${this.clientId}:${channel.channelPath}`);
});
channel.open(channel.channelPath);
// 针对前端需要重新设置下后台状态的情况
if (channel.fireReOpen) {
channel.fireReOpen();
}
});
}
});
this.connection.addEventListener('close', (event) => {
if (this.channelMap.size) {
this.channelMap.forEach((channel) => {
channel.close(event.code, event.reason);
channel.close(code ?? 1000, reason ?? '');
});
}
});
});
}
private getChannelSend = (connection) => (content: string) => {
connection.send(content, (err: Error) => {
if (err) {
this.logger.warn(err);
}
});
};
public async openChannel(channelPath: string) {
const channelSend = this.getChannelSend(this.connection);
const channelId = `${this.clientId}:${channelPath}`;
const channel = new WSChannel(channelSend, channelId);
const channel = new WSChannel(this.connection, {
id: channelId,
logger: this.logger,
});
this.channelMap.set(channel.id, channel);
await new Promise((resolve) => {
await new Promise<void>((resolve) => {
channel.onOpen(() => {
resolve(undefined);
resolve();
});
channel.onClose((code: number, reason: string) => {
this.channelCloseEventMap.set(channelId, {
@ -127,9 +139,9 @@ export class WSChannelHandler {
closeEvent: { code, reason },
connectInfo: (navigator as any).connection as ConnectionInfo,
});
this.logger.log('channel close: ', code, reason);
this.logger.log(this.LOG_TAG, 'channel close: ', code, reason);
});
channel.open(channelPath);
channel.open(channelPath, this.clientId);
});
return channel;

View File

@ -1,212 +1,26 @@
import { MessageConnection } from '@opensumi/vscode-jsonrpc/lib/common/connection';
import { RPCProxy, NOTREGISTERMETHOD, ILogger } from './proxy';
export type RPCServiceMethod = (...args: any[]) => any;
export type ServiceProxy = any;
export enum ServiceType {
Service,
Stub,
}
export class RPCServiceStub {
constructor(private serviceName: string, private center: RPCServiceCenter, private type: ServiceType) {
if (this.type === ServiceType.Service) {
this.center.registerService(serviceName, this.type);
}
}
async ready() {
return this.center.when();
}
getNotificationName(name: string) {
return `on:${this.serviceName}:${name}`;
}
getRequestName(name: string) {
return `${this.serviceName}:${name}`;
}
// 服务方
on(name: string, method: RPCServiceMethod) {
this.onRequest(name, method);
}
getServiceMethod(service): string[] {
let props: any[] = [];
if (/^\s*class/.test(service.constructor.toString())) {
let obj = service;
do {
props = props.concat(Object.getOwnPropertyNames(obj));
} while ((obj = Object.getPrototypeOf(obj)));
props = props.sort().filter((e, i, arr) => e !== arr[i + 1] && typeof service[e] === 'function');
} else {
for (const prop in service) {
if (service[prop] && typeof service[prop] === 'function') {
props.push(prop);
}
}
}
return props;
}
onRequestService(service: any) {
const methods = this.getServiceMethod(service);
for (const method of methods) {
this.onRequest(method, service[method].bind(service));
}
}
onRequest(name: string, method: RPCServiceMethod) {
this.center.onRequest(this.getMethodName(name), method);
}
broadcast(name: string, ...args): Promise<any> {
return this.center.broadcast(this.getMethodName(name), ...args);
}
getMethodName(name: string) {
return name.startsWith('on') ? this.getNotificationName(name) : this.getRequestName(name);
}
getProxy = <T>() =>
new Proxy<RPCServiceStub & T>(this as any, {
// 调用方
get: (target, prop: string) => {
if (!target[prop]) {
if (typeof prop === 'symbol') {
return Promise.resolve();
} else {
return (...args) => this.ready().then(() => this.broadcast(prop, ...args));
}
} else {
return target[prop];
}
},
});
}
import { RPCServiceCenter, RPCServiceStub } from './rpc-service';
import { ServiceType } from './types';
export function initRPCService<T = void>(center: RPCServiceCenter) {
return {
createRPCService: (name: string, service?: any) => {
const proxy = new RPCServiceStub(name, center, ServiceType.Service).getProxy<T>();
const proxy = createRPCService<T>(name, center);
if (service) {
proxy.onRequestService(service);
}
return proxy;
},
getRPCService: (name: string) => new RPCServiceStub(name, center, ServiceType.Stub).getProxy<T>(),
getRPCService: (name: string) => getRPCService<T>(name, center),
};
}
interface IBench {
registerService: (service: string) => void;
}
export interface RPCMessageConnection extends MessageConnection {
uid?: string;
writer?: any;
reader?: any;
}
export function createRPCService<T = void>(name: string, center: RPCServiceCenter): any {
export function createRPCService<T = void>(name: string, center: RPCServiceCenter) {
return new RPCServiceStub(name, center, ServiceType.Service).getProxy<T>();
}
export function getRPCService<T = void>(name: string, center: RPCServiceCenter): any {
export function getRPCService<T = void>(name: string, center: RPCServiceCenter) {
return new RPCServiceStub(name, center, ServiceType.Stub).getProxy<T>();
}
export class RPCServiceCenter {
public uid: string;
public rpcProxy: RPCProxy[] = [];
public serviceProxy: ServiceProxy[] = [];
private connection: Array<MessageConnection> = [];
private serviceMethodMap = { client: undefined };
private createService: string[] = [];
private getService: string[] = [];
private connectionPromise: Promise<void>;
private connectionPromiseResolve: () => void;
private logger: ILogger;
constructor(private bench?: IBench, logger?: ILogger) {
this.uid = 'RPCServiceCenter:' + process.pid;
this.connectionPromise = new Promise((resolve) => {
this.connectionPromiseResolve = resolve;
});
this.logger = logger || console;
}
registerService(serviceName: string, type: ServiceType): void {
if (type === ServiceType.Service) {
this.createService.push(serviceName);
if (this.bench) {
this.bench.registerService(serviceName);
}
} else if (type === ServiceType.Stub) {
this.getService.push(serviceName);
}
}
when() {
return this.connectionPromise;
}
setConnection(connection: MessageConnection) {
if (!this.connection.length) {
this.connectionPromiseResolve();
}
this.connection.push(connection);
const rpcProxy = new RPCProxy(this.serviceMethodMap, this.logger);
rpcProxy.listen(connection);
this.rpcProxy.push(rpcProxy);
const serviceProxy = rpcProxy.createProxy();
this.serviceProxy.push(serviceProxy);
}
removeConnection(connection: MessageConnection) {
const removeIndex = this.connection.indexOf(connection);
if (removeIndex !== -1) {
this.connection.splice(removeIndex, 1);
this.rpcProxy.splice(removeIndex, 1);
this.serviceProxy.splice(removeIndex, 1);
}
return removeIndex !== -1;
}
onRequest(name: string, method: RPCServiceMethod) {
if (!this.connection.length) {
this.serviceMethodMap[name] = method;
} else {
this.rpcProxy.forEach((proxy) => {
proxy.listenService({ [name]: method });
});
}
}
async broadcast(name: string, ...args): Promise<any> {
const broadcastResult = this.serviceProxy.map((proxy) => proxy[name](...args));
if (!broadcastResult || broadcastResult.length === 0) {
throw new Error(`broadcast rpc \`${name}\` error: no remote service can handle this call`);
}
const doubtfulResult = [] as any[];
const result = [] as any[];
for (const i of broadcastResult) {
if (i === NOTREGISTERMETHOD) {
doubtfulResult.push(i);
} else {
result.push(i);
}
}
if (doubtfulResult.length > 0) {
this.logger.warn(`broadcast rpc \`${name}\` getting doubtful responses: ${doubtfulResult.join(',')}`);
}
// FIXME: this is an unreasonable design, if remote service only returned doubtful result, we will return an empty array.
// but actually we should throw an error to tell user that no remote service can handle this call.
// or just return `undefined`.
return result.length === 1 ? result[0] : result;
}
}
export * from './rpc-service';

View File

@ -0,0 +1,191 @@
/**
* Treat a collection of Buffers as a single contiguous partially mutable Buffer.
*
* Where possible, operations execute without creating a new Buffer and copying everything over.
*/
const emptyBuffer = new Uint8Array(0);
function copy(source: Uint8Array, target: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number) {
target.set(source.subarray(sourceStart, sourceEnd), targetStart);
}
export class Buffers {
buffers = [] as Uint8Array[];
protected size = 0;
get byteLength() {
return this.size;
}
push(buffer: Uint8Array) {
this.buffers.push(buffer);
this.size += buffer.length;
}
unshift(buffer: Uint8Array) {
this.buffers.unshift(buffer);
this.size += buffer.length;
}
slice(start?: number, end?: number) {
const buffers = this.buffers;
if (end === undefined) {
end = this.size;
}
if (start === undefined) {
start = 0;
}
if (end > this.size) {
end = this.size;
}
if (start >= end) {
return emptyBuffer;
}
let startBytes = 0;
let si = 0;
for (; si < buffers.length && startBytes + buffers[si].length <= start; si++) {
startBytes += buffers[si].length;
}
const target = new Uint8Array(end - start);
let ti = 0;
for (let ii = si; ti < end - start && ii < buffers.length; ii++) {
const len = buffers[ii].length;
const _start = ti === 0 ? start - startBytes : 0;
const _end = ti + len >= end - start ? Math.min(_start + (end - start) - ti, len) : len;
copy(buffers[ii], target, ti, _start, _end);
ti += _end - _start;
}
return target;
}
pos(i: number): { buf: number; offset: number } {
if (i < 0 || i >= this.size) {
throw new Error('oob');
}
let l = i;
let bi = 0;
let bu: Uint8Array | null = null;
for (;;) {
bu = this.buffers[bi];
if (l < bu.length) {
return { buf: bi, offset: l };
} else {
l -= bu.length;
}
bi++;
}
}
copy(target: Uint8Array, targetStart = 0, sourceStart = 0, sourceEnd = this.size) {
return copy(this.slice(sourceStart, sourceEnd), target, targetStart, 0, sourceEnd - sourceStart);
}
splice(start: number, deleteCount: number, ...reps: Uint8Array[]) {
const buffers = this.buffers;
const index = start >= 0 ? start : this.size - start;
if (deleteCount === undefined) {
deleteCount = this.size - index;
} else if (deleteCount > this.size - index) {
deleteCount = this.size - index;
}
for (const i of reps) {
this.size += i.length;
}
const removed = new Buffers();
let startBytes = 0;
let ii = 0;
for (; ii < buffers.length && startBytes + buffers[ii].length < index; ii++) {
startBytes += buffers[ii].length;
}
if (index - startBytes > 0) {
const start = index - startBytes;
if (start + deleteCount < buffers[ii].length) {
removed.push(buffers[ii].slice(start, start + deleteCount));
const orig = buffers[ii];
const buf0 = new Uint8Array(start);
for (let i = 0; i < start; i++) {
buf0[i] = orig[i];
}
const buf1 = new Uint8Array(orig.length - start - deleteCount);
for (let i = start + deleteCount; i < orig.length; i++) {
buf1[i - deleteCount - start] = orig[i];
}
if (reps.length > 0) {
const reps_ = reps.slice();
reps_.unshift(buf0);
reps_.push(buf1);
buffers.splice.apply(buffers, [ii, 1, ...reps_]);
ii += reps_.length;
reps = [];
} else {
buffers.splice(ii, 1, buf0, buf1);
// buffers[ii] = buf;
ii += 2;
}
} else {
removed.push(buffers[ii].slice(start));
buffers[ii] = buffers[ii].slice(0, start);
ii++;
}
}
if (reps.length > 0) {
buffers.splice.apply(buffers, [ii, 0, ...reps]);
ii += reps.length;
}
while (removed.byteLength < deleteCount) {
const buf = buffers[ii];
const len = buf.length;
const take = Math.min(len, deleteCount - removed.byteLength);
if (take === len) {
removed.push(buf);
buffers.splice(ii, 1);
} else {
removed.push(buf.slice(0, take));
buffers[ii] = buffers[ii].slice(take);
}
}
this.size -= removed.byteLength;
return removed;
}
get(i: number) {
const { buf, offset } = this.pos(i);
return this.buffers[buf][offset];
}
set(i: number, v: number) {
const { buf, offset } = this.pos(i);
this.buffers[buf][offset] = v;
}
toUint8Array() {
return this.slice();
}
dispose() {
this.buffers = [];
this.size = 0;
}
}

View File

@ -0,0 +1,15 @@
import { IDisposable } from '@opensumi/ide-core-common';
import { IConnectionShape } from '../types';
import { createQueue } from './utils';
export abstract class BaseConnection<T> implements IConnectionShape<T> {
abstract send(data: T): void;
abstract onMessage(cb: (data: T) => void): IDisposable;
abstract onceClose(cb: () => void): IDisposable;
createQueue(): IConnectionShape<T> {
return createQueue(this);
}
}

View File

@ -0,0 +1,19 @@
import { IDisposable } from '@opensumi/ide-core-common';
import { BaseConnection } from './base';
export class EmptyConnection extends BaseConnection<Uint8Array> {
send(data: Uint8Array): void {
// do nothing
}
onMessage(cb: (data: Uint8Array) => void): IDisposable {
return {
dispose: () => {},
};
}
onceClose(cb: () => void): IDisposable {
return {
dispose: () => {},
};
}
}

View File

@ -0,0 +1,5 @@
export * from './base';
export * from './utils';
export * from './node-message-port';
export * from './socket';
export * from './ws-websocket';

View File

@ -0,0 +1,33 @@
import type { MessagePort } from 'worker_threads';
import { IDisposable } from '@opensumi/ide-core-common';
import { BaseConnection } from './base';
export class NodeMessagePortConnection extends BaseConnection<Uint8Array> {
constructor(private port: MessagePort) {
super();
}
send(data: Uint8Array): void {
this.port.postMessage(data);
}
onMessage(cb: (data: Uint8Array) => void): IDisposable {
this.port.on('message', cb);
return {
dispose: () => {
this.port.off('message', cb);
},
};
}
onceClose(cb: () => void): IDisposable {
this.port.once('close', cb);
return {
dispose: () => {
this.port.off('close', cb);
},
};
}
}

View File

@ -0,0 +1,86 @@
import ReconnectingWebSocket, { Options as ReconnectingWebSocketOptions, UrlProvider } from 'reconnecting-websocket';
import type { ErrorEvent } from 'reconnecting-websocket';
import { IDisposable } from '@opensumi/ide-core-common';
import { BaseConnection } from './base';
export class ReconnectingWebSocketConnection extends BaseConnection<Uint8Array> {
constructor(private socket: ReconnectingWebSocket) {
super();
}
send(data: Uint8Array): void {
this.socket.send(data);
}
isOpen(): boolean {
return this.socket.readyState === this.socket.OPEN;
}
onOpen(cb: () => void): IDisposable {
this.socket.addEventListener('open', cb);
return {
dispose: () => {
this.socket.removeEventListener('open', cb);
},
};
}
onMessage(cb: (data: Uint8Array) => void): IDisposable {
const handler = (e: MessageEvent) => {
let buffer: Promise<ArrayBuffer>;
if (e.data instanceof Blob) {
buffer = e.data.arrayBuffer();
} else if (e.data instanceof ArrayBuffer) {
buffer = Promise.resolve(e.data);
} else if (e.data?.constructor?.name === 'Buffer') {
// Compatibility with nodejs Buffer in test environment
buffer = Promise.resolve(e.data);
} else {
throw new Error('unknown message type, expect Blob or ArrayBuffer, received: ' + typeof e.data);
}
buffer.then((v) => cb(new Uint8Array(v)));
};
this.socket.addEventListener('message', handler);
return {
dispose: () => {
this.socket.removeEventListener('message', handler);
},
};
}
onceClose(cb: (code?: number, reason?: string) => void): IDisposable {
const handler = (e: CloseEvent) => {
cb(e.code, e.reason);
this.socket.removeEventListener('close', handler);
};
this.socket.addEventListener('close', handler);
return {
dispose: () => {
this.socket.removeEventListener('close', handler);
},
};
}
onError(cb: (e: Error) => void): IDisposable {
const handler = (e: ErrorEvent) => {
cb(e.error);
};
this.socket.addEventListener('error', handler);
return {
dispose: () => {
this.socket.removeEventListener('error', handler);
},
};
}
static forURL(url: UrlProvider, protocols?: string | string[], options?: ReconnectingWebSocketOptions) {
const rawConnection = new ReconnectingWebSocket(url, protocols, options);
rawConnection.binaryType = 'arraybuffer';
const connection = new ReconnectingWebSocketConnection(rawConnection);
return connection;
}
}

View File

@ -0,0 +1,63 @@
import type net from 'net';
import { IDisposable } from '@opensumi/ide-core-common';
import { BaseConnection } from './base';
import { StreamPacketDecoder, createStreamPacket } from './stream-decoder';
export class NetSocketConnection extends BaseConnection<Uint8Array> {
protected decoder = new StreamPacketDecoder();
constructor(private socket: net.Socket) {
super();
this.socket.on('data', (chunk) => {
this.decoder.push(chunk);
});
this.socket.once('close', () => {
this.decoder.dispose();
});
}
isOpen(): boolean {
// currently we use `@types/node@10`, 10.x does not have `readyState` property
return (this.socket as any).readyState === 'open';
}
send(data: Uint8Array): void {
this.socket.write(createStreamPacket(data));
}
onMessage(cb: (data: Uint8Array) => void): IDisposable {
const dispose = this.decoder.onData(cb);
return {
dispose,
};
}
onceClose(cb: () => void): IDisposable {
this.socket.once('close', cb);
return {
dispose: () => {
this.socket.off('close', cb);
},
};
}
onOpen(cb: () => void): IDisposable {
this.socket.on('connect', cb);
return {
dispose: () => {
this.socket.off('connect', cb);
},
};
}
onError(cb: (err: Error) => void): IDisposable {
this.socket.on('error', cb);
return {
dispose: () => {
this.socket.off('error', cb);
},
};
}
}

View File

@ -0,0 +1,202 @@
import { BinaryReader } from '@furyjs/fury/dist/lib/reader';
import { BinaryWriter } from '@furyjs/fury/dist/lib/writer';
import { EventEmitter } from '@opensumi/events';
import { Buffers } from '../buffers';
export const kMagicNumber = 0x53756d69;
const writer = BinaryWriter({});
/**
* When we send data through net.Socket, the data is not guaranteed to be sent as a whole.
*
* So we need to add a header to the data, so that the receiver can know the length of the data,
* The header is 4 bytes, the first 4 bytes is a magic number, which is `Sumi` in little endian.
* use magic number can help us to detect the start of the packet in the stream.
* > You can use `Buffer.from('Sumi')` to get this magic number
*
* The next 4 bytes is a varUInt32, which means the length of the following data, and
* the following data is the content.
*/
export function createStreamPacket(content: Uint8Array) {
writer.reset();
writer.uint32(kMagicNumber);
writer.varUInt32(content.byteLength);
writer.buffer(content);
return writer.dump();
}
export class StreamPacketDecoder {
protected emitter = new EventEmitter<{
data: [Uint8Array];
}>();
protected _buffers = new Buffers();
protected reader = BinaryReader({});
protected _tmpChunksTotalBytesCursor: number;
protected _tmpPacketState: number;
protected _tmpContentLength: number;
constructor() {
this.reset();
}
reset() {
this._tmpChunksTotalBytesCursor = 0;
this._tmpPacketState = 0;
this._tmpContentLength = 0;
}
push(chunk: Uint8Array): void {
this._buffers.push(chunk);
let done = false;
while (!done) {
done = this._parsePacket();
}
}
_parsePacket(): boolean {
const found = this._detectPacketHeader();
if (found) {
const fullBinary = this._buffers.splice(0, this._tmpChunksTotalBytesCursor + this._tmpContentLength);
const binary = fullBinary.splice(this._tmpChunksTotalBytesCursor, this._tmpContentLength).slice();
this.emitter.emit('data', binary);
this.reset();
if (this._buffers.byteLength > 0) {
// has more data, continue to parse
return false;
}
return true;
}
return true;
}
/**
* First we read the first 4 bytes, if it is not magic 4 bytes
* discard it and continue to read the next byte until we get magic 4 bytes
* Then read the next byte, this is a varUint32, which means the length of the following data
* Then read the following data, until we get the length of varUint32, then return this data and continue to read the next packet
*/
_detectPacketHeader() {
if (this._buffers.byteLength === 0) {
return false;
}
if (this._tmpPacketState !== 4) {
this._tmpChunksTotalBytesCursor = this._detectPacketMagicNumber();
}
if (this._tmpPacketState !== 4) {
// Not enough data yet, wait for more data
return false;
}
if (this._tmpChunksTotalBytesCursor + 4 > this._buffers.byteLength) {
// Not enough data yet, wait for more data
return false;
}
if (!this._tmpContentLength) {
// read the content length
const buffers = this._buffers.slice(this._tmpChunksTotalBytesCursor, this._tmpChunksTotalBytesCursor + 4);
this.reader.reset(buffers);
this._tmpContentLength = this.reader.varUInt32();
this._tmpChunksTotalBytesCursor += this.reader.getCursor();
}
if (this._tmpChunksTotalBytesCursor + this._tmpContentLength > this._buffers.byteLength) {
// Not enough data yet, wait for more data
return false;
}
return true;
}
_detectPacketMagicNumber() {
let chunkIndex = 0;
let chunkCursor = 0;
// try read the magic number
row: while (chunkIndex < this._buffers.buffers.length) {
const chunk = this._buffers.buffers[chunkIndex];
const chunkLength = chunk.byteLength;
let chunkOffset = 0;
while (chunkOffset < chunkLength) {
const num = chunk[chunkOffset];
chunkOffset++;
chunkCursor++;
// Fury use little endian to store data
switch (num) {
case 0x69:
switch (this._tmpPacketState) {
case 0:
this._tmpPacketState = 1;
break;
default:
this._tmpPacketState = 0;
break;
}
break;
case 0x6d:
switch (this._tmpPacketState) {
case 1:
this._tmpPacketState = 2;
break;
default:
this._tmpPacketState = 0;
break;
}
break;
case 0x75:
switch (this._tmpPacketState) {
case 2:
this._tmpPacketState = 3;
break;
default:
this._tmpPacketState = 0;
break;
}
break;
case 0x53:
switch (this._tmpPacketState) {
case 3:
this._tmpPacketState = 4;
break row;
default:
this._tmpPacketState = 0;
break;
}
break;
default:
this._tmpPacketState = 0;
break;
}
}
chunkIndex++;
}
return chunkCursor;
}
onData(cb: (data: Uint8Array) => void) {
return this.emitter.on('data', cb);
}
dispose() {
this.reader = BinaryReader({});
this.emitter.dispose();
this._buffers.dispose();
}
}

View File

@ -0,0 +1,52 @@
import { Emitter } from '@opensumi/ide-core-common';
import { IConnectionShape } from '../types';
export class EventQueue<T> {
emitter = new Emitter<T>();
queue: T[] = [];
isOpened = false;
open() {
this.isOpened = true;
this.queue.forEach((data) => {
this.emitter.fire(data);
});
this.queue = [];
}
push(data: T) {
if (this.isOpened) {
this.emitter.fire(data);
} else {
this.queue.push(data);
}
}
on(cb: (data: T) => void) {
const disposable = this.emitter.event(cb);
if (!this.isOpened) {
this.open();
}
return disposable;
}
}
export const createQueue = <T>(socket: IConnectionShape<T>): IConnectionShape<T> => {
const queue = new EventQueue<T>();
socket.onMessage((data) => {
queue.push(data);
});
return {
send: (data) => {
socket.send(data);
},
onMessage: (cb) => queue.on(cb),
onceClose: (cb) => socket.onceClose(cb),
};
};

View File

@ -0,0 +1,31 @@
import type WebSocket from 'ws';
import { IDisposable } from '@opensumi/ide-core-common';
import { BaseConnection } from './base';
export class WSWebSocketConnection extends BaseConnection<Uint8Array> {
constructor(public socket: WebSocket) {
super();
}
send(data: Uint8Array): void {
this.socket.send(data);
}
onMessage(cb: (data: Uint8Array) => void): IDisposable {
this.socket.on('message', cb);
return {
dispose: () => {
this.socket.off('message', cb);
},
};
}
onceClose(cb: () => void): IDisposable {
this.socket.once('close', cb);
return {
dispose: () => {
this.socket.off('close', cb);
},
};
}
}

View File

@ -0,0 +1 @@
export * from './drivers';

View File

@ -0,0 +1,7 @@
import { IDisposable } from '@opensumi/ide-core-common';
export interface IConnectionShape<T> {
send(data: T): void;
onMessage: (cb: (data: T) => void) => IDisposable;
onceClose: (cb: () => void) => IDisposable;
}

View File

@ -0,0 +1 @@
export const METHOD_NOT_REGISTERED = '$$METHOD_NOT_REGISTERED';

View File

@ -1,4 +1,17 @@
import { CancellationToken, CancellationTokenSource, Deferred, Event, Uri } from '@opensumi/ide-core-common';
import {
CancellationToken,
CancellationTokenSource,
Deferred,
Emitter,
Event,
SerializedError,
Uri,
transformErrorForSerialization,
} from '@opensumi/ide-core-common';
import { ILogger } from './types';
import { WSChannel } from './ws-channel';
// Uri: vscode 中的 uri
// URI: 在 vscode 中的 uri 基础上包装了一些基础方法
@ -7,30 +20,6 @@ export enum RPCProtocolEnv {
EXT,
}
export interface SerializedError {
readonly $isError: true;
readonly name: string;
readonly message: string;
readonly stack: string;
}
export function transformErrorForSerialization(error: Error): SerializedError;
export function transformErrorForSerialization(error: any): any;
export function transformErrorForSerialization(error: any): any {
if (error instanceof Error) {
const { name, message } = error;
const stack: string = (error as any).stacktrace || (error as any).stack;
return {
$isError: true,
name,
message,
stack,
};
}
return error;
}
export interface IProxyIdentifier {
serviceId: string;
countId: number;
@ -56,7 +45,7 @@ export function createMainContextProxyIdentifier<T>(identifier: string): ProxyId
return result;
}
export interface IMessagePassingProtocol {
send(msg): void;
send(msg: string): void;
onMessage: Event<string>;
timeout?: number;
}
@ -212,9 +201,9 @@ export class RPCProtocol implements IRPCProtocol {
private readonly _timeoutHandles: Map<string, NodeJS.Timeout | number>;
private _lastMessageId: number;
private _pendingRPCReplies: Map<string, Deferred<any>>;
private logger;
private logger: ILogger;
constructor(connection: IMessagePassingProtocol, logger?: any) {
constructor(connection: IMessagePassingProtocol, logger?: ILogger) {
this._protocol = connection;
this._locals = new Map();
this._proxies = new Map();
@ -290,8 +279,8 @@ export class RPCProtocol implements IRPCProtocol {
return result.promise;
}
private _receiveOneMessage(rawmsg: string): void {
const msg = JSON.parse(rawmsg, ObjectTransfer.reviver);
private _receiveOneMessage(rawMsg: string): void {
const msg = JSON.parse(rawMsg, ObjectTransfer.reviver);
if (this._timeoutHandles.has(msg.id)) {
// 忽略一些 jest 测试场景 clearTimeout not defined 的问题
@ -410,6 +399,29 @@ export class RPCProtocol implements IRPCProtocol {
this._pendingRPCReplies.delete(callId);
this._timeoutHandles.delete(callId);
pendingReply.reject(new Error('RPC Timeout: '+ callId));
pendingReply.reject(new Error('RPC Timeout: ' + callId));
}
}
interface RPCProtocolCreateOptions {
timeout?: number;
}
export function createRPCProtocol(channel: WSChannel, options: RPCProtocolCreateOptions = {}): RPCProtocol {
const onMessageEmitter = new Emitter<string>();
channel.onMessage((msg: string) => {
onMessageEmitter.fire(msg);
});
const onMessage = onMessageEmitter.event;
const send = (msg: string) => {
channel.send(msg);
};
const mainThreadProtocol = new RPCProtocol({
onMessage,
send,
timeout: options.timeout,
});
return mainThreadProtocol;
}

View File

@ -1,6 +1,7 @@
export * from './message';
export * from './proxy';
export * from './rpcProtocol';
export * from './ext-rpc-protocol';
export * from './utils';
export * from './ws-channel';
export * from './connect';
export * from './types';

View File

@ -1,303 +0,0 @@
import { isDefined, uuid } from '@opensumi/ide-core-common';
import type { MessageConnection } from '@opensumi/vscode-jsonrpc/lib/common/connection';
import { MessageType, ResponseStatus, ICapturedMessage, getCapturer } from './utils';
export interface ILogger {
warn(...args: any[]): void;
}
export abstract class RPCService<T = any> {
rpcClient?: T[];
rpcRegistered?: boolean;
register?(): () => Promise<T>;
get client() {
return this.rpcClient ? this.rpcClient[0] : undefined;
}
}
export const NOTREGISTERMETHOD = '$$NOTREGISTERMETHOD';
export class ProxyClient {
public proxy: any;
public reservedWords: string[];
constructor(proxy: any, reservedWords = ['then']) {
this.proxy = proxy;
this.reservedWords = reservedWords;
}
public getClient() {
return new Proxy(
{},
{
get: (target, prop: string | symbol) => {
if (this.reservedWords.includes(prop as string) || typeof prop === 'symbol') {
return Promise.resolve();
} else {
return this.proxy[prop];
}
},
},
);
}
}
interface IRPCResult {
error: boolean;
data: any;
}
export class RPCProxy {
private connectionPromise: Promise<MessageConnection>;
private connectionPromiseResolve: (connection: MessageConnection) => void;
private connection: MessageConnection;
private proxyService: any = {};
private logger: ILogger;
// capture messages for opensumi devtools
private capture(message: ICapturedMessage): void {
const capturer = getCapturer();
if (isDefined(capturer)) {
capturer(message);
}
}
constructor(public target?: RPCService, logger?: ILogger) {
this.waitForConnection();
this.logger = logger || console;
}
public listenService(service) {
if (this.connection) {
const proxyService = this.proxyService;
this.bindOnRequest(service, (service, prop) => {
proxyService[prop] = service[prop].bind(service);
});
} else {
const target = this.target || {};
const methods = this.getServiceMethod(service);
methods.forEach((method) => {
target[method] = service[method].bind(service);
});
}
}
public listen(connection: MessageConnection) {
this.connection = connection;
if (this.target) {
this.listenService(this.target);
}
this.connectionPromiseResolve(connection);
connection.listen();
}
public createProxy(): any {
const proxy = new Proxy(this, this);
const proxyClient = new ProxyClient(proxy);
return proxyClient.getClient();
}
public get(target: any, p: PropertyKey) {
const prop = p.toString();
return (...args: any[]) =>
this.connectionPromise.then((connection) => {
connection = this.connection || connection;
return new Promise((resolve, reject) => {
try {
let isSingleArray = false;
if (args.length === 1 && Array.isArray(args[0])) {
isSingleArray = true;
}
// 调用方法为 on 开头时,作为单项通知
if (prop.startsWith('on')) {
if (isSingleArray) {
connection.sendNotification(prop, [...args]);
this.capture({ type: MessageType.SendNotification, serviceMethod: prop, arguments: args });
} else {
connection.sendNotification(prop, ...args);
this.capture({ type: MessageType.SendNotification, serviceMethod: prop, arguments: args });
}
resolve(null);
} else {
let requestResult: Promise<any>;
// generate a unique requestId to associate request and requestResult
const requestId = uuid();
if (isSingleArray) {
requestResult = connection.sendRequest(prop, [...args]) as Promise<any>;
this.capture({ type: MessageType.SendRequest, requestId, serviceMethod: prop, arguments: args });
} else {
requestResult = connection.sendRequest(prop, ...args) as Promise<any>;
this.capture({ type: MessageType.SendRequest, requestId, serviceMethod: prop, arguments: args });
}
requestResult
.catch((err) => {
reject(err);
})
.then((result: IRPCResult) => {
if (result.error) {
const error = new Error(result.data.message);
if (result.data.stack) {
error.stack = result.data.stack;
}
this.capture({
type: MessageType.RequestResult,
status: ResponseStatus.Fail,
requestId,
serviceMethod: prop,
error: result.data,
});
reject(error);
} else {
this.capture({
type: MessageType.RequestResult,
status: ResponseStatus.Success,
requestId,
serviceMethod: prop,
data: result.data,
});
resolve(result.data);
}
});
}
} catch (e) {}
});
});
}
private getServiceMethod(service): string[] {
let props: any[] = [];
if (/^\s*class/.test(service.constructor.toString())) {
let obj = service;
do {
props = props.concat(Object.getOwnPropertyNames(obj));
} while ((obj = Object.getPrototypeOf(obj)));
props = props.sort().filter((e, i, arr) => e !== arr[i + 1] && typeof service[e] === 'function');
} else {
for (const prop in service) {
if (service[prop] && typeof service[prop] === 'function') {
props.push(prop);
}
}
}
return props;
}
private bindOnRequest(service, cb?) {
if (this.connection) {
const connection = this.connection;
const methods = this.getServiceMethod(service);
methods.forEach((method) => {
if (method.startsWith('on')) {
connection.onNotification(method, (...args) => {
this.onNotification(method, ...args);
this.capture({ type: MessageType.OnNotification, serviceMethod: method, arguments: args });
});
} else {
connection.onRequest(method, (...args) => {
const requestId = uuid();
const result = this.onRequest(method, ...args);
this.capture({ type: MessageType.OnRequest, requestId, serviceMethod: method, arguments: args });
result
.then((result) => {
this.capture({
type: MessageType.OnRequestResult,
status: ResponseStatus.Success,
requestId,
serviceMethod: method,
data: result.data,
});
})
.catch((err) => {
this.capture({
type: MessageType.OnRequestResult,
status: ResponseStatus.Fail,
requestId,
serviceMethod: method,
error: err.data,
});
});
return result;
});
}
if (cb) {
cb(service, method);
}
});
connection.onRequest((method) => {
if (!this.proxyService[method]) {
const requestId = uuid();
this.capture({ type: MessageType.OnRequest, requestId, serviceMethod: method });
const result = {
data: NOTREGISTERMETHOD,
};
this.capture({
type: MessageType.OnRequestResult,
status: ResponseStatus.Fail,
requestId,
serviceMethod: method,
error: result.data,
});
return result;
}
});
}
}
private waitForConnection() {
this.connectionPromise = new Promise((resolve) => {
this.connectionPromiseResolve = resolve;
});
}
/**
* /
* rpc CancellationToken
* , 使 spread (...args) [...args, MutableToken]
* [[...args]], {@link RPCProxy.get get}
* 2 MutableToken
* @param args
* @returns args
*/
private serializeArguments(args: any[]): any[] {
const maybeCancellationToken = args[args.length - 1];
if (args.length === 2 && Array.isArray(args[0]) && maybeCancellationToken.hasOwnProperty('_isCancelled')) {
return [...args[0], maybeCancellationToken];
}
return args;
}
private async onRequest(prop: PropertyKey, ...args: any[]) {
try {
const result = await this.proxyService[prop](...this.serializeArguments(args));
return {
error: false,
data: result,
};
} catch (e) {
return {
error: true,
data: {
message: e.message,
stack: e.stack,
},
};
}
}
private onNotification(prop: PropertyKey, ...args: any[]) {
try {
this.proxyService[prop](...this.serializeArguments(args));
} catch (e) {
this.logger.warn('notification', e);
}
}
}

View File

@ -0,0 +1,67 @@
import { Deferred, isDefined } from '@opensumi/ide-core-common';
import { ILogger, IRPCServiceMap } from '../types';
import { ICapturedMessage, getCapturer, getServiceMethods } from '../utils';
interface IBaseConnection {
listen(): void;
}
export abstract class ProxyBase<T extends IBaseConnection> {
protected proxyService: any = {};
protected logger: ILogger;
protected connection: T;
protected connectionPromise: Deferred<void> = new Deferred<void>();
protected abstract engine: 'legacy';
constructor(public target?: IRPCServiceMap, logger?: ILogger) {
this.logger = logger || console;
}
// capture messages for opensumi devtools
protected capture(message: ICapturedMessage): void {
const capturer = getCapturer();
if (isDefined(capturer)) {
capturer({
...message,
engine: this.engine,
});
}
}
listen(connection: T): void {
this.connection = connection;
if (this.target) {
this.listenService(this.target);
}
connection.listen();
this.connectionPromise.resolve();
}
public listenService(service: IRPCServiceMap) {
if (this.connection) {
this.bindOnRequest(service, (service, prop) => {
this.proxyService[prop] = service[prop].bind(service);
});
} else {
if (!this.target) {
this.target = {} as any;
}
const methods = getServiceMethods(service);
for (const method of methods) {
// `getServiceMethods` ensure that method is a function
(this.target as any)[method] = service[method]!.bind(service);
}
}
}
abstract getInvokeProxy(): any;
protected abstract bindOnRequest(service: IRPCServiceMap, cb: (service: IRPCServiceMap, prop: string) => void): void;
}

View File

@ -0,0 +1,10 @@
export * from './legacy';
export abstract class RPCService<T = any> {
rpcClient?: T[];
rpcRegistered?: boolean;
register?(): () => Promise<T>;
get client() {
return this.rpcClient ? this.rpcClient[0] : undefined;
}
}

View File

@ -0,0 +1,200 @@
import { uuid } from '@opensumi/ide-core-common';
import { MessageConnection } from '@opensumi/vscode-jsonrpc';
import { METHOD_NOT_REGISTERED } from '../constants';
import { IRPCServiceMap } from '../types';
import { MessageType, ResponseStatus, getServiceMethods } from '../utils';
import { ProxyBase } from './base';
interface IRPCResult {
error: boolean;
data: any;
}
export class ProxyLegacy extends ProxyBase<MessageConnection> {
engine = 'legacy' as const;
public getInvokeProxy(): any {
return new Proxy(this, this);
}
public get(target: any, p: PropertyKey) {
const prop = p.toString();
return async (...args: any[]) => {
await this.connectionPromise.promise;
let isSingleArray = false;
if (args.length === 1 && Array.isArray(args[0])) {
isSingleArray = true;
}
// 调用方法为 on 开头时,作为单项通知
if (prop.startsWith('on')) {
if (isSingleArray) {
this.connection.sendNotification(prop, [...args]);
} else {
this.connection.sendNotification(prop, ...args);
}
this.capture({ type: MessageType.SendNotification, serviceMethod: prop, arguments: args });
} else {
let requestResult: Promise<any>;
// generate a unique requestId to associate request and requestResult
const requestId = uuid();
if (isSingleArray) {
requestResult = this.connection.sendRequest(prop, [...args]) as Promise<any>;
} else {
requestResult = this.connection.sendRequest(prop, ...args) as Promise<any>;
}
this.capture({ type: MessageType.SendRequest, requestId, serviceMethod: prop, arguments: args });
const result: IRPCResult = await requestResult;
if (result.error) {
const error = new Error(result.data.message);
if (result.data.stack) {
error.stack = result.data.stack;
}
this.capture({
type: MessageType.RequestResult,
status: ResponseStatus.Fail,
requestId,
serviceMethod: prop,
error: result.data,
});
throw error;
} else {
this.capture({
type: MessageType.RequestResult,
status: ResponseStatus.Success,
requestId,
serviceMethod: prop,
data: result.data,
});
return result.data;
}
}
};
}
protected bindOnRequest(service: IRPCServiceMap, cb?: ((service: IRPCServiceMap, prop: string) => void) | undefined) {
if (this.connection) {
const connection = this.connection;
const methods = getServiceMethods(service);
methods.forEach((method) => {
if (method.startsWith('on')) {
connection.onNotification(method, (...args) => {
this.onNotification(method, ...args);
this.capture({ type: MessageType.OnNotification, serviceMethod: method, arguments: args });
});
} else {
connection.onRequest(method, (...args) => {
const requestId = uuid();
const result = this.onRequest(method, ...args);
this.capture({ type: MessageType.OnRequest, requestId, serviceMethod: method, arguments: args });
result
.then((result) => {
this.capture({
type: MessageType.OnRequestResult,
status: ResponseStatus.Success,
requestId,
serviceMethod: method,
data: result.data,
});
})
.catch((err) => {
this.capture({
type: MessageType.OnRequestResult,
status: ResponseStatus.Fail,
requestId,
serviceMethod: method,
error: err.data,
});
});
return result;
});
}
if (cb) {
cb(service, method);
}
});
}
}
/**
* /
* rpc CancellationToken
* , 使 spread (...args) [...args, MutableToken]
* [[...args]], {@link ProxyLegacy.get get}
* 2 MutableToken
* @param args
* @returns args
*/
private serializeArguments(args: any[]): any[] {
const maybeCancellationToken = args[args.length - 1];
if (
args.length === 2 &&
Array.isArray(args[0]) &&
Object.prototype.hasOwnProperty.call(maybeCancellationToken, '_isCancelled')
) {
return [...args[0], maybeCancellationToken];
}
return args;
}
private async onRequest(prop: PropertyKey, ...args: any[]) {
try {
const result = await this.proxyService[prop](...this.serializeArguments(args));
return {
error: false,
data: result,
};
} catch (e) {
return {
error: true,
data: {
message: e.message,
stack: e.stack,
},
};
}
}
private onNotification(prop: PropertyKey, ...args: any[]) {
try {
this.proxyService[prop](...this.serializeArguments(args));
} catch (e) {
this.logger.warn('notification', e);
}
}
listen(connection: MessageConnection): void {
super.listen(connection);
connection.onRequest((method) => {
if (!this.proxyService[method]) {
const requestId = uuid();
this.capture({ type: MessageType.OnRequest, requestId, serviceMethod: method });
const result = {
data: METHOD_NOT_REGISTERED,
};
this.capture({
type: MessageType.OnRequestResult,
status: ResponseStatus.Fail,
requestId,
serviceMethod: method,
error: result.data,
});
return result;
}
});
}
}

View File

@ -0,0 +1,124 @@
import { Deferred } from '@opensumi/ide-core-common';
import { METHOD_NOT_REGISTERED } from '../constants';
import { ProxyLegacy } from '../proxy';
import { IBench, ILogger, IRPCServiceMap, RPCServiceMethod, ServiceType } from '../types';
import { getMethodName } from '../utils';
import { WSChannel } from '../ws-channel';
const safeProcess: { pid: string } = typeof process === 'undefined' ? { pid: 'mock' } : (process as any);
const defaultReservedWordSet = new Set(['then']);
class Invoker {
legacyProxy: ProxyLegacy;
private legacyInvokeProxy: any;
setLegacyProxy(proxy: ProxyLegacy) {
this.legacyProxy = proxy;
this.legacyInvokeProxy = proxy.getInvokeProxy();
}
invoke(name: string, ...args: any[]) {
if (defaultReservedWordSet.has(name) || typeof name === 'symbol') {
return Promise.resolve();
}
return this.legacyInvokeProxy[name](...args);
}
}
export class RPCServiceCenter {
public uid: string;
private invokers: Invoker[] = [];
private connection: Array<WSChannel> = [];
private serviceMethodMap = { client: undefined } as unknown as IRPCServiceMap;
private connectionDeferred = new Deferred<void>();
private logger: ILogger;
constructor(private bench?: IBench, logger?: ILogger) {
this.uid = 'RPCServiceCenter:' + safeProcess.pid;
this.logger = logger || console;
}
registerService(serviceName: string, type: ServiceType): void {
if (type === ServiceType.Service) {
if (this.bench) {
this.bench.registerService(serviceName);
}
}
}
ready() {
return this.connectionDeferred.promise;
}
setChannel(channel: WSChannel) {
if (!this.connection.length) {
this.connectionDeferred.resolve();
}
this.connection.push(channel);
const index = this.connection.length - 1;
const rpcProxy = new ProxyLegacy(this.serviceMethodMap, this.logger);
const connection = channel.createMessageConnection();
rpcProxy.listen(connection);
const invoker = new Invoker();
invoker.setLegacyProxy(rpcProxy);
this.invokers.push(invoker);
return {
dispose: () => {
this.connection.splice(index, 1);
this.invokers.splice(index, 1);
connection.dispose();
},
};
}
onRequest(serviceName: string, _name: string, method: RPCServiceMethod) {
const name = getMethodName(serviceName, _name);
if (!this.connection.length) {
this.serviceMethodMap[name] = method;
} else {
this.invokers.forEach((proxy) => {
proxy.legacyProxy.listenService({ [name]: method });
});
}
}
async broadcast(serviceName: string, _name: string, ...args: any[]): Promise<any> {
const name = getMethodName(serviceName, _name);
const broadcastResult = await Promise.all(this.invokers.map((proxy) => proxy.invoke(name, ...args)));
const doubtfulResult = [] as any[];
const result = [] as any[];
for (const i of broadcastResult) {
if (i === METHOD_NOT_REGISTERED) {
doubtfulResult.push(i);
} else {
result.push(i);
}
}
if (doubtfulResult.length > 0) {
this.logger.warn(`broadcast rpc \`${name}\` getting doubtful responses: ${doubtfulResult.join(',')}`);
}
if (result.length === 0) {
throw new Error(`broadcast rpc \`${name}\` error: no remote service can handle this call`);
}
// FIXME: this is an unreasonable design, if remote service only returned doubtful result, we will return an empty array.
// but actually we should throw an error to tell user that no remote service can handle this call.
// or just return `undefined`.
return result.length === 1 ? result[0] : result;
}
}

View File

@ -0,0 +1,2 @@
export * from './stub';
export * from './center';

View File

@ -0,0 +1,49 @@
import { RPCServiceMethod, ServiceType } from '../types';
import { getServiceMethods } from '../utils';
import { RPCServiceCenter } from './center';
export class RPCServiceStub {
constructor(private serviceName: string, private center: RPCServiceCenter, private type: ServiceType) {
this.center.registerService(serviceName, this.type);
}
async ready() {
return this.center.ready();
}
on(name: string, method: RPCServiceMethod) {
this.onRequest(name, method);
}
onRequestService(service: any) {
const methods = getServiceMethods(service);
for (const method of methods) {
this.onRequest(method, service[method].bind(service));
}
}
onRequest(name: string, method: RPCServiceMethod) {
this.center.onRequest(this.serviceName, name, method);
}
broadcast(name: string, ...args: any[]): Promise<any> {
return this.center.broadcast(this.serviceName, name, ...args);
}
getProxy = <T>() =>
new Proxy<T extends void ? RPCServiceStub : RPCServiceStub & T>(this as any, {
// 调用方
get: (target, prop: string) => {
if (!target[prop]) {
if (typeof prop === 'symbol') {
return Promise.resolve();
} else {
return (...args: any[]) => this.ready().then(() => this.broadcast(prop, ...args));
}
} else {
return target[prop];
}
},
});
}

View File

@ -0,0 +1,17 @@
export interface ILogger {
log(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
}
export type RPCServiceMethod = (...args: any[]) => any;
export type IRPCServiceMap = Record<string, RPCServiceMethod>;
export enum ServiceType {
Service,
Stub,
}
export interface IBench {
registerService: (service: string) => void;
}

View File

@ -41,17 +41,40 @@ export interface WSCloseInfo {
connectInfo: ConnectionInfo;
}
export function stringify(obj: any): string {
return JSON.stringify(obj);
}
export function parse(input: string, reviver?: (this: any, key: string, value: any) => any): any {
return JSON.parse(input, reviver);
}
export function getCapturer() {
if (typeof window !== 'undefined' && window.__OPENSUMI_DEVTOOLS_GLOBAL_HOOK__?.captureRPC) {
return window.__OPENSUMI_DEVTOOLS_GLOBAL_HOOK__.captureRPC;
}
return;
}
export function getServiceMethods(service: any): string[] {
let props: any[] = [];
if (/^\s*class/.test(service.constructor.toString())) {
let obj = service;
do {
props = props.concat(Object.getOwnPropertyNames(obj));
} while ((obj = Object.getPrototypeOf(obj)));
props = props.sort().filter((e, i, arr) => e !== arr[i + 1] && typeof service[e] === 'function');
} else {
for (const prop in service) {
if (service[prop] && typeof service[prop] === 'function') {
props.push(prop);
}
}
}
return props;
}
export function getNotificationName(serviceName: string, name: string) {
return `on:${serviceName}:${name}`;
}
export function getRequestName(serviceName: string, name: string) {
return `${serviceName}:${name}`;
}
export function getMethodName(serviceName: string, name: string) {
return name.startsWith('on') ? getNotificationName(serviceName, name) : getRequestName(serviceName, name);
}

View File

@ -1,105 +1,188 @@
import { stringify } from './utils';
import type net from 'net';
import Fury, { Type } from '@furyjs/fury';
import type WebSocket from 'ws';
import { EventEmitter } from '@opensumi/events';
import { DisposableCollection } from '@opensumi/ide-core-common';
import { NetSocketConnection, WSWebSocketConnection } from './connection';
import { IConnectionShape } from './connection/types';
import { createWebSocketConnection } from './message';
import { ILogger } from './types';
export interface IWebSocket {
send(content: string): void;
close(...args): void;
close(...args: any[]): void;
onMessage(cb: (data: any) => void): void;
onError(cb: (reason: any) => void): void;
onClose(cb: (code: number, reason: string) => void): void;
}
export interface ClientMessage {
kind: 'client';
/**
* `ping` and `pong` are used to detect whether the connection is alive.
*/
export interface PingMessage {
kind: 'ping';
id: string;
clientId: string;
}
export interface HeartbeatMessage {
kind: 'heartbeat';
/**
* when server receive a `ping` message, it should reply a `pong` message, vice versa.
*/
export interface PongMessage {
kind: 'pong';
id: string;
clientId: string;
}
/**
* `open` message is used to open a new channel.
* `path` is used to identify which handler should be used to handle the channel.
* `clientId` is used to identify the client.
*/
export interface OpenMessage {
kind: 'open';
id: number;
id: string;
path: string;
clientId: string;
}
export interface ReadyMessage {
kind: 'ready';
id: number;
/**
* when server receive a `open` message, it should reply a `server-ready` message.
* this is indicate that the channel is ready to use.
*/
export interface ServerReadyMessage {
kind: 'server-ready';
id: string;
}
/**
* `data` message indicate that the channel has received some data.
* the `content` field is the data, it should be a string.
*/
export interface DataMessage {
kind: 'data';
id: number;
id: string;
content: string;
}
export interface CloseMessage {
kind: 'close';
id: number;
id: string;
code: number;
reason: string;
}
export type ChannelMessage = HeartbeatMessage | ClientMessage | OpenMessage | ReadyMessage | DataMessage | CloseMessage;
export type ChannelMessage = PingMessage | PongMessage | OpenMessage | ServerReadyMessage | DataMessage | CloseMessage;
export interface IWSChannelCreateOptions {
/**
* every channel's unique id, it only used in client to server architecture.
* server will store this id and use it to identify which channel should be used.
*/
id: string;
logger?: ILogger;
}
export class WSChannel implements IWebSocket {
public id: number | string;
protected emitter = new EventEmitter<{
message: [data: string];
open: [id: string];
reopen: [];
close: [code?: number, reason?: string];
}>();
public id: string;
public LOG_TAG = '[WSChannel]';
public channelPath: string;
private connectionSend: (content: string) => void;
private fireMessage: (data: any) => void;
private fireOpen: (id: number) => void;
public fireReOpen: () => void;
private fireClose: (code: number, reason: string) => void;
logger: ILogger = console;
public messageConnection: any;
static forClient(connection: IConnectionShape<Uint8Array>, options: IWSChannelCreateOptions) {
const disposable = new DisposableCollection();
const channel = new WSChannel(connection, options);
constructor(connectionSend: (content: string) => void, id?: number | string) {
this.connectionSend = connectionSend;
if (id) {
this.id = id;
}
disposable.push(
connection.onMessage((data) => {
channel.handleMessage(parse(data));
}),
);
disposable.push(channel);
disposable.push(
connection.onceClose(() => {
disposable.dispose();
}),
);
return channel;
}
public setConnectionSend(connectionSend: (content: string) => void) {
this.connectionSend = connectionSend;
static forWebSocket(socket: WebSocket, options: IWSChannelCreateOptions) {
const wsConnection = new WSWebSocketConnection(socket);
return WSChannel.forClient(wsConnection, options);
}
static forNetSocket(socket: net.Socket, options: IWSChannelCreateOptions) {
const wsConnection = new NetSocketConnection(socket);
return WSChannel.forClient(wsConnection, options);
}
constructor(public connection: IConnectionShape<Uint8Array>, options: IWSChannelCreateOptions) {
const { id, logger } = options;
this.id = id;
if (logger) {
this.logger = logger;
}
this.LOG_TAG = `[WSChannel] [id:${id}]`;
}
// server
onMessage(cb: (data: any) => any) {
this.fireMessage = cb;
onMessage(cb: (data: string) => any) {
return this.emitter.on('message', cb);
}
onOpen(cb: (id: number) => void) {
this.fireOpen = cb;
onOpen(cb: (id: string) => void) {
return this.emitter.on('open', cb);
}
onReOpen(cb: () => void) {
this.fireReOpen = cb;
onReopen(cb: () => void) {
return this.emitter.on('reopen', cb);
}
ready() {
this.connectionSend(
serverReady() {
this.connection.send(
stringify({
kind: 'ready',
kind: 'server-ready',
id: this.id,
}),
);
}
handleMessage(msg: ChannelMessage) {
if (msg.kind === 'ready' && this.fireOpen) {
this.fireOpen(msg.id);
} else if (msg.kind === 'data' && this.fireMessage) {
this.fireMessage(msg.content);
if (msg.kind === 'server-ready') {
this.emitter.emit('open', msg.id);
} else if (msg.kind === 'data') {
this.emitter.emit('message', msg.content);
}
}
// client
open(path: string) {
open(path: string, clientId: string) {
this.channelPath = path;
this.connectionSend(
this.connection.send(
stringify({
kind: 'open',
id: this.id,
path,
clientId,
}),
);
}
send(content: string) {
this.connectionSend(
this.connection.send(
stringify({
kind: 'data',
id: this.id,
@ -107,14 +190,36 @@ export class WSChannel implements IWebSocket {
}),
);
}
hasMessageListener() {
return this.emitter.hasListener('message');
}
onError() {}
close(code: number, reason: string) {
if (this.fireClose) {
this.fireClose(code, reason);
}
close(code?: number, reason?: string) {
this.emitter.emit('close', code, reason);
}
fireReopen() {
this.emitter.emit('reopen');
}
onClose(cb: (code: number, reason: string) => void) {
this.fireClose = cb;
return this.emitter.on('close', cb);
}
createMessageConnection() {
return createWebSocketConnection(this);
}
dispose() {
this.emitter.dispose();
}
listen(channel: WSChannel) {
channel.onMessage((data) => {
this.send(data);
});
channel.onClose((code, reason) => {
this.close(code, reason);
});
channel.onReopen(() => {
this.fireReopen();
});
}
}
@ -142,3 +247,25 @@ export class ChildConnectPath {
};
}
}
const fury = new Fury({});
export const wsChannelProtocol = Type.object('ws-channel-protocol', {
kind: Type.string(),
clientId: Type.string(),
id: Type.string(),
path: Type.string(),
content: Type.string(),
code: Type.uint32(),
reason: Type.string(),
});
const wsChannelProtocolSerializer = fury.registerSerializer(wsChannelProtocol);
export function stringify(obj: ChannelMessage): Uint8Array {
return wsChannelProtocolSerializer.serialize(obj);
}
export function parse(input: Uint8Array): ChannelMessage {
return wsChannelProtocolSerializer.deserialize(input) as any;
}

View File

@ -1,23 +1,24 @@
import pathMatch from 'path-match';
import ws from 'ws';
import { MatchFunction, match } from 'path-to-regexp';
import WebSocket from 'ws';
import { stringify, parse } from '../common/utils';
import { WSChannel, ChannelMessage } from '../common/ws-channel';
import { ILogger } from '../common';
import { WSWebSocketConnection } from '../common/connection';
import { WSChannel, ChannelMessage, stringify, parse } from '../common/ws-channel';
import { WebSocketHandler, CommonChannelHandlerOptions } from './ws';
export interface IPathHander {
export interface IPathHandler {
dispose: (connection: any, connectionId: string) => void;
handler: (connection: any, connectionId: string, params?: any) => void;
handler: (connection: any, connectionId: string, params?: Record<string, string>) => void;
reconnect?: (connection: any, connectionId: string) => void;
connection?: any;
}
export class CommonChannelPathHandler {
private handlerMap: Map<string, IPathHander[]> = new Map();
private handlerMap: Map<string, IPathHandler[]> = new Map();
private paramsKey: Map<string, string> = new Map();
register(channelPath: string, handler: IPathHander) {
register(channelPath: string, handler: IPathHandler) {
const paramsIndex = channelPath.indexOf('/:');
const hasParams = paramsIndex >= 0;
let channelToken = channelPath;
@ -28,18 +29,18 @@ export class CommonChannelPathHandler {
if (!this.handlerMap.has(channelToken)) {
this.handlerMap.set(channelToken, []);
}
const handlerArr = this.handlerMap.get(channelToken) as IPathHander[];
const handlerArr = this.handlerMap.get(channelToken) as IPathHandler[];
const handlerFn = handler.handler.bind(handler);
const setHandler = (connection, clientId, params) => {
handler.connection = connection;
handlerFn(connection, clientId, params);
const setHandler = (channel: WSChannel, clientId: string, params: any) => {
handler.connection = channel;
handlerFn(channel, clientId, params);
};
handler.handler = setHandler;
handlerArr.push(handler);
this.handlerMap.set(channelToken, handlerArr);
}
getParams(channelPath: string, value: string) {
const params = {};
getParams(channelPath: string, value: string): Record<string, string> {
const params = {} as Record<string, string>;
if (this.paramsKey.has(channelPath)) {
const key = this.paramsKey.get(channelPath);
if (key) {
@ -48,7 +49,7 @@ export class CommonChannelPathHandler {
}
return params;
}
removeHandler(channelPath: string, handler: IPathHander) {
removeHandler(channelPath: string, handler: IPathHandler) {
const paramsIndex = channelPath.indexOf(':');
const hasParams = paramsIndex >= 0;
let channelToken = channelPath;
@ -65,9 +66,9 @@ export class CommonChannelPathHandler {
get(channelPath: string) {
return this.handlerMap.get(channelPath);
}
disposeConnectionClientId(connection: ws, clientId: string) {
this.handlerMap.forEach((handlerArr: IPathHander[]) => {
handlerArr.forEach((handler: IPathHander) => {
disposeConnectionClientId(connection: WebSocket, clientId: string) {
this.handlerMap.forEach((handlerArr: IPathHandler[]) => {
handlerArr.forEach((handler: IPathHandler) => {
handler.dispose(connection, clientId);
});
});
@ -79,28 +80,26 @@ export class CommonChannelPathHandler {
export const commonChannelPathHandler = new CommonChannelPathHandler();
// 后台 Web 链接处理类
/**
* Channel Handler for nodejs
*/
export class CommonChannelHandler extends WebSocketHandler {
static channelId = 0;
public handlerId = 'common-channel';
private wsServer: ws.Server;
protected handlerRoute: (wsPathname: string) => any;
private channelMap: Map<string | number, WSChannel> = new Map();
private connectionMap: Map<string, ws> = new Map();
private wsServer: WebSocket.Server;
protected handlerRoute: MatchFunction;
private channelMap: Map<string, WSChannel> = new Map();
private heartbeatMap: Map<string, NodeJS.Timeout> = new Map();
constructor(routePath: string, private logger: any = console, private options: CommonChannelHandlerOptions = {}) {
constructor(routePath: string, private logger: ILogger = console, private options: CommonChannelHandlerOptions = {}) {
super();
const route = pathMatch(options.pathMatchOptions);
this.handlerRoute = route(`${routePath}`);
this.handlerRoute = match(routePath, options.pathMatchOptions);
this.initWSServer();
}
private hearbeat(connectionId: string, connection: ws) {
private heartbeat(connectionId: string, connection: WebSocket) {
const timer = global.setTimeout(() => {
connection.ping();
// console.log(`connectionId ${connectionId} ping`);
this.hearbeat(connectionId, connection);
this.heartbeat(connectionId, connection);
}, 5000);
this.heartbeatMap.set(connectionId, timer);
@ -108,40 +107,38 @@ export class CommonChannelHandler extends WebSocketHandler {
private initWSServer() {
this.logger.log('init Common Channel Handler');
this.wsServer = new ws.Server({
this.wsServer = new WebSocket.Server({
noServer: true,
...this.options.wsServerOptions,
});
this.wsServer.on('connection', (connection: ws) => {
let connectionId;
connection.on('message', (msg: string) => {
this.wsServer.on('connection', (connection: WebSocket) => {
let clientId: string;
connection.on('message', (msg: Uint8Array) => {
let msgObj: ChannelMessage;
try {
msgObj = parse(msg);
// 心跳消息
if (msgObj.kind === 'heartbeat') {
connection.send(stringify(`heartbeat ${msgObj.clientId}`));
} else if (msgObj.kind === 'client') {
const clientId = msgObj.clientId;
this.logger.log(`New connection with id ${clientId}`);
connectionId = clientId;
this.connectionMap.set(clientId, connection);
this.hearbeat(connectionId, connection);
// channel 消息处理
if (msgObj.kind === 'ping') {
connection.send(
stringify({
kind: 'pong',
id: msgObj.id,
clientId,
}),
);
} else if (msgObj.kind === 'open') {
const channelId = msgObj.id; // CommonChannelHandler.channelId ++;
const { path } = msgObj;
this.logger.log(`Open a new connection channel ${channelId} with path ${path}`);
const { id, path } = msgObj;
clientId = msgObj.clientId;
this.logger.log(`open a new connection channel ${clientId} with path ${path}`);
this.heartbeat(id, connection);
// 生成 channel 对象
const connectionSend = this.channelConnectionSend(connection);
const channel = new WSChannel(connectionSend, channelId);
this.channelMap.set(channelId, channel);
const channel = new WSChannel(new WSWebSocketConnection(connection), { id });
this.channelMap.set(id, channel);
// 根据 path 拿到注册的 handler
let handlerArr = commonChannelPathHandler.get(path);
let params;
let params: Record<string, string> | undefined;
// 尝试通过父路径查找处理函数如server/:id方式注册的handler
if (!handlerArr) {
const slashIndex = path.indexOf('/');
@ -155,11 +152,11 @@ export class CommonChannelHandler extends WebSocketHandler {
if (handlerArr) {
for (let i = 0, len = handlerArr.length; i < len; i++) {
const handler = handlerArr[i];
handler.handler(channel, connectionId, params);
handler.handler(channel, clientId, params);
}
}
channel.ready();
channel.serverReady();
} else {
const { id } = msgObj;
const channel = this.channelMap.get(id);
@ -170,51 +167,42 @@ export class CommonChannelHandler extends WebSocketHandler {
}
}
} catch (e) {
this.logger.warn(e);
this.logger.error('handle connection message error', e);
}
});
connection.on('close', () => {
commonChannelPathHandler.disposeConnectionClientId(connection, connectionId as string);
connection.once('close', () => {
commonChannelPathHandler.disposeConnectionClientId(connection, clientId);
if (this.heartbeatMap.has(connectionId)) {
clearTimeout(this.heartbeatMap.get(connectionId) as NodeJS.Timeout);
this.heartbeatMap.delete(connectionId);
if (this.heartbeatMap.has(clientId)) {
clearTimeout(this.heartbeatMap.get(clientId) as NodeJS.Timeout);
this.heartbeatMap.delete(clientId);
this.logger.verbose(`Clear heartbeat from channel ${connectionId}`);
this.logger.log(`Clear heartbeat from channel ${clientId}`);
}
Array.from(this.channelMap.values())
.filter((channel) => channel.id.toString().indexOf(connectionId) !== -1)
.filter((channel) => channel.id.toString().indexOf(clientId) !== -1)
.forEach((channel) => {
channel.close(1, 'close');
channel.dispose();
this.channelMap.delete(channel.id);
this.logger.verbose(`Remove connection channel ${channel.id}`);
this.logger.log(`Remove connection channel ${channel.id}`);
});
});
});
}
private channelConnectionSend = (connection: ws) => (content: string) => {
if (connection.readyState === connection.OPEN) {
connection.send(content, (err: any) => {
if (err) {
this.logger.log(err);
}
});
}
};
public handleUpgrade(wsPathname: string, request: any, socket: any, head: any): boolean {
const routeResult = this.handlerRoute(wsPathname);
public handleUpgrade(pathname: string, request: any, socket: any, head: any): boolean {
const routeResult = this.handlerRoute(pathname);
if (routeResult) {
const wsServer = this.wsServer;
wsServer.handleUpgrade(request, socket, head, (connection: any) => {
connection.routeParam = {
pathname: wsPathname,
this.wsServer.handleUpgrade(request, socket, head, (connection) => {
(connection as any).routeParam = {
pathname,
};
wsServer.emit('connection', connection);
this.wsServer.emit('connection', connection);
});
return true;
}

View File

@ -1,11 +0,0 @@
import type net from 'net';
import {
SocketMessageReader,
SocketMessageWriter,
createMessageConnection,
} from '@opensumi/vscode-jsonrpc/lib/node/main';
export function createSocketConnection(socket: net.Socket) {
return createMessageConnection(new SocketMessageReader(socket), new SocketMessageWriter(socket));
}

View File

@ -1,7 +1,2 @@
import { SocketMessageReader, SocketMessageWriter } from '@opensumi/vscode-jsonrpc/lib/node/main';
export * from './ws';
export * from './common-channel-handler';
export * from './connect';
export { SocketMessageReader, SocketMessageWriter };

View File

@ -5,7 +5,7 @@ import ws from 'ws';
export abstract class WebSocketHandler {
abstract handlerId: string;
abstract handleUpgrade(wsPathname: string, request: any, socket: any, head: any): boolean;
abstract handleUpgrade(pathname: string, request: any, socket: any, head: any): boolean;
init?(): void;
}

View File

@ -1,11 +1,10 @@
import { WebSocket, Server } from 'mock-socket';
import { IEventBus, BrowserConnectionErrorEvent } from '@opensumi/ide-core-common';
import { WebSocket, Server } from '@opensumi/mock-socket';
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { MockInjector } from '../../../../tools/dev-tool/src/mock-injector';
import { ClientAppStateService } from '../../src/application';
import { createClientConnection2 } from '../../src/bootstrap/connection';
import { createClientConnection4Web } from '../../src/bootstrap/connection';
(global as any).WebSocket = WebSocket;
describe('packages/core-browser/src/bootstrap/connection.test.ts', () => {
@ -30,7 +29,7 @@ describe('packages/core-browser/src/bootstrap/connection.test.ts', () => {
done();
});
stateService = injector.get(ClientAppStateService);
createClientConnection2(injector, [], fakeWSURL, () => {});
createClientConnection4Web(injector, [], fakeWSURL, () => {});
stateService.state = 'core_module_initialized';
new Promise<void>((resolve) => {
setTimeout(() => {

View File

@ -6,7 +6,7 @@ import '@opensumi/monaco-editor-core/esm/vs/editor/editor.main';
import ResizeObserver from 'resize-observer-polyfill';
import { Injector } from '@opensumi/di';
import { RPCMessageConnection } from '@opensumi/ide-connection';
import { WSChannel } from '@opensumi/ide-connection';
import { WSChannelHandler } from '@opensumi/ide-connection/lib/browser';
import {
CommandRegistry,
@ -45,7 +45,6 @@ import {
} from '@opensumi/ide-core-common/lib/const/application';
import { IElectronMainLifeCycleService } from '@opensumi/ide-core-common/lib/electron';
import { createElectronClientConnection } from '..';
import { ClientAppStateService } from '../application';
import { BrowserModule, IClientApp } from '../browser-module';
import { ClientAppContribution } from '../common';
@ -71,7 +70,7 @@ import { electronEnv } from '../utils';
import { IClientAppOpts, IconInfo, IconMap, IPreferences, LayoutConfig, ModuleConstructor } from './app.interface';
import { renderClientApp, IAppRenderer } from './app.view';
import { createClientConnection2, bindConnectionService } from './connection';
import { createClientConnection4Web, createClientConnection4Electron, bindConnectionService } from './connection';
import { injectInnerProviders } from './inner-providers';
import { injectElectronInnerProviders } from './inner-providers-electron';
@ -208,35 +207,33 @@ export class ClientApp implements IClientApp, IDisposable {
public async start(
container: HTMLElement | IAppRenderer,
type?: 'electron' | 'web',
connection?: RPCMessageConnection,
channel?: WSChannel,
): Promise<void> {
const reporterService: IReporterService = this.injector.get(IReporterService);
const measureReporter = reporterService.time(REPORT_NAME.MEASURE);
this.lifeCycleService.phase = LifeCyclePhase.Prepare;
if (connection) {
await bindConnectionService(this.injector, this.modules, connection);
} else {
if (type === 'electron') {
await bindConnectionService(this.injector, this.modules, createElectronClientConnection());
} else if (type === 'web') {
await createClientConnection2(
this.injector,
this.modules,
this.connectionPath,
() => {
this.onReconnectContributions();
},
this.connectionProtocols,
this.config.clientId,
);
this.logger = this.getLogger();
// Replace Logger
this.injector.get(WSChannelHandler).replaceLogger(this.logger);
}
if (channel) {
await bindConnectionService(this.injector, this.modules, channel);
} else if (type === 'electron') {
await createClientConnection4Electron(this.injector, this.modules, this.config.clientId);
} else if (type === 'web') {
await createClientConnection4Web(
this.injector,
this.modules,
this.connectionPath,
() => {
this.onReconnectContributions();
},
this.connectionProtocols,
this.config.clientId,
);
this.logger = this.getLogger();
// Replace Logger
this.injector.get(WSChannelHandler).replaceLogger(this.logger);
}
measureReporter.timeEnd('ClientApp.createConnection');
this.logger = this.getLogger();
@ -402,7 +399,6 @@ export class ClientApp implements IClientApp, IDisposable {
const eventBus = this.injector.get(IEventBus);
eventBus.fire(new RenderedEvent());
}
protected async measure<T>(name: string, fn: () => MaybePromise<T>): Promise<T> {
const reporterService: IReporterService = this.injector.get(IReporterService);
const measureReporter = reporterService.time(REPORT_NAME.MEASURE);

View File

@ -1,7 +1,8 @@
import { Injector, Provider } from '@opensumi/di';
import { RPCServiceCenter, initRPCService, RPCMessageConnection } from '@opensumi/ide-connection';
import { RPCServiceCenter, WSChannel, initRPCService } from '@opensumi/ide-connection';
import { WSChannelHandler } from '@opensumi/ide-connection/lib/browser';
import { createWebSocketConnection } from '@opensumi/ide-connection/lib/common/message';
import { NetSocketConnection } from '@opensumi/ide-connection/lib/common/connection';
import { ReconnectingWebSocketConnection } from '@opensumi/ide-connection/lib/common/connection/drivers/reconnecting-websocket';
import {
getDebugLogger,
IReporterService,
@ -15,63 +16,104 @@ import {
import { BackService } from '@opensumi/ide-core-common/lib/module';
import { ClientAppStateService } from '../application';
import { createNetSocketConnection, fromWindowClientId } from '../utils';
import { ModuleConstructor } from './app.interface';
const initialLogger = getDebugLogger();
export async function createClientConnection2(
export async function createClientConnection4Web(
injector: Injector,
modules: ModuleConstructor[],
wsPath: UrlProvider,
onReconnect: () => void,
protocols?: string[],
clientId?: string,
) {
return createConnectionService(
injector,
modules,
onReconnect,
ReconnectingWebSocketConnection.forURL(wsPath, protocols),
clientId,
);
}
export async function createClientConnection4Electron(
injector: Injector,
modules: ModuleConstructor[],
clientId?: string,
) {
const connection = createNetSocketConnection();
const channel = WSChannel.forClient(connection, {
id: clientId || fromWindowClientId('RPCService'),
logger: console,
});
return bindConnectionService(injector, modules, channel);
}
export async function createConnectionService(
injector: Injector,
modules: ModuleConstructor[],
onReconnect: () => void,
connection: ReconnectingWebSocketConnection | NetSocketConnection,
clientId?: string,
) {
const reporterService: IReporterService = injector.get(IReporterService);
const eventBus = injector.get(IEventBus);
const stateService = injector.get(ClientAppStateService);
const wsChannelHandler = new WSChannelHandler(wsPath, initialLogger, protocols, clientId);
wsChannelHandler.setReporter(reporterService);
wsChannelHandler.connection.addEventListener('open', async () => {
await stateService.reachedState('core_module_initialized');
eventBus.fire(new BrowserConnectionOpenEvent());
const channelHandler = new WSChannelHandler(connection, initialLogger, clientId);
channelHandler.setReporter(reporterService);
const onOpen = () => {
stateService.reachedState('core_module_initialized').then(() => {
eventBus.fire(new BrowserConnectionOpenEvent());
});
};
if (channelHandler.connection.isOpen()) {
onOpen();
} else {
const dispose = channelHandler.connection.onOpen(() => {
onOpen();
dispose.dispose();
});
}
channelHandler.connection.onceClose(() => {
stateService.reachedState('core_module_initialized').then(() => {
eventBus.fire(new BrowserConnectionCloseEvent());
});
});
wsChannelHandler.connection.addEventListener('close', async () => {
await stateService.reachedState('core_module_initialized');
eventBus.fire(new BrowserConnectionCloseEvent());
channelHandler.connection.onError((e) => {
stateService.reachedState('core_module_initialized').then(() => {
eventBus.fire(new BrowserConnectionErrorEvent(e));
});
});
wsChannelHandler.connection.addEventListener('error', async (e) => {
await stateService.reachedState('core_module_initialized');
eventBus.fire(new BrowserConnectionErrorEvent(e));
});
await wsChannelHandler.initHandler();
await channelHandler.initHandler();
injector.addProviders({
token: WSChannelHandler,
useValue: wsChannelHandler,
useValue: channelHandler,
});
// 重连不会执行后面的逻辑
const channel = await wsChannelHandler.openChannel('RPCService');
channel.onReOpen(() => onReconnect());
bindConnectionService(injector, modules, createWebSocketConnection(channel));
// 重连不会执行后面的逻辑
const channel = await channelHandler.openChannel('RPCService');
channel.onReopen(() => onReconnect());
bindConnectionService(injector, modules, channel);
}
export async function bindConnectionService(
injector: Injector,
modules: ModuleConstructor[],
connection: RPCMessageConnection,
) {
export async function bindConnectionService(injector: Injector, modules: ModuleConstructor[], channel: WSChannel) {
const clientCenter = new RPCServiceCenter();
clientCenter.setConnection(connection);
const dispose = clientCenter.setChannel(channel);
connection.onClose(() => {
clientCenter.removeConnection(connection);
const toRemove = channel.onClose(() => {
dispose.dispose();
toRemove();
});
const { getRPCService } = initRPCService(clientCenter);

View File

@ -1,6 +1,6 @@
import { NetSocketConnection } from '@opensumi/ide-connection/lib/common/connection';
import { IDisposable, isDefined } from '@opensumi/ide-core-common';
import { IElectronMainApi } from '@opensumi/ide-core-common/lib/electron';
import type { MessageConnection } from '@opensumi/vscode-jsonrpc';
declare const ElectronIpcRenderer: IElectronIpcRenderer;
@ -92,12 +92,18 @@ export function createElectronMainApi(name: string, enableCaptured?: boolean): I
);
}
export interface IElectronEnvMetadata {
windowClientId: string;
[key: string]: any;
}
export const electronEnv: {
currentWindowId: number;
currentWebContentsId: number;
ipcRenderer: IElectronIpcRenderer;
webviewPreload: string;
plainWebviewPreload: string;
metadata: IElectronEnvMetadata;
[key: string]: any;
} = (global as any) || {};
@ -107,19 +113,21 @@ if (typeof ElectronIpcRenderer !== 'undefined') {
export interface IElectronNativeDialogService {
showOpenDialog(options: Electron.OpenDialogOptions): Promise<string[] | undefined>;
showSaveDialog(options: Electron.SaveDialogOptions): Promise<string | undefined>;
}
export const IElectronNativeDialogService = Symbol('IElectronNativeDialogService');
export function createElectronClientConnection(connectPath?: string): MessageConnection {
export function createNetSocketConnection(connectPath?: string): NetSocketConnection {
let socket;
if (connectPath) {
socket = electronEnv.createNetConnection(connectPath);
} else {
socket = electronEnv.createRPCNetConnection();
}
const { createSocketConnection } = require('@opensumi/ide-connection/lib/node/connect');
return createSocketConnection(socket);
return new NetSocketConnection(socket);
}
export function fromWindowClientId(suffix: string) {
return `${suffix}-${electronEnv.metadata.windowClientId}`;
}

View File

@ -4,4 +4,4 @@ export class BrowserConnectionCloseEvent extends BasicEvent<void> {}
export class BrowserConnectionOpenEvent extends BasicEvent<void> {}
export class BrowserConnectionErrorEvent extends BasicEvent<Event> {}
export class BrowserConnectionErrorEvent extends BasicEvent<Error> {}

View File

@ -215,7 +215,8 @@ export class DebugLog implements IDebugLog {
}
private getPre(level: string, color: string) {
const text = this.namespace ? `[${this.namespace}:${level}]` : `[${level}]`;
let text = this.getColor('green', `[${new Date().toLocaleString('zh-CN')}] `);
text += this.namespace ? `[${this.namespace}:${level}]` : `[${level}]`;
return this.getColor(color, text);
}

View File

@ -150,29 +150,17 @@ export class ServerApp implements IServerApp {
) {
await this.initializeContribution();
let serviceCenter;
if (serviceHandler) {
serviceCenter = new RPCServiceCenter();
serviceHandler(serviceCenter);
serviceHandler(new RPCServiceCenter());
} else {
if (server instanceof http.Server || server instanceof https.Server) {
// 创建 websocket 通道
serviceCenter = createServerConnection2(
server,
this.injector,
this.modulesInstances,
this.webSocketHandler,
this.opts,
);
createServerConnection2(server, this.injector, this.modulesInstances, this.webSocketHandler, this.opts);
} else if (server instanceof net.Server) {
serviceCenter = createNetServerConnection(server, this.injector, this.modulesInstances);
createNetServerConnection(server, this.injector, this.modulesInstances);
}
}
// TODO: 每次链接来的时候绑定一次,或者是服务获取的时候多实例化出来
// bindModuleBackService(this.injector, this.modulesInstances, serviceCenter);
await this.startContribution();
}

View File

@ -1,16 +1,14 @@
import http from 'http';
import net from 'net';
import { Injector, InstanceCreator, ClassCreator, FactoryCreator } from '@opensumi/di';
import { WSChannel, initRPCService, RPCServiceCenter } from '@opensumi/ide-connection';
import { createWebSocketConnection } from '@opensumi/ide-connection/lib/common/message';
import { NetSocketConnection } from '@opensumi/ide-connection/lib/common/connection';
import {
WebSocketServerRoute,
WebSocketHandler,
CommonChannelHandler,
commonChannelPathHandler,
createSocketConnection,
} from '@opensumi/ide-connection/lib/node';
import { INodeLogger } from './logger/node-logger';
@ -19,10 +17,32 @@ import { IServerAppOpts } from './types';
export { RPCServiceCenter };
function handleClientChannel(
injector: Injector,
modulesInstances: NodeModule[],
channel: WSChannel,
clientId: string,
logger: INodeLogger,
) {
logger.log(`New RPC connection ${clientId}`);
const serviceCenter = new RPCServiceCenter(undefined, logger);
const serviceChildInjector = bindModuleBackService(injector, modulesInstances, serviceCenter, clientId);
const remove = serviceCenter.setChannel(channel);
channel.onClose(() => {
remove.dispose();
serviceChildInjector.disposeAll();
logger.log(`Remove RPC connection ${clientId}`);
});
}
export function createServerConnection2(
server: http.Server,
injector,
modulesInstances,
injector: Injector,
modulesInstances: NodeModule[],
handlerArr: WebSocketHandler[],
serverAppOpts: IServerAppOpts,
) {
@ -35,22 +55,8 @@ export function createServerConnection2(
// 事件由 connection 的时机来触发
commonChannelPathHandler.register('RPCService', {
handler: (connection: WSChannel, clientId: string) => {
logger.log(`New RPC connection ${clientId}`);
const serviceCenter = new RPCServiceCenter(undefined, logger);
const serviceChildInjector = bindModuleBackService(injector, modulesInstances, serviceCenter, clientId);
const serverConnection = createWebSocketConnection(connection);
connection.messageConnection = serverConnection;
serviceCenter.setConnection(serverConnection);
connection.onClose(() => {
serviceCenter.removeConnection(serverConnection);
serviceChildInjector.disposeAll();
logger.log(`Remove RPC connection ${clientId}`);
});
handler: (channel: WSChannel, clientId: string) => {
handleClientChannel(injector, modulesInstances, channel, clientId, logger);
},
dispose: () => {},
});
@ -64,27 +70,17 @@ export function createServerConnection2(
socketRoute.init();
}
export function createNetServerConnection(server: net.Server, injector, modulesInstances) {
const logger = injector.get(INodeLogger);
const serviceCenter = new RPCServiceCenter(undefined, logger);
const serviceChildInjector = bindModuleBackService(
injector,
modulesInstances,
serviceCenter,
process.env.CODE_WINDOW_CLIENT_ID as string,
);
export function createNetServerConnection(server: net.Server, injector: Injector, modulesInstances: NodeModule[]) {
const logger = injector.get(INodeLogger) as INodeLogger;
server.on('connection', (connection) => {
const serverConnection = createSocketConnection(connection);
serviceCenter.setConnection(serverConnection);
connection.on('close', () => {
serviceCenter.removeConnection(serverConnection);
serviceChildInjector.disposeAll();
server.on('connection', (socket) => {
logger.log('new connection', socket.remoteAddress, socket.remotePort);
const channel = WSChannel.forClient(new NetSocketConnection(socket), {
id: 'RPCService-' + process.env.CODE_WINDOW_CLIENT_ID!,
logger,
});
handleClientChannel(injector, modulesInstances, channel, process.env.CODE_WINDOW_CLIENT_ID!, logger);
});
return serviceCenter;
}
export function bindModuleBackService(

View File

@ -1,5 +1,6 @@
import { WSChannel } from '@opensumi/ide-connection';
import { WSChannelHandler } from '@opensumi/ide-connection/lib/browser/ws-channel-handler';
import { EmptyConnection } from '@opensumi/ide-connection/lib/common/connection/drivers/empty';
import { IFileServiceClient, IContextKeyService } from '@opensumi/ide-core-browser';
import { Disposable } from '@opensumi/ide-core-common';
import {
@ -101,10 +102,7 @@ describe('Debug console component Test Suites', () => {
useValue: {
clientId: 'mock_id' + Math.random(),
openChannel(id: string) {
const channelSend = (content) => {
//
};
return new WSChannel(channelSend, 'mock_wschannel' + id);
return new WSChannel(new EmptyConnection(), { id: 'mock_wschannel' + id });
},
},
});

View File

@ -1,22 +1,14 @@
import net from 'net';
import { RPCServiceCenter, initRPCService } from '@opensumi/ide-connection';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { createSocketConnection } from '@opensumi/ide-connection/lib/node';
import { argv } from '@opensumi/ide-core-common/lib/node/cli';
import { KT_PROCESS_SOCK_OPTION_KEY } from '../src/common';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { Emitter } from '@opensumi/ide-core-common';
export async function initMockRPCProtocol(client): Promise<RPCProtocol> {
const extCenter = new RPCServiceCenter();
const { getRPCService } = initRPCService(extCenter);
const extConnection = net.createConnection('/tmp/test.sock');
extCenter.setConnection(createSocketConnection(extConnection));
const service = getRPCService('ExtProtocol');
service.on('onMessage', (msg) => {
// console.log('service onmessage', msg);
console.log('service onmessage', msg);
});
const extProtocol = new RPCProtocol({
onMessage: client.onMessage,
@ -25,3 +17,24 @@ export async function initMockRPCProtocol(client): Promise<RPCProtocol> {
return extProtocol;
}
export function createMockPairRPCProtocol() {
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
return {
rpcProtocolExt,
rpcProtocolMain,
};
}

View File

@ -7,6 +7,7 @@ import { ICommentsService } from '@opensumi/ide-comments';
import { CommentsService } from '@opensumi/ide-comments/lib/browser/comments.service';
import { WSChannel } from '@opensumi/ide-connection';
import { WSChannelHandler } from '@opensumi/ide-connection/lib/browser/ws-channel-handler';
import { EmptyConnection } from '@opensumi/ide-connection/lib/common/connection/drivers/empty';
import {
IContextKeyService,
StorageProvider,
@ -540,10 +541,7 @@ export function setupExtensionServiceInjector() {
useValue: {
clientId: 'mock_id' + Math.random(),
openChannel() {
const channelSend = (content) => {
//
};
return new WSChannel(channelSend, 'mock_wschannel');
return new WSChannel(new EmptyConnection(), { id: 'mock_wschannel' });
},
},
},

View File

@ -1,7 +1,6 @@
import type vscode from 'vscode';
import { RPCProtocol } from '@opensumi/ide-connection';
import { Emitter, CancellationToken, MonacoService, DisposableCollection } from '@opensumi/ide-core-browser';
import { CancellationToken, MonacoService, DisposableCollection } from '@opensumi/ide-core-browser';
import { useMockStorage } from '@opensumi/ide-core-browser/__mocks__/storage';
import { URI, Uri, Position } from '@opensumi/ide-core-common';
import {
@ -29,6 +28,7 @@ import { createModel } from '@opensumi/monaco-editor-core/esm/vs/editor/standalo
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { mockService } from '../../../../tools/dev-tool/src/mock-injector';
import { MockedMonacoService } from '../../../monaco/__mocks__/monaco.service.mock';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { MainThreadCommands } from '../../src/browser/vscode/api/main.thread.commands';
import { MainThreadLanguages } from '../../src/browser/vscode/api/main.thread.language';
import { ExtHostAPIIdentifier, IExtensionDescription, MainThreadAPIIdentifier } from '../../src/common/vscode';
@ -39,20 +39,7 @@ import { ExtHostCommands } from '../../src/hosted/api/vscode/ext.host.command';
import { ExtHostLanguages } from '../../src/hosted/api/vscode/ext.host.language';
import { createToken } from '../../src/hosted/api/vscode/language/util';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
const defaultSelector = { scheme: 'far', language: 'a' };
const disposables: DisposableCollection = new DisposableCollection();

View File

@ -1,9 +1,9 @@
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { Emitter, CommandRegistry, CommandRegistryImpl } from '@opensumi/ide-core-common';
import { CommandRegistry, CommandRegistryImpl } from '@opensumi/ide-core-common';
import { MonacoCommandService } from '@opensumi/ide-editor/lib/browser/monaco-contrib/command/command.service';
import { ICommandServiceToken } from '@opensumi/ide-monaco/lib/browser/contrib/command';
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { MainThreadCommands } from '../../src/browser/vscode/api/main.thread.commands';
import { ExtHostAPIIdentifier, MainThreadAPIIdentifier } from '../../src/common/vscode';
import { ExtHostCommands } from '../../src/hosted/api/vscode/ext.host.command';
@ -23,21 +23,7 @@ describe('MainThreadCommandAPI Test Suites ', () => {
},
);
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
beforeAll((done) => {
extHostCommands = new ExtHostCommands(rpcProtocolExt);

View File

@ -1,5 +1,4 @@
import { RPCProtocol } from '@opensumi/ide-connection';
import { Emitter, IEventBus, URI, CancellationTokenSource } from '@opensumi/ide-core-common';
import { IEventBus, URI, CancellationTokenSource } from '@opensumi/ide-core-common';
import { ResourceDecorationNeedChangeEvent } from '@opensumi/ide-editor/lib/browser/types';
import { IEditorDocumentModelService } from '@opensumi/ide-editor/src/browser';
import { MainThreadWebview } from '@opensumi/ide-extension/lib/browser/vscode/api/main.thread.api.webview';
@ -18,21 +17,9 @@ import { IWebviewService } from '@opensumi/ide-webview/lib/browser/types';
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { mockService, MockInjector } from '../../../../tools/dev-tool/src/mock-injector';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
let extHost: IExtHostCustomEditor;
let mainThread: MainThreadCustomEditor;

View File

@ -1,6 +1,5 @@
import type vscode from 'vscode';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { Event, Uri, Emitter, DisposableCollection, CancellationToken } from '@opensumi/ide-core-common';
import { IDecorationsService } from '@opensumi/ide-decoration';
import { FileDecorationsService } from '@opensumi/ide-decoration/lib/browser/decorationsService';
@ -14,24 +13,11 @@ import { createWindowApiFactory } from '@opensumi/ide-extension/lib/hosted/api/v
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { mockExtensions } from '../../__mocks__/extensions';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { ExtHostDecorations } from '../../src/hosted/api/vscode/ext.host.decoration';
import ExtensionHostextWindowAPIImpl from '../../src/hosted/ext.host';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
describe('MainThreadDecorationAPI Test Suites ', () => {
const injector = createBrowserInjector([]);

View File

@ -2,7 +2,6 @@ import path from 'path';
import isEqual from 'lodash/isEqual';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { URI, IContextKeyService } from '@opensumi/ide-core-browser';
import { CorePreferences, MonacoOverrideServiceRegistry } from '@opensumi/ide-core-browser';
import { injectMockPreferences } from '@opensumi/ide-core-browser/__mocks__/preference';
@ -77,24 +76,13 @@ import {
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { TestEditorDocumentProvider, TestResourceResolver } from '../../../editor/__tests__/browser/test-providers';
import { MockContextKeyService } from '../../../monaco/__mocks__/monaco.context-key.service';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { MainThreadEditorService } from '../../src/browser/vscode/api/main.thread.editor';
import * as types from '../../src/common/vscode/ext-types';
import { ExtensionHostEditorService } from '../../src/hosted/api/vscode/editor/editor.host';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const preferences: Map<string, any> = new Map();
const emitter = new Emitter<IConfigurationChangeEvent>();

View File

@ -1,12 +1,6 @@
import { Injectable } from '@opensumi/di';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { AppConfig } from '@opensumi/ide-core-browser';
import {
Emitter,
LogServiceForClientPath,
LogLevel,
getLanguageId,
} from '@opensumi/ide-core-common';
import { LogServiceForClientPath, LogLevel, getLanguageId } from '@opensumi/ide-core-common';
import { MainThreadEnv } from '@opensumi/ide-extension/lib/browser/vscode/api/main.thread.env';
import { MainThreadStorage } from '@opensumi/ide-extension/lib/browser/vscode/api/main.thread.storage';
import {
@ -23,22 +17,10 @@ import ExtensionHostServiceImpl from '@opensumi/ide-extension/lib/hosted/ext.hos
import { IExtensionStorageService } from '@opensumi/ide-extension-storage';
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { MockExtensionStorageService } from '../hosted/__mocks__/extensionStorageService';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
@Injectable()
class MockLogServiceForClient {

View File

@ -1,5 +1,4 @@
import { Injectable, Injector } from '@opensumi/di';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IContextKeyService, AppConfig } from '@opensumi/ide-core-browser';
import { MockedStorageProvider } from '@opensumi/ide-core-browser/__mocks__/storage';
import { StaticResourceService } from '@opensumi/ide-core-browser/lib/static-resource';
@ -25,6 +24,7 @@ import { MockWorkbenchEditorService } from '../../../editor/src/common/mocks/wor
import { MockContextKeyService } from '../../../monaco/__mocks__/monaco.context-key.service';
import { MainThreadExtensionService } from '../../__mocks__/api/mainthread.extension.service';
import { MockExtNodeClientService } from '../../__mocks__/extension.service.client';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { MainThreadWebview } from '../../src/browser/vscode/api/main.thread.api.webview';
import { MainThreadExtensionLog } from '../../src/browser/vscode/api/main.thread.log';
import { MainThreadStorage } from '../../src/browser/vscode/api/main.thread.storage';
@ -56,22 +56,7 @@ class MockStaticResourceService {
}
resourceRoots: [] = [];
}
const emitterA = new ideCoreCommon.Emitter<any>();
const emitterB = new ideCoreCommon.Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
describe('MainThreadExtensions Test Suites', () => {
const extHostInjector = new Injector();

View File

@ -1,24 +1,15 @@
import { Injector } from '@opensumi/di';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { Emitter } from '@opensumi/ide-core-common';
import { ExtHostAPIIdentifier } from '@opensumi/ide-extension/lib/common/vscode';
import { OutputPreferences } from '@opensumi/ide-output/lib/browser/output-preference';
import { OutputService } from '@opensumi/ide-output/lib/browser/output.service';
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { MockOutputService } from '../../__mocks__/api/output.service';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import * as types from '../../src/common/vscode/ext-types';
import { ExtHostOutput } from '../../src/hosted/api/vscode/ext.host.output';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const { rpcProtocolExt } = createMockPairRPCProtocol();
describe('MainThreadOutput Test Suites', () => {
const injector = createBrowserInjector(

View File

@ -1,5 +1,4 @@
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { Emitter, CommandRegistry, CommandRegistryImpl } from '@opensumi/ide-core-common';
import { CommandRegistry, CommandRegistryImpl } from '@opensumi/ide-core-common';
import { MainThreadStatusBar } from '@opensumi/ide-extension/lib/browser/vscode/api/main.thread.statusbar';
import { ExtHostAPIIdentifier, MainThreadAPIIdentifier } from '@opensumi/ide-extension/lib/common/vscode';
import { StatusBarAlignment } from '@opensumi/ide-extension/lib/common/vscode/ext-types';
@ -9,20 +8,9 @@ import { StatusBarService } from '@opensumi/ide-status-bar/lib/browser/status-ba
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { mockExtensionDescription } from '../../__mocks__/extensions';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
describe('MainThreadStatusBar API Test Suites', () => {
const injector = createBrowserInjector([]);

View File

@ -1,7 +1,6 @@
import path from 'path';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { Emitter, FileUri, ITaskDefinitionRegistry, TaskDefinitionRegistryImpl } from '@opensumi/ide-core-common';
import { FileUri, ITaskDefinitionRegistry, TaskDefinitionRegistryImpl } from '@opensumi/ide-core-common';
import { addEditorProviders } from '@opensumi/ide-dev-tool/src/injector-editor';
import { ExtensionService } from '@opensumi/ide-extension';
import { ExtensionServiceImpl } from '@opensumi/ide-extension/lib/browser/extension.service';
@ -33,6 +32,7 @@ import { MockWorkspaceService } from '@opensumi/ide-workspace/lib/common/mocks';
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { MockedMonacoService } from '../../../monaco/__mocks__/monaco.service.mock';
import { mockExtensions } from '../../__mocks__/extensions';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { MockExtensionStorageService } from '../hosted/__mocks__/extensionStorageService';
const extension = Object.assign({}, mockExtensions[0], {
@ -64,20 +64,7 @@ class TestTaskProvider {
}
}
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
describe('MainThreadTask Test Suite', () => {
const injector = createBrowserInjector([VariableModule]);

View File

@ -1,4 +1,3 @@
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IOpenerService } from '@opensumi/ide-core-browser/lib/opener';
import { StaticResourceService } from '@opensumi/ide-core-browser/lib/static-resource/static.definition';
import {
@ -27,6 +26,7 @@ import { IWebviewService, IWebview } from '@opensumi/ide-webview';
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { mockService } from '../../../../tools/dev-tool/src/mock-injector';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { IExtHostWebview, ExtHostAPIIdentifier, MainThreadAPIIdentifier } from '../../lib/common/vscode';
async function delay(ms: number) {
@ -104,21 +104,7 @@ describe('Webview view tests ', () => {
},
);
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
beforeAll((done) => {
extHostWebview = new ExtHostWebviewService(rpcProtocolExt);

View File

@ -6,7 +6,6 @@ import util from 'util';
import temp = require('temp');
import vscode from 'vscode';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import {
PreferenceProviderProvider,
PreferenceProvider,
@ -95,26 +94,14 @@ import { WorkspaceFileService } from '@opensumi/ide-workspace-edit/lib/browser/w
import { createBrowserInjector } from '../../../../tools/dev-tool/src/injector-helper';
import { mockService } from '../../../../tools/dev-tool/src/mock-injector';
import { mockExtensions } from '../../__mocks__/extensions';
import { createMockPairRPCProtocol } from '../../__mocks__/initRPCProtocol';
import { MainThreadFileSystemEvent } from '../../lib/browser/vscode/api/main.thread.file-system-event';
import { MainThreadWebview } from '../../src/browser/vscode/api/main.thread.api.webview';
import { MainThreadWorkspace } from '../../src/browser/vscode/api/main.thread.workspace';
import { ExtHostFileSystemInfo } from '../../src/hosted/api/vscode/ext.host.file-system-info';
import { ExtHostWorkspace, createWorkspaceApiFactory } from '../../src/hosted/api/vscode/ext.host.workspace';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
function getFileStatType(stat: fs.Stats) {
if (stat.isDirectory()) {

View File

@ -1,4 +1,4 @@
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { mockService } from '../../../../../../tools/dev-tool/src/mock-injector';
import { mockExtensions } from '../../../../__mocks__/extensions';

View File

@ -1,4 +1,4 @@
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { Deferred, Emitter } from '@opensumi/ide-core-common';
import { ExtHostCommon } from '@opensumi/ide-extension/lib/hosted/api/sumi/ext.host.common';
import {

View File

@ -1,4 +1,4 @@
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { IWindowInfo } from '@opensumi/ide-extension/lib/common/sumi/window';
import { ExtHostIDEWindow, ExtIDEWebviewWindow } from '@opensumi/ide-extension/lib/hosted/api/sumi/ext.host.window';
import { createWindowApiFactory } from '@opensumi/ide-extension/lib/hosted/api/sumi/ext.host.window';
@ -8,7 +8,6 @@ import { MainThreadSumiAPIIdentifier } from '../../../../src/common/sumi';
import { MainThreadAPIIdentifier } from '../../../../src/common/vscode';
import { ExtHostCommands } from '../../../../src/hosted/api/vscode/ext.host.command';
const mockMainThreadIDEWindowProxy = {
$createWebviewWindow: jest.fn(async () => {
const info: IWindowInfo = {

View File

@ -1,7 +1,6 @@
import type vscode from 'vscode';
import { Injector } from '@opensumi/di';
import { RPCProtocol } from '@opensumi/ide-connection';
import { MockedStorageProvider } from '@opensumi/ide-core-browser/__mocks__/storage';
import { IMenuRegistry, MenuId, IMenuItem } from '@opensumi/ide-core-browser/src/menu/next';
import { Emitter, StorageProvider, IAuthenticationService, CommandRegistry } from '@opensumi/ide-core-common';
@ -22,6 +21,7 @@ import { QuickPickService } from '@opensumi/ide-quick-open';
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';
import { mockService } from '../../../../../../tools/dev-tool/src/mock-injector';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
describe('extension/__tests__/hosted/api/vscode/ext.host.authentication.test.ts', () => {
let injector: Injector;
@ -30,20 +30,10 @@ describe('extension/__tests__/hosted/api/vscode/ext.host.authentication.test.ts'
let mainThreadAuthentication: IMainThreadAuthentication;
let authenticationService: IAuthenticationService;
const extensionId = 'vscode.vim';
const emitterExt = new Emitter<any>();
const emitterMain = new Emitter<any>();
let authenticationProvider: vscode.AuthenticationProvider;
const mockClientExt = {
send: (msg) => emitterMain.fire(msg),
onMessage: emitterExt.event,
};
const mockClientMain = {
send: (msg) => emitterExt.fire(msg),
onMessage: emitterMain.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientExt);
const rpcProtocolMain = new RPCProtocol(mockClientMain);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
beforeEach(async () => {
injector = createBrowserInjector([]);

View File

@ -4,9 +4,8 @@ import { Injector } from '@opensumi/di';
import { ICommentsService, ICommentsFeatureRegistry, CommentReactionClick } from '@opensumi/ide-comments';
import { CommentsFeatureRegistry } from '@opensumi/ide-comments/lib/browser/comments-feature.registry';
import { CommentsService } from '@opensumi/ide-comments/lib/browser/comments.service';
import { RPCProtocol } from '@opensumi/ide-connection';
import { IContextKeyService } from '@opensumi/ide-core-browser';
import { Uri, Emitter, Disposable, IEventBus, URI, Deferred } from '@opensumi/ide-core-common';
import { Uri, Disposable, IEventBus, URI, Deferred } from '@opensumi/ide-core-common';
import { WorkbenchEditorService } from '@opensumi/ide-editor';
import { WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service';
import { MainthreadComments } from '@opensumi/ide-extension/lib/browser/vscode/api/main.thread.comments';
@ -27,6 +26,7 @@ import { LayoutService } from '@opensumi/ide-main-layout/lib/browser/layout.serv
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';
import { mockService } from '../../../../../../tools/dev-tool/src/mock-injector';
import { MockContextKeyService } from '../../../../../monaco/__mocks__/monaco.context-key.service';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@ -37,18 +37,7 @@ describe('extension/__tests__/hosted/api/vscode/ext.host.comments.test.ts', () =
let vscodeComments: typeof vscode.comments;
let extComments: ExtHostComments;
let mainThreadComments: IMainThreadComments;
const emitterExt = new Emitter<any>();
const emitterMain = new Emitter<any>();
const mockClientExt = {
send: (msg) => emitterMain.fire(msg),
onMessage: emitterExt.event,
};
const mockClientMain = {
send: (msg) => emitterExt.fire(msg),
onMessage: emitterMain.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientExt);
const rpcProtocolMain = new RPCProtocol(mockClientMain);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
beforeEach(() => {
injector = createBrowserInjector([]);

View File

@ -1,4 +1,3 @@
import { RPCProtocol } from '@opensumi/ide-connection';
import { Emitter, CancellationTokenSource, Uri } from '@opensumi/ide-core-common';
import { URI } from '@opensumi/ide-core-common';
import {
@ -19,22 +18,9 @@ import { ExtHostWebviewService } from '@opensumi/ide-extension/lib/hosted/api/vs
import { ExtHostCustomEditorImpl } from '@opensumi/ide-extension/lib/hosted/api/vscode/ext.host.custom-editor';
import { mockService } from '../../../../../../tools/dev-tool/src/mock-injector';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
let extHost: ExtHostCustomEditorImpl;
let mainThread: IMainThreadCustomEditor;

View File

@ -1,6 +1,6 @@
import path from 'path';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { Deferred, URI } from '@opensumi/ide-core-common';
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';

View File

@ -1,8 +1,7 @@
import type vscode from 'vscode';
import { RPCProtocol } from '@opensumi/ide-connection';
import { WSChannelHandler } from '@opensumi/ide-connection/lib/browser/ws-channel-handler';
import { Emitter, Uri, uuid } from '@opensumi/ide-core-common';
import { Uri, uuid } from '@opensumi/ide-core-common';
import { MainThreadEnv } from '@opensumi/ide-extension/lib/browser/vscode/api/main.thread.env';
import { MainThreadAPIIdentifier, ExtHostAPIIdentifier } from '@opensumi/ide-extension/lib/common/vscode';
import { UIKind } from '@opensumi/ide-extension/lib/common/vscode/ext-types';
@ -11,21 +10,9 @@ import { ExtHostEnv } from '@opensumi/ide-extension/lib/hosted/api/vscode/env/ex
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';
import { mockService } from '../../../../../../tools/dev-tool/src/mock-injector';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
let extHost: ExtHostEnv;
let mainThread: MainThreadEnv;

View File

@ -3,7 +3,7 @@ import path from 'path';
import { URI as Uri } from 'vscode-uri';
import { Injector } from '@opensumi/di';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { IExtensionProps, isWindows, URI } from '@opensumi/ide-core-common';
import { initMockRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
@ -36,7 +36,7 @@ const mockExtension = {
defaultPkgNlsJSON: {},
};
describe(`test ${__filename}`, () => {
describe('test ext host extension', () => {
let rpcProtocol: RPCProtocol;
let context: ExtensionContext;
let extHostStorage: ExtHostStorage;

View File

@ -1,5 +1,4 @@
import { RPCProtocol } from '@opensumi/ide-connection';
import { Emitter, Disposable } from '@opensumi/ide-core-common';
import { Disposable } from '@opensumi/ide-core-common';
import { IQuickInputService, QuickOpenService, QuickPickService } from '@opensumi/ide-quick-open';
import { QuickInputService } from '@opensumi/ide-quick-open/lib/browser/quick-input-service';
import { QuickTitleBar } from '@opensumi/ide-quick-open/lib/browser/quick-title-bar';
@ -8,6 +7,7 @@ import { IIconService, IThemeService } from '@opensumi/ide-theme/lib/common/them
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';
import { mockService } from '../../../../../../tools/dev-tool/src/mock-injector';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
import { MainThreadQuickOpen } from '../../../../src/browser/vscode/api/main.thread.quickopen';
import { MainThreadAPIIdentifier, ExtHostAPIIdentifier } from '../../../../src/common/vscode';
import { InputBoxValidationSeverity, QuickPickItemKind } from '../../../../src/common/vscode/ext-types';
@ -15,20 +15,7 @@ import { ExtHostQuickOpen } from '../../../../src/hosted/api/vscode/ext.host.qui
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
let extHost: ExtHostQuickOpen;
let mainThread: MainThreadQuickOpen;

View File

@ -1,4 +1,4 @@
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';
import { MainThreadAPIIdentifier } from '../../../../src/common/vscode';

View File

@ -1,32 +1,19 @@
import { RPCProtocol } from '@opensumi/ide-connection';
import { WSChannelHandler } from '@opensumi/ide-connection/lib/browser/ws-channel-handler';
import { IContextKeyService, IStatusBarService } from '@opensumi/ide-core-browser';
import { MockContextKeyService } from '@opensumi/ide-core-browser/__mocks__/context-key';
import { Emitter, uuid } from '@opensumi/ide-core-common';
import { uuid } from '@opensumi/ide-core-common';
import { StatusBarService } from '@opensumi/ide-status-bar/lib/browser/status-bar.service';
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';
import { mockService } from '../../../../../../tools/dev-tool/src/mock-injector';
import { mockExtensionDescription } from '../../../../__mocks__/extensions';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
import { MainThreadStatusBar } from '../../../../src/browser/vscode/api/main.thread.statusbar';
import { MainThreadAPIIdentifier, ExtHostAPIIdentifier } from '../../../../src/common/vscode';
import { ThemeColor } from '../../../../src/common/vscode/ext-types';
import { ExtHostStatusBar } from '../../../../src/hosted/api/vscode/ext.host.statusbar';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
let extHost: ExtHostStatusBar;
let mainThread: MainThreadStatusBar;

View File

@ -1,4 +1,4 @@
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { URI, StoragePaths } from '@opensumi/ide-core-common';
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';

View File

@ -1,6 +1,5 @@
import path from 'path';
import { RPCProtocol } from '@opensumi/ide-connection';
import { WSChannelHandler } from '@opensumi/ide-connection/lib/browser';
import { MockedStorageProvider } from '@opensumi/ide-core-browser/__mocks__/storage';
import {
@ -33,9 +32,7 @@ import {
ITerminalService,
ITerminalTheme,
} from '@opensumi/ide-terminal-next';
import {
createTerminalClientFactory2,
} from '@opensumi/ide-terminal-next/lib/browser/terminal.client';
import { createTerminalClientFactory2 } from '@opensumi/ide-terminal-next/lib/browser/terminal.client';
import { TerminalController } from '@opensumi/ide-terminal-next/lib/browser/terminal.controller';
import { TerminalEnvironmentService } from '@opensumi/ide-terminal-next/lib/browser/terminal.environment.service';
import { TerminalInternalService } from '@opensumi/ide-terminal-next/lib/browser/terminal.internal.service';
@ -51,11 +48,12 @@ import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/inje
import { mockService } from '../../../../../../tools/dev-tool/src/mock-injector';
import {
MockMainLayoutService,
MockSocketService,
MockTerminalService,
MockTerminalProfileInternalService,
MockTerminalThemeService,
} from '../../../../../terminal-next/__tests__/browser/mock.service';
import { mockExtensionProps } from '../../../../__mocks__/extensions';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
import { MainthreadTasks } from '../../../../src/browser/vscode/api/main.thread.tasks';
import { MainThreadTerminal } from '../../../../src/browser/vscode/api/main.thread.terminal';
import { MainThreadAPIIdentifier, ExtHostAPIIdentifier } from '../../../../src/common/vscode';
@ -67,20 +65,7 @@ import { CustomBuildTaskProvider } from './__mock__/taskProvider';
const extension = mockExtensionProps;
const emitterA = new EventEmitter<any>();
const emitterB = new EventEmitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
let extHostTask: ExtHostTasks;
let extHostTerminal: ExtHostTerminal;
@ -122,7 +107,7 @@ describe('ExtHostTask API', () => {
},
{
token: ITerminalService,
useValue: new MockSocketService(),
useValue: new MockTerminalService(),
},
{
token: ITerminalInternalService,

View File

@ -1,4 +1,3 @@
import { RPCProtocol } from '@opensumi/ide-connection';
import { PreferenceService } from '@opensumi/ide-core-browser';
import { Emitter, Disposable, ILogger, OperatingSystem, Deferred } from '@opensumi/ide-core-common';
import { IExtension } from '@opensumi/ide-extension';
@ -19,6 +18,7 @@ import {
MockProfileService,
MockTerminalProfileInternalService,
} from '../../../../../terminal-next/__tests__/browser/mock.service';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
import { MainThreadTerminal } from '../../../../src/browser/vscode/api/main.thread.terminal';
import { MainThreadAPIIdentifier, ExtHostAPIIdentifier } from '../../../../src/common/vscode';
import {
@ -28,20 +28,7 @@ import {
} from '../../../../src/hosted/api/vscode/ext.host.terminal';
import { MockEnvironmentVariableService } from '../../__mocks__/environmentVariableService';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
let extHost: ExtHostTerminal;
let mainThread: MainThreadTerminal;

View File

@ -1,4 +1,3 @@
import { RPCProtocol } from '@opensumi/ide-connection';
import { Emitter } from '@opensumi/ide-core-common';
import { MainThreadTheming } from '@opensumi/ide-extension/lib/browser/vscode/api/main.thread.theming';
import { MainThreadAPIIdentifier, ExtHostAPIIdentifier } from '@opensumi/ide-extension/lib/common/vscode';
@ -7,21 +6,9 @@ import { ExtHostTheming } from '@opensumi/ide-extension/lib/hosted/api/vscode/ex
import { IThemeService, ThemeType } from '@opensumi/ide-theme';
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';
import { createMockPairRPCProtocol } from '../../../../__mocks__/initRPCProtocol';
const emitterA = new Emitter<any>();
const emitterB = new Emitter<any>();
const mockClientA = {
send: (msg) => emitterB.fire(msg),
onMessage: emitterA.event,
};
const mockClientB = {
send: (msg) => emitterA.fire(msg),
onMessage: emitterB.event,
};
const rpcProtocolExt = new RPCProtocol(mockClientA);
const rpcProtocolMain = new RPCProtocol(mockClientB);
const { rpcProtocolExt, rpcProtocolMain } = createMockPairRPCProtocol();
let extHost: ExtHostTheming;
let mainThread: MainThreadTheming;

View File

@ -1,11 +1,5 @@
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import {
Emitter,
Disposable,
CancellationTokenSource,
uuid,
BinaryBuffer,
} from '@opensumi/ide-core-common';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { Emitter, Disposable, CancellationTokenSource, uuid, BinaryBuffer } from '@opensumi/ide-core-common';
import { ExtHostTreeViews } from '@opensumi/ide-extension/lib/hosted/api/vscode/ext.host.treeview';
import { createBrowserInjector } from '../../../../../../tools/dev-tool/src/injector-helper';

View File

@ -1,6 +1,6 @@
import { Injector } from '@opensumi/di';
import { ProxyIdentifier } from '@opensumi/ide-connection';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { RPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { Deferred, DefaultReporter, IReporter } from '@opensumi/ide-core-common';
import { MainThreadExtensionLog } from '../../__mocks__/api/mainthread.extension.log';

View File

@ -9,6 +9,7 @@ import { extensionHostManagerTester } from './extension.host.manager.common-test
const PROXY_PORT = 10297;
let extHostProxy: ExtHostProxy;
// KTLOG_SHOW_DEBUG=1 yarn jest packages/extension/__tests__/node/extension.host.proxy.manager.test.ts --detectOpenHandles
extensionHostManagerTester({
providers: [
{
@ -36,5 +37,6 @@ extensionHostManagerTester({
}),
dispose: () => {
extHostProxy.dispose();
extHostProxy = null as any;
},
});

View File

@ -1,24 +1,18 @@
import { Autowired, Injectable, Injector, INJECTOR_TOKEN } from '@opensumi/di';
import {
initRPCService,
IRPCProtocol,
RPCProtocol,
RPCServiceCenter,
createWebSocketConnection,
} from '@opensumi/ide-connection';
import { createRPCProtocol, IRPCProtocol, WSChannel } from '@opensumi/ide-connection';
import { WSChannelHandler as IWSChannelHandler } from '@opensumi/ide-connection/lib/browser';
import {
AppConfig,
Deferred,
electronEnv,
Emitter,
IExtensionProps,
ILogger,
IDisposable,
toDisposable,
createElectronClientConnection,
IApplicationService,
fromWindowClientId,
} from '@opensumi/ide-core-browser';
import { createNetSocketConnection } from '@opensumi/ide-core-browser';
import {
CONNECTION_HANDLE_BETWEEN_EXTENSION_AND_MAIN_THREAD,
@ -154,7 +148,7 @@ export class NodeExtProcessService implements AbstractNodeExtProcessService<IExt
}
private async initExtProtocol() {
const mainThreadCenter = new RPCServiceCenter();
let channel: WSChannel;
// Electron 环境下,未指定 isRemote 时默认使用本地连接
// 否则使用 WebSocket 连接
@ -163,33 +157,18 @@ export class NodeExtProcessService implements AbstractNodeExtProcessService<IExt
electronEnv.metadata.windowClientId,
);
this.logger.verbose('electron initExtProtocol connectPath', connectPath);
// electron 环境下要使用 Node 端的 connection
mainThreadCenter.setConnection(createElectronClientConnection(connectPath));
const connection = createNetSocketConnection(connectPath);
channel = WSChannel.forClient(connection, {
id: fromWindowClientId('NodeExtProcessService'),
});
} else {
const WSChannelHandler = this.injector.get(IWSChannelHandler);
const channel = await WSChannelHandler.openChannel(CONNECTION_HANDLE_BETWEEN_EXTENSION_AND_MAIN_THREAD);
mainThreadCenter.setConnection(createWebSocketConnection(channel));
channel = await WSChannelHandler.openChannel(CONNECTION_HANDLE_BETWEEN_EXTENSION_AND_MAIN_THREAD);
}
const { getRPCService } = initRPCService<{
onMessage: (msg: string) => void;
}>(mainThreadCenter);
const service = getRPCService('ExtProtocol');
const onMessageEmitter = new Emitter<string>();
service.on('onMessage', (msg) => {
onMessageEmitter.fire(msg);
});
const onMessage = onMessageEmitter.event;
const send = service.onMessage;
const mainThreadProtocol = new RPCProtocol({
onMessage,
send,
const mainThreadProtocol = createRPCProtocol(channel, {
timeout: this.appConfig.rpcMessageTimeout,
});
// 重启/重连时直接覆盖前一个连接
return mainThreadProtocol;
}

View File

@ -1,6 +1,6 @@
import { Injectable, Autowired, INJECTOR_TOKEN, Injector } from '@opensumi/di';
import { warning } from '@opensumi/ide-components/lib/utils';
import { IRPCProtocol, RPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol, RPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { AppConfig, Deferred, Emitter, IExtensionProps, ILogger, URI } from '@opensumi/ide-core-browser';
import { Disposable, IDisposable, toDisposable, path } from '@opensumi/ide-core-common';
@ -56,7 +56,6 @@ export class WorkerExtProcessService
if (this.protocol) {
this.ready.resolve();
this.logger.log('[Worker Host] init worker thread api proxy');
this.logger.verbose(this.protocol);
this.apiFactoryDisposable.push(
toDisposable(await initWorkerThreadAPIProxy(this.protocol, this.injector, this)),
toDisposable(createSumiApiFactory(this.protocol, this.injector)),

View File

@ -1,4 +1,4 @@
import { ProxyIdentifier } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { ProxyIdentifier } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { IDisposable, Uri, path } from '@opensumi/ide-core-common';
import { EditorComponentRenderMode } from '@opensumi/ide-editor/lib/browser';
import { ToolBarPosition } from '@opensumi/ide-toolbar/lib/browser';

View File

@ -1,4 +1,4 @@
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { Deferred } from '@opensumi/ide-core-common';
import { ActivatedExtensionJSON } from './activator';

View File

@ -1,5 +1,5 @@
import { Injectable, Injector, Autowired } from '@opensumi/di';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { Disposable, IDisposable, ILogger } from '@opensumi/ide-core-common';
import { IExtension } from '..';

View File

@ -1,6 +1,6 @@
import type vscode from 'vscode';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { Schemes } from '@opensumi/ide-core-common';
import { MainThreadAPIIdentifier, IMainThreadEnv, IExtHostEnv } from '../../../../common/vscode';

View File

@ -19,7 +19,7 @@ import type {
TestRunResult,
} from 'vscode';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/rpcProtocol';
import { IRPCProtocol } from '@opensumi/ide-connection/lib/common/ext-rpc-protocol';
import { getDebugLogger } from '@opensumi/ide-core-common';
import {
CancellationToken,

View File

@ -1,8 +1,7 @@
import type { ForkOptions } from 'child_process';
import net from 'net';
import { RPCService, RPCServiceCenter, getRPCService, IRPCProtocol, RPCProtocol } from '@opensumi/ide-connection';
import { createSocketConnection } from '@opensumi/ide-connection/lib/node';
import { RPCService, IRPCProtocol, WSChannel, createRPCProtocol } from '@opensumi/ide-connection';
import { Emitter, Disposable, IDisposable, getDebugLogger } from '@opensumi/ide-core-node';
import { IExtensionHostManager } from '../common';
@ -10,7 +9,6 @@ import {
IExtHostProxyRPCService,
IExtHostProxy,
IExtHostProxyOptions,
EXT_HOST_PROXY_PROTOCOL,
EXT_HOST_PROXY_IDENTIFIER,
IExtServerProxyRPCService,
EXT_SERVER_IDENTIFIER,
@ -21,6 +19,8 @@ import { ExtensionHostManager } from '../node/extension.host.manager';
class ExtHostProxyRPCService extends RPCService implements IExtHostProxyRPCService {
private extensionHostManager: IExtensionHostManager;
LOG_TAG = '[ExtHostProxyRPCService]';
constructor(private extServerProxy: IExtServerProxyRPCService) {
super();
this.extensionHostManager = new ExtensionHostManager();
@ -83,8 +83,6 @@ class ExtHostProxyRPCService extends RPCService implements IExtHostProxyRPCServi
export class ExtHostProxy extends Disposable implements IExtHostProxy {
private socket: net.Socket;
private readonly clientCenter: RPCServiceCenter;
private protocol: IRPCProtocol;
private options: IExtHostProxyOptions;
@ -95,12 +93,16 @@ export class ExtHostProxy extends Disposable implements IExtHostProxy {
private previouslyDisposer: IDisposable;
private connectedEmitter = new Emitter<void>();
private connectedEmitter = this.registerDispose(new Emitter<void>());
private readonly debug = getDebugLogger();
private readonly logger = getDebugLogger();
public readonly onConnected = this.connectedEmitter.event;
LOG_TAG = '[ExtHostProxy]';
channel: WSChannel;
disposer: Disposable;
constructor(options?: IExtHostProxyOptions) {
super();
this.options = {
@ -110,7 +112,6 @@ export class ExtHostProxy extends Disposable implements IExtHostProxy {
},
...options,
};
this.clientCenter = new RPCServiceCenter();
}
init() {
@ -121,33 +122,25 @@ export class ExtHostProxy extends Disposable implements IExtHostProxy {
if (this.previouslyDisposer) {
this.previouslyDisposer.dispose();
}
const disposer = new Disposable();
// 每次断连后重新生成 Socket 实例,否则会触发两次 close
this.disposer = new Disposable();
// 每次断连后重新生成 Socket 实例
this.socket = new net.Socket();
disposer.addDispose(this.bindEvent());
disposer.addDispose(this.connect());
this.previouslyDisposer = disposer;
this.disposer.addDispose(this.bindEvent());
this.disposer.addDispose(this.connect());
this.previouslyDisposer = this.disposer;
this.addDispose(this.previouslyDisposer);
}
private setRPCMethods() {
const proxyService = getRPCService(EXT_HOST_PROXY_PROTOCOL, this.clientCenter);
const onMessageEmitter = new Emitter<string>();
proxyService.on('onMessage', (msg: string) => {
onMessageEmitter.fire(msg);
});
const onMessage = onMessageEmitter.event;
const send = proxyService.onMessage;
this.protocol = new RPCProtocol({
onMessage,
send,
this.protocol = createRPCProtocol(this.channel, {
timeout: this.options.rpcMessageTimeout,
});
this.extServerProxy = this.protocol.getProxy(EXT_SERVER_IDENTIFIER);
const extHostProxyRPCService = new ExtHostProxyRPCService(this.extServerProxy);
this.protocol.set(EXT_HOST_PROXY_IDENTIFIER, extHostProxyRPCService);
this.addDispose({
this.disposer.addDispose({
dispose: () => extHostProxyRPCService.$dispose(),
});
}
@ -155,13 +148,13 @@ export class ExtHostProxy extends Disposable implements IExtHostProxy {
private reconnectOnEvent = () => {
global.clearTimeout(this.reconnectingTimer);
this.reconnectingTimer = global.setTimeout(() => {
this.debug.warn('reconnecting ext host server');
this.logger.warn(this.LOG_TAG, 'reconnecting ext host server');
this.createSocket();
}, this.options.retryTime!);
};
private connectOnEvent = () => {
this.debug.info('connect success');
this.logger.info(this.LOG_TAG, 'connect success');
// this.previouslyConnected = true;
global.clearTimeout(this.reconnectingTimer);
this.setConnection();
@ -192,12 +185,12 @@ export class ExtHostProxy extends Disposable implements IExtHostProxy {
}
private setConnection() {
const connection = createSocketConnection(this.socket);
this.clientCenter.setConnection(connection);
this.socket.once('close', () => {
connection.dispose();
this.clientCenter.removeConnection(connection);
this.channel = WSChannel.forNetSocket(this.socket, {
id: 'EXT_HOST_PROXY',
logger: this.logger,
});
this.disposer.addDispose(this.channel);
}
private connect = (): IDisposable => {

View File

@ -3,8 +3,7 @@ import { performance } from 'perf_hooks';
import Stream from 'stream';
import { ConstructorOf, Injector } from '@opensumi/di';
import { RPCProtocol, initRPCService, RPCServiceCenter } from '@opensumi/ide-connection';
import { createSocketConnection } from '@opensumi/ide-connection/lib/node';
import { WSChannel, createRPCProtocol } from '@opensumi/ide-connection';
import {
Emitter,
ReporterProcessMessage,
@ -78,35 +77,22 @@ export interface ExtProcessConfig {
}
async function initRPCProtocol(extInjector: Injector): Promise<any> {
const extCenter = new RPCServiceCenter();
const { getRPCService } = initRPCService<{
onMessage(msg: string): void;
}>(extCenter);
const extConnection = argv[KT_PROCESS_SOCK_OPTION_KEY];
const extConnection = net.createConnection(JSON.parse(argv[KT_PROCESS_SOCK_OPTION_KEY] || '{}'));
logger = new ExtensionLogger2(extInjector);
logger.log('init rpc protocol for ext connection path', extConnection);
extCenter.setConnection(createSocketConnection(extConnection));
const service = getRPCService('ExtProtocol');
const onMessageEmitter = new Emitter<string>();
service.on('onMessage', (msg: string) => {
onMessageEmitter.fire(msg);
const socket = net.createConnection(JSON.parse(extConnection));
const channel = WSChannel.forNetSocket(socket, {
id: 'ExtProcessBaseRPCProtocol',
});
const onMessage = onMessageEmitter.event;
const send = service.onMessage;
const appConfig = extInjector.get(AppConfig);
const extProtocol = new RPCProtocol({
onMessage,
send,
const extProtocol = createRPCProtocol(channel, {
timeout: appConfig.rpcMessageTimeout,
});
logger = new ExtensionLogger2(extInjector);
logger.log('process extConnection path', argv[KT_PROCESS_SOCK_OPTION_KEY]);
return { extProtocol, logger };
}

View File

@ -1,15 +1,14 @@
import net from 'net';
import { Injectable, Optional, Autowired } from '@opensumi/di';
import { getRPCService, RPCProtocol, IRPCProtocol } from '@opensumi/ide-connection';
import { createSocketConnection } from '@opensumi/ide-connection/lib/node';
import { MaybePromise, Emitter, IDisposable, toDisposable, Disposable } from '@opensumi/ide-core-common';
import { RPCServiceCenter, INodeLogger, AppConfig } from '@opensumi/ide-core-node';
import { IRPCProtocol, WSChannel, createRPCProtocol } from '@opensumi/ide-connection';
import { NetSocketConnection } from '@opensumi/ide-connection/lib/common/connection';
import { MaybePromise, IDisposable, toDisposable, Disposable } from '@opensumi/ide-core-common';
import { INodeLogger, AppConfig } from '@opensumi/ide-core-node';
import {
IExtensionHostManager,
Output,
EXT_HOST_PROXY_PROTOCOL,
EXT_SERVER_IDENTIFIER,
IExtHostProxyRPCService,
EXT_HOST_PROXY_IDENTIFIER,
@ -22,14 +21,12 @@ export class ExtensionHostProxyManager implements IExtensionHostManager {
private readonly logger: INodeLogger;
@Autowired(AppConfig)
private readonly appconfig: AppConfig;
private readonly appConfig: AppConfig;
private callId = 0;
private extHostProxyProtocol: IRPCProtocol;
private readonly extServiceProxyCenter = new RPCServiceCenter();
private extHostProxy: IExtHostProxyRPCService;
private callbackMap = new Map<number, (...args: any[]) => void>();
@ -38,6 +35,9 @@ export class ExtensionHostProxyManager implements IExtensionHostManager {
private disposer = new Disposable();
LOG_TAG = '[ExtensionHostProxyManager]';
channel: WSChannel;
constructor(
@Optional()
private listenOptions: net.ListenOptions = {
@ -47,23 +47,27 @@ export class ExtensionHostProxyManager implements IExtensionHostManager {
async init() {
await this.startProxyServer();
this.setExtHostProxyRPCProtocol();
}
private startProxyServer() {
return new Promise<net.Socket | void>((resolve) => {
return new Promise<void>((resolve) => {
const server = net.createServer();
this.disposer.addDispose(
toDisposable(() => {
this.logger.warn('dispose server');
server.close();
this.logger.warn(this.LOG_TAG, 'dispose server');
server.close((err) => {
if (err) {
this.logger.error(this.LOG_TAG, 'close server error', err);
}
});
}),
);
this.logger.log('waiting ext-proxy connecting...');
server.on('connection', (connection) => {
this.logger.log('there are new connections coming in');
this.logger.log(this.LOG_TAG, 'waiting ext-proxy connecting...');
server.on('connection', (socket) => {
this.logger.log(this.LOG_TAG, 'there are new connections coming in');
// 有新的连接时重新设置 RPCProtocol
this.setProxyConnection(connection);
this.setProxyConnection(socket);
this.setExtHostProxyRPCProtocol();
resolve();
});
@ -72,11 +76,11 @@ export class ExtensionHostProxyManager implements IExtensionHostManager {
}
private setProxyConnection(connection: net.Socket) {
const serverConnection = createSocketConnection(connection);
this.extServiceProxyCenter.setConnection(serverConnection);
connection.on('close', () => {
this.extServiceProxyCenter.removeConnection(serverConnection);
this.channel = WSChannel.forClient(new NetSocketConnection(connection), {
id: 'EXT_HOST_PROXY',
logger: this.logger,
});
this.disposer.addDispose(
toDisposable(() => {
if (!connection.destroyed) {
@ -87,19 +91,8 @@ export class ExtensionHostProxyManager implements IExtensionHostManager {
}
private setExtHostProxyRPCProtocol() {
const proxyService = getRPCService(EXT_HOST_PROXY_PROTOCOL, this.extServiceProxyCenter);
const onMessageEmitter = new Emitter<string>();
proxyService.on('onMessage', (msg) => {
onMessageEmitter.fire(msg);
});
const onMessage = onMessageEmitter.event;
const send = proxyService.onMessage;
this.extHostProxyProtocol = new RPCProtocol({
onMessage,
send,
timeout: this.appconfig.rpcMessageTimeout,
this.extHostProxyProtocol = createRPCProtocol(this.channel, {
timeout: this.appConfig.rpcMessageTimeout,
});
this.extHostProxyProtocol.set(EXT_SERVER_IDENTIFIER, {
@ -179,6 +172,7 @@ export class ExtensionHostProxyManager implements IExtensionHostManager {
async dispose() {
if (!this.disposer.disposed) {
this.logger.log(this.LOG_TAG, 'dispose ext host proxy');
await this.extHostProxy?.$dispose();
this.disposer.dispose();
}

View File

@ -5,8 +5,8 @@ import util from 'util';
import { Injectable, Autowired } from '@opensumi/di';
import { WSChannel } from '@opensumi/ide-connection';
import { WebSocketMessageReader, WebSocketMessageWriter } from '@opensumi/ide-connection/lib/common/message';
import { commonChannelPathHandler, SocketMessageReader, SocketMessageWriter } from '@opensumi/ide-connection/lib/node';
import { NetSocketConnection } from '@opensumi/ide-connection/lib/common/connection';
import { commonChannelPathHandler } from '@opensumi/ide-connection/lib/node';
import {
Event,
Emitter,
@ -79,7 +79,13 @@ export class ExtensionNodeServiceImpl implements IExtensionNodeService {
private clientExtProcessMap: Map<string, number> = new Map();
private clientExtProcessInspectPortMap: Map<string, number> = new Map();
private clientExtProcessInitDeferredMap: Map<string, Deferred<void>> = new Map();
private clientExtProcessExtConnection: Map<string, any> = new Map();
private clientExtProcessExtConnection: Map<
string,
{
connection: net.Socket;
channel?: WSChannel;
}
> = new Map();
private clientExtProcessExtConnectionDeferredMap: Map<string, Deferred<void>> = new Map();
private clientExtProcessExtConnectionServer: Map<string, net.Server> = new Map();
private clientExtProcessFinishDeferredMap: Map<string, Deferred<void>> = new Map();
@ -168,7 +174,7 @@ export class ExtensionNodeServiceImpl implements IExtensionNodeService {
private setExtProcessConnectionForward() {
this.logger.log('setExtProcessConnectionForward', this.instanceId);
this._setMainThreadConnection(async (connectionResult) => {
const { connection: mainThreadConnection, clientId } = connectionResult;
const { channel, clientId } = connectionResult;
await this.clientExtProcessExtConnectionDeferredMap.get(clientId)?.promise;
@ -191,24 +197,26 @@ export class ExtensionNodeServiceImpl implements IExtensionNodeService {
return;
}
const extConnection = this.clientExtProcessExtConnection.get(clientId);
const extConnection = this.clientExtProcessExtConnection.get(clientId)!;
if (extConnection.channel) {
extConnection.channel.dispose();
extConnection.channel = undefined;
}
// 重新生成实例,避免 tcp 消息有残留的缓存,造成分包错误
const extConnectionReader = new SocketMessageReader(extConnection.connection);
const extConnectionWriter = new SocketMessageWriter(extConnection.connection);
const extChannel = WSChannel.forClient(new NetSocketConnection(extConnection.connection), {
id: 'ExtensionHostForward-' + clientId,
logger: this.logger,
});
this.clientExtProcessExtConnection.set(clientId, {
reader: extConnectionReader,
writer: extConnectionWriter,
channel: extChannel,
connection: extConnection.connection,
});
mainThreadConnection.reader.listen((input) => {
extConnectionWriter.write(input);
});
extChannel.listen(channel);
channel.listen(extChannel);
extConnectionReader.listen((input) => {
mainThreadConnection.writer.write(input);
});
// 连接恢复后清除销毁的定时器
if (this.clientExtProcessThresholdExitTimerMap.has(clientId)) {
const timer = this.clientExtProcessThresholdExitTimerMap.get(clientId) as NodeJS.Timeout;
@ -475,7 +483,9 @@ export class ExtensionNodeServiceImpl implements IExtensionNodeService {
return this.clientExtProcessInspectPortMap.get(clientId);
}
private async _setMainThreadConnection(handler) {
private async _setMainThreadConnection(
handler: (connectionResult: { channel: WSChannel; clientId: string }) => void,
) {
if (process.env.KTELECTRON) {
const clientId = process.env.CODE_WINDOW_CLIENT_ID as string;
const mainThreadServer: net.Server = net.createServer();
@ -485,17 +495,19 @@ export class ExtensionNodeServiceImpl implements IExtensionNodeService {
mainThreadServer.on('connection', (connection) => {
this.logger.log(`The electron mainThread ${clientId} connected`);
const channel = WSChannel.forClient(new NetSocketConnection(connection), {
id: 'ElectronMainThreadForward- + clientId',
});
handler({
connection: {
reader: new SocketMessageReader(connection),
writer: new SocketMessageWriter(connection),
},
channel,
clientId,
});
connection.on('close', () => {
connection.once('close', () => {
this.logger.log(`Dispose client by clientId ${clientId}`);
// electron 只要端口进程就杀死插件进程
// if renderer connection is lost, kill ext process
// this means user has close the window
this.disposeClientExtProcess(clientId);
});
});
@ -505,33 +517,23 @@ export class ExtensionNodeServiceImpl implements IExtensionNodeService {
});
} else {
commonChannelPathHandler.register(CONNECTION_HANDLE_BETWEEN_EXTENSION_AND_MAIN_THREAD, {
handler: (connection: WSChannel, connectionClientId: string) => {
const reader = new WebSocketMessageReader(connection);
const writer = new WebSocketMessageWriter(connection);
handler: (channel: WSChannel, clientId: string) => {
handler({
connection: {
reader,
writer,
},
clientId: connectionClientId,
channel,
clientId,
});
connection.onClose(() => {
reader.dispose();
writer.dispose();
this.logger.log(`The connection client ${connectionClientId} closed`);
channel.onClose(() => {
channel.dispose();
this.logger.log(`The connection client ${clientId} closed`);
if (this.clientExtProcessExtConnection.has(connectionClientId)) {
const extConnection: any = this.clientExtProcessExtConnection.get(connectionClientId);
if (extConnection.writer) {
extConnection.writer.dispose();
}
if (extConnection.reader) {
extConnection.reader.dispose();
if (this.clientExtProcessExtConnection.has(clientId)) {
const extConnection = this.clientExtProcessExtConnection.get(clientId)!;
if (extConnection.channel) {
extConnection.channel.dispose();
}
}
// 当连接关闭后启动定时器清除插件进程
this.closeExtProcessWhenConnectionClose(connectionClientId);
this.closeExtProcessWhenConnectionClose(clientId);
});
},
dispose: () => {},
@ -596,7 +598,7 @@ export class ExtensionNodeServiceImpl implements IExtensionNodeService {
}
// connect 关闭
if (this.clientExtProcessExtConnection.has(clientId)) {
const connection = this.clientExtProcessExtConnection.get(clientId);
const connection = this.clientExtProcessExtConnection.get(clientId)!;
connection.connection.destroy();
}

View File

@ -570,17 +570,18 @@ export class DiskFileSystemProvider extends RPCService<IRPCDiskFileSystemProvide
const lstat = await fse.lstat(filePath);
if (lstat.isSymbolicLink()) {
let realPath;
let realPath: string;
try {
realPath = await fse.realpath(FileUri.fsPath(new URI(uri)));
} catch (e) {
this.logger.warn('Cannot resolve symbolic link', uri.toString(), e);
return undefined;
}
const stat = await fse.stat(filePath);
const realURI = FileUri.create(realPath);
const realStat = await fse.lstat(realPath);
let realStatData;
let realStatData: FileStat;
if (stat.isDirectory()) {
realStatData = await this.doCreateDirectoryStat(realURI.codeUri, realStat, depth);
} else {
@ -603,6 +604,7 @@ export class DiskFileSystemProvider extends RPCService<IRPCDiskFileSystemProvide
return fileStat;
}
} catch (error) {
this.logger.error('Error occurred when getting file stat', uri, error);
if (options?.throwError) {
handleError(error);
}

View File

@ -1,6 +1,3 @@
/**
* Terminal Client Test
*/
import os from 'os';
import path from 'path';

View File

@ -1,6 +1,3 @@
/**
* Terminal Controller Test
*/
import WebSocket from 'ws';
import { Uri } from '@opensumi/ide-core-common';

View File

@ -56,7 +56,7 @@ import { ITerminalPreference } from '../../src/common/preference';
import {
MockMainLayoutService,
MockTerminalThemeService,
MockSocketService,
MockTerminalService,
MockPreferenceService,
MockThemeService,
MockFileService,
@ -88,7 +88,7 @@ export const injector = new MockInjector([
},
{
token: ITerminalService,
useClass: MockSocketService,
useClass: MockTerminalService,
},
{
token: IApplicationService,

View File

@ -2,6 +2,8 @@ import WebSocket from 'ws';
import { Terminal } from 'xterm';
import { Injectable } from '@opensumi/di';
import { WSChannel } from '@opensumi/ide-connection';
import { WSWebSocketConnection } from '@opensumi/ide-connection/lib/common/connection';
import { Disposable, PreferenceProvider, PreferenceResolveResult } from '@opensumi/ide-core-browser';
import { PreferenceService } from '@opensumi/ide-core-browser';
import { uuid, URI, Emitter, IDisposable, PreferenceScope, Deferred, OperatingSystem } from '@opensumi/ide-core-common';
@ -44,14 +46,16 @@ Object.defineProperty(window, 'matchMedia', {
export const defaultName = 'bash';
@Injectable()
export class MockSocketService implements ITerminalService {
export class MockTerminalService implements ITerminalService {
static resId = 1;
private _socks: Map<string, WebSocket>;
private channels: Map<string, WSChannel>;
private socks: Map<string, WebSocket>;
private _response: Map<number, { resolve: (value: any) => void }>;
constructor() {
this._socks = new Map();
this.channels = new Map();
this.socks = new Map();
this._response = new Map();
}
@ -66,7 +70,12 @@ export class MockSocketService implements ITerminalService {
launchConfig: IShellLaunchConfig,
): Promise<ITerminalConnection | undefined> {
const sock = new WebSocket(localhost(getPort()));
this._socks.set(sessionId, sock);
const channel = WSChannel.forClient(new WSWebSocketConnection(sock), {
id: sessionId,
});
this.channels.set(sessionId, channel);
this.socks.set(sessionId, sock);
await delay(2000);
this._handleMethod(sessionId);
@ -119,11 +128,12 @@ export class MockSocketService implements ITerminalService {
}
private _handleStdoutMessage(sessionId: string, handler: (json: any) => void) {
const socket = this._socks.get(sessionId);
if (!socket) {
const channel = this.channels.get(sessionId);
if (!channel) {
return;
}
socket.addEventListener('message', ({ data }) => {
channel.onMessage((data) => {
const json = JSON.parse(data) as any;
if (!json.method) {
handler(json.data);
@ -154,7 +164,7 @@ export class MockSocketService implements ITerminalService {
}
private _sendMessage(sessionId: string, json: any) {
const sock = this._socks.get(sessionId);
const sock = this.channels.get(sessionId);
if (!sock) {
return;
}
@ -163,7 +173,7 @@ export class MockSocketService implements ITerminalService {
private async _doMethod(sessionId: string, method: string, params: any) {
return new Promise((resolve) => {
const id = MockSocketService.resId++;
const id = MockTerminalService.resId++;
this._sendMessage(sessionId, { id, method, params });
if (id !== -1) {
this._response.set(id, { resolve });
@ -172,22 +182,20 @@ export class MockSocketService implements ITerminalService {
}
private _handleMethod(sessionId: string) {
const socket = this._socks.get(sessionId);
const socket = this.channels.get(sessionId);
if (!socket) {
return;
}
const handleSocketMessage = (msg: MessageEvent) => {
const json = JSON.parse(msg.data);
socket.onMessage((data) => {
const json = JSON.parse(data);
if (json.method) {
const handler = this._response.get(json.id);
handler && handler.resolve(json);
this._response.delete(json.id);
}
};
socket.addEventListener('message', handleSocketMessage as any);
});
}
async attach(sessionId: string, term: Terminal) {
@ -204,7 +212,7 @@ export class MockSocketService implements ITerminalService {
}
disposeById(sessionId: string) {
const socket = this._socks.get(sessionId);
const socket = this.socks.get(sessionId);
this._doMethod(sessionId, MessageMethod.resize, { id: sessionId });

View File

@ -7,6 +7,8 @@ import httpProxy from 'http-proxy';
import * as pty from 'node-pty';
import WebSocket from 'ws';
import { WSChannel } from '@opensumi/ide-connection';
import { WSWebSocketConnection } from '@opensumi/ide-connection/lib/common/connection';
import { uuid } from '@opensumi/ide-core-browser';
function getRandomInt(min: number, max: number) {
@ -83,7 +85,7 @@ export function killPty(json: RPCRequest<{ sessionId: string }>) {
}
export function createPty(
socket: WebSocket,
channel: WSChannel,
json: RPCRequest<{ sessionId: string; cols: number; rows: number }>,
): RPCResponse<{ sessionId: string }> {
const { sessionId, cols, rows } = json.params;
@ -98,12 +100,12 @@ export function createPty(
ptyProcess.onData((data) => {
// handleStdOutMessage
socket.send(JSON.stringify({ sessionId, data } as PtyStdOut));
channel.send(JSON.stringify({ sessionId, data } as PtyStdOut));
});
ptyProcess.onExit(() => {
try {
socket.close();
channel.close();
} catch (_e) {}
});
@ -121,10 +123,10 @@ export function resizePty(json: RPCRequest<{ sessionId: string; cols: number; ro
return _makeResponse(json, { sessionId });
}
export function handleServerMethod(socket: WebSocket, json: RPCRequest): RPCResponse {
export function handleServerMethod(channel: WSChannel, json: RPCRequest): RPCResponse {
switch (json.method) {
case MessageMethod.create:
return createPty(socket, json);
return createPty(channel, json);
case MessageMethod.resize:
return resizePty(json);
case MessageMethod.close:
@ -144,16 +146,21 @@ export function handleStdinMessage(json: PtyStdIn) {
export function createWsServer() {
const server = new WebSocket.Server({ port: getPort() });
server.on('connection', (socket) => {
socket.on('message', (data) => {
const channel = WSChannel.forClient(new WSWebSocketConnection(socket), {
id: 'ws-server',
});
channel.onMessage((data) => {
const json = JSON.parse(data.toString());
if (json.method) {
const res = handleServerMethod(socket, json);
socket.send(JSON.stringify(res));
const res = handleServerMethod(channel, json);
channel.send(JSON.stringify(res));
} else {
handleStdinMessage(json);
}
});
socket.on('error', () => {});
});

Some files were not shown because too many files have changed in this diff Show More