diff options
Diffstat (limited to 'scripts/telemetry_utils.js')
| -rw-r--r-- | scripts/telemetry_utils.js | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/scripts/telemetry_utils.js b/scripts/telemetry_utils.js new file mode 100644 index 00000000..62eb910b --- /dev/null +++ b/scripts/telemetry_utils.js @@ -0,0 +1,362 @@ +#!/usr/bin/env node + +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import path from 'path'; +import fs from 'fs'; +import net from 'net'; +import os from 'os'; +import { execSync } from 'child_process'; // Removed spawn, it's not used here +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const ROOT_DIR = path.resolve(__dirname, '..'); +export const GEMINI_DIR = path.join(ROOT_DIR, '.gemini'); +export const OTEL_DIR = path.join(GEMINI_DIR, 'otel'); +export const BIN_DIR = path.join(OTEL_DIR, 'bin'); +export const WORKSPACE_SETTINGS_FILE = path.join(GEMINI_DIR, 'settings.json'); + +export function getJson(url) { + const tmpFile = path.join( + os.tmpdir(), + `gemini-cli-releases-${Date.now()}.json`, + ); + try { + execSync( + `curl -sL -H "User-Agent: gemini-cli-dev-script" -o "${tmpFile}" "${url}"`, + { stdio: 'pipe' }, + ); + const content = fs.readFileSync(tmpFile, 'utf-8'); + return JSON.parse(content); + } catch (e) { + console.error(`Failed to fetch or parse JSON from ${url}`); + throw e; + } finally { + if (fs.existsSync(tmpFile)) { + fs.unlinkSync(tmpFile); + } + } +} + +export function downloadFile(url, dest) { + try { + execSync(`curl -fL -sS -o "${dest}" "${url}"`, { + stdio: 'pipe', + }); + return dest; + } catch (e) { + console.error(`Failed to download file from ${url}`); + throw e; + } +} + +export function findFile(startPath, filter) { + if (!fs.existsSync(startPath)) { + return null; + } + const files = fs.readdirSync(startPath); + for (const file of files) { + const filename = path.join(startPath, file); + const stat = fs.lstatSync(filename); + if (stat.isDirectory()) { + const result = findFile(filename, filter); + if (result) return result; + } else if (filter(file)) { + return filename; + } + } + return null; +} + +export function fileExists(filePath) { + return fs.existsSync(filePath); +} + +export function readJsonFile(filePath) { + if (!fileExists(filePath)) { + return {}; + } + const content = fs.readFileSync(filePath, 'utf-8'); + try { + return JSON.parse(content); + } catch (e) { + console.error(`Error parsing JSON from ${filePath}: ${e.message}`); + return {}; + } +} + +export function writeJsonFile(filePath, data) { + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); +} + +export function waitForPort(port, timeout = 10000) { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + const tryConnect = () => { + const socket = new net.Socket(); + socket.once('connect', () => { + socket.end(); + resolve(); + }); + socket.once('error', (_) => { + if (Date.now() - startTime > timeout) { + reject(new Error(`Timeout waiting for port ${port} to open.`)); + } else { + setTimeout(tryConnect, 500); + } + }); + socket.connect(port, 'localhost'); + }; + tryConnect(); + }); +} + +export async function ensureBinary( + executableName, + repo, + assetNameCallback, + binaryNameInArchive, + isJaeger = false, +) { + const executablePath = path.join(BIN_DIR, executableName); + if (fileExists(executablePath)) { + console.log(`ā
${executableName} already exists at ${executablePath}`); + return executablePath; + } + + console.log(`š ${executableName} not found. Downloading from ${repo}...`); + + const platform = process.platform === 'win32' ? 'windows' : process.platform; + const arch = process.arch === 'x64' ? 'amd64' : process.arch; + const ext = platform === 'windows' ? 'zip' : 'tar.gz'; + + if (isJaeger && platform === 'windows' && arch === 'arm64') { + console.warn( + `ā ļø Jaeger does not have a release for Windows on ARM64. Skipping.`, + ); + return null; + } + + let release; + let asset; + + if (isJaeger) { + console.log(`š Finding latest Jaeger v2+ asset...`); + const releases = getJson(`https://api.github.com/repos/${repo}/releases`); + const sortedReleases = releases + .filter((r) => !r.prerelease && r.tag_name.startsWith('v')) + .sort((a, b) => { + const aVersion = a.tag_name.substring(1).split('.').map(Number); + const bVersion = b.tag_name.substring(1).split('.').map(Number); + for (let i = 0; i < Math.max(aVersion.length, bVersion.length); i++) { + if ((aVersion[i] || 0) > (bVersion[i] || 0)) return -1; + if ((aVersion[i] || 0) < (bVersion[i] || 0)) return 1; + } + return 0; + }); + + for (const r of sortedReleases) { + const expectedSuffix = + platform === 'windows' + ? `-${platform}-${arch}.zip` + : `-${platform}-${arch}.tar.gz`; + const foundAsset = r.assets.find( + (a) => + a.name.startsWith('jaeger-2.') && a.name.endsWith(expectedSuffix), + ); + + if (foundAsset) { + release = r; + asset = foundAsset; + console.log( + `ā¬ļø Found ${asset.name} in release ${r.tag_name}, downloading...`, + ); + break; + } + } + if (!asset) { + throw new Error( + `Could not find a suitable Jaeger v2 asset for platform ${platform}/${arch}.`, + ); + } + } else { + release = getJson(`https://api.github.com/repos/${repo}/releases/latest`); + const version = release.tag_name.startsWith('v') + ? release.tag_name.substring(1) + : release.tag_name; + const assetName = assetNameCallback(version, platform, arch, ext); + asset = release.assets.find((a) => a.name === assetName); + if (!asset) { + throw new Error( + `Could not find a suitable asset for ${repo} (version ${version}) on platform ${platform}/${arch}. Searched for: ${assetName}`, + ); + } + } + + const downloadUrl = asset.browser_download_url; + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'gemini-cli-telemetry-'), + ); + const archivePath = path.join(tmpDir, asset.name); + + try { + console.log(`ā¬ļø Downloading ${asset.name}...`); + downloadFile(downloadUrl, archivePath); + console.log(`š¦ Extracting ${asset.name}...`); + + const actualExt = asset.name.endsWith('.zip') ? 'zip' : 'tar.gz'; + + if (actualExt === 'zip') { + execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`, { stdio: 'pipe' }); + } else { + execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, { stdio: 'pipe' }); + } + + const nameToFind = binaryNameInArchive || executableName; + const foundBinaryPath = findFile(tmpDir, (file) => { + if (platform === 'windows') { + return file === `${nameToFind}.exe`; + } + return file === nameToFind; + }); + + if (!foundBinaryPath) { + throw new Error( + `Could not find binary "${nameToFind}" in extracted archive at ${tmpDir}. Contents: ${fs.readdirSync(tmpDir).join(', ')}`, + ); + } + + fs.renameSync(foundBinaryPath, executablePath); + + if (platform !== 'windows') { + fs.chmodSync(executablePath, '755'); + } + + console.log(`ā
${executableName} installed at ${executablePath}`); + return executablePath; + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + if (fs.existsSync(archivePath)) { + fs.unlinkSync(archivePath); + } + } +} + +export function manageTelemetrySettings( + enable, + oTelEndpoint = 'http://localhost:4317', + originalSandboxSettingToRestore, +) { + const workspaceSettings = readJsonFile(WORKSPACE_SETTINGS_FILE); + const currentSandboxSetting = workspaceSettings.sandbox; + let settingsModified = false; + + if (enable) { + if (workspaceSettings.telemetry !== true) { + workspaceSettings.telemetry = true; + settingsModified = true; + console.log('āļø Enabled telemetry in workspace settings.'); + } + if (workspaceSettings.sandbox !== false) { + workspaceSettings.sandbox = false; + settingsModified = true; + console.log('ā
Disabled sandbox mode for telemetry.'); + } + if (workspaceSettings.telemetryOtlpEndpoint !== oTelEndpoint) { + workspaceSettings.telemetryOtlpEndpoint = oTelEndpoint; + settingsModified = true; + console.log(`š§ Set telemetry OTLP endpoint to ${oTelEndpoint}.`); + } + } else { + if (workspaceSettings.telemetry === true) { + delete workspaceSettings.telemetry; + settingsModified = true; + console.log('āļø Disabled telemetry in workspace settings.'); + } + if (workspaceSettings.telemetryOtlpEndpoint) { + delete workspaceSettings.telemetryOtlpEndpoint; + settingsModified = true; + console.log('š§ Cleared telemetry OTLP endpoint.'); + } + if ( + originalSandboxSettingToRestore !== undefined && + workspaceSettings.sandbox !== originalSandboxSettingToRestore + ) { + workspaceSettings.sandbox = originalSandboxSettingToRestore; + settingsModified = true; + console.log('ā
Restored original sandbox setting.'); + } + } + + if (settingsModified) { + writeJsonFile(WORKSPACE_SETTINGS_FILE, workspaceSettings); + console.log('ā
Workspace settings updated.'); + } else { + console.log( + enable + ? 'ā
Workspace settings are already configured for telemetry.' + : 'ā
Workspace settings already reflect telemetry disabled.', + ); + } + return currentSandboxSetting; +} + +export function registerCleanup( + getProcesses, + getLogFileDescriptors, + originalSandboxSetting, +) { + let cleanedUp = false; + const cleanup = () => { + if (cleanedUp) return; + cleanedUp = true; + + console.log('\nš Shutting down...'); + + manageTelemetrySettings(false, null, originalSandboxSetting); + + const processes = getProcesses ? getProcesses() : []; + processes.forEach((proc) => { + if (proc && proc.pid) { + const name = path.basename(proc.spawnfile); + try { + console.log(`š Stopping ${name} (PID: ${proc.pid})...`); + process.kill(proc.pid, 'SIGTERM'); + console.log(`ā
${name} stopped.`); + } catch (e) { + if (e.code !== 'ESRCH') { + console.error(`Error stopping ${name}: ${e.message}`); + } + } + } + }); + + const logFileDescriptors = getLogFileDescriptors + ? getLogFileDescriptors() + : []; + logFileDescriptors.forEach((fd) => { + if (fd) { + try { + fs.closeSync(fd); + } catch (_) { + /* no-op */ + } + } + }); + }; + + process.on('exit', cleanup); + process.on('SIGINT', () => process.exit(0)); + process.on('SIGTERM', () => process.exit(0)); + process.on('uncaughtException', (err) => { + console.error('Uncaught Exception:', err); + cleanup(); + process.exit(1); + }); +} |
