vouchington-tooling 0.0.8 → 0.0.9

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.
@@ -0,0 +1,4 @@
1
+ export type NativeFamily = 'elf' | 'macho' | 'pe';
2
+ export declare function nativeFamilyFromMagic(buffer: Buffer): NativeFamily | undefined;
3
+ export declare function expectedNativeFamily(platform?: NodeJS.Platform): NativeFamily | undefined;
4
+ export declare function nativeBinariesMatchRuntime(root?: string, platform?: NodeJS.Platform): Promise<boolean>;
@@ -0,0 +1,84 @@
1
+ import { glob, open, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const ELF = Buffer.from([0x7f, 0x45, 0x4c, 0x46]);
4
+ const PE = Buffer.from([0x4d, 0x5a]);
5
+ const MACHO = [
6
+ Buffer.from([0xcf, 0xfa, 0xed, 0xfe]),
7
+ Buffer.from([0xfe, 0xed, 0xfa, 0xcf]),
8
+ Buffer.from([0xce, 0xfa, 0xed, 0xfe]),
9
+ Buffer.from([0xfe, 0xed, 0xfa, 0xce]),
10
+ Buffer.from([0xca, 0xfe, 0xba, 0xbe]),
11
+ Buffer.from([0xbe, 0xba, 0xfe, 0xca]),
12
+ ];
13
+ function startsWith(buffer, magic) {
14
+ return buffer.length >= magic.length && buffer.subarray(0, magic.length).equals(magic);
15
+ }
16
+ export function nativeFamilyFromMagic(buffer) {
17
+ if (startsWith(buffer, ELF))
18
+ return 'elf';
19
+ if (MACHO.some((magic) => startsWith(buffer, magic)))
20
+ return 'macho';
21
+ if (startsWith(buffer, PE))
22
+ return 'pe';
23
+ return undefined;
24
+ }
25
+ export function expectedNativeFamily(platform = process.platform) {
26
+ if (platform === 'linux')
27
+ return 'elf';
28
+ if (platform === 'darwin')
29
+ return 'macho';
30
+ if (platform === 'win32')
31
+ return 'pe';
32
+ return undefined;
33
+ }
34
+ async function readMagic(pathname) {
35
+ let handle;
36
+ try {
37
+ handle = await open(pathname, 'r');
38
+ const buffer = Buffer.alloc(4);
39
+ const { bytesRead } = await handle.read(buffer, 0, 4, 0);
40
+ return bytesRead === 0 ? undefined : buffer.subarray(0, bytesRead);
41
+ }
42
+ catch (error) {
43
+ if (error.code === 'ENOENT')
44
+ return undefined;
45
+ throw error;
46
+ }
47
+ finally {
48
+ await handle?.close();
49
+ }
50
+ }
51
+ async function searchRoot(nodeModules) {
52
+ const pnpmStore = path.join(nodeModules, '.pnpm');
53
+ try {
54
+ const info = await stat(pnpmStore);
55
+ return info.isDirectory() ? pnpmStore : nodeModules;
56
+ }
57
+ catch {
58
+ return nodeModules;
59
+ }
60
+ }
61
+ export async function nativeBinariesMatchRuntime(root = process.cwd(), platform = process.platform) {
62
+ const expected = expectedNativeFamily(platform);
63
+ if (expected === undefined)
64
+ return true;
65
+ const nodeModules = path.join(root, 'node_modules');
66
+ try {
67
+ const info = await stat(nodeModules);
68
+ if (!info.isDirectory())
69
+ return true;
70
+ }
71
+ catch {
72
+ return true;
73
+ }
74
+ const cwd = await searchRoot(nodeModules);
75
+ for await (const relative of glob('**/*.{node,bin}', { cwd })) {
76
+ const magic = await readMagic(path.join(cwd, relative));
77
+ if (magic === undefined)
78
+ continue;
79
+ const family = nativeFamilyFromMagic(magic);
80
+ if (family !== undefined && family !== expected)
81
+ return false;
82
+ }
83
+ return true;
84
+ }
@@ -1,5 +1,6 @@
1
1
  import { scheduler } from 'node:timers/promises';
2
2
  import { persistentDependencyTreeIsCold, persistentMetadataFingerprint, persistentMetadataMatches, writePersistentMetadataStamp, } from './metadata.mjs';
3
+ import { nativeBinariesMatchRuntime } from './native-health.mjs';
3
4
  import { runPnpm } from './exec.mjs';
4
5
  import { INSTALL_TERMINATION_FAILED } from './process.mjs';
5
6
  import { formatReleaseAgeFailure, isReleaseAgeViolation } from './release-age.mjs';
@@ -47,11 +48,15 @@ async function persistent(options) {
47
48
  const runCapture = (args) => runPnpm(args, options, true);
48
49
  const fingerprint = await persistentMetadataFingerprint(runCapture, options.installScripts);
49
50
  const stamped = await persistentMetadataMatches(fingerprint);
51
+ const nativesMatch = await nativeBinariesMatchRuntime();
52
+ const provenanceOk = stamped && nativesMatch;
50
53
  // An absent tree has nothing to repair, so one ordinary install below matches the
51
54
  // reconciled end state. Check first: an install would otherwise make the tree non-cold.
52
- const cold = !stamped && (await persistentDependencyTreeIsCold());
53
- if (!stamped && !cold) {
54
- console.warn('persistent dependency metadata provenance is missing or changed; reconciling');
55
+ const cold = !provenanceOk && (await persistentDependencyTreeIsCold());
56
+ if (!provenanceOk && !cold) {
57
+ console.warn(stamped && !nativesMatch
58
+ ? 'persistent optional native binaries do not match this runtime; reconciling'
59
+ : 'persistent dependency metadata provenance is missing or changed; reconciling');
55
60
  await reconcileOrFail(options, runCapture);
56
61
  await writePersistentMetadataStamp(fingerprint);
57
62
  return 'persistent metadata reconciled';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vouchington-tooling",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Vouchington CLI and extractable tooling libraries.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-tooling/tree/main/packages/vouchington-tooling#readme",
6
6
  "bugs": {
@@ -211,6 +211,7 @@ SCRIPT_NAME = "allocate-browser-safe-ports.py"
211
211
  HOLD_POLL_SECONDS = 0.05
212
212
  HOLD_READY_TIMEOUT_SECONDS = 2.0
213
213
  CONTROL_WAIT_SECONDS = 2.0
214
+ OWNER_PROBE_SECONDS = 0.4
214
215
 
215
216
 
216
217
  def resolve_workspace(workspace: str) -> str:
@@ -560,6 +561,90 @@ def port_is_bindable(port: int) -> bool:
560
561
  probe.close()
561
562
 
562
563
 
564
+ def run_owner_probe(command: list[str]) -> str:
565
+ try:
566
+ result = subprocess.run(
567
+ command,
568
+ capture_output=True,
569
+ text=True,
570
+ check=False,
571
+ timeout=OWNER_PROBE_SECONDS,
572
+ )
573
+ except (OSError, subprocess.TimeoutExpired):
574
+ return ""
575
+ return result.stdout
576
+
577
+
578
+ def compact_probe_text(output: str) -> str:
579
+ lines = [line.strip() for line in output.splitlines() if line.strip()]
580
+ return " | ".join(lines)[:200]
581
+
582
+
583
+ def pids_holding_port(port: int) -> list[int]:
584
+ pids: list[int] = []
585
+ seen: set[int] = set()
586
+ for raw in run_owner_probe(["lsof", "-nP", "-t", f"-iTCP:{port}"]).split():
587
+ if not raw.isdigit():
588
+ continue
589
+ pid = int(raw)
590
+ if pid in seen:
591
+ continue
592
+ seen.add(pid)
593
+ pids.append(pid)
594
+ if pids:
595
+ return pids
596
+ for token in run_owner_probe(["ss", "-ntp", f"( sport = :{port} )"]).replace(",", " ").split():
597
+ if not token.startswith("pid="):
598
+ continue
599
+ raw_pid = token[4:].split(")")[0]
600
+ if not raw_pid.isdigit():
601
+ continue
602
+ pid = int(raw_pid)
603
+ if pid in seen:
604
+ continue
605
+ seen.add(pid)
606
+ pids.append(pid)
607
+ return pids
608
+
609
+
610
+ def sibling_allocator_owner(port: int, excluded_pid: int | None) -> tuple[int, str] | None:
611
+ for owner_pid in pids_holding_port(port):
612
+ if excluded_pid is not None and owner_pid == excluded_pid:
613
+ continue
614
+ command = process_command_line(owner_pid)
615
+ if command is None or SCRIPT_NAME not in command:
616
+ continue
617
+ return owner_pid, command
618
+ return None
619
+
620
+
621
+ def describe_port_owner(port: int) -> str:
622
+ pids = pids_holding_port(port)
623
+ if pids:
624
+ details: list[str] = []
625
+ for owner_pid in pids:
626
+ command = process_command_line(owner_pid) or "unknown"
627
+ details.append(f"pid {owner_pid} {command}")
628
+ return compact_probe_text("; ".join(details))
629
+ lsof_text = compact_probe_text(run_owner_probe(["lsof", "-nP", f"-iTCP:{port}"]))
630
+ if lsof_text:
631
+ return f"lsof {lsof_text}"
632
+ ss_text = compact_probe_text(run_owner_probe(["ss", "-ntp", f"( sport = :{port} )"]))
633
+ if ss_text:
634
+ lowered = ss_text.lower()
635
+ if "time-wait" in lowered or "time_wait" in lowered:
636
+ return f"TIME_WAIT {ss_text}"
637
+ return f"ss {ss_text}"
638
+ return "no owner"
639
+
640
+
641
+ def report_sibling_owner(port: int, owner_pid: int, command: str) -> None:
642
+ print(
643
+ f"port {port} still held by pid {owner_pid} ({command}); leaving owner in place",
644
+ file=sys.stderr,
645
+ )
646
+
647
+
563
648
  def wait_until_port_bindable(port: int) -> None:
564
649
  deadline = time.monotonic() + CONTROL_WAIT_SECONDS
565
650
  while time.monotonic() < deadline:
@@ -569,6 +654,29 @@ def wait_until_port_bindable(port: int) -> None:
569
654
  raise RuntimeError(f"port {port} did not become bindable")
570
655
 
571
656
 
657
+ def wait_until_port_bindable_or_sibling(port: int, excluded_pid: int | None) -> None:
658
+ deadline = time.monotonic() + CONTROL_WAIT_SECONDS
659
+ probed = False
660
+ while time.monotonic() < deadline:
661
+ if port_is_bindable(port):
662
+ return
663
+ if not probed:
664
+ sibling = sibling_allocator_owner(port, excluded_pid)
665
+ if sibling is not None:
666
+ report_sibling_owner(port, sibling[0], sibling[1])
667
+ return
668
+ probed = True
669
+ time.sleep(HOLD_POLL_SECONDS)
670
+ sibling = sibling_allocator_owner(port, excluded_pid)
671
+ if sibling is not None:
672
+ report_sibling_owner(port, sibling[0], sibling[1])
673
+ return
674
+ occupancy = describe_port_owner(port)
675
+ raise RuntimeError(
676
+ f"port {port} did not become bindable after {CONTROL_WAIT_SECONDS}s ({occupancy})"
677
+ )
678
+
679
+
572
680
  def request_release(hold_dir: Path, port: int) -> None:
573
681
  hold_dir = hold_dir.resolve()
574
682
  release_path = hold_dir / "release" / str(port)
@@ -592,9 +700,11 @@ def request_stop(hold_dir: Path, workspace: str) -> None:
592
700
  (hold_dir / "stop").write_text("1")
593
701
  pid_path = hold_dir / "pid"
594
702
  pid = read_pid(pid_path) if pid_path.is_file() else None
703
+ stopped_live_holder = False
595
704
  if pid is not None and pid_is_alive(pid) and is_our_holder(pid, workspace):
596
705
  try:
597
706
  os.kill(pid, signal.SIGTERM)
707
+ stopped_live_holder = True
598
708
  except ProcessLookupError:
599
709
  pass
600
710
  deadline = time.monotonic() + CONTROL_WAIT_SECONDS
@@ -607,8 +717,10 @@ def request_stop(hold_dir: Path, workspace: str) -> None:
607
717
  if successor is not None and successor != pid:
608
718
  # A replacement holder for this workspace already reaped us and took the ports.
609
719
  return
720
+ if not stopped_live_holder:
721
+ return
610
722
  for port in remaining:
611
- wait_until_port_bindable(port)
723
+ wait_until_port_bindable_or_sibling(port, pid)
612
724
 
613
725
 
614
726
  def check_holder(hold_dir: Path, workspace: str) -> None: