@buda-ai/connector 0.1.23 → 0.1.25

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.
Files changed (2) hide show
  1. package/dist/cli.js +100 -18
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -25,8 +25,8 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
25
25
  } catch (error) {
26
26
  if (!isAlreadyExists(error)) throw error;
27
27
  if (await shouldRecoverLock(lockPath, staleMs)) {
28
- await unlink(lockPath).catch(() => void 0);
29
- continue;
28
+ const recovered = await unlink(lockPath).then(() => true).catch(() => false);
29
+ if (recovered) continue;
30
30
  }
31
31
  if (Date.now() >= deadline) {
32
32
  throw new Error(`Timed out waiting for exclusive access to ${targetPath}`);
@@ -35,17 +35,23 @@ async function acquireExclusiveFileLock(targetPath, options = {}) {
35
35
  continue;
36
36
  }
37
37
  const ownerToken = randomUUID();
38
- await handle.writeFile(
39
- `${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
38
+ try {
39
+ await handle.writeFile(
40
+ `${JSON.stringify({ pid: process.pid, ownerToken, createdAt: Date.now() })}
40
41
  `
41
- );
42
- await handle.sync();
42
+ );
43
+ await handle.sync();
44
+ } catch (error) {
45
+ await handle.close().catch(() => void 0);
46
+ await unlink(lockPath).catch(() => void 0);
47
+ throw error;
48
+ }
49
+ await handle.close();
43
50
  let released = false;
44
51
  return {
45
52
  release: async () => {
46
53
  if (released) return;
47
54
  released = true;
48
- await handle.close();
49
55
  const currentOwner = await readFile(lockPath, "utf8").then((raw) => JSON.parse(raw).ownerToken).catch(() => void 0);
50
56
  if (currentOwner === ownerToken) await unlink(lockPath).catch(() => void 0);
51
57
  }
@@ -517,7 +523,7 @@ import path2 from "path";
517
523
  // package.json
518
524
  var package_default = {
519
525
  name: "@buda-ai/connector",
520
- version: "0.1.23",
526
+ version: "0.1.25",
521
527
  private: false,
522
528
  type: "module",
523
529
  publishConfig: {
@@ -2595,6 +2601,7 @@ async function ensureStableConnectorRuntime() {
2595
2601
  // src/daemon-client.ts
2596
2602
  var START_TIMEOUT_MS = 45e3;
2597
2603
  var BOOTSTRAP_TIMEOUT_MS = 5 * 6e4;
2604
+ var LOCK_ONLY_RECOVERY_MAX_AGE_MS = 5 * 6e4;
2598
2605
  var POLL_INTERVAL_MS = 250;
2599
2606
  var wait2 = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
2600
2607
  var getConfigRoot = () => process.env.BUDA_CONNECTOR_CONFIG_ROOT;
@@ -2662,8 +2669,13 @@ async function spawnDetachedDaemonProcess(command, args, options, dependencies =
2662
2669
  });
2663
2670
  try {
2664
2671
  await waitForChildSpawn(child);
2672
+ if (!child.pid) {
2673
+ throw Object.assign(new Error("spawned Connector daemon did not expose a PID"), {
2674
+ code: "NO_PID"
2675
+ });
2676
+ }
2665
2677
  child.unref();
2666
- return strategy.name;
2678
+ return { pid: child.pid, strategy: strategy.name };
2667
2679
  } catch (error) {
2668
2680
  const code = getErrorCode(error);
2669
2681
  const shouldRetry = platform4 === "win32" && index === 0 && code === "EINVAL";
@@ -2682,14 +2694,18 @@ async function spawnDetachedDaemonProcess(command, args, options, dependencies =
2682
2694
  }
2683
2695
  throw new Error("Connector daemon process creation failed without a spawn attempt");
2684
2696
  }
2697
+ var readReadyDaemon = async (launchConfig) => {
2698
+ const config = await readDeviceConfig(launchConfig, getConfigRoot());
2699
+ if (!config) return null;
2700
+ const status = await readLiveStatus(config);
2701
+ return status ? { config, status } : null;
2702
+ };
2685
2703
  var waitForDaemon = async (launchConfig, deadline) => {
2686
2704
  let lastConfig = null;
2687
2705
  while (Date.now() < deadline) {
2706
+ const ready = await readReadyDaemon(launchConfig);
2707
+ if (ready) return ready;
2688
2708
  lastConfig = await readDeviceConfig(launchConfig, getConfigRoot());
2689
- if (lastConfig) {
2690
- const status = await readLiveStatus(lastConfig);
2691
- if (status) return { config: lastConfig, status };
2692
- }
2693
2709
  await wait2(POLL_INTERVAL_MS);
2694
2710
  }
2695
2711
  throw new Error(
@@ -2699,7 +2715,7 @@ var waitForDaemon = async (launchConfig, deadline) => {
2699
2715
  var spawnInternalDaemon = async (launchConfig, mode, preparedRuntime, options = {}) => {
2700
2716
  const runtime = preparedRuntime ?? await ensureStableConnectorRuntime();
2701
2717
  const debugDaemonStdio = process.env.BUDA_CONNECTOR_DEBUG_DAEMON_STDIO === "1";
2702
- await spawnDetachedDaemonProcess(
2718
+ return spawnDetachedDaemonProcess(
2703
2719
  runtime.nodeExecutable,
2704
2720
  getRuntimeCommandArgs(runtime, mode, options),
2705
2721
  {
@@ -2711,6 +2727,70 @@ var spawnInternalDaemon = async (launchConfig, mode, preparedRuntime, options =
2711
2727
  }
2712
2728
  );
2713
2729
  };
2730
+ var waitForProcessExit = async (pid, timeoutMs) => {
2731
+ const deadline = Date.now() + timeoutMs;
2732
+ while (Date.now() < deadline) {
2733
+ if (!isProcessAlive2(pid)) return true;
2734
+ await wait2(50);
2735
+ }
2736
+ return !isProcessAlive2(pid);
2737
+ };
2738
+ var signalProcess = (pid, signal) => {
2739
+ try {
2740
+ process.kill(pid, signal);
2741
+ return true;
2742
+ } catch (error) {
2743
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH") {
2744
+ return false;
2745
+ }
2746
+ throw error;
2747
+ }
2748
+ };
2749
+ var stopUnresponsiveDaemonProcess = async (pid) => {
2750
+ if (!isProcessAlive2(pid)) return;
2751
+ if (!signalProcess(pid, "SIGTERM")) return;
2752
+ if (await waitForProcessExit(pid, 2e3)) return;
2753
+ if (!signalProcess(pid, "SIGKILL")) return;
2754
+ if (await waitForProcessExit(pid, 3e3)) return;
2755
+ throw new Error(`Timed-out Connector daemon PID ${pid} could not be stopped safely`);
2756
+ };
2757
+ var readDaemonLockOwnerPid = async (config) => {
2758
+ const lockPath = `${getConnectorDaemonLockTarget(
2759
+ config.serverUrl,
2760
+ config.connectorId,
2761
+ getConfigRoot()
2762
+ )}.lock`;
2763
+ const raw = await readFile6(lockPath, "utf8").catch(() => "");
2764
+ try {
2765
+ const metadata = JSON.parse(raw);
2766
+ const age = typeof metadata.createdAt === "number" && Number.isFinite(metadata.createdAt) ? Date.now() - metadata.createdAt : Number.POSITIVE_INFINITY;
2767
+ return typeof metadata.pid === "number" && Number.isSafeInteger(metadata.pid) && metadata.pid > 0 && age >= 0 && age <= LOCK_ONLY_RECOVERY_MAX_AGE_MS ? metadata.pid : null;
2768
+ } catch {
2769
+ return null;
2770
+ }
2771
+ };
2772
+ async function startDaemonWithRecovery(dependencies, maxAttempts = 2) {
2773
+ let lastError;
2774
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2775
+ const spawned = await dependencies.spawnDaemon();
2776
+ try {
2777
+ return await dependencies.waitForReady();
2778
+ } catch (error) {
2779
+ lastError = error;
2780
+ const lateReady = await dependencies.readReady().catch(() => null);
2781
+ if (lateReady) return lateReady;
2782
+ await dependencies.stopDaemon(spawned.pid);
2783
+ if (attempt === maxAttempts) throw error;
2784
+ }
2785
+ }
2786
+ throw lastError;
2787
+ }
2788
+ var startInternalDaemonWithRecovery = (launchConfig, mode, preparedRuntime, options = {}) => startDaemonWithRecovery({
2789
+ readReady: () => readReadyDaemon(launchConfig),
2790
+ spawnDaemon: () => spawnInternalDaemon(launchConfig, mode, preparedRuntime, options),
2791
+ stopDaemon: stopUnresponsiveDaemonProcess,
2792
+ waitForReady: () => waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS)
2793
+ });
2714
2794
  var stopOlderIdleDaemon = async (config, status) => {
2715
2795
  if (!shouldReplaceDaemonVersion(status.version, CONNECTOR_RUNTIME_VERSION)) return false;
2716
2796
  if (!status.idle) return false;
@@ -2743,8 +2823,7 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
2743
2823
  const status = mode === "persistent" ? await requestDaemonControl(existingConfig, "persist", getConfigRoot()) : existingStatus;
2744
2824
  return { config: existingConfig, status };
2745
2825
  }
2746
- await spawnInternalDaemon(launchConfig, mode, preparedRuntime, options);
2747
- return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
2826
+ return await startInternalDaemonWithRecovery(launchConfig, mode, preparedRuntime, options);
2748
2827
  }
2749
2828
  const persistedStatus = await readPersistedDaemonStatus(
2750
2829
  existingConfig,
@@ -2755,9 +2834,12 @@ async function ensureConnectorDaemon(launchConfig, mode, options = {}) {
2755
2834
  `Connector daemon PID ${persistedStatus.pid} is running but its local control channel is not responding. Stop that process and retry.`
2756
2835
  );
2757
2836
  }
2837
+ const lockOwnerPid = await readDaemonLockOwnerPid(existingConfig);
2838
+ if (lockOwnerPid && isProcessAlive2(lockOwnerPid)) {
2839
+ await stopUnresponsiveDaemonProcess(lockOwnerPid);
2840
+ }
2758
2841
  }
2759
- await spawnInternalDaemon(launchConfig, mode, void 0, options);
2760
- return await waitForDaemon(launchConfig, Date.now() + START_TIMEOUT_MS);
2842
+ return await startInternalDaemonWithRecovery(launchConfig, mode, void 0, options);
2761
2843
  } finally {
2762
2844
  await bootstrapLock.release();
2763
2845
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buda-ai/connector",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "publishConfig": {