mirror of https://github.com/opensumi/core
refactor: update node ws channel code (#3241)
This commit is contained in:
parent
32419c9fca
commit
c89984983a
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: () => {},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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 () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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: () => {},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export * from './base';
|
||||
export * from './utils';
|
||||
export * from './node-message-port';
|
||||
export * from './socket';
|
||||
export * from './ws-websocket';
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './drivers';
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export const METHOD_NOT_REGISTERED = '$$METHOD_NOT_REGISTERED';
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './stub';
|
||||
export * from './center';
|
|
@ -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];
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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> {}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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' });
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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([]);
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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([]);
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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([]);
|
||||
|
|
|
@ -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([]);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 '..';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/**
|
||||
* Terminal Client Test
|
||||
*/
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/**
|
||||
* Terminal Controller Test
|
||||
*/
|
||||
import WebSocket from 'ws';
|
||||
|
||||
import { Uri } from '@opensumi/ide-core-common';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue