sussudio
Version:
An unofficial VS Code Internal API
1,138 lines (1,137 loc) • 45.8 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IntervalTimer } from "../../../common/async.mjs";
import { VSBuffer } from "../../../common/buffer.mjs";
import { Emitter } from "../../../common/event.mjs";
import { Disposable, DisposableStore } from "../../../common/lifecycle.mjs";
import { IPCClient } from "./ipc.mjs";
export var SocketDiagnosticsEventType;
(function (SocketDiagnosticsEventType) {
SocketDiagnosticsEventType["Created"] = "created";
SocketDiagnosticsEventType["Read"] = "read";
SocketDiagnosticsEventType["Write"] = "write";
SocketDiagnosticsEventType["Open"] = "open";
SocketDiagnosticsEventType["Error"] = "error";
SocketDiagnosticsEventType["Close"] = "close";
SocketDiagnosticsEventType["BrowserWebSocketBlobReceived"] = "browserWebSocketBlobReceived";
SocketDiagnosticsEventType["NodeEndReceived"] = "nodeEndReceived";
SocketDiagnosticsEventType["NodeEndSent"] = "nodeEndSent";
SocketDiagnosticsEventType["NodeDrainBegin"] = "nodeDrainBegin";
SocketDiagnosticsEventType["NodeDrainEnd"] = "nodeDrainEnd";
SocketDiagnosticsEventType["zlibInflateError"] = "zlibInflateError";
SocketDiagnosticsEventType["zlibInflateData"] = "zlibInflateData";
SocketDiagnosticsEventType["zlibInflateInitialWrite"] = "zlibInflateInitialWrite";
SocketDiagnosticsEventType["zlibInflateInitialFlushFired"] = "zlibInflateInitialFlushFired";
SocketDiagnosticsEventType["zlibInflateWrite"] = "zlibInflateWrite";
SocketDiagnosticsEventType["zlibInflateFlushFired"] = "zlibInflateFlushFired";
SocketDiagnosticsEventType["zlibDeflateError"] = "zlibDeflateError";
SocketDiagnosticsEventType["zlibDeflateData"] = "zlibDeflateData";
SocketDiagnosticsEventType["zlibDeflateWrite"] = "zlibDeflateWrite";
SocketDiagnosticsEventType["zlibDeflateFlushFired"] = "zlibDeflateFlushFired";
SocketDiagnosticsEventType["WebSocketNodeSocketWrite"] = "webSocketNodeSocketWrite";
SocketDiagnosticsEventType["WebSocketNodeSocketPeekedHeader"] = "webSocketNodeSocketPeekedHeader";
SocketDiagnosticsEventType["WebSocketNodeSocketReadHeader"] = "webSocketNodeSocketReadHeader";
SocketDiagnosticsEventType["WebSocketNodeSocketReadData"] = "webSocketNodeSocketReadData";
SocketDiagnosticsEventType["WebSocketNodeSocketUnmaskedData"] = "webSocketNodeSocketUnmaskedData";
SocketDiagnosticsEventType["WebSocketNodeSocketDrainBegin"] = "webSocketNodeSocketDrainBegin";
SocketDiagnosticsEventType["WebSocketNodeSocketDrainEnd"] = "webSocketNodeSocketDrainEnd";
SocketDiagnosticsEventType["ProtocolHeaderRead"] = "protocolHeaderRead";
SocketDiagnosticsEventType["ProtocolMessageRead"] = "protocolMessageRead";
SocketDiagnosticsEventType["ProtocolHeaderWrite"] = "protocolHeaderWrite";
SocketDiagnosticsEventType["ProtocolMessageWrite"] = "protocolMessageWrite";
SocketDiagnosticsEventType["ProtocolWrite"] = "protocolWrite";
})(SocketDiagnosticsEventType || (SocketDiagnosticsEventType = {}));
export var SocketDiagnostics;
(function (SocketDiagnostics) {
SocketDiagnostics.enableDiagnostics = false;
SocketDiagnostics.records = [];
const socketIds = new WeakMap();
let lastUsedSocketId = 0;
function getSocketId(nativeObject, label) {
if (!socketIds.has(nativeObject)) {
const id = String(++lastUsedSocketId);
socketIds.set(nativeObject, id);
}
return socketIds.get(nativeObject);
}
function traceSocketEvent(nativeObject, socketDebugLabel, type, data) {
if (!SocketDiagnostics.enableDiagnostics) {
return;
}
const id = getSocketId(nativeObject, socketDebugLabel);
if (data instanceof VSBuffer || data instanceof Uint8Array || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const copiedData = VSBuffer.alloc(data.byteLength);
copiedData.set(data);
SocketDiagnostics.records.push({ timestamp: Date.now(), id, label: socketDebugLabel, type, buff: copiedData });
}
else {
// data is a custom object
SocketDiagnostics.records.push({ timestamp: Date.now(), id, label: socketDebugLabel, type, data: data });
}
}
SocketDiagnostics.traceSocketEvent = traceSocketEvent;
})(SocketDiagnostics || (SocketDiagnostics = {}));
export var SocketCloseEventType;
(function (SocketCloseEventType) {
SocketCloseEventType[SocketCloseEventType["NodeSocketCloseEvent"] = 0] = "NodeSocketCloseEvent";
SocketCloseEventType[SocketCloseEventType["WebSocketCloseEvent"] = 1] = "WebSocketCloseEvent";
})(SocketCloseEventType || (SocketCloseEventType = {}));
let emptyBuffer = null;
function getEmptyBuffer() {
if (!emptyBuffer) {
emptyBuffer = VSBuffer.alloc(0);
}
return emptyBuffer;
}
export class ChunkStream {
_chunks;
_totalLength;
get byteLength() {
return this._totalLength;
}
constructor() {
this._chunks = [];
this._totalLength = 0;
}
acceptChunk(buff) {
this._chunks.push(buff);
this._totalLength += buff.byteLength;
}
read(byteCount) {
return this._read(byteCount, true);
}
peek(byteCount) {
return this._read(byteCount, false);
}
_read(byteCount, advance) {
if (byteCount === 0) {
return getEmptyBuffer();
}
if (byteCount > this._totalLength) {
throw new Error(`Cannot read so many bytes!`);
}
if (this._chunks[0].byteLength === byteCount) {
// super fast path, precisely first chunk must be returned
const result = this._chunks[0];
if (advance) {
this._chunks.shift();
this._totalLength -= byteCount;
}
return result;
}
if (this._chunks[0].byteLength > byteCount) {
// fast path, the reading is entirely within the first chunk
const result = this._chunks[0].slice(0, byteCount);
if (advance) {
this._chunks[0] = this._chunks[0].slice(byteCount);
this._totalLength -= byteCount;
}
return result;
}
const result = VSBuffer.alloc(byteCount);
let resultOffset = 0;
let chunkIndex = 0;
while (byteCount > 0) {
const chunk = this._chunks[chunkIndex];
if (chunk.byteLength > byteCount) {
// this chunk will survive
const chunkPart = chunk.slice(0, byteCount);
result.set(chunkPart, resultOffset);
resultOffset += byteCount;
if (advance) {
this._chunks[chunkIndex] = chunk.slice(byteCount);
this._totalLength -= byteCount;
}
byteCount -= byteCount;
}
else {
// this chunk will be entirely read
result.set(chunk, resultOffset);
resultOffset += chunk.byteLength;
if (advance) {
this._chunks.shift();
this._totalLength -= chunk.byteLength;
}
else {
chunkIndex++;
}
byteCount -= chunk.byteLength;
}
}
return result;
}
}
var ProtocolMessageType;
(function (ProtocolMessageType) {
ProtocolMessageType[ProtocolMessageType["None"] = 0] = "None";
ProtocolMessageType[ProtocolMessageType["Regular"] = 1] = "Regular";
ProtocolMessageType[ProtocolMessageType["Control"] = 2] = "Control";
ProtocolMessageType[ProtocolMessageType["Ack"] = 3] = "Ack";
ProtocolMessageType[ProtocolMessageType["Disconnect"] = 5] = "Disconnect";
ProtocolMessageType[ProtocolMessageType["ReplayRequest"] = 6] = "ReplayRequest";
ProtocolMessageType[ProtocolMessageType["Pause"] = 7] = "Pause";
ProtocolMessageType[ProtocolMessageType["Resume"] = 8] = "Resume";
ProtocolMessageType[ProtocolMessageType["KeepAlive"] = 9] = "KeepAlive";
ProtocolMessageType[ProtocolMessageType["LatencyMeasurementRequest"] = 10] = "LatencyMeasurementRequest";
ProtocolMessageType[ProtocolMessageType["LatencyMeasurementResponse"] = 11] = "LatencyMeasurementResponse";
})(ProtocolMessageType || (ProtocolMessageType = {}));
function protocolMessageTypeToString(messageType) {
switch (messageType) {
case 0 /* ProtocolMessageType.None */: return 'None';
case 1 /* ProtocolMessageType.Regular */: return 'Regular';
case 2 /* ProtocolMessageType.Control */: return 'Control';
case 3 /* ProtocolMessageType.Ack */: return 'Ack';
case 5 /* ProtocolMessageType.Disconnect */: return 'Disconnect';
case 6 /* ProtocolMessageType.ReplayRequest */: return 'ReplayRequest';
case 7 /* ProtocolMessageType.Pause */: return 'PauseWriting';
case 8 /* ProtocolMessageType.Resume */: return 'ResumeWriting';
case 9 /* ProtocolMessageType.KeepAlive */: return 'KeepAlive';
case 10 /* ProtocolMessageType.LatencyMeasurementRequest */: return 'LatencyMeasurementRequest';
case 11 /* ProtocolMessageType.LatencyMeasurementResponse */: return 'LatencyMeasurementResponse';
}
}
export var ProtocolConstants;
(function (ProtocolConstants) {
ProtocolConstants[ProtocolConstants["HeaderLength"] = 13] = "HeaderLength";
/**
* Send an Acknowledge message at most 2 seconds later...
*/
ProtocolConstants[ProtocolConstants["AcknowledgeTime"] = 2000] = "AcknowledgeTime";
/**
* If there is a sent message that has been unacknowledged for 20 seconds,
* and we didn't see any incoming server data in the past 20 seconds,
* then consider the connection has timed out.
*/
ProtocolConstants[ProtocolConstants["TimeoutTime"] = 20000] = "TimeoutTime";
/**
* If there is no reconnection within this time-frame, consider the connection permanently closed...
*/
ProtocolConstants[ProtocolConstants["ReconnectionGraceTime"] = 10800000] = "ReconnectionGraceTime";
/**
* Maximal grace time between the first and the last reconnection...
*/
ProtocolConstants[ProtocolConstants["ReconnectionShortGraceTime"] = 300000] = "ReconnectionShortGraceTime";
/**
* Send a message every 5 seconds to avoid that the connection is closed by the OS.
*/
ProtocolConstants[ProtocolConstants["KeepAliveSendTime"] = 5000] = "KeepAliveSendTime";
/**
* Measure the latency every 1 minute.
*/
ProtocolConstants[ProtocolConstants["LatencySampleTime"] = 60000] = "LatencySampleTime";
/**
* Keep the last 5 samples for latency measurement.
*/
ProtocolConstants[ProtocolConstants["LatencySampleCount"] = 5] = "LatencySampleCount";
/**
* A latency over 1s will be considered high.
*/
ProtocolConstants[ProtocolConstants["HighLatencyTimeThreshold"] = 1000] = "HighLatencyTimeThreshold";
/**
* Having 3 or more samples with high latency will trigger a high latency event.
*/
ProtocolConstants[ProtocolConstants["HighLatencySampleThreshold"] = 3] = "HighLatencySampleThreshold";
})(ProtocolConstants || (ProtocolConstants = {}));
class ProtocolMessage {
type;
id;
ack;
data;
writtenTime;
constructor(type, id, ack, data) {
this.type = type;
this.id = id;
this.ack = ack;
this.data = data;
this.writtenTime = 0;
}
get size() {
return this.data.byteLength;
}
}
class ProtocolReader extends Disposable {
_socket;
_isDisposed;
_incomingData;
lastReadTime;
_onMessage = this._register(new Emitter());
onMessage = this._onMessage.event;
_state = {
readHead: true,
readLen: 13 /* ProtocolConstants.HeaderLength */,
messageType: 0 /* ProtocolMessageType.None */,
id: 0,
ack: 0
};
constructor(socket) {
super();
this._socket = socket;
this._isDisposed = false;
this._incomingData = new ChunkStream();
this._register(this._socket.onData(data => this.acceptChunk(data)));
this.lastReadTime = Date.now();
}
acceptChunk(data) {
if (!data || data.byteLength === 0) {
return;
}
this.lastReadTime = Date.now();
this._incomingData.acceptChunk(data);
while (this._incomingData.byteLength >= this._state.readLen) {
const buff = this._incomingData.read(this._state.readLen);
if (this._state.readHead) {
// buff is the header
// save new state => next time will read the body
this._state.readHead = false;
this._state.readLen = buff.readUInt32BE(9);
this._state.messageType = buff.readUInt8(0);
this._state.id = buff.readUInt32BE(1);
this._state.ack = buff.readUInt32BE(5);
this._socket.traceSocketEvent("protocolHeaderRead" /* SocketDiagnosticsEventType.ProtocolHeaderRead */, { messageType: protocolMessageTypeToString(this._state.messageType), id: this._state.id, ack: this._state.ack, messageSize: this._state.readLen });
}
else {
// buff is the body
const messageType = this._state.messageType;
const id = this._state.id;
const ack = this._state.ack;
// save new state => next time will read the header
this._state.readHead = true;
this._state.readLen = 13 /* ProtocolConstants.HeaderLength */;
this._state.messageType = 0 /* ProtocolMessageType.None */;
this._state.id = 0;
this._state.ack = 0;
this._socket.traceSocketEvent("protocolMessageRead" /* SocketDiagnosticsEventType.ProtocolMessageRead */, buff);
this._onMessage.fire(new ProtocolMessage(messageType, id, ack, buff));
if (this._isDisposed) {
// check if an event listener lead to our disposal
break;
}
}
}
}
readEntireBuffer() {
return this._incomingData.read(this._incomingData.byteLength);
}
dispose() {
this._isDisposed = true;
super.dispose();
}
}
class ProtocolWriter {
_isDisposed;
_isPaused;
_socket;
_data;
_totalLength;
lastWriteTime;
constructor(socket) {
this._isDisposed = false;
this._isPaused = false;
this._socket = socket;
this._data = [];
this._totalLength = 0;
this.lastWriteTime = 0;
}
dispose() {
try {
this.flush();
}
catch (err) {
// ignore error, since the socket could be already closed
}
this._isDisposed = true;
}
drain() {
this.flush();
return this._socket.drain();
}
flush() {
// flush
this._writeNow();
}
pause() {
this._isPaused = true;
}
resume() {
this._isPaused = false;
this._scheduleWriting();
}
write(msg) {
if (this._isDisposed) {
// ignore: there could be left-over promises which complete and then
// decide to write a response, etc...
return;
}
msg.writtenTime = Date.now();
this.lastWriteTime = Date.now();
const header = VSBuffer.alloc(13 /* ProtocolConstants.HeaderLength */);
header.writeUInt8(msg.type, 0);
header.writeUInt32BE(msg.id, 1);
header.writeUInt32BE(msg.ack, 5);
header.writeUInt32BE(msg.data.byteLength, 9);
this._socket.traceSocketEvent("protocolHeaderWrite" /* SocketDiagnosticsEventType.ProtocolHeaderWrite */, { messageType: protocolMessageTypeToString(msg.type), id: msg.id, ack: msg.ack, messageSize: msg.data.byteLength });
this._socket.traceSocketEvent("protocolMessageWrite" /* SocketDiagnosticsEventType.ProtocolMessageWrite */, msg.data);
this._writeSoon(header, msg.data);
}
_bufferAdd(head, body) {
const wasEmpty = this._totalLength === 0;
this._data.push(head, body);
this._totalLength += head.byteLength + body.byteLength;
return wasEmpty;
}
_bufferTake() {
const ret = VSBuffer.concat(this._data, this._totalLength);
this._data.length = 0;
this._totalLength = 0;
return ret;
}
_writeSoon(header, data) {
if (this._bufferAdd(header, data)) {
this._scheduleWriting();
}
}
_writeNowTimeout = null;
_scheduleWriting() {
if (this._writeNowTimeout) {
return;
}
this._writeNowTimeout = setTimeout(() => {
this._writeNowTimeout = null;
this._writeNow();
});
}
_writeNow() {
if (this._totalLength === 0) {
return;
}
if (this._isPaused) {
return;
}
const data = this._bufferTake();
this._socket.traceSocketEvent("protocolWrite" /* SocketDiagnosticsEventType.ProtocolWrite */, { byteLength: data.byteLength });
this._socket.write(data);
}
}
/**
* A message has the following format:
* ```
* /-------------------------------|------\
* | HEADER | |
* |-------------------------------| DATA |
* | TYPE | ID | ACK | DATA_LENGTH | |
* \-------------------------------|------/
* ```
* The header is 9 bytes and consists of:
* - TYPE is 1 byte (ProtocolMessageType) - the message type
* - ID is 4 bytes (u32be) - the message id (can be 0 to indicate to be ignored)
* - ACK is 4 bytes (u32be) - the acknowledged message id (can be 0 to indicate to be ignored)
* - DATA_LENGTH is 4 bytes (u32be) - the length in bytes of DATA
*
* Only Regular messages are counted, other messages are not counted, nor acknowledged.
*/
export class Protocol extends Disposable {
_socket;
_socketWriter;
_socketReader;
_onMessage = new Emitter();
onMessage = this._onMessage.event;
_onDidDispose = new Emitter();
onDidDispose = this._onDidDispose.event;
constructor(socket) {
super();
this._socket = socket;
this._socketWriter = this._register(new ProtocolWriter(this._socket));
this._socketReader = this._register(new ProtocolReader(this._socket));
this._register(this._socketReader.onMessage((msg) => {
if (msg.type === 1 /* ProtocolMessageType.Regular */) {
this._onMessage.fire(msg.data);
}
}));
this._register(this._socket.onClose(() => this._onDidDispose.fire()));
}
drain() {
return this._socketWriter.drain();
}
getSocket() {
return this._socket;
}
sendDisconnect() {
// Nothing to do...
}
send(buffer) {
this._socketWriter.write(new ProtocolMessage(1 /* ProtocolMessageType.Regular */, 0, 0, buffer));
}
}
export class Client extends IPCClient {
protocol;
static fromSocket(socket, id) {
return new Client(new Protocol(socket), id);
}
get onDidDispose() { return this.protocol.onDidDispose; }
constructor(protocol, id, ipcLogger = null) {
super(protocol, id, ipcLogger);
this.protocol = protocol;
}
dispose() {
super.dispose();
const socket = this.protocol.getSocket();
this.protocol.sendDisconnect();
this.protocol.dispose();
socket.end();
}
}
/**
* Will ensure no messages are lost if there are no event listeners.
*/
export class BufferedEmitter {
_emitter;
event;
_hasListeners = false;
_isDeliveringMessages = false;
_bufferedMessages = [];
constructor() {
this._emitter = new Emitter({
onWillAddFirstListener: () => {
this._hasListeners = true;
// it is important to deliver these messages after this call, but before
// other messages have a chance to be received (to guarantee in order delivery)
// that's why we're using here queueMicrotask and not other types of timeouts
queueMicrotask(() => this._deliverMessages());
},
onDidRemoveLastListener: () => {
this._hasListeners = false;
}
});
this.event = this._emitter.event;
}
_deliverMessages() {
if (this._isDeliveringMessages) {
return;
}
this._isDeliveringMessages = true;
while (this._hasListeners && this._bufferedMessages.length > 0) {
this._emitter.fire(this._bufferedMessages.shift());
}
this._isDeliveringMessages = false;
}
fire(event) {
if (this._hasListeners) {
if (this._bufferedMessages.length > 0) {
this._bufferedMessages.push(event);
}
else {
this._emitter.fire(event);
}
}
else {
this._bufferedMessages.push(event);
}
}
flushBuffer() {
this._bufferedMessages = [];
}
}
class QueueElement {
data;
next;
constructor(data) {
this.data = data;
this.next = null;
}
}
class Queue {
_first;
_last;
constructor() {
this._first = null;
this._last = null;
}
length() {
let result = 0;
let current = this._first;
while (current) {
current = current.next;
result++;
}
return result;
}
peek() {
if (!this._first) {
return null;
}
return this._first.data;
}
toArray() {
const result = [];
let resultLen = 0;
let it = this._first;
while (it) {
result[resultLen++] = it.data;
it = it.next;
}
return result;
}
pop() {
if (!this._first) {
return;
}
if (this._first === this._last) {
this._first = null;
this._last = null;
return;
}
this._first = this._first.next;
}
push(item) {
const element = new QueueElement(item);
if (!this._first) {
this._first = element;
this._last = element;
return;
}
this._last.next = element;
this._last = element;
}
}
class LoadEstimator {
static _HISTORY_LENGTH = 10;
static _INSTANCE = null;
static getInstance() {
if (!LoadEstimator._INSTANCE) {
LoadEstimator._INSTANCE = new LoadEstimator();
}
return LoadEstimator._INSTANCE;
}
lastRuns;
constructor() {
this.lastRuns = [];
const now = Date.now();
for (let i = 0; i < LoadEstimator._HISTORY_LENGTH; i++) {
this.lastRuns[i] = now - 1000 * i;
}
setInterval(() => {
for (let i = LoadEstimator._HISTORY_LENGTH; i >= 1; i--) {
this.lastRuns[i] = this.lastRuns[i - 1];
}
this.lastRuns[0] = Date.now();
}, 1000);
}
/**
* returns an estimative number, from 0 (low load) to 1 (high load)
*/
load() {
const now = Date.now();
const historyLimit = (1 + LoadEstimator._HISTORY_LENGTH) * 1000;
let score = 0;
for (let i = 0; i < LoadEstimator._HISTORY_LENGTH; i++) {
if (now - this.lastRuns[i] <= historyLimit) {
score++;
}
}
return 1 - score / LoadEstimator._HISTORY_LENGTH;
}
hasHighLoad() {
return this.load() >= 0.5;
}
}
export var ConnectionHealth;
(function (ConnectionHealth) {
/**
* The connection health is considered good when a certain number of recent round trip time measurements are below a certain threshold.
* @see ProtocolConstants.HighLatencyTimeThreshold @see ProtocolConstants.HighLatencySampleThreshold
*/
ConnectionHealth[ConnectionHealth["Good"] = 0] = "Good";
/**
* The connection health is considered poor when a certain number of recent round trip time measurements are above a certain threshold.
* @see ProtocolConstants.HighLatencyTimeThreshold @see ProtocolConstants.HighLatencySampleThreshold
*/
ConnectionHealth[ConnectionHealth["Poor"] = 1] = "Poor";
})(ConnectionHealth || (ConnectionHealth = {}));
export function connectionHealthToString(connectionHealth) {
switch (connectionHealth) {
case 0 /* ConnectionHealth.Good */: return 'good';
case 1 /* ConnectionHealth.Poor */: return 'poor';
}
}
/**
* An event describing that the connection health has changed.
*/
export class ConnectionHealthChangedEvent {
connectionHealth;
constructor(connectionHealth) {
this.connectionHealth = connectionHealth;
}
}
/**
* An event describing that a round trip time measurement was above a certain threshold.
*/
export class HighRoundTripTimeEvent {
roundTripTime;
recentHighRoundTripCount;
constructor(
/**
* The round trip time in milliseconds.
*/
roundTripTime,
/**
* The number of recent round trip time measurements that were above the threshold.
* @see ProtocolConstants.HighLatencyTimeThreshold @see ProtocolConstants.HighLatencySampleThreshold
*/
recentHighRoundTripCount) {
this.roundTripTime = roundTripTime;
this.recentHighRoundTripCount = recentHighRoundTripCount;
}
}
/**
* Same as Protocol, but will actually track messages and acks.
* Moreover, it will ensure no messages are lost if there are no event listeners.
*/
export class PersistentProtocol {
_isReconnecting;
_outgoingUnackMsg;
_outgoingMsgId;
_outgoingAckId;
_outgoingAckTimeout;
_incomingMsgId;
_incomingAckId;
_incomingMsgLastTime;
_incomingAckTimeout;
_keepAliveInterval;
_lastReplayRequestTime;
_lastSocketTimeoutTime;
_socket;
_socketWriter;
_socketReader;
_socketLatencyMonitor;
_socketDisposables;
_loadEstimator;
_measureRoundTripTime;
_shouldSendKeepAlive;
_onControlMessage = new BufferedEmitter();
onControlMessage = this._onControlMessage.event;
_onMessage = new BufferedEmitter();
onMessage = this._onMessage.event;
_onDidDispose = new BufferedEmitter();
onDidDispose = this._onDidDispose.event;
_onSocketClose = new BufferedEmitter();
onSocketClose = this._onSocketClose.event;
_onSocketTimeout = new BufferedEmitter();
onSocketTimeout = this._onSocketTimeout.event;
_onHighRoundTripTime = new BufferedEmitter();
onHighRoundTripTime = this._onHighRoundTripTime.event;
_onDidChangeConnectionHealth = new BufferedEmitter();
onDidChangeConnectionHealth = this._onDidChangeConnectionHealth.event;
get unacknowledgedCount() {
return this._outgoingMsgId - this._outgoingAckId;
}
constructor(opts) {
this._loadEstimator = opts.loadEstimator ?? LoadEstimator.getInstance();
this._measureRoundTripTime = opts.measureRoundTripTime ?? false;
this._shouldSendKeepAlive = opts.sendKeepAlive ?? true;
this._isReconnecting = false;
this._outgoingUnackMsg = new Queue();
this._outgoingMsgId = 0;
this._outgoingAckId = 0;
this._outgoingAckTimeout = null;
this._incomingMsgId = 0;
this._incomingAckId = 0;
this._incomingMsgLastTime = 0;
this._incomingAckTimeout = null;
this._lastReplayRequestTime = 0;
this._lastSocketTimeoutTime = Date.now();
this._socketDisposables = new DisposableStore();
this._socket = opts.socket;
this._socketWriter = this._socketDisposables.add(new ProtocolWriter(this._socket));
this._socketReader = this._socketDisposables.add(new ProtocolReader(this._socket));
this._socketDisposables.add(this._socketReader.onMessage(msg => this._receiveMessage(msg)));
this._socketDisposables.add(this._socket.onClose(e => this._onSocketClose.fire(e)));
this._socketLatencyMonitor = this._socketDisposables.add(new LatencyMonitor()); // is started immediately
this._socketDisposables.add(this._socketLatencyMonitor.onSendLatencyRequest(buffer => this._sendLatencyMeasurementRequest(buffer)));
this._socketDisposables.add(this._socketLatencyMonitor.onHighRoundTripTime(e => this._onHighRoundTripTime.fire(e)));
this._socketDisposables.add(this._socketLatencyMonitor.onDidChangeConnectionHealth(e => this._onDidChangeConnectionHealth.fire(e)));
if (this._measureRoundTripTime) {
this._socketLatencyMonitor.start();
}
if (opts.initialChunk) {
this._socketReader.acceptChunk(opts.initialChunk);
}
if (this._shouldSendKeepAlive) {
this._keepAliveInterval = setInterval(() => {
this._sendKeepAlive();
}, 5000 /* ProtocolConstants.KeepAliveSendTime */);
}
else {
this._keepAliveInterval = null;
}
}
dispose() {
if (this._outgoingAckTimeout) {
clearTimeout(this._outgoingAckTimeout);
this._outgoingAckTimeout = null;
}
if (this._incomingAckTimeout) {
clearTimeout(this._incomingAckTimeout);
this._incomingAckTimeout = null;
}
if (this._keepAliveInterval) {
clearInterval(this._keepAliveInterval);
this._keepAliveInterval = null;
}
this._socketDisposables.dispose();
}
drain() {
return this._socketWriter.drain();
}
sendDisconnect() {
const msg = new ProtocolMessage(5 /* ProtocolMessageType.Disconnect */, 0, 0, getEmptyBuffer());
this._socketWriter.write(msg);
this._socketWriter.flush();
}
sendPause() {
const msg = new ProtocolMessage(7 /* ProtocolMessageType.Pause */, 0, 0, getEmptyBuffer());
this._socketWriter.write(msg);
}
sendResume() {
const msg = new ProtocolMessage(8 /* ProtocolMessageType.Resume */, 0, 0, getEmptyBuffer());
this._socketWriter.write(msg);
}
pauseSocketWriting() {
this._socketWriter.pause();
}
getSocket() {
return this._socket;
}
getMillisSinceLastIncomingData() {
return Date.now() - this._socketReader.lastReadTime;
}
beginAcceptReconnection(socket, initialDataChunk) {
this._isReconnecting = true;
this._socketDisposables.dispose();
this._socketDisposables = new DisposableStore();
this._onControlMessage.flushBuffer();
this._onSocketClose.flushBuffer();
this._onSocketTimeout.flushBuffer();
this._socket.dispose();
this._lastReplayRequestTime = 0;
this._lastSocketTimeoutTime = Date.now();
this._socket = socket;
this._socketWriter = this._socketDisposables.add(new ProtocolWriter(this._socket));
this._socketReader = this._socketDisposables.add(new ProtocolReader(this._socket));
this._socketDisposables.add(this._socketReader.onMessage(msg => this._receiveMessage(msg)));
this._socketDisposables.add(this._socket.onClose(e => this._onSocketClose.fire(e)));
this._socketLatencyMonitor = this._socketDisposables.add(new LatencyMonitor()); // will be started later
this._socketDisposables.add(this._socketLatencyMonitor.onSendLatencyRequest(buffer => this._sendLatencyMeasurementRequest(buffer)));
this._socketDisposables.add(this._socketLatencyMonitor.onHighRoundTripTime(e => this._onHighRoundTripTime.fire(e)));
this._socketDisposables.add(this._socketLatencyMonitor.onDidChangeConnectionHealth(e => this._onDidChangeConnectionHealth.fire(e)));
this._socketReader.acceptChunk(initialDataChunk);
}
endAcceptReconnection() {
this._isReconnecting = false;
if (this._measureRoundTripTime) {
this._socketLatencyMonitor.start();
}
// After a reconnection, let the other party know (again) which messages have been received.
// (perhaps the other party didn't receive a previous ACK)
this._incomingAckId = this._incomingMsgId;
const msg = new ProtocolMessage(3 /* ProtocolMessageType.Ack */, 0, this._incomingAckId, getEmptyBuffer());
this._socketWriter.write(msg);
// Send again all unacknowledged messages
const toSend = this._outgoingUnackMsg.toArray();
for (let i = 0, len = toSend.length; i < len; i++) {
this._socketWriter.write(toSend[i]);
}
this._recvAckCheck();
}
acceptDisconnect() {
this._onDidDispose.fire();
}
_receiveMessage(msg) {
if (msg.ack > this._outgoingAckId) {
this._outgoingAckId = msg.ack;
do {
const first = this._outgoingUnackMsg.peek();
if (first && first.id <= msg.ack) {
// this message has been confirmed, remove it
this._outgoingUnackMsg.pop();
}
else {
break;
}
} while (true);
}
switch (msg.type) {
case 0 /* ProtocolMessageType.None */: {
// N/A
break;
}
case 1 /* ProtocolMessageType.Regular */: {
if (msg.id > this._incomingMsgId) {
if (msg.id !== this._incomingMsgId + 1) {
// in case we missed some messages we ask the other party to resend them
const now = Date.now();
if (now - this._lastReplayRequestTime > 10000) {
// send a replay request at most once every 10s
this._lastReplayRequestTime = now;
this._socketWriter.write(new ProtocolMessage(6 /* ProtocolMessageType.ReplayRequest */, 0, 0, getEmptyBuffer()));
}
}
else {
this._incomingMsgId = msg.id;
this._incomingMsgLastTime = Date.now();
this._sendAckCheck();
this._onMessage.fire(msg.data);
}
}
break;
}
case 2 /* ProtocolMessageType.Control */: {
this._onControlMessage.fire(msg.data);
break;
}
case 3 /* ProtocolMessageType.Ack */: {
// nothing to do, .ack is handled above already
break;
}
case 5 /* ProtocolMessageType.Disconnect */: {
this._onDidDispose.fire();
break;
}
case 6 /* ProtocolMessageType.ReplayRequest */: {
// Send again all unacknowledged messages
const toSend = this._outgoingUnackMsg.toArray();
for (let i = 0, len = toSend.length; i < len; i++) {
this._socketWriter.write(toSend[i]);
}
this._recvAckCheck();
break;
}
case 7 /* ProtocolMessageType.Pause */: {
this._socketWriter.pause();
break;
}
case 8 /* ProtocolMessageType.Resume */: {
this._socketWriter.resume();
break;
}
case 9 /* ProtocolMessageType.KeepAlive */: {
// nothing to do
break;
}
case 10 /* ProtocolMessageType.LatencyMeasurementRequest */: {
// we just send the data back
this._sendLatencyMeasurementResponse(msg.data);
break;
}
case 11 /* ProtocolMessageType.LatencyMeasurementResponse */: {
this._socketLatencyMonitor.handleResponse(msg.data);
break;
}
}
}
readEntireBuffer() {
return this._socketReader.readEntireBuffer();
}
flush() {
this._socketWriter.flush();
}
send(buffer) {
const myId = ++this._outgoingMsgId;
this._incomingAckId = this._incomingMsgId;
const msg = new ProtocolMessage(1 /* ProtocolMessageType.Regular */, myId, this._incomingAckId, buffer);
this._outgoingUnackMsg.push(msg);
if (!this._isReconnecting) {
this._socketWriter.write(msg);
this._recvAckCheck();
}
}
/**
* Send a message which will not be part of the regular acknowledge flow.
* Use this for early control messages which are repeated in case of reconnection.
*/
sendControl(buffer) {
const msg = new ProtocolMessage(2 /* ProtocolMessageType.Control */, 0, 0, buffer);
this._socketWriter.write(msg);
}
_sendAckCheck() {
if (this._incomingMsgId <= this._incomingAckId) {
// nothink to acknowledge
return;
}
if (this._incomingAckTimeout) {
// there will be a check in the near future
return;
}
const timeSinceLastIncomingMsg = Date.now() - this._incomingMsgLastTime;
if (timeSinceLastIncomingMsg >= 2000 /* ProtocolConstants.AcknowledgeTime */) {
// sufficient time has passed since this message has been received,
// and no message from our side needed to be sent in the meantime,
// so we will send a message containing only an ack.
this._sendAck();
return;
}
this._incomingAckTimeout = setTimeout(() => {
this._incomingAckTimeout = null;
this._sendAckCheck();
}, 2000 /* ProtocolConstants.AcknowledgeTime */ - timeSinceLastIncomingMsg + 5);
}
_recvAckCheck() {
if (this._outgoingMsgId <= this._outgoingAckId) {
// everything has been acknowledged
return;
}
if (this._outgoingAckTimeout) {
// there will be a check in the near future
return;
}
if (this._isReconnecting) {
// do not cause a timeout during reconnection,
// because messages will not be actually written until `endAcceptReconnection`
return;
}
const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek();
const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime;
const timeSinceLastReceivedSomeData = Date.now() - this._socketReader.lastReadTime;
const timeSinceLastTimeout = Date.now() - this._lastSocketTimeoutTime;
if (timeSinceOldestUnacknowledgedMsg >= 20000 /* ProtocolConstants.TimeoutTime */
&& timeSinceLastReceivedSomeData >= 20000 /* ProtocolConstants.TimeoutTime */
&& timeSinceLastTimeout >= 20000 /* ProtocolConstants.TimeoutTime */) {
// It's been a long time since our sent message was acknowledged
// and a long time since we received some data
// But this might be caused by the event loop being busy and failing to read messages
if (!this._loadEstimator.hasHighLoad()) {
// Trash the socket
this._lastSocketTimeoutTime = Date.now();
this._onSocketTimeout.fire({
unacknowledgedMsgCount: this._outgoingUnackMsg.length(),
timeSinceOldestUnacknowledgedMsg,
timeSinceLastReceivedSomeData
});
return;
}
}
const minimumTimeUntilTimeout = Math.max(20000 /* ProtocolConstants.TimeoutTime */ - timeSinceOldestUnacknowledgedMsg, 20000 /* ProtocolConstants.TimeoutTime */ - timeSinceLastReceivedSomeData, 20000 /* ProtocolConstants.TimeoutTime */ - timeSinceLastTimeout, 500);
this._outgoingAckTimeout = setTimeout(() => {
this._outgoingAckTimeout = null;
this._recvAckCheck();
}, minimumTimeUntilTimeout);
}
_sendAck() {
if (this._incomingMsgId <= this._incomingAckId) {
// nothink to acknowledge
return;
}
this._incomingAckId = this._incomingMsgId;
const msg = new ProtocolMessage(3 /* ProtocolMessageType.Ack */, 0, this._incomingAckId, getEmptyBuffer());
this._socketWriter.write(msg);
}
_sendKeepAlive() {
this._incomingAckId = this._incomingMsgId;
const msg = new ProtocolMessage(9 /* ProtocolMessageType.KeepAlive */, 0, this._incomingAckId, getEmptyBuffer());
this._socketWriter.write(msg);
}
_sendLatencyMeasurementRequest(buffer) {
this._incomingAckId = this._incomingMsgId;
const msg = new ProtocolMessage(10 /* ProtocolMessageType.LatencyMeasurementRequest */, 0, this._incomingAckId, buffer);
this._socketWriter.write(msg);
}
_sendLatencyMeasurementResponse(buffer) {
this._incomingAckId = this._incomingMsgId;
const msg = new ProtocolMessage(11 /* ProtocolMessageType.LatencyMeasurementResponse */, 0, this._incomingAckId, buffer);
this._socketWriter.write(msg);
}
}
class LatencyMonitor extends Disposable {
_onSendLatencyRequest = this._register(new Emitter());
onSendLatencyRequest = this._onSendLatencyRequest.event;
_onHighRoundTripTime = this._register(new Emitter());
onHighRoundTripTime = this._onHighRoundTripTime.event;
_onDidChangeConnectionHealth = this._register(new Emitter());
onDidChangeConnectionHealth = this._onDidChangeConnectionHealth.event;
_measureLatencyTimer = this._register(new IntervalTimer());
/**
* Timestamp of our last latency request message sent to the other host.
*/
_lastLatencyMeasurementSent = -1;
/**
* ID separate from the regular message IDs. Used to match up latency
* requests with responses so we know we're timing the right message
* even if a reconnection occurs.
*/
_lastLatencyMeasurementId = 0;
/**
* Circular buffer of latency measurements
*/
_latencySamples = Array.from({ length: 5 /* ProtocolConstants.LatencySampleCount */ }, (_) => 0);
_latencySampleIndex = 0;
_connectionHealth = 0 /* ConnectionHealth.Good */;
constructor() {
super();
}
start() {
this._measureLatencyTimer.cancelAndSet(() => {
this._lastLatencyMeasurementSent = Date.now();
const measurementId = ++this._lastLatencyMeasurementId;
const buffer = VSBuffer.alloc(4);
buffer.writeUInt32BE(measurementId, 0);
this._onSendLatencyRequest.fire(buffer);
}, 60000 /* ProtocolConstants.LatencySampleTime */);
}
handleResponse(buffer) {
if (buffer.byteLength !== 4) {
// invalid measurementId
return;
}
const measurementId = buffer.readUInt32BE(0);
if (this._lastLatencyMeasurementSent <= 0 || measurementId !== this._lastLatencyMeasurementId) {
// invalid measurementId
return;
}
const roundtripTime = Date.now() - this._lastLatencyMeasurementSent;
const sampleIndex = this._latencySampleIndex++;
this._latencySamples[sampleIndex % this._latencySamples.length] = roundtripTime;
const previousConnectionHealth = this._connectionHealth;
const highLatencySampleCount = this._latencySamples.filter(s => s >= 1000 /* ProtocolConstants.HighLatencyTimeThreshold */).length;
this._connectionHealth = (highLatencySampleCount >= 3 /* ProtocolConstants.HighLatencySampleThreshold */ ? 1 /* ConnectionHealth.Poor */ : 0 /* ConnectionHealth.Good */);
if (roundtripTime > 1000 /* ProtocolConstants.HighLatencyTimeThreshold */) {
this._onHighRoundTripTime.fire(new HighRoundTripTimeEvent(roundtripTime, highLatencySampleCount));
}
if (previousConnectionHealth !== this._connectionHealth) {
this._onDidChangeConnectionHealth.fire(this._connectionHealth);
}
}
}
// (() => {
// if (!SocketDiagnostics.enableDiagnostics) {
// return;
// }
// if (typeof require.__$__nodeRequire !== 'function') {
// console.log(`Can only log socket diagnostics on native platforms.`);
// return;
// }
// const type = (
// process.argv.includes('--type=renderer')
// ? 'renderer'
// : (process.argv.includes('--type=extensionHost')
// ? 'extensionHost'
// : (process.argv.some(item => item.includes('server-main'))
// ? 'server'
// : 'unknown'
// )
// )
// );
// setTimeout(() => {
// SocketDiagnostics.records.forEach(r => {
// if (r.buff) {
// r.data = Buffer.from(r.buff.buffer).toString('base64');
// r.buff = undefined;
// }
// });
// const fs = <typeof import('fs')>require.__$__nodeRequire('fs');
// const path = <typeof import('path')>require.__$__nodeRequire('path');
// const logPath = path.join(process.cwd(),`${type}-${process.pid}`);
// console.log(`dumping socket diagnostics at ${logPath}`);
// fs.writeFileSync(logPath, JSON.stringify(SocketDiagnostics.records));
// }, 20000);
// })();