@openagents-org/agent-launcher 0.2.182 → 0.2.184

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/wsl.js CHANGED
@@ -27,20 +27,50 @@
27
27
  * The mirror direction needs no marker: a path ending in .exe/.cmd/.bat while
28
28
  * we are running inside a distro is unambiguously a Windows binary.
29
29
  *
30
- * Cost: resolution never spawns wsl.exe per binary. One probe per process
31
- * (30s TTL) reads the distro's login PATH, and every later lookup is a
32
- * filesystem check through the \\wsl.localhost UNC mount — which Windows serves
33
- * without starting a distro session.
30
+ * Cost: resolution never spawns wsl.exe per binary. One probe (5 min TTL) reads
31
+ * the distro's login PATH, and every later lookup is a filesystem check through
32
+ * the \\wsl.localhost UNC mount.
33
+ *
34
+ * Inside the desktop app's main process none of that may block: starting
35
+ * wsl.exe can mean booting the WSL VM, and the first touch of the UNC mount
36
+ * waits on it too — seconds during which the window is frozen. There every
37
+ * probe answers from its cache and refreshes in the background (probe-mode.js).
34
38
  */
35
39
 
36
40
  'use strict';
37
41
 
38
42
  const fs = require('fs');
39
- const { spawn: nodeSpawn, execFileSync } = require('child_process');
43
+ const { spawn: nodeSpawn, execFile, execFileSync } = require('child_process');
44
+ const { canBlock } = require('./probe-mode');
40
45
 
41
46
  const IS_WINDOWS = process.platform === 'win32';
42
47
  const PROBE_TIMEOUT_MS = 10000;
48
+ /** Which binaries were found. Short: an install inside the distro should show up. */
43
49
  const CACHE_TTL_MS = 30 * 1000;
50
+ /**
51
+ * The distro list, its UNC root and its login PATH describe the machine, not
52
+ * any one install, and each costs a wsl.exe start — re-asking every 30s kept
53
+ * the VM (and, in the desktop app, the window) busy for nothing.
54
+ */
55
+ const MACHINE_TTL_MS = 5 * 60 * 1000;
56
+
57
+ function _fresh(cache, ttl) {
58
+ return !!cache.at && Date.now() - cache.at < ttl;
59
+ }
60
+
61
+ /** execFile as a promise of stdout, or null on any failure. stdin is closed. */
62
+ function _execFileOut(file, argv, opts) {
63
+ return new Promise((resolve) => {
64
+ try {
65
+ const child = execFile(file, argv, { windowsHide: true, timeout: PROBE_TIMEOUT_MS, ...opts }, (err, stdout) => {
66
+ resolve(err ? null : stdout);
67
+ });
68
+ if (child.stdin) child.stdin.end();
69
+ } catch {
70
+ resolve(null);
71
+ }
72
+ });
73
+ }
44
74
 
45
75
  /** Marks a resolved path as living inside the distro: `wsl:/home/u/bin/claude`. */
46
76
  const WSL_PREFIX = 'wsl:';
@@ -95,6 +125,36 @@ function isRunningInWsl() {
95
125
  // ---------------------------------------------------------------------------
96
126
 
97
127
  let distrosCache = { value: null, at: 0 };
128
+ let distrosRefresh = null;
129
+
130
+ function _parseDistros(buf) {
131
+ return Buffer.from(buf)
132
+ .toString('utf16le')
133
+ .split(/\r?\n/)
134
+ .map((l) => l.replace(/\0/g, '').trim())
135
+ .filter(Boolean);
136
+ }
137
+
138
+ /** Record the distro list; a changed one invalidates everything derived from it. */
139
+ function _setDistros(names) {
140
+ const prev = distrosCache.value;
141
+ distrosCache = { value: names, at: Date.now() };
142
+ if (prev && prev.join('\0') !== names.join('\0')) {
143
+ uncRootCache = { value: null, at: 0 };
144
+ shellProbeCache = { value: null, at: 0 };
145
+ wslBinaryCache.clear();
146
+ }
147
+ }
148
+
149
+ function _refreshDistros() {
150
+ if (distrosCache.value && _fresh(distrosCache, MACHINE_TTL_MS)) return Promise.resolve();
151
+ if (!distrosRefresh) {
152
+ distrosRefresh = _execFileOut('wsl.exe', ['-l', '-q'], { encoding: 'buffer', maxBuffer: 1024 * 1024 })
153
+ .then((out) => _setDistros(out ? _parseDistros(out) : []))
154
+ .finally(() => { distrosRefresh = null; });
155
+ }
156
+ return distrosRefresh;
157
+ }
98
158
 
99
159
  /**
100
160
  * Registered, non-broken distro names, default first.
@@ -110,9 +170,13 @@ let distrosCache = { value: null, at: 0 };
110
170
  */
111
171
  function wslDistros() {
112
172
  if (!IS_WINDOWS) return [];
113
- if (distrosCache.value && Date.now() - distrosCache.at < CACHE_TTL_MS) {
173
+ if (distrosCache.value && _fresh(distrosCache, MACHINE_TTL_MS)) {
114
174
  return [...distrosCache.value];
115
175
  }
176
+ if (!canBlock()) {
177
+ void _refreshDistros();
178
+ return [...(distrosCache.value || [])];
179
+ }
116
180
  let names = [];
117
181
  try {
118
182
  const buf = execFileSync('wsl.exe', ['-l', '-q'], {
@@ -121,15 +185,11 @@ function wslDistros() {
121
185
  stdio: ['ignore', 'pipe', 'ignore'],
122
186
  maxBuffer: 1024 * 1024,
123
187
  });
124
- names = buf
125
- .toString('utf16le')
126
- .split(/\r?\n/)
127
- .map((l) => l.replace(/\0/g, '').trim())
128
- .filter(Boolean);
188
+ names = _parseDistros(buf);
129
189
  } catch {
130
190
  names = [];
131
191
  }
132
- distrosCache = { value: names, at: Date.now() };
192
+ _setDistros(names);
133
193
  return [...names];
134
194
  }
135
195
 
@@ -148,19 +208,51 @@ function defaultDistro() {
148
208
  // ---------------------------------------------------------------------------
149
209
 
150
210
  let uncRootCache = { value: null, at: 0 };
211
+ let uncRootRefresh = null;
212
+
213
+ const UNC_PREFIXES = ['\\\\wsl.localhost\\', '\\\\wsl$\\'];
214
+
215
+ function _refreshUncRoot() {
216
+ if (_fresh(uncRootCache, MACHINE_TTL_MS)) return Promise.resolve();
217
+ if (!uncRootRefresh) {
218
+ uncRootRefresh = (async () => {
219
+ await _refreshDistros();
220
+ const distro = (distrosCache.value || [])[0];
221
+ if (!distro) return;
222
+ let root = null;
223
+ for (const prefix of UNC_PREFIXES) {
224
+ try {
225
+ await fs.promises.access(prefix + distro);
226
+ root = prefix + distro;
227
+ break;
228
+ } catch {
229
+ /* unreachable share — try the next spelling */
230
+ }
231
+ }
232
+ uncRootCache = { value: root, at: Date.now() };
233
+ })()
234
+ .catch(() => {})
235
+ .finally(() => { uncRootRefresh = null; });
236
+ }
237
+ return uncRootRefresh;
238
+ }
151
239
 
152
240
  /**
153
241
  * The UNC prefix that maps the default distro's root into Windows, or null.
154
242
  * WSL2 serves \\wsl.localhost\<distro>; WSL1 and older builds only \\wsl$\.
155
243
  */
156
244
  function _uncRoot() {
157
- if (uncRootCache.at && Date.now() - uncRootCache.at < CACHE_TTL_MS) {
245
+ if (_fresh(uncRootCache, MACHINE_TTL_MS)) {
246
+ return uncRootCache.value;
247
+ }
248
+ if (!canBlock()) {
249
+ void _refreshUncRoot();
158
250
  return uncRootCache.value;
159
251
  }
160
252
  const distro = defaultDistro();
161
253
  if (!distro) return null;
162
254
  let root = null;
163
- for (const prefix of ['\\\\wsl.localhost\\', '\\\\wsl$\\']) {
255
+ for (const prefix of UNC_PREFIXES) {
164
256
  const candidate = prefix + distro;
165
257
  try {
166
258
  if (fs.existsSync(candidate)) { root = candidate; break; }
@@ -198,9 +290,70 @@ function toUncPath(linuxPath) {
198
290
 
199
291
  const PROBE_DELIM = '__OPENAGENTS_WSL__';
200
292
  let shellProbeCache = { value: null, at: 0 };
293
+ let shellProbeRefresh = null;
294
+
295
+ const SHELL_PROBE_SCRIPT =
296
+ 'echo ' + PROBE_DELIM + '; echo "$PATH"; echo "$HOME"; ' +
297
+ "wslpath -u 'C:\\' 2>/dev/null || echo /mnt/c/";
298
+ const SHELL_PROBE_ARGVS = [
299
+ ['-e', 'bash', '-ilc', SHELL_PROBE_SCRIPT],
300
+ ['-e', 'sh', '-lc', SHELL_PROBE_SCRIPT],
301
+ ];
302
+
303
+ function _emptyShellProbe() {
304
+ return { dirs: [], home: null, automount: '/mnt/' };
305
+ }
306
+
307
+ /** The probe's delimited output as {dirs, home, automount}, or null if unusable. */
308
+ function _parseShellProbe(raw) {
309
+ const idx = raw.indexOf(PROBE_DELIM);
310
+ if (idx === -1) return null;
311
+ const lines = raw.slice(idx + PROBE_DELIM.length).split(/\r?\n/).map((l) => l.trim());
312
+ const pathLine = lines[1];
313
+ const homeLine = lines[2];
314
+ const cRoot = lines[3];
315
+ if (!pathLine) return null;
316
+ const dirs = [];
317
+ for (const d of pathLine.split(':')) {
318
+ const dir = d.trim();
319
+ // /mnt/<drive> entries are the Windows PATH bounced back by interop.
320
+ // They hold Windows binaries, never the in-distro install we are after.
321
+ if (dir && dir.startsWith('/') && !/^\/mnt\/[a-z]\//i.test(dir) && !dirs.includes(dir)) {
322
+ dirs.push(dir);
323
+ }
324
+ }
325
+ // `wslpath -u 'C:\'` answers /mnt/c/ by default, but /etc/wsl.conf can
326
+ // move the automount root — derive it rather than hardcoding /mnt.
327
+ let automount = '/mnt/';
328
+ const m = /^(\/.*\/)c\/?$/i.exec(cRoot || '');
329
+ if (m) automount = m[1];
330
+ return { dirs, home: homeLine && homeLine.startsWith('/') ? homeLine : null, automount };
331
+ }
332
+
333
+ function _refreshShellProbe() {
334
+ if (_fresh(shellProbeCache, MACHINE_TTL_MS)) return Promise.resolve();
335
+ if (!shellProbeRefresh) {
336
+ shellProbeRefresh = (async () => {
337
+ await _refreshDistros();
338
+ if (!(distrosCache.value || []).length) return;
339
+ let out = null;
340
+ for (const argv of SHELL_PROBE_ARGVS) {
341
+ const raw = await _execFileOut('wsl.exe', argv, { encoding: 'utf-8', maxBuffer: 4 * 1024 * 1024 });
342
+ out = raw == null ? null : _parseShellProbe(raw);
343
+ if (out) break;
344
+ }
345
+ shellProbeCache = { value: out || _emptyShellProbe(), at: Date.now() };
346
+ // Lookups answered before the PATH was known searched too few dirs.
347
+ if (out) wslBinaryCache.clear();
348
+ })()
349
+ .catch(() => {})
350
+ .finally(() => { shellProbeRefresh = null; });
351
+ }
352
+ return shellProbeRefresh;
353
+ }
201
354
 
202
355
  /**
203
- * One wsl.exe call per process (30s TTL) that answers everything later lookups
356
+ * One wsl.exe call (5 min TTL) that answers everything later lookups
204
357
  * need: the login PATH, $HOME, and the real automount root.
205
358
  *
206
359
  * `bash -ilc` — INTERACTIVE as well as login — because the version managers
@@ -211,18 +364,18 @@ let shellProbeCache = { value: null, at: 0 };
211
364
  * stderr is dropped: an interactive shell with no tty warns about job control.
212
365
  */
213
366
  function _shellProbe() {
214
- const empty = { dirs: [], home: null, automount: '/mnt/' };
215
- if (shellProbeCache.at && Date.now() - shellProbeCache.at < CACHE_TTL_MS) {
367
+ const empty = _emptyShellProbe();
368
+ if (_fresh(shellProbeCache, MACHINE_TTL_MS)) {
369
+ return shellProbeCache.value || empty;
370
+ }
371
+ if (!canBlock()) {
372
+ void _refreshShellProbe();
216
373
  return shellProbeCache.value || empty;
217
374
  }
218
375
  if (!isWslAvailable()) return empty;
219
376
 
220
- const script =
221
- 'echo ' + PROBE_DELIM + '; echo "$PATH"; echo "$HOME"; ' +
222
- "wslpath -u 'C:\\' 2>/dev/null || echo /mnt/c/";
223
-
224
377
  let out = null;
225
- for (const argv of [['-e', 'bash', '-ilc', script], ['-e', 'sh', '-lc', script]]) {
378
+ for (const argv of SHELL_PROBE_ARGVS) {
226
379
  try {
227
380
  const raw = execFileSync('wsl.exe', argv, {
228
381
  encoding: 'utf-8',
@@ -231,29 +384,8 @@ function _shellProbe() {
231
384
  stdio: ['ignore', 'pipe', 'ignore'],
232
385
  maxBuffer: 4 * 1024 * 1024,
233
386
  });
234
- const idx = raw.indexOf(PROBE_DELIM);
235
- if (idx === -1) continue;
236
- const lines = raw.slice(idx + PROBE_DELIM.length).split(/\r?\n/).map((l) => l.trim());
237
- const pathLine = lines[1];
238
- const homeLine = lines[2];
239
- const cRoot = lines[3];
240
- if (!pathLine) continue;
241
- const dirs = [];
242
- for (const d of pathLine.split(':')) {
243
- const dir = d.trim();
244
- // /mnt/<drive> entries are the Windows PATH bounced back by interop.
245
- // They hold Windows binaries, never the in-distro install we are after.
246
- if (dir && dir.startsWith('/') && !/^\/mnt\/[a-z]\//i.test(dir) && !dirs.includes(dir)) {
247
- dirs.push(dir);
248
- }
249
- }
250
- // `wslpath -u 'C:\'` answers /mnt/c/ by default, but /etc/wsl.conf can
251
- // move the automount root — derive it rather than hardcoding /mnt.
252
- let automount = '/mnt/';
253
- const m = /^(\/.*\/)c\/?$/i.exec(cRoot || '');
254
- if (m) automount = m[1];
255
- out = { dirs, home: homeLine && homeLine.startsWith('/') ? homeLine : null, automount };
256
- break;
387
+ out = _parseShellProbe(raw);
388
+ if (out) break;
257
389
  } catch {
258
390
  /* try the next shell */
259
391
  }
@@ -290,6 +422,42 @@ function wslBinDirs() {
290
422
  }
291
423
 
292
424
  const wslBinaryCache = new Map();
425
+ const wslBinaryRefresh = new Map();
426
+
427
+ /** resolveWslBinary's search, off the calling thread. See probe-mode.js. */
428
+ function _refreshWslBinary(key, list) {
429
+ if (wslBinaryRefresh.has(key)) return wslBinaryRefresh.get(key);
430
+ const job = (async () => {
431
+ await _refreshDistros();
432
+ let value = null;
433
+ if ((distrosCache.value || []).length) {
434
+ await Promise.all([_refreshUncRoot(), _refreshShellProbe()]);
435
+ if (uncRootCache.value) {
436
+ search:
437
+ for (const dir of wslBinDirs()) {
438
+ for (const name of list) {
439
+ const linuxPath = dir.replace(/\/+$/, '') + '/' + name;
440
+ const unc = toUncPath(linuxPath);
441
+ if (!unc) continue;
442
+ try {
443
+ if ((await fs.promises.stat(unc)).isFile()) {
444
+ value = WSL_PREFIX + linuxPath;
445
+ break search;
446
+ }
447
+ } catch {
448
+ /* unreadable path — keep looking */
449
+ }
450
+ }
451
+ }
452
+ }
453
+ }
454
+ wslBinaryCache.set(key, { value, at: Date.now() });
455
+ })()
456
+ .catch(() => {})
457
+ .finally(() => wslBinaryRefresh.delete(key));
458
+ wslBinaryRefresh.set(key, job);
459
+ return job;
460
+ }
293
461
 
294
462
  /**
295
463
  * Resolve an agent binary inside the default distro, or null.
@@ -309,6 +477,10 @@ function resolveWslBinary(names) {
309
477
  const key = list.join('\0');
310
478
  const cached = wslBinaryCache.get(key);
311
479
  if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.value;
480
+ if (!canBlock()) {
481
+ void _refreshWslBinary(key, list);
482
+ return cached ? cached.value : null;
483
+ }
312
484
 
313
485
  let value = null;
314
486
  if (isWslAvailable() && _uncRoot()) {