mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-01 09:35:41 -05:00
Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 (#6381)
* Merge from vscode 8e0f348413f4f616c23a88ae30030efa85811973 * disable strict null check
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
import * as fs from 'fs';
|
||||
import { dirname } from 'vs/base/common/path';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as json from 'vs/base/common/json';
|
||||
import { statLink } from 'vs/base/node/pfs';
|
||||
@@ -41,20 +41,17 @@ export interface IConfigOptions<T> {
|
||||
* - delayed processing of changes to accomodate for lots of changes
|
||||
* - configurable defaults
|
||||
*/
|
||||
export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
export class ConfigWatcher<T> extends Disposable implements IConfigWatcher<T> {
|
||||
private cache: T;
|
||||
private parseErrors: json.ParseError[];
|
||||
private disposed: boolean;
|
||||
private loaded: boolean;
|
||||
private timeoutHandle: NodeJS.Timer | null;
|
||||
private disposables: IDisposable[];
|
||||
private readonly _onDidUpdateConfiguration: Emitter<IConfigurationChangeEvent<T>>;
|
||||
|
||||
constructor(private _path: string, private options: IConfigOptions<T> = { defaultConfig: Object.create(null), onError: error => console.error(error) }) {
|
||||
this.disposables = [];
|
||||
|
||||
this._onDidUpdateConfiguration = new Emitter<IConfigurationChangeEvent<T>>();
|
||||
this.disposables.push(this._onDidUpdateConfiguration);
|
||||
super();
|
||||
this._onDidUpdateConfiguration = this._register(new Emitter<IConfigurationChangeEvent<T>>());
|
||||
|
||||
this.registerWatcher();
|
||||
this.initAsync();
|
||||
@@ -111,10 +108,10 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
try {
|
||||
this.parseErrors = [];
|
||||
res = this.options.parse ? this.options.parse(raw, this.parseErrors) : json.parse(raw, this.parseErrors);
|
||||
|
||||
return res || this.options.defaultConfig;
|
||||
} catch (error) {
|
||||
// Ignore parsing errors
|
||||
return this.options.defaultConfig;
|
||||
return this.options.defaultConfig; // Ignore parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +122,7 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
this.watch(parentFolder, true);
|
||||
|
||||
// Check if the path is a symlink and watch its target if so
|
||||
this.handleSymbolicLink().then(undefined, error => { /* ignore error */ });
|
||||
this.handleSymbolicLink().then(undefined, () => { /* ignore error */ });
|
||||
}
|
||||
|
||||
private async handleSymbolicLink(): Promise<void> {
|
||||
@@ -143,9 +140,9 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
}
|
||||
|
||||
if (isFolder) {
|
||||
this.disposables.push(watchFolder(path, (type, path) => path === this._path ? this.onConfigFileChange() : undefined, error => this.options.onError(error)));
|
||||
this._register(watchFolder(path, (type, path) => path === this._path ? this.onConfigFileChange() : undefined, error => this.options.onError(error)));
|
||||
} else {
|
||||
this.disposables.push(watchFile(path, (type, path) => this.onConfigFileChange(), error => this.options.onError(error)));
|
||||
this._register(watchFile(path, () => this.onConfigFileChange(), error => this.options.onError(error)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +184,6 @@ export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
|
||||
|
||||
dispose(): void {
|
||||
this.disposed = true;
|
||||
this.disposables = dispose(this.disposables);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -395,7 +395,7 @@ export async function resolveTerminalEncoding(verbose?: boolean): Promise<string
|
||||
|
||||
exec('chcp', (err, stdout, stderr) => {
|
||||
if (stdout) {
|
||||
const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings);
|
||||
const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array<keyof typeof windowsTerminalEncodings>;
|
||||
for (const key of windowsTerminalEncodingKeys) {
|
||||
if (stdout.indexOf(key) >= 0) {
|
||||
return resolve(windowsTerminalEncodings[key]);
|
||||
|
||||
@@ -7,11 +7,12 @@ import * as errors from 'vs/base/common/errors';
|
||||
import * as uuid from 'vs/base/common/uuid';
|
||||
import { networkInterfaces } from 'os';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
import { getMac } from 'vs/base/node/macAddress';
|
||||
|
||||
// http://www.techrepublic.com/blog/data-center/mac-address-scorecard-for-common-virtual-machine-platforms/
|
||||
// VMware ESX 3, Server, Workstation, Player 00-50-56, 00-0C-29, 00-05-69
|
||||
// Microsoft Hyper-V, Virtual Server, Virtual PC 00-03-FF
|
||||
// Parallells Desktop, Workstation, Server, Virtuozzo 00-1C-42
|
||||
// Parallels Desktop, Workstation, Server, Virtuozzo 00-1C-42
|
||||
// Virtual Iron 4 00-0F-4B
|
||||
// Red Hat Xen 00-16-3E
|
||||
// Oracle VM 00-16-3E
|
||||
@@ -76,35 +77,25 @@ export const virtualMachineHint: { value(): number } = new class {
|
||||
};
|
||||
|
||||
let machineId: Promise<string>;
|
||||
export function getMachineId(): Promise<string> {
|
||||
return machineId || (machineId = getMacMachineId()
|
||||
.then(id => id || uuid.generateUuid())); // fallback, generate a UUID
|
||||
export async function getMachineId(): Promise<string> {
|
||||
if (!machineId) {
|
||||
machineId = (async () => {
|
||||
const id = await getMacMachineId();
|
||||
|
||||
return id || uuid.generateUuid(); // fallback, generate a UUID
|
||||
})();
|
||||
}
|
||||
|
||||
return machineId;
|
||||
}
|
||||
|
||||
function getMacMachineId(): Promise<string> {
|
||||
return new Promise<string>(resolve => {
|
||||
Promise.all([import('crypto'), import('getmac')]).then(([crypto, getmac]) => {
|
||||
try {
|
||||
getmac.getMac((error, macAddress) => {
|
||||
if (!error) {
|
||||
resolve(crypto.createHash('sha256').update(macAddress, 'utf8').digest('hex'));
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
// Timeout due to hang with reduced privileges #58392
|
||||
// TODO@sbatten: Remove this when getmac is patched
|
||||
setTimeout(() => {
|
||||
resolve(undefined);
|
||||
}, 10000);
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
resolve(undefined);
|
||||
}
|
||||
}, err => {
|
||||
errors.onUnexpectedError(err);
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
async function getMacMachineId(): Promise<string | undefined> {
|
||||
try {
|
||||
const crypto = await import('crypto');
|
||||
const macAddress = await getMac();
|
||||
return crypto.createHash('sha256').update(macAddress, 'utf8').digest('hex');
|
||||
} catch (err) {
|
||||
errors.onUnexpectedError(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
69
src/vs/base/node/macAddress.ts
Normal file
69
src/vs/base/node/macAddress.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
|
||||
const cmdline = {
|
||||
windows: 'getmac.exe',
|
||||
unix: '/sbin/ifconfig -a || /sbin/ip link'
|
||||
};
|
||||
|
||||
const invalidMacAddresses = [
|
||||
'00:00:00:00:00:00',
|
||||
'ff:ff:ff:ff:ff:ff',
|
||||
'ac:de:48:00:11:22'
|
||||
];
|
||||
|
||||
function validateMacAddress(candidate: string): boolean {
|
||||
let tempCandidate = candidate.replace(/\-/g, ':').toLowerCase();
|
||||
for (let invalidMacAddress of invalidMacAddresses) {
|
||||
if (invalidMacAddress === tempCandidate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getMac(): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject('Unable to retrieve mac address (timeout after 10s)'), 10000);
|
||||
|
||||
try {
|
||||
resolve(await doGetMac());
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function doGetMac(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
exec(isWindows ? cmdline.windows : cmdline.unix, { timeout: 10000 }, (err, stdout, stdin) => {
|
||||
if (err) {
|
||||
return reject(`Unable to retrieve mac address (${err.toString()})`);
|
||||
} else {
|
||||
const regex = /(?:[a-f\d]{2}[:\-]){5}[a-f\d]{2}/gi;
|
||||
|
||||
let match;
|
||||
while ((match = regex.exec(stdout)) !== null) {
|
||||
const macAddressCandidate = match[0];
|
||||
if (validateMacAddress(macAddressCandidate)) {
|
||||
return resolve(macAddressCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
return reject('Unable to retrieve mac address (unexpected format)');
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -138,6 +138,20 @@ export async function readdir(path: string): Promise<string[]> {
|
||||
return handleDirectoryChildren(await promisify(fs.readdir)(path));
|
||||
}
|
||||
|
||||
export async function readdirWithFileTypes(path: string): Promise<fs.Dirent[]> {
|
||||
const children = await promisify(fs.readdir)(path, { withFileTypes: true });
|
||||
|
||||
// Mac: uses NFD unicode form on disk, but we want NFC
|
||||
// See also https://github.com/nodejs/node/issues/2165
|
||||
if (platform.isMacintosh) {
|
||||
for (const child of children) {
|
||||
child.name = normalizeNFC(child.name);
|
||||
}
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export function readdirSync(path: string): string[] {
|
||||
return handleDirectoryChildren(fs.readdirSync(path));
|
||||
}
|
||||
@@ -226,7 +240,7 @@ export function readFile(path: string, encoding?: string): Promise<Buffer | stri
|
||||
// According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback)
|
||||
// it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return.
|
||||
// Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly.
|
||||
const writeFilePathQueue: { [path: string]: Queue<void> } = Object.create(null);
|
||||
const writeFilePathQueues: Map<string, Queue<void>> = new Map();
|
||||
|
||||
export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise<void>;
|
||||
export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise<void>;
|
||||
@@ -249,18 +263,20 @@ function toQueueKey(path: string): string {
|
||||
}
|
||||
|
||||
function ensureWriteFileQueue(queueKey: string): Queue<void> {
|
||||
let writeFileQueue = writeFilePathQueue[queueKey];
|
||||
if (!writeFileQueue) {
|
||||
writeFileQueue = new Queue<void>();
|
||||
writeFilePathQueue[queueKey] = writeFileQueue;
|
||||
|
||||
const onFinish = Event.once(writeFileQueue.onFinished);
|
||||
onFinish(() => {
|
||||
delete writeFilePathQueue[queueKey];
|
||||
writeFileQueue.dispose();
|
||||
});
|
||||
const existingWriteFileQueue = writeFilePathQueues.get(queueKey);
|
||||
if (existingWriteFileQueue) {
|
||||
return existingWriteFileQueue;
|
||||
}
|
||||
|
||||
const writeFileQueue = new Queue<void>();
|
||||
writeFilePathQueues.set(queueKey, writeFileQueue);
|
||||
|
||||
const onFinish = Event.once(writeFileQueue.onFinished);
|
||||
onFinish(() => {
|
||||
writeFilePathQueues.delete(queueKey);
|
||||
writeFileQueue.dispose();
|
||||
});
|
||||
|
||||
return writeFileQueue;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import * as cp from 'child_process';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as Types from 'vs/base/common/types';
|
||||
@@ -404,7 +405,7 @@ export function createQueuedSender(childProcess: cp.ChildProcess): IQueuedSender
|
||||
}
|
||||
|
||||
export namespace win32 {
|
||||
export function findExecutable(command: string, cwd?: string, paths?: string[]): string {
|
||||
export async function findExecutable(command: string, cwd?: string, paths?: string[]): Promise<string> {
|
||||
// If we have an absolute path then we take it.
|
||||
if (path.isAbsolute(command)) {
|
||||
return command;
|
||||
@@ -435,15 +436,15 @@ export namespace win32 {
|
||||
} else {
|
||||
fullPath = path.join(cwd, pathEntry, command);
|
||||
}
|
||||
if (fs.existsSync(fullPath)) {
|
||||
if (await promisify(fs.exists)(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
let withExtension = fullPath + '.com';
|
||||
if (fs.existsSync(withExtension)) {
|
||||
if (await promisify(fs.exists)(withExtension)) {
|
||||
return withExtension;
|
||||
}
|
||||
withExtension = fullPath + '.exe';
|
||||
if (fs.existsSync(withExtension)) {
|
||||
if (await promisify(fs.exists)(withExtension)) {
|
||||
return withExtension;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Url, parse as parseUrl } from 'url';
|
||||
import { isBoolean } from 'vs/base/common/types';
|
||||
import { Agent } from './request';
|
||||
|
||||
function getSystemProxyURI(requestURL: Url): string | null {
|
||||
if (requestURL.protocol === 'http:') {
|
||||
return process.env.HTTP_PROXY || process.env.http_proxy || null;
|
||||
} else if (requestURL.protocol === 'https:') {
|
||||
return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface IOptions {
|
||||
proxyUrl?: string;
|
||||
strictSSL?: boolean;
|
||||
}
|
||||
|
||||
export async function getProxyAgent(rawRequestURL: string, options: IOptions = {}): Promise<Agent> {
|
||||
const requestURL = parseUrl(rawRequestURL);
|
||||
const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL);
|
||||
|
||||
if (!proxyURL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const proxyEndpoint = parseUrl(proxyURL);
|
||||
|
||||
if (!/^https?:$/.test(proxyEndpoint.protocol || '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const opts = {
|
||||
host: proxyEndpoint.hostname || '',
|
||||
port: Number(proxyEndpoint.port),
|
||||
auth: proxyEndpoint.auth,
|
||||
rejectUnauthorized: isBoolean(options.strictSSL) ? options.strictSSL : true
|
||||
};
|
||||
|
||||
const Ctor = requestURL.protocol === 'http:'
|
||||
? await import('http-proxy-agent')
|
||||
: await import('https-proxy-agent');
|
||||
|
||||
return new Ctor(opts);
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isBoolean, isNumber } from 'vs/base/common/types';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { Stream } from 'stream';
|
||||
import { parse as parseUrl } from 'url';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { createGunzip } from 'zlib';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
|
||||
export type Agent = any;
|
||||
|
||||
export interface IRawRequestFunction {
|
||||
(options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
|
||||
}
|
||||
|
||||
export interface IRequestOptions {
|
||||
type?: string;
|
||||
url?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
headers?: any;
|
||||
timeout?: number;
|
||||
data?: string | Stream;
|
||||
agent?: Agent;
|
||||
followRedirects?: number;
|
||||
strictSSL?: boolean;
|
||||
getRawRequest?(options: IRequestOptions): IRawRequestFunction;
|
||||
}
|
||||
|
||||
export interface IRequestContext {
|
||||
// req: http.ClientRequest;
|
||||
// res: http.ClientResponse;
|
||||
res: {
|
||||
headers: { [n: string]: string };
|
||||
statusCode?: number;
|
||||
};
|
||||
stream: Stream;
|
||||
}
|
||||
|
||||
export interface IRequestFunction {
|
||||
(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext>;
|
||||
}
|
||||
|
||||
async function getNodeRequest(options: IRequestOptions): Promise<IRawRequestFunction> {
|
||||
const endpoint = parseUrl(options.url!);
|
||||
const module = endpoint.protocol === 'https:' ? await import('https') : await import('http');
|
||||
return module.request;
|
||||
}
|
||||
|
||||
export function request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
let req: http.ClientRequest;
|
||||
|
||||
const rawRequestPromise = options.getRawRequest
|
||||
? Promise.resolve(options.getRawRequest(options))
|
||||
: Promise.resolve(getNodeRequest(options));
|
||||
|
||||
return rawRequestPromise.then(rawRequest => {
|
||||
|
||||
return new Promise<IRequestContext>((c, e) => {
|
||||
const endpoint = parseUrl(options.url!);
|
||||
|
||||
const opts: https.RequestOptions = {
|
||||
hostname: endpoint.hostname,
|
||||
port: endpoint.port ? parseInt(endpoint.port) : (endpoint.protocol === 'https:' ? 443 : 80),
|
||||
protocol: endpoint.protocol,
|
||||
path: endpoint.path,
|
||||
method: options.type || 'GET',
|
||||
headers: options.headers,
|
||||
agent: options.agent,
|
||||
rejectUnauthorized: isBoolean(options.strictSSL) ? options.strictSSL : true
|
||||
};
|
||||
|
||||
if (options.user && options.password) {
|
||||
opts.auth = options.user + ':' + options.password;
|
||||
}
|
||||
|
||||
req = rawRequest(opts, (res: http.IncomingMessage) => {
|
||||
const followRedirects: number = isNumber(options.followRedirects) ? options.followRedirects : 3;
|
||||
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && followRedirects > 0 && res.headers['location']) {
|
||||
request(assign({}, options, {
|
||||
url: res.headers['location'],
|
||||
followRedirects: followRedirects - 1
|
||||
}), token).then(c, e);
|
||||
} else {
|
||||
let stream: Stream = res;
|
||||
|
||||
if (res.headers['content-encoding'] === 'gzip') {
|
||||
stream = stream.pipe(createGunzip());
|
||||
}
|
||||
|
||||
c({ res, stream } as IRequestContext);
|
||||
}
|
||||
});
|
||||
|
||||
req.on('error', e);
|
||||
|
||||
if (options.timeout) {
|
||||
req.setTimeout(options.timeout);
|
||||
}
|
||||
|
||||
if (options.data) {
|
||||
if (typeof options.data === 'string') {
|
||||
req.write(options.data);
|
||||
} else {
|
||||
options.data.pipe(req);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
req.end();
|
||||
|
||||
token.onCancellationRequested(() => {
|
||||
req.abort();
|
||||
e(canceled());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isSuccess(context: IRequestContext): boolean {
|
||||
return (context.res.statusCode && context.res.statusCode >= 200 && context.res.statusCode < 300) || context.res.statusCode === 1223;
|
||||
}
|
||||
|
||||
function hasNoContent(context: IRequestContext): boolean {
|
||||
return context.res.statusCode === 204;
|
||||
}
|
||||
|
||||
export function download(filePath: string, context: IRequestContext): Promise<void> {
|
||||
return new Promise<void>((c, e) => {
|
||||
const out = createWriteStream(filePath);
|
||||
|
||||
out.once('finish', () => c(undefined));
|
||||
context.stream.once('error', e);
|
||||
context.stream.pipe(out);
|
||||
});
|
||||
}
|
||||
|
||||
export function asText(context: IRequestContext): Promise<string | null> {
|
||||
return new Promise((c, e) => {
|
||||
if (!isSuccess(context)) {
|
||||
return e('Server returned ' + context.res.statusCode);
|
||||
}
|
||||
|
||||
if (hasNoContent(context)) {
|
||||
return c(null);
|
||||
}
|
||||
|
||||
const buffer: string[] = [];
|
||||
context.stream.on('data', (d: string) => buffer.push(d));
|
||||
context.stream.on('end', () => c(buffer.join('')));
|
||||
context.stream.on('error', e);
|
||||
});
|
||||
}
|
||||
|
||||
export function asJson<T = {}>(context: IRequestContext): Promise<T | null> {
|
||||
return new Promise((c, e) => {
|
||||
if (!isSuccess(context)) {
|
||||
return e('Server returned ' + context.res.statusCode);
|
||||
}
|
||||
|
||||
if (hasNoContent(context)) {
|
||||
return c(null);
|
||||
}
|
||||
|
||||
if (!/application\/json/.test(context.res.headers['content-type'])) {
|
||||
// {{SQL CARBON EDIT}}
|
||||
//return e('Response doesn\'t appear to be JSON');
|
||||
}
|
||||
|
||||
const buffer: string[] = [];
|
||||
context.stream.on('data', (d: string) => buffer.push(d));
|
||||
context.stream.on('end', () => {
|
||||
try {
|
||||
c(JSON.parse(buffer.join('')));
|
||||
} catch (err) {
|
||||
e(err);
|
||||
}
|
||||
});
|
||||
context.stream.on('error', e);
|
||||
});
|
||||
}
|
||||
@@ -1,786 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Database, Statement } from 'vscode-sqlite3';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ThrottledDelayer, timeout } from 'vs/base/common/async';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
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 { fill } from 'vs/base/common/arrays';
|
||||
|
||||
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 {
|
||||
hint?: StorageHint;
|
||||
}
|
||||
|
||||
export interface IUpdateRequest {
|
||||
insert?: Map<string, string>;
|
||||
delete?: Set<string>;
|
||||
}
|
||||
|
||||
export interface IStorageItemsChangeEvent {
|
||||
items: Map<string, 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>;
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string>;
|
||||
}
|
||||
|
||||
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>;
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string>;
|
||||
}
|
||||
|
||||
enum StorageState {
|
||||
None,
|
||||
Initialized,
|
||||
Closed
|
||||
}
|
||||
|
||||
export class Storage extends Disposable implements IStorage {
|
||||
_serviceBrand: any;
|
||||
|
||||
private static readonly DEFAULT_FLUSH_DELAY = 100;
|
||||
|
||||
private readonly _onDidChangeStorage: Emitter<string> = this._register(new Emitter<string>());
|
||||
get onDidChangeStorage(): Event<string> { return this._onDidChangeStorage.event; }
|
||||
|
||||
private state = StorageState.None;
|
||||
|
||||
private cache: Map<string, string> = new Map<string, string>();
|
||||
|
||||
private flushDelayer: ThrottledDelayer<void>;
|
||||
|
||||
private pendingDeletes: Set<string> = new Set<string>();
|
||||
private pendingInserts: Map<string, string> = new Map();
|
||||
|
||||
constructor(
|
||||
protected database: IStorageDatabase,
|
||||
private options: IStorageOptions = Object.create(null)
|
||||
) {
|
||||
super();
|
||||
|
||||
this.flushDelayer = this._register(new ThrottledDelayer(Storage.DEFAULT_FLUSH_DELAY));
|
||||
|
||||
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.items.forEach((value, key) => this.accept(key, value));
|
||||
}
|
||||
|
||||
private accept(key: string, value: string): 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 Promise.resolve(); // 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 Promise.resolve();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
return this.database.checkIntegrity(full);
|
||||
}
|
||||
}
|
||||
|
||||
interface IDatabaseConnection {
|
||||
db: Database;
|
||||
|
||||
isInMemory: boolean;
|
||||
|
||||
isErroneous?: boolean;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface ISQLiteStorageDatabaseOptions {
|
||||
logging?: ISQLiteStorageDatabaseLoggingOptions;
|
||||
}
|
||||
|
||||
export interface ISQLiteStorageDatabaseLoggingOptions {
|
||||
logError?: (error: string | Error) => void;
|
||||
logTrace?: (msg: string) => void;
|
||||
}
|
||||
|
||||
export class SQLiteStorageDatabase implements IStorageDatabase {
|
||||
|
||||
static 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 BUSY_OPEN_TIMEOUT = 2000; // timeout in ms to retry when opening DB fails with SQLITE_BUSY
|
||||
private static MAX_HOST_PARAMETERS = 256; // maximum number of parameters within a statement
|
||||
|
||||
private path: string;
|
||||
private name: string;
|
||||
|
||||
private logger: SQLiteStorageDatabaseLogger;
|
||||
|
||||
private whenConnected: Promise<IDatabaseConnection>;
|
||||
|
||||
constructor(path: string, options: ISQLiteStorageDatabaseOptions = Object.create(null)) {
|
||||
this.path = path;
|
||||
this.name = basename(path);
|
||||
|
||||
this.logger = new SQLiteStorageDatabaseLogger(options.logging);
|
||||
|
||||
this.whenConnected = this.connect(path);
|
||||
}
|
||||
|
||||
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> {
|
||||
let updateCount = 0;
|
||||
if (request.insert) {
|
||||
updateCount += request.insert.size;
|
||||
}
|
||||
if (request.delete) {
|
||||
updateCount += request.delete.size;
|
||||
}
|
||||
|
||||
if (updateCount === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
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, () => {
|
||||
|
||||
// INSERT
|
||||
if (request.insert && request.insert.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;
|
||||
request.insert.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 ${fill(keysValuesChunk.length / 2, '(?,?)').join(',')}`, stmt => stmt.run(keysValuesChunk), () => {
|
||||
const keys: string[] = [];
|
||||
let length = 0;
|
||||
request.insert!.forEach((value, key) => {
|
||||
keys.push(key);
|
||||
length += value.length;
|
||||
});
|
||||
|
||||
return `Keys: ${keys.join(', ')} Length: ${length}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE
|
||||
if (request.delete && request.delete.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;
|
||||
request.delete.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 (${fill(keysChunk.length, '?').join(',')})`, stmt => stmt.run(keysChunk), () => {
|
||||
const keys: string[] = [];
|
||||
request.delete!.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, closeError, `[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 restablished
|
||||
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['integrity_check'] : row['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, error: Error & { code?: string }, 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, error, `[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, error, `[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, error, `[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, error, `[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, error, `[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, error, `[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;
|
||||
private readonly logError: (error: string | Error) => void;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InMemoryStorageDatabase implements IStorageDatabase {
|
||||
|
||||
readonly onDidChangeItemsExternal = Event.None;
|
||||
|
||||
private items = new Map<string, string>();
|
||||
|
||||
getItems(): Promise<Map<string, string>> {
|
||||
return Promise.resolve(this.items);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
checkIntegrity(full: boolean): Promise<string> {
|
||||
return Promise.resolve('ok');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user