Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface ICommonContextMenuItem {
label?: string;
type?: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio';
accelerator?: string;
enabled?: boolean;
visible?: boolean;
checked?: boolean;
}
export interface ISerializableContextMenuItem extends ICommonContextMenuItem {
id: number;
submenu?: ISerializableContextMenuItem[];
}
export interface IContextMenuItem extends ICommonContextMenuItem {
click?: (event: IContextMenuEvent) => void;
submenu?: IContextMenuItem[];
}
export interface IContextMenuEvent {
shiftKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
}
export interface IPopupOptions {
x?: number;
y?: number;
positioningItem?: number;
}
export const CONTEXT_MENU_CHANNEL = 'vscode:contextmenu';
export const CONTEXT_MENU_CLOSE_CHANNEL = 'vscode:onCloseContextMenu';

View File

@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Menu, MenuItem, BrowserWindow, ipcMain, IpcMainEvent } from 'electron';
import { ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions } from 'vs/base/parts/contextmenu/common/contextmenu';
import { withNullAsUndefined } from 'vs/base/common/types';
export function registerContextMenuListener(): void {
ipcMain.on(CONTEXT_MENU_CHANNEL, (event: IpcMainEvent, contextMenuId: number, items: ISerializableContextMenuItem[], onClickChannel: string, options?: IPopupOptions) => {
const menu = createMenu(event, onClickChannel, items);
menu.popup({
window: withNullAsUndefined(BrowserWindow.fromWebContents(event.sender)),
x: options ? options.x : undefined,
y: options ? options.y : undefined,
positioningItem: options ? options.positioningItem : undefined,
callback: () => {
// Workaround for https://github.com/microsoft/vscode/issues/72447
// It turns out that the menu gets GC'ed if not referenced anymore
// As such we drag it into this scope so that it is not being GC'ed
if (menu) {
event.sender.send(CONTEXT_MENU_CLOSE_CHANNEL, contextMenuId);
}
}
});
});
}
function createMenu(event: IpcMainEvent, onClickChannel: string, items: ISerializableContextMenuItem[]): Menu {
const menu = new Menu();
items.forEach(item => {
let menuitem: MenuItem;
// Separator
if (item.type === 'separator') {
menuitem = new MenuItem({
type: item.type,
});
}
// Sub Menu
else if (Array.isArray(item.submenu)) {
menuitem = new MenuItem({
submenu: createMenu(event, onClickChannel, item.submenu),
label: item.label
});
}
// Normal Menu Item
else {
menuitem = new MenuItem({
label: item.label,
type: item.type,
accelerator: item.accelerator,
checked: item.checked,
enabled: item.enabled,
visible: item.visible,
click: (menuItem, win, contextmenuEvent) => event.sender.send(onClickChannel, item.id, contextmenuEvent)
});
}
menu.append(menuitem);
});
return menu;
}

View File

@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { IContextMenuItem, ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions, IContextMenuEvent } from 'vs/base/parts/contextmenu/common/contextmenu';
let contextMenuIdPool = 0;
export function popup(items: IContextMenuItem[], options?: IPopupOptions, onHide?: () => void): void {
const processedItems: IContextMenuItem[] = [];
const contextMenuId = contextMenuIdPool++;
const onClickChannel = `vscode:onContextMenu${contextMenuId}`;
const onClickChannelHandler = (event: unknown, itemId: number, context: IContextMenuEvent) => {
const item = processedItems[itemId];
if (item.click) {
item.click(context);
}
};
ipcRenderer.once(onClickChannel, onClickChannelHandler);
ipcRenderer.once(CONTEXT_MENU_CLOSE_CHANNEL, (event: unknown, closedContextMenuId: number) => {
if (closedContextMenuId !== contextMenuId) {
return;
}
ipcRenderer.removeListener(onClickChannel, onClickChannelHandler);
if (onHide) {
onHide();
}
});
ipcRenderer.send(CONTEXT_MENU_CHANNEL, contextMenuId, items.map(item => createItem(item, processedItems)), onClickChannel, options);
}
function createItem(item: IContextMenuItem, processedItems: IContextMenuItem[]): ISerializableContextMenuItem {
const serializableItem: ISerializableContextMenuItem = {
id: processedItems.length,
label: item.label,
type: item.type,
accelerator: item.accelerator,
checked: item.checked,
enabled: typeof item.enabled === 'boolean' ? item.enabled : true,
visible: typeof item.visible === 'boolean' ? item.visible : true
};
processedItems.push(item);
// Submenu
if (Array.isArray(item.submenu)) {
serializableItem.submenu = item.submenu.map(submenuItem => createItem(submenuItem, processedItems));
}
return serializableItem;
}

View File

@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { Event } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
export interface Sender {
send(channel: string, msg: unknown): void;
}
export class Protocol implements IMessagePassingProtocol {
constructor(private sender: Sender, readonly onMessage: Event<VSBuffer>) { }
send(message: VSBuffer): void {
try {
this.sender.send('vscode:message', message.buffer);
} catch (e) {
// systems are going down
}
}
dispose(): void {
this.sender.send('vscode:disconnect', null);
}
}

View File

@ -0,0 +1,902 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { IMessagePassingProtocol, IPCClient, IIPCLogger } from 'vs/base/parts/ipc/common/ipc';
import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
import * as platform from 'vs/base/common/platform';
import * as process from 'vs/base/common/process';
export interface ISocket extends IDisposable {
onData(listener: (e: VSBuffer) => void): IDisposable;
onClose(listener: () => void): IDisposable;
onEnd(listener: () => void): IDisposable;
write(buffer: VSBuffer): void;
end(): void;
drain(): Promise<void>;
}
let emptyBuffer: VSBuffer | null = null;
function getEmptyBuffer(): VSBuffer {
if (!emptyBuffer) {
emptyBuffer = VSBuffer.alloc(0);
}
return emptyBuffer;
}
export class ChunkStream {
private _chunks: VSBuffer[];
private _totalLength: number;
public get byteLength() {
return this._totalLength;
}
constructor() {
this._chunks = [];
this._totalLength = 0;
}
public acceptChunk(buff: VSBuffer) {
this._chunks.push(buff);
this._totalLength += buff.byteLength;
}
public read(byteCount: number): VSBuffer {
return this._read(byteCount, true);
}
public peek(byteCount: number): VSBuffer {
return this._read(byteCount, false);
}
private _read(byteCount: number, advance: boolean): VSBuffer {
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;
}
let 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;
}
}
const enum ProtocolMessageType {
None = 0,
Regular = 1,
Control = 2,
Ack = 3,
KeepAlive = 4,
Disconnect = 5
}
export const enum ProtocolConstants {
HeaderLength = 13,
/**
* Send an Acknowledge message at most 2 seconds later...
*/
AcknowledgeTime = 2000, // 2 seconds
/**
* If there is a message that has been unacknowledged for 10 seconds, consider the connection closed...
*/
AcknowledgeTimeoutTime = 20000, // 20 seconds
/**
* Send at least a message every 5s for keep alive reasons.
*/
KeepAliveTime = 5000, // 5 seconds
/**
* If there is no message received for 10 seconds, consider the connection closed...
*/
KeepAliveTimeoutTime = 20000, // 20 seconds
/**
* If there is no reconnection within this time-frame, consider the connection permanently closed...
*/
ReconnectionGraceTime = 3 * 60 * 60 * 1000, // 3hrs
/**
* Maximal grace time between the first and the last reconnection...
*/
ReconnectionShortGraceTime = 5 * 60 * 1000, // 5min
}
class ProtocolMessage {
public writtenTime: number;
constructor(
public readonly type: ProtocolMessageType,
public readonly id: number,
public readonly ack: number,
public readonly data: VSBuffer
) {
this.writtenTime = 0;
}
public get size(): number {
return this.data.byteLength;
}
}
class ProtocolReader extends Disposable {
private readonly _socket: ISocket;
private _isDisposed: boolean;
private readonly _incomingData: ChunkStream;
public lastReadTime: number;
private readonly _onMessage = this._register(new Emitter<ProtocolMessage>());
public readonly onMessage: Event<ProtocolMessage> = this._onMessage.event;
private readonly _state = {
readHead: true,
readLen: ProtocolConstants.HeaderLength,
messageType: ProtocolMessageType.None,
id: 0,
ack: 0
};
constructor(socket: ISocket) {
super();
this._socket = socket;
this._isDisposed = false;
this._incomingData = new ChunkStream();
this._register(this._socket.onData(data => this.acceptChunk(data)));
this.lastReadTime = Date.now();
}
public acceptChunk(data: VSBuffer | null): void {
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);
} 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 = ProtocolConstants.HeaderLength;
this._state.messageType = ProtocolMessageType.None;
this._state.id = 0;
this._state.ack = 0;
this._onMessage.fire(new ProtocolMessage(messageType, id, ack, buff));
if (this._isDisposed) {
// check if an event listener lead to our disposal
break;
}
}
}
}
public readEntireBuffer(): VSBuffer {
return this._incomingData.read(this._incomingData.byteLength);
}
public dispose(): void {
this._isDisposed = true;
super.dispose();
}
}
class ProtocolWriter {
private _isDisposed: boolean;
private readonly _socket: ISocket;
private _data: VSBuffer[];
private _totalLength: number;
public lastWriteTime: number;
constructor(socket: ISocket) {
this._isDisposed = false;
this._socket = socket;
this._data = [];
this._totalLength = 0;
this.lastWriteTime = 0;
}
public dispose(): void {
this.flush();
this._isDisposed = true;
}
public drain(): Promise<void> {
this.flush();
return this._socket.drain();
}
public flush(): void {
// flush
this._writeNow();
}
public write(msg: ProtocolMessage) {
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(ProtocolConstants.HeaderLength);
header.writeUInt8(msg.type, 0);
header.writeUInt32BE(msg.id, 1);
header.writeUInt32BE(msg.ack, 5);
header.writeUInt32BE(msg.data.byteLength, 9);
this._writeSoon(header, msg.data);
}
private _bufferAdd(head: VSBuffer, body: VSBuffer): boolean {
const wasEmpty = this._totalLength === 0;
this._data.push(head, body);
this._totalLength += head.byteLength + body.byteLength;
return wasEmpty;
}
private _bufferTake(): VSBuffer {
const ret = VSBuffer.concat(this._data, this._totalLength);
this._data.length = 0;
this._totalLength = 0;
return ret;
}
private _writeSoon(header: VSBuffer, data: VSBuffer): void {
if (this._bufferAdd(header, data)) {
platform.setImmediate(() => {
this._writeNow();
});
}
}
private _writeNow(): void {
if (this._totalLength === 0) {
return;
}
this._socket.write(this._bufferTake());
}
}
/**
* 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 implements IMessagePassingProtocol {
private _socket: ISocket;
private _socketWriter: ProtocolWriter;
private _socketReader: ProtocolReader;
private readonly _onMessage = new Emitter<VSBuffer>();
readonly onMessage: Event<VSBuffer> = this._onMessage.event;
private readonly _onClose = new Emitter<void>();
readonly onClose: Event<void> = this._onClose.event;
constructor(socket: ISocket) {
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 === ProtocolMessageType.Regular) {
this._onMessage.fire(msg.data);
}
}));
this._register(this._socket.onClose(() => this._onClose.fire()));
}
drain(): Promise<void> {
return this._socketWriter.drain();
}
getSocket(): ISocket {
return this._socket;
}
sendDisconnect(): void {
// Nothing to do...
}
send(buffer: VSBuffer): void {
this._socketWriter.write(new ProtocolMessage(ProtocolMessageType.Regular, 0, 0, buffer));
}
}
export class Client<TContext = string> extends IPCClient<TContext> {
static fromSocket<TContext = string>(socket: ISocket, id: TContext): Client<TContext> {
return new Client(new Protocol(socket), id);
}
get onClose(): Event<void> { return this.protocol.onClose; }
constructor(private protocol: Protocol | PersistentProtocol, id: TContext, ipcLogger: IIPCLogger | null = null) {
super(protocol, id, ipcLogger);
}
dispose(): void {
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<T> {
private _emitter: Emitter<T>;
public readonly event: Event<T>;
private _hasListeners = false;
private _isDeliveringMessages = false;
private _bufferedMessages: T[] = [];
constructor() {
this._emitter = new Emitter<T>({
onFirstListenerAdd: () => {
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 nextTick and not other types of timeouts
process.nextTick(() => this._deliverMessages());
},
onLastListenerRemove: () => {
this._hasListeners = false;
}
});
this.event = this._emitter.event;
}
private _deliverMessages(): void {
if (this._isDeliveringMessages) {
return;
}
this._isDeliveringMessages = true;
while (this._hasListeners && this._bufferedMessages.length > 0) {
this._emitter.fire(this._bufferedMessages.shift()!);
}
this._isDeliveringMessages = false;
}
public fire(event: T): void {
if (this._hasListeners) {
if (this._bufferedMessages.length > 0) {
this._bufferedMessages.push(event);
} else {
this._emitter.fire(event);
}
} else {
this._bufferedMessages.push(event);
}
}
public flushBuffer(): void {
this._bufferedMessages = [];
}
}
class QueueElement<T> {
public readonly data: T;
public next: QueueElement<T> | null;
constructor(data: T) {
this.data = data;
this.next = null;
}
}
class Queue<T> {
private _first: QueueElement<T> | null;
private _last: QueueElement<T> | null;
constructor() {
this._first = null;
this._last = null;
}
public peek(): T | null {
if (!this._first) {
return null;
}
return this._first.data;
}
public toArray(): T[] {
let result: T[] = [], resultLen = 0;
let it = this._first;
while (it) {
result[resultLen++] = it.data;
it = it.next;
}
return result;
}
public pop(): void {
if (!this._first) {
return;
}
if (this._first === this._last) {
this._first = null;
this._last = null;
return;
}
this._first = this._first.next;
}
public push(item: T): void {
const element = new QueueElement(item);
if (!this._first) {
this._first = element;
this._last = element;
return;
}
this._last!.next = element;
this._last = element;
}
}
class LoadEstimator {
private static _HISTORY_LENGTH = 10;
private static _INSTANCE: LoadEstimator | null = null;
public static getInstance(): LoadEstimator {
if (!LoadEstimator._INSTANCE) {
LoadEstimator._INSTANCE = new LoadEstimator();
}
return LoadEstimator._INSTANCE;
}
private lastRuns: number[];
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)
*/
public load(): number {
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;
}
public hasHighLoad(): boolean {
return this.load() >= 0.5;
}
}
/**
* 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 implements IMessagePassingProtocol {
private _isReconnecting: boolean;
private _outgoingUnackMsg: Queue<ProtocolMessage>;
private _outgoingMsgId: number;
private _outgoingAckId: number;
private _outgoingAckTimeout: any | null;
private _incomingMsgId: number;
private _incomingAckId: number;
private _incomingMsgLastTime: number;
private _incomingAckTimeout: any | null;
private _outgoingKeepAliveTimeout: any | null;
private _incomingKeepAliveTimeout: any | null;
private _socket: ISocket;
private _socketWriter: ProtocolWriter;
private _socketReader: ProtocolReader;
private _socketDisposables: IDisposable[];
private readonly _loadEstimator = LoadEstimator.getInstance();
private readonly _onControlMessage = new BufferedEmitter<VSBuffer>();
readonly onControlMessage: Event<VSBuffer> = this._onControlMessage.event;
private readonly _onMessage = new BufferedEmitter<VSBuffer>();
readonly onMessage: Event<VSBuffer> = this._onMessage.event;
private readonly _onClose = new BufferedEmitter<void>();
readonly onClose: Event<void> = this._onClose.event;
private readonly _onSocketClose = new BufferedEmitter<void>();
readonly onSocketClose: Event<void> = this._onSocketClose.event;
private readonly _onSocketTimeout = new BufferedEmitter<void>();
readonly onSocketTimeout: Event<void> = this._onSocketTimeout.event;
public get unacknowledgedCount(): number {
return this._outgoingMsgId - this._outgoingAckId;
}
constructor(socket: ISocket, initialChunk: VSBuffer | null = null) {
this._isReconnecting = false;
this._outgoingUnackMsg = new Queue<ProtocolMessage>();
this._outgoingMsgId = 0;
this._outgoingAckId = 0;
this._outgoingAckTimeout = null;
this._incomingMsgId = 0;
this._incomingAckId = 0;
this._incomingMsgLastTime = 0;
this._incomingAckTimeout = null;
this._outgoingKeepAliveTimeout = null;
this._incomingKeepAliveTimeout = null;
this._socketDisposables = [];
this._socket = socket;
this._socketWriter = new ProtocolWriter(this._socket);
this._socketDisposables.push(this._socketWriter);
this._socketReader = new ProtocolReader(this._socket);
this._socketDisposables.push(this._socketReader);
this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg)));
this._socketDisposables.push(this._socket.onClose(() => this._onSocketClose.fire()));
if (initialChunk) {
this._socketReader.acceptChunk(initialChunk);
}
this._sendKeepAliveCheck();
this._recvKeepAliveCheck();
}
dispose(): void {
if (this._outgoingAckTimeout) {
clearTimeout(this._outgoingAckTimeout);
this._outgoingAckTimeout = null;
}
if (this._incomingAckTimeout) {
clearTimeout(this._incomingAckTimeout);
this._incomingAckTimeout = null;
}
if (this._outgoingKeepAliveTimeout) {
clearTimeout(this._outgoingKeepAliveTimeout);
this._outgoingKeepAliveTimeout = null;
}
if (this._incomingKeepAliveTimeout) {
clearTimeout(this._incomingKeepAliveTimeout);
this._incomingKeepAliveTimeout = null;
}
this._socketDisposables = dispose(this._socketDisposables);
}
drain(): Promise<void> {
return this._socketWriter.drain();
}
sendDisconnect(): void {
const msg = new ProtocolMessage(ProtocolMessageType.Disconnect, 0, 0, getEmptyBuffer());
this._socketWriter.write(msg);
this._socketWriter.flush();
}
private _sendKeepAliveCheck(): void {
if (this._outgoingKeepAliveTimeout) {
// there will be a check in the near future
return;
}
const timeSinceLastOutgoingMsg = Date.now() - this._socketWriter.lastWriteTime;
if (timeSinceLastOutgoingMsg >= ProtocolConstants.KeepAliveTime) {
// sufficient time has passed since last message was written,
// and no message from our side needed to be sent in the meantime,
// so we will send a message containing only a keep alive.
const msg = new ProtocolMessage(ProtocolMessageType.KeepAlive, 0, 0, getEmptyBuffer());
this._socketWriter.write(msg);
this._sendKeepAliveCheck();
return;
}
this._outgoingKeepAliveTimeout = setTimeout(() => {
this._outgoingKeepAliveTimeout = null;
this._sendKeepAliveCheck();
}, ProtocolConstants.KeepAliveTime - timeSinceLastOutgoingMsg + 5);
}
private _recvKeepAliveCheck(): void {
if (this._incomingKeepAliveTimeout) {
// there will be a check in the near future
return;
}
const timeSinceLastIncomingMsg = Date.now() - this._socketReader.lastReadTime;
if (timeSinceLastIncomingMsg >= ProtocolConstants.KeepAliveTimeoutTime) {
// It's been a long time since we received a server message
// But this might be caused by the event loop being busy and failing to read messages
if (!this._loadEstimator.hasHighLoad()) {
// Trash the socket
this._onSocketTimeout.fire(undefined);
return;
}
}
this._incomingKeepAliveTimeout = setTimeout(() => {
this._incomingKeepAliveTimeout = null;
this._recvKeepAliveCheck();
}, Math.max(ProtocolConstants.KeepAliveTimeoutTime - timeSinceLastIncomingMsg, 0) + 5);
}
public getSocket(): ISocket {
return this._socket;
}
public beginAcceptReconnection(socket: ISocket, initialDataChunk: VSBuffer | null): void {
this._isReconnecting = true;
this._socketDisposables = dispose(this._socketDisposables);
this._onControlMessage.flushBuffer();
this._onSocketClose.flushBuffer();
this._onSocketTimeout.flushBuffer();
this._socket.dispose();
this._socket = socket;
this._socketWriter = new ProtocolWriter(this._socket);
this._socketDisposables.push(this._socketWriter);
this._socketReader = new ProtocolReader(this._socket);
this._socketDisposables.push(this._socketReader);
this._socketDisposables.push(this._socketReader.onMessage(msg => this._receiveMessage(msg)));
this._socketDisposables.push(this._socket.onClose(() => this._onSocketClose.fire()));
this._socketReader.acceptChunk(initialDataChunk);
}
public endAcceptReconnection(): void {
this._isReconnecting = false;
// 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();
this._sendKeepAliveCheck();
this._recvKeepAliveCheck();
}
public acceptDisconnect(): void {
this._onClose.fire();
}
private _receiveMessage(msg: ProtocolMessage): void {
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);
}
if (msg.type === ProtocolMessageType.Regular) {
if (msg.id > this._incomingMsgId) {
if (msg.id !== this._incomingMsgId + 1) {
console.error(`PROTOCOL CORRUPTION, LAST SAW MSG ${this._incomingMsgId} AND HAVE NOW RECEIVED MSG ${msg.id}`);
}
this._incomingMsgId = msg.id;
this._incomingMsgLastTime = Date.now();
this._sendAckCheck();
this._onMessage.fire(msg.data);
}
} else if (msg.type === ProtocolMessageType.Control) {
this._onControlMessage.fire(msg.data);
} else if (msg.type === ProtocolMessageType.Disconnect) {
this._onClose.fire();
}
}
readEntireBuffer(): VSBuffer {
return this._socketReader.readEntireBuffer();
}
flush(): void {
this._socketWriter.flush();
}
send(buffer: VSBuffer): void {
const myId = ++this._outgoingMsgId;
this._incomingAckId = this._incomingMsgId;
const msg = new ProtocolMessage(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: VSBuffer): void {
const msg = new ProtocolMessage(ProtocolMessageType.Control, 0, 0, buffer);
this._socketWriter.write(msg);
}
private _sendAckCheck(): void {
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 >= 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();
}, ProtocolConstants.AcknowledgeTime - timeSinceLastIncomingMsg + 5);
}
private _recvAckCheck(): void {
if (this._outgoingMsgId <= this._outgoingAckId) {
// everything has been acknowledged
return;
}
if (this._outgoingAckTimeout) {
// there will be a check in the near future
return;
}
const oldestUnacknowledgedMsg = this._outgoingUnackMsg.peek()!;
const timeSinceOldestUnacknowledgedMsg = Date.now() - oldestUnacknowledgedMsg.writtenTime;
if (timeSinceOldestUnacknowledgedMsg >= ProtocolConstants.AcknowledgeTimeoutTime) {
// It's been a long time since our sent message was acknowledged
// But this might be caused by the event loop being busy and failing to read messages
if (!this._loadEstimator.hasHighLoad()) {
// Trash the socket
this._onSocketTimeout.fire(undefined);
return;
}
}
this._outgoingAckTimeout = setTimeout(() => {
this._outgoingAckTimeout = null;
this._recvAckCheck();
}, Math.max(ProtocolConstants.AcknowledgeTimeoutTime - timeSinceOldestUnacknowledgedMsg, 0) + 5);
}
private _sendAck(): void {
if (this._incomingMsgId <= this._incomingAckId) {
// nothink to acknowledge
return;
}
this._incomingAckId = this._incomingMsgId;
const msg = new ProtocolMessage(ProtocolMessageType.Ack, 0, this._incomingAckId, getEmptyBuffer());
this._socketWriter.write(msg);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc';
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
import { ipcMain, WebContents } from 'electron';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
interface IIPCEvent {
event: { sender: WebContents; };
message: Buffer | null;
}
function createScopedOnMessageEvent(senderId: number, eventName: string): Event<VSBuffer | null> {
const onMessage = Event.fromNodeEventEmitter<IIPCEvent>(ipcMain, eventName, (event, message) => ({ event, message }));
const onMessageFromSender = Event.filter(onMessage, ({ event }) => event.sender.id === senderId);
return Event.map(onMessageFromSender, ({ message }) => message ? VSBuffer.wrap(message) : message);
}
export class Server extends IPCServer {
private static readonly Clients = new Map<number, IDisposable>();
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
const onHello = Event.fromNodeEventEmitter<WebContents>(ipcMain, 'vscode:hello', ({ sender }) => sender);
return Event.map(onHello, webContents => {
const id = webContents.id;
const client = Server.Clients.get(id);
if (client) {
client.dispose();
}
const onDidClientReconnect = new Emitter<void>();
Server.Clients.set(id, toDisposable(() => onDidClientReconnect.fire()));
const onMessage = createScopedOnMessageEvent(id, 'vscode:message') as Event<VSBuffer>;
const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'vscode:disconnect')), onDidClientReconnect.event);
const protocol = new Protocol(webContents, onMessage);
return { protocol, onDidClientDisconnect };
});
}
constructor() {
super(Server.getOnDidClientConnect());
}
}

View File

@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { IPCClient } from 'vs/base/parts/ipc/common/ipc';
import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron';
import { IDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
export class Client extends IPCClient implements IDisposable {
private protocol: Protocol;
private static createProtocol(): Protocol {
const onMessage = Event.fromNodeEventEmitter<VSBuffer>(ipcRenderer, 'vscode:message', (_, message) => VSBuffer.wrap(message));
ipcRenderer.send('vscode:hello');
return new Protocol(ipcRenderer, onMessage);
}
constructor(id: string) {
const protocol = Client.createProtocol();
super(protocol, id);
this.protocol = protocol;
}
dispose(): void {
this.protocol.dispose();
}
}

View File

@ -0,0 +1,288 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ChildProcess, fork, ForkOptions } from 'child_process';
import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
import { Delayer, createCancelablePromise } from 'vs/base/common/async';
import { deepClone } from 'vs/base/common/objects';
import { Emitter, Event } from 'vs/base/common/event';
import { createQueuedSender } from 'vs/base/node/processes';
import { IChannel, ChannelServer as IPCServer, ChannelClient as IPCClient, IChannelClient } from 'vs/base/parts/ipc/common/ipc';
import { isRemoteConsoleLog, log } from 'vs/base/common/console';
import { CancellationToken } from 'vs/base/common/cancellation';
import * as errors from 'vs/base/common/errors';
import { VSBuffer } from 'vs/base/common/buffer';
import { isMacintosh } from 'vs/base/common/platform';
/**
* This implementation doesn't perform well since it uses base64 encoding for buffers.
* We should move all implementations to use named ipc.net, so we stop depending on cp.fork.
*/
export class Server<TContext extends string> extends IPCServer<TContext> {
constructor(ctx: TContext) {
super({
send: r => {
try {
if (process.send) {
process.send((<Buffer>r.buffer).toString('base64'));
}
} catch (e) { /* not much to do */ }
},
onMessage: Event.fromNodeEventEmitter(process, 'message', msg => VSBuffer.wrap(Buffer.from(msg, 'base64')))
}, ctx);
process.once('disconnect', () => this.dispose());
}
}
export interface IIPCOptions {
/**
* A descriptive name for the server this connection is to. Used in logging.
*/
serverName: string;
/**
* Time in millies before killing the ipc process. The next request after killing will start it again.
*/
timeout?: number;
/**
* Arguments to the module to execute.
*/
args?: string[];
/**
* Environment key-value pairs to be passed to the process that gets spawned for the ipc.
*/
env?: any;
/**
* Allows to assign a debug port for debugging the application executed.
*/
debug?: number;
/**
* Allows to assign a debug port for debugging the application and breaking it on the first line.
*/
debugBrk?: number;
/**
* See https://github.com/microsoft/vscode/issues/27665
* Allows to pass in fresh execArgv to the forked process such that it doesn't inherit them from `process.execArgv`.
* e.g. Launching the extension host process with `--inspect-brk=xxx` and then forking a process from the extension host
* results in the forked process inheriting `--inspect-brk=xxx`.
*/
freshExecArgv?: boolean;
/**
* Enables our createQueuedSender helper for this Client. Uses a queue when the internal Node.js queue is
* full of messages - see notes on that method.
*/
useQueue?: boolean;
}
export class Client implements IChannelClient, IDisposable {
private disposeDelayer: Delayer<void> | undefined;
private activeRequests = new Set<IDisposable>();
private child: ChildProcess | null;
private _client: IPCClient | null;
private channels = new Map<string, IChannel>();
private readonly _onDidProcessExit = new Emitter<{ code: number, signal: string }>();
readonly onDidProcessExit = this._onDidProcessExit.event;
constructor(private modulePath: string, private options: IIPCOptions) {
const timeout = options && options.timeout ? options.timeout : 60000;
this.disposeDelayer = new Delayer<void>(timeout);
this.child = null;
this._client = null;
}
getChannel<T extends IChannel>(channelName: string): T {
const that = this;
return {
call<T>(command: string, arg?: any, cancellationToken?: CancellationToken): Promise<T> {
return that.requestPromise<T>(channelName, command, arg, cancellationToken);
},
listen(event: string, arg?: any) {
return that.requestEvent(channelName, event, arg);
}
} as T;
}
protected requestPromise<T>(channelName: string, name: string, arg?: any, cancellationToken = CancellationToken.None): Promise<T> {
if (!this.disposeDelayer) {
return Promise.reject(new Error('disposed'));
}
if (cancellationToken.isCancellationRequested) {
return Promise.reject(errors.canceled());
}
this.disposeDelayer.cancel();
const channel = this.getCachedChannel(channelName);
const result = createCancelablePromise(token => channel.call<T>(name, arg, token));
const cancellationTokenListener = cancellationToken.onCancellationRequested(() => result.cancel());
const disposable = toDisposable(() => result.cancel());
this.activeRequests.add(disposable);
result.finally(() => {
cancellationTokenListener.dispose();
this.activeRequests.delete(disposable);
if (this.activeRequests.size === 0 && this.disposeDelayer) {
this.disposeDelayer.trigger(() => this.disposeClient());
}
});
return result;
}
protected requestEvent<T>(channelName: string, name: string, arg?: any): Event<T> {
if (!this.disposeDelayer) {
return Event.None;
}
this.disposeDelayer.cancel();
let listener: IDisposable;
const emitter = new Emitter<any>({
onFirstListenerAdd: () => {
const channel = this.getCachedChannel(channelName);
const event: Event<T> = channel.listen(name, arg);
listener = event(emitter.fire, emitter);
this.activeRequests.add(listener);
},
onLastListenerRemove: () => {
this.activeRequests.delete(listener);
listener.dispose();
if (this.activeRequests.size === 0 && this.disposeDelayer) {
this.disposeDelayer.trigger(() => this.disposeClient());
}
}
});
return emitter.event;
}
private get client(): IPCClient {
if (!this._client) {
const args = this.options && this.options.args ? this.options.args : [];
const forkOpts: ForkOptions = Object.create(null);
forkOpts.env = { ...deepClone(process.env), 'VSCODE_PARENT_PID': String(process.pid) };
if (this.options && this.options.env) {
forkOpts.env = { ...forkOpts.env, ...this.options.env };
}
if (this.options && this.options.freshExecArgv) {
forkOpts.execArgv = [];
}
if (this.options && typeof this.options.debug === 'number') {
forkOpts.execArgv = ['--nolazy', '--inspect=' + this.options.debug];
}
if (this.options && typeof this.options.debugBrk === 'number') {
forkOpts.execArgv = ['--nolazy', '--inspect-brk=' + this.options.debugBrk];
}
if (isMacintosh && forkOpts.env) {
// Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes
// See https://github.com/microsoft/vscode/issues/105848
delete forkOpts.env['DYLD_LIBRARY_PATH'];
}
this.child = fork(this.modulePath, args, forkOpts);
const onMessageEmitter = new Emitter<VSBuffer>();
const onRawMessage = Event.fromNodeEventEmitter(this.child, 'message', msg => msg);
onRawMessage(msg => {
// Handle remote console logs specially
if (isRemoteConsoleLog(msg)) {
log(msg, `IPC Library: ${this.options.serverName}`);
return;
}
// Anything else goes to the outside
onMessageEmitter.fire(VSBuffer.wrap(Buffer.from(msg, 'base64')));
});
const sender = this.options.useQueue ? createQueuedSender(this.child) : this.child;
const send = (r: VSBuffer) => this.child && this.child.connected && sender.send((<Buffer>r.buffer).toString('base64'));
const onMessage = onMessageEmitter.event;
const protocol = { send, onMessage };
this._client = new IPCClient(protocol);
const onExit = () => this.disposeClient();
process.once('exit', onExit);
this.child.on('error', err => console.warn('IPC "' + this.options.serverName + '" errored with ' + err));
this.child.on('exit', (code: any, signal: any) => {
process.removeListener('exit' as 'loaded', onExit); // https://github.com/electron/electron/issues/21475
this.activeRequests.forEach(r => dispose(r));
this.activeRequests.clear();
if (code !== 0 && signal !== 'SIGTERM') {
console.warn('IPC "' + this.options.serverName + '" crashed with exit code ' + code + ' and signal ' + signal);
}
if (this.disposeDelayer) {
this.disposeDelayer.cancel();
}
this.disposeClient();
this._onDidProcessExit.fire({ code, signal });
});
}
return this._client;
}
private getCachedChannel(name: string): IChannel {
let channel = this.channels.get(name);
if (!channel) {
channel = this.client.getChannel(name);
this.channels.set(name, channel);
}
return channel;
}
private disposeClient() {
if (this._client) {
if (this.child) {
this.child.kill();
this.child = null;
}
this._client = null;
this.channels.clear();
}
}
dispose() {
this._onDidProcessExit.dispose();
if (this.disposeDelayer) {
this.disposeDelayer.cancel();
this.disposeDelayer = undefined;
}
this.disposeClient();
this.activeRequests.clear();
}
}

View File

@ -0,0 +1,418 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createHash } from 'crypto';
import { Socket, Server as NetServer, createConnection, createServer } from 'net';
import { Event, Emitter } from 'vs/base/common/event';
import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { generateUuid } from 'vs/base/common/uuid';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
import { ISocket, Protocol, Client, ChunkStream } from 'vs/base/parts/ipc/common/ipc.net';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Platform, platform } from 'vs/base/common/platform';
export class NodeSocket implements ISocket {
public readonly socket: Socket;
constructor(socket: Socket) {
this.socket = socket;
}
public dispose(): void {
this.socket.destroy();
}
public onData(_listener: (e: VSBuffer) => void): IDisposable {
const listener = (buff: Buffer) => _listener(VSBuffer.wrap(buff));
this.socket.on('data', listener);
return {
dispose: () => this.socket.off('data', listener)
};
}
public onClose(listener: () => void): IDisposable {
this.socket.on('close', listener);
return {
dispose: () => this.socket.off('close', listener)
};
}
public onEnd(listener: () => void): IDisposable {
this.socket.on('end', listener);
return {
dispose: () => this.socket.off('end', listener)
};
}
public write(buffer: VSBuffer): void {
// return early if socket has been destroyed in the meantime
if (this.socket.destroyed) {
return;
}
// we ignore the returned value from `write` because we would have to cached the data
// anyways and nodejs is already doing that for us:
// > https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback
// > However, the false return value is only advisory and the writable stream will unconditionally
// > accept and buffer chunk even if it has not been allowed to drain.
try {
this.socket.write(<Buffer>buffer.buffer);
} catch (err) {
if (err.code === 'EPIPE') {
// An EPIPE exception at the wrong time can lead to a renderer process crash
// so ignore the error since the socket will fire the close event soon anyways:
// > https://nodejs.org/api/errors.html#errors_common_system_errors
// > EPIPE (Broken pipe): A write on a pipe, socket, or FIFO for which there is no
// > process to read the data. Commonly encountered at the net and http layers,
// > indicative that the remote side of the stream being written to has been closed.
return;
}
onUnexpectedError(err);
}
}
public end(): void {
this.socket.end();
}
public drain(): Promise<void> {
return new Promise<void>((resolve, reject) => {
if (this.socket.bufferSize === 0) {
resolve();
return;
}
const finished = () => {
this.socket.off('close', finished);
this.socket.off('end', finished);
this.socket.off('error', finished);
this.socket.off('timeout', finished);
this.socket.off('drain', finished);
resolve();
};
this.socket.on('close', finished);
this.socket.on('end', finished);
this.socket.on('error', finished);
this.socket.on('timeout', finished);
this.socket.on('drain', finished);
});
}
}
const enum Constants {
MinHeaderByteSize = 2
}
const enum ReadState {
PeekHeader = 1,
ReadHeader = 2,
ReadBody = 3,
Fin = 4
}
/**
* See https://tools.ietf.org/html/rfc6455#section-5.2
*/
export class WebSocketNodeSocket extends Disposable implements ISocket {
public readonly socket: NodeSocket;
private readonly _incomingData: ChunkStream;
private readonly _onData = this._register(new Emitter<VSBuffer>());
private readonly _state = {
state: ReadState.PeekHeader,
readLen: Constants.MinHeaderByteSize,
mask: 0
};
constructor(socket: NodeSocket) {
super();
this.socket = socket;
this._incomingData = new ChunkStream();
this._register(this.socket.onData(data => this._acceptChunk(data)));
}
public dispose(): void {
this.socket.dispose();
}
public onData(listener: (e: VSBuffer) => void): IDisposable {
return this._onData.event(listener);
}
public onClose(listener: () => void): IDisposable {
return this.socket.onClose(listener);
}
public onEnd(listener: () => void): IDisposable {
return this.socket.onEnd(listener);
}
public write(buffer: VSBuffer): void {
let headerLen = Constants.MinHeaderByteSize;
if (buffer.byteLength < 126) {
headerLen += 0;
} else if (buffer.byteLength < 2 ** 16) {
headerLen += 2;
} else {
headerLen += 8;
}
const header = VSBuffer.alloc(headerLen);
header.writeUInt8(0b10000010, 0);
if (buffer.byteLength < 126) {
header.writeUInt8(buffer.byteLength, 1);
} else if (buffer.byteLength < 2 ** 16) {
header.writeUInt8(126, 1);
let offset = 1;
header.writeUInt8((buffer.byteLength >>> 8) & 0b11111111, ++offset);
header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
} else {
header.writeUInt8(127, 1);
let offset = 1;
header.writeUInt8(0, ++offset);
header.writeUInt8(0, ++offset);
header.writeUInt8(0, ++offset);
header.writeUInt8(0, ++offset);
header.writeUInt8((buffer.byteLength >>> 24) & 0b11111111, ++offset);
header.writeUInt8((buffer.byteLength >>> 16) & 0b11111111, ++offset);
header.writeUInt8((buffer.byteLength >>> 8) & 0b11111111, ++offset);
header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
}
this.socket.write(VSBuffer.concat([header, buffer]));
}
public end(): void {
this.socket.end();
}
private _acceptChunk(data: VSBuffer): void {
if (data.byteLength === 0) {
return;
}
this._incomingData.acceptChunk(data);
while (this._incomingData.byteLength >= this._state.readLen) {
if (this._state.state === ReadState.PeekHeader) {
// peek to see if we can read the entire header
const peekHeader = this._incomingData.peek(this._state.readLen);
// const firstByte = peekHeader.readUInt8(0);
// const finBit = (firstByte & 0b10000000) >>> 7;
const secondByte = peekHeader.readUInt8(1);
const hasMask = (secondByte & 0b10000000) >>> 7;
const len = (secondByte & 0b01111111);
this._state.state = ReadState.ReadHeader;
this._state.readLen = Constants.MinHeaderByteSize + (hasMask ? 4 : 0) + (len === 126 ? 2 : 0) + (len === 127 ? 8 : 0);
this._state.mask = 0;
} else if (this._state.state === ReadState.ReadHeader) {
// read entire header
const header = this._incomingData.read(this._state.readLen);
const secondByte = header.readUInt8(1);
const hasMask = (secondByte & 0b10000000) >>> 7;
let len = (secondByte & 0b01111111);
let offset = 1;
if (len === 126) {
len = (
header.readUInt8(++offset) * 2 ** 8
+ header.readUInt8(++offset)
);
} else if (len === 127) {
len = (
header.readUInt8(++offset) * 0
+ header.readUInt8(++offset) * 0
+ header.readUInt8(++offset) * 0
+ header.readUInt8(++offset) * 0
+ header.readUInt8(++offset) * 2 ** 24
+ header.readUInt8(++offset) * 2 ** 16
+ header.readUInt8(++offset) * 2 ** 8
+ header.readUInt8(++offset)
);
}
let mask = 0;
if (hasMask) {
mask = (
header.readUInt8(++offset) * 2 ** 24
+ header.readUInt8(++offset) * 2 ** 16
+ header.readUInt8(++offset) * 2 ** 8
+ header.readUInt8(++offset)
);
}
this._state.state = ReadState.ReadBody;
this._state.readLen = len;
this._state.mask = mask;
} else if (this._state.state === ReadState.ReadBody) {
// read body
const body = this._incomingData.read(this._state.readLen);
unmask(body, this._state.mask);
this._state.state = ReadState.PeekHeader;
this._state.readLen = Constants.MinHeaderByteSize;
this._state.mask = 0;
this._onData.fire(body);
}
}
}
public drain(): Promise<void> {
return this.socket.drain();
}
}
function unmask(buffer: VSBuffer, mask: number): void {
if (mask === 0) {
return;
}
let cnt = buffer.byteLength >>> 2;
for (let i = 0; i < cnt; i++) {
const v = buffer.readUInt32BE(i * 4);
buffer.writeUInt32BE(v ^ mask, i * 4);
}
let offset = cnt * 4;
let bytesLeft = buffer.byteLength - offset;
const m3 = (mask >>> 24) & 0b11111111;
const m2 = (mask >>> 16) & 0b11111111;
const m1 = (mask >>> 8) & 0b11111111;
if (bytesLeft >= 1) {
buffer.writeUInt8(buffer.readUInt8(offset) ^ m3, offset);
}
if (bytesLeft >= 2) {
buffer.writeUInt8(buffer.readUInt8(offset + 1) ^ m2, offset + 1);
}
if (bytesLeft >= 3) {
buffer.writeUInt8(buffer.readUInt8(offset + 2) ^ m1, offset + 2);
}
}
// Read this before there's any chance it is overwritten
// Related to https://github.com/microsoft/vscode/issues/30624
export const XDG_RUNTIME_DIR = <string | undefined>process.env['XDG_RUNTIME_DIR'];
const safeIpcPathLengths: { [platform: number]: number } = {
[Platform.Linux]: 107,
[Platform.Mac]: 103
};
export function createRandomIPCHandle(): string {
const randomSuffix = generateUuid();
// Windows: use named pipe
if (process.platform === 'win32') {
return `\\\\.\\pipe\\vscode-ipc-${randomSuffix}-sock`;
}
// Mac/Unix: use socket file and prefer
// XDG_RUNTIME_DIR over tmpDir
let result: string;
if (XDG_RUNTIME_DIR) {
result = join(XDG_RUNTIME_DIR, `vscode-ipc-${randomSuffix}.sock`);
} else {
result = join(tmpdir(), `vscode-ipc-${randomSuffix}.sock`);
}
// Validate length
validateIPCHandleLength(result);
return result;
}
export function createStaticIPCHandle(directoryPath: string, type: string, version: string): string {
const scope = createHash('md5').update(directoryPath).digest('hex');
// Windows: use named pipe
if (process.platform === 'win32') {
return `\\\\.\\pipe\\${scope}-${version}-${type}-sock`;
}
// Mac/Unix: use socket file and prefer
// XDG_RUNTIME_DIR over user data path
// unless portable
let result: string;
if (XDG_RUNTIME_DIR && !process.env['VSCODE_PORTABLE']) {
result = join(XDG_RUNTIME_DIR, `vscode-${scope.substr(0, 8)}-${version}-${type}.sock`);
} else {
result = join(directoryPath, `${version}-${type}.sock`);
}
// Validate length
validateIPCHandleLength(result);
return result;
}
function validateIPCHandleLength(handle: string): void {
const limit = safeIpcPathLengths[platform];
if (typeof limit === 'number' && handle.length >= limit) {
// https://nodejs.org/api/net.html#net_identifying_paths_for_ipc_connections
console.warn(`WARNING: IPC handle "${handle}" is longer than ${limit} chars, try a shorter --user-data-dir`);
}
}
export class Server extends IPCServer {
private static toClientConnectionEvent(server: NetServer): Event<ClientConnectionEvent> {
const onConnection = Event.fromNodeEventEmitter<Socket>(server, 'connection');
return Event.map(onConnection, socket => ({
protocol: new Protocol(new NodeSocket(socket)),
onDidClientDisconnect: Event.once(Event.fromNodeEventEmitter<void>(socket, 'close'))
}));
}
private server: NetServer | null;
constructor(server: NetServer) {
super(Server.toClientConnectionEvent(server));
this.server = server;
}
dispose(): void {
super.dispose();
if (this.server) {
this.server.close();
this.server = null;
}
}
}
export function serve(port: number): Promise<Server>;
export function serve(namedPipe: string): Promise<Server>;
export function serve(hook: any): Promise<Server> {
return new Promise<Server>((c, e) => {
const server = createServer();
server.on('error', e);
server.listen(hook, () => {
server.removeListener('error', e);
c(new Server(server));
});
});
}
export function connect(options: { host: string, port: number }, clientId: string): Promise<Client>;
export function connect(port: number, clientId: string): Promise<Client>;
export function connect(namedPipe: string, clientId: string): Promise<Client>;
export function connect(hook: any, clientId: string): Promise<Client> {
return new Promise<Client>((c, e) => {
const socket = createConnection(hook, () => {
socket.removeListener('error', e);
c(Client.fromSocket(new NodeSocket(socket), clientId));
});
socket.once('error', e);
});
}

View File

@ -0,0 +1,493 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { Emitter, Event } from 'vs/base/common/event';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { canceled } from 'vs/base/common/errors';
import { timeout } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { URI } from 'vs/base/common/uri';
import { isEqual } from 'vs/base/common/resources';
class QueueProtocol implements IMessagePassingProtocol {
private buffering = true;
private buffers: VSBuffer[] = [];
private readonly _onMessage = new Emitter<VSBuffer>({
onFirstListenerDidAdd: () => {
for (const buffer of this.buffers) {
this._onMessage.fire(buffer);
}
this.buffers = [];
this.buffering = false;
},
onLastListenerRemove: () => {
this.buffering = true;
}
});
readonly onMessage = this._onMessage.event;
other!: QueueProtocol;
send(buffer: VSBuffer): void {
this.other.receive(buffer);
}
protected receive(buffer: VSBuffer): void {
if (this.buffering) {
this.buffers.push(buffer);
} else {
this._onMessage.fire(buffer);
}
}
}
function createProtocolPair(): [IMessagePassingProtocol, IMessagePassingProtocol] {
const one = new QueueProtocol();
const other = new QueueProtocol();
one.other = other;
other.other = one;
return [one, other];
}
class TestIPCClient extends IPCClient<string> {
private readonly _onDidDisconnect = new Emitter<void>();
readonly onDidDisconnect = this._onDidDisconnect.event;
constructor(protocol: IMessagePassingProtocol, id: string) {
super(protocol, id);
}
dispose(): void {
this._onDidDisconnect.fire();
super.dispose();
}
}
class TestIPCServer extends IPCServer<string> {
private readonly onDidClientConnect: Emitter<ClientConnectionEvent>;
constructor() {
const onDidClientConnect = new Emitter<ClientConnectionEvent>();
super(onDidClientConnect.event);
this.onDidClientConnect = onDidClientConnect;
}
createConnection(id: string): IPCClient<string> {
const [pc, ps] = createProtocolPair();
const client = new TestIPCClient(pc, id);
this.onDidClientConnect.fire({
protocol: ps,
onDidClientDisconnect: client.onDidDisconnect
});
return client;
}
}
const TestChannelId = 'testchannel';
interface ITestService {
marco(): Promise<string>;
error(message: string): Promise<void>;
neverComplete(): Promise<void>;
neverCompleteCT(cancellationToken: CancellationToken): Promise<void>;
buffersLength(buffers: VSBuffer[]): Promise<number>;
marshall(uri: URI): Promise<URI>;
context(): Promise<unknown>;
onPong: Event<string>;
}
class TestService implements ITestService {
private readonly _onPong = new Emitter<string>();
readonly onPong = this._onPong.event;
marco(): Promise<string> {
return Promise.resolve('polo');
}
error(message: string): Promise<void> {
return Promise.reject(new Error(message));
}
neverComplete(): Promise<void> {
return new Promise(_ => { });
}
neverCompleteCT(cancellationToken: CancellationToken): Promise<void> {
if (cancellationToken.isCancellationRequested) {
return Promise.reject(canceled());
}
return new Promise((_, e) => cancellationToken.onCancellationRequested(() => e(canceled())));
}
buffersLength(buffers: VSBuffer[]): Promise<number> {
return Promise.resolve(buffers.reduce((r, b) => r + b.buffer.length, 0));
}
ping(msg: string): void {
this._onPong.fire(msg);
}
marshall(uri: URI): Promise<URI> {
return Promise.resolve(uri);
}
context(context?: unknown): Promise<unknown> {
return Promise.resolve(context);
}
}
class TestChannel implements IServerChannel {
constructor(private service: ITestService) { }
call(_: unknown, command: string, arg: any, cancellationToken: CancellationToken): Promise<any> {
switch (command) {
case 'marco': return this.service.marco();
case 'error': return this.service.error(arg);
case 'neverComplete': return this.service.neverComplete();
case 'neverCompleteCT': return this.service.neverCompleteCT(cancellationToken);
case 'buffersLength': return this.service.buffersLength(arg);
default: return Promise.reject(new Error('not implemented'));
}
}
listen(_: unknown, event: string, arg?: any): Event<any> {
switch (event) {
case 'onPong': return this.service.onPong;
default: throw new Error('not implemented');
}
}
}
class TestChannelClient implements ITestService {
get onPong(): Event<string> {
return this.channel.listen('onPong');
}
constructor(private channel: IChannel) { }
marco(): Promise<string> {
return this.channel.call('marco');
}
error(message: string): Promise<void> {
return this.channel.call('error', message);
}
neverComplete(): Promise<void> {
return this.channel.call('neverComplete');
}
neverCompleteCT(cancellationToken: CancellationToken): Promise<void> {
return this.channel.call('neverCompleteCT', undefined, cancellationToken);
}
buffersLength(buffers: VSBuffer[]): Promise<number> {
return this.channel.call('buffersLength', buffers);
}
marshall(uri: URI): Promise<URI> {
return this.channel.call('marshall', uri);
}
context(): Promise<unknown> {
return this.channel.call('context');
}
}
suite('Base IPC', function () {
test('createProtocolPair', async function () {
const [clientProtocol, serverProtocol] = createProtocolPair();
const b1 = VSBuffer.alloc(0);
clientProtocol.send(b1);
const b3 = VSBuffer.alloc(0);
serverProtocol.send(b3);
const b2 = await Event.toPromise(serverProtocol.onMessage);
const b4 = await Event.toPromise(clientProtocol.onMessage);
assert.strictEqual(b1, b2);
assert.strictEqual(b3, b4);
});
suite('one to one', function () {
let server: IPCServer;
let client: IPCClient;
let service: TestService;
let ipcService: ITestService;
setup(function () {
service = new TestService();
const testServer = new TestIPCServer();
server = testServer;
server.registerChannel(TestChannelId, new TestChannel(service));
client = testServer.createConnection('client1');
ipcService = new TestChannelClient(client.getChannel(TestChannelId));
});
teardown(function () {
client.dispose();
server.dispose();
});
test('call success', async function () {
const r = await ipcService.marco();
return assert.equal(r, 'polo');
});
test('call error', async function () {
try {
await ipcService.error('nice error');
return assert.fail('should not reach here');
} catch (err) {
return assert.equal(err.message, 'nice error');
}
});
test('cancel call with cancelled cancellation token', async function () {
try {
await ipcService.neverCompleteCT(CancellationToken.Cancelled);
return assert.fail('should not reach here');
} catch (err) {
return assert(err.message === 'Canceled');
}
});
test('cancel call with cancellation token (sync)', function () {
const cts = new CancellationTokenSource();
const promise = ipcService.neverCompleteCT(cts.token).then(
_ => assert.fail('should not reach here'),
err => assert(err.message === 'Canceled')
);
cts.cancel();
return promise;
});
test('cancel call with cancellation token (async)', function () {
const cts = new CancellationTokenSource();
const promise = ipcService.neverCompleteCT(cts.token).then(
_ => assert.fail('should not reach here'),
err => assert(err.message === 'Canceled')
);
setTimeout(() => cts.cancel());
return promise;
});
test('listen to events', async function () {
const messages: string[] = [];
ipcService.onPong(msg => messages.push(msg));
await timeout(0);
assert.deepEqual(messages, []);
service.ping('hello');
await timeout(0);
assert.deepEqual(messages, ['hello']);
service.ping('world');
await timeout(0);
assert.deepEqual(messages, ['hello', 'world']);
});
test('buffers in arrays', async function () {
const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]);
return assert.equal(r, 5);
});
});
suite('one to one (proxy)', function () {
let server: IPCServer;
let client: IPCClient;
let service: TestService;
let ipcService: ITestService;
setup(function () {
service = new TestService();
const testServer = new TestIPCServer();
server = testServer;
server.registerChannel(TestChannelId, createChannelReceiver(service));
client = testServer.createConnection('client1');
ipcService = createChannelSender(client.getChannel(TestChannelId));
});
teardown(function () {
client.dispose();
server.dispose();
});
test('call success', async function () {
const r = await ipcService.marco();
return assert.equal(r, 'polo');
});
test('call error', async function () {
try {
await ipcService.error('nice error');
return assert.fail('should not reach here');
} catch (err) {
return assert.equal(err.message, 'nice error');
}
});
test('listen to events', async function () {
const messages: string[] = [];
ipcService.onPong(msg => messages.push(msg));
await timeout(0);
assert.deepEqual(messages, []);
service.ping('hello');
await timeout(0);
assert.deepEqual(messages, ['hello']);
service.ping('world');
await timeout(0);
assert.deepEqual(messages, ['hello', 'world']);
});
test('marshalling uri', async function () {
const uri = URI.file('foobar');
const r = await ipcService.marshall(uri);
assert.ok(r instanceof URI);
return assert.ok(isEqual(r, uri));
});
test('buffers in arrays', async function () {
const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]);
return assert.equal(r, 5);
});
});
suite('one to one (proxy, extra context)', function () {
let server: IPCServer;
let client: IPCClient;
let service: TestService;
let ipcService: ITestService;
setup(function () {
service = new TestService();
const testServer = new TestIPCServer();
server = testServer;
server.registerChannel(TestChannelId, createChannelReceiver(service));
client = testServer.createConnection('client1');
ipcService = createChannelSender(client.getChannel(TestChannelId), { context: 'Super Context' });
});
teardown(function () {
client.dispose();
server.dispose();
});
test('call extra context', async function () {
const r = await ipcService.context();
return assert.equal(r, 'Super Context');
});
});
suite('one to many', function () {
test('all clients get pinged', async function () {
const service = new TestService();
const channel = new TestChannel(service);
const server = new TestIPCServer();
server.registerChannel('channel', channel);
let client1GotPinged = false;
const client1 = server.createConnection('client1');
const ipcService1 = new TestChannelClient(client1.getChannel('channel'));
ipcService1.onPong(() => client1GotPinged = true);
let client2GotPinged = false;
const client2 = server.createConnection('client2');
const ipcService2 = new TestChannelClient(client2.getChannel('channel'));
ipcService2.onPong(() => client2GotPinged = true);
await timeout(1);
service.ping('hello');
await timeout(1);
assert(client1GotPinged, 'client 1 got pinged');
assert(client2GotPinged, 'client 2 got pinged');
client1.dispose();
client2.dispose();
server.dispose();
});
test('server gets pings from all clients (broadcast channel)', async function () {
const server = new TestIPCServer();
const client1 = server.createConnection('client1');
const clientService1 = new TestService();
const clientChannel1 = new TestChannel(clientService1);
client1.registerChannel('channel', clientChannel1);
const pings: string[] = [];
const channel = server.getChannel('channel', () => true);
const service = new TestChannelClient(channel);
service.onPong(msg => pings.push(msg));
await timeout(1);
clientService1.ping('hello 1');
await timeout(1);
assert.deepEqual(pings, ['hello 1']);
const client2 = server.createConnection('client2');
const clientService2 = new TestService();
const clientChannel2 = new TestChannel(clientService2);
client2.registerChannel('channel', clientChannel2);
await timeout(1);
clientService2.ping('hello 2');
await timeout(1);
assert.deepEqual(pings, ['hello 1', 'hello 2']);
client1.dispose();
clientService1.ping('hello 1');
await timeout(1);
assert.deepEqual(pings, ['hello 1', 'hello 2']);
await timeout(1);
clientService2.ping('hello again 2');
await timeout(1);
assert.deepEqual(pings, ['hello 1', 'hello 2', 'hello again 2']);
client2.dispose();
server.dispose();
});
});
});

View File

@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { TestServiceClient } from './testService';
import { getPathFromAmdModule } from 'vs/base/common/amd';
function createClient(): Client {
return new Client(getPathFromAmdModule(require, 'bootstrap-fork'), {
serverName: 'TestServer',
env: { AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true }
});
}
suite('IPC, Child Process', () => {
test('createChannel', () => {
const client = createClient();
const channel = client.getChannel('test');
const service = new TestServiceClient(channel);
const result = service.pong('ping').then(r => {
assert.equal(r.incoming, 'ping');
assert.equal(r.outgoing, 'pong');
});
return result.finally(() => client.dispose());
});
test('events', () => {
const client = createClient();
const channel = client.getChannel('test');
const service = new TestServiceClient(channel);
const event = new Promise((c, e) => {
service.onMarco(({ answer }) => {
try {
assert.equal(answer, 'polo');
c(undefined);
} catch (err) {
e(err);
}
});
});
const request = service.marco();
const result = Promise.all([request, event]);
return result.finally(() => client.dispose());
});
test('event dispose', () => {
const client = createClient();
const channel = client.getChannel('test');
const service = new TestServiceClient(channel);
let count = 0;
const disposable = service.onMarco(() => count++);
const result = service.marco().then(async answer => {
assert.equal(answer, 'polo');
assert.equal(count, 1);
const answer_1 = await service.marco();
assert.equal(answer_1, 'polo');
assert.equal(count, 2);
disposable.dispose();
const answer_2 = await service.marco();
assert.equal(answer_2, 'polo');
assert.equal(count, 2);
});
return result.finally(() => client.dispose());
});
});

View File

@ -0,0 +1,258 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { createServer, Socket } from 'net';
import { EventEmitter } from 'events';
import { Protocol, PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net';
import { createRandomIPCHandle, createStaticIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
import { VSBuffer } from 'vs/base/common/buffer';
import { tmpdir } from 'os';
import product from 'vs/platform/product/common/product';
class MessageStream {
private _currentComplete: ((data: VSBuffer) => void) | null;
private _messages: VSBuffer[];
constructor(x: Protocol | PersistentProtocol) {
this._currentComplete = null;
this._messages = [];
x.onMessage(data => {
this._messages.push(data);
this._trigger();
});
}
private _trigger(): void {
if (!this._currentComplete) {
return;
}
if (this._messages.length === 0) {
return;
}
const complete = this._currentComplete;
const msg = this._messages.shift()!;
this._currentComplete = null;
complete(msg);
}
public waitForOne(): Promise<VSBuffer> {
return new Promise<VSBuffer>((complete) => {
this._currentComplete = complete;
this._trigger();
});
}
}
class EtherStream extends EventEmitter {
constructor(
private readonly _ether: Ether,
private readonly _name: 'a' | 'b'
) {
super();
}
write(data: Buffer, cb?: Function): boolean {
if (!Buffer.isBuffer(data)) {
throw new Error(`Invalid data`);
}
this._ether.write(this._name, data);
return true;
}
}
class Ether {
private readonly _a: EtherStream;
private readonly _b: EtherStream;
private _ab: Buffer[];
private _ba: Buffer[];
public get a(): Socket {
return <any>this._a;
}
public get b(): Socket {
return <any>this._b;
}
constructor() {
this._a = new EtherStream(this, 'a');
this._b = new EtherStream(this, 'b');
this._ab = [];
this._ba = [];
}
public write(from: 'a' | 'b', data: Buffer): void {
if (from === 'a') {
this._ab.push(data);
} else {
this._ba.push(data);
}
setImmediate(() => this._deliver());
}
private _deliver(): void {
if (this._ab.length > 0) {
const data = Buffer.concat(this._ab);
this._ab.length = 0;
this._b.emit('data', data);
setImmediate(() => this._deliver());
return;
}
if (this._ba.length > 0) {
const data = Buffer.concat(this._ba);
this._ba.length = 0;
this._a.emit('data', data);
setImmediate(() => this._deliver());
return;
}
}
}
suite('IPC, Socket Protocol', () => {
let ether: Ether;
setup(() => {
ether = new Ether();
});
test('read/write', async () => {
const a = new Protocol(new NodeSocket(ether.a));
const b = new Protocol(new NodeSocket(ether.b));
const bMessages = new MessageStream(b);
a.send(VSBuffer.fromString('foobarfarboo'));
const msg1 = await bMessages.waitForOne();
assert.equal(msg1.toString(), 'foobarfarboo');
const buffer = VSBuffer.alloc(1);
buffer.writeUInt8(123, 0);
a.send(buffer);
const msg2 = await bMessages.waitForOne();
assert.equal(msg2.readUInt8(0), 123);
});
test('read/write, object data', async () => {
const a = new Protocol(new NodeSocket(ether.a));
const b = new Protocol(new NodeSocket(ether.b));
const bMessages = new MessageStream(b);
const data = {
pi: Math.PI,
foo: 'bar',
more: true,
data: 'Hello World'.split('')
};
a.send(VSBuffer.fromString(JSON.stringify(data)));
const msg = await bMessages.waitForOne();
assert.deepEqual(JSON.parse(msg.toString()), data);
});
});
suite('PersistentProtocol reconnection', () => {
let ether: Ether;
setup(() => {
ether = new Ether();
});
test('acks get piggybacked with messages', async () => {
const a = new PersistentProtocol(new NodeSocket(ether.a));
const aMessages = new MessageStream(a);
const b = new PersistentProtocol(new NodeSocket(ether.b));
const bMessages = new MessageStream(b);
a.send(VSBuffer.fromString('a1'));
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 0);
a.send(VSBuffer.fromString('a2'));
assert.equal(a.unacknowledgedCount, 2);
assert.equal(b.unacknowledgedCount, 0);
a.send(VSBuffer.fromString('a3'));
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
const a1 = await bMessages.waitForOne();
assert.equal(a1.toString(), 'a1');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
const a2 = await bMessages.waitForOne();
assert.equal(a2.toString(), 'a2');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
const a3 = await bMessages.waitForOne();
assert.equal(a3.toString(), 'a3');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
b.send(VSBuffer.fromString('b1'));
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 1);
const b1 = await aMessages.waitForOne();
assert.equal(b1.toString(), 'b1');
assert.equal(a.unacknowledgedCount, 0);
assert.equal(b.unacknowledgedCount, 1);
a.send(VSBuffer.fromString('a4'));
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 1);
const b2 = await bMessages.waitForOne();
assert.equal(b2.toString(), 'a4');
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 0);
});
});
suite('IPC, create handle', () => {
test('createRandomIPCHandle', async () => {
return testIPCHandle(createRandomIPCHandle());
});
test('createStaticIPCHandle', async () => {
return testIPCHandle(createStaticIPCHandle(tmpdir(), 'test', product.version));
});
function testIPCHandle(handle: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
const pipeName = createRandomIPCHandle();
const server = createServer();
server.on('error', () => {
return new Promise(() => server.close(() => reject()));
});
server.listen(pipeName, () => {
server.removeListener('error', reject);
return new Promise(() => {
server.close(() => resolve());
});
});
});
}
});

View File

@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { TestChannel, TestService } from './testService';
const server = new Server('test');
const service = new TestService();
server.registerChannel('test', new TestChannel(service));

View File

@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event, Emitter } from 'vs/base/common/event';
import { timeout } from 'vs/base/common/async';
export interface IMarcoPoloEvent {
answer: string;
}
export interface ITestService {
onMarco: Event<IMarcoPoloEvent>;
marco(): Promise<string>;
pong(ping: string): Promise<{ incoming: string, outgoing: string }>;
cancelMe(): Promise<boolean>;
}
export class TestService implements ITestService {
private readonly _onMarco = new Emitter<IMarcoPoloEvent>();
onMarco: Event<IMarcoPoloEvent> = this._onMarco.event;
marco(): Promise<string> {
this._onMarco.fire({ answer: 'polo' });
return Promise.resolve('polo');
}
pong(ping: string): Promise<{ incoming: string, outgoing: string }> {
return Promise.resolve({ incoming: ping, outgoing: 'pong' });
}
cancelMe(): Promise<boolean> {
return Promise.resolve(timeout(100)).then(() => true);
}
}
export class TestChannel implements IServerChannel {
constructor(private testService: ITestService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'marco': return this.testService.onMarco;
}
throw new Error('Event not found');
}
call(_: unknown, command: string, ...args: any[]): Promise<any> {
switch (command) {
case 'pong': return this.testService.pong(args[0]);
case 'cancelMe': return this.testService.cancelMe();
case 'marco': return this.testService.marco();
default: return Promise.reject(new Error(`command not found: ${command}`));
}
}
}
export class TestServiceClient implements ITestService {
get onMarco(): Event<IMarcoPoloEvent> { return this.channel.listen('marco'); }
constructor(private channel: IChannel) { }
marco(): Promise<string> {
return this.channel.call('marco');
}
pong(ping: string): Promise<{ incoming: string, outgoing: string }> {
return this.channel.call('pong', ping);
}
cancelMe(): Promise<boolean> {
return this.channel.call('cancelMe');
}
}

View File

@ -0,0 +1,280 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.quick-input-widget {
position: absolute;
width: 600px;
z-index: 2000;
padding-bottom: 6px;
left: 50%;
margin-left: -300px;
}
.quick-input-titlebar {
display: flex;
}
.quick-input-left-action-bar {
display: flex;
margin-left: 4px;
flex: 1;
}
.quick-input-left-action-bar.monaco-action-bar .actions-container {
justify-content: flex-start;
}
.quick-input-title {
padding: 3px 0px;
text-align: center;
}
.quick-input-right-action-bar {
display: flex;
margin-right: 4px;
flex: 1;
}
.quick-input-titlebar .monaco-action-bar .action-label.codicon {
margin: 0;
width: 19px;
height: 100%;
background-position: center;
background-repeat: no-repeat;
}
.quick-input-description {
margin: 6px;
}
.quick-input-header .quick-input-description {
margin: 4px 2px;
}
.quick-input-header {
display: flex;
padding: 6px 6px 0px 6px;
margin-bottom: -2px;
}
.quick-input-widget.hidden-input .quick-input-header {
/* reduce margins and paddings when input box hidden */
padding: 0;
margin-bottom: 0;
}
.quick-input-and-message {
display: flex;
flex-direction: column;
flex-grow: 1;
position: relative;
}
.quick-input-check-all {
align-self: center;
margin: 0;
}
.quick-input-filter {
flex-grow: 1;
display: flex;
position: relative;
}
.quick-input-box {
flex-grow: 1;
}
.quick-input-widget.show-checkboxes .quick-input-box,
.quick-input-widget.show-checkboxes .quick-input-message {
margin-left: 5px;
}
.quick-input-visible-count {
position: absolute;
left: -10000px;
}
.quick-input-count {
align-self: center;
position: absolute;
right: 4px;
display: flex;
align-items: center;
}
.quick-input-count .monaco-count-badge {
vertical-align: middle;
padding: 2px 4px;
border-radius: 2px;
min-height: auto;
line-height: normal;
}
.quick-input-action {
margin-left: 6px;
}
.quick-input-action .monaco-text-button {
font-size: 11px;
padding: 0 6px;
display: flex;
height: 100%;
align-items: center;
}
.quick-input-message {
margin-top: -1px;
padding: 5px 5px 2px 5px;
}
.quick-input-progress.monaco-progress-container {
position: relative;
}
.quick-input-progress.monaco-progress-container,
.quick-input-progress.monaco-progress-container .progress-bit {
height: 2px;
}
.quick-input-list {
line-height: 22px;
margin-top: 6px;
}
.quick-input-widget.hidden-input .quick-input-list {
margin-top: 0; /* reduce margins when input box hidden */
}
.quick-input-list .monaco-list {
overflow: hidden;
max-height: calc(20 * 22px);
}
.quick-input-list .quick-input-list-entry {
box-sizing: border-box;
overflow: hidden;
display: flex;
height: 100%;
padding: 0 6px;
}
.quick-input-list .quick-input-list-entry.quick-input-list-separator-border {
border-top-width: 1px;
border-top-style: solid;
}
.quick-input-list .monaco-list-row:first-child .quick-input-list-entry.quick-input-list-separator-border {
border-top-style: none;
}
.quick-input-list .quick-input-list-label {
overflow: hidden;
display: flex;
height: 100%;
flex: 1;
}
.quick-input-list .quick-input-list-checkbox {
align-self: center;
margin: 0;
}
.quick-input-list .quick-input-list-rows {
overflow: hidden;
text-overflow: ellipsis;
display: flex;
flex-direction: column;
height: 100%;
flex: 1;
margin-left: 5px;
}
.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-rows {
margin-left: 10px;
}
.quick-input-widget .quick-input-list .quick-input-list-checkbox {
display: none;
}
.quick-input-widget.show-checkboxes .quick-input-list .quick-input-list-checkbox {
display: inline;
}
.quick-input-list .quick-input-list-rows > .quick-input-list-row {
display: flex;
align-items: center;
}
.quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label,
.quick-input-list .quick-input-list-rows > .quick-input-list-row .monaco-icon-label .monaco-icon-label-container > .monaco-icon-name-container {
flex: 1; /* make sure the icon label grows within the row */
}
.quick-input-list .quick-input-list-rows > .quick-input-list-row .codicon[class*='codicon-'] {
vertical-align: sub;
}
.quick-input-list .quick-input-list-rows .monaco-highlighted-label span {
opacity: 1;
}
.quick-input-list .quick-input-list-entry .quick-input-list-entry-keybinding {
margin-right: 8px; /* separate from the separator label or scrollbar if any */
}
.quick-input-list .quick-input-list-label-meta {
opacity: 0.7;
line-height: normal;
text-overflow: ellipsis;
overflow: hidden;
}
.quick-input-list .monaco-highlighted-label .highlight {
font-weight: bold;
}
.quick-input-list .quick-input-list-entry .quick-input-list-separator {
margin-right: 8px; /* separate from keybindings or actions */
}
.quick-input-list .quick-input-list-entry-action-bar {
display: flex;
flex: 0;
overflow: visible;
}
.quick-input-list .quick-input-list-entry-action-bar .action-label {
/*
* By default, actions in the quick input action bar are hidden
* until hovered over them or selected.
*/
display: none;
}
.quick-input-list .quick-input-list-entry-action-bar .action-label.codicon {
margin: 0;
height: 100%;
padding: 0 2px;
vertical-align: middle;
}
.quick-input-list .quick-input-list-entry-action-bar {
margin-top: 1px;
}
.quick-input-list .quick-input-list-entry-action-bar {
margin-right: 4px; /* separate from scrollbar */
}
.quick-input-list .quick-input-list-entry-action-bar .action-label.codicon {
margin-right: 4px; /* separate actions */
}
.quick-input-list .quick-input-list-entry .quick-input-list-entry-action-bar .action-label.always-visible,
.quick-input-list .quick-input-list-entry:hover .quick-input-list-entry-action-bar .action-label,
.quick-input-list .monaco-list-row.focused .quick-input-list-entry-action-bar .action-label {
display: flex;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/quickInput';
import * as dom from 'vs/base/browser/dom';
import { InputBox, IRange, MessageType, IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import Severity from 'vs/base/common/severity';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
const $ = dom.$;
export class QuickInputBox extends Disposable {
private container: HTMLElement;
private inputBox: InputBox;
constructor(
private parent: HTMLElement
) {
super();
this.container = dom.append(this.parent, $('.quick-input-box'));
this.inputBox = this._register(new InputBox(this.container, undefined));
}
onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => {
return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
handler(new StandardKeyboardEvent(e));
});
};
onMouseDown = (handler: (event: StandardMouseEvent) => void): IDisposable => {
return dom.addDisposableListener(this.inputBox.inputElement, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
handler(new StandardMouseEvent(e));
});
};
onDidChange = (handler: (event: string) => void): IDisposable => {
return this.inputBox.onDidChange(handler);
};
get value() {
return this.inputBox.value;
}
set value(value: string) {
this.inputBox.value = value;
}
select(range: IRange | null = null): void {
this.inputBox.select(range);
}
isSelectionAtEnd(): boolean {
return this.inputBox.isSelectionAtEnd();
}
setPlaceholder(placeholder: string): void {
this.inputBox.setPlaceHolder(placeholder);
}
get placeholder() {
return this.inputBox.inputElement.getAttribute('placeholder') || '';
}
set placeholder(placeholder: string) {
this.inputBox.setPlaceHolder(placeholder);
}
get ariaLabel() {
return this.inputBox.getAriaLabel();
}
set ariaLabel(ariaLabel: string) {
this.inputBox.setAriaLabel(ariaLabel);
}
get password() {
return this.inputBox.inputElement.type === 'password';
}
set password(password: boolean) {
this.inputBox.inputElement.type = password ? 'password' : 'text';
}
set enabled(enabled: boolean) {
this.inputBox.setEnabled(enabled);
}
hasFocus(): boolean {
return this.inputBox.hasFocus();
}
setAttribute(name: string, value: string): void {
this.inputBox.inputElement.setAttribute(name, value);
}
removeAttribute(name: string): void {
this.inputBox.inputElement.removeAttribute(name);
}
showDecoration(decoration: Severity): void {
if (decoration === Severity.Ignore) {
this.inputBox.hideMessage();
} else {
this.inputBox.showMessage({ type: decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR, content: '' });
}
}
stylesForType(decoration: Severity) {
return this.inputBox.stylesForType(decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR);
}
setFocus(): void {
this.inputBox.focus();
}
layout(): void {
this.inputBox.layout();
}
style(styles: IInputBoxStyles): void {
this.inputBox.style(styles);
}
}

View File

@ -0,0 +1,726 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/quickInput';
import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list';
import * as dom from 'vs/base/browser/dom';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput';
import { IMatch } from 'vs/base/common/filters';
import { matchesFuzzyCodiconAware, parseCodicons } from 'vs/base/common/codicon';
import { compareAnything } from 'vs/base/common/comparers';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { memoize } from 'vs/base/common/decorators';
import { range } from 'vs/base/common/arrays';
import * as platform from 'vs/base/common/platform';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { Action } from 'vs/base/common/actions';
import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput';
import { IListOptions, List, IListStyles, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
import { localize } from 'vs/nls';
const $ = dom.$;
interface IListElement {
readonly index: number;
readonly item: IQuickPickItem;
readonly saneLabel: string;
readonly saneAriaLabel: string;
readonly saneDescription?: string;
readonly saneDetail?: string;
readonly labelHighlights?: IMatch[];
readonly descriptionHighlights?: IMatch[];
readonly detailHighlights?: IMatch[];
readonly checked: boolean;
readonly separator?: IQuickPickSeparator;
readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
}
class ListElement implements IListElement, IDisposable {
index!: number;
item!: IQuickPickItem;
saneLabel!: string;
saneAriaLabel!: string;
saneDescription?: string;
saneDetail?: string;
hidden = false;
private readonly _onChecked = new Emitter<boolean>();
onChecked = this._onChecked.event;
_checked?: boolean;
get checked() {
return !!this._checked;
}
set checked(value: boolean) {
if (value !== this._checked) {
this._checked = value;
this._onChecked.fire(value);
}
}
separator?: IQuickPickSeparator;
labelHighlights?: IMatch[];
descriptionHighlights?: IMatch[];
detailHighlights?: IMatch[];
fireButtonTriggered!: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void;
constructor(init: IListElement) {
Object.assign(this, init);
}
dispose() {
this._onChecked.dispose();
}
}
interface IListElementTemplateData {
entry: HTMLDivElement;
checkbox: HTMLInputElement;
label: IconLabel;
keybinding: KeybindingLabel;
detail: HighlightedLabel;
separator: HTMLDivElement;
actionBar: ActionBar;
element: ListElement;
toDisposeElement: IDisposable[];
toDisposeTemplate: IDisposable[];
}
class ListElementRenderer implements IListRenderer<ListElement, IListElementTemplateData> {
static readonly ID = 'listelement';
get templateId() {
return ListElementRenderer.ID;
}
renderTemplate(container: HTMLElement): IListElementTemplateData {
const data: IListElementTemplateData = Object.create(null);
data.toDisposeElement = [];
data.toDisposeTemplate = [];
data.entry = dom.append(container, $('.quick-input-list-entry'));
// Checkbox
const label = dom.append(data.entry, $('label.quick-input-list-label'));
data.toDisposeTemplate.push(dom.addStandardDisposableListener(label, dom.EventType.CLICK, e => {
if (!data.checkbox.offsetParent) { // If checkbox not visible:
e.preventDefault(); // Prevent toggle of checkbox when it is immediately shown afterwards. #91740
}
}));
data.checkbox = <HTMLInputElement>dom.append(label, $('input.quick-input-list-checkbox'));
data.checkbox.type = 'checkbox';
data.toDisposeTemplate.push(dom.addStandardDisposableListener(data.checkbox, dom.EventType.CHANGE, e => {
data.element.checked = data.checkbox.checked;
}));
// Rows
const rows = dom.append(label, $('.quick-input-list-rows'));
const row1 = dom.append(rows, $('.quick-input-list-row'));
const row2 = dom.append(rows, $('.quick-input-list-row'));
// Label
data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportCodicons: true });
// Keybinding
const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding'));
data.keybinding = new KeybindingLabel(keybindingContainer, platform.OS);
// Detail
const detailContainer = dom.append(row2, $('.quick-input-list-label-meta'));
data.detail = new HighlightedLabel(detailContainer, true);
// Separator
data.separator = dom.append(data.entry, $('.quick-input-list-separator'));
// Actions
data.actionBar = new ActionBar(data.entry);
data.actionBar.domNode.classList.add('quick-input-list-entry-action-bar');
data.toDisposeTemplate.push(data.actionBar);
return data;
}
renderElement(element: ListElement, index: number, data: IListElementTemplateData): void {
data.toDisposeElement = dispose(data.toDisposeElement);
data.element = element;
data.checkbox.checked = element.checked;
data.toDisposeElement.push(element.onChecked(checked => data.checkbox.checked = checked));
const { labelHighlights, descriptionHighlights, detailHighlights } = element;
// Label
const options: IIconLabelValueOptions = Object.create(null);
options.matches = labelHighlights || [];
options.descriptionTitle = element.saneDescription;
options.descriptionMatches = descriptionHighlights || [];
options.extraClasses = element.item.iconClasses;
options.italic = element.item.italic;
options.strikethrough = element.item.strikethrough;
data.label.setLabel(element.saneLabel, element.saneDescription, options);
// Keybinding
data.keybinding.set(element.item.keybinding);
// Meta
data.detail.set(element.saneDetail, detailHighlights);
// Separator
if (element.separator && element.separator.label) {
data.separator.textContent = element.separator.label;
data.separator.style.display = '';
} else {
data.separator.style.display = 'none';
}
data.entry.classList.toggle('quick-input-list-separator-border', !!element.separator);
// Actions
data.actionBar.clear();
const buttons = element.item.buttons;
if (buttons && buttons.length) {
data.actionBar.push(buttons.map((button, index) => {
let cssClasses = button.iconClass || (button.iconPath ? getIconClass(button.iconPath) : undefined);
if (button.alwaysVisible) {
cssClasses = cssClasses ? `${cssClasses} always-visible` : 'always-visible';
}
const action = new Action(`id-${index}`, '', cssClasses, true, () => {
element.fireButtonTriggered({
button,
item: element.item
});
return Promise.resolve();
});
action.tooltip = button.tooltip || '';
return action;
}), { icon: true, label: false });
data.entry.classList.add('has-actions');
} else {
data.entry.classList.remove('has-actions');
}
}
disposeElement(element: ListElement, index: number, data: IListElementTemplateData): void {
data.toDisposeElement = dispose(data.toDisposeElement);
}
disposeTemplate(data: IListElementTemplateData): void {
data.toDisposeElement = dispose(data.toDisposeElement);
data.toDisposeTemplate = dispose(data.toDisposeTemplate);
}
}
class ListElementDelegate implements IListVirtualDelegate<ListElement> {
getHeight(element: ListElement): number {
return element.saneDetail ? 44 : 22;
}
getTemplateId(element: ListElement): string {
return ListElementRenderer.ID;
}
}
export enum QuickInputListFocus {
First = 1,
Second,
Last,
Next,
Previous,
NextPage,
PreviousPage
}
export class QuickInputList {
readonly id: string;
private container: HTMLElement;
private list: List<ListElement>;
private inputElements: Array<IQuickPickItem | IQuickPickSeparator> = [];
private elements: ListElement[] = [];
private elementsToIndexes = new Map<IQuickPickItem, number>();
matchOnDescription = false;
matchOnDetail = false;
matchOnLabel = true;
sortByLabel = true;
private readonly _onChangedAllVisibleChecked = new Emitter<boolean>();
onChangedAllVisibleChecked: Event<boolean> = this._onChangedAllVisibleChecked.event;
private readonly _onChangedCheckedCount = new Emitter<number>();
onChangedCheckedCount: Event<number> = this._onChangedCheckedCount.event;
private readonly _onChangedVisibleCount = new Emitter<number>();
onChangedVisibleCount: Event<number> = this._onChangedVisibleCount.event;
private readonly _onChangedCheckedElements = new Emitter<IQuickPickItem[]>();
onChangedCheckedElements: Event<IQuickPickItem[]> = this._onChangedCheckedElements.event;
private readonly _onButtonTriggered = new Emitter<IQuickPickItemButtonEvent<IQuickPickItem>>();
onButtonTriggered = this._onButtonTriggered.event;
private readonly _onKeyDown = new Emitter<StandardKeyboardEvent>();
onKeyDown: Event<StandardKeyboardEvent> = this._onKeyDown.event;
private readonly _onLeave = new Emitter<void>();
onLeave: Event<void> = this._onLeave.event;
private _fireCheckedEvents = true;
private elementDisposables: IDisposable[] = [];
private disposables: IDisposable[] = [];
constructor(
private parent: HTMLElement,
id: string,
options: IQuickInputOptions,
) {
this.id = id;
this.container = dom.append(this.parent, $('.quick-input-list'));
const delegate = new ListElementDelegate();
const accessibilityProvider = new QuickInputAccessibilityProvider();
this.list = options.createList('QuickInput', this.container, delegate, [new ListElementRenderer()], {
identityProvider: { getId: element => element.saneLabel },
setRowLineHeight: false,
multipleSelectionSupport: false,
horizontalScrolling: false,
accessibilityProvider
} as IListOptions<ListElement>);
this.list.getHTMLElement().id = id;
this.disposables.push(this.list);
this.disposables.push(this.list.onKeyDown(e => {
const event = new StandardKeyboardEvent(e);
switch (event.keyCode) {
case KeyCode.Space:
this.toggleCheckbox();
break;
case KeyCode.KEY_A:
if (platform.isMacintosh ? e.metaKey : e.ctrlKey) {
this.list.setFocus(range(this.list.length));
}
break;
case KeyCode.UpArrow:
const focus1 = this.list.getFocus();
if (focus1.length === 1 && focus1[0] === 0) {
this._onLeave.fire();
}
break;
case KeyCode.DownArrow:
const focus2 = this.list.getFocus();
if (focus2.length === 1 && focus2[0] === this.list.length - 1) {
this._onLeave.fire();
}
break;
}
this._onKeyDown.fire(event);
}));
this.disposables.push(this.list.onMouseDown(e => {
if (e.browserEvent.button !== 2) {
// Works around / fixes #64350.
e.browserEvent.preventDefault();
}
}));
this.disposables.push(dom.addDisposableListener(this.container, dom.EventType.CLICK, e => {
if (e.x || e.y) { // Avoid 'click' triggered by 'space' on checkbox.
this._onLeave.fire();
}
}));
this.disposables.push(this.list.onMouseMiddleClick(e => {
this._onLeave.fire();
}));
this.disposables.push(this.list.onContextMenu(e => {
if (typeof e.index === 'number') {
e.browserEvent.preventDefault();
// we want to treat a context menu event as
// a gesture to open the item at the index
// since we do not have any context menu
// this enables for example macOS to Ctrl-
// click on an item to open it.
this.list.setSelection([e.index]);
}
}));
this.disposables.push(
this._onChangedAllVisibleChecked,
this._onChangedCheckedCount,
this._onChangedVisibleCount,
this._onChangedCheckedElements,
this._onButtonTriggered,
this._onLeave,
this._onKeyDown
);
}
@memoize
get onDidChangeFocus() {
return Event.map(this.list.onDidChangeFocus, e => e.elements.map(e => e.item));
}
@memoize
get onDidChangeSelection() {
return Event.map(this.list.onDidChangeSelection, e => ({ items: e.elements.map(e => e.item), event: e.browserEvent }));
}
getAllVisibleChecked() {
return this.allVisibleChecked(this.elements, false);
}
private allVisibleChecked(elements: ListElement[], whenNoneVisible = true) {
for (let i = 0, n = elements.length; i < n; i++) {
const element = elements[i];
if (!element.hidden) {
if (!element.checked) {
return false;
} else {
whenNoneVisible = true;
}
}
}
return whenNoneVisible;
}
getCheckedCount() {
let count = 0;
const elements = this.elements;
for (let i = 0, n = elements.length; i < n; i++) {
if (elements[i].checked) {
count++;
}
}
return count;
}
getVisibleCount() {
let count = 0;
const elements = this.elements;
for (let i = 0, n = elements.length; i < n; i++) {
if (!elements[i].hidden) {
count++;
}
}
return count;
}
setAllVisibleChecked(checked: boolean) {
try {
this._fireCheckedEvents = false;
this.elements.forEach(element => {
if (!element.hidden) {
element.checked = checked;
}
});
} finally {
this._fireCheckedEvents = true;
this.fireCheckedEvents();
}
}
setElements(inputElements: Array<IQuickPickItem | IQuickPickSeparator>): void {
this.elementDisposables = dispose(this.elementDisposables);
const fireButtonTriggered = (event: IQuickPickItemButtonEvent<IQuickPickItem>) => this.fireButtonTriggered(event);
this.inputElements = inputElements;
this.elements = inputElements.reduce((result, item, index) => {
if (item.type !== 'separator') {
const previous = index && inputElements[index - 1];
const saneLabel = item.label && item.label.replace(/\r?\n/g, ' ');
const saneDescription = item.description && item.description.replace(/\r?\n/g, ' ');
const saneDetail = item.detail && item.detail.replace(/\r?\n/g, ' ');
const saneAriaLabel = item.ariaLabel || [saneLabel, saneDescription, saneDetail]
.map(s => s && parseCodicons(s).text)
.filter(s => !!s)
.join(', ');
result.push(new ListElement({
index,
item,
saneLabel,
saneAriaLabel,
saneDescription,
saneDetail,
labelHighlights: item.highlights?.label,
descriptionHighlights: item.highlights?.description,
detailHighlights: item.highlights?.detail,
checked: false,
separator: previous && previous.type === 'separator' ? previous : undefined,
fireButtonTriggered
}));
}
return result;
}, [] as ListElement[]);
this.elementDisposables.push(...this.elements);
this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents())));
this.elementsToIndexes = this.elements.reduce((map, element, index) => {
map.set(element.item, index);
return map;
}, new Map<IQuickPickItem, number>());
this.list.splice(0, this.list.length); // Clear focus and selection first, sending the events when the list is empty.
this.list.splice(0, this.list.length, this.elements);
this._onChangedVisibleCount.fire(this.elements.length);
}
getElementsCount(): number {
return this.inputElements.length;
}
getFocusedElements() {
return this.list.getFocusedElements()
.map(e => e.item);
}
setFocusedElements(items: IQuickPickItem[]) {
this.list.setFocus(items
.filter(item => this.elementsToIndexes.has(item))
.map(item => this.elementsToIndexes.get(item)!));
if (items.length > 0) {
const focused = this.list.getFocus()[0];
if (typeof focused === 'number') {
this.list.reveal(focused);
}
}
}
getActiveDescendant() {
return this.list.getHTMLElement().getAttribute('aria-activedescendant');
}
getSelectedElements() {
return this.list.getSelectedElements()
.map(e => e.item);
}
setSelectedElements(items: IQuickPickItem[]) {
this.list.setSelection(items
.filter(item => this.elementsToIndexes.has(item))
.map(item => this.elementsToIndexes.get(item)!));
}
getCheckedElements() {
return this.elements.filter(e => e.checked)
.map(e => e.item);
}
setCheckedElements(items: IQuickPickItem[]) {
try {
this._fireCheckedEvents = false;
const checked = new Set();
for (const item of items) {
checked.add(item);
}
for (const element of this.elements) {
element.checked = checked.has(element.item);
}
} finally {
this._fireCheckedEvents = true;
this.fireCheckedEvents();
}
}
set enabled(value: boolean) {
this.list.getHTMLElement().style.pointerEvents = value ? '' : 'none';
}
focus(what: QuickInputListFocus): void {
if (!this.list.length) {
return;
}
if (what === QuickInputListFocus.Next && this.list.getFocus()[0] === this.list.length - 1) {
what = QuickInputListFocus.First;
}
if (what === QuickInputListFocus.Previous && this.list.getFocus()[0] === 0) {
what = QuickInputListFocus.Last;
}
if (what === QuickInputListFocus.Second && this.list.length < 2) {
what = QuickInputListFocus.First;
}
switch (what) {
case QuickInputListFocus.First:
this.list.focusFirst();
break;
case QuickInputListFocus.Second:
this.list.focusNth(1);
break;
case QuickInputListFocus.Last:
this.list.focusLast();
break;
case QuickInputListFocus.Next:
this.list.focusNext();
break;
case QuickInputListFocus.Previous:
this.list.focusPrevious();
break;
case QuickInputListFocus.NextPage:
this.list.focusNextPage();
break;
case QuickInputListFocus.PreviousPage:
this.list.focusPreviousPage();
break;
}
const focused = this.list.getFocus()[0];
if (typeof focused === 'number') {
this.list.reveal(focused);
}
}
clearFocus() {
this.list.setFocus([]);
}
domFocus() {
this.list.domFocus();
}
layout(maxHeight?: number): void {
this.list.getHTMLElement().style.maxHeight = maxHeight ? `calc(${Math.floor(maxHeight / 44) * 44}px)` : '';
this.list.layout();
}
filter(query: string): boolean {
if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
this.list.layout();
return false;
}
query = query.trim();
// Reset filtering
if (!query || !(this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) {
this.elements.forEach(element => {
element.labelHighlights = undefined;
element.descriptionHighlights = undefined;
element.detailHighlights = undefined;
element.hidden = false;
const previous = element.index && this.inputElements[element.index - 1];
element.separator = previous && previous.type === 'separator' ? previous : undefined;
});
}
// Filter by value (since we support codicons, use codicon aware fuzzy matching)
else {
this.elements.forEach(element => {
const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneLabel))) : undefined;
const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDescription || ''))) : undefined;
const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyCodiconAware(query, parseCodicons(element.saneDetail || ''))) : undefined;
if (labelHighlights || descriptionHighlights || detailHighlights) {
element.labelHighlights = labelHighlights;
element.descriptionHighlights = descriptionHighlights;
element.detailHighlights = detailHighlights;
element.hidden = false;
} else {
element.labelHighlights = undefined;
element.descriptionHighlights = undefined;
element.detailHighlights = undefined;
element.hidden = !element.item.alwaysShow;
}
element.separator = undefined;
});
}
const shownElements = this.elements.filter(element => !element.hidden);
// Sort by value
if (this.sortByLabel && query) {
const normalizedSearchValue = query.toLowerCase();
shownElements.sort((a, b) => {
return compareEntries(a, b, normalizedSearchValue);
});
}
this.elementsToIndexes = shownElements.reduce((map, element, index) => {
map.set(element.item, index);
return map;
}, new Map<IQuickPickItem, number>());
this.list.splice(0, this.list.length, shownElements);
this.list.setFocus([]);
this.list.layout();
this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
this._onChangedVisibleCount.fire(shownElements.length);
return true;
}
toggleCheckbox() {
try {
this._fireCheckedEvents = false;
const elements = this.list.getFocusedElements();
const allChecked = this.allVisibleChecked(elements);
for (const element of elements) {
element.checked = !allChecked;
}
} finally {
this._fireCheckedEvents = true;
this.fireCheckedEvents();
}
}
display(display: boolean) {
this.container.style.display = display ? '' : 'none';
}
isDisplayed() {
return this.container.style.display !== 'none';
}
dispose() {
this.elementDisposables = dispose(this.elementDisposables);
this.disposables = dispose(this.disposables);
}
private fireCheckedEvents() {
if (this._fireCheckedEvents) {
this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked());
this._onChangedCheckedCount.fire(this.getCheckedCount());
this._onChangedCheckedElements.fire(this.getCheckedElements());
}
}
private fireButtonTriggered(event: IQuickPickItemButtonEvent<IQuickPickItem>) {
this._onButtonTriggered.fire(event);
}
style(styles: IListStyles) {
this.list.style(styles);
}
}
function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number {
const labelHighlightsA = elementA.labelHighlights || [];
const labelHighlightsB = elementB.labelHighlights || [];
if (labelHighlightsA.length && !labelHighlightsB.length) {
return -1;
}
if (!labelHighlightsA.length && labelHighlightsB.length) {
return 1;
}
if (labelHighlightsA.length === 0 && labelHighlightsB.length === 0) {
return 0;
}
return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor);
}
class QuickInputAccessibilityProvider implements IListAccessibilityProvider<ListElement> {
getWidgetAriaLabel(): string {
return localize('quickInput', "Quick Input");
}
getAriaLabel(element: ListElement): string | null {
return element.saneAriaLabel;
}
getWidgetRole() {
return 'listbox';
}
getRole() {
return 'option';
}
}

View File

@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/quickInput';
import * as dom from 'vs/base/browser/dom';
import { URI } from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
const iconPathToClass: Record<string, string> = {};
const iconClassGenerator = new IdGenerator('quick-input-button-icon-');
export function getIconClass(iconPath: { dark: URI; light?: URI; } | undefined): string | undefined {
if (!iconPath) {
return undefined;
}
let iconClass: string;
const key = iconPath.dark.toString();
if (iconPathToClass[key]) {
iconClass = iconPathToClass[key];
} else {
iconClass = iconClassGenerator.nextId();
dom.createCSSRule(`.${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.light || iconPath.dark)}`);
dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.dark)}`);
iconPathToClass[key] = iconClass;
}
return iconClass;
}

View File

@ -0,0 +1,365 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { URI } from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IMatch } from 'vs/base/common/filters';
import { IItemAccessor } from 'vs/base/common/fuzzyScorer';
import { Schemas } from 'vs/base/common/network';
export interface IQuickPickItemHighlights {
label?: IMatch[];
description?: IMatch[];
detail?: IMatch[];
}
export interface IQuickPickItem {
type?: 'item';
id?: string;
label: string;
ariaLabel?: string;
description?: string;
detail?: string;
/**
* Allows to show a keybinding next to the item to indicate
* how the item can be triggered outside of the picker using
* keyboard shortcut.
*/
keybinding?: ResolvedKeybinding;
iconClasses?: string[];
italic?: boolean;
strikethrough?: boolean;
highlights?: IQuickPickItemHighlights;
buttons?: IQuickInputButton[];
picked?: boolean;
alwaysShow?: boolean;
}
export interface IQuickPickSeparator {
type: 'separator';
label?: string;
}
export interface IKeyMods {
readonly ctrlCmd: boolean;
readonly alt: boolean;
}
export const NO_KEY_MODS: IKeyMods = { ctrlCmd: false, alt: false };
export interface IQuickNavigateConfiguration {
keybindings: ResolvedKeybinding[];
}
export interface IPickOptions<T extends IQuickPickItem> {
/**
* an optional string to show as placeholder in the input box to guide the user what she picks on
*/
placeHolder?: string;
/**
* an optional flag to include the description when filtering the picks
*/
matchOnDescription?: boolean;
/**
* an optional flag to include the detail when filtering the picks
*/
matchOnDetail?: boolean;
/**
* an optional flag to filter the picks based on label. Defaults to true.
*/
matchOnLabel?: boolean;
/**
* an option flag to control whether focus is always automatically brought to a list item. Defaults to true.
*/
autoFocusOnList?: boolean;
/**
* an optional flag to not close the picker on focus lost
*/
ignoreFocusLost?: boolean;
/**
* an optional flag to make this picker multi-select
*/
canPickMany?: boolean;
/**
* enables quick navigate in the picker to open an element without typing
*/
quickNavigate?: IQuickNavigateConfiguration;
/**
* a context key to set when this picker is active
*/
contextKey?: string;
/**
* an optional property for the item to focus initially.
*/
activeItem?: Promise<T> | T;
onKeyMods?: (keyMods: IKeyMods) => void;
onDidFocus?: (entry: T) => void;
onDidTriggerItemButton?: (context: IQuickPickItemButtonContext<T>) => void;
}
export interface IInputOptions {
/**
* the value to prefill in the input box
*/
value?: string;
/**
* the selection of value, default to the whole word
*/
valueSelection?: [number, number];
/**
* the text to display underneath the input box
*/
prompt?: string;
/**
* an optional string to show as placeholder in the input box to guide the user what to type
*/
placeHolder?: string;
/**
* Controls if a password input is shown. Password input hides the typed text.
*/
password?: boolean;
ignoreFocusLost?: boolean;
/**
* an optional function that is used to validate user input.
*/
validateInput?: (input: string) => Promise<string | null | undefined>;
}
export interface IQuickInput extends IDisposable {
readonly onDidHide: Event<void>;
readonly onDispose: Event<void>;
title: string | undefined;
description: string | undefined;
step: number | undefined;
totalSteps: number | undefined;
enabled: boolean;
contextKey: string | undefined;
busy: boolean;
ignoreFocusOut: boolean;
show(): void;
hide(): void;
}
export interface IQuickPickAcceptEvent {
/**
* Signals if the picker item is to be accepted
* in the background while keeping the picker open.
*/
inBackground: boolean;
}
export enum ItemActivation {
NONE,
FIRST,
SECOND,
LAST
}
export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
value: string;
/**
* A method that allows to massage the value used
* for filtering, e.g, to remove certain parts.
*/
filterValue: (value: string) => string;
ariaLabel: string | undefined;
placeholder: string | undefined;
readonly onDidChangeValue: Event<string>;
readonly onDidAccept: Event<IQuickPickAcceptEvent>;
/**
* If enabled, will fire the `onDidAccept` event when
* pressing the arrow-right key with the idea of accepting
* the selected item without closing the picker.
*/
canAcceptInBackground: boolean;
ok: boolean | 'default';
readonly onDidCustom: Event<void>;
customButton: boolean;
customLabel: string | undefined;
customHover: string | undefined;
buttons: ReadonlyArray<IQuickInputButton>;
readonly onDidTriggerButton: Event<IQuickInputButton>;
readonly onDidTriggerItemButton: Event<IQuickPickItemButtonEvent<T>>;
items: ReadonlyArray<T | IQuickPickSeparator>;
canSelectMany: boolean;
matchOnDescription: boolean;
matchOnDetail: boolean;
matchOnLabel: boolean;
sortByLabel: boolean;
autoFocusOnList: boolean;
quickNavigate: IQuickNavigateConfiguration | undefined;
activeItems: ReadonlyArray<T>;
readonly onDidChangeActive: Event<T[]>;
/**
* Allows to control which entry should be activated by default.
*/
itemActivation: ItemActivation;
selectedItems: ReadonlyArray<T>;
readonly onDidChangeSelection: Event<T[]>;
readonly keyMods: IKeyMods;
valueSelection: Readonly<[number, number]> | undefined;
validationMessage: string | undefined;
inputHasFocus(): boolean;
focusOnInput(): void;
/**
* Hides the input box from the picker UI. This is typically used
* in combination with quick-navigation where no search UI should
* be presented.
*/
hideInput: boolean;
hideCheckAll: boolean;
}
export interface IInputBox extends IQuickInput {
value: string;
valueSelection: Readonly<[number, number]> | undefined;
placeholder: string | undefined;
password: boolean;
readonly onDidChangeValue: Event<string>;
readonly onDidAccept: Event<void>;
buttons: ReadonlyArray<IQuickInputButton>;
readonly onDidTriggerButton: Event<IQuickInputButton>;
prompt: string | undefined;
validationMessage: string | undefined;
}
export interface IQuickInputButton {
/** iconPath or iconClass required */
iconPath?: { dark: URI; light?: URI; };
/** iconPath or iconClass required */
iconClass?: string;
tooltip?: string;
/**
* Whether to always show the button. By default buttons
* are only visible when hovering over them with the mouse
*/
alwaysVisible?: boolean;
}
export interface IQuickPickItemButtonEvent<T extends IQuickPickItem> {
button: IQuickInputButton;
item: T;
}
export interface IQuickPickItemButtonContext<T extends IQuickPickItem> extends IQuickPickItemButtonEvent<T> {
removeItem(): void;
}
export type QuickPickInput<T = IQuickPickItem> = T | IQuickPickSeparator;
//region Fuzzy Scorer Support
export type IQuickPickItemWithResource = IQuickPickItem & { resource?: URI };
export class QuickPickItemScorerAccessor implements IItemAccessor<IQuickPickItemWithResource> {
constructor(private options?: { skipDescription?: boolean, skipPath?: boolean }) { }
getItemLabel(entry: IQuickPickItemWithResource): string {
return entry.label;
}
getItemDescription(entry: IQuickPickItemWithResource): string | undefined {
if (this.options?.skipDescription) {
return undefined;
}
return entry.description;
}
getItemPath(entry: IQuickPickItemWithResource): string | undefined {
if (this.options?.skipPath) {
return undefined;
}
if (entry.resource?.scheme === Schemas.file) {
return entry.resource.fsPath;
}
return entry.resource?.path;
}
}
export const quickPickItemScorerAccessor = new QuickPickItemScorerAccessor();
//#endregion

View File

@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { canceled } from 'vs/base/common/errors';
import { VSBuffer, bufferToStream } from 'vs/base/common/buffer';
import { IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
export function request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
if (options.proxyAuthorization) {
options.headers = {
...(options.headers || {}),
'Proxy-Authorization': options.proxyAuthorization
};
}
const xhr = new XMLHttpRequest();
return new Promise<IRequestContext>((resolve, reject) => {
xhr.open(options.type || 'GET', options.url || '', true, options.user, options.password);
setRequestHeaders(xhr, options);
xhr.responseType = 'arraybuffer';
xhr.onerror = e => reject(new Error(xhr.statusText && ('XHR failed: ' + xhr.statusText) || 'XHR failed'));
xhr.onload = (e) => {
resolve({
res: {
statusCode: xhr.status,
headers: getResponseHeaders(xhr)
},
stream: bufferToStream(VSBuffer.wrap(new Uint8Array(xhr.response)))
});
};
xhr.ontimeout = e => reject(new Error(`XHR timeout: ${options.timeout}ms`));
if (options.timeout) {
xhr.timeout = options.timeout;
}
xhr.send(options.data);
// cancel
token.onCancellationRequested(() => {
xhr.abort();
reject(canceled());
});
});
}
function setRequestHeaders(xhr: XMLHttpRequest, options: IRequestOptions): void {
if (options.headers) {
outer: for (let k in options.headers) {
switch (k) {
case 'User-Agent':
case 'Accept-Encoding':
case 'Content-Length':
// unsafe headers
continue outer;
}
xhr.setRequestHeader(k, options.headers[k]);
}
}
}
function getResponseHeaders(xhr: XMLHttpRequest): { [name: string]: string } {
const headers: { [name: string]: string } = Object.create(null);
for (const line of xhr.getAllResponseHeaders().split(/\r\n|\n|\r/g)) {
if (line) {
const idx = line.indexOf(':');
headers[line.substr(0, idx).trim().toLowerCase()] = line.substr(idx + 1).trim();
}
}
return headers;
}

View File

@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBufferReadableStream } from 'vs/base/common/buffer';
export interface IHeaders {
[header: string]: string;
}
export interface IRequestOptions {
type?: string;
url?: string;
user?: string;
password?: string;
headers?: IHeaders;
timeout?: number;
data?: string;
followRedirects?: number;
proxyAuthorization?: string;
}
export interface IRequestContext {
res: {
headers: IHeaders;
statusCode?: number;
};
stream: VSBufferReadableStream;
}

View File

@ -0,0 +1,253 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// #######################################################################
// ### ###
// ### electron.d.ts types we need in a common layer for reuse ###
// ### (copied from Electron 9.x) ###
// ### ###
// #######################################################################
export interface MessageBoxOptions {
/**
* Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows,
* `"question"` displays the same icon as `"info"`, unless you set an icon using
* the `"icon"` option. On macOS, both `"warning"` and `"error"` display the same
* warning icon.
*/
type?: string;
/**
* Array of texts for buttons. On Windows, an empty array will result in one button
* labeled "OK".
*/
buttons?: string[];
/**
* Index of the button in the buttons array which will be selected by default when
* the message box opens.
*/
defaultId?: number;
/**
* Title of the message box, some platforms will not show it.
*/
title?: string;
/**
* Content of the message box.
*/
message: string;
/**
* Extra information of the message.
*/
detail?: string;
/**
* If provided, the message box will include a checkbox with the given label.
*/
checkboxLabel?: string;
/**
* Initial checked state of the checkbox. `false` by default.
*/
checkboxChecked?: boolean;
// icon?: NativeImage;
/**
* The index of the button to be used to cancel the dialog, via the `Esc` key. By
* default this is assigned to the first button with "cancel" or "no" as the label.
* If no such labeled buttons exist and this option is not set, `0` will be used as
* the return value.
*/
cancelId?: number;
/**
* On Windows Electron will try to figure out which one of the `buttons` are common
* buttons (like "Cancel" or "Yes"), and show the others as command links in the
* dialog. This can make the dialog appear in the style of modern Windows apps. If
* you don't like this behavior, you can set `noLink` to `true`.
*/
noLink?: boolean;
/**
* Normalize the keyboard access keys across platforms. Default is `false`.
* Enabling this assumes `&` is used in the button labels for the placement of the
* keyboard shortcut access key and labels will be converted so they work correctly
* on each platform, `&` characters are removed on macOS, converted to `_` on
* Linux, and left untouched on Windows. For example, a button label of `Vie&w`
* will be converted to `Vie_w` on Linux and `View` on macOS and can be selected
* via `Alt-W` on Windows and Linux.
*/
normalizeAccessKeys?: boolean;
}
export interface MessageBoxReturnValue {
/**
* The index of the clicked button.
*/
response: number;
/**
* The checked state of the checkbox if `checkboxLabel` was set. Otherwise `false`.
*/
checkboxChecked: boolean;
}
export interface OpenDevToolsOptions {
/**
* Opens the devtools with specified dock state, can be `right`, `bottom`,
* `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's
* possible to dock back. In `detach` mode it's not.
*/
mode: ('right' | 'bottom' | 'undocked' | 'detach');
/**
* Whether to bring the opened devtools window to the foreground. The default is
* `true`.
*/
activate?: boolean;
}
export interface SaveDialogOptions {
title?: string;
/**
* Absolute directory path, absolute file path, or file name to use by default.
*/
defaultPath?: string;
/**
* Custom label for the confirmation button, when left empty the default label will
* be used.
*/
buttonLabel?: string;
filters?: FileFilter[];
/**
* Message to display above text fields.
*
* @platform darwin
*/
message?: string;
/**
* Custom label for the text displayed in front of the filename text field.
*
* @platform darwin
*/
nameFieldLabel?: string;
/**
* Show the tags input box, defaults to `true`.
*
* @platform darwin
*/
showsTagField?: boolean;
properties?: Array<'showHiddenFiles' | 'createDirectory' | 'treatPackageAsDirectory' | 'showOverwriteConfirmation' | 'dontAddToRecent'>;
/**
* Create a security scoped bookmark when packaged for the Mac App Store. If this
* option is enabled and the file doesn't already exist a blank file will be
* created at the chosen path.
*
* @platform darwin,mas
*/
securityScopedBookmarks?: boolean;
}
export interface OpenDialogOptions {
title?: string;
defaultPath?: string;
/**
* Custom label for the confirmation button, when left empty the default label will
* be used.
*/
buttonLabel?: string;
filters?: FileFilter[];
/**
* Contains which features the dialog should use. The following values are
* supported:
*/
properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory' | 'dontAddToRecent'>;
/**
* Message to display above input boxes.
*
* @platform darwin
*/
message?: string;
/**
* Create security scoped bookmarks when packaged for the Mac App Store.
*
* @platform darwin,mas
*/
securityScopedBookmarks?: boolean;
}
export interface OpenDialogReturnValue {
/**
* whether or not the dialog was canceled.
*/
canceled: boolean;
/**
* An array of file paths chosen by the user. If the dialog is cancelled this will
* be an empty array.
*/
filePaths: string[];
/**
* An array matching the `filePaths` array of base64 encoded strings which contains
* security scoped bookmark data. `securityScopedBookmarks` must be enabled for
* this to be populated. (For return values, see table here.)
*
* @platform darwin,mas
*/
bookmarks?: string[];
}
export interface SaveDialogReturnValue {
/**
* whether or not the dialog was canceled.
*/
canceled: boolean;
/**
* If the dialog is canceled, this will be `undefined`.
*/
filePath?: string;
/**
* Base64 encoded string which contains the security scoped bookmark data for the
* saved file. `securityScopedBookmarks` must be enabled for this to be present.
* (For return values, see table here.)
*
* @platform darwin,mas
*/
bookmark?: string;
}
export interface FileFilter {
// Docs: http://electronjs.org/docs/api/structures/file-filter
extensions: string[];
name: string;
}
export interface InputEvent {
// Docs: http://electronjs.org/docs/api/structures/input-event
/**
* An array of modifiers of the event, can be `shift`, `control`, `ctrl`, `alt`,
* `meta`, `command`, `cmd`, `isKeypad`, `isAutoRepeat`, `leftButtonDown`,
* `middleButtonDown`, `rightButtonDown`, `capsLock`, `numLock`, `left`, `right`.
*/
modifiers?: Array<'shift' | 'control' | 'ctrl' | 'alt' | 'meta' | 'command' | 'cmd' | 'isKeypad' | 'isAutoRepeat' | 'leftButtonDown' | 'middleButtonDown' | 'rightButtonDown' | 'capsLock' | 'numLock' | 'left' | 'right'>;
}
export interface MouseInputEvent extends InputEvent {
// Docs: http://electronjs.org/docs/api/structures/mouse-input-event
/**
* The button pressed, can be `left`, `middle`, `right`.
*/
button?: ('left' | 'middle' | 'right');
clickCount?: number;
globalX?: number;
globalY?: number;
movementX?: number;
movementY?: number;
/**
* The type of the event, can be `mouseDown`, `mouseUp`, `mouseEnter`,
* `mouseLeave`, `contextMenu`, `mouseWheel` or `mouseMove`.
*/
type: ('mouseDown' | 'mouseUp' | 'mouseEnter' | 'mouseLeave' | 'contextMenu' | 'mouseWheel' | 'mouseMove');
x: number;
y: number;
}

View File

@ -0,0 +1,250 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// @ts-check
(function () {
'use strict';
const { ipcRenderer, webFrame, crashReporter, contextBridge } = require('electron');
// #######################################################################
// ### ###
// ### !!! DO NOT USE GET/SET PROPERTIES ANYWHERE HERE !!! ###
// ### !!! UNLESS THE ACCESS IS WITHOUT SIDE EFFECTS !!! ###
// ### (https://github.com/electron/electron/issues/25516) ###
// ### ###
// #######################################################################
const globals = {
/**
* A minimal set of methods exposed from Electron's `ipcRenderer`
* to support communication to main process.
*/
ipcRenderer: {
/**
* @param {string} channel
* @param {any[]} args
*/
send(channel, ...args) {
if (validateIPC(channel)) {
ipcRenderer.send(channel, ...args);
}
},
/**
* @param {string} channel
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
*/
on(channel, listener) {
if (validateIPC(channel)) {
ipcRenderer.on(channel, listener);
}
},
/**
* @param {string} channel
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
*/
once(channel, listener) {
if (validateIPC(channel)) {
ipcRenderer.once(channel, listener);
}
},
/**
* @param {string} channel
* @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener
*/
removeListener(channel, listener) {
if (validateIPC(channel)) {
ipcRenderer.removeListener(channel, listener);
}
}
},
/**
* Support for subset of methods of Electron's `webFrame` type.
*/
webFrame: {
/**
* @param {number} level
*/
setZoomLevel(level) {
if (typeof level === 'number') {
webFrame.setZoomLevel(level);
}
}
},
/**
* Support for subset of methods of Electron's `crashReporter` type.
*/
crashReporter: {
/**
* @param {string} key
* @param {string} value
*/
addExtraParameter(key, value) {
crashReporter.addExtraParameter(key, value);
}
},
/**
* Support for a subset of access to node.js global `process`.
*/
process: {
get platform() { return process.platform; },
get env() { return process.env; },
get versions() { return process.versions; },
get type() { return 'renderer'; },
_whenEnvResolved: undefined,
whenEnvResolved:
/**
* @returns when the shell environment has been resolved.
*/
function () {
if (!this._whenEnvResolved) {
this._whenEnvResolved = resolveEnv();
}
return this._whenEnvResolved;
},
nextTick:
/**
* Adds callback to the "next tick queue". This queue is fully drained
* after the current operation on the JavaScript stack runs to completion
* and before the event loop is allowed to continue.
*
* @param {Function} callback
* @param {any[]} args
*/
function nextTick(callback, ...args) {
return process.nextTick(callback, ...args);
},
cwd:
/**
* @returns the current working directory.
*/
function () {
return process.cwd();
},
getuid:
/**
* @returns the numeric user identity of the process
*/
function () {
return process.getuid();
},
getProcessMemoryInfo:
/**
* @returns {Promise<import('electron').ProcessMemoryInfo>}
*/
function () {
return process.getProcessMemoryInfo();
},
on:
/**
* @param {string} type
* @param {() => void} callback
*/
function (type, callback) {
if (validateProcessEventType(type)) {
process.on(type, callback);
}
}
},
/**
* Some information about the context we are running in.
*/
context: {
get sandbox() { return process.argv.includes('--enable-sandbox'); }
}
};
// Use `contextBridge` APIs to expose globals to VSCode
// only if context isolation is enabled, otherwise just
// add to the DOM global.
let useContextBridge = process.argv.includes('--context-isolation');
if (useContextBridge) {
try {
contextBridge.exposeInMainWorld('vscode', globals);
} catch (error) {
console.error(error);
useContextBridge = false;
}
}
if (!useContextBridge) {
// @ts-ignore
window.vscode = globals;
}
//#region Utilities
/**
* @param {string} channel
*/
function validateIPC(channel) {
if (!channel || !channel.startsWith('vscode:')) {
throw new Error(`Unsupported event IPC channel '${channel}'`);
}
return true;
}
/**
* @param {string} type
* @returns {type is 'uncaughtException'}
*/
function validateProcessEventType(type) {
if (type !== 'uncaughtException') {
throw new Error(`Unsupported process event '${type}'`);
}
return true;
}
/**
* If VSCode is not run from a terminal, we should resolve additional
* shell specific environment from the OS shell to ensure we are seeing
* all development related environment variables. We do this from the
* main process because it may involve spawning a shell.
*/
function resolveEnv() {
return new Promise(function (resolve) {
const handle = setTimeout(function () {
console.warn('Preload: Unable to resolve shell environment in a reasonable time');
// It took too long to fetch the shell environment, return
resolve();
}, 3000);
ipcRenderer.once('vscode:acceptShellEnv', function (event, shellEnv) {
clearTimeout(handle);
// Assign all keys of the shell environment to our process environment
Object.assign(process.env, shellEnv);
resolve();
});
ipcRenderer.send('vscode:fetchShellEnv');
});
}
//#endregion
}());

View File

@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// #######################################################################
// ### ###
// ### electron.d.ts types we expose from electron-sandbox ###
// ### (copied from Electron 9.x) ###
// ### ###
// #######################################################################
export interface IpcRenderer {
/**
* Listens to `channel`, when a new message arrives `listener` would be called with
* `listener(event, args...)`.
*/
on(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
/**
* Adds a one time `listener` function for the event. This `listener` is invoked
* only the next time a message is sent to `channel`, after which it is removed.
*/
once(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
/**
* Removes the specified `listener` from the listener array for the specified
* `channel`.
*/
removeListener(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
/**
* Send an asynchronous message to the main process via `channel`, along with
* arguments. Arguments will be serialized with the Structured Clone Algorithm,
* just like `postMessage`, so prototype chains will not be included. Sending
* Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception.
*
* > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special
* Electron objects is deprecated, and will begin throwing an exception starting
* with Electron 9.
*
* The main process handles it by listening for `channel` with the `ipcMain`
* module.
*/
send(channel: string, ...args: any[]): void;
}
export interface WebFrame {
/**
* Changes the zoom level to the specified level. The original size is 0 and each
* increment above or below represents zooming 20% larger or smaller to default
* limits of 300% and 50% of original size, respectively.
*/
setZoomLevel(level: number): void;
}
export interface CrashReporter {
/**
* Set an extra parameter to be sent with the crash report. The values specified
* here will be sent in addition to any values set via the `extra` option when
* `start` was called.
*
* Parameters added in this fashion (or via the `extra` parameter to
* `crashReporter.start`) are specific to the calling process. Adding extra
* parameters in the main process will not cause those parameters to be sent along
* with crashes from renderer or other child processes. Similarly, adding extra
* parameters in a renderer process will not result in those parameters being sent
* with crashes that occur in other renderer processes or in the main process.
*
* **Note:** Parameters have limits on the length of the keys and values. Key names
* must be no longer than 39 bytes, and values must be no longer than 127 bytes.
* Keys with names longer than the maximum will be silently ignored. Key values
* longer than the maximum length will be truncated.
*/
addExtraParameter(key: string, value: string): void;
}
export interface ProcessMemoryInfo {
// Docs: http://electronjs.org/docs/api/structures/process-memory-info
/**
* The amount of memory not shared by other processes, such as JS heap or HTML
* content in Kilobytes.
*/
private: number;
/**
* The amount of memory currently pinned to actual physical RAM in Kilobytes.
*
* @platform linux,win32
*/
residentSet: number;
/**
* The amount of memory shared between processes, typically memory consumed by the
* Electron code itself in Kilobytes.
*/
shared: number;
}
export interface CrashReporterStartOptions {
/**
* URL that crash reports will be sent to as POST.
*/
submitURL: string;
/**
* Defaults to `app.name`.
*/
productName?: string;
/**
* Deprecated alias for `{ globalExtra: { _companyName: ... } }`.
*
* @deprecated
*/
companyName?: string;
/**
* Whether crash reports should be sent to the server. If false, crash reports will
* be collected and stored in the crashes directory, but not uploaded. Default is
* `true`.
*/
uploadToServer?: boolean;
/**
* If true, crashes generated in the main process will not be forwarded to the
* system crash handler. Default is `false`.
*/
ignoreSystemCrashHandler?: boolean;
/**
* If true, limit the number of crashes uploaded to 1/hour. Default is `false`.
*
* @platform darwin,win32
*/
rateLimit?: boolean;
/**
* If true, crash reports will be compressed and uploaded with `Content-Encoding:
* gzip`. Not all collection servers support compressed payloads. Default is
* `false`.
*
* @platform darwin,win32
*/
compress?: boolean;
/**
* Extra string key/value annotations that will be sent along with crash reports
* that are generated in the main process. Only string values are supported.
* Crashes generated in child processes will not contain these extra parameters to
* crash reports generated from child processes, call `addExtraParameter` from the
* child process.
*/
extra?: Record<string, string>;
/**
* Extra string key/value annotations that will be sent along with any crash
* reports generated in any process. These annotations cannot be changed once the
* crash reporter has been started. If a key is present in both the global extra
* parameters and the process-specific extra parameters, then the global one will
* take precedence. By default, `productName` and the app version are included, as
* well as the Electron version.
*/
globalExtra?: Record<string, string>;
}
/**
* Additional information around a `app.on('login')` event.
*/
export interface AuthInfo {
isProxy: boolean;
scheme: string;
host: string;
port: number;
realm: string;
}

View File

@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { globals, INodeProcess, IProcessEnvironment } from 'vs/base/common/platform';
import { ProcessMemoryInfo, CrashReporter, IpcRenderer, WebFrame } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes';
export interface ISandboxNodeProcess extends INodeProcess {
/**
* The process.platform property returns a string identifying the operating system platform
* on which the Node.js process is running.
*/
platform: 'win32' | 'linux' | 'darwin';
/**
* The type will always be Electron renderer.
*/
type: 'renderer';
/**
* A list of versions for the current node.js/electron configuration.
*/
versions: { [key: string]: string | undefined };
/**
* The process.env property returns an object containing the user environment.
*/
env: IProcessEnvironment;
/**
* The current working directory.
*/
cwd(): string;
/**
* Returns the numeric user identity of the process.
*/
getuid(): number;
/**
* Allows to await resolving the full process environment by checking for the shell environment
* of the OS in certain cases (e.g. when the app is started from the Dock on macOS).
*/
whenEnvResolved(): Promise<void>;
/**
* Adds callback to the "next tick queue". This queue is fully drained
* after the current operation on the JavaScript stack runs to completion
* and before the event loop is allowed to continue.
*/
nextTick(callback: (...args: any[]) => void, ...args: any[]): void;
/**
* A listener on the process. Only a small subset of listener types are allowed.
*/
on: (type: string, callback: Function) => void;
/**
* Resolves with a ProcessMemoryInfo
*
* Returns an object giving memory usage statistics about the current process. Note
* that all statistics are reported in Kilobytes. This api should be called after
* app ready.
*
* Chromium does not provide `residentSet` value for macOS. This is because macOS
* performs in-memory compression of pages that haven't been recently used. As a
* result the resident set size value is not what one would expect. `private`
* memory is more representative of the actual pre-compression memory usage of the
* process on macOS.
*/
getProcessMemoryInfo: () => Promise<ProcessMemoryInfo>;
}
export interface ISandboxContext {
/**
* Wether the renderer runs with `sandbox` enabled or not.
*/
sandbox: boolean;
}
export const ipcRenderer: IpcRenderer = globals.vscode.ipcRenderer;
export const webFrame: WebFrame = globals.vscode.webFrame;
export const crashReporter: CrashReporter = globals.vscode.crashReporter;
export const process: ISandboxNodeProcess = globals.vscode.process;
export const context: ISandboxContext = globals.vscode.context;

View File

@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { ipcRenderer, crashReporter, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals';
suite('Sandbox', () => {
test('globals', () => {
assert.ok(ipcRenderer);
assert.ok(crashReporter);
assert.ok(webFrame);
});
});

View File

@ -0,0 +1,314 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { ThrottledDelayer } from 'vs/base/common/async';
import { isUndefinedOrNull } from 'vs/base/common/types';
export enum StorageHint {
// A hint to the storage that the storage
// does not exist on disk yet. This allows
// the storage library to improve startup
// time by not checking the storage for data.
STORAGE_DOES_NOT_EXIST
}
export interface IStorageOptions {
readonly hint?: StorageHint;
}
export interface IUpdateRequest {
readonly insert?: Map<string, string>;
readonly delete?: Set<string>;
}
export interface IStorageItemsChangeEvent {
readonly changed?: Map<string, string>;
readonly deleted?: Set<string>;
}
export interface IStorageDatabase {
readonly onDidChangeItemsExternal: Event<IStorageItemsChangeEvent>;
getItems(): Promise<Map<string, string>>;
updateItems(request: IUpdateRequest): Promise<void>;
close(recovery?: () => Map<string, string>): Promise<void>;
}
export interface IStorage extends IDisposable {
readonly items: Map<string, string>;
readonly size: number;
readonly onDidChangeStorage: Event<string>;
init(): Promise<void>;
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
set(key: string, value: string | boolean | number | undefined | null): Promise<void>;
delete(key: string): Promise<void>;
close(): Promise<void>;
}
enum StorageState {
None,
Initialized,
Closed
}
export class Storage extends Disposable implements IStorage {
private static readonly DEFAULT_FLUSH_DELAY = 100;
private readonly _onDidChangeStorage = this._register(new Emitter<string>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private state = StorageState.None;
private cache = new Map<string, string>();
private readonly flushDelayer = this._register(new ThrottledDelayer<void>(Storage.DEFAULT_FLUSH_DELAY));
private pendingDeletes = new Set<string>();
private pendingInserts = new Map<string, string>();
constructor(
protected readonly database: IStorageDatabase,
private readonly options: IStorageOptions = Object.create(null)
) {
super();
this.registerListeners();
}
private registerListeners(): void {
this._register(this.database.onDidChangeItemsExternal(e => this.onDidChangeItemsExternal(e)));
}
private onDidChangeItemsExternal(e: IStorageItemsChangeEvent): void {
// items that change external require us to update our
// caches with the values. we just accept the value and
// emit an event if there is a change.
e.changed?.forEach((value, key) => this.accept(key, value));
e.deleted?.forEach(key => this.accept(key, undefined));
}
private accept(key: string, value: string | undefined): void {
if (this.state === StorageState.Closed) {
return; // Return early if we are already closed
}
let changed = false;
// Item got removed, check for deletion
if (isUndefinedOrNull(value)) {
changed = this.cache.delete(key);
}
// Item got updated, check for change
else {
const currentValue = this.cache.get(key);
if (currentValue !== value) {
this.cache.set(key, value);
changed = true;
}
}
// Signal to outside listeners
if (changed) {
this._onDidChangeStorage.fire(key);
}
}
get items(): Map<string, string> {
return this.cache;
}
get size(): number {
return this.cache.size;
}
async init(): Promise<void> {
if (this.state !== StorageState.None) {
return; // either closed or already initialized
}
this.state = StorageState.Initialized;
if (this.options.hint === StorageHint.STORAGE_DOES_NOT_EXIST) {
// return early if we know the storage file does not exist. this is a performance
// optimization to not load all items of the underlying storage if we know that
// there can be no items because the storage does not exist.
return;
}
this.cache = await this.database.getItems();
}
get(key: string, fallbackValue: string): string;
get(key: string, fallbackValue?: string): string | undefined;
get(key: string, fallbackValue?: string): string | undefined {
const value = this.cache.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value;
}
getBoolean(key: string, fallbackValue: boolean): boolean;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined;
getBoolean(key: string, fallbackValue?: boolean): boolean | undefined {
const value = this.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return value === 'true';
}
getNumber(key: string, fallbackValue: number): number;
getNumber(key: string, fallbackValue?: number): number | undefined;
getNumber(key: string, fallbackValue?: number): number | undefined {
const value = this.get(key);
if (isUndefinedOrNull(value)) {
return fallbackValue;
}
return parseInt(value, 10);
}
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // Return early if we are already closed
}
// We remove the key for undefined/null values
if (isUndefinedOrNull(value)) {
return this.delete(key);
}
// Otherwise, convert to String and store
const valueStr = String(value);
// Return early if value already set
const currentValue = this.cache.get(key);
if (currentValue === valueStr) {
return Promise.resolve();
}
// Update in cache and pending
this.cache.set(key, valueStr);
this.pendingInserts.set(key, valueStr);
this.pendingDeletes.delete(key);
// Event
this._onDidChangeStorage.fire(key);
// Accumulate work by scheduling after timeout
return this.flushDelayer.trigger(() => this.flushPending());
}
delete(key: string): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // Return early if we are already closed
}
// Remove from cache and add to pending
const wasDeleted = this.cache.delete(key);
if (!wasDeleted) {
return Promise.resolve(); // Return early if value already deleted
}
if (!this.pendingDeletes.has(key)) {
this.pendingDeletes.add(key);
}
this.pendingInserts.delete(key);
// Event
this._onDidChangeStorage.fire(key);
// Accumulate work by scheduling after timeout
return this.flushDelayer.trigger(() => this.flushPending());
}
async close(): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // return if already closed
}
// Update state
this.state = StorageState.Closed;
// Trigger new flush to ensure data is persisted and then close
// even if there is an error flushing. We must always ensure
// the DB is closed to avoid corruption.
//
// Recovery: we pass our cache over as recovery option in case
// the DB is not healthy.
try {
await this.flushDelayer.trigger(() => this.flushPending(), 0 /* as soon as possible */);
} catch (error) {
// Ignore
}
await this.database.close(() => this.cache);
}
private flushPending(): Promise<void> {
if (this.pendingInserts.size === 0 && this.pendingDeletes.size === 0) {
return Promise.resolve(); // return early if nothing to do
}
// Get pending data
const updateRequest: IUpdateRequest = { insert: this.pendingInserts, delete: this.pendingDeletes };
// Reset pending data for next run
this.pendingDeletes = new Set<string>();
this.pendingInserts = new Map<string, string>();
// Update in storage
return this.database.updateItems(updateRequest);
}
}
export class InMemoryStorageDatabase implements IStorageDatabase {
readonly onDidChangeItemsExternal = Event.None;
private readonly items = new Map<string, string>();
async getItems(): Promise<Map<string, string>> {
return this.items;
}
async updateItems(request: IUpdateRequest): Promise<void> {
if (request.insert) {
request.insert.forEach((value, key) => this.items.set(key, value));
}
if (request.delete) {
request.delete.forEach(key => this.items.delete(key));
}
}
async close(): Promise<void> { }
}

View File

@ -0,0 +1,443 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { Database, Statement } from 'vscode-sqlite3';
import { Event } from 'vs/base/common/event';
import { timeout } from 'vs/base/common/async';
import { mapToString, setToString } from 'vs/base/common/map';
import { basename } from 'vs/base/common/path';
import { copy, renameIgnoreError, unlink } from 'vs/base/node/pfs';
import { IStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest } from 'vs/base/parts/storage/common/storage';
interface IDatabaseConnection {
readonly db: Database;
readonly isInMemory: boolean;
isErroneous?: boolean;
lastError?: string;
}
export interface ISQLiteStorageDatabaseOptions {
readonly logging?: ISQLiteStorageDatabaseLoggingOptions;
}
export interface ISQLiteStorageDatabaseLoggingOptions {
logError?: (error: string | Error) => void;
logTrace?: (msg: string) => void;
}
export class SQLiteStorageDatabase implements IStorageDatabase {
static readonly IN_MEMORY_PATH = ':memory:';
get onDidChangeItemsExternal(): Event<IStorageItemsChangeEvent> { return Event.None; } // since we are the only client, there can be no external changes
private static readonly BUSY_OPEN_TIMEOUT = 2000; // timeout in ms to retry when opening DB fails with SQLITE_BUSY
private static readonly MAX_HOST_PARAMETERS = 256; // maximum number of parameters within a statement
private readonly name = basename(this.path);
private readonly logger = new SQLiteStorageDatabaseLogger(this.options.logging);
private readonly whenConnected = this.connect(this.path);
constructor(private readonly path: string, private readonly options: ISQLiteStorageDatabaseOptions = Object.create(null)) { }
async getItems(): Promise<Map<string, string>> {
const connection = await this.whenConnected;
const items = new Map<string, string>();
const rows = await this.all(connection, 'SELECT * FROM ItemTable');
rows.forEach(row => items.set(row.key, row.value));
if (this.logger.isTracing) {
this.logger.trace(`[storage ${this.name}] getItems(): ${items.size} rows`);
}
return items;
}
async updateItems(request: IUpdateRequest): Promise<void> {
const connection = await this.whenConnected;
return this.doUpdateItems(connection, request);
}
private doUpdateItems(connection: IDatabaseConnection, request: IUpdateRequest): Promise<void> {
if (this.logger.isTracing) {
this.logger.trace(`[storage ${this.name}] updateItems(): insert(${request.insert ? mapToString(request.insert) : '0'}), delete(${request.delete ? setToString(request.delete) : '0'})`);
}
return this.transaction(connection, () => {
const toInsert = request.insert;
const toDelete = request.delete;
// INSERT
if (toInsert && toInsert.size > 0) {
const keysValuesChunks: (string[])[] = [];
keysValuesChunks.push([]); // seed with initial empty chunk
// Split key/values into chunks of SQLiteStorageDatabase.MAX_HOST_PARAMETERS
// so that we can efficiently run the INSERT with as many HOST parameters as possible
let currentChunkIndex = 0;
toInsert.forEach((value, key) => {
let keyValueChunk = keysValuesChunks[currentChunkIndex];
if (keyValueChunk.length > SQLiteStorageDatabase.MAX_HOST_PARAMETERS) {
currentChunkIndex++;
keyValueChunk = [];
keysValuesChunks.push(keyValueChunk);
}
keyValueChunk.push(key, value);
});
keysValuesChunks.forEach(keysValuesChunk => {
this.prepare(connection, `INSERT INTO ItemTable VALUES ${new Array(keysValuesChunk.length / 2).fill('(?,?)').join(',')}`, stmt => stmt.run(keysValuesChunk), () => {
const keys: string[] = [];
let length = 0;
toInsert.forEach((value, key) => {
keys.push(key);
length += value.length;
});
return `Keys: ${keys.join(', ')} Length: ${length}`;
});
});
}
// DELETE
if (toDelete && toDelete.size) {
const keysChunks: (string[])[] = [];
keysChunks.push([]); // seed with initial empty chunk
// Split keys into chunks of SQLiteStorageDatabase.MAX_HOST_PARAMETERS
// so that we can efficiently run the DELETE with as many HOST parameters
// as possible
let currentChunkIndex = 0;
toDelete.forEach(key => {
let keyChunk = keysChunks[currentChunkIndex];
if (keyChunk.length > SQLiteStorageDatabase.MAX_HOST_PARAMETERS) {
currentChunkIndex++;
keyChunk = [];
keysChunks.push(keyChunk);
}
keyChunk.push(key);
});
keysChunks.forEach(keysChunk => {
this.prepare(connection, `DELETE FROM ItemTable WHERE key IN (${new Array(keysChunk.length).fill('?').join(',')})`, stmt => stmt.run(keysChunk), () => {
const keys: string[] = [];
toDelete.forEach(key => {
keys.push(key);
});
return `Keys: ${keys.join(', ')}`;
});
});
}
});
}
async close(recovery?: () => Map<string, string>): Promise<void> {
this.logger.trace(`[storage ${this.name}] close()`);
const connection = await this.whenConnected;
return this.doClose(connection, recovery);
}
private doClose(connection: IDatabaseConnection, recovery?: () => Map<string, string>): Promise<void> {
return new Promise((resolve, reject) => {
connection.db.close(closeError => {
if (closeError) {
this.handleSQLiteError(connection, `[storage ${this.name}] close(): ${closeError}`);
}
// Return early if this storage was created only in-memory
// e.g. when running tests we do not need to backup.
if (this.path === SQLiteStorageDatabase.IN_MEMORY_PATH) {
return resolve();
}
// If the DB closed successfully and we are not running in-memory
// and the DB did not get errors during runtime, make a backup
// of the DB so that we can use it as fallback in case the actual
// DB becomes corrupt in the future.
if (!connection.isErroneous && !connection.isInMemory) {
return this.backup().then(resolve, error => {
this.logger.error(`[storage ${this.name}] backup(): ${error}`);
return resolve(); // ignore failing backup
});
}
// Recovery: if we detected errors while using the DB or we are using
// an inmemory DB (as a fallback to not being able to open the DB initially)
// and we have a recovery function provided, we recreate the DB with this
// data to recover all known data without loss if possible.
if (typeof recovery === 'function') {
// Delete the existing DB. If the path does not exist or fails to
// be deleted, we do not try to recover anymore because we assume
// that the path is no longer writeable for us.
return unlink(this.path).then(() => {
// Re-open the DB fresh
return this.doConnect(this.path).then(recoveryConnection => {
const closeRecoveryConnection = () => {
return this.doClose(recoveryConnection, undefined /* do not attempt to recover again */);
};
// Store items
return this.doUpdateItems(recoveryConnection, { insert: recovery() }).then(() => closeRecoveryConnection(), error => {
// In case of an error updating items, still ensure to close the connection
// to prevent SQLITE_BUSY errors when the connection is reestablished
closeRecoveryConnection();
return Promise.reject(error);
});
});
}).then(resolve, reject);
}
// Finally without recovery we just reject
return reject(closeError || new Error('Database has errors or is in-memory without recovery option'));
});
});
}
private backup(): Promise<void> {
const backupPath = this.toBackupPath(this.path);
return copy(this.path, backupPath);
}
private toBackupPath(path: string): string {
return `${path}.backup`;
}
async checkIntegrity(full: boolean): Promise<string> {
this.logger.trace(`[storage ${this.name}] checkIntegrity(full: ${full})`);
const connection = await this.whenConnected;
const row = await this.get(connection, full ? 'PRAGMA integrity_check' : 'PRAGMA quick_check');
const integrity = full ? (row as any)['integrity_check'] : (row as any)['quick_check'];
if (connection.isErroneous) {
return `${integrity} (last error: ${connection.lastError})`;
}
if (connection.isInMemory) {
return `${integrity} (in-memory!)`;
}
return integrity;
}
private async connect(path: string, retryOnBusy: boolean = true): Promise<IDatabaseConnection> {
this.logger.trace(`[storage ${this.name}] open(${path}, retryOnBusy: ${retryOnBusy})`);
try {
return await this.doConnect(path);
} catch (error) {
this.logger.error(`[storage ${this.name}] open(): Unable to open DB due to ${error}`);
// SQLITE_BUSY should only arise if another process is locking the same DB we want
// to open at that time. This typically never happens because a DB connection is
// limited per window. However, in the event of a window reload, it may be possible
// that the previous connection was not properly closed while the new connection is
// already established.
//
// In this case we simply wait for some time and retry once to establish the connection.
//
if (error.code === 'SQLITE_BUSY' && retryOnBusy) {
await timeout(SQLiteStorageDatabase.BUSY_OPEN_TIMEOUT);
return this.connect(path, false /* not another retry */);
}
// Otherwise, best we can do is to recover from a backup if that exists, as such we
// move the DB to a different filename and try to load from backup. If that fails,
// a new empty DB is being created automatically.
//
// The final fallback is to use an in-memory DB which should only happen if the target
// folder is really not writeable for us.
//
try {
await unlink(path);
await renameIgnoreError(this.toBackupPath(path), path);
return await this.doConnect(path);
} catch (error) {
this.logger.error(`[storage ${this.name}] open(): Unable to use backup due to ${error}`);
// In case of any error to open the DB, use an in-memory
// DB so that we always have a valid DB to talk to.
return this.doConnect(SQLiteStorageDatabase.IN_MEMORY_PATH);
}
}
}
private handleSQLiteError(connection: IDatabaseConnection, msg: string): void {
connection.isErroneous = true;
connection.lastError = msg;
this.logger.error(msg);
}
private doConnect(path: string): Promise<IDatabaseConnection> {
return new Promise((resolve, reject) => {
import('vscode-sqlite3').then(sqlite3 => {
const connection: IDatabaseConnection = {
db: new (this.logger.isTracing ? sqlite3.verbose().Database : sqlite3.Database)(path, error => {
if (error) {
return connection.db ? connection.db.close(() => reject(error)) : reject(error);
}
// The following exec() statement serves two purposes:
// - create the DB if it does not exist yet
// - validate that the DB is not corrupt (the open() call does not throw otherwise)
return this.exec(connection, [
'PRAGMA user_version = 1;',
'CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)'
].join('')).then(() => {
return resolve(connection);
}, error => {
return connection.db.close(() => reject(error));
});
}),
isInMemory: path === SQLiteStorageDatabase.IN_MEMORY_PATH
};
// Errors
connection.db.on('error', error => this.handleSQLiteError(connection, `[storage ${this.name}] Error (event): ${error}`));
// Tracing
if (this.logger.isTracing) {
connection.db.on('trace', sql => this.logger.trace(`[storage ${this.name}] Trace (event): ${sql}`));
}
}, reject);
});
}
private exec(connection: IDatabaseConnection, sql: string): Promise<void> {
return new Promise((resolve, reject) => {
connection.db.exec(sql, error => {
if (error) {
this.handleSQLiteError(connection, `[storage ${this.name}] exec(): ${error}`);
return reject(error);
}
return resolve();
});
});
}
private get(connection: IDatabaseConnection, sql: string): Promise<object> {
return new Promise((resolve, reject) => {
connection.db.get(sql, (error, row) => {
if (error) {
this.handleSQLiteError(connection, `[storage ${this.name}] get(): ${error}`);
return reject(error);
}
return resolve(row);
});
});
}
private all(connection: IDatabaseConnection, sql: string): Promise<{ key: string, value: string }[]> {
return new Promise((resolve, reject) => {
connection.db.all(sql, (error, rows) => {
if (error) {
this.handleSQLiteError(connection, `[storage ${this.name}] all(): ${error}`);
return reject(error);
}
return resolve(rows);
});
});
}
private transaction(connection: IDatabaseConnection, transactions: () => void): Promise<void> {
return new Promise((resolve, reject) => {
connection.db.serialize(() => {
connection.db.run('BEGIN TRANSACTION');
transactions();
connection.db.run('END TRANSACTION', error => {
if (error) {
this.handleSQLiteError(connection, `[storage ${this.name}] transaction(): ${error}`);
return reject(error);
}
return resolve();
});
});
});
}
private prepare(connection: IDatabaseConnection, sql: string, runCallback: (stmt: Statement) => void, errorDetails: () => string): void {
const stmt = connection.db.prepare(sql);
const statementErrorListener = (error: Error) => {
this.handleSQLiteError(connection, `[storage ${this.name}] prepare(): ${error} (${sql}). Details: ${errorDetails()}`);
};
stmt.on('error', statementErrorListener);
runCallback(stmt);
stmt.finalize(error => {
if (error) {
statementErrorListener(error);
}
stmt.removeListener('error', statementErrorListener);
});
}
}
class SQLiteStorageDatabaseLogger {
private readonly logTrace: ((msg: string) => void) | undefined;
private readonly logError: ((error: string | Error) => void) | undefined;
constructor(options?: ISQLiteStorageDatabaseLoggingOptions) {
if (options && typeof options.logTrace === 'function') {
this.logTrace = options.logTrace;
}
if (options && typeof options.logError === 'function') {
this.logError = options.logError;
}
}
get isTracing(): boolean {
return !!this.logTrace;
}
trace(msg: string): void {
if (this.logTrace) {
this.logTrace(msg);
}
}
error(error: string | Error): void {
if (this.logError) {
this.logError(error);
}
}
}

File diff suppressed because one or more lines are too long