@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.
- package/dist/env-writer.d.ts +6 -1
- package/dist/env-writer.js +28 -3
- package/dist/process-stopper.d.ts +8 -4
- package/dist/process-stopper.js +19 -11
- package/dist/provider.js +1 -1
- package/package.json +2 -2
package/dist/env-writer.d.ts
CHANGED
|
@@ -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
|
package/dist/env-writer.js
CHANGED
|
@@ -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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
/**
|
package/dist/process-stopper.js
CHANGED
|
@@ -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
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
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
|
-
//
|
|
99
|
-
// the original exited and the pid was recycled.
|
|
100
|
-
|
|
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} (
|
|
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.
|
|
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.
|
|
39
|
+
"@launchfile/sdk": "^0.11.0",
|
|
40
40
|
"semver": "^7.7.4"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|