@launchfile/macos-dev 0.10.0 → 0.11.0

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.
@@ -59,8 +59,13 @@ export declare function computeAppEndpoints(launch: NormalizedLaunch): Record<st
59
59
  * every named published endpoint resolving `""` — and the `uses` each
60
60
  * resource entry declares (`declaredUses`), which the resolver reads to
61
61
  * resolve `$<resource>.<use>.<property>` strictly.
62
+ *
63
+ * `declared` carries the declared components, whose `provides` entries are
64
+ * what the named-endpoint form `$components.<name>.<endpoint>.<prop>`
65
+ * (D-6, D-66) resolves against. Omit it and only the primary
66
+ * `url`/`host`/`port` are registered.
62
67
  */
63
- export declare function buildResolverContext(resourceMap: Record<string, ResourceProperties>, componentPorts: Record<string, number>, secrets: Record<string, string>, app: Record<string, string | number>, appEndpoints?: Record<string, AppEndpointProperties>, uses?: Record<string, readonly string[]>): ResolverContext;
68
+ export declare function buildResolverContext(resourceMap: Record<string, ResourceProperties>, componentPorts: Record<string, number>, secrets: Record<string, string>, app: Record<string, string | number>, appEndpoints?: Record<string, AppEndpointProperties>, uses?: Record<string, readonly string[]>, declared?: Record<string, NormalizedComponent>): ResolverContext;
64
69
  /**
65
70
  * The use keys each resource entry declares (`db`, `db.cache`), keyed like
66
71
  * the resource namespace (`name ?? type`, app-global). Same-name entries pool
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { writeFile, mkdir } from "node:fs/promises";
8
8
  import { join } from "node:path";
9
- import { deriveAppUrlProperties, resolveExpression, isExpression, parseUseKey, suppliedAppProperties, UNPUBLISHED_APP_ENDPOINT, unsuppliedRequiredEnv, useKeys, } from "@launchfile/sdk";
9
+ import { deriveAppUrlProperties, endpointProperties, resolveExpression, isExpression, parseUseKey, suppliedAppProperties, UNPUBLISHED_APP_ENDPOINT, unsuppliedRequiredEnv, useKeys, } from "@launchfile/sdk";
10
10
  import { declaredPrimaryComponent, wireHttpsOrigins } from "./https-origin.js";
11
11
  import { getProvisioner } from "./resources/index.js";
12
12
  import { coveredUses, namedDatabases, withCoveredUses } from "./resources/uses.js";
@@ -95,6 +95,25 @@ export function computeAppEndpoints(launch) {
95
95
  }
96
96
  return endpoints;
97
97
  }
98
+ /**
99
+ * The `provides` entries this provider can name a reachable port for, or
100
+ * `undefined` when it can name none.
101
+ *
102
+ * macos-dev allocates exactly one host port per component and hands it to the
103
+ * process as `PORT`. That port belongs to whichever declared endpoint the
104
+ * allocator anchored on; the rest are unallocated, and the process binds the
105
+ * ports they declare. When the component's preferred port was taken, the
106
+ * allocator moves it to a free port outside the declared set — then no declared
107
+ * endpoint can be named at that port, so the component reports no per-endpoint
108
+ * properties rather than an endpoint address nothing listens on (L-4). A
109
+ * container provider has no such collapse: it binds every declared port.
110
+ */
111
+ function allocatedEndpoints(component, allocatedPort) {
112
+ const provides = component?.provides;
113
+ if (!provides?.some((entry) => entry.port === allocatedPort))
114
+ return undefined;
115
+ return provides;
116
+ }
98
117
  /**
99
118
  * Build a ResolverContext from provisioned resources, component ports,
100
119
  * secrets, (D-33) the platform-injected app properties, (D-63) the
@@ -102,8 +121,13 @@ export function computeAppEndpoints(launch) {
102
121
  * every named published endpoint resolving `""` — and the `uses` each
103
122
  * resource entry declares (`declaredUses`), which the resolver reads to
104
123
  * resolve `$<resource>.<use>.<property>` strictly.
124
+ *
125
+ * `declared` carries the declared components, whose `provides` entries are
126
+ * what the named-endpoint form `$components.<name>.<endpoint>.<prop>`
127
+ * (D-6, D-66) resolves against. Omit it and only the primary
128
+ * `url`/`host`/`port` are registered.
105
129
  */
106
- export function buildResolverContext(resourceMap, componentPorts, secrets, app, appEndpoints = {}, uses = {}) {
130
+ export function buildResolverContext(resourceMap, componentPorts, secrets, app, appEndpoints = {}, uses = {}, declared) {
107
131
  // Build components map from ports
108
132
  const components = {};
109
133
  for (const [name, port] of Object.entries(componentPorts)) {
@@ -111,6 +135,7 @@ export function buildResolverContext(resourceMap, componentPorts, secrets, app,
111
135
  url: `http://localhost:${port}`,
112
136
  host: "localhost",
113
137
  port,
138
+ ...endpointProperties(allocatedEndpoints(declared?.[name], port), "localhost"),
114
139
  };
115
140
  }
116
141
  // Build named resources map
@@ -195,7 +220,7 @@ export async function resourceMapFromState(launch, state, projectDir) {
195
220
  export function resolverContextFor(launch, resourceMap, state) {
196
221
  const appProperties = computeAppProperties(launch, state.ports, state.appUrl);
197
222
  wireHttpsOrigins(launch, resourceMap, state.appUrl);
198
- return buildResolverContext(resourceMap, state.ports, state.secrets, appProperties, computeAppEndpoints(launch), declaredUses(launch));
223
+ return buildResolverContext(resourceMap, state.ports, state.secrets, appProperties, computeAppEndpoints(launch), declaredUses(launch), launch.components);
199
224
  }
200
225
  /**
201
226
  * Resolve all environment variables for a single component.
@@ -16,9 +16,12 @@
16
16
  * 1. **Liveness** — `process.kill(pid, 0)` throws ESRCH if no such process
17
17
  * exists. If it's dead, we skip (already stopped).
18
18
  * 2. **Identity** — we compare the recorded spawn time against the live
19
- * process's actual start time via `ps -o lstart= -p <pid>`. If the live
20
- * process started meaningfully later than we recorded, the pid was
21
- * recycled and we REFUSE to signal it.
19
+ * process's actual start time via `ps -o lstart= -p <pid>`. If the two
20
+ * are further apart than the tolerance window, in either direction, the
21
+ * record does not describe this process and we REFUSE to signal it. A
22
+ * later live start means the pid was recycled; an earlier one means the
23
+ * record itself is untrustworthy (a backward clock jump between spawn and
24
+ * `down`, or a state file carried over from another run).
22
25
  *
23
26
  * This is a best-effort guarantee, not a cryptographic one. Its honest limits:
24
27
  * - `ps lstart` has ~1s resolution, so we allow a small tolerance window. A
@@ -76,7 +79,8 @@ export declare const realSignalFns: SignalFns;
76
79
  * - "alive-verified": process exists AND start time is consistent → safe to signal
77
80
  * - "alive-unverified": process exists but start time couldn't be read → signal group only, cautiously
78
81
  * - "dead": no such process (ESRCH) → already stopped, skip
79
- * - "mismatch": process exists but started too late → recycled pid, DO NOT signal
82
+ * - "mismatch": process exists but its start time is outside the tolerance
83
+ * window on either side → the record does not describe it, DO NOT signal
80
84
  */
81
85
  export declare function checkIdentity(rec: RecordedProcess, fns: SignalFns): Promise<"alive-verified" | "alive-unverified" | "dead" | "mismatch">;
82
86
  /**
@@ -16,9 +16,12 @@
16
16
  * 1. **Liveness** — `process.kill(pid, 0)` throws ESRCH if no such process
17
17
  * exists. If it's dead, we skip (already stopped).
18
18
  * 2. **Identity** — we compare the recorded spawn time against the live
19
- * process's actual start time via `ps -o lstart= -p <pid>`. If the live
20
- * process started meaningfully later than we recorded, the pid was
21
- * recycled and we REFUSE to signal it.
19
+ * process's actual start time via `ps -o lstart= -p <pid>`. If the two
20
+ * are further apart than the tolerance window, in either direction, the
21
+ * record does not describe this process and we REFUSE to signal it. A
22
+ * later live start means the pid was recycled; an earlier one means the
23
+ * record itself is untrustworthy (a backward clock jump between spawn and
24
+ * `down`, or a state file carried over from another run).
22
25
  *
23
26
  * This is a best-effort guarantee, not a cryptographic one. Its honest limits:
24
27
  * - `ps lstart` has ~1s resolution, so we allow a small tolerance window. A
@@ -33,10 +36,12 @@
33
36
  */
34
37
  import { execFile } from "node:child_process";
35
38
  /**
36
- * Identity tolerance: how much later than `startedAt` a live process may report
37
- * having started before we treat it as a recycled pid. `ps lstart` rounds to
38
- * whole seconds and there's scheduling slop between our `Date.now()` snapshot
39
- * and the kernel's recorded start, so we allow a few seconds of forward drift.
39
+ * Identity tolerance: how far a live process's reported start time may sit from
40
+ * `startedAt`, on either side, before we stop believing the record describes
41
+ * that process. `spawn()` forks before we timestamp the record and `ps lstart`
42
+ * rounds down to whole seconds, so a genuine record reads a second or so
43
+ * *earlier* than `startedAt`; scheduling slop covers the rest. The window is
44
+ * symmetric because a record can be wrong in either direction.
40
45
  */
41
46
  const START_TIME_TOLERANCE_MS = 3000;
42
47
  /** Default real implementation backed by `process.kill` and `ps`. */
@@ -74,7 +79,8 @@ function queryStartTime(pid) {
74
79
  * - "alive-verified": process exists AND start time is consistent → safe to signal
75
80
  * - "alive-unverified": process exists but start time couldn't be read → signal group only, cautiously
76
81
  * - "dead": no such process (ESRCH) → already stopped, skip
77
- * - "mismatch": process exists but started too late → recycled pid, DO NOT signal
82
+ * - "mismatch": process exists but its start time is outside the tolerance
83
+ * window on either side → the record does not describe it, DO NOT signal
78
84
  */
79
85
  export async function checkIdentity(rec, fns) {
80
86
  // Liveness probe.
@@ -95,9 +101,11 @@ export async function checkIdentity(rec, fns) {
95
101
  const recordedStart = Date.parse(rec.startedAt);
96
102
  if (Number.isNaN(recordedStart))
97
103
  return "alive-unverified";
98
- // If the live process started meaningfully *after* we recorded the spawn,
99
- // the original exited and the pid was recycled. Refuse to signal it.
100
- if (liveStart > recordedStart + START_TIME_TOLERANCE_MS)
104
+ // The live start time must sit within tolerance of the record on both sides.
105
+ // Later than the record: the original exited and the pid was recycled.
106
+ // Earlier than the record: the record cannot be describing this process.
107
+ // Either way the pid is not ours to signal.
108
+ if (Math.abs(liveStart - recordedStart) > START_TIME_TOLERANCE_MS)
101
109
  return "mismatch";
102
110
  return "alive-verified";
103
111
  }
package/dist/provider.js CHANGED
@@ -866,7 +866,7 @@ export async function launchDown(opts = {}) {
866
866
  console.log(` ${o.component} was not running`);
867
867
  break;
868
868
  case "identity-mismatch":
869
- console.log(` Skipped ${o.component} (pid recycled — left untouched)`);
869
+ console.log(` Skipped ${o.component} (live process start time does not match the recorded one — left untouched)`);
870
870
  break;
871
871
  case "error":
872
872
  console.log(` Failed to stop ${o.component}: ${o.error}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchfile/macos-dev",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "macOS dev provider for Launchfile — run apps locally via brew services and native runtimes",
5
5
  "os": [
6
6
  "darwin"
@@ -36,7 +36,7 @@
36
36
  "directory": "providers/macos-dev"
37
37
  },
38
38
  "dependencies": {
39
- "@launchfile/sdk": "^0.10.0",
39
+ "@launchfile/sdk": "^0.11.0",
40
40
  "semver": "^7.7.4"
41
41
  },
42
42
  "devDependencies": {