|
@@ -23,7 +23,14 @@ interface GrepResolverDependencies {
|
|
|
logger?: (message: string, data?: unknown) => void;
|
|
logger?: (message: string, data?: unknown) => void;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-let autoInstallPromise: Promise<ResolvedGrepCli> | null = null;
|
|
|
|
|
|
|
+interface SharedAutoInstallState {
|
|
|
|
|
+ promise: Promise<ResolvedGrepCli>;
|
|
|
|
|
+ controller: AbortController;
|
|
|
|
|
+ waiters: number;
|
|
|
|
|
+ settled: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let autoInstallState: SharedAutoInstallState | null = null;
|
|
|
|
|
|
|
|
function defaultFindExecutable(name: string): string | null {
|
|
function defaultFindExecutable(name: string): string | null {
|
|
|
try {
|
|
try {
|
|
@@ -152,36 +159,66 @@ function raceWithAbort<T>(
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export async function resolveGrepCliWithAutoInstall(
|
|
|
|
|
- deps: GrepResolverDependencies = {},
|
|
|
|
|
- signal?: AbortSignal,
|
|
|
|
|
-): Promise<ResolvedGrepCli> {
|
|
|
|
|
- if (signal?.aborted) {
|
|
|
|
|
- throw new AbortWaitError('Search was cancelled before execution started.');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function releaseAutoInstallWaiter(state: SharedAutoInstallState): void {
|
|
|
|
|
+ state.waiters = Math.max(0, state.waiters - 1);
|
|
|
|
|
|
|
|
- const current = resolveSync(deps);
|
|
|
|
|
- if (isResolvedRipgrep(current)) {
|
|
|
|
|
- return current;
|
|
|
|
|
|
|
+ if (state.waiters > 0 || state.settled) {
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (autoInstallPromise) {
|
|
|
|
|
- return raceWithAbort(autoInstallPromise, signal);
|
|
|
|
|
|
|
+ if (autoInstallState === state) {
|
|
|
|
|
+ autoInstallState = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- autoInstallPromise = (async () => {
|
|
|
|
|
- const installManagedRipgrep =
|
|
|
|
|
- deps.installLatestStableRipgrep ?? installLatestStableRipgrep;
|
|
|
|
|
|
|
+ state.controller.abort();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function waitForSharedAutoInstall(
|
|
|
|
|
+ state: SharedAutoInstallState,
|
|
|
|
|
+ signal?: AbortSignal,
|
|
|
|
|
+): Promise<ResolvedGrepCli> {
|
|
|
|
|
+ state.waiters += 1;
|
|
|
|
|
+
|
|
|
|
|
+ let released = false;
|
|
|
|
|
+ const release = () => {
|
|
|
|
|
+ if (released) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ released = true;
|
|
|
|
|
+ releaseAutoInstallWaiter(state);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return raceWithAbort(state.promise, signal).finally(release);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createSharedAutoInstall(
|
|
|
|
|
+ deps: GrepResolverDependencies,
|
|
|
|
|
+): SharedAutoInstallState {
|
|
|
|
|
+ const installManagedRipgrep =
|
|
|
|
|
+ deps.installLatestStableRipgrep ?? installLatestStableRipgrep;
|
|
|
|
|
+ const controller = new AbortController();
|
|
|
|
|
+ const state: SharedAutoInstallState = {
|
|
|
|
|
+ controller,
|
|
|
|
|
+ waiters: 0,
|
|
|
|
|
+ settled: false,
|
|
|
|
|
+ promise: Promise.resolve({
|
|
|
|
|
+ path: RG_BINARY,
|
|
|
|
|
+ backend: 'rg' as const,
|
|
|
|
|
+ source: 'missing-rg' as const,
|
|
|
|
|
+ }),
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
|
|
+ state.promise = (async () => {
|
|
|
try {
|
|
try {
|
|
|
- const installedPath = await installManagedRipgrep(signal);
|
|
|
|
|
|
|
+ const installedPath = await installManagedRipgrep(controller.signal);
|
|
|
return {
|
|
return {
|
|
|
path: installedPath,
|
|
path: installedPath,
|
|
|
backend: 'rg' as const,
|
|
backend: 'rg' as const,
|
|
|
source: 'managed-rg' as const,
|
|
source: 'managed-rg' as const,
|
|
|
};
|
|
};
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- if (isAbortLikeError(error) || signal?.aborted) {
|
|
|
|
|
|
|
+ if (isAbortLikeError(error) || controller.signal.aborted) {
|
|
|
throw new AbortWaitError(
|
|
throw new AbortWaitError(
|
|
|
'Search was cancelled before execution started.',
|
|
'Search was cancelled before execution started.',
|
|
|
);
|
|
);
|
|
@@ -203,13 +240,39 @@ export async function resolveGrepCliWithAutoInstall(
|
|
|
});
|
|
});
|
|
|
throw new Error(buildUnavailableBackendMessage(error));
|
|
throw new Error(buildUnavailableBackendMessage(error));
|
|
|
} finally {
|
|
} finally {
|
|
|
- autoInstallPromise = null;
|
|
|
|
|
|
|
+ state.settled = true;
|
|
|
|
|
+
|
|
|
|
|
+ if (autoInstallState === state) {
|
|
|
|
|
+ autoInstallState = null;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
})();
|
|
})();
|
|
|
|
|
|
|
|
- return raceWithAbort(autoInstallPromise, signal);
|
|
|
|
|
|
|
+ return state;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export async function resolveGrepCliWithAutoInstall(
|
|
|
|
|
+ deps: GrepResolverDependencies = {},
|
|
|
|
|
+ signal?: AbortSignal,
|
|
|
|
|
+): Promise<ResolvedGrepCli> {
|
|
|
|
|
+ if (signal?.aborted) {
|
|
|
|
|
+ throw new AbortWaitError('Search was cancelled before execution started.');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const current = resolveSync(deps);
|
|
|
|
|
+ if (isResolvedRipgrep(current)) {
|
|
|
|
|
+ return current;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (autoInstallState) {
|
|
|
|
|
+ return waitForSharedAutoInstall(autoInstallState, signal);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ autoInstallState = createSharedAutoInstall(deps);
|
|
|
|
|
+
|
|
|
|
|
+ return waitForSharedAutoInstall(autoInstallState, signal);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
export function resetGrepCliResolverForTests(): void {
|
|
export function resetGrepCliResolverForTests(): void {
|
|
|
- autoInstallPromise = null;
|
|
|
|
|
|
|
+ autoInstallState = null;
|
|
|
}
|
|
}
|