config.ts 5.8 KB
Newer Older
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

import * as fs from 'fs';
9
import * as path from 'path';
10
import * as objects from 'vs/base/common/objects';
J
Johannes Rieken 已提交
11 12
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
13 14
import * as json from 'vs/base/common/json';

B
Benjamin Pasero 已提交
15
export interface IConfigurationChangeEvent<T> {
16 17 18 19
	config: T;
}

export interface IConfigWatcher<T> {
20 21 22
	path: string;
	hasParseErrors: boolean;

B
Benjamin Pasero 已提交
23
	reload(callback: (config: T) => void): void;
24 25 26 27 28
	getConfig(): T;
	getValue<V>(key: string, fallback?: V): V;
}

export interface IConfigOptions<T> {
29
	onError: (error: Error | string) => void;
30 31
	defaultConfig?: T;
	changeBufferDelay?: number;
32
	parse?: (content: string, errors: any[]) => T;
33
	initCallback?: (config: T) => void;
34 35 36 37
}

/**
 * A simple helper to watch a configured file for changes and process its contents as JSON object.
B
Benjamin Pasero 已提交
38 39 40 41 42
 * Supports:
 * - comments in JSON files and errors
 * - symlinks for the config file itself
 * - delayed processing of changes to accomodate for lots of changes
 * - configurable defaults
43 44 45
 */
export class ConfigWatcher<T> implements IConfigWatcher<T>, IDisposable {
	private cache: T;
46
	private parseErrors: json.ParseError[];
B
Benjamin Pasero 已提交
47
	private disposed: boolean;
48
	private loaded: boolean;
J
Joao Moreno 已提交
49
	private timeoutHandle: NodeJS.Timer;
50
	private disposables: IDisposable[];
B
Benjamin Pasero 已提交
51
	private _onDidUpdateConfiguration: Emitter<IConfigurationChangeEvent<T>>;
52

53
	constructor(private _path: string, private options: IConfigOptions<T> = { changeBufferDelay: 0, defaultConfig: Object.create(null), onError: error => console.error(error) }) {
54 55
		this.disposables = [];

B
Benjamin Pasero 已提交
56
		this._onDidUpdateConfiguration = new Emitter<IConfigurationChangeEvent<T>>();
57 58 59 60 61 62
		this.disposables.push(this._onDidUpdateConfiguration);

		this.registerWatcher();
		this.initAsync();
	}

63 64 65 66 67 68 69 70
	public get path(): string {
		return this._path;
	}

	public get hasParseErrors(): boolean {
		return this.parseErrors && this.parseErrors.length > 0;
	}

B
Benjamin Pasero 已提交
71
	public get onDidUpdateConfiguration(): Event<IConfigurationChangeEvent<T>> {
72 73 74 75 76 77 78 79
		return this._onDidUpdateConfiguration.event;
	}

	private initAsync(): void {
		this.loadAsync(config => {
			if (!this.loaded) {
				this.updateCache(config); // prevent race condition if config was loaded sync already
			}
80 81 82
			if (this.options.initCallback) {
				this.options.initCallback(this.getConfig());
			}
83 84 85 86 87 88 89 90 91 92
		});
	}

	private updateCache(value: T): void {
		this.cache = value;
		this.loaded = true;
	}

	private loadSync(): T {
		try {
B
Benjamin Pasero 已提交
93
			return this.parse(fs.readFileSync(this._path).toString());
94 95 96 97 98 99
		} catch (error) {
			return this.options.defaultConfig;
		}
	}

	private loadAsync(callback: (config: T) => void): void {
100
		fs.readFile(this._path, (error, raw) => {
101 102 103 104 105 106 107 108 109
			if (error) {
				return callback(this.options.defaultConfig);
			}

			return callback(this.parse(raw.toString()));
		});
	}

	private parse(raw: string): T {
B
Benjamin Pasero 已提交
110
		let res: T;
111
		try {
112
			this.parseErrors = [];
113
			res = this.options.parse ? this.options.parse(raw, this.parseErrors) : json.parse(raw, this.parseErrors);
114
		} catch (error) {
B
Benjamin Pasero 已提交
115
			// Ignore parsing errors
116 117
		}

B
Benjamin Pasero 已提交
118
		return res || this.options.defaultConfig;
119 120 121 122
	}

	private registerWatcher(): void {

123 124
		// Watch the parent of the path so that we detect ADD and DELETES
		const parentFolder = path.dirname(this._path);
B
Benjamin Pasero 已提交
125
		this.watch(parentFolder);
126 127

		// Check if the path is a symlink and watch its target if so
128
		fs.lstat(this._path, (err, stat) => {
129 130 131 132 133 134
			if (err || stat.isDirectory()) {
				return; // path is not a valid file
			}

			// We found a symlink
			if (stat.isSymbolicLink()) {
135
				fs.readlink(this._path, (err, realPath) => {
136 137 138 139 140 141 142 143 144 145 146
					if (err) {
						return; // path is not a valid symlink
					}

					this.watch(realPath);
				});
			}
		});
	}

	private watch(path: string): void {
B
Benjamin Pasero 已提交
147 148 149 150
		if (this.disposed) {
			return; // avoid watchers that will never get disposed by checking for being disposed
		}

151 152 153
		try {
			const watcher = fs.watch(path);
			watcher.on('change', () => this.onConfigFileChange());
154
			watcher.on('error', (code, signal) => this.options.onError(`Error watching ${path} for configuration changes (${code}, ${signal})`));
155

156 157 158 159 160
			this.disposables.push(toDisposable(() => {
				watcher.removeAllListeners();
				watcher.close();
			}));
		} catch (error) {
B
Benjamin Pasero 已提交
161 162
			fs.exists(path, exists => {
				if (exists) {
163
					this.options.onError(`Failed to watch ${path} for configuration changes (${error.toString()})`);
B
Benjamin Pasero 已提交
164 165
				}
			});
166
		}
167 168 169 170 171 172 173 174
	}

	private onConfigFileChange(): void {
		if (this.timeoutHandle) {
			global.clearTimeout(this.timeoutHandle);
			this.timeoutHandle = null;
		}

B
Benjamin Pasero 已提交
175 176
		// we can get multiple change events for one change, so we buffer through a timeout
		this.timeoutHandle = global.setTimeout(() => this.reload(), this.options.changeBufferDelay);
177 178
	}

B
Benjamin Pasero 已提交
179 180 181 182 183 184 185 186 187 188 189 190 191 192
	public reload(callback?: (config: T) => void): void {
		this.loadAsync(currentConfig => {
			if (!objects.equals(currentConfig, this.cache)) {
				this.updateCache(currentConfig);

				this._onDidUpdateConfiguration.fire({ config: this.cache });
			}

			if (callback) {
				return callback(currentConfig);
			}
		});
	}

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
	public getConfig(): T {
		this.ensureLoaded();

		return this.cache;
	}

	public getValue<V>(key: string, fallback?: V): V {
		this.ensureLoaded();

		if (!key) {
			return fallback;
		}

		const value = this.cache ? this.cache[key] : void 0;

		return typeof value !== 'undefined' ? value : fallback;
	}

	private ensureLoaded(): void {
		if (!this.loaded) {
			this.updateCache(this.loadSync());
		}
	}

	public dispose(): void {
B
Benjamin Pasero 已提交
218
		this.disposed = true;
219 220 221
		this.disposables = dispose(this.disposables);
	}
}