diff --git a/news/1 Enhancements/11327.md b/news/1 Enhancements/11327.md new file mode 100644 index 000000000000..cca80793130b --- /dev/null +++ b/news/1 Enhancements/11327.md @@ -0,0 +1 @@ +Show a prompt asking user to upgrade Code runner to new version to keep using it when in Deprecate PythonPath experiment. diff --git a/package.nls.json b/package.nls.json index 5089f0f86cc0..77622a77439c 100644 --- a/package.nls.json +++ b/package.nls.json @@ -151,6 +151,7 @@ "InterpreterQuickPickList.enterPath.placeholder": "Enter path to a Python interpreter.", "InterpreterQuickPickList.browsePath.label": "Find...", "InterpreterQuickPickList.browsePath.detail": "Browse your file system to find a Python interpreter.", + "diagnostics.upgradeCodeRunner": "Please update the Code Runner extension for it to be compatible with the Python extension.", "Common.bannerLabelYes": "Yes", "Common.bannerLabelNo": "No", "Common.doNotShowAgain": "Do not show again", diff --git a/src/client/application/diagnostics/checks/upgradeCodeRunner.ts b/src/client/application/diagnostics/checks/upgradeCodeRunner.ts new file mode 100644 index 000000000000..c2f43939ce6e --- /dev/null +++ b/src/client/application/diagnostics/checks/upgradeCodeRunner.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, named } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { CODE_RUNNER_EXTENSION_ID } from '../../../common/constants'; +import { DeprecatePythonPath } from '../../../common/experimentGroups'; +import { IDisposableRegistry, IExperimentsManager, IExtensions, Resource } from '../../../common/types'; +import { Common, Diagnostics } from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { IDiagnosticsCommandFactory } from '../commands/types'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +export class UpgradeCodeRunnerDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.UpgradeCodeRunnerDiagnostic, + message, + DiagnosticSeverity.Information, + DiagnosticScope.Global, + resource + ); + } +} + +export const UpgradeCodeRunnerDiagnosticServiceId = 'UpgradeCodeRunnerDiagnosticServiceId'; + +export class UpgradeCodeRunnerDiagnosticService extends BaseDiagnosticsService { + public _diagnosticReturned: boolean = false; + private workspaceService: IWorkspaceService; + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + @inject(IExtensions) private readonly extensions: IExtensions + ) { + super([DiagnosticCodes.UpgradeCodeRunnerDiagnostic], serviceContainer, disposableRegistry, true); + this.workspaceService = this.serviceContainer.get(IWorkspaceService); + } + public async diagnose(resource: Resource): Promise { + if (this._diagnosticReturned) { + return []; + } + const experiments = this.serviceContainer.get(IExperimentsManager); + experiments.sendTelemetryIfInExperiment(DeprecatePythonPath.control); + if (!experiments.inExperiment(DeprecatePythonPath.experiment)) { + return []; + } + const extension = this.extensions.getExtension(CODE_RUNNER_EXTENSION_ID); + if (!extension) { + return []; + } + const flagValue: boolean | undefined = extension.packageJSON?.featureFlags?.usingNewPythonInterpreterPathApi; + if (flagValue) { + // Using new version of Code runner already, no need to upgrade + return []; + } + const pythonExecutor = this.workspaceService + .getConfiguration('code-runner', resource) + .get('executorMap.python'); + if (pythonExecutor?.includes('$pythonPath')) { + this._diagnosticReturned = true; + return [new UpgradeCodeRunnerDiagnostic(Diagnostics.upgradeCodeRunner(), resource)]; + } + return []; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !(await this.canHandle(diagnostics[0]))) { + return; + } + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); + const options = [ + { + prompt: Common.doNotShowAgain(), + command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }) + } + ]; + + await this.messageService.handle(diagnostic, { commandPrompts: options }); + } +} diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts index c89a2efdb3be..b975ae9222a0 100644 --- a/src/client/application/diagnostics/constants.ts +++ b/src/client/application/diagnostics/constants.ts @@ -17,5 +17,6 @@ export enum DiagnosticCodes { PythonPathDeprecatedDiagnostic = 'PythonPathDeprecatedDiagnostic', JustMyCodeDiagnostic = 'JustMyCodeDiagnostic', ConsoleTypeDiagnostic = 'ConsoleTypeDiagnostic', - ConfigPythonPathDiagnostic = 'ConfigPythonPathDiagnostic' + ConfigPythonPathDiagnostic = 'ConfigPythonPathDiagnostic', + UpgradeCodeRunnerDiagnostic = 'UpgradeCodeRunnerDiagnostic' } diff --git a/src/client/application/diagnostics/serviceRegistry.ts b/src/client/application/diagnostics/serviceRegistry.ts index d142aecbc8b8..9aefb8e742d9 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -33,6 +33,7 @@ import { PythonPathDeprecatedDiagnosticService, PythonPathDeprecatedDiagnosticServiceId } from './checks/pythonPathDeprecated'; +import { UpgradeCodeRunnerDiagnosticService, UpgradeCodeRunnerDiagnosticServiceId } from './checks/upgradeCodeRunner'; import { DiagnosticsCommandFactory } from './commands/factory'; import { IDiagnosticsCommandFactory } from './commands/types'; import { DiagnosticFilterService } from './filter'; @@ -85,6 +86,12 @@ export function registerTypes(serviceManager: IServiceManager, languageServerTyp PythonPathDeprecatedDiagnosticService, PythonPathDeprecatedDiagnosticServiceId ); + + serviceManager.addSingleton( + IDiagnosticsService, + UpgradeCodeRunnerDiagnosticService, + UpgradeCodeRunnerDiagnosticServiceId + ); serviceManager.addSingleton(IDiagnosticsCommandFactory, DiagnosticsCommandFactory); serviceManager.addSingleton(IApplicationDiagnostics, ApplicationDiagnostics); diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 372e4844bd18..526cd0df6035 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -26,6 +26,10 @@ export namespace Diagnostics { 'diagnostics.lsNotSupported', 'Your operating system does not meet the minimum requirements of the Python Language Server. Reverting to the alternative autocompletion provider, Jedi.' ); + export const upgradeCodeRunner = localize( + 'diagnostics.upgradeCodeRunner', + 'Please update the Code Runner extension for it to be compatible with the Python extension.' + ); export const removePythonPathSettingsJson = localize( 'diagnostics.removePythonPathSettingsJson', 'The setting "python.pythonPath" defined in your workspace settings is now deprecated. Do you want us to delete it? This will only remove the "python.pythonPath" entry from your settings.json.' diff --git a/src/test/application/diagnostics/checks/upgradeCodeRunner.unit.test.ts b/src/test/application/diagnostics/checks/upgradeCodeRunner.unit.test.ts new file mode 100644 index 000000000000..64b4f9f35ee5 --- /dev/null +++ b/src/test/application/diagnostics/checks/upgradeCodeRunner.unit.test.ts @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:max-func-body-length no-any max-classes-per-file + +import { assert, expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { DiagnosticSeverity, Extension, Uri, WorkspaceConfiguration } from 'vscode'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + UpgradeCodeRunnerDiagnostic, + UpgradeCodeRunnerDiagnosticService +} from '../../../../client/application/diagnostics/checks/upgradeCodeRunner'; +import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService +} from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { CODE_RUNNER_EXTENSION_ID } from '../../../../client/common/constants'; +import { DeprecatePythonPath } from '../../../../client/common/experimentGroups'; +import { IDisposableRegistry, IExperimentsManager, IExtensions, Resource } from '../../../../client/common/types'; +import { Common, Diagnostics } from '../../../../client/common/utils/localize'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Upgrade Code Runner', () => { + const resource = Uri.parse('a'); + let diagnosticService: UpgradeCodeRunnerDiagnosticService; + let messageHandler: typemoq.IMock>; + let commandFactory: typemoq.IMock; + let workspaceService: typemoq.IMock; + let filterService: typemoq.IMock; + let serviceContainer: typemoq.IMock; + let experimentsManager: typemoq.IMock; + let extensions: typemoq.IMock; + function createContainer() { + extensions = typemoq.Mock.ofType(); + serviceContainer = typemoq.Mock.ofType(); + experimentsManager = typemoq.Mock.ofType(); + filterService = typemoq.Mock.ofType(); + messageHandler = typemoq.Mock.ofType>(); + serviceContainer + .setup((s) => + s.get( + typemoq.It.isValue(IDiagnosticHandlerService), + typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) + ) + ) + .returns(() => messageHandler.object); + commandFactory = typemoq.Mock.ofType(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IExperimentsManager))) + .returns(() => experimentsManager.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) + .returns(() => commandFactory.object); + workspaceService = typemoq.Mock.ofType(); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(typemoq.It.isValue(IDisposableRegistry))).returns(() => []); + return serviceContainer.object; + } + suite('Diagnostics', () => { + setup(() => { + diagnosticService = new (class extends UpgradeCodeRunnerDiagnosticService { + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(createContainer(), messageHandler.object, [], extensions.object); + (diagnosticService as any)._clear(); + }); + + test('Can handle UpgradeCodeRunnerDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.UpgradeCodeRunnerDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal( + true, + `Should be able to handle ${DiagnosticCodes.UpgradeCodeRunnerDiagnostic}` + ); + diagnostic.verifyAll(); + }); + + test('Can not handle non-UpgradeCodeRunnerDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType(); + diagnostic + .setup((d) => d.code) + .returns(() => 'Something Else' as any) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); + + test('Should not display a message if the diagnostic code has been ignored', async () => { + const diagnostic = typemoq.Mock.ofType(); + + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.UpgradeCodeRunnerDiagnostic))) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.UpgradeCodeRunnerDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + messageHandler + .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic.object]); + + filterService.verifyAll(); + diagnostic.verifyAll(); + commandFactory.verifyAll(); + messageHandler.verifyAll(); + }); + + test('UpgradeCodeRunnerDiagnostic is handled as expected', async () => { + const diagnostic = new UpgradeCodeRunnerDiagnostic('message', resource); + const ignoreCmd = ({ cmd: 'ignoreCmd' } as any) as IDiagnosticCommand; + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.UpgradeCodeRunnerDiagnostic))) + .returns(() => Promise.resolve(false)); + let messagePrompt: MessageCommandPrompt | undefined; + messageHandler + .setup((i) => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => (messagePrompt = p)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith>({ type: 'ignore' }) + ) + ) + .returns(() => ignoreCmd) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); + expect(messagePrompt!.commandPrompts.length).to.equal(1, 'Incorrect length'); + expect(messagePrompt!.commandPrompts[0]).to.be.deep.equal({ + prompt: Common.doNotShowAgain(), + command: ignoreCmd + }); + }); + + test('Handling an empty diagnostic should not show a message nor return a command', async () => { + const diagnostics: IDiagnostic[] = []; + + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle(diagnostics); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + + test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { + const diagnostic = new (class SomeRandomDiagnostic extends BaseDiagnostic { + constructor(message: string, uri: Resource) { + super( + 'SomeRandomDiagnostic' as any, + message, + DiagnosticSeverity.Information, + DiagnosticScope.WorkspaceFolder, + uri + ); + } + })('message', undefined); + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => f.createCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + + test('If a diagnostic has already been returned, empty diagnostics is returned', async () => { + diagnosticService._diagnosticReturned = false; + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + }); + + test('If not in DeprecatePythonPath experiment, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => false); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + }); + + test('If Code Runner extension is not installed, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => undefined); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + }); + + test('If Code Runner extension is installed but the appropriate feature flag is set in package.json, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const extension = typemoq.Mock.ofType>(); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => extension.object); + extension + .setup((e) => e.packageJSON) + .returns(() => ({ + featureFlags: { + usingNewPythonInterpreterPathApi: true + } + })); + workspaceService + .setup((w) => w.getConfiguration('code-runner', resource)) + .verifiable(typemoq.Times.never()); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + workspaceService.verifyAll(); + }); + + test('If old version of Code Runner extension is installed but setting `code-runner.executorMap.python` is not set, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType(); + const extension = typemoq.Mock.ofType>(); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => extension.object); + extension.setup((e) => e.packageJSON).returns(() => undefined); + workspaceService + .setup((w) => w.getConfiguration('code-runner', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig.setup((w) => w.get('executorMap.python')).returns(() => undefined); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + workspaceService.verifyAll(); + }); + + test('If old version of Code Runner extension is installed but $pythonPath is not being used, empty diagnostics is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType(); + const extension = typemoq.Mock.ofType>(); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => extension.object); + extension.setup((e) => e.packageJSON).returns(() => undefined); + workspaceService + .setup((w) => w.getConfiguration('code-runner', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig.setup((w) => w.get('executorMap.python')).returns(() => 'Random string'); + + const diagnostics = await diagnosticService.diagnose(resource); + + assert.deepEqual(diagnostics, []); + workspaceService.verifyAll(); + }); + + test('If old version of Code Runner extension is installed and $pythonPath is being used, diagnostic with appropriate message is returned', async () => { + experimentsManager.setup((e) => e.inExperiment(DeprecatePythonPath.experiment)).returns(() => true); + experimentsManager + .setup((e) => e.sendTelemetryIfInExperiment(DeprecatePythonPath.control)) + .returns(() => undefined); + const workspaceConfig = typemoq.Mock.ofType(); + const extension = typemoq.Mock.ofType>(); + extensions.setup((e) => e.getExtension(CODE_RUNNER_EXTENSION_ID)).returns(() => extension.object); + extension.setup((e) => e.packageJSON).returns(() => undefined); + workspaceService + .setup((w) => w.getConfiguration('code-runner', resource)) + .returns(() => workspaceConfig.object) + .verifiable(typemoq.Times.once()); + workspaceConfig + .setup((w) => w.get('executorMap.python')) + .returns(() => 'This string contains $pythonPath'); + + const diagnostics = await diagnosticService.diagnose(resource); + + expect(diagnostics.length).to.equal(1); + expect(diagnostics[0].message).to.equal(Diagnostics.upgradeCodeRunner()); + expect(diagnostics[0].resource).to.equal(resource); + expect(diagnosticService._diagnosticReturned).to.equal(true, ''); + + workspaceService.verifyAll(); + }); + }); +}); diff --git a/src/test/application/diagnostics/serviceRegistry.unit.test.ts b/src/test/application/diagnostics/serviceRegistry.unit.test.ts index 813649f7e304..6c688abf7317 100644 --- a/src/test/application/diagnostics/serviceRegistry.unit.test.ts +++ b/src/test/application/diagnostics/serviceRegistry.unit.test.ts @@ -38,6 +38,10 @@ import { PythonPathDeprecatedDiagnosticService, PythonPathDeprecatedDiagnosticServiceId } from '../../../client/application/diagnostics/checks/pythonPathDeprecated'; +import { + UpgradeCodeRunnerDiagnosticService, + UpgradeCodeRunnerDiagnosticServiceId +} from '../../../client/application/diagnostics/checks/upgradeCodeRunner'; import { DiagnosticsCommandFactory } from '../../../client/application/diagnostics/commands/factory'; import { IDiagnosticsCommandFactory } from '../../../client/application/diagnostics/commands/types'; import { DiagnosticFilterService } from '../../../client/application/diagnostics/filter'; @@ -88,6 +92,13 @@ suite('Application Diagnostics - Register classes in IOC Container', () => { InvalidLaunchJsonDebuggerServiceId ) ); + verify( + serviceManager.addSingleton( + IDiagnosticsService, + UpgradeCodeRunnerDiagnosticService, + UpgradeCodeRunnerDiagnosticServiceId + ) + ); verify( serviceManager.addSingleton( IDiagnosticsService,