@farmslot/adapter-web 0.3.0 → 0.3.1
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/CHANGELOG.md +5 -0
- package/dist/browser-resolver.d.cts +1 -0
- package/package.json +1 -1
- package/src/browser-resolver.cjs +95 -21
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,11 @@ All notable changes to `@farmslot/adapter-web` are tracked here.
|
|
|
6
6
|
|
|
7
7
|
- Active-development baseline; add user-facing changes here before release or package publication.
|
|
8
8
|
|
|
9
|
+
## 0.3.1 - 2026-10-06
|
|
10
|
+
|
|
11
|
+
- Browser probes no longer leave Chrome for Testing running when the resolver is stopped mid-probe (a hook timeout, a signal, or the resolver killed outright): the probe browser and its temporary profile are removed.
|
|
12
|
+
- A resolver killed outright cannot clean up; the next probe by the same user stops its browser and removes its profile. `browser-resolver` exports that step as `reapOrphanedProbes(tmp?)`.
|
|
13
|
+
|
|
9
14
|
## 0.3.0 - 2026-10-05
|
|
10
15
|
|
|
11
16
|
- Add `dapp`, the web3 layer for a dapp under test, moved from `@deeeed/metamask-harness`'s Web Terminal adapter: the strict EIP-1193 test wallet (`createStrictWallet`), the page script that wraps the app's provider and logs its wallet requests or injects a provider the host answers (`pageScriptSource`, `pageReadyExpression`), the host's side of its bindings (`createWalletRequestBinding`: frame and document attribution, `outside-app-frame` and `unattributed` records, refusals) and the wallet request log (`windowSinceCursor`, `resetWindow`, `evaluateSignatureLog`, `awaitSignatureLog`, `writeLogArtifact`). Product policy is passed in: the typed data to refuse (`refuseTypedData`), the injected wallet's EIP-6963 identity and the log entries and typed-data classes a product forbids. Page bindings are `__farmslotWallet{Log,Request,Resolve}` and the page marker `__farmslotDapp`.
|
|
@@ -64,6 +64,7 @@ export function probeLaunch(executable: any, { launchMethod, display, settleMs,
|
|
|
64
64
|
durationMs: number;
|
|
65
65
|
}>;
|
|
66
66
|
export function readProbeCache(cachePath: any): any;
|
|
67
|
+
export function reapOrphanedProbes(tmp?: string): void;
|
|
67
68
|
export function renderCheck(port: any): Promise<{
|
|
68
69
|
ok: boolean;
|
|
69
70
|
reason?: undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farmslot/adapter-web",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Web platform adapter for Farmslot recipes: resolve and launch an isolated Chromium, own its CDP port, load unpacked extensions, and keep macOS focus.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"exports": {
|
package/src/browser-resolver.cjs
CHANGED
|
@@ -529,8 +529,12 @@ async function withProbeLock(
|
|
|
529
529
|
}
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
// Probes in flight
|
|
533
|
-
|
|
532
|
+
// Probes in flight (profile -> spawned browser pid, or null for LaunchServices),
|
|
533
|
+
// so a watchdog, a signal or process exit can stop their browsers first.
|
|
534
|
+
const activeProbes = new Map();
|
|
535
|
+
const PROBE_PROFILE_PREFIX = 'farmslot-browser-probe-';
|
|
536
|
+
const PROBE_PROFILE_OWNER = new RegExp(`^${PROBE_PROFILE_PREFIX}(\\d+)-`, 'u');
|
|
537
|
+
const PROBE_SIGNALS = ['SIGTERM', 'SIGINT', 'SIGHUP', 'SIGQUIT'];
|
|
534
538
|
|
|
535
539
|
function profilePids(profile) {
|
|
536
540
|
try {
|
|
@@ -556,6 +560,72 @@ function killProfile(profile) {
|
|
|
556
560
|
}
|
|
557
561
|
}
|
|
558
562
|
|
|
563
|
+
// Synchronous, so it also runs from 'exit' and signal handlers. The browser is
|
|
564
|
+
// outside the caller's process group (LaunchServices parents it; a spawned
|
|
565
|
+
// probe is detached), so a group kill of the caller never reaches it.
|
|
566
|
+
function stopProbe(profile) {
|
|
567
|
+
const pid = activeProbes.get(profile);
|
|
568
|
+
try {
|
|
569
|
+
if (pid) process.kill(-pid, 'SIGKILL');
|
|
570
|
+
} catch {
|
|
571
|
+
// Already exited.
|
|
572
|
+
}
|
|
573
|
+
killProfile(profile);
|
|
574
|
+
fs.rmSync(profile, { recursive: true, force: true });
|
|
575
|
+
activeProbes.delete(profile);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function stopActiveProbes() {
|
|
579
|
+
for (const profile of [...activeProbes.keys()]) stopProbe(profile);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// A hook timeout sends SIGTERM to the caller's process group. Stop the probe
|
|
583
|
+
// browsers, then let the signal end the process as it would have; a host with
|
|
584
|
+
// its own handler for that signal (a `once` handler is already gone by now, so
|
|
585
|
+
// it is noted at install) keeps control of the exit.
|
|
586
|
+
function onProbeSignal(signal) {
|
|
587
|
+
stopActiveProbes();
|
|
588
|
+
const hostHandles = hostSignals.has(signal) || process.listenerCount(signal) > 1;
|
|
589
|
+
setProbeExitHandlers(false);
|
|
590
|
+
if (!hostHandles) process.kill(process.pid, signal);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let probeExitHandlers = false;
|
|
594
|
+
let hostSignals = new Set();
|
|
595
|
+
|
|
596
|
+
function setProbeExitHandlers(on) {
|
|
597
|
+
if (probeExitHandlers === on) return;
|
|
598
|
+
probeExitHandlers = on;
|
|
599
|
+
if (on) hostSignals = new Set(PROBE_SIGNALS.filter((signal) => process.listenerCount(signal)));
|
|
600
|
+
for (const signal of PROBE_SIGNALS) {
|
|
601
|
+
if (on) process.on(signal, onProbeSignal);
|
|
602
|
+
else process.off(signal, onProbeSignal);
|
|
603
|
+
}
|
|
604
|
+
if (on) process.on('exit', stopActiveProbes);
|
|
605
|
+
else process.off('exit', stopActiveProbes);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function trackProbe(profile, pid = null) {
|
|
609
|
+
activeProbes.set(profile, pid);
|
|
610
|
+
setProbeExitHandlers(true);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// A resolver killed outright (SIGKILL) cannot clean up. Its profile name
|
|
614
|
+
// carries its pid, so the next probe by the same user stops that browser and
|
|
615
|
+
// removes the profile. Another user's profiles (a shared /tmp) are left alone.
|
|
616
|
+
function reapOrphanedProbes(tmp = os.tmpdir()) {
|
|
617
|
+
for (const name of fs.readdirSync(tmp)) {
|
|
618
|
+
const owner = PROBE_PROFILE_OWNER.exec(name);
|
|
619
|
+
if (!owner || Number(owner[1]) === process.pid || pidAlive(Number(owner[1]))) continue;
|
|
620
|
+
const profile = path.join(tmp, name);
|
|
621
|
+
// Another resolver's reaper may have removed it already.
|
|
622
|
+
const stat = fs.lstatSync(profile, { throwIfNoEntry: false });
|
|
623
|
+
if (!stat || stat.uid !== process.getuid()) continue;
|
|
624
|
+
killProfile(profile);
|
|
625
|
+
fs.rmSync(profile, { recursive: true, force: true });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
559
629
|
// `--version` succeeds even for a build that crashes at startup, so a browser
|
|
560
630
|
// is accepted only when a start answers on DevTools, paints a frame, and
|
|
561
631
|
// survives a settle without dying. The Chrome for Testing crash on macOS 26+
|
|
@@ -574,8 +644,9 @@ async function probeLaunch(
|
|
|
574
644
|
} = {},
|
|
575
645
|
) {
|
|
576
646
|
const method = effectiveLaunchMethod(executable, launchMethod);
|
|
577
|
-
|
|
578
|
-
|
|
647
|
+
reapOrphanedProbes();
|
|
648
|
+
const profile = fs.mkdtempSync(path.join(os.tmpdir(), `${PROBE_PROFILE_PREFIX}${process.pid}-`));
|
|
649
|
+
trackProbe(profile);
|
|
579
650
|
const startedAt = Date.now();
|
|
580
651
|
const verdict = (ok, reason, transient = false) => ({
|
|
581
652
|
ok,
|
|
@@ -589,6 +660,8 @@ async function probeLaunch(
|
|
|
589
660
|
let exited = null;
|
|
590
661
|
try {
|
|
591
662
|
const port = await freePort();
|
|
663
|
+
// A host that survives a signal resumes here after stopProbe ran.
|
|
664
|
+
if (!activeProbes.has(profile)) return verdict(false, 'probe stopped by its caller', true);
|
|
592
665
|
// A headful probe starts like the slot browsers (the launchers and the
|
|
593
666
|
// library autolaunch): in the background with no startup window (Chrome
|
|
594
667
|
// activates itself when it opens one); renderCheck opens a background
|
|
@@ -615,6 +688,7 @@ async function probeLaunch(
|
|
|
615
688
|
});
|
|
616
689
|
} else {
|
|
617
690
|
child = spawn(executable, args, { detached: true, stdio: 'ignore' });
|
|
691
|
+
if (child.pid) trackProbe(profile, child.pid);
|
|
618
692
|
child.on('exit', (code, signal) => {
|
|
619
693
|
exited = signal ?? `code ${code}`;
|
|
620
694
|
});
|
|
@@ -624,12 +698,16 @@ async function probeLaunch(
|
|
|
624
698
|
}
|
|
625
699
|
const alive = () =>
|
|
626
700
|
method === LAUNCH_SERVICES ? profilePids(profile).length > 0 : exited === null;
|
|
701
|
+
// stopProbe removes the profile: the caller stopped the browser, so its
|
|
702
|
+
// exit says nothing about the binary.
|
|
627
703
|
const died = (when) =>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
704
|
+
activeProbes.has(profile)
|
|
705
|
+
? verdict(
|
|
706
|
+
false,
|
|
707
|
+
`exited ${when}${exited ? ` (${exited})` : ''}`,
|
|
708
|
+
String(exited).startsWith('spawn failed'),
|
|
709
|
+
)
|
|
710
|
+
: verdict(false, 'probe stopped by its caller', true);
|
|
633
711
|
let up = false;
|
|
634
712
|
while (!up && Date.now() - startedAt < startupMs) {
|
|
635
713
|
if (exited) return died('during startup');
|
|
@@ -655,7 +733,9 @@ async function probeLaunch(
|
|
|
655
733
|
if (!alive()) return died('after DevTools started');
|
|
656
734
|
await sleep(250);
|
|
657
735
|
}
|
|
658
|
-
|
|
736
|
+
const answers = await devtoolsAnswers(port);
|
|
737
|
+
if (!activeProbes.has(profile)) return verdict(false, 'probe stopped by its caller', true);
|
|
738
|
+
if (answers) return verdict(true, 'started and rendered');
|
|
659
739
|
// Only a proven exit is a property of the binary.
|
|
660
740
|
return alive()
|
|
661
741
|
? verdict(false, 'DevTools stopped answering after the settle', true)
|
|
@@ -663,15 +743,11 @@ async function probeLaunch(
|
|
|
663
743
|
} catch (error) {
|
|
664
744
|
return verdict(false, `launch failed: ${error.message}`, true);
|
|
665
745
|
} finally {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
// Already exited.
|
|
670
|
-
}
|
|
671
|
-
killProfile(profile);
|
|
746
|
+
stopProbe(profile);
|
|
747
|
+
if (activeProbes.size === 0) setProbeExitHandlers(false);
|
|
748
|
+
// A dying browser can still write into its profile; remove it once more.
|
|
672
749
|
await sleep(300);
|
|
673
750
|
fs.rmSync(profile, { recursive: true, force: true });
|
|
674
|
-
activeProbeProfiles.delete(profile);
|
|
675
751
|
}
|
|
676
752
|
}
|
|
677
753
|
|
|
@@ -1093,10 +1169,7 @@ async function main(argv) {
|
|
|
1093
1169
|
}
|
|
1094
1170
|
const args = parseCliArgs(rest);
|
|
1095
1171
|
const watchdog = setTimeout(() => {
|
|
1096
|
-
|
|
1097
|
-
killProfile(profile);
|
|
1098
|
-
fs.rmSync(profile, { recursive: true, force: true });
|
|
1099
|
-
}
|
|
1172
|
+
stopActiveProbes();
|
|
1100
1173
|
process.stderr.write(
|
|
1101
1174
|
`browser-resolver ${command} did not finish within ${CLI_DEADLINE_MS}ms.\n`,
|
|
1102
1175
|
);
|
|
@@ -1200,6 +1273,7 @@ module.exports = {
|
|
|
1200
1273
|
probeCacheKey,
|
|
1201
1274
|
probeLaunch,
|
|
1202
1275
|
readProbeCache,
|
|
1276
|
+
reapOrphanedProbes,
|
|
1203
1277
|
renderCheck,
|
|
1204
1278
|
resolveBrowser,
|
|
1205
1279
|
withProbeLock,
|