Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/node/ripgrepShim.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { promises as fs } from 'fs';
7
import * as path from 'path';
8
import { ILogService } from '../../../../platform/log/common/logService';
9
10
let shimCreated: Promise<void> | undefined = undefined;
11
12
const RETRIABLE_COPY_ERROR_CODES = new Set(['EPERM', 'EBUSY']);
13
const MAX_COPY_ATTEMPTS = 6;
14
const RETRY_DELAY_BASE_MS = 50;
15
const RETRY_DELAY_CAP_MS = 500;
16
const MATERIALIZATION_TIMEOUT_MS = 4000;
17
const MATERIALIZATION_POLL_INTERVAL_MS = 100;
18
19
/**
20
* Copies the ripgrep files from VS Code's installation into a @github/copilot location
21
*
22
* MUST be called before any `import('@github/copilot/sdk')` or `import('@github/copilot')`.
23
*
24
* @github/copilot bundles the ripgrep code
25
*
26
* @param extensionPath The extension's path (where to create the shim)
27
* @param vscodeAppRoot VS Code's installation path (where ripgrep is located)
28
*/
29
export async function ensureRipgrepShim(extensionPath: string, vscodeAppRoot: string, logService: ILogService): Promise<void> {
30
if (shimCreated) {
31
return shimCreated;
32
}
33
34
const creation = _ensureRipgrepShim(extensionPath, vscodeAppRoot, logService);
35
shimCreated = creation.catch(error => {
36
shimCreated = undefined;
37
throw error;
38
});
39
return shimCreated;
40
}
41
42
async function _ensureRipgrepShim(extensionPath: string, vscodeAppRoot: string, logService: ILogService): Promise<void> {
43
const vscodeRipgrepPath = path.join(vscodeAppRoot, 'node_modules', '@vscode', 'ripgrep', 'bin');
44
45
await copyRipgrepShim(extensionPath, vscodeRipgrepPath, logService);
46
}
47
48
export async function copyRipgrepShim(extensionPath: string, vscodeRipgrepPath: string, logService: ILogService): Promise<void> {
49
const ripgrepDir = path.join(extensionPath, 'node_modules', '@github', 'copilot', 'sdk', 'ripgrep', 'bin', process.platform + '-' + process.arch);
50
51
logService.info(`Creating ripgrep shim: source=${vscodeRipgrepPath}, dest=${ripgrepDir}`);
52
try {
53
await fs.mkdir(ripgrepDir, { recursive: true });
54
const entries = await fs.readdir(vscodeRipgrepPath);
55
const uniqueEntries = [...new Set(entries)];
56
logService.info(`Found ${uniqueEntries.length} entries to copy${uniqueEntries.length !== entries.length ? ` (${entries.length - uniqueEntries.length} duplicates ignored)` : ''}: ${uniqueEntries.join(', ')}`);
57
58
await copyRipgrepWithRetries(vscodeRipgrepPath, ripgrepDir, uniqueEntries, logService);
59
} catch (error) {
60
logService.error(`Failed to create ripgrep shim (vscode dir: ${vscodeRipgrepPath}, extension dir: ${ripgrepDir})`, error);
61
throw error;
62
}
63
}
64
65
async function copyRipgrepWithRetries(sourceDir: string, destDir: string, entries: string[], logService: ILogService): Promise<void> {
66
const primaryBinary = entries.find(entry => entry.endsWith('.node'));
67
for (let attempt = 1; attempt <= MAX_COPY_ATTEMPTS; attempt++) {
68
try {
69
await fs.cp(sourceDir, destDir, {
70
recursive: true,
71
dereference: true,
72
force: true,
73
filter: async (srcPath) => shouldCopyEntry(srcPath, logService)
74
});
75
logService.trace(`Copied ripgrep prebuilds to ${destDir} (attempt ${attempt})`);
76
return;
77
} catch (error) {
78
if (await waitForMaterializedShim(destDir, primaryBinary, logService)) {
79
logService.trace(`Detected ripgrep shim materialized at ${destDir} by another extension host`);
80
return;
81
}
82
83
if (!RETRIABLE_COPY_ERROR_CODES.has(error?.code) || attempt === MAX_COPY_ATTEMPTS) {
84
throw error;
85
}
86
87
const delayMs = Math.min(RETRY_DELAY_BASE_MS * Math.pow(2, attempt - 1), RETRY_DELAY_CAP_MS);
88
logService.warn(`Retryable error (${error.code}) copying ripgrep shim. Retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_COPY_ATTEMPTS})`);
89
await new Promise(resolve => setTimeout(resolve, delayMs));
90
}
91
}
92
}
93
94
async function shouldCopyEntry(srcPath: string, logService: ILogService): Promise<boolean> {
95
try {
96
const stat = await fs.stat(srcPath);
97
if (stat.isDirectory()) {
98
return true;
99
}
100
101
if (stat.size === 0) {
102
logService.trace(`Skipping ${path.basename(srcPath)}: zero-byte file (likely symlink or special file)`);
103
return false;
104
}
105
106
return true;
107
} catch (error) {
108
logService.warn(`Failed to stat ${srcPath}: ${error?.message ?? error}`);
109
return false;
110
}
111
}
112
113
async function waitForMaterializedShim(destDir: string, primaryBinary: string | undefined, logService: ILogService): Promise<boolean> {
114
const deadline = Date.now() + MATERIALIZATION_TIMEOUT_MS;
115
while (Date.now() <= deadline) {
116
if (await isShimMaterialized(destDir, primaryBinary)) {
117
logService.trace(`Reusing ripgrep shim that materialized at ${destDir}`);
118
return true;
119
}
120
121
await new Promise(resolve => setTimeout(resolve, MATERIALIZATION_POLL_INTERVAL_MS));
122
}
123
124
return false;
125
}
126
127
async function isShimMaterialized(destDir: string, primaryBinary: string | undefined): Promise<boolean> {
128
if (primaryBinary) {
129
const binaryStat = await fs.stat(path.join(destDir, primaryBinary)).catch(() => undefined);
130
if (binaryStat && binaryStat.isFile() && binaryStat.size > 0) {
131
return true;
132
}
133
}
134
135
const entries = await fs.readdir(destDir).catch(() => []);
136
for (const entry of entries) {
137
const stat = await fs.stat(path.join(destDir, entry)).catch(() => undefined);
138
if (stat && stat.isFile() && stat.size > 0) {
139
return true;
140
}
141
}
142
143
return false;
144
}
145
146