Skip to content

Commit dc10b2d

Browse files
theoephraimclaude
andcommitted
optimize WSL2 performance: cache binary path, skip redundant .exe spawns
- Cache resolveNativeBinary() result (was called 6x per invocation) - Reuse keys from status response in keyExists() to skip key-exists .exe spawn - Fix daemon spawn polling: simpler pipe_exists check at 50ms intervals - Increase timeout to 60s for WSL2 decrypt (includes Windows Hello prompt) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b50854c commit dc10b2d

3 files changed

Lines changed: 31 additions & 19 deletions

File tree

packages/encryption-binary-rust/src/daemon_client.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,16 +180,8 @@ fn spawn_daemon() -> Result<(), String> {
180180
// Wait for daemon to become ready (poll pipe availability)
181181
let start = std::time::Instant::now();
182182
while start.elapsed() < MAX_SPAWN_WAIT {
183-
std::thread::sleep(std::time::Duration::from_millis(100));
184-
185-
// Try to connect — if it works, daemon is ready
186-
if try_daemon_decrypt("", "").is_err() {
187-
// Could be "pipe not found" (not ready) or "decrypt failed" (ready but bad request)
188-
// Check if the pipe exists by trying to open it
189-
if pipe_exists() {
190-
return Ok(());
191-
}
192-
} else {
183+
std::thread::sleep(std::time::Duration::from_millis(50));
184+
if pipe_exists() {
193185
return Ok(());
194186
}
195187
}

packages/varlock/src/lib/local-encrypt/binary-resolver.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,29 +145,37 @@ function ensureExecutable(binaryPath: string): string {
145145
* Resolve the native helper binary path.
146146
* Returns undefined if no binary is found — caller should fall back to pure JS.
147147
*/
148+
let _cachedBinaryPath: string | undefined | null = null; // null = not yet resolved
149+
148150
export function resolveNativeBinary(): string | undefined {
151+
if (_cachedBinaryPath !== null) return _cachedBinaryPath;
152+
149153
debug(`resolving: platform=${process.platform}, isWSL=${isWSL()}, binaryName=${getPlatformBinaryName()}, subdir=${getNativeBinSubdir()}`);
150154

151155
const seaSibling = resolveSeaSibling();
152156
if (seaSibling) {
153157
debug(`resolved via SEA sibling: ${seaSibling}`);
154-
return ensureExecutable(seaSibling);
158+
_cachedBinaryPath = ensureExecutable(seaSibling);
159+
return _cachedBinaryPath;
155160
}
156161

157162
const npmBundled = resolveNpmBundled();
158163
if (npmBundled) {
159164
debug(`resolved via npm bundled: ${npmBundled}`);
160-
return ensureExecutable(npmBundled);
165+
_cachedBinaryPath = ensureExecutable(npmBundled);
166+
return _cachedBinaryPath;
161167
}
162168

163169
const devFallback = resolveDevFallback();
164170
if (devFallback) {
165171
debug(`resolved via dev fallback: ${devFallback}`);
166-
return ensureExecutable(devFallback);
172+
_cachedBinaryPath = ensureExecutable(devFallback);
173+
return _cachedBinaryPath;
167174
}
168175

169176
debug('NOT FOUND: no binary resolved from any strategy');
170177
debug(` SEA sibling dir: ${path.dirname(process.execPath)}`);
171178
debug(` npm bundled dir: ${path.resolve(__dirname, '..', '..', '..', 'native-bins', getNativeBinSubdir())}`);
179+
_cachedBinaryPath = undefined;
172180
return undefined;
173181
}

packages/varlock/src/lib/local-encrypt/index.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function debug(msg: string) {
3030

3131
// ── Native binary one-shot commands ────────────────────────────────────
3232

33-
function runNativeBinary(args: Array<string>): string {
33+
function runNativeBinary(args: Array<string>, opts?: { timeout?: number }): string {
3434
const binaryPath = resolveNativeBinary();
3535
if (!binaryPath) {
3636
debug('runNativeBinary: no binary found');
@@ -39,14 +39,14 @@ function runNativeBinary(args: Array<string>): string {
3939
debug(`runNativeBinary: ${binaryPath} ${args.join(' ')}`);
4040
const output = execFileSync(binaryPath, args, {
4141
encoding: 'utf-8',
42-
timeout: 30_000,
42+
timeout: opts?.timeout ?? 30_000,
4343
}).trim();
4444
debug(`runNativeBinary result: ${output.slice(0, 200)}`);
4545
return output;
4646
}
4747

48-
function runNativeBinaryJson<T = Record<string, unknown>>(args: Array<string>): T {
49-
const output = runNativeBinary(args);
48+
function runNativeBinaryJson<T = Record<string, unknown>>(args: Array<string>, opts?: { timeout?: number }): T {
49+
const output = runNativeBinary(args, opts);
5050
const parsed = JSON.parse(output);
5151
if (parsed.error) {
5252
throw new Error(parsed.error);
@@ -57,6 +57,8 @@ function runNativeBinaryJson<T = Record<string, unknown>>(args: Array<string>):
5757
// ── Backend detection ──────────────────────────────────────────────────
5858

5959
let cachedBackendInfo: BackendInfo | undefined;
60+
/** Keys reported by the status command — avoids a separate key-exists .exe spawn on WSL2 */
61+
let cachedStatusKeys: Array<string> | undefined;
6062

6163
function detectBackendType(): { type: BackendType; isFileFallback: boolean } {
6264
const binaryPath = resolveNativeBinary();
@@ -89,7 +91,8 @@ export function getBackendInfo(): BackendInfo {
8991
// Query the native binary for its actual capabilities
9092
try {
9193
const status = runNativeBinaryJson<NativeStatusResult>(['status']);
92-
debug(`getBackendInfo: status result: hardwareBacked=${status.hardwareBacked}, biometricAvailable=${status.biometricAvailable}, backend=${status.backend}`);
94+
debug(`getBackendInfo: status result: hardwareBacked=${status.hardwareBacked}, biometricAvailable=${status.biometricAvailable}, backend=${status.backend}, keys=${status.keys?.join(',')}`);
95+
cachedStatusKeys = status.keys;
9396
cachedBackendInfo = {
9497
type,
9598
platform: process.platform,
@@ -146,6 +149,11 @@ export function keyExists(keyId: string = DEFAULT_KEY_ID): boolean {
146149
if (backend.type === 'file') {
147150
return fileBackend.keyExists(keyId);
148151
}
152+
// Use cached keys from status command to avoid an extra .exe spawn (significant on WSL2)
153+
if (cachedStatusKeys) {
154+
debug(`keyExists: using cached status keys for ${keyId}`);
155+
return cachedStatusKeys.includes(keyId);
156+
}
149157
const result = runNativeBinaryJson<{ exists: boolean }>(['key-exists', '--key-id', keyId]);
150158
return result.exists;
151159
}
@@ -204,7 +212,11 @@ export async function decryptValue(ciphertext: string, keyId: string = DEFAULT_K
204212
if (backend.biometricAvailable) {
205213
if (isWSL()) {
206214
debug('decryptValue: WSL2 biometric decrypt via --via-daemon');
207-
const result = runNativeBinaryJson<{ plaintext: string }>(['decrypt', '--key-id', keyId, '--data', ciphertext, '--via-daemon']);
215+
// Longer timeout: includes daemon spawn + Windows Hello biometric prompt
216+
const result = runNativeBinaryJson<{ plaintext: string }>(
217+
['decrypt', '--key-id', keyId, '--data', ciphertext, '--via-daemon'],
218+
{ timeout: 60_000 },
219+
);
208220
return result.plaintext;
209221
}
210222
debug('decryptValue: biometric decrypt via daemon client');

0 commit comments

Comments
 (0)