Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/node/ripgrepShim.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { promises as fs } from 'fs';6import * as path from 'path';7import { ILogService } from '../../../../platform/log/common/logService';89let shimCreated: Promise<void> | undefined = undefined;1011const RETRIABLE_COPY_ERROR_CODES = new Set(['EPERM', 'EBUSY']);12const MAX_COPY_ATTEMPTS = 6;13const RETRY_DELAY_BASE_MS = 50;14const RETRY_DELAY_CAP_MS = 500;15const MATERIALIZATION_TIMEOUT_MS = 4000;16const MATERIALIZATION_POLL_INTERVAL_MS = 100;1718/**19* Copies the ripgrep files from VS Code's installation into a @github/copilot location20*21* MUST be called before any `import('@github/copilot/sdk')` or `import('@github/copilot')`.22*23* @github/copilot bundles the ripgrep code24*25* @param extensionPath The extension's path (where to create the shim)26* @param vscodeAppRoot VS Code's installation path (where ripgrep is located)27*/28export async function ensureRipgrepShim(extensionPath: string, vscodeAppRoot: string, logService: ILogService): Promise<void> {29if (shimCreated) {30return shimCreated;31}3233const creation = _ensureRipgrepShim(extensionPath, vscodeAppRoot, logService);34shimCreated = creation.catch(error => {35shimCreated = undefined;36throw error;37});38return shimCreated;39}4041async function _ensureRipgrepShim(extensionPath: string, vscodeAppRoot: string, logService: ILogService): Promise<void> {42const vscodeRipgrepPath = path.join(vscodeAppRoot, 'node_modules', '@vscode', 'ripgrep', 'bin');4344await copyRipgrepShim(extensionPath, vscodeRipgrepPath, logService);45}4647export async function copyRipgrepShim(extensionPath: string, vscodeRipgrepPath: string, logService: ILogService): Promise<void> {48const ripgrepDir = path.join(extensionPath, 'node_modules', '@github', 'copilot', 'sdk', 'ripgrep', 'bin', process.platform + '-' + process.arch);4950logService.info(`Creating ripgrep shim: source=${vscodeRipgrepPath}, dest=${ripgrepDir}`);51try {52await fs.mkdir(ripgrepDir, { recursive: true });53const entries = await fs.readdir(vscodeRipgrepPath);54const uniqueEntries = [...new Set(entries)];55logService.info(`Found ${uniqueEntries.length} entries to copy${uniqueEntries.length !== entries.length ? ` (${entries.length - uniqueEntries.length} duplicates ignored)` : ''}: ${uniqueEntries.join(', ')}`);5657await copyRipgrepWithRetries(vscodeRipgrepPath, ripgrepDir, uniqueEntries, logService);58} catch (error) {59logService.error(`Failed to create ripgrep shim (vscode dir: ${vscodeRipgrepPath}, extension dir: ${ripgrepDir})`, error);60throw error;61}62}6364async function copyRipgrepWithRetries(sourceDir: string, destDir: string, entries: string[], logService: ILogService): Promise<void> {65const primaryBinary = entries.find(entry => entry.endsWith('.node'));66for (let attempt = 1; attempt <= MAX_COPY_ATTEMPTS; attempt++) {67try {68await fs.cp(sourceDir, destDir, {69recursive: true,70dereference: true,71force: true,72filter: async (srcPath) => shouldCopyEntry(srcPath, logService)73});74logService.trace(`Copied ripgrep prebuilds to ${destDir} (attempt ${attempt})`);75return;76} catch (error) {77if (await waitForMaterializedShim(destDir, primaryBinary, logService)) {78logService.trace(`Detected ripgrep shim materialized at ${destDir} by another extension host`);79return;80}8182if (!RETRIABLE_COPY_ERROR_CODES.has(error?.code) || attempt === MAX_COPY_ATTEMPTS) {83throw error;84}8586const delayMs = Math.min(RETRY_DELAY_BASE_MS * Math.pow(2, attempt - 1), RETRY_DELAY_CAP_MS);87logService.warn(`Retryable error (${error.code}) copying ripgrep shim. Retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_COPY_ATTEMPTS})`);88await new Promise(resolve => setTimeout(resolve, delayMs));89}90}91}9293async function shouldCopyEntry(srcPath: string, logService: ILogService): Promise<boolean> {94try {95const stat = await fs.stat(srcPath);96if (stat.isDirectory()) {97return true;98}99100if (stat.size === 0) {101logService.trace(`Skipping ${path.basename(srcPath)}: zero-byte file (likely symlink or special file)`);102return false;103}104105return true;106} catch (error) {107logService.warn(`Failed to stat ${srcPath}: ${error?.message ?? error}`);108return false;109}110}111112async function waitForMaterializedShim(destDir: string, primaryBinary: string | undefined, logService: ILogService): Promise<boolean> {113const deadline = Date.now() + MATERIALIZATION_TIMEOUT_MS;114while (Date.now() <= deadline) {115if (await isShimMaterialized(destDir, primaryBinary)) {116logService.trace(`Reusing ripgrep shim that materialized at ${destDir}`);117return true;118}119120await new Promise(resolve => setTimeout(resolve, MATERIALIZATION_POLL_INTERVAL_MS));121}122123return false;124}125126async function isShimMaterialized(destDir: string, primaryBinary: string | undefined): Promise<boolean> {127if (primaryBinary) {128const binaryStat = await fs.stat(path.join(destDir, primaryBinary)).catch(() => undefined);129if (binaryStat && binaryStat.isFile() && binaryStat.size > 0) {130return true;131}132}133134const entries = await fs.readdir(destDir).catch(() => []);135for (const entry of entries) {136const stat = await fs.stat(path.join(destDir, entry)).catch(() => undefined);137if (stat && stat.isFile() && stat.size > 0) {138return true;139}140}141142return false;143}144145146