@launchfile/macos-dev 0.2.0 → 0.4.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/bootstrap.d.ts +56 -8
- package/dist/bootstrap.js +170 -93
- package/dist/env-writer.d.ts +40 -11
- package/dist/env-writer.js +84 -23
- package/dist/health.d.ts +8 -2
- package/dist/health.js +11 -17
- package/dist/prereqs.js +2 -2
- package/dist/process-manager.d.ts +13 -0
- package/dist/process-manager.js +59 -3
- package/dist/process-stopper.d.ts +99 -0
- package/dist/process-stopper.js +165 -0
- package/dist/provider.d.ts +50 -0
- package/dist/provider.js +337 -28
- package/dist/redact.d.ts +55 -0
- package/dist/redact.js +92 -0
- package/dist/resources/identifiers.d.ts +22 -0
- package/dist/resources/identifiers.js +31 -0
- package/dist/resources/index.d.ts +3 -3
- package/dist/resources/index.js +21 -10
- package/dist/resources/mysql.d.ts +3 -1
- package/dist/resources/mysql.js +49 -11
- package/dist/resources/postgres.d.ts +4 -1
- package/dist/resources/postgres.js +63 -17
- package/dist/resources/redis.d.ts +3 -1
- package/dist/resources/redis.js +14 -4
- package/dist/resources/sqlite.js +2 -2
- package/dist/resources/types.d.ts +21 -3
- package/dist/runtimes/bun.js +2 -2
- package/dist/runtimes/installed-versions.d.ts +12 -0
- package/dist/runtimes/installed-versions.js +21 -0
- package/dist/runtimes/node.js +13 -8
- package/dist/runtimes/python.js +14 -7
- package/dist/runtimes/ruby.js +13 -8
- package/dist/secret-generator.js +11 -3
- package/dist/shell.d.ts +28 -6
- package/dist/shell.js +83 -28
- package/dist/state.d.ts +38 -0
- package/dist/state.js +12 -1
- package/dist/storage.d.ts +16 -3
- package/dist/storage.js +23 -6
- package/package.json +7 -4
- package/dist/__tests__/bootstrap.test.d.ts +0 -2
- package/dist/__tests__/bootstrap.test.js +0 -90
- package/dist/__tests__/dry-run.test.d.ts +0 -2
- package/dist/__tests__/dry-run.test.js +0 -141
- package/dist/__tests__/env-writer.test.d.ts +0 -2
- package/dist/__tests__/env-writer.test.js +0 -201
- package/dist/__tests__/lockfile-detect.test.d.ts +0 -2
- package/dist/__tests__/lockfile-detect.test.js +0 -75
- package/dist/__tests__/port-allocator.test.d.ts +0 -2
- package/dist/__tests__/port-allocator.test.js +0 -53
- package/dist/__tests__/secret-generator.test.d.ts +0 -2
- package/dist/__tests__/secret-generator.test.js +0 -26
- package/dist/__tests__/state.test.d.ts +0 -2
- package/dist/__tests__/state.test.js +0 -30
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-session app-process termination for `launch down`.
|
|
3
|
+
*
|
|
4
|
+
* The foreground `launch up` session kills its children directly via the
|
|
5
|
+
* ProcessManager on Ctrl+C. But `launch down` may run from a *different* shell
|
|
6
|
+
* (or after the up-session has exited), so it has no live ChildProcess handles —
|
|
7
|
+
* only the pids recorded in `.launchfile/state.json`. This module signals those
|
|
8
|
+
* recorded process groups, with liveness + identity checks to avoid killing an
|
|
9
|
+
* unrelated process that happens to have inherited a recycled pid.
|
|
10
|
+
*
|
|
11
|
+
* ## pid-reuse identity guarantee (read this before trusting the kill)
|
|
12
|
+
*
|
|
13
|
+
* macOS recycles pids. A pid we recorded at `up` time may, by `down` time,
|
|
14
|
+
* belong to a completely unrelated process. Before sending any signal we:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Liveness** — `process.kill(pid, 0)` throws ESRCH if no such process
|
|
17
|
+
* exists. If it's dead, we skip (already stopped).
|
|
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.
|
|
22
|
+
*
|
|
23
|
+
* This is a best-effort guarantee, not a cryptographic one. Its honest limits:
|
|
24
|
+
* - `ps lstart` has ~1s resolution, so we allow a small tolerance window. A
|
|
25
|
+
* recycled process that started within that window of the original spawn
|
|
26
|
+
* could theoretically slip through — vanishingly unlikely in practice.
|
|
27
|
+
* - If `ps` is unavailable/fails, we fall back to liveness-only and signal
|
|
28
|
+
* conservatively ONLY the process group (never a bare pid), accepting the
|
|
29
|
+
* small residual risk rather than orphaning the user's processes forever.
|
|
30
|
+
* - We signal the process GROUP (negative pgid) to also reap children the app
|
|
31
|
+
* spawned, matching the foreground SIGINT behavior. A recycled *group*
|
|
32
|
+
* leader is the residual risk the start-time check exists to close.
|
|
33
|
+
*/
|
|
34
|
+
import { execFile } from "node:child_process";
|
|
35
|
+
/**
|
|
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.
|
|
40
|
+
*/
|
|
41
|
+
const START_TIME_TOLERANCE_MS = 3000;
|
|
42
|
+
/** Default real implementation backed by `process.kill` and `ps`. */
|
|
43
|
+
export const realSignalFns = {
|
|
44
|
+
kill: (pid, signal) => {
|
|
45
|
+
process.kill(pid, signal);
|
|
46
|
+
},
|
|
47
|
+
startTime: (pid) => queryStartTime(pid),
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Query a process's start time via `ps -o lstart= -p <pid>` using array args
|
|
51
|
+
* (no shell, no interpolation). Returns epoch ms, or null on any failure.
|
|
52
|
+
*/
|
|
53
|
+
function queryStartTime(pid) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
execFile("ps", ["-o", "lstart=", "-p", String(pid)], (error, stdout) => {
|
|
56
|
+
if (error) {
|
|
57
|
+
resolve(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const text = stdout.trim();
|
|
61
|
+
if (!text) {
|
|
62
|
+
resolve(null);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const parsed = Date.parse(text);
|
|
66
|
+
resolve(Number.isNaN(parsed) ? null : parsed);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Verify a recorded process is still the one we started.
|
|
72
|
+
*
|
|
73
|
+
* Returns:
|
|
74
|
+
* - "alive-verified": process exists AND start time is consistent → safe to signal
|
|
75
|
+
* - "alive-unverified": process exists but start time couldn't be read → signal group only, cautiously
|
|
76
|
+
* - "dead": no such process (ESRCH) → already stopped, skip
|
|
77
|
+
* - "mismatch": process exists but started too late → recycled pid, DO NOT signal
|
|
78
|
+
*/
|
|
79
|
+
export async function checkIdentity(rec, fns) {
|
|
80
|
+
// Liveness probe.
|
|
81
|
+
try {
|
|
82
|
+
fns.kill(rec.pid, 0);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (isErrno(err) && err.code === "ESRCH")
|
|
86
|
+
return "dead";
|
|
87
|
+
// EPERM means it exists but we can't signal it — treat as alive-unverified.
|
|
88
|
+
if (isErrno(err) && err.code === "EPERM")
|
|
89
|
+
return "alive-unverified";
|
|
90
|
+
return "dead";
|
|
91
|
+
}
|
|
92
|
+
const liveStart = await fns.startTime(rec.pid);
|
|
93
|
+
if (liveStart === null)
|
|
94
|
+
return "alive-unverified";
|
|
95
|
+
const recordedStart = Date.parse(rec.startedAt);
|
|
96
|
+
if (Number.isNaN(recordedStart))
|
|
97
|
+
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)
|
|
101
|
+
return "mismatch";
|
|
102
|
+
return "alive-verified";
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Stop one recorded process: graceful SIGTERM to the group, then escalate to
|
|
106
|
+
* SIGKILL after `graceMs` if it's still alive. Always targets the process GROUP
|
|
107
|
+
* (negative pgid) so child processes the app spawned die too.
|
|
108
|
+
*/
|
|
109
|
+
export async function stopProcess(component, rec, fns, opts = {}) {
|
|
110
|
+
const graceMs = opts.graceMs ?? 5000;
|
|
111
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
112
|
+
const identity = await checkIdentity(rec, fns);
|
|
113
|
+
if (identity === "dead")
|
|
114
|
+
return { component, result: "already-dead" };
|
|
115
|
+
if (identity === "mismatch")
|
|
116
|
+
return { component, result: "identity-mismatch" };
|
|
117
|
+
// Target the process group. pgid is the leader pid because we spawned detached.
|
|
118
|
+
const groupTarget = -rec.pgid;
|
|
119
|
+
try {
|
|
120
|
+
fns.kill(groupTarget, "SIGTERM");
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (isErrno(err) && err.code === "ESRCH")
|
|
124
|
+
return { component, result: "already-dead" };
|
|
125
|
+
return { component, result: "error", error: errMessage(err) };
|
|
126
|
+
}
|
|
127
|
+
// Wait for graceful exit, then escalate if the leader is still alive.
|
|
128
|
+
await sleep(graceMs);
|
|
129
|
+
let stillAlive = true;
|
|
130
|
+
try {
|
|
131
|
+
fns.kill(rec.pid, 0);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
stillAlive = false;
|
|
135
|
+
}
|
|
136
|
+
if (stillAlive) {
|
|
137
|
+
try {
|
|
138
|
+
fns.kill(groupTarget, "SIGKILL");
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (!(isErrno(err) && err.code === "ESRCH")) {
|
|
142
|
+
return { component, result: "error", error: errMessage(err) };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { component, result: "stopped" };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Stop every recorded process. Pure orchestration over `stopProcess`; returns
|
|
150
|
+
* one outcome per component so the caller can report and tests can assert.
|
|
151
|
+
*/
|
|
152
|
+
export async function stopRecordedProcesses(processes, fns = realSignalFns, opts = {}) {
|
|
153
|
+
const outcomes = [];
|
|
154
|
+
for (const [component, rec] of Object.entries(processes)) {
|
|
155
|
+
outcomes.push(await stopProcess(component, rec, fns, opts));
|
|
156
|
+
}
|
|
157
|
+
return outcomes;
|
|
158
|
+
}
|
|
159
|
+
function isErrno(err) {
|
|
160
|
+
return typeof err === "object" && err !== null && "code" in err;
|
|
161
|
+
}
|
|
162
|
+
function errMessage(err) {
|
|
163
|
+
return err instanceof Error ? err.message : String(err);
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=process-stopper.js.map
|
package/dist/provider.d.ts
CHANGED
|
@@ -4,13 +4,63 @@
|
|
|
4
4
|
* Reads a Launchfile, provisions resources, resolves env vars,
|
|
5
5
|
* installs runtimes, and starts all components.
|
|
6
6
|
*/
|
|
7
|
+
import { type NormalizedLaunch, type NormalizedComponent } from "@launchfile/sdk";
|
|
8
|
+
/**
|
|
9
|
+
* This provider runs apps from source. A component is source-runnable when
|
|
10
|
+
* {@link resolveSourceRunCommand} (D-38) resolves a command — declares `dev`,
|
|
11
|
+
* or a `start` with no `image`. An `image` without a `dev` override stays
|
|
12
|
+
* artifact-mode, which this source-only provider can't launch.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isSourceRunnable(component: NormalizedComponent): boolean;
|
|
7
15
|
export interface LaunchUpOpts {
|
|
8
16
|
withOptional?: boolean;
|
|
9
17
|
noBuild?: boolean;
|
|
10
18
|
detach?: boolean;
|
|
11
19
|
dryRun?: boolean;
|
|
12
20
|
projectDir?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Component selector (#77): if non-empty, these components plus their
|
|
23
|
+
* transitive downward `depends_on` closure are started (D-41), and the
|
|
24
|
+
* `requires` of every closure member are provisioned. The start-set is the
|
|
25
|
+
* SDK's `selectionClosure` — the same definition the Docker provider uses —
|
|
26
|
+
* so both yield the identical running topology (P-5). Empty = all components.
|
|
27
|
+
*/
|
|
28
|
+
components?: string[];
|
|
13
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Components this provider must refuse, mapped to the capabilities it cannot
|
|
32
|
+
* grant (D-44, PROVIDERS.md §11). Both spellings fold together so the `host:`
|
|
33
|
+
* entry form and the legacy top-level block produce the same outcome.
|
|
34
|
+
*
|
|
35
|
+
* A refusal must remove the component from the run, not merely report it —
|
|
36
|
+
* this provider grants no host capabilities, so anything listed here cannot
|
|
37
|
+
* be installed, wired, registered, or started.
|
|
38
|
+
*/
|
|
39
|
+
export declare function refusedHostCapabilities(launch: NormalizedLaunch): Map<string, string[]>;
|
|
40
|
+
/**
|
|
41
|
+
* The launch-time notice a provider without a scheduler owes for a declared
|
|
42
|
+
* `schedule` (D-51, PROVIDERS.md §10 item 8).
|
|
43
|
+
*
|
|
44
|
+
* States what *this provider* does, not what will happen to the app: a
|
|
45
|
+
* component may schedule itself — `catalog/drafts/diun` sets its own
|
|
46
|
+
* `DIUN_WATCH_SCHEDULE`, and nextcloud's `cron.sh` is a foreground `crond` —
|
|
47
|
+
* so claiming the job will not run would be false about those apps, and a
|
|
48
|
+
* warning that misstates the user's app is worse than the silence it replaces.
|
|
49
|
+
*/
|
|
50
|
+
export declare function scheduleWarning(component: string, schedule: string): string;
|
|
51
|
+
/**
|
|
52
|
+
* Remove every component this provider must refuse, and say so on stderr.
|
|
53
|
+
*
|
|
54
|
+
* The removal is the refusal (D-44, PROVIDERS.md §11): a component left in the
|
|
55
|
+
* map goes on to be installed, env-wired, registered with the process manager
|
|
56
|
+
* and started, so logging alone would have the provider assert a refusal it
|
|
57
|
+
* did not perform. Mutates `launch.components` for exactly that reason —
|
|
58
|
+
* everything downstream reads it.
|
|
59
|
+
*
|
|
60
|
+
* Returns "none-left" when nothing survives, so the caller can fail rather than
|
|
61
|
+
* report success over an empty set.
|
|
62
|
+
*/
|
|
63
|
+
export declare function applyHostCapabilityRefusals(launch: NormalizedLaunch): "ok" | "none-left";
|
|
14
64
|
export declare function launchUp(opts?: LaunchUpOpts): Promise<void>;
|
|
15
65
|
export declare function launchDown(opts?: {
|
|
16
66
|
destroy?: boolean;
|