/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { once } from 'vs/base/common/functional'; import { Iterable } from 'vs/base/common/iterator'; /** * Enables logging of potentially leaked disposables. * * A disposable is considered leaked if it is not disposed or not registered as the child of * another disposable. This tracking is very simple an only works for classes that either * extend Disposable or use a DisposableStore. This means there are a lot of false positives. */ const TRACK_DISPOSABLES = false; let disposableTracker: IDisposableTracker | null = null; export interface IDisposableTracker { trackDisposable(x: IDisposable): void; markTracked(x: IDisposable): void; } export function setDisposableTracker(tracker: IDisposableTracker | null): void { disposableTracker = tracker; } if (TRACK_DISPOSABLES) { const __is_disposable_tracked__ = '__is_disposable_tracked__'; disposableTracker = new class implements IDisposableTracker { trackDisposable(x: IDisposable): void { const stack = new Error('Potentially leaked disposable').stack!; setTimeout(() => { if (!(x as any)[__is_disposable_tracked__]) { console.log(stack); } }, 3000); } markTracked(x: IDisposable): void { if (x && x !== Disposable.None) { try { (x as any)[__is_disposable_tracked__] = true; } catch { // noop } } } }; } function markTracked(x: T): void { if (!disposableTracker) { return; } disposableTracker.markTracked(x); } export function trackDisposable(x: T): T { if (!disposableTracker) { return x; } disposableTracker.trackDisposable(x); return x; } export class MultiDisposeError extends Error { constructor( public readonly errors: any[] ) { super(`Encountered errors while disposing of store. Errors: [${errors.join(', ')}]`); } } export interface IDisposable { dispose(): void; } export function isDisposable(thing: E): thing is E & IDisposable { return typeof (thing).dispose === 'function' && (thing).dispose.length === 0; } export function dispose(disposable: T): T; export function dispose(disposable: T | undefined): T | undefined; export function dispose = IterableIterator>(disposables: IterableIterator): A; export function dispose(disposables: Array): Array; export function dispose(disposables: ReadonlyArray): ReadonlyArray; export function dispose(arg: T | IterableIterator | undefined): any { if (Iterable.is(arg)) { let errors: any[] = []; for (const d of arg) { if (d) { markTracked(d); try { d.dispose(); } catch (e) { errors.push(e); } } } if (errors.length === 1) { throw errors[0]; } else if (errors.length > 1) { throw new MultiDisposeError(errors); } return Array.isArray(arg) ? [] : arg; } else if (arg) { markTracked(arg); arg.dispose(); return arg; } } export function combinedDisposable(...disposables: IDisposable[]): IDisposable { disposables.forEach(markTracked); return toDisposable(() => dispose(disposables)); } export function toDisposable(fn: () => void): IDisposable { const self = trackDisposable({ dispose: () => { markTracked(self); fn(); } }); return self; } export class DisposableStore implements IDisposable { static DISABLE_DISPOSED_WARNING = false; private _toDispose = new Set(); private _isDisposed = false; /** * Dispose of all registered disposables and mark this object as disposed. * * Any future disposables added to this object will be disposed of on `add`. */ public dispose(): void { if (this._isDisposed) { return; } markTracked(this); this._isDisposed = true; this.clear(); } /** * Dispose of all registered disposables but do not mark this object as disposed. */ public clear(): void { try { dispose(this._toDispose.values()); } finally { this._toDispose.clear(); } } public add(t: T): T { if (!t) { return t; } if ((t as unknown as DisposableStore) === this) { throw new Error('Cannot register a disposable on itself!'); } markTracked(t); if (this._isDisposed) { if (!DisposableStore.DISABLE_DISPOSED_WARNING) { console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack); } } else { this._toDispose.add(t); } return t; } } export abstract class Disposable implements IDisposable { static readonly None = Object.freeze({ dispose() { } }); private readonly _store = new DisposableStore(); constructor() { trackDisposable(this); } public dispose(): void { markTracked(this); this._store.dispose(); } protected _register(t: T): T { if ((t as unknown as Disposable) === this) { throw new Error('Cannot register a disposable on itself!'); } return this._store.add(t); } } /** * Manages the lifecycle of a disposable value that may be changed. * * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up. */ export class MutableDisposable implements IDisposable { private _value?: T; private _isDisposed = false; constructor() { trackDisposable(this); } get value(): T | undefined { return this._isDisposed ? undefined : this._value; } set value(value: T | undefined) { if (this._isDisposed || value === this._value) { return; } this._value?.dispose(); if (value) { markTracked(value); } this._value = value; } clear() { this.value = undefined; } dispose(): void { this._isDisposed = true; markTracked(this); this._value?.dispose(); this._value = undefined; } } export interface IReference extends IDisposable { readonly object: T; } export abstract class ReferenceCollection { private readonly references: Map = new Map(); acquire(key: string, ...args: any[]): IReference { let reference = this.references.get(key); if (!reference) { reference = { counter: 0, object: this.createReferencedObject(key, ...args) }; this.references.set(key, reference); } const { object } = reference; const dispose = once(() => { if (--reference!.counter === 0) { this.destroyReferencedObject(key, reference!.object); this.references.delete(key); } }); reference.counter++; return { object, dispose }; } protected abstract createReferencedObject(key: string, ...args: any[]): T; protected abstract destroyReferencedObject(key: string, object: T): void; } export class ImmortalReference implements IReference { constructor(public object: T) { } dispose(): void { /* noop */ } }