diff --git a/.vscode/launch.json b/.vscode/launch.json index 0e07b0f4a..37d174957 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,7 @@ "preLaunchTask": "npm: watch" }, { - "name": "Run Extension (no server)", + "name": "Run Extension (no webpack)", "type": "extensionHost", "request": "launch", "args": [ diff --git a/__mocks__/vscode.ts b/__mocks__/vscode.ts index 2ea1673b3..b3ce6f287 100644 --- a/__mocks__/vscode.ts +++ b/__mocks__/vscode.ts @@ -133,7 +133,9 @@ export const env = { clipboard: { writeText: vi.fn() }, - openExternal: vi.fn() + openExternal: vi.fn(), + onDidChangeTelemetryEnabled: vi.fn(), + isTelemetryEnabled: false, } export const NotebookData = vi.fn() diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 81b8aeba2..6d624ba5e 100644 --- a/src/extension/extension.ts +++ b/src/extension/extension.ts @@ -97,6 +97,7 @@ export class RunmeExtension { const server = new KernelServer( context.extensionUri, + kernel.envProps, { retryOnFailure: true, maxNumberOfIntents: 10, diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts index 5278f2bbd..1c1d93d78 100644 --- a/src/extension/kernel.ts +++ b/src/extension/kernel.ts @@ -73,6 +73,7 @@ import { handleNotebookAutosaveSettings, getWorkspaceFolder, getRunnerSessionEnvs, + getEnvProps, } from './utils' import { getEventReporter } from './ai/events' import { getSystemShellPath, isShellLanguage } from './executors/utils' @@ -226,6 +227,14 @@ export class Kernel implements Disposable { } } + get envProps() { + const ext = { + id: this.context!.extension.id, + version: this.context!.extension.packageJSON.version, + } + return getEnvProps(ext) + } + isFeatureOn(featureName: FeatureName): boolean { if (!this.featuresState$) { return false diff --git a/src/extension/server/kernelServer.ts b/src/extension/server/kernelServer.ts index f7e823e82..a9542016f 100644 --- a/src/extension/server/kernelServer.ts +++ b/src/extension/server/kernelServer.ts @@ -5,7 +5,7 @@ import crypto from 'node:crypto' import { ChannelCredentials } from '@grpc/grpc-js' import { GrpcTransport } from '@protobuf-ts/grpc-transport' -import { Disposable, Uri, EventEmitter, Event } from 'vscode' +import { Disposable, Uri, EventEmitter, Event, env } from 'vscode' import getLogger from '../logger' import { HealthCheckRequest, HealthCheckResponse_ServingStatus } from '../grpc/healthTypes' @@ -20,7 +20,7 @@ import { getTLSDir, getTLSEnabled, } from '../../utils/configuration' -import { isPortAvailable } from '../utils' +import { EnvProps, isPortAvailable } from '../utils' import { HealthClient } from '../grpc/client' import KernelServerError from './kernelServerError' @@ -78,6 +78,7 @@ class KernelServer implements IServer { constructor( protected readonly extBasePath: Uri, + protected envProps: EnvProps, options: IServerConfig, externalServer: boolean, protected readonly enableRunner = false, @@ -94,6 +95,10 @@ class KernelServer implements IServer { this.#acceptsIntents = options.acceptsConnection?.intents || 50 this.#acceptsInterval = options.acceptsConnection?.interval || 200 this.#forceExternalServer = externalServer + + env.onDidChangeTelemetryEnabled(() => { + this.kill() + }) } dispose() { @@ -132,6 +137,10 @@ class KernelServer implements IServer { return false } + protected kill(): boolean { + return this.#process?.kill() ?? false + } + address(): string { const customAddress = getCustomServerAddress() if (customAddress) { @@ -238,28 +247,29 @@ class KernelServer implements IServer { args.push('--insecure') } - const process = spawn(binaryLocation, args) + const env = this.getConfiguredEnv() + const child = spawn(binaryLocation, args, { env }) - process.on('close', (code) => { + child.on('close', (code) => { if (this.#loggingEnabled) { log.info(`Server process #${this.#process?.pid} closed with code ${code}`) } this.#onClose.fire({ code }) - this.disposeProcess(process) + this.disposeProcess(child) }) - process.stderr.once('data', () => { + child.stderr.once('data', () => { log.info(`Server process #${this.#process?.pid} started at ${address}`) }) - process.stderr.on('data', (data) => { + child.stderr.on('data', (data) => { if (this.#loggingEnabled) { log.info(data.toString()) } }) - this.#process = process + this.#process = child return Promise.race([ new Promise((resolve, reject) => { @@ -280,7 +290,7 @@ class KernelServer implements IServer { } if (log.addr) { - process.stderr.off('data', cb) + child.stderr.off('data', cb) return resolve(log.addr) } } @@ -289,7 +299,7 @@ class KernelServer implements IServer { } } - process.stderr.on('data', cb) + child.stderr.on('data', cb) }), new Promise((_, reject) => { const { dispose } = this.#onClose.event(() => { @@ -303,6 +313,21 @@ class KernelServer implements IServer { ]) } + protected getConfiguredEnv(): NodeJS.ProcessEnv { + const penv: NodeJS.ProcessEnv = Object.assign(process.env) + + if (!env.isTelemetryEnabled) { + penv['DO_NOT_TRACK'] = 'true' + return penv + } + + Object.entries(this.envProps).forEach(([k, v]) => { + penv[`TELEMETRY_${k.toUpperCase()}`] = v + }) + + return penv + } + protected async acceptsConnection(): Promise { const INTERVAL = this.#acceptsInterval const INTENTS = this.#acceptsIntents diff --git a/src/extension/utils.ts b/src/extension/utils.ts index a16cd2fcd..c297b482e 100644 --- a/src/extension/utils.ts +++ b/src/extension/utils.ts @@ -9,6 +9,7 @@ import vscode, { Uri, workspace, env, + UIKind, window, Disposable, NotebookCell, @@ -822,3 +823,38 @@ export async function getGitContext(path: string) { } } } + +export interface EnvProps { + extname: string + extversion: string + remotename: string + appname: string + product: string + platform: string + uikind: string +} + +export function getEnvProps(extension: { id: string; version: string }) { + const extProps: EnvProps = { + extname: extension.id, + extversion: extension.version, + remotename: env.remoteName ?? 'none', + appname: env.appName, + product: env.appHost, + platform: `${os.platform()}_${os.arch()}`, + uikind: 'other', + } + + switch (env.uiKind) { + case UIKind.Web: + extProps['uikind'] = 'web' + break + case UIKind.Desktop: + extProps['uikind'] = 'desktop' + break + default: + extProps['uikind'] = 'other' + } + + return extProps +} diff --git a/tests/extension/extension.test.ts b/tests/extension/extension.test.ts index 8ba4bc7e0..8c32e409a 100644 --- a/tests/extension/extension.test.ts +++ b/tests/extension/extension.test.ts @@ -70,6 +70,15 @@ vi.mock('../../src/extension/utils', async () => ({ resetNotebookSettings: vi.fn(), getGithubAuthSession: vi.fn().mockResolvedValue(undefined), getPlatformAuthSession: vi.fn().mockResolvedValue(undefined), + getEnvProps: vi.fn().mockReturnValue({ + extname: 'stateful.runme', + extversion: '1.2.3-foo.1', + remotename: 'none', + appname: 'Visual Studio Code', + product: 'desktop', + platform: 'darwin_arm64', + uikind: 'desktop', + }), })) vi.mock('../../src/extension/grpc/runner/v1', () => ({})) @@ -92,6 +101,12 @@ test('initializes all providers', async () => { const context: any = { subscriptions: [], extensionUri: { fsPath: '/foo/bar' }, + extension: { + id: 'foo.bar', + packageJSON: { + version: '1.2.3-rc.4', + }, + }, environmentVariableCollection: { prepend: vi.fn(), append: vi.fn(), diff --git a/tests/extension/kernel.test.ts b/tests/extension/kernel.test.ts index 4f44a1bcc..b51f7e87d 100644 --- a/tests/extension/kernel.test.ts +++ b/tests/extension/kernel.test.ts @@ -32,6 +32,15 @@ vi.mock('../../src/extension/utils', async () => { getGithubAuthSession: vi.fn().mockResolvedValue({ accessToken: '123', }), + getEnvProps: vi.fn().mockReturnValue({ + extname: 'stateful.runme', + extversion: '1.2.3-foo.1', + remotename: 'none', + appname: 'Visual Studio Code', + product: 'desktop', + platform: 'darwin_arm64', + uikind: 'desktop', + }), } }) vi.mock('../../src/utils/configuration', async (importActual) => { @@ -414,3 +423,19 @@ test('supportedLanguages', async () => { expect(k.getSupportedLanguages()![0]).toStrictEqual('shellscript') }) + +test('#envProps', async () => { + const k = new Kernel({ + extension: { id: 'stateful.runme', packageJSON: { version: '1.2.3-rc.0' } }, + } as any) + + expect(k.envProps).toStrictEqual({ + appname: 'Visual Studio Code', + extname: 'stateful.runme', + extversion: '1.2.3-foo.1', + platform: 'darwin_arm64', + product: 'desktop', + remotename: 'none', + uikind: 'desktop', + }) +}) diff --git a/tests/extension/server/kernelServer.test.ts b/tests/extension/server/kernelServer.test.ts index f790c9205..70547797e 100644 --- a/tests/extension/server/kernelServer.test.ts +++ b/tests/extension/server/kernelServer.test.ts @@ -2,7 +2,7 @@ import fs from 'node:fs/promises' import { suite, test, expect, vi, beforeEach } from 'vitest' -import { Uri, workspace } from 'vscode' +import { Uri, workspace, env } from 'vscode' // eslint-disable-next-line max-len import { HealthCheckResponse_ServingStatus } from '@buf/grpc_grpc.community_timostamm-protobuf-ts/grpc/health/v1/health_pb' @@ -98,6 +98,7 @@ suite('Kernel server spawn process', () => { const server = new Server( Uri.file('/Users/user/.vscode/extension/stateful.runme'), + {}, { retryOnFailure: true, maxNumberOfIntents: 2, @@ -114,6 +115,7 @@ suite('Kernel server spawn process', () => { const server = new Server( Uri.file('/Users/user/.vscode/extension/stateful.runme'), + {}, { retryOnFailure: true, maxNumberOfIntents: 2, @@ -130,6 +132,7 @@ suite('Kernel server spawn process', () => { const server = new Server( Uri.file('/Users/user/.vscode/extension/stateful.runme'), + {}, { retryOnFailure: true, maxNumberOfIntents: 2, @@ -146,6 +149,7 @@ suite('Kernel server spawn process', () => { const server = new Server( Uri.file('/Users/user/.vscode/extension/stateful.runme'), + {}, { retryOnFailure: true, maxNumberOfIntents: 2, @@ -157,7 +161,7 @@ suite('Kernel server spawn process', () => { expect(address).toStrictEqual('localhost:7863') }) - test('Should try 2 times before failing', async () => { + test('Should try twice before failing', async () => { configValues.enableTLS = true const server = createServer({ @@ -189,6 +193,18 @@ suite('Kernel server spawn process', () => { expect(server['_port']()).toStrictEqual(port + 1) }) + + test('Should respect telemetry choice', async () => { + configValues.enableTLS = true + + const server = createServer({ + retryOnFailure: true, + maxNumberOfIntents: 2, + }) + + vi.mocked(env).isTelemetryEnabled = false + expect(server['getConfiguredEnv']()['DO_NOT_TRACK']).toBe('true') + }) }) suite('Kernel server accept connections', () => { @@ -230,6 +246,7 @@ function createServer( ) { return new Server( Uri.file('/Users/user/.vscode/extension/stateful.runme'), + {}, config, externalServer, )