Archived
1
0

chore(vscode): update to 1.53.2

These conflicts will be resolved in the following commits. We do it this way so
that PR review is possible.
This commit is contained in:
Joe Previte
2021-02-25 11:27:27 -07:00
1900 changed files with 83066 additions and 64589 deletions

View File

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from 'vs/base/common/lifecycle';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp';
/**
* An implementation of a `IPCClient` on top of DOM `MessagePort`.
*/
export class Client extends MessagePortClient implements IDisposable {
/**
* @param clientId a way to uniquely identify this client among
* other clients. this is important for routing because every
* client can also be a server
*/
constructor(port: MessagePort, clientId: string) {
super(port, clientId);
}
}

View File

@ -11,6 +11,11 @@ export interface Sender {
send(channel: string, msg: unknown): void;
}
/**
* The Electron `Protocol` leverages Electron style IPC communication (`ipcRenderer`, `ipcMain`)
* for the implementation of the `IMessagePassingProtocol`. That style of API requires a channel
* name for sending data.
*/
export class Protocol implements IMessagePassingProtocol {
constructor(private sender: Sender, readonly onMessage: Event<VSBuffer>) { }
@ -23,7 +28,7 @@ export class Protocol implements IMessagePassingProtocol {
}
}
dispose(): void {
disconnect(): void {
this.sender.send('vscode:disconnect', null);
}
}

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 { Event } from 'vs/base/common/event';
import { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc';
import { IDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
/**
* Declare minimal `MessageEvent` and `MessagePort` interfaces here
* so that this utility can be used both from `browser` and
* `electron-main` namespace where message ports are available.
*/
export interface MessageEvent {
/**
* For our use we only consider `Uint8Array` a valid data transfer
* via message ports because our protocol implementation is buffer based.
*/
data: Uint8Array;
}
export interface MessagePort {
addEventListener(type: 'message', listener: (this: MessagePort, e: MessageEvent) => unknown): void;
removeEventListener(type: 'message', listener: (this: MessagePort, e: MessageEvent) => unknown): void;
postMessage(message: Uint8Array): void;
start(): void;
close(): void;
}
/**
* The MessagePort `Protocol` leverages MessagePort style IPC communication
* for the implementation of the `IMessagePassingProtocol`. That style of API
* is a simple `onmessage` / `postMessage` pattern.
*/
export class Protocol implements IMessagePassingProtocol {
readonly onMessage = Event.fromDOMEventEmitter<VSBuffer>(this.port, 'message', (e: MessageEvent) => VSBuffer.wrap(e.data));
constructor(private port: MessagePort) {
// we must call start() to ensure messages are flowing
port.start();
}
send(message: VSBuffer): void {
this.port.postMessage(message.buffer);
}
disconnect(): void {
this.port.close();
}
}
/**
* An implementation of a `IPCClient` on top of MessagePort style IPC communication.
*/
export class Client extends IPCClient implements IDisposable {
private protocol: Protocol;
constructor(port: MessagePort, clientId: string) {
const protocol = new Protocol(port);
super(protocol, clientId);
this.protocol = protocol;
}
dispose(): void {
this.protocol.disconnect();
}
}

View File

@ -365,8 +365,8 @@ export class Protocol extends Disposable implements IMessagePassingProtocol {
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;
private readonly _onDidDispose = new Emitter<void>();
readonly onDidDispose: Event<void> = this._onDidDispose.event;
constructor(socket: ISocket) {
super();
@ -380,7 +380,7 @@ export class Protocol extends Disposable implements IMessagePassingProtocol {
}
}));
this._register(this._socket.onClose(() => this._onClose.fire()));
this._register(this._socket.onClose(() => this._onDidDispose.fire()));
}
drain(): Promise<void> {
@ -406,7 +406,7 @@ export class Client<TContext = string> extends IPCClient<TContext> {
return new Client(new Protocol(socket), id);
}
get onClose(): Event<void> { return this.protocol.onClose; }
get onDidDispose(): Event<void> { return this.protocol.onDidDispose; }
constructor(private protocol: Protocol | PersistentProtocol, id: TContext, ipcLogger: IIPCLogger | null = null) {
super(protocol, id, ipcLogger);
@ -621,8 +621,8 @@ export class PersistentProtocol implements IMessagePassingProtocol {
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 _onDidDispose = new BufferedEmitter<void>();
readonly onDidDispose: Event<void> = this._onDidDispose.event;
private readonly _onSocketClose = new BufferedEmitter<void>();
readonly onSocketClose: Event<void> = this._onSocketClose.event;
@ -747,6 +747,10 @@ export class PersistentProtocol implements IMessagePassingProtocol {
return this._socket;
}
public getMillisSinceLastIncomingData(): number {
return Date.now() - this._socketReader.lastReadTime;
}
public beginAcceptReconnection(socket: ISocket, initialDataChunk: VSBuffer | null): void {
this._isReconnecting = true;
@ -783,7 +787,7 @@ export class PersistentProtocol implements IMessagePassingProtocol {
}
public acceptDisconnect(): void {
this._onClose.fire();
this._onDidDispose.fire();
}
private _receiveMessage(msg: ProtocolMessage): void {
@ -820,7 +824,7 @@ export class PersistentProtocol implements IMessagePassingProtocol {
} else if (msg.type === ProtocolMessageType.Control) {
this._onControlMessage.fire(msg.data);
} else if (msg.type === ProtocolMessageType.Disconnect) {
this._onClose.fire();
this._onDidDispose.fire();
} else if (msg.type === ProtocolMessageType.ReplayRequest) {
// Send again all unacknowledged messages
const toSend = this._outgoingUnackMsg.toArray();

View File

@ -505,6 +505,7 @@ export interface IIPCLogger {
export class ChannelClient implements IChannelClient, IDisposable {
private isDisposed: boolean = false;
private state: State = State.Uninitialized;
private activeRequests = new Set<IDisposable>();
private handlers = new Map<number, IHandler>();
@ -525,9 +526,15 @@ export class ChannelClient implements IChannelClient, IDisposable {
return {
call(command: string, arg?: any, cancellationToken?: CancellationToken) {
if (that.isDisposed) {
return Promise.reject(errors.canceled());
}
return that.requestPromise(channelName, command, arg, cancellationToken);
},
listen(event: string, arg: any) {
if (that.isDisposed) {
return Promise.reject(errors.canceled());
}
return that.requestEvent(channelName, event, arg);
}
} as T;
@ -725,6 +732,7 @@ export class ChannelClient implements IChannelClient, IDisposable {
}
dispose(): void {
this.isDisposed = true;
if (this.protocolListener) {
this.protocolListener.dispose();
this.protocolListener = null;

View File

@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ipcMain, WebContents } from 'electron';
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 { Protocol as ElectronProtocol } from 'vs/base/parts/ipc/common/ipc.electron';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
@ -18,9 +18,13 @@ interface IIPCEvent {
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);
}
/**
* An implemention of `IPCServer` on top of Electron `ipcMain` API.
*/
export class Server extends IPCServer {
private static readonly Clients = new Map<number, IDisposable>();
@ -41,7 +45,7 @@ export class Server extends IPCServer {
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);
const protocol = new ElectronProtocol(webContents, onMessage);
return { protocol, onDidClientDisconnect };
});

View File

@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { BrowserWindow, ipcMain, IpcMainEvent, MessagePortMain } from 'electron';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { generateUuid } from 'vs/base/common/uuid';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/common/ipc.mp';
/**
* An implementation of a `IPCClient` on top of Electron `MessagePortMain`.
*/
export class Client extends MessagePortClient implements IDisposable {
/**
* @param clientId a way to uniquely identify this client among
* other clients. this is important for routing because every
* client can also be a server
*/
constructor(port: MessagePortMain, clientId: string) {
super({
addEventListener: (type, listener) => port.addListener(type, listener),
removeEventListener: (type, listener) => port.removeListener(type, listener),
postMessage: message => port.postMessage(message),
start: () => port.start(),
close: () => port.close()
}, clientId);
}
}
/**
* This method opens a message channel connection
* in the target window. The target window needs
* to use the `Server` from `electron-sandbox/ipc.mp`.
*/
export async function connect(window: BrowserWindow): Promise<MessagePortMain> {
// Assert healthy window to talk to
if (window.isDestroyed() || window.webContents.isDestroyed()) {
throw new Error('ipc.mp#connect: Cannot talk to window because it is closed or destroyed');
}
// Ask to create message channel inside the window
// and send over a UUID to correlate the response
const nonce = generateUuid();
window.webContents.send('vscode:createMessageChannel', nonce);
// Wait until the window has returned the `MessagePort`
// We need to filter by the `nonce` to ensure we listen
// to the right response.
const onMessageChannelResult = Event.fromNodeEventEmitter<{ nonce: string, port: MessagePortMain }>(ipcMain, 'vscode:createMessageChannelResult', (e: IpcMainEvent, nonce: string) => ({ nonce, port: e.ports[0] }));
const { port } = await Event.toPromise(Event.once(Event.filter(onMessageChannelResult, e => e.nonce === nonce)));
return port;
}

View File

@ -5,28 +5,34 @@
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 { Protocol as ElectronProtocol } 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';
/**
* An implemention of `IPCClient` on top of Electron `ipcRenderer` IPC communication
* provided from sandbox globals (via preload script).
*/
export class Client extends IPCClient implements IDisposable {
private protocol: Protocol;
private protocol: ElectronProtocol;
private static createProtocol(): Protocol {
private static createProtocol(): ElectronProtocol {
const onMessage = Event.fromNodeEventEmitter<VSBuffer>(ipcRenderer, 'vscode:message', (_, message) => VSBuffer.wrap(message));
ipcRenderer.send('vscode:hello');
return new Protocol(ipcRenderer, onMessage);
return new ElectronProtocol(ipcRenderer, onMessage);
}
constructor(id: string) {
const protocol = Client.createProtocol();
super(protocol, id);
this.protocol = protocol;
}
dispose(): void {
this.protocol.dispose();
this.protocol.disconnect();
}
}

View File

@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* 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 { Event } from 'vs/base/common/event';
import { ClientConnectionEvent, IPCServer } from 'vs/base/parts/ipc/common/ipc';
import { Protocol as MessagePortProtocol } from 'vs/base/parts/ipc/common/ipc.mp';
/**
* An implementation of a `IPCServer` on top of MessagePort style IPC communication.
* The clients register themselves via Electron IPC transfer.
*/
export class Server extends IPCServer {
private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
// Clients connect via `vscode:createMessageChannel` to get a
// `MessagePort` that is ready to be used. For every connection
// we create a pair of message ports and send it back.
//
// The `nonce` is included so that the main side has a chance to
// correlate the response back to the sender.
const onCreateMessageChannel = Event.fromNodeEventEmitter<string>(ipcRenderer, 'vscode:createMessageChannel', (_, nonce: string) => nonce);
return Event.map(onCreateMessageChannel, nonce => {
// Create a new pair of ports and protocol for this connection
const { port1: incomingPort, port2: outgoingPort } = new MessageChannel();
const protocol = new MessagePortProtocol(incomingPort);
const result: ClientConnectionEvent = {
protocol,
// Not part of the standard spec, but in Electron we get a `close` event
// when the other side closes. We can use this to detect disconnects
// (https://github.com/electron/electron/blob/11-x-y/docs/api/message-port-main.md#event-close)
onDidClientDisconnect: Event.fromDOMEventEmitter(incomingPort, 'close')
};
// Send one port back to the requestor
ipcRenderer.postMessage('vscode:createMessageChannelResult', nonce, [outgoingPort]);
return result;
});
}
constructor() {
super(Server.getOnDidClientConnect());
}
}

View File

@ -5,6 +5,7 @@
import { createHash } from 'crypto';
import { Socket, Server as NetServer, createConnection, createServer } from 'net';
import * as zlib from 'zlib';
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';
@ -120,24 +121,130 @@ const enum ReadState {
export class WebSocketNodeSocket extends Disposable implements ISocket {
public readonly socket: NodeSocket;
public readonly permessageDeflate: boolean;
private _totalIncomingWireBytes: number;
private _totalIncomingDataBytes: number;
private _totalOutgoingWireBytes: number;
private _totalOutgoingDataBytes: number;
private readonly _zlibInflate: zlib.InflateRaw | null;
private readonly _zlibDeflate: zlib.DeflateRaw | null;
private _zlibDeflateFlushWaitingCount: number;
private readonly _onDidZlibFlush = this._register(new Emitter<void>());
private readonly _recordInflateBytes: boolean;
private readonly _recordedInflateBytes: Buffer[] = [];
private readonly _pendingInflateData: Buffer[] = [];
private readonly _pendingDeflateData: Buffer[] = [];
private readonly _incomingData: ChunkStream;
private readonly _onData = this._register(new Emitter<VSBuffer>());
private readonly _onClose = this._register(new Emitter<void>());
private _isEnded: boolean = false;
private readonly _state = {
state: ReadState.PeekHeader,
readLen: Constants.MinHeaderByteSize,
fin: 0,
mask: 0
};
constructor(socket: NodeSocket) {
public get totalIncomingWireBytes(): number {
return this._totalIncomingWireBytes;
}
public get totalIncomingDataBytes(): number {
return this._totalIncomingDataBytes;
}
public get totalOutgoingWireBytes(): number {
return this._totalOutgoingWireBytes;
}
public get totalOutgoingDataBytes(): number {
return this._totalOutgoingDataBytes;
}
public get recordedInflateBytes(): VSBuffer {
if (this._recordInflateBytes) {
return VSBuffer.wrap(Buffer.concat(this._recordedInflateBytes));
}
return VSBuffer.alloc(0);
}
/**
* Create a socket which can communicate using WebSocket frames.
*
* **NOTE**: When using the permessage-deflate WebSocket extension, if parts of inflating was done
* in a different zlib instance, we need to pass all those bytes into zlib, otherwise the inflate
* might hit an inflated portion referencing a distance too far back.
*
* @param socket The underlying socket
* @param permessageDeflate Use the permessage-deflate WebSocket extension
* @param inflateBytes "Seed" zlib inflate with these bytes.
* @param recordInflateBytes Record all bytes sent to inflate
*/
constructor(socket: NodeSocket, permessageDeflate: boolean, inflateBytes: VSBuffer | null, recordInflateBytes: boolean) {
super();
this.socket = socket;
this._totalIncomingWireBytes = 0;
this._totalIncomingDataBytes = 0;
this._totalOutgoingWireBytes = 0;
this._totalOutgoingDataBytes = 0;
this.permessageDeflate = permessageDeflate;
this._recordInflateBytes = recordInflateBytes;
if (permessageDeflate) {
// See https://tools.ietf.org/html/rfc7692#page-16
// To simplify our logic, we don't negociate the window size
// and simply dedicate (2^15) / 32kb per web socket
this._zlibInflate = zlib.createInflateRaw({
windowBits: 15
});
this._zlibInflate.on('error', (err) => {
// zlib errors are fatal, since we have no idea how to recover
console.error(err);
onUnexpectedError(err);
this._onClose.fire();
});
this._zlibInflate.on('data', (data: Buffer) => {
this._pendingInflateData.push(data);
});
if (inflateBytes) {
this._zlibInflate.write(inflateBytes.buffer);
this._zlibInflate.flush(() => {
this._pendingInflateData.length = 0;
});
}
this._zlibDeflate = zlib.createDeflateRaw({
windowBits: 15
});
this._zlibDeflate.on('error', (err) => {
// zlib errors are fatal, since we have no idea how to recover
console.error(err);
onUnexpectedError(err);
this._onClose.fire();
});
this._zlibDeflate.on('data', (data: Buffer) => {
this._pendingDeflateData.push(data);
});
} else {
this._zlibInflate = null;
this._zlibDeflate = null;
}
this._zlibDeflateFlushWaitingCount = 0;
this._incomingData = new ChunkStream();
this._register(this.socket.onData(data => this._acceptChunk(data)));
this._register(this.socket.onClose(() => this._onClose.fire()));
}
public dispose(): void {
this.socket.dispose();
if (this._zlibDeflateFlushWaitingCount > 0) {
// Wait for any outstanding writes to finish before disposing
this._register(this._onDidZlibFlush.event(() => {
this.dispose();
}));
} else {
this.socket.dispose();
super.dispose();
}
}
public onData(listener: (e: VSBuffer) => void): IDisposable {
@ -145,7 +252,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
}
public onClose(listener: () => void): IDisposable {
return this.socket.onClose(listener);
return this._onClose.event(listener);
}
public onEnd(listener: () => void): IDisposable {
@ -153,6 +260,36 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
}
public write(buffer: VSBuffer): void {
this._totalOutgoingDataBytes += buffer.byteLength;
if (this._zlibDeflate) {
this._zlibDeflate.write(<Buffer>buffer.buffer);
this._zlibDeflateFlushWaitingCount++;
// See https://zlib.net/manual.html#Constants
this._zlibDeflate.flush(/*Z_SYNC_FLUSH*/2, () => {
this._zlibDeflateFlushWaitingCount--;
let data = Buffer.concat(this._pendingDeflateData);
this._pendingDeflateData.length = 0;
// See https://tools.ietf.org/html/rfc7692#section-7.2.1
data = data.slice(0, data.length - 4);
if (!this._isEnded) {
// Avoid ERR_STREAM_WRITE_AFTER_END
this._write(VSBuffer.wrap(data), true);
}
if (this._zlibDeflateFlushWaitingCount === 0) {
this._onDidZlibFlush.fire();
}
});
} else {
this._write(buffer, false);
}
}
private _write(buffer: VSBuffer, compressed: boolean): void {
let headerLen = Constants.MinHeaderByteSize;
if (buffer.byteLength < 126) {
headerLen += 0;
@ -163,7 +300,12 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
}
const header = VSBuffer.alloc(headerLen);
header.writeUInt8(0b10000010, 0);
if (compressed) {
// The RSV1 bit indicates a compressed frame
header.writeUInt8(0b11000010, 0);
} else {
header.writeUInt8(0b10000010, 0);
}
if (buffer.byteLength < 126) {
header.writeUInt8(buffer.byteLength, 1);
} else if (buffer.byteLength < 2 ** 16) {
@ -184,10 +326,12 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
header.writeUInt8((buffer.byteLength >>> 0) & 0b11111111, ++offset);
}
this._totalOutgoingWireBytes += header.byteLength + buffer.byteLength;
this.socket.write(VSBuffer.concat([header, buffer]));
}
public end(): void {
this._isEnded = true;
this.socket.end();
}
@ -195,6 +339,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
if (data.byteLength === 0) {
return;
}
this._totalIncomingWireBytes += data.byteLength;
this._incomingData.acceptChunk(data);
@ -203,14 +348,15 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
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 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.fin = finBit;
this._state.mask = 0;
} else if (this._state.state === ReadState.ReadHeader) {
@ -263,13 +409,37 @@ export class WebSocketNodeSocket extends Disposable implements ISocket {
this._state.readLen = Constants.MinHeaderByteSize;
this._state.mask = 0;
this._onData.fire(body);
if (this._zlibInflate) {
// See https://tools.ietf.org/html/rfc7692#section-7.2.2
if (this._recordInflateBytes) {
this._recordedInflateBytes.push(Buffer.from(<Buffer>body.buffer));
}
this._zlibInflate.write(<Buffer>body.buffer);
if (this._state.fin) {
if (this._recordInflateBytes) {
this._recordedInflateBytes.push(Buffer.from([0x00, 0x00, 0xff, 0xff]));
}
this._zlibInflate.write(Buffer.from([0x00, 0x00, 0xff, 0xff]));
}
this._zlibInflate.flush(() => {
const data = Buffer.concat(this._pendingInflateData);
this._pendingInflateData.length = 0;
this._totalIncomingDataBytes += data.length;
this._onData.fire(VSBuffer.wrap(data));
});
} else {
this._totalIncomingDataBytes += body.byteLength;
this._onData.fire(body);
}
}
}
}
public drain(): Promise<void> {
return this.socket.drain();
public async drain(): Promise<void> {
if (this._zlibDeflateFlushWaitingCount > 0) {
await Event.toPromise(this._onDidZlibFlush.event);
}
await this.socket.drain();
}
}

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 * as assert from 'assert';
import { Event } from 'vs/base/common/event';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Client as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp';
suite('IPC, MessagePorts', () => {
test('message passing', async () => {
const { port1, port2 } = new MessageChannel();
const client1 = new MessagePortClient(port1, 'client1');
const client2 = new MessagePortClient(port2, 'client2');
client1.registerChannel('client1', {
call(_: unknown, command: string, arg: any, cancellationToken: CancellationToken): Promise<any> {
switch (command) {
case 'testMethodClient1': return Promise.resolve('success1');
default: return Promise.reject(new Error('not implemented'));
}
},
listen(_: unknown, event: string, arg?: any): Event<any> {
switch (event) {
default: throw new Error('not implemented');
}
}
});
client2.registerChannel('client2', {
call(_: unknown, command: string, arg: any, cancellationToken: CancellationToken): Promise<any> {
switch (command) {
case 'testMethodClient2': return Promise.resolve('success2');
default: return Promise.reject(new Error('not implemented'));
}
},
listen(_: unknown, event: string, arg?: any): Event<any> {
switch (event) {
default: throw new Error('not implemented');
}
}
});
const channelClient1 = client2.getChannel('client1');
assert.strictEqual(await channelClient1.call('testMethodClient1'), 'success1');
const channelClient2 = client1.getChannel('client2');
assert.strictEqual(await channelClient2.call('testMethodClient2'), 'success2');
client1.dispose();
client2.dispose();
});
});

View File

@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* 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 as MessagePortClient } from 'vs/base/parts/ipc/browser/ipc.mp';
suite('IPC, MessagePorts', () => {
test('message port close event', async () => {
const { port1, port2 } = new MessageChannel();
new MessagePortClient(port1, 'client1');
const client2 = new MessagePortClient(port2, 'client2');
// This test ensures that Electron's API for the close event
// does not break because we rely on it to dispose client
// connections from the server.
//
// This event is not provided by browser MessagePort API though.
const whenClosed = new Promise<boolean>(resolve => port1.addEventListener('close', () => resolve(true)));
client2.dispose();
assert.ok(await whenClosed);
});
});

View File

@ -11,7 +11,7 @@ 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 }
env: { VSCODE_AMD_ENTRYPOINT: 'vs/base/parts/ipc/test/node/testApp', verbose: true }
});
}

View File

@ -135,13 +135,13 @@ suite('IPC, Socket Protocol', () => {
a.send(VSBuffer.fromString('foobarfarboo'));
const msg1 = await bMessages.waitForOne();
assert.equal(msg1.toString(), 'foobarfarboo');
assert.strictEqual(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);
assert.strictEqual(msg2.readUInt8(0), 123);
});
@ -160,7 +160,7 @@ suite('IPC, Socket Protocol', () => {
a.send(VSBuffer.fromString(JSON.stringify(data)));
const msg = await bMessages.waitForOne();
assert.deepEqual(JSON.parse(msg.toString()), data);
assert.deepStrictEqual(JSON.parse(msg.toString()), data);
});
});
@ -179,49 +179,49 @@ suite('PersistentProtocol reconnection', () => {
const bMessages = new MessageStream(b);
a.send(VSBuffer.fromString('a1'));
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 0);
a.send(VSBuffer.fromString('a2'));
assert.equal(a.unacknowledgedCount, 2);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a.unacknowledgedCount, 2);
assert.strictEqual(b.unacknowledgedCount, 0);
a.send(VSBuffer.fromString('a3'));
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 0);
const a1 = await bMessages.waitForOne();
assert.equal(a1.toString(), 'a1');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a1.toString(), 'a1');
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 0);
const a2 = await bMessages.waitForOne();
assert.equal(a2.toString(), 'a2');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a2.toString(), 'a2');
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 0);
const a3 = await bMessages.waitForOne();
assert.equal(a3.toString(), 'a3');
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(a3.toString(), 'a3');
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 0);
b.send(VSBuffer.fromString('b1'));
assert.equal(a.unacknowledgedCount, 3);
assert.equal(b.unacknowledgedCount, 1);
assert.strictEqual(a.unacknowledgedCount, 3);
assert.strictEqual(b.unacknowledgedCount, 1);
const b1 = await aMessages.waitForOne();
assert.equal(b1.toString(), 'b1');
assert.equal(a.unacknowledgedCount, 0);
assert.equal(b.unacknowledgedCount, 1);
assert.strictEqual(b1.toString(), 'b1');
assert.strictEqual(a.unacknowledgedCount, 0);
assert.strictEqual(b.unacknowledgedCount, 1);
a.send(VSBuffer.fromString('a4'));
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 1);
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 1);
const b2 = await bMessages.waitForOne();
assert.equal(b2.toString(), 'a4');
assert.equal(a.unacknowledgedCount, 1);
assert.equal(b.unacknowledgedCount, 0);
assert.strictEqual(b2.toString(), 'a4');
assert.strictEqual(a.unacknowledgedCount, 1);
assert.strictEqual(b.unacknowledgedCount, 0);
});
});

View File

@ -121,7 +121,7 @@
font-size: 11px;
padding: 0 6px;
display: flex;
height: 100%;
height: 27.5px;
align-items: center;
}

View File

@ -30,7 +30,7 @@ import { Color } from 'vs/base/common/color';
import { registerCodicon, Codicon } from 'vs/base/common/codicons';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { escape } from 'vs/base/common/strings';
import { renderCodicons } from 'vs/base/browser/codicons';
import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels';
export interface IQuickInputOptions {
idPrefix: string;
@ -72,7 +72,7 @@ const $ = dom.$;
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
const backButtonIcon = registerCodicon('quick-input-back', Codicon.arrowLeft, localize('backButtonIcon', 'Icon for the back button in the quick input dialog.'));
const backButtonIcon = registerCodicon('quick-input-back', Codicon.arrowLeft);
const backButton = {
iconClass: backButtonIcon.classNames,
@ -967,7 +967,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
const validationMessage = this.validationMessage || '';
if (this._lastValidationMessage !== validationMessage) {
this._lastValidationMessage = validationMessage;
dom.reset(this.ui.message, ...renderCodicons(escape(validationMessage)));
dom.reset(this.ui.message, ...renderLabelWithIcons(escape(validationMessage)));
this.showMessageDecoration(this.validationMessage ? Severity.Error : Severity.Ignore);
}
this.ui.customButton.label = this.customLabel || '';
@ -1103,7 +1103,7 @@ class InputBox extends QuickInput implements IInputBox {
const validationMessage = this.validationMessage || this.noValidationMessage;
if (this._lastValidationMessage !== validationMessage) {
this._lastValidationMessage = validationMessage;
dom.reset(this.ui.message, ...renderCodicons(validationMessage));
dom.reset(this.ui.message, ...renderLabelWithIcons(validationMessage));
this.showMessageDecoration(this.validationMessage ? Severity.Error : Severity.Ignore);
}
}

View File

@ -9,7 +9,7 @@ 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 { matchesFuzzyIconAware, parseLabelWithIcons } from 'vs/base/common/iconLabels';
import { compareAnything } from 'vs/base/common/comparers';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
@ -34,6 +34,7 @@ interface IListElement {
readonly index: number;
readonly item: IQuickPickItem;
readonly saneLabel: string;
readonly saneMeta?: string;
readonly saneAriaLabel: string;
readonly saneDescription?: string;
readonly saneDetail?: string;
@ -49,6 +50,7 @@ class ListElement implements IListElement, IDisposable {
index!: number;
item!: IQuickPickItem;
saneLabel!: string;
saneMeta!: string;
saneAriaLabel!: string;
saneDescription?: string;
saneDetail?: string;
@ -127,7 +129,7 @@ class ListElementRenderer implements IListRenderer<ListElement, IListElementTemp
const row2 = dom.append(rows, $('.quick-input-list-row'));
// Label
data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportCodicons: true });
data.label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true, supportIcons: true });
// Keybinding
const keybindingContainer = dom.append(row1, $('.quick-input-list-entry-keybinding'));
@ -190,12 +192,11 @@ class ListElementRenderer implements IListRenderer<ListElement, IListElementTemp
if (button.alwaysVisible) {
cssClasses = cssClasses ? `${cssClasses} always-visible` : 'always-visible';
}
const action = new Action(`id-${index}`, '', cssClasses, true, () => {
const action = new Action(`id-${index}`, '', cssClasses, true, async () => {
element.fireButtonTriggered({
button,
item: element.item
});
return Promise.resolve();
});
action.tooltip = button.tooltip || '';
return action;
@ -248,6 +249,7 @@ export class QuickInputList {
matchOnDescription = false;
matchOnDetail = false;
matchOnLabel = true;
matchOnMeta = true;
sortByLabel = true;
private readonly _onChangedAllVisibleChecked = new Emitter<boolean>();
onChangedAllVisibleChecked: Event<boolean> = this._onChangedAllVisibleChecked.event;
@ -421,10 +423,11 @@ export class QuickInputList {
if (item.type !== 'separator') {
const previous = index && inputElements[index - 1];
const saneLabel = item.label && item.label.replace(/\r?\n/g, ' ');
const saneMeta = item.meta && item.meta.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)
.map(s => s && parseLabelWithIcons(s).text)
.filter(s => !!s)
.join(', ');
@ -432,6 +435,7 @@ export class QuickInputList {
index,
item,
saneLabel,
saneMeta,
saneAriaLabel,
saneDescription,
saneDetail,
@ -597,14 +601,15 @@ export class QuickInputList {
});
}
// Filter by value (since we support codicons, use codicon aware fuzzy matching)
// Filter by value (since we support icons in labels, use $(..) 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;
const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined;
const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDescription || ''))) : undefined;
const detailHighlights = this.matchOnDetail ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDetail || ''))) : undefined;
const metaHighlights = this.matchOnMeta ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneMeta || ''))) : undefined;
if (labelHighlights || descriptionHighlights || detailHighlights) {
if (labelHighlights || descriptionHighlights || detailHighlights || metaHighlights) {
element.labelHighlights = labelHighlights;
element.descriptionHighlights = descriptionHighlights;
element.detailHighlights = detailHighlights;

View File

@ -21,6 +21,7 @@ export interface IQuickPickItem {
type?: 'item';
id?: string;
label: string;
meta?: string;
ariaLabel?: string;
description?: string;
detail?: string;

View File

@ -7,7 +7,7 @@
// #######################################################################
// ### ###
// ### electron.d.ts types we need in a common layer for reuse ###
// ### (copied from Electron 9.x) ###
// ### (copied from Electron 11.x) ###
// ### ###
// #######################################################################
@ -212,7 +212,7 @@ export interface SaveDialogReturnValue {
export interface FileFilter {
// Docs: http://electronjs.org/docs/api/structures/file-filter
// Docs: https://electronjs.org/docs/api/structures/file-filter
extensions: string[];
name: string;
@ -220,7 +220,7 @@ export interface FileFilter {
export interface InputEvent {
// Docs: http://electronjs.org/docs/api/structures/input-event
// Docs: https://electronjs.org/docs/api/structures/input-event
/**
* An array of modifiers of the event, can be `shift`, `control`, `ctrl`, `alt`,
@ -232,7 +232,7 @@ export interface InputEvent {
export interface MouseInputEvent extends InputEvent {
// Docs: http://electronjs.org/docs/api/structures/mouse-input-event
// Docs: https://electronjs.org/docs/api/structures/mouse-input-event
/**
* The button pressed, can be `left`, `middle`, `right`.

View File

@ -35,6 +35,17 @@
}
},
/**
* @param {string} channel
* @param {any} message
* @param {MessagePort[]} transfer
*/
postMessage(channel, message, transfer) {
if (validateIPC(channel)) {
ipcRenderer.postMessage(channel, message, transfer);
}
},
/**
* @param {string} channel
* @param {any[]} args
@ -114,6 +125,7 @@
*/
process: {
get platform() { return process.platform; },
get arch() { return process.arch; },
get env() { return process.env; },
get versions() { return process.versions; },
get type() { return 'renderer'; },

View File

@ -7,35 +7,54 @@
// #######################################################################
// ### ###
// ### electron.d.ts types we expose from electron-sandbox ###
// ### (copied from Electron 9.x) ###
// ### (copied from Electron 11.x) ###
// ### ###
// #######################################################################
export interface IpcRendererEvent extends Event {
// Docs: https://electronjs.org/docs/api/structures/ipc-renderer-event
/**
* A list of MessagePorts that were transferred with this message
*/
ports: MessagePort[];
/**
* The `IpcRenderer` instance that emitted the event originally
*/
sender: IpcRenderer;
/**
* The `webContents.id` that sent the message, you can call
* `event.sender.sendTo(event.senderId, ...)` to reply to the message, see
* ipcRenderer.sendTo for more information. This only applies to messages sent from
* a different renderer. Messages sent directly from the main process set
* `event.senderId` to `0`.
*/
senderId: number;
}
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;
on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this;
/**
* 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;
once(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this;
/**
* Removes the specified `listener` from the listener array for the specified
* `channel`.
*/
removeListener(channel: string, listener: (event: unknown, ...args: any[]) => void): void;
removeListener(channel: string, listener: (...args: any[]) => void): this;
/**
* 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.
* just like `window.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
@ -43,8 +62,51 @@ export interface IpcRenderer {
*
* The main process handles it by listening for `channel` with the `ipcMain`
* module.
*
* If you need to transfer a `MessagePort` to the main process, use
* `ipcRenderer.postMessage`.
*
* If you want to receive a single response from the main process, like the result
* of a method call, consider using `ipcRenderer.invoke`.
*/
send(channel: string, ...args: any[]): void;
/**
* Resolves with the response from the main process.
*
* Send a message to the main process via `channel` and expect a result
* asynchronously. Arguments will be serialized with the Structured Clone
* Algorithm, just like `window.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 should listen for `channel` with `ipcMain.handle()`.
*
* For example:
*
* If you need to transfer a `MessagePort` to the main process, use
* `ipcRenderer.postMessage`.
*
* If you do not need a response to the message, consider using `ipcRenderer.send`.
*/
invoke(channel: string, ...args: any[]): Promise<any>;
/**
* Send a message to the main process, optionally transferring ownership of zero or
* more `MessagePort` objects.
*
* The transferred `MessagePort` objects will be available in the main process as
* `MessagePortMain` objects by accessing the `ports` property of the emitted
* event.
*
* For example:
*
* For more information on using `MessagePort` and `MessageChannel`, see the MDN
* documentation.
*/
postMessage(channel: string, message: any, transfer?: MessagePort[]): void;
}
export interface WebFrame {
@ -70,16 +132,23 @@ export interface CrashReporter {
* 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.
* must be no longer than 39 bytes, and values must be no longer than 20320 bytes.
* Keys with names longer than the maximum will be silently ignored. Key values
* longer than the maximum length will be truncated.
*
* **Note:** On linux values that are longer than 127 bytes will be chunked into
* multiple keys, each 127 bytes in length. E.g. `addExtraParameter('foo',
* 'a'.repeat(130))` will result in two chunked keys `foo__1` and `foo__2`, the
* first will contain the first 127 bytes and the second will contain the remaining
* 3 bytes. On your crash reporting backend you should stitch together keys in
* this format.
*/
addExtraParameter(key: string, value: string): void;
}
export interface ProcessMemoryInfo {
// Docs: http://electronjs.org/docs/api/structures/process-memory-info
// Docs: https://electronjs.org/docs/api/structures/process-memory-info
/**
* The amount of memory not shared by other processes, such as JS heap or HTML
@ -133,10 +202,7 @@ export interface CrashReporterStartOptions {
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
* gzip`. Default is `false`.
*/
compress?: boolean;
/**

View File

@ -14,6 +14,12 @@ export interface ISandboxNodeProcess extends INodeProcess {
*/
readonly platform: 'win32' | 'linux' | 'darwin';
/**
* The process.arch property returns a string identifying the CPU architecture
* on which the Node.js process is running.
*/
readonly arch: string;
/**
* The type will always be Electron renderer.
*/
@ -48,7 +54,7 @@ export interface ISandboxNodeProcess extends INodeProcess {
* The order of overwrites is `process.env` < `shellEnv` < `userEnv`.
*
* It is critical that every process awaits this method early on startup to get the right
* set of environment in `process.env`.
* set of environment in `process.env`.
*/
resolveEnv(userEnv: IProcessEnvironment): Promise<void>;
@ -76,7 +82,7 @@ export interface ISandboxNodeProcess extends INodeProcess {
export interface ISandboxContext {
/**
* Wether the renderer runs with `sandbox` enabled or not.
* Whether the renderer runs with `sandbox` enabled or not.
*/
sandbox: boolean;
}

View File

@ -200,9 +200,9 @@ export class Storage extends Disposable implements IStorage {
return parseInt(value, 10);
}
set(key: string, value: string | boolean | number | null | undefined): Promise<void> {
async 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
return; // Return early if we are already closed
}
// We remove the key for undefined/null values
@ -216,7 +216,7 @@ export class Storage extends Disposable implements IStorage {
// Return early if value already set
const currentValue = this.cache.get(key);
if (currentValue === valueStr) {
return Promise.resolve();
return;
}
// Update in cache and pending
@ -231,15 +231,15 @@ export class Storage extends Disposable implements IStorage {
return this.flushDelayer.trigger(() => this.flushPending());
}
delete(key: string): Promise<void> {
async delete(key: string): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // Return early if we are already closed
return; // 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
return; // Return early if value already deleted
}
if (!this.pendingDeletes.has(key)) {
@ -257,7 +257,7 @@ export class Storage extends Disposable implements IStorage {
async close(): Promise<void> {
if (this.state === StorageState.Closed) {
return Promise.resolve(); // return if already closed
return; // return if already closed
}
// Update state
@ -282,9 +282,9 @@ export class Storage extends Disposable implements IStorage {
return this.pendingInserts.size > 0 || this.pendingDeletes.size > 0;
}
private flushPending(): Promise<void> {
private async flushPending(): Promise<void> {
if (!this.hasPending) {
return Promise.resolve(); // return early if nothing to do
return; // return early if nothing to do
}
// Get pending data
@ -305,9 +305,9 @@ export class Storage extends Disposable implements IStorage {
});
}
whenFlushed(): Promise<void> {
async whenFlushed(): Promise<void> {
if (!this.hasPending) {
return Promise.resolve(); // return early if nothing to do
return; // return early if nothing to do
}
return new Promise(resolve => this.whenFlushedCallbacks.push(resolve));

View File

@ -412,11 +412,17 @@ export class SQLiteStorageDatabase implements IStorageDatabase {
}
class SQLiteStorageDatabaseLogger {
// to reduce lots of output, require an environment variable to enable tracing
// this helps when running with --verbose normally where the storage tracing
// might hide useful output to look at
static readonly VSCODE_TRACE_STORAGE = 'VSCODE_TRACE_STORAGE';
private readonly logTrace: ((msg: string) => void) | undefined;
private readonly logError: ((error: string | Error) => void) | undefined;
constructor(options?: ISQLiteStorageDatabaseLoggingOptions) {
if (options && typeof options.logTrace === 'function') {
if (options && typeof options.logTrace === 'function' && process.env[SQLiteStorageDatabaseLogger.VSCODE_TRACE_STORAGE]) {
this.logTrace = options.logTrace;
}

View File

@ -5,40 +5,39 @@
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseOptions } from 'vs/base/parts/storage/node/storage';
import { Storage, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/base/parts/storage/common/storage';
import { generateUuid } from 'vs/base/common/uuid';
import { join } from 'vs/base/common/path';
import { tmpdir } from 'os';
import { equal, ok } from 'assert';
import { mkdirp, writeFile, exists, unlink, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { strictEqual, ok } from 'assert';
import { mkdirp, writeFile, exists, unlink, rimraf } from 'vs/base/node/pfs';
import { timeout } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { isWindows } from 'vs/base/common/platform';
import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils';
import { generateUuid } from 'vs/base/common/uuid';
suite('Storage Library', function () {
flakySuite('Storage Library', function () {
// Given issues such as https://github.com/microsoft/vscode/issues/108113
// we see random test failures when accessing the native file system.
this.retries(3);
this.timeout(1000 * 20);
let testDir: string;
function uniqueStorageDir(): string {
const id = generateUuid();
setup(function () {
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary');
return join(tmpdir(), 'vsctests', id, 'storage2', id);
}
return mkdirp(testDir);
});
teardown(function () {
return rimraf(testDir);
});
test('basics', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db')));
const storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
await storage.init();
// Empty fallbacks
equal(storage.get('foo', 'bar'), 'bar');
equal(storage.getNumber('foo', 55), 55);
equal(storage.getBoolean('foo', true), true);
strictEqual(storage.get('foo', 'bar'), 'bar');
strictEqual(storage.getNumber('foo', 55), 55);
strictEqual(storage.getBoolean('foo', true), true);
let changes = new Set<string>();
storage.onDidChangeStorage(key => {
@ -55,19 +54,19 @@ suite('Storage Library', function () {
let flushPromiseResolved = false;
storage.whenFlushed().then(() => flushPromiseResolved = true);
equal(storage.get('bar'), 'foo');
equal(storage.getNumber('barNumber'), 55);
equal(storage.getBoolean('barBoolean'), true);
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.getNumber('barNumber'), 55);
strictEqual(storage.getBoolean('barBoolean'), true);
equal(changes.size, 3);
strictEqual(changes.size, 3);
ok(changes.has('bar'));
ok(changes.has('barNumber'));
ok(changes.has('barBoolean'));
let setPromiseResolved = false;
await Promise.all([set1Promise, set2Promise, set3Promise]).then(() => setPromiseResolved = true);
equal(setPromiseResolved, true);
equal(flushPromiseResolved, true);
strictEqual(setPromiseResolved, true);
strictEqual(flushPromiseResolved, true);
changes = new Set<string>();
@ -75,7 +74,7 @@ suite('Storage Library', function () {
storage.set('bar', 'foo');
storage.set('barNumber', 55);
storage.set('barBoolean', true);
equal(changes.size, 0);
strictEqual(changes.size, 0);
// Simple deletes
const delete1Promise = storage.delete('bar');
@ -86,7 +85,7 @@ suite('Storage Library', function () {
ok(!storage.getNumber('barNumber'));
ok(!storage.getBoolean('barBoolean'));
equal(changes.size, 3);
strictEqual(changes.size, 3);
ok(changes.has('bar'));
ok(changes.has('barNumber'));
ok(changes.has('barBoolean'));
@ -97,19 +96,16 @@ suite('Storage Library', function () {
storage.delete('bar');
storage.delete('barNumber');
storage.delete('barBoolean');
equal(changes.size, 0);
strictEqual(changes.size, 0);
let deletePromiseResolved = false;
await Promise.all([delete1Promise, delete2Promise, delete3Promise]).then(() => deletePromiseResolved = true);
equal(deletePromiseResolved, true);
strictEqual(deletePromiseResolved, true);
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
test('external changes', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
class TestSQLiteStorageDatabase extends SQLiteStorageDatabase {
private readonly _onDidChangeItemsExternal = new Emitter<IStorageItemsChangeEvent>();
@ -120,7 +116,7 @@ suite('Storage Library', function () {
}
}
const database = new TestSQLiteStorageDatabase(join(storageDir, 'storage.db'));
const database = new TestSQLiteStorageDatabase(join(testDir, 'storage.db'));
const storage = new Storage(database);
let changes = new Set<string>();
@ -138,35 +134,31 @@ suite('Storage Library', function () {
const changed = new Map<string, string>();
changed.set('foo', 'bar');
database.fireDidChangeItemsExternal({ changed });
equal(changes.size, 0);
strictEqual(changes.size, 0);
// Change is accepted if valid
changed.set('foo', 'bar1');
database.fireDidChangeItemsExternal({ changed });
ok(changes.has('foo'));
equal(storage.get('foo'), 'bar1');
strictEqual(storage.get('foo'), 'bar1');
changes.clear();
// Delete is accepted
const deleted = new Set<string>(['foo']);
database.fireDidChangeItemsExternal({ deleted });
ok(changes.has('foo'));
equal(storage.get('foo', undefined), undefined);
strictEqual(storage.get('foo', undefined), undefined);
changes.clear();
// Nothing happens if changing to same value
database.fireDidChangeItemsExternal({ deleted });
equal(changes.size, 0);
strictEqual(changes.size, 0);
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
test('close flushes data', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
let storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db')));
let storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
await storage.init();
const set1Promise = storage.set('foo', 'bar');
@ -175,26 +167,26 @@ suite('Storage Library', function () {
let flushPromiseResolved = false;
storage.whenFlushed().then(() => flushPromiseResolved = true);
equal(storage.get('foo'), 'bar');
equal(storage.get('bar'), 'foo');
strictEqual(storage.get('foo'), 'bar');
strictEqual(storage.get('bar'), 'foo');
let setPromiseResolved = false;
Promise.all([set1Promise, set2Promise]).then(() => setPromiseResolved = true);
await storage.close();
equal(setPromiseResolved, true);
equal(flushPromiseResolved, true);
strictEqual(setPromiseResolved, true);
strictEqual(flushPromiseResolved, true);
storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db')));
storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
await storage.init();
equal(storage.get('foo'), 'bar');
equal(storage.get('bar'), 'foo');
strictEqual(storage.get('foo'), 'bar');
strictEqual(storage.get('bar'), 'foo');
await storage.close();
storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db')));
storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
await storage.init();
const delete1Promise = storage.delete('foo');
@ -208,23 +200,19 @@ suite('Storage Library', function () {
await storage.close();
equal(deletePromiseResolved, true);
strictEqual(deletePromiseResolved, true);
storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db')));
storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
await storage.init();
ok(!storage.get('foo'));
ok(!storage.get('bar'));
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
test('conflicting updates', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
let storage = new Storage(new SQLiteStorageDatabase(join(storageDir, 'storage.db')));
let storage = new Storage(new SQLiteStorageDatabase(join(testDir, 'storage.db')));
await storage.init();
let changes = new Set<string>();
@ -239,8 +227,8 @@ suite('Storage Library', function () {
let flushPromiseResolved = false;
storage.whenFlushed().then(() => flushPromiseResolved = true);
equal(storage.get('foo'), 'bar3');
equal(changes.size, 1);
strictEqual(storage.get('foo'), 'bar3');
strictEqual(changes.size, 1);
ok(changes.has('foo'));
let setPromiseResolved = false;
@ -255,7 +243,7 @@ suite('Storage Library', function () {
ok(!storage.get('bar'));
equal(changes.size, 1);
strictEqual(changes.size, 1);
ok(changes.has('bar'));
let setAndDeletePromiseResolved = false;
@ -263,14 +251,10 @@ suite('Storage Library', function () {
ok(setAndDeletePromiseResolved);
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
test('corrupt DB recovers', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const storageFile = join(storageDir, 'storage.db');
const storageFile = join(testDir, 'storage.db');
let storage = new Storage(new SQLiteStorageDatabase(storageFile));
await storage.init();
@ -281,34 +265,22 @@ suite('Storage Library', function () {
await storage.set('foo', 'bar');
equal(storage.get('bar'), 'foo');
equal(storage.get('foo'), 'bar');
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.get('foo'), 'bar');
await storage.close();
storage = new Storage(new SQLiteStorageDatabase(storageFile));
await storage.init();
equal(storage.get('bar'), 'foo');
equal(storage.get('foo'), 'bar');
strictEqual(storage.get('bar'), 'foo');
strictEqual(storage.get('foo'), 'bar');
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
});
suite('SQLite Storage Library', function () {
// Given issues such as https://github.com/microsoft/vscode/issues/108113
// we see random test failures when accessing the native file system.
this.retries(3);
this.timeout(1000 * 20);
function uniqueStorageDir(): string {
const id = generateUuid();
return join(tmpdir(), 'vsctests', id, 'storage', id);
}
flakySuite('SQLite Storage Library', function () {
function toSet(elements: string[]): Set<string> {
const set = new Set<string>();
@ -317,6 +289,18 @@ suite('SQLite Storage Library', function () {
return set;
}
let storageDir: string;
setup(function () {
storageDir = getRandomTestPath(tmpdir(), 'vsctests', 'storagelibrary');
return mkdirp(storageDir);
});
teardown(function () {
return rimraf(storageDir);
});
async function testDBBasics(path: string, logError?: (error: Error | string) => void) {
let options!: ISQLiteStorageDatabaseOptions;
if (logError) {
@ -335,49 +319,49 @@ suite('SQLite Storage Library', function () {
items.set(JSON.stringify({ foo: 'bar' }), JSON.stringify({ bar: 'foo' }));
let storedItems = await storage.getItems();
equal(storedItems.size, 0);
strictEqual(storedItems.size, 0);
await storage.updateItems({ insert: items });
storedItems = await storage.getItems();
equal(storedItems.size, items.size);
equal(storedItems.get('foo'), 'bar');
equal(storedItems.get('some/foo/path'), 'some/bar/path');
equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
strictEqual(storedItems.size, items.size);
strictEqual(storedItems.get('foo'), 'bar');
strictEqual(storedItems.get('some/foo/path'), 'some/bar/path');
strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
await storage.updateItems({ delete: toSet(['foo']) });
storedItems = await storage.getItems();
equal(storedItems.size, items.size - 1);
strictEqual(storedItems.size, items.size - 1);
ok(!storedItems.has('foo'));
equal(storedItems.get('some/foo/path'), 'some/bar/path');
equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
strictEqual(storedItems.get('some/foo/path'), 'some/bar/path');
strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
await storage.updateItems({ insert: items });
storedItems = await storage.getItems();
equal(storedItems.size, items.size);
equal(storedItems.get('foo'), 'bar');
equal(storedItems.get('some/foo/path'), 'some/bar/path');
equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
strictEqual(storedItems.size, items.size);
strictEqual(storedItems.get('foo'), 'bar');
strictEqual(storedItems.get('some/foo/path'), 'some/bar/path');
strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
const itemsChange = new Map<string, string>();
itemsChange.set('foo', 'otherbar');
await storage.updateItems({ insert: itemsChange });
storedItems = await storage.getItems();
equal(storedItems.get('foo'), 'otherbar');
strictEqual(storedItems.get('foo'), 'otherbar');
await storage.updateItems({ delete: toSet(['foo', 'bar', 'some/foo/path', JSON.stringify({ foo: 'bar' })]) });
storedItems = await storage.getItems();
equal(storedItems.size, 0);
strictEqual(storedItems.size, 0);
await storage.updateItems({ insert: items, delete: toSet(['foo', 'some/foo/path', 'other']) });
storedItems = await storage.getItems();
equal(storedItems.size, 1);
equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
strictEqual(storedItems.size, 1);
strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
await storage.updateItems({ delete: toSet([JSON.stringify({ foo: 'bar' })]) });
storedItems = await storage.getItems();
equal(storedItems.size, 0);
strictEqual(storedItems.size, 0);
let recoveryCalled = false;
await storage.close(() => {
@ -386,35 +370,19 @@ suite('SQLite Storage Library', function () {
return new Map();
});
equal(recoveryCalled, false);
strictEqual(recoveryCalled, false);
}
test('basics', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
await testDBBasics(join(storageDir, 'storage.db'));
await rimraf(storageDir, RimRafMode.MOVE);
});
test('basics (open multiple times)', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
await testDBBasics(join(storageDir, 'storage.db'));
await testDBBasics(join(storageDir, 'storage.db'));
await rimraf(storageDir, RimRafMode.MOVE);
});
test('basics (corrupt DB falls back to empty DB)', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const corruptDBPath = join(storageDir, 'broken.db');
await writeFile(corruptDBPath, 'This is a broken DB');
@ -424,15 +392,9 @@ suite('SQLite Storage Library', function () {
});
ok(expectedError);
await rimraf(storageDir, RimRafMode.MOVE);
});
test('basics (corrupt DB restores from previous backup)', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const storagePath = join(storageDir, 'storage.db');
let storage = new SQLiteStorageDatabase(storagePath);
@ -449,10 +411,10 @@ suite('SQLite Storage Library', function () {
storage = new SQLiteStorageDatabase(storagePath);
const storedItems = await storage.getItems();
equal(storedItems.size, items.size);
equal(storedItems.get('foo'), 'bar');
equal(storedItems.get('some/foo/path'), 'some/bar/path');
equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
strictEqual(storedItems.size, items.size);
strictEqual(storedItems.get('foo'), 'bar');
strictEqual(storedItems.get('some/foo/path'), 'some/bar/path');
strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
let recoveryCalled = false;
await storage.close(() => {
@ -461,16 +423,10 @@ suite('SQLite Storage Library', function () {
return new Map();
});
equal(recoveryCalled, false);
await rimraf(storageDir, RimRafMode.MOVE);
strictEqual(recoveryCalled, false);
});
test('basics (corrupt DB falls back to empty DB if backup is corrupt)', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const storagePath = join(storageDir, 'storage.db');
let storage = new SQLiteStorageDatabase(storagePath);
@ -488,24 +444,12 @@ suite('SQLite Storage Library', function () {
storage = new SQLiteStorageDatabase(storagePath);
const storedItems = await storage.getItems();
equal(storedItems.size, 0);
strictEqual(storedItems.size, 0);
await testDBBasics(storagePath);
await rimraf(storageDir, RimRafMode.MOVE);
});
test('basics (DB that becomes corrupt during runtime stores all state from cache on close)', async () => {
if (isWindows) {
await Promise.resolve(); // Windows will fail to write to open DB due to locking
return;
}
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
(isWindows ? test.skip /* Windows will fail to write to open DB due to locking */ : test)('basics (DB that becomes corrupt during runtime stores all state from cache on close)', async () => {
const storagePath = join(storageDir, 'storage.db');
let storage = new SQLiteStorageDatabase(storagePath);
@ -518,7 +462,7 @@ suite('SQLite Storage Library', function () {
await storage.close();
const backupPath = `${storagePath}.backup`;
equal(await exists(backupPath), true);
strictEqual(await exists(backupPath), true);
storage = new SQLiteStorageDatabase(storagePath);
await storage.getItems();
@ -540,16 +484,16 @@ suite('SQLite Storage Library', function () {
return items;
});
equal(recoveryCalled, true);
equal(await exists(backupPath), true);
strictEqual(recoveryCalled, true);
strictEqual(await exists(backupPath), true);
storage = new SQLiteStorageDatabase(storagePath);
const storedItems = await storage.getItems();
equal(storedItems.size, items.size);
equal(storedItems.get('foo'), 'bar');
equal(storedItems.get('some/foo/path'), 'some/bar/path');
equal(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
strictEqual(storedItems.size, items.size);
strictEqual(storedItems.get('foo'), 'bar');
strictEqual(storedItems.get('some/foo/path'), 'some/bar/path');
strictEqual(storedItems.get(JSON.stringify({ foo: 'bar' })), JSON.stringify({ bar: 'foo' }));
recoveryCalled = false;
await storage.close(() => {
@ -558,16 +502,10 @@ suite('SQLite Storage Library', function () {
return new Map();
});
equal(recoveryCalled, false);
await rimraf(storageDir, RimRafMode.MOVE);
strictEqual(recoveryCalled, false);
});
test('real world example', async function () {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
let storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db'));
const items1 = new Map<string, string>();
@ -587,7 +525,7 @@ suite('SQLite Storage Library', function () {
items3.set('very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.very.long.key.', 'is long');
let storedItems = await storage.getItems();
equal(storedItems.size, 0);
strictEqual(storedItems.size, 0);
await Promise.all([
await storage.updateItems({ insert: items1 }),
@ -595,28 +533,28 @@ suite('SQLite Storage Library', function () {
await storage.updateItems({ insert: items3 })
]);
equal(await storage.checkIntegrity(true), 'ok');
equal(await storage.checkIntegrity(false), 'ok');
strictEqual(await storage.checkIntegrity(true), 'ok');
strictEqual(await storage.checkIntegrity(false), 'ok');
storedItems = await storage.getItems();
equal(storedItems.size, items1.size + items2.size + items3.size);
strictEqual(storedItems.size, items1.size + items2.size + items3.size);
const items1Keys: string[] = [];
items1.forEach((value, key) => {
items1Keys.push(key);
equal(storedItems.get(key), value);
strictEqual(storedItems.get(key), value);
});
const items2Keys: string[] = [];
items2.forEach((value, key) => {
items2Keys.push(key);
equal(storedItems.get(key), value);
strictEqual(storedItems.get(key), value);
});
const items3Keys: string[] = [];
items3.forEach((value, key) => {
items3Keys.push(key);
equal(storedItems.get(key), value);
strictEqual(storedItems.get(key), value);
});
await Promise.all([
@ -626,7 +564,7 @@ suite('SQLite Storage Library', function () {
]);
storedItems = await storage.getItems();
equal(storedItems.size, 0);
strictEqual(storedItems.size, 0);
await Promise.all([
await storage.updateItems({ insert: items1 }),
@ -638,27 +576,19 @@ suite('SQLite Storage Library', function () {
]);
storedItems = await storage.getItems();
equal(storedItems.size, items1.size + items2.size + items3.size);
strictEqual(storedItems.size, items1.size + items2.size + items3.size);
await storage.close();
storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db'));
storedItems = await storage.getItems();
equal(storedItems.size, items1.size + items2.size + items3.size);
strictEqual(storedItems.size, items1.size + items2.size + items3.size);
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
test('very large item value', async function () {
this.timeout(20000);
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
let storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db'));
const items = new Map<string, string>();
@ -675,9 +605,9 @@ suite('SQLite Storage Library', function () {
await storage.updateItems({ insert: items });
let storedItems = await storage.getItems();
equal(items.get('colorthemedata'), storedItems.get('colorthemedata'));
equal(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache'));
equal(items.get('super.large.string'), storedItems.get('super.large.string'));
strictEqual(items.get('colorthemedata'), storedItems.get('colorthemedata'));
strictEqual(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache'));
strictEqual(items.get('super.large.string'), storedItems.get('super.large.string'));
uuid = generateUuid();
value = [];
@ -689,27 +619,23 @@ suite('SQLite Storage Library', function () {
await storage.updateItems({ insert: items });
storedItems = await storage.getItems();
equal(items.get('colorthemedata'), storedItems.get('colorthemedata'));
equal(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache'));
equal(items.get('super.large.string'), storedItems.get('super.large.string'));
strictEqual(items.get('colorthemedata'), storedItems.get('colorthemedata'));
strictEqual(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache'));
strictEqual(items.get('super.large.string'), storedItems.get('super.large.string'));
const toDelete = new Set<string>();
toDelete.add('super.large.string');
await storage.updateItems({ delete: toDelete });
storedItems = await storage.getItems();
equal(items.get('colorthemedata'), storedItems.get('colorthemedata'));
equal(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache'));
strictEqual(items.get('colorthemedata'), storedItems.get('colorthemedata'));
strictEqual(items.get('commandpalette.mru.cache'), storedItems.get('commandpalette.mru.cache'));
ok(!storedItems.get('super.large.string'));
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
test('multiple concurrent writes execute in sequence', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
class TestStorage extends Storage {
getStorage(): IStorageDatabase {
@ -750,25 +676,19 @@ suite('SQLite Storage Library', function () {
await storage.set('some/foo3/path', 'some/bar/path');
const items = await storage.getStorage().getItems();
equal(items.get('foo'), 'bar');
equal(items.get('some/foo/path'), 'some/bar/path');
equal(items.has('foo1'), false);
equal(items.has('some/foo1/path'), false);
equal(items.get('foo2'), 'bar');
equal(items.get('some/foo2/path'), 'some/bar/path');
equal(items.get('foo3'), 'bar');
equal(items.get('some/foo3/path'), 'some/bar/path');
strictEqual(items.get('foo'), 'bar');
strictEqual(items.get('some/foo/path'), 'some/bar/path');
strictEqual(items.has('foo1'), false);
strictEqual(items.has('some/foo1/path'), false);
strictEqual(items.get('foo2'), 'bar');
strictEqual(items.get('some/foo2/path'), 'some/bar/path');
strictEqual(items.get('foo3'), 'bar');
strictEqual(items.get('some/foo3/path'), 'some/bar/path');
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
test('lots of INSERT & DELETE (below inline max)', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db'));
const items = new Map<string, string>();
@ -784,23 +704,17 @@ suite('SQLite Storage Library', function () {
await storage.updateItems({ insert: items });
let storedItems = await storage.getItems();
equal(storedItems.size, items.size);
strictEqual(storedItems.size, items.size);
await storage.updateItems({ delete: keys });
storedItems = await storage.getItems();
equal(storedItems.size, 0);
strictEqual(storedItems.size, 0);
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
test('lots of INSERT & DELETE (above inline max)', async () => {
const storageDir = uniqueStorageDir();
await mkdirp(storageDir);
const storage = new SQLiteStorageDatabase(join(storageDir, 'storage.db'));
const items = new Map<string, string>();
@ -816,15 +730,13 @@ suite('SQLite Storage Library', function () {
await storage.updateItems({ insert: items });
let storedItems = await storage.getItems();
equal(storedItems.size, items.size);
strictEqual(storedItems.size, items.size);
await storage.updateItems({ delete: keys });
storedItems = await storage.getItems();
equal(storedItems.size, 0);
strictEqual(storedItems.size, 0);
await storage.close();
await rimraf(storageDir, RimRafMode.MOVE);
});
});