@volter/world-runtime 3.0.163 → 3.0.165
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/src/ancestry.js +2 -2
- package/dist/src/app-url.d.ts +5 -1
- package/dist/src/app-url.js +14 -1
- package/dist/src/branch.js +28 -8
- package/dist/src/browser-files.d.ts +5 -0
- package/dist/src/browser-files.js +14 -0
- package/dist/src/browsers.d.ts +2 -3
- package/dist/src/browsers.js +4 -4
- package/dist/src/catalog.d.ts +2 -1
- package/dist/src/catalog.js +9 -3
- package/dist/src/changeset.js +1 -1
- package/dist/src/cli-commands.js +71 -18
- package/dist/src/cli-load-clock.d.ts +1 -0
- package/dist/src/cli-load-clock.js +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/clickhouse-backing.d.ts +2 -0
- package/dist/src/clickhouse-backing.js +145 -0
- package/dist/src/command-lifetime.d.mts +1 -1
- package/dist/src/command-lifetime.mjs +5 -2
- package/dist/src/configs.js +1 -1
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +4 -2
- package/dist/src/covers.js +8 -4
- package/dist/src/go-mysql-binding.d.ts +19 -0
- package/dist/src/go-mysql-binding.js +51 -0
- package/dist/src/guest-processes.d.mts +1 -1
- package/dist/src/guest-processes.mjs +22 -14
- package/dist/src/host-cli.js +8 -1
- package/dist/src/host-ready.d.ts +16 -2
- package/dist/src/host-ready.js +66 -39
- package/dist/src/host-worker.js +7 -1
- package/dist/src/host.d.ts +26 -0
- package/dist/src/host.js +93 -3
- package/dist/src/infra-definition.d.ts +35 -0
- package/dist/src/infra-definition.js +146 -0
- package/dist/src/infra.d.ts +3 -0
- package/dist/src/infra.js +375 -105
- package/dist/src/init.d.ts +46 -0
- package/dist/src/init.js +446 -19
- package/dist/src/lifecycle-record.d.ts +5 -1
- package/dist/src/lifecycle-record.js +27 -4
- package/dist/src/local-branches.js +1 -1
- package/dist/src/mariadb-bootstrap.d.ts +32 -0
- package/dist/src/mariadb-bootstrap.js +90 -0
- package/dist/src/mariadb-supplier.d.ts +23 -0
- package/dist/src/mariadb-supplier.js +215 -0
- package/dist/src/mongodb-backing.d.ts +4 -0
- package/dist/src/mongodb-backing.js +71 -1
- package/dist/src/native-mariadb-backing.d.ts +13 -0
- package/dist/src/native-mariadb-backing.js +512 -0
- package/dist/src/native-postgres-backing.d.ts +4 -10
- package/dist/src/native-postgres-backing.js +428 -43
- package/dist/src/native-server-inputs.d.ts +16 -0
- package/dist/src/native-server-inputs.js +204 -0
- package/dist/src/origin.js +3 -3
- package/dist/src/original-inputs.d.ts +14 -0
- package/dist/src/original-inputs.js +84 -0
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pglite-backing.d.ts +41 -17
- package/dist/src/pglite-backing.js +213 -79
- package/dist/src/pglite-host.mjs +482 -10
- package/dist/src/process-groups.d.mts +6 -1
- package/dist/src/process-groups.d.ts +2 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/process-groups.mjs +82 -15
- package/dist/src/process-signal.d.mts +2 -2
- package/dist/src/process-signal.mjs +29 -9
- package/dist/src/project-inspect.d.ts +12 -0
- package/dist/src/project-inspect.js +41 -4
- package/dist/src/proxy-env.d.ts +34 -0
- package/dist/src/proxy-env.js +114 -0
- package/dist/src/redirect-proxy.d.ts +1 -31
- package/dist/src/redirect-proxy.js +3 -112
- package/dist/src/redis-backing.d.ts +6 -4
- package/dist/src/redis-backing.js +20 -168
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +503 -218
- package/dist/src/schema.d.ts +1 -0
- package/dist/src/schema.js +5 -3
- package/dist/src/served-world.js +3 -3
- package/dist/src/twin-backing.d.ts +47 -0
- package/dist/src/twin-backing.js +343 -0
- package/dist/src/up-cli.js +56 -3
- package/package.json +3 -3
- package/src/ancestry.ts +2 -2
- package/src/app-url.ts +9 -2
- package/src/branch.ts +26 -8
- package/src/browser-files.ts +15 -0
- package/src/browsers.ts +4 -4
- package/src/catalog.ts +9 -4
- package/src/changeset.ts +1 -1
- package/src/cli-commands.ts +85 -18
- package/src/cli-load-clock.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/clickhouse-backing.ts +70 -0
- package/src/command-lifetime.d.mts +1 -1
- package/src/command-lifetime.mjs +5 -2
- package/src/configs.ts +1 -1
- package/src/consumers.ts +1 -1
- package/src/covers.ts +9 -4
- package/src/go-mysql-binding.ts +49 -0
- package/src/guest-processes.d.mts +1 -1
- package/src/guest-processes.mjs +22 -14
- package/src/host-cli.ts +7 -1
- package/src/host-ready.ts +62 -31
- package/src/host-worker.ts +6 -2
- package/src/host.ts +88 -6
- package/src/infra-definition.ts +135 -0
- package/src/infra.ts +250 -45
- package/src/init.ts +417 -18
- package/src/lifecycle-record.ts +18 -4
- package/src/local-branches.ts +1 -1
- package/src/mariadb-bootstrap.ts +107 -0
- package/src/mariadb-supplier.ts +199 -0
- package/src/mongodb-backing.ts +59 -1
- package/src/native-mariadb-backing.ts +418 -0
- package/src/native-postgres-backing.ts +353 -41
- package/src/native-server-inputs.ts +169 -0
- package/src/origin.ts +3 -3
- package/src/original-inputs.ts +72 -0
- package/src/pack-facts.ts +1 -1
- package/src/pglite-backing.ts +192 -80
- package/src/pglite-host.mjs +482 -10
- package/src/process-groups.d.mts +6 -1
- package/src/process-groups.mjs +82 -15
- package/src/process-groups.ts +2 -1
- package/src/process-signal.d.mts +2 -2
- package/src/process-signal.mjs +29 -9
- package/src/project-inspect.ts +37 -4
- package/src/proxy-env.ts +128 -0
- package/src/redirect-proxy.ts +3 -125
- package/src/redis-backing.ts +19 -136
- package/src/runtime.ts +342 -107
- package/src/schema.ts +5 -4
- package/src/served-world.ts +3 -3
- package/src/twin-backing.ts +300 -0
- package/src/up-cli.ts +52 -3
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// The actual WALI MariaDB server owns SQL and files. This lifecycle owns only
|
|
2
|
+
// its issued policy, child/group, receipts, readiness and stopped data.
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { chmodSync, chownSync, closeSync, constants, existsSync, fchmodSync, fchownSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
6
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { commandLifetime, COMMAND_GRACE_MS } from "./command-lifetime.js";
|
|
8
|
+
import { survivingOwnedGroups, worldEvent } from "./process-groups.js";
|
|
9
|
+
import { publishProcessReceipt, spawnReceiptPath, verifySpawnIdentity, signalOwnedProcess } from "./process-receipt.js";
|
|
10
|
+
import { childIdentity, signalChild } from './process-signal.mjs';
|
|
11
|
+
import { mariaDBBootstrap, mariaDBClientOptions, mariaDBReadinessCommand } from "./mariadb-bootstrap.js";
|
|
12
|
+
import { nativeMariaDBShellEnvironment, nativeMariaDBSupplier } from "./mariadb-supplier.js";
|
|
13
|
+
const starts = new Map();
|
|
14
|
+
const ownerFile = (root) => join(root, 'native-mariadb.owner.json');
|
|
15
|
+
const startingFile = (root) => join(root, 'native-mariadb.starting.json');
|
|
16
|
+
const dataDirectory = (root) => resolve(root, 'native-mariadb-data');
|
|
17
|
+
const wait = (ms) => new Promise(done => setTimeout(done, ms));
|
|
18
|
+
const alive = (pid) => pid > 0 && survivingOwnedGroups([pid]).length !== 0;
|
|
19
|
+
// Keep the existing managed-store readiness/retirement budget and native PID
|
|
20
|
+
// inspection cadence (native-postgres-backing.ts READY_TIMEOUT_MS/READY_CHECK_MS,
|
|
21
|
+
// measured in cookbook/calcom/output/native-lifetime.txt). No MariaDB duration
|
|
22
|
+
// or readiness result is inferred from that PostgreSQL measurement.
|
|
23
|
+
const STORE_BUDGET_MS = 60_000;
|
|
24
|
+
const CHECK_MS = 20;
|
|
25
|
+
function receiptFiles(files) {
|
|
26
|
+
return files.flatMap(file => {
|
|
27
|
+
let fd;
|
|
28
|
+
try {
|
|
29
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code === 'ENOENT')
|
|
33
|
+
return [];
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const stat = fstatSync(fd);
|
|
38
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid?.())
|
|
39
|
+
throw new Error('native MariaDB lifecycle receipt is not its owned regular file');
|
|
40
|
+
const data = readFileSync(fd), current = lstatSync(file);
|
|
41
|
+
if (!current.isFile() || current.dev !== stat.dev || current.ino !== stat.ino || current.ctimeMs !== stat.ctimeMs)
|
|
42
|
+
throw new Error('native MariaDB lifecycle receipt changed while reading');
|
|
43
|
+
return [{ file, dev: stat.dev, ino: stat.ino, ctimeMs: stat.ctimeMs,
|
|
44
|
+
sha256: createHash('sha256').update(data).digest('hex'), text: data.toString('utf8') }];
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
closeSync(fd);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function removeReceipts(expected) {
|
|
52
|
+
const failures = [];
|
|
53
|
+
for (const held of expected) {
|
|
54
|
+
try {
|
|
55
|
+
const current = receiptFiles([held.file])[0];
|
|
56
|
+
if (!current || current.dev !== held.dev || current.ino !== held.ino || current.ctimeMs !== held.ctimeMs || current.sha256 !== held.sha256)
|
|
57
|
+
throw new Error('native MariaDB lifecycle receipt changed during retirement: ' + held.file);
|
|
58
|
+
rmSync(held.file);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
failures.push(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (failures.length)
|
|
65
|
+
throw new AggregateError(failures, 'native MariaDB retired with uncertain or unremovable receipts');
|
|
66
|
+
}
|
|
67
|
+
function runAs() {
|
|
68
|
+
const uid = process.getuid?.(), gid = process.getgid?.();
|
|
69
|
+
if (uid === undefined || gid === undefined)
|
|
70
|
+
throw new Error('native MariaDB needs a numeric OS identity');
|
|
71
|
+
if (uid !== 0)
|
|
72
|
+
return { uid, gid };
|
|
73
|
+
const accounts = readFileSync('/etc/passwd', 'utf8').split('\n').map(line => line.split(':'));
|
|
74
|
+
for (const name of ['mysql', 'nobody']) {
|
|
75
|
+
const row = accounts.find(fields => fields[0] === name && /^\d+$/u.test(fields[2] ?? '') && /^\d+$/u.test(fields[3] ?? '')
|
|
76
|
+
&& Number(fields[2]) > 0 && Number(fields[2]) < 0xffffffff && Number(fields[3]) > 0 && Number(fields[3]) < 0xffffffff);
|
|
77
|
+
if (row)
|
|
78
|
+
return { uid: Number(row[2]), gid: Number(row[3]) };
|
|
79
|
+
}
|
|
80
|
+
throw new Error('native MariaDB started by root needs an installed non-root mysql or nobody account');
|
|
81
|
+
}
|
|
82
|
+
function privateFile(file, body, identity) {
|
|
83
|
+
publishProcessReceipt(file, body);
|
|
84
|
+
chownSync(file, identity.uid, identity.gid);
|
|
85
|
+
chmodSync(file, 0o600);
|
|
86
|
+
}
|
|
87
|
+
function privateLog(file, identity) {
|
|
88
|
+
const fd = openSync(file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
89
|
+
try {
|
|
90
|
+
const stat = fstatSync(fd);
|
|
91
|
+
if (!stat.isFile() || stat.nlink !== 1 || ![process.getuid?.(), identity.uid].includes(stat.uid))
|
|
92
|
+
throw new Error('native MariaDB private log has uncertain ownership or file kind');
|
|
93
|
+
fchmodSync(fd, 0o600);
|
|
94
|
+
fchownSync(fd, identity.uid, identity.gid);
|
|
95
|
+
return fd;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
closeSync(fd);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function privateText(file, identity) {
|
|
103
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
104
|
+
try {
|
|
105
|
+
const stat = fstatSync(fd);
|
|
106
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== identity.uid || (stat.mode & 0o777) !== 0o600)
|
|
107
|
+
throw new Error('native MariaDB private policy/options file has uncertain ownership or permissions');
|
|
108
|
+
return readFileSync(fd, 'utf8');
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
closeSync(fd);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function ownData(directory, identity) {
|
|
115
|
+
const stat = lstatSync(directory);
|
|
116
|
+
if (stat.isSymbolicLink())
|
|
117
|
+
throw new Error('native MariaDB stopped data contains an unsupported storage link');
|
|
118
|
+
if (!stat.isDirectory() && !stat.isFile())
|
|
119
|
+
throw new Error('native MariaDB data contains an unsupported special file');
|
|
120
|
+
if (stat.uid !== identity.uid || stat.gid !== identity.gid)
|
|
121
|
+
chownSync(directory, identity.uid, identity.gid);
|
|
122
|
+
if (stat.isDirectory())
|
|
123
|
+
for (const name of readdirSync(directory))
|
|
124
|
+
ownData(join(directory, name), identity);
|
|
125
|
+
}
|
|
126
|
+
function searchRoot(root, env, identity) {
|
|
127
|
+
const directories = [root];
|
|
128
|
+
if (env.VOLTER_WORLD_DATA && resolve(env.VOLTER_WORLD_DATA) === resolve(root) && env.VOLTER_WORLD_INSTANCE
|
|
129
|
+
&& dirname(resolve(env.VOLTER_WORLD_INSTANCE)) === dirname(resolve(root)))
|
|
130
|
+
directories.push(dirname(resolve(root)));
|
|
131
|
+
for (const directory of directories) {
|
|
132
|
+
const stat = lstatSync(directory);
|
|
133
|
+
if (!stat.isDirectory())
|
|
134
|
+
throw new Error('native MariaDB bookkeeping root is not a regular directory');
|
|
135
|
+
if ((stat.uid === identity.uid && (stat.mode & 0o100)) || (stat.gid === identity.gid && (stat.mode & 0o010)) || (stat.mode & 0o001))
|
|
136
|
+
continue;
|
|
137
|
+
chownSync(directory, stat.uid, identity.gid);
|
|
138
|
+
chmodSync(directory, (stat.mode & 0o7777) | 0o010);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function state(root) {
|
|
142
|
+
const files = [];
|
|
143
|
+
const read = (file, parse) => {
|
|
144
|
+
try {
|
|
145
|
+
const retained = receiptFiles([file])[0];
|
|
146
|
+
if (!retained)
|
|
147
|
+
return { present: false };
|
|
148
|
+
files.push(retained);
|
|
149
|
+
return { present: true, value: parse(retained.text) };
|
|
150
|
+
}
|
|
151
|
+
catch (cause) {
|
|
152
|
+
return { present: true, error: new Error('native MariaDB ownership reading is uncertain', { cause }) };
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const rows = [ownerFile(root), startingFile(root)].map(file => read(file, text => {
|
|
156
|
+
const row = JSON.parse(text);
|
|
157
|
+
if (!Number.isSafeInteger(row.pid) || row.pid < 0 || row.data !== dataDirectory(root)
|
|
158
|
+
|| !Number.isInteger(row.port) || row.port < 1 || row.port > 65535
|
|
159
|
+
|| !/^[a-f0-9]{64}$/u.test(row.receiptSha256) || !/^\d+\.\d+\.\d+$/u.test(row.release))
|
|
160
|
+
throw new Error('native MariaDB ownership receipt is invalid');
|
|
161
|
+
if (row.pidFile !== undefined && (!row.pidFile || typeof row.pidFile !== 'object'
|
|
162
|
+
|| !Number.isSafeInteger(row.pidFile.dev) || row.pidFile.dev < 0
|
|
163
|
+
|| !Number.isSafeInteger(row.pidFile.ino) || row.pidFile.ino < 0
|
|
164
|
+
|| !Number.isFinite(row.pidFile.ctimeMs) || row.pidFile.ctimeMs < 0
|
|
165
|
+
|| Object.keys(row.pidFile).some(key => !['dev', 'ino', 'ctimeMs'].includes(key))))
|
|
166
|
+
throw new Error('native MariaDB server PID-file receipt is invalid');
|
|
167
|
+
return row;
|
|
168
|
+
}));
|
|
169
|
+
const birth = read(spawnReceiptPath(ownerFile(root)), text => {
|
|
170
|
+
const row = JSON.parse(text);
|
|
171
|
+
if (!Number.isSafeInteger(row.pid) || row.pid <= 0 || typeof row.start !== 'string' || !row.start.trim())
|
|
172
|
+
throw new Error('native MariaDB birth token is invalid');
|
|
173
|
+
return row;
|
|
174
|
+
});
|
|
175
|
+
const values = rows.flatMap(row => row.value ? [row.value] : []);
|
|
176
|
+
const errors = [...rows, birth].flatMap(row => row.error ? [row.error] : []);
|
|
177
|
+
if (values.length > 1 && values.some(row => JSON.stringify(row) !== JSON.stringify(values[0])))
|
|
178
|
+
errors.push(new Error('native MariaDB ownership receipts conflict'));
|
|
179
|
+
if (birth.value && values.some(row => row.pid > 0 && row.pid !== birth.value.pid))
|
|
180
|
+
errors.push(new Error('native MariaDB process birth and ownership receipts conflict'));
|
|
181
|
+
return { present: rows.some(row => row.present) || birth.present, owner: values[0], identity: birth.value, values, errors, files };
|
|
182
|
+
}
|
|
183
|
+
function pidFile(owner) {
|
|
184
|
+
const file = join(owner.data, 'server.pid');
|
|
185
|
+
let fd;
|
|
186
|
+
try {
|
|
187
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (error.code === 'ENOENT')
|
|
191
|
+
return false;
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
let text, stat;
|
|
195
|
+
try {
|
|
196
|
+
stat = fstatSync(fd);
|
|
197
|
+
if (!stat.isFile())
|
|
198
|
+
throw new Error('native MariaDB PID file is not a regular file');
|
|
199
|
+
text = readFileSync(fd, 'utf8').trim();
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
closeSync(fd);
|
|
203
|
+
}
|
|
204
|
+
if (!text)
|
|
205
|
+
return false; // the server creates, then writes, its PID file
|
|
206
|
+
if (!/^\d+$/u.test(text) || Number(text) !== owner.pid)
|
|
207
|
+
throw new Error('native MariaDB PID file differs from its recorded child');
|
|
208
|
+
if (owner.pidFile && (stat.dev !== owner.pidFile.dev || stat.ino !== owner.pidFile.ino || stat.ctimeMs !== owner.pidFile.ctimeMs))
|
|
209
|
+
throw new Error('native MariaDB PID file identity changed');
|
|
210
|
+
const current = lstatSync(file);
|
|
211
|
+
if (!current.isFile() || current.dev !== stat.dev || current.ino !== stat.ino || current.ctimeMs !== stat.ctimeMs)
|
|
212
|
+
throw new Error('native MariaDB PID file changed while reading');
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
async function stop(pid, deliver) {
|
|
216
|
+
if (!alive(pid))
|
|
217
|
+
return;
|
|
218
|
+
// Vendor sql/mysqld.cc:3418-3430 handles SIGTERM with break_connect_loop;
|
|
219
|
+
// give the server its shutdown before touching its group.
|
|
220
|
+
const asked = Date.now();
|
|
221
|
+
worldEvent({ event: 'world.service.stop', service: 'mysql', by: 'native MariaDB down', pid, signal: 'SIGTERM', to: 'pid' });
|
|
222
|
+
await deliver('SIGTERM', false);
|
|
223
|
+
while (alive(pid) && Date.now() < asked + STORE_BUDGET_MS)
|
|
224
|
+
await wait(CHECK_MS);
|
|
225
|
+
if (alive(pid)) {
|
|
226
|
+
worldEvent({ event: 'world.service.killed', service: 'mysql', by: 'native MariaDB down', pid, signal: 'SIGKILL', to: 'group', afterMs: Date.now() - asked });
|
|
227
|
+
await deliver('SIGKILL', true);
|
|
228
|
+
const deadline = Date.now() + COMMAND_GRACE_MS;
|
|
229
|
+
while (alive(pid) && Date.now() < deadline)
|
|
230
|
+
await wait(CHECK_MS);
|
|
231
|
+
}
|
|
232
|
+
if (alive(pid))
|
|
233
|
+
throw new Error('native MariaDB retirement is unconfirmed; receipts retained');
|
|
234
|
+
}
|
|
235
|
+
/** Tool output is private: bootstrap errors can contain an issued password.
|
|
236
|
+
* Only SELECT's one answer is returned; errors name the retained private log. */
|
|
237
|
+
async function tool(command, args, root, env, identity, deadline, signal) {
|
|
238
|
+
signal?.throwIfAborted();
|
|
239
|
+
const remaining = deadline - Date.now();
|
|
240
|
+
if (remaining <= 0)
|
|
241
|
+
throw new Error('native MariaDB lifecycle deadline elapsed');
|
|
242
|
+
const log = join(root, 'native-mariadb-tools.log'), fd = privateLog(log, identity);
|
|
243
|
+
let child;
|
|
244
|
+
try {
|
|
245
|
+
child = spawn(command, args, { env, ...identity, detached: true, windowsHide: true, stdio: ['ignore', 'pipe', fd] });
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
closeSync(fd);
|
|
249
|
+
}
|
|
250
|
+
const lifetime = commandLifetime(child, signal);
|
|
251
|
+
let stdout = '', spawnError, expired = false;
|
|
252
|
+
child.stdout?.on('data', chunk => { stdout += chunk.toString(); });
|
|
253
|
+
child.on('error', error => { spawnError = error; });
|
|
254
|
+
const receipt = join(root, `native-mariadb-tool-${child.pid ?? 'unspawned'}.json`);
|
|
255
|
+
const timer = setTimeout(() => { expired = true; lifetime.cancel('SIGTERM'); }, remaining);
|
|
256
|
+
let failed, published = [];
|
|
257
|
+
try {
|
|
258
|
+
try {
|
|
259
|
+
if (child.pid) {
|
|
260
|
+
publishProcessReceipt(receipt, JSON.stringify({ pid: child.pid, command: basename(command) }) + '\n');
|
|
261
|
+
published = receiptFiles([receipt]);
|
|
262
|
+
}
|
|
263
|
+
const birth = await lifetime.identity;
|
|
264
|
+
if (birth?.start) {
|
|
265
|
+
publishProcessReceipt(spawnReceiptPath(receipt), JSON.stringify(birth) + '\n');
|
|
266
|
+
published = receiptFiles([receipt, spawnReceiptPath(receipt)]);
|
|
267
|
+
}
|
|
268
|
+
else if (child.pid && alive(child.pid))
|
|
269
|
+
throw new Error('native MariaDB helper birth capture failed');
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
lifetime.cancel('SIGTERM');
|
|
273
|
+
try {
|
|
274
|
+
await lifetime.finish();
|
|
275
|
+
}
|
|
276
|
+
catch (cleanup) {
|
|
277
|
+
throw new AggregateError([error, cleanup], 'native MariaDB helper publication/retirement failed');
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
const result = await lifetime.finish();
|
|
282
|
+
signal?.throwIfAborted();
|
|
283
|
+
if (expired || lifetime.forwarded)
|
|
284
|
+
throw new Error('native MariaDB helper cancelled; private log ' + log);
|
|
285
|
+
if (spawnError)
|
|
286
|
+
throw spawnError;
|
|
287
|
+
return { code: result.code, stdout };
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
failed = error;
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
if (!child.pid || !alive(child.pid)) {
|
|
296
|
+
try {
|
|
297
|
+
removeReceipts(published);
|
|
298
|
+
}
|
|
299
|
+
catch (cleanup) {
|
|
300
|
+
if (failed !== undefined)
|
|
301
|
+
throw new AggregateError([failed, cleanup], 'native MariaDB helper and receipt cleanup failed');
|
|
302
|
+
throw cleanup;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function policy(service) {
|
|
308
|
+
// Secrets remain only in this 0600 data-owned file. No password digest,
|
|
309
|
+
// length or material is written into process receipts or status output.
|
|
310
|
+
return JSON.stringify({ version: 1, implementation: 'mariadb', image: service.image,
|
|
311
|
+
release: service.reference.release, sourceSha256: service.reference.sourceSha256,
|
|
312
|
+
receiptSha256: service.reference.receiptSha256, recipe: service.reference.recipe,
|
|
313
|
+
credentials: service.credentials }) + '\n';
|
|
314
|
+
}
|
|
315
|
+
function safeBootstrapPaths(paths) {
|
|
316
|
+
// Upstream mysql_install_db.sh:193-207,302,353-357 expands unquoted path
|
|
317
|
+
// lists. Refuse paths its source cannot carry, rather than rewriting it.
|
|
318
|
+
if (paths.some(path => /[\s\\`$*?\[\]"']/u.test(path)))
|
|
319
|
+
throw new Error('native MariaDB install-db path-quoting gap');
|
|
320
|
+
}
|
|
321
|
+
async function readiness(service, root, config, env, identity, deadline, signal, startingPrograms) {
|
|
322
|
+
const held = state(root);
|
|
323
|
+
if (held.errors.length || !held.owner || !held.owner.pidFile || !held.identity || existsSync(startingFile(root))
|
|
324
|
+
|| held.identity.pid !== held.owner.pid || !alive(held.owner.pid)
|
|
325
|
+
|| held.owner.receiptSha256 !== service.reference.receiptSha256 || held.owner.release !== service.reference.release
|
|
326
|
+
|| held.owner.port !== service.hostPort)
|
|
327
|
+
return false;
|
|
328
|
+
await verifySpawnIdentity(held.identity);
|
|
329
|
+
if (!pidFile(held.owner) || privateText(join(held.owner.data, '.world-policy.json'), identity) !== policy(service))
|
|
330
|
+
return false;
|
|
331
|
+
// Up authenticated its installed prefix before launching the server. Reuse
|
|
332
|
+
// that same selection within its readiness loop; each exec still enters the
|
|
333
|
+
// kernel admission gate. An independent status call authenticates it afresh.
|
|
334
|
+
const programs = startingPrograms ?? nativeMariaDBSupplier(dirname(config), service.image, service.reference);
|
|
335
|
+
const options = join(root, 'native-mariadb-client.cnf');
|
|
336
|
+
if (privateText(options, identity) !== mariaDBClientOptions(service.credentials, '127.0.0.1', service.hostPort))
|
|
337
|
+
return false;
|
|
338
|
+
const command = mariaDBReadinessCommand(programs, options);
|
|
339
|
+
const answer = await tool(command.command, command.args, root, env, identity, deadline, signal);
|
|
340
|
+
const fields = answer.stdout.trim().split('\t');
|
|
341
|
+
await verifySpawnIdentity(held.identity);
|
|
342
|
+
return answer.code === 0 && fields.length === 4
|
|
343
|
+
&& fields[0].startsWith(service.reference.release + '-MariaDB')
|
|
344
|
+
&& fields[1] === String(service.hostPort) && resolve(fields[2]) === held.owner.data
|
|
345
|
+
&& fields[3] === Buffer.from(service.credentials.user + '@%', 'utf8').toString('hex').toUpperCase()
|
|
346
|
+
&& alive(held.owner.pid) && pidFile(held.owner);
|
|
347
|
+
}
|
|
348
|
+
export async function nativeMariaDBUp(service, root, config, launch) {
|
|
349
|
+
if (process[Symbol.for('tabnode.run.vfs')] === undefined)
|
|
350
|
+
throw new Error('native MariaDB WALI supply requires the attached kernel and its program admission; no host server substitution');
|
|
351
|
+
const env = { ...(launch.env ?? process.env), LC_ALL: 'C' }, identity = runAs();
|
|
352
|
+
const programs = nativeMariaDBSupplier(dirname(config), service.image, service.reference);
|
|
353
|
+
const deadline = Date.now() + STORE_BUDGET_MS, data = dataDirectory(root);
|
|
354
|
+
const previous = state(root);
|
|
355
|
+
if (previous.present || starts.has(root))
|
|
356
|
+
await nativeMariaDBDown(root);
|
|
357
|
+
await retireTools(root);
|
|
358
|
+
searchRoot(root, env, identity);
|
|
359
|
+
const stamp = join(data, '.world-policy.json'), extra = join(root, 'native-mariadb-bootstrap.sql');
|
|
360
|
+
safeBootstrapPaths([programs.basedir, data, extra]);
|
|
361
|
+
if (!existsSync(data))
|
|
362
|
+
mkdirSync(data, { mode: 0o700 });
|
|
363
|
+
if (!lstatSync(data).isDirectory())
|
|
364
|
+
throw new Error('native MariaDB data root must be a regular directory');
|
|
365
|
+
if (existsSync(join(data, 'server.pid')) || existsSync(join(data, 'server.sock')))
|
|
366
|
+
throw new Error('native MariaDB retains a PID/socket without confirmed clean teardown; data retained');
|
|
367
|
+
if (existsSync(stamp)) {
|
|
368
|
+
if (!lstatSync(stamp).isFile() || readFileSync(stamp, 'utf8') !== policy(service))
|
|
369
|
+
throw new Error('native MariaDB provider or issued policy changed; explicit configuration/upgrade required');
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
if (readdirSync(data).length)
|
|
373
|
+
throw new Error('native MariaDB has unstamped or partial data; initialization refused');
|
|
374
|
+
}
|
|
375
|
+
ownData(data, identity);
|
|
376
|
+
if (!existsSync(stamp)) {
|
|
377
|
+
const bootstrapEnv = nativeMariaDBShellEnvironment(env);
|
|
378
|
+
const bootstrap = mariaDBBootstrap(programs, service.credentials, data, extra);
|
|
379
|
+
privateFile(join(data, '.world-initializing.json'), policy(service), identity);
|
|
380
|
+
privateFile(extra, bootstrap.privateSql, identity);
|
|
381
|
+
const initialized = await tool(bootstrap.command, bootstrap.args, root, bootstrapEnv, identity, deadline, launch.signal);
|
|
382
|
+
if (initialized.code !== 0 || !existsSync(join(data, 'mysql')))
|
|
383
|
+
throw new Error('native MariaDB genuine install-db failed; private log/data retained');
|
|
384
|
+
privateFile(stamp, policy(service), identity);
|
|
385
|
+
rmSync(join(data, '.world-initializing.json'));
|
|
386
|
+
rmSync(extra);
|
|
387
|
+
}
|
|
388
|
+
privateFile(join(root, 'native-mariadb-client.cnf'), mariaDBClientOptions(service.credentials, '127.0.0.1', service.hostPort), identity);
|
|
389
|
+
const owner = { pid: 0, data, port: service.hostPort,
|
|
390
|
+
receiptSha256: service.reference.receiptSha256, release: service.reference.release };
|
|
391
|
+
publishProcessReceipt(startingFile(root), JSON.stringify(owner) + '\n');
|
|
392
|
+
let started;
|
|
393
|
+
try {
|
|
394
|
+
launch.signal?.throwIfAborted();
|
|
395
|
+
const fd = privateLog(join(root, 'native-mariadb.log'), identity);
|
|
396
|
+
let child;
|
|
397
|
+
try {
|
|
398
|
+
child = spawn(programs.server, ['--no-defaults', '--basedir=' + programs.basedir, '--datadir=' + data,
|
|
399
|
+
'--bind-address=127.0.0.1', '--port=' + service.hostPort, '--pid-file=' + join(data, 'server.pid'),
|
|
400
|
+
'--socket=' + join(data, 'server.sock')], { env, ...identity, detached: true, windowsHide: true, stdio: ['ignore', fd, fd] });
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
closeSync(fd);
|
|
404
|
+
}
|
|
405
|
+
owner.pid = child.pid ?? 0;
|
|
406
|
+
started = { child, owner, closed: false };
|
|
407
|
+
const observed = started;
|
|
408
|
+
child.on('error', error => { observed.error = error; });
|
|
409
|
+
child.once('close', () => { observed.closed = true; });
|
|
410
|
+
starts.set(root, started);
|
|
411
|
+
// A failed birth capture still retains the actual attempted PID. It is
|
|
412
|
+
// never sufficient for a later numeric signal without its birth token.
|
|
413
|
+
publishProcessReceipt(startingFile(root), JSON.stringify(owner) + '\n');
|
|
414
|
+
const birth = await childIdentity(child);
|
|
415
|
+
if (!child.pid || !birth?.start)
|
|
416
|
+
throw new Error('native MariaDB server birth capture failed');
|
|
417
|
+
started.identity = birth;
|
|
418
|
+
publishProcessReceipt(spawnReceiptPath(ownerFile(root)), JSON.stringify(birth) + '\n');
|
|
419
|
+
publishProcessReceipt(startingFile(root), JSON.stringify(owner) + '\n');
|
|
420
|
+
publishProcessReceipt(ownerFile(root), JSON.stringify(owner) + '\n');
|
|
421
|
+
// Status refuses a starting record. Remove it only after the server's own
|
|
422
|
+
// PID file agrees, then run the same authenticated proof status uses.
|
|
423
|
+
while (!pidFile(owner)) {
|
|
424
|
+
launch.signal?.throwIfAborted();
|
|
425
|
+
if (observed.error || observed.closed || child.exitCode !== null || Date.now() >= deadline)
|
|
426
|
+
throw new Error('native MariaDB ended before its PID file; private log retained');
|
|
427
|
+
await wait(CHECK_MS);
|
|
428
|
+
}
|
|
429
|
+
const stat = lstatSync(join(data, 'server.pid'));
|
|
430
|
+
owner.pidFile = { dev: stat.dev, ino: stat.ino, ctimeMs: stat.ctimeMs };
|
|
431
|
+
publishProcessReceipt(ownerFile(root), JSON.stringify(owner) + '\n');
|
|
432
|
+
rmSync(startingFile(root));
|
|
433
|
+
while (!await readiness(service, root, config, env, identity, deadline, launch.signal, programs)) {
|
|
434
|
+
launch.signal?.throwIfAborted();
|
|
435
|
+
if (observed.error || observed.closed || child.exitCode !== null || Date.now() >= deadline)
|
|
436
|
+
throw new Error('native MariaDB failed authenticated readiness; private log retained');
|
|
437
|
+
await wait(CHECK_MS);
|
|
438
|
+
}
|
|
439
|
+
child.unref();
|
|
440
|
+
launch.note?.('native MariaDB ready: owned PID/start, server PID file and authenticated version/port/datadir');
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
try {
|
|
444
|
+
await nativeMariaDBDown(root);
|
|
445
|
+
}
|
|
446
|
+
catch (cleanup) {
|
|
447
|
+
throw new AggregateError([error, cleanup], 'native MariaDB startup and retirement failed; receipts retained');
|
|
448
|
+
}
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
export async function nativeMariaDBStatus(service, root, config, launch) {
|
|
453
|
+
try {
|
|
454
|
+
return await readiness(service, root, config, launch.env ?? process.env, runAs(), Date.now() + STORE_BUDGET_MS, launch.signal);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return false;
|
|
458
|
+
} // corrupt/unreadable receipts and replaced supply are not-ready
|
|
459
|
+
}
|
|
460
|
+
async function retireTools(root) {
|
|
461
|
+
if (!existsSync(root))
|
|
462
|
+
return;
|
|
463
|
+
const files = new Set(readdirSync(root).filter(name => /^native-mariadb-tool-.*\.json(?:\.spawn\.json)?$/u.test(name))
|
|
464
|
+
.map(name => name.replace(/\.spawn\.json$/u, '')));
|
|
465
|
+
for (const name of files) {
|
|
466
|
+
const file = join(root, name);
|
|
467
|
+
const recorded = receiptFiles([file, spawnReceiptPath(file)]);
|
|
468
|
+
const body = recorded.find(row => row.file === spawnReceiptPath(file));
|
|
469
|
+
if (!body)
|
|
470
|
+
throw new Error('native MariaDB helper ownership uncertain; receipts retained');
|
|
471
|
+
const birth = JSON.parse(body.text);
|
|
472
|
+
await verifySpawnIdentity(birth);
|
|
473
|
+
if (alive(birth.pid)) {
|
|
474
|
+
await signalOwnedProcess(birth, 'SIGTERM', true, 'native MariaDB helper down');
|
|
475
|
+
const deadline = Date.now() + COMMAND_GRACE_MS;
|
|
476
|
+
while (alive(birth.pid) && Date.now() < deadline)
|
|
477
|
+
await wait(CHECK_MS);
|
|
478
|
+
if (alive(birth.pid))
|
|
479
|
+
await signalOwnedProcess(birth, 'SIGKILL', true, 'native MariaDB helper down');
|
|
480
|
+
const confirm = Date.now() + COMMAND_GRACE_MS;
|
|
481
|
+
while (alive(birth.pid) && Date.now() < confirm)
|
|
482
|
+
await wait(CHECK_MS);
|
|
483
|
+
if (alive(birth.pid))
|
|
484
|
+
throw new Error('native MariaDB helper retirement unconfirmed; receipts retained');
|
|
485
|
+
}
|
|
486
|
+
removeReceipts(recorded);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** The held identity is enough to retire even if the supplier/config is gone. */
|
|
490
|
+
export async function nativeMariaDBDown(root) {
|
|
491
|
+
const held = starts.get(root), previous = state(root);
|
|
492
|
+
if (held) {
|
|
493
|
+
held.child.ref();
|
|
494
|
+
await stop(held.owner.pid, async (signal, group) => signalChild(held.child, signal, group, 'native MariaDB down')
|
|
495
|
+
|| (held.identity ? signalOwnedProcess(held.identity, signal, group, 'native MariaDB down') : false));
|
|
496
|
+
}
|
|
497
|
+
else if (previous.identity) {
|
|
498
|
+
await verifySpawnIdentity(previous.identity);
|
|
499
|
+
await stop(previous.identity.pid, (signal, group) => signalOwnedProcess(previous.identity, signal, group, 'native MariaDB down'));
|
|
500
|
+
}
|
|
501
|
+
else if (previous.present)
|
|
502
|
+
throw new Error('native MariaDB ownership uncertain; down retains receipts');
|
|
503
|
+
if (previous.values.some(row => row.pid > 0 && alive(row.pid)))
|
|
504
|
+
throw new Error('native MariaDB conflicting live ownership retained');
|
|
505
|
+
await retireTools(root);
|
|
506
|
+
if (previous.errors.length)
|
|
507
|
+
throw new AggregateError(previous.errors, 'native MariaDB retired with uncertain ownership; receipts retained');
|
|
508
|
+
if (existsSync(join(dataDirectory(root), 'server.pid')) || existsSync(join(dataDirectory(root), 'server.sock')))
|
|
509
|
+
throw new Error('native MariaDB retired with PID/socket left; stopped data/receipts retained');
|
|
510
|
+
removeReceipts(previous.files);
|
|
511
|
+
starts.delete(root);
|
|
512
|
+
}
|
|
@@ -9,15 +9,9 @@ export declare function nativePreparedCluster(worldConfig: string): {
|
|
|
9
9
|
directory: string;
|
|
10
10
|
stamp: string;
|
|
11
11
|
};
|
|
12
|
-
/**
|
|
13
|
-
* `volter-world-infra prepare` for a World whose PostgreSQL is native (architecture, managed infrastructure; ADR
|
|
14
|
-
* 0018's phase): this lifecycle's own steps once, into `<World dir>/.volter-prepared/native-postgres-data`. initdb with
|
|
15
|
-
* up's arguments, the postmaster on the World's declared loopback port, the declared database, and a clean SIGINT stop (its
|
|
16
|
-
* shutdown checkpoint; no postmaster.pid left), then the stamp. Where the World declares runtime.database.prepare, the
|
|
17
|
-
* step runs against that postmaster before its stop, and a step that fails leaves no cluster. The image build runs it with the programs the tab
|
|
18
|
-
* runs, under the tab's kernel, so the cluster is the one a first boot there would write.
|
|
19
|
-
*/
|
|
20
12
|
export declare function nativePrepare(service: InfraService, definition: string, worldConfig: string, launch?: HostLaunch): Promise<string>;
|
|
21
13
|
export declare function nativePostgresUp(services: InfraService[], root: string, definition: string, launch: HostLaunch): Promise<void>;
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
/** The native postmaster's status proof: its ownership known, its held group live, its spawn identity and owner
|
|
15
|
+
* verified, and its port answering. `signal` ends the port's wait with the boot that asks (an up's hand-over). */
|
|
16
|
+
export declare function nativePostgresStatus(root: string, signal?: AbortSignal): Promise<boolean>;
|
|
17
|
+
export declare function nativePostgresDown(root: string, by?: string): Promise<void>;
|