@hraness/sponge 0.2.6 → 0.3.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/README.md +21 -9
- package/bin/sponge.js +8 -7
- package/dist/contracts/index.js +24 -6
- package/dist/contracts/validate.js +106 -29
- package/dist/migrations/008.d.ts +3 -0
- package/dist/migrations/008.js +5 -0
- package/dist/migrations/009.d.ts +3 -0
- package/dist/migrations/009.js +8 -0
- package/dist/migrations/010.d.ts +3 -0
- package/dist/migrations/010.js +11 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +39 -0
- package/dist/src/acquisition/capture.js +87 -28
- package/dist/src/acquisition/cleanup.d.ts +0 -4
- package/dist/src/acquisition/cleanup.js +3 -5
- package/dist/src/acquisition/errors.js +17 -2
- package/dist/src/acquisition/files.d.ts +0 -1
- package/dist/src/acquisition/files.js +14 -11
- package/dist/src/acquisition/index.d.ts +2 -3
- package/dist/src/acquisition/index.js +1 -2
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/normalize.js +57 -18
- package/dist/src/acquisition/pins.js +22 -4
- package/dist/src/acquisition/process.js +67 -29
- package/dist/src/acquisition/runtime.d.ts +5 -7
- package/dist/src/acquisition/runtime.js +129 -68
- package/dist/src/acquisition/verify-helper.js +10 -3
- package/dist/src/annotations/quote.js +40 -12
- package/dist/src/annotations/store.d.ts +0 -1
- package/dist/src/annotations/store.js +66 -14
- package/dist/src/app/article.d.ts +2 -1
- package/dist/src/app/article.js +66 -70
- package/dist/src/app/bundle-files.d.ts +0 -4
- package/dist/src/app/bundle-files.js +11 -14
- package/dist/src/app/markdown.js +18 -6
- package/dist/src/app/source.d.ts +0 -4
- package/dist/src/app/source.js +18 -14
- package/dist/src/cli/arguments.d.ts +2 -8
- package/dist/src/cli/arguments.js +67 -80
- package/dist/src/cli/commands.d.ts +42 -0
- package/dist/src/cli/commands.js +795 -0
- package/dist/src/cli/config.js +12 -9
- package/dist/src/cli/digest.d.ts +1 -1
- package/dist/src/cli/digest.js +38 -16
- package/dist/src/cli/import.js +25 -4
- package/dist/src/cli/indexing.d.ts +2 -0
- package/dist/src/cli/indexing.js +26 -9
- package/dist/src/cli/main.js +47 -386
- package/dist/src/cli/portability.d.ts +0 -2
- package/dist/src/cli/portability.js +13 -8
- package/dist/src/cli/queue.js +12 -8
- package/dist/src/cli/runtimes.d.ts +1 -3
- package/dist/src/cli/runtimes.js +13 -6
- package/dist/src/cli/terminal.js +1 -1
- package/dist/src/cli/worker.d.ts +7 -0
- package/dist/src/cli/worker.js +54 -15
- package/dist/src/derivations/kinds.d.ts +34 -0
- package/dist/src/derivations/kinds.js +14 -0
- package/dist/src/derivations/types.d.ts +48 -0
- package/dist/src/derivations/types.js +1 -0
- package/dist/src/digest/derivation.d.ts +25 -0
- package/dist/src/digest/derivation.js +124 -0
- package/dist/src/digest/editorial.d.ts +9 -46
- package/dist/src/digest/editorial.js +57 -49
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +49 -28
- package/dist/src/digest/pipeline.js +343 -100
- package/dist/src/digest/schema.d.ts +11 -25
- package/dist/src/digest/schema.js +86 -18
- package/dist/src/errors.d.ts +46 -0
- package/dist/src/errors.js +117 -0
- package/dist/src/identity/query-policy.d.ts +0 -5
- package/dist/src/identity/query-policy.js +61 -9
- package/dist/src/identity/uri.d.ts +6 -5
- package/dist/src/identity/uri.js +37 -18
- package/dist/src/identity/visibility.d.ts +11 -1
- package/dist/src/identity/visibility.js +81 -17
- package/dist/src/importers/apply.js +104 -36
- package/dist/src/importers/custody.js +27 -9
- package/dist/src/importers/inspect.js +125 -38
- package/dist/src/importers/jungle-digest.d.ts +3 -6
- package/dist/src/importers/jungle-digest.js +196 -69
- package/dist/src/importers/jungle-store.d.ts +15 -6
- package/dist/src/importers/jungle-store.js +122 -28
- package/dist/src/importers/jungle.d.ts +14 -14
- package/dist/src/importers/jungle.js +136 -31
- package/dist/src/importers/manifest.js +57 -21
- package/dist/src/importers/verify-helper.js +18 -4
- package/dist/src/indexing/image.js +29 -10
- package/dist/src/indexing/index.d.ts +37 -6
- package/dist/src/indexing/index.js +360 -124
- package/dist/src/indexing/jobs.js +156 -47
- package/dist/src/indexing/prune-run.d.ts +2 -0
- package/dist/src/indexing/prune-run.js +69 -26
- package/dist/src/indexing/prune.d.ts +12 -3
- package/dist/src/indexing/prune.js +85 -35
- package/dist/src/indexing/runner.d.ts +2 -1
- package/dist/src/indexing/runner.js +83 -40
- package/dist/src/indexing/upgrade.d.ts +9 -0
- package/dist/src/indexing/upgrade.js +79 -0
- package/dist/src/inference/asr/bridge.js +6 -2
- package/dist/src/inference/asr/protocol.js +109 -19
- package/dist/src/inference/asr/runtime.js +58 -21
- package/dist/src/inference/asr/windows.js +9 -5
- package/dist/src/inference/embedding/index.d.ts +2 -6
- package/dist/src/inference/embedding/index.js +89 -29
- package/dist/src/ingest/attempt.d.ts +29 -0
- package/dist/src/ingest/attempt.js +55 -0
- package/dist/src/media/capture-helper.js +7 -2
- package/dist/src/media/capture.d.ts +2 -1
- package/dist/src/media/capture.js +124 -43
- package/dist/src/media/handler.d.ts +4 -2
- package/dist/src/media/handler.js +88 -70
- package/dist/src/media/transcribe.js +63 -21
- package/dist/src/metadata/article.js +49 -17
- package/dist/src/models/artifacts.d.ts +10 -8
- package/dist/src/models/artifacts.js +237 -106
- package/dist/src/models/catalog.d.ts +2 -5
- package/dist/src/models/catalog.js +94 -25
- package/dist/src/models/download.js +55 -15
- package/dist/src/models/runtime.d.ts +0 -2
- package/dist/src/models/runtime.js +69 -33
- package/dist/src/pdf/capture-helper.js +3 -1
- package/dist/src/pdf/capture.d.ts +2 -1
- package/dist/src/pdf/capture.js +95 -27
- package/dist/src/pdf/errors.js +12 -1
- package/dist/src/pdf/handler.d.ts +2 -1
- package/dist/src/pdf/handler.js +70 -71
- package/dist/src/pdf/index.d.ts +1 -3
- package/dist/src/pdf/index.js +0 -2
- package/dist/src/pdf/input.js +12 -7
- package/dist/src/pdf/normalize.js +80 -21
- package/dist/src/pdf/source-identity.js +3 -2
- package/dist/src/pdf/source.js +12 -8
- package/dist/src/pdf/verify-helper.js +15 -3
- package/dist/src/portability/backup.d.ts +2 -9
- package/dist/src/portability/backup.js +106 -22
- package/dist/src/portability/database.js +51 -17
- package/dist/src/portability/export.js +35 -9
- package/dist/src/portability/files.d.ts +1 -4
- package/dist/src/portability/files.js +127 -67
- package/dist/src/portability/gc.js +65 -22
- package/dist/src/publish/component.js +1 -1
- package/dist/src/publish/feed.d.ts +2 -2
- package/dist/src/publish/feed.js +75 -29
- package/dist/src/publish/identity.d.ts +5 -1
- package/dist/src/publish/identity.js +31 -11
- package/dist/src/publish/index.d.ts +2 -3
- package/dist/src/publish/index.js +34 -21
- package/dist/src/publish/projection.d.ts +2 -9
- package/dist/src/publish/projection.js +21 -14
- package/dist/src/search/extension.js +3 -1
- package/dist/src/search/hybrid.d.ts +3 -8
- package/dist/src/search/hybrid.js +45 -11
- package/dist/src/search/lexical.js +18 -4
- package/dist/src/search/query.js +56 -25
- package/dist/src/search/vector.js +37 -10
- package/dist/src/service/launch-agent.d.ts +5 -3
- package/dist/src/service/launch-agent.js +272 -83
- package/dist/src/service/lifetime.js +47 -14
- package/dist/src/service/loop.d.ts +3 -3
- package/dist/src/service/loop.js +47 -19
- package/dist/src/store/annotations.d.ts +32 -0
- package/dist/src/store/annotations.js +44 -0
- package/dist/src/store/backups.d.ts +18 -0
- package/dist/src/store/backups.js +110 -0
- package/dist/src/store/blobs.d.ts +24 -0
- package/dist/src/store/blobs.js +139 -0
- package/dist/src/store/changes.d.ts +5 -12
- package/dist/src/store/changes.js +108 -56
- package/dist/src/store/context.d.ts +15 -0
- package/dist/src/store/context.js +19 -0
- package/dist/src/store/derivations.d.ts +42 -0
- package/dist/src/store/derivations.js +104 -0
- package/dist/src/store/files.d.ts +0 -2
- package/dist/src/store/files.js +19 -24
- package/dist/src/store/ids.d.ts +30 -0
- package/dist/src/store/ids.js +34 -0
- package/dist/src/store/imports.d.ts +26 -0
- package/dist/src/store/imports.js +57 -0
- package/dist/src/store/index.d.ts +3 -186
- package/dist/src/store/index.js +2 -809
- package/dist/src/store/lease.d.ts +20 -0
- package/dist/src/store/lease.js +62 -0
- package/dist/src/store/library.d.ts +75 -0
- package/dist/src/store/library.js +174 -0
- package/dist/src/store/open.d.ts +7 -0
- package/dist/src/store/open.js +183 -0
- package/dist/src/store/queue.d.ts +65 -0
- package/dist/src/store/queue.js +327 -0
- package/dist/src/store/revisions.d.ts +12 -0
- package/dist/src/store/revisions.js +189 -0
- package/dist/src/store/search-index.d.ts +63 -0
- package/dist/src/store/search-index.js +190 -0
- package/dist/src/store/sync-ready.d.ts +6 -0
- package/dist/src/store/sync-ready.js +121 -0
- package/dist/src/store/types.d.ts +1 -5
- package/dist/src/store/types.js +1 -8
- package/dist/src/util/canonical.d.ts +2 -0
- package/dist/src/util/canonical.js +17 -0
- package/dist/src/util/exact.d.ts +2 -0
- package/dist/src/util/exact.js +9 -0
- package/dist/src/util/fsync.d.ts +4 -0
- package/dist/src/util/fsync.js +22 -0
- package/dist/src/util/hash.d.ts +4 -0
- package/dist/src/util/hash.js +8 -0
- package/dist/src/worker/basic.d.ts +0 -1
- package/dist/src/worker/basic.js +23 -7
- package/dist/src/worker/custody.d.ts +4 -1
- package/dist/src/worker/custody.js +111 -38
- package/dist/src/worker/engine.d.ts +2 -1
- package/dist/src/worker/engine.js +42 -20
- package/dist/src/worker/identity.js +13 -5
- package/dist/src/worker/journal.d.ts +1 -1
- package/dist/src/worker/journal.js +54 -24
- package/dist/src/worker/lock.d.ts +18 -0
- package/dist/src/worker/lock.js +196 -0
- package/dist/src/worker/runtime-gc.d.ts +4 -3
- package/dist/src/worker/runtime-gc.js +68 -41
- package/dist/src/worker/supervisor-child.js +19 -6
- package/dist/src/worker/supervisor.js +41 -8
- package/native/runtime-manifest.json +4 -4
- package/package.json +10 -2
- package/dist/src/digest/store.d.ts +0 -57
- package/dist/src/digest/store.js +0 -79
- package/dist/src/store/foundation.d.ts +0 -7
- package/dist/src/store/foundation.js +0 -7
|
@@ -4,86 +4,201 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { join, dirname, resolve } from 'node:path';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { realpathSync, readdirSync, lstatSync, readFileSync, existsSync, unlinkSync } from 'node:fs';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { safeDirectory, regularFile } from '../store/files.js';
|
|
8
|
+
import { sha256 } from '../util/hash.js';
|
|
9
|
+
import { syncDirectory } from '../util/fsync.js';
|
|
9
10
|
import { describe, exact, readExact, openRead, writePrivateExclusive } from '../portability/files.js';
|
|
10
11
|
import { fstatSync, closeSync } from 'node:fs';
|
|
11
12
|
import { identityAlive } from '../worker/identity.js';
|
|
12
13
|
import { recoverServiceLifetime } from './lifetime.js';
|
|
14
|
+
import { SpongeError } from '../errors.js';
|
|
13
15
|
const exec = promisify(execFile);
|
|
14
|
-
export function serviceEnvironment(version) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
export function serviceEnvironment(version) {
|
|
17
|
+
return {
|
|
18
|
+
platform: process.platform,
|
|
19
|
+
uid: process.getuid?.() ?? -1,
|
|
20
|
+
home: realpathSync(homedir()),
|
|
21
|
+
node: realpathSync(process.execPath),
|
|
22
|
+
cli: realpathSync(process.argv[1]),
|
|
23
|
+
version,
|
|
24
|
+
launchctl: async (args) => {
|
|
25
|
+
try {
|
|
26
|
+
const r = await exec('/bin/launchctl', args, {
|
|
27
|
+
timeout: 30000,
|
|
28
|
+
maxBuffer: 65536,
|
|
29
|
+
env: { HOME: homedir(), PATH: '/usr/bin:/bin', LANG: 'en_US.UTF-8' },
|
|
30
|
+
});
|
|
31
|
+
return { code: 0, stdout: r.stdout };
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const e = error;
|
|
35
|
+
if (typeof e.code === 'number')
|
|
36
|
+
return { code: e.code, stdout: e.stdout ?? '' };
|
|
37
|
+
throw new SpongeError('unavailable');
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
alive: identityAlive,
|
|
41
|
+
pause: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function paths(store, env) {
|
|
45
|
+
if (env.platform !== 'darwin')
|
|
46
|
+
throw new SpongeError('unavailable');
|
|
47
|
+
if (!Number.isSafeInteger(env.uid) || env.uid <= 0)
|
|
48
|
+
throw new SpongeError('invalid-input');
|
|
49
|
+
const root = join(store.root, 'runtime', 'service'), agents = join(env.home, 'Library', 'LaunchAgents');
|
|
50
|
+
const label = 'computer.sponge.worker.' + sha256(store.root).slice(0, 20);
|
|
51
|
+
return {
|
|
52
|
+
root,
|
|
53
|
+
agents,
|
|
54
|
+
label,
|
|
55
|
+
log: join(root, 'launchd.log'),
|
|
56
|
+
record: join(root, 'installation.json'),
|
|
57
|
+
plist: join(agents, label + '.plist'),
|
|
58
|
+
target: `gui/${env.uid}/${label}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function readJson(path) {
|
|
62
|
+
const fd = openRead(path);
|
|
63
|
+
try {
|
|
64
|
+
const size = fstatSync(fd).size;
|
|
65
|
+
if (size > 4 * 1024 * 1024)
|
|
66
|
+
throw new SpongeError('limit-exceeded');
|
|
67
|
+
return JSON.parse(readExact(fd, size).toString());
|
|
17
68
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
throw new StoreError('unavailable');
|
|
23
|
-
} }, alive: identityAlive, pause: ms => new Promise(r => setTimeout(r, ms)) }; }
|
|
24
|
-
function paths(store, env) { if (env.platform !== 'darwin')
|
|
25
|
-
throw new StoreError('unavailable'); if (!Number.isSafeInteger(env.uid) || env.uid <= 0)
|
|
26
|
-
throw new StoreError('invalid-input'); const root = join(store.root, 'runtime', 'service'), agents = join(env.home, 'Library', 'LaunchAgents'); const label = 'computer.sponge.worker.' + digest(store.root).slice(0, 20); return { root, agents, label, record: join(root, 'installation.json'), plist: join(agents, label + '.plist'), target: `gui/${env.uid}/${label}` }; }
|
|
27
|
-
function readJson(path) { const fd = openRead(path); try {
|
|
28
|
-
const size = fstatSync(fd).size;
|
|
29
|
-
if (size > 4 * 1024 * 1024)
|
|
30
|
-
throw new StoreError('limit-exceeded');
|
|
31
|
-
return JSON.parse(readExact(fd, size).toString());
|
|
32
|
-
}
|
|
33
|
-
finally {
|
|
34
|
-
closeSync(fd);
|
|
35
|
-
} }
|
|
69
|
+
finally {
|
|
70
|
+
closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
36
73
|
function installation(store, env) {
|
|
37
74
|
const p = paths(store, env);
|
|
38
75
|
if (!existsSync(p.record))
|
|
39
76
|
return null;
|
|
40
|
-
const r = exact(readJson(p.record), [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
const r = exact(readJson(p.record), [
|
|
78
|
+
'language',
|
|
79
|
+
'schemaVersion',
|
|
80
|
+
'id',
|
|
81
|
+
'label',
|
|
82
|
+
'uid',
|
|
83
|
+
'version',
|
|
84
|
+
'node',
|
|
85
|
+
'cli',
|
|
86
|
+
'library',
|
|
87
|
+
'plist',
|
|
88
|
+
'plistHash',
|
|
89
|
+
'launcher',
|
|
90
|
+
'launcherHash',
|
|
91
|
+
'pins',
|
|
92
|
+
]);
|
|
93
|
+
if ((r.language !== null && r.language !== 'en') ||
|
|
94
|
+
r.schemaVersion !== 1 ||
|
|
95
|
+
typeof r.id !== 'string' ||
|
|
96
|
+
!/^[a-f0-9-]{36}$/u.test(r.id) ||
|
|
97
|
+
r.label !== p.label ||
|
|
98
|
+
r.uid !== env.uid ||
|
|
99
|
+
r.library !== store.root ||
|
|
100
|
+
r.plist !== p.plist ||
|
|
101
|
+
typeof r.version !== 'string' ||
|
|
102
|
+
!/^\d+\.\d+\.\d+$/u.test(r.version) ||
|
|
103
|
+
typeof r.node !== 'string' ||
|
|
104
|
+
typeof r.cli !== 'string' ||
|
|
105
|
+
typeof r.launcher !== 'string' ||
|
|
106
|
+
typeof r.plistHash !== 'string' ||
|
|
107
|
+
typeof r.launcherHash !== 'string' ||
|
|
108
|
+
!Array.isArray(r.pins) ||
|
|
109
|
+
r.pins.length > 10000)
|
|
110
|
+
throw new SpongeError('integrity');
|
|
111
|
+
if (r.launcher !== join(p.root, 'launchers', r.version + '-' + r.id, 'run.mjs') ||
|
|
112
|
+
!/^\/[\s\S]+/u.test(r.node) ||
|
|
113
|
+
!/^\/[\s\S]+/u.test(r.cli))
|
|
114
|
+
throw new SpongeError('integrity');
|
|
45
115
|
for (const pin of r.pins) {
|
|
46
116
|
const f = exact(pin, ['path', 'bytes', 'sha256']);
|
|
47
|
-
if (typeof f.path !== 'string' ||
|
|
48
|
-
|
|
117
|
+
if (typeof f.path !== 'string' ||
|
|
118
|
+
resolve(f.path) !== f.path ||
|
|
119
|
+
typeof f.bytes !== 'number' ||
|
|
120
|
+
!Number.isSafeInteger(f.bytes) ||
|
|
121
|
+
f.bytes < 0 ||
|
|
122
|
+
typeof f.sha256 !== 'string' ||
|
|
123
|
+
!/^[a-f0-9]{64}$/u.test(f.sha256))
|
|
124
|
+
throw new SpongeError('integrity');
|
|
49
125
|
}
|
|
50
126
|
return r;
|
|
51
127
|
}
|
|
52
|
-
function verifyOwnedFiles(record) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
128
|
+
function verifyOwnedFiles(record) {
|
|
129
|
+
for (const [path, sha] of [
|
|
130
|
+
[record.plist, record.plistHash],
|
|
131
|
+
[record.launcher, record.launcherHash],
|
|
132
|
+
]) {
|
|
133
|
+
if (describe(path, path, 4 * 1024 * 1024).sha256 !== sha)
|
|
134
|
+
throw new SpongeError('integrity');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function pinCurrent(p) {
|
|
138
|
+
try {
|
|
139
|
+
const r = describe(p.path, p.path, 512 * 1024 ** 2);
|
|
140
|
+
return r.bytes === p.bytes && r.sha256 === p.sha256;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function current(record) {
|
|
147
|
+
return record.pins.every(pinCurrent);
|
|
148
|
+
}
|
|
149
|
+
/** Why the registered worker no longer matches this installed Sponge. Any entry means it cannot (or should not) start;
|
|
150
|
+
* `worker install` repairs every one of them by reinstalling from the current package. */
|
|
151
|
+
function drift(record, env, log) {
|
|
152
|
+
const result = new Set();
|
|
153
|
+
for (const pin of record.pins)
|
|
154
|
+
if (!pinCurrent(pin))
|
|
155
|
+
result.add(pin.path === record.node ? 'node-changed' : 'package-changed');
|
|
156
|
+
if (record.version !== env.version)
|
|
157
|
+
result.add('version-changed');
|
|
158
|
+
if (record.cli !== env.cli)
|
|
159
|
+
result.add('cli-moved');
|
|
160
|
+
if (sha256(agentPlist({ ...record, log })) !== record.plistHash)
|
|
161
|
+
result.add('plist-outdated');
|
|
162
|
+
return [...result];
|
|
163
|
+
}
|
|
164
|
+
const driftRepair = 'Run sponge worker install to reinstall the worker for the current installed package and Node.';
|
|
165
|
+
function xml(value) {
|
|
166
|
+
return value
|
|
167
|
+
.replaceAll('&', '&')
|
|
168
|
+
.replaceAll('<', '<')
|
|
169
|
+
.replaceAll('>', '>')
|
|
170
|
+
.replaceAll('"', '"')
|
|
171
|
+
.replaceAll("'", ''');
|
|
172
|
+
}
|
|
173
|
+
export function agentPlist(record) {
|
|
174
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${xml(record.label)}</string><key>ProgramArguments</key><array><string>${xml(record.node)}</string><string>${xml(record.launcher)}</string></array><key>RunAtLoad</key><true/><key>StartInterval</key><integer>30</integer><key>ProcessType</key><string>Background</string><key>ExitTimeOut</key><integer>60</integer><key>Umask</key><integer>63</integer><key>EnvironmentVariables</key><dict><key>PATH</key><string>/usr/bin:/bin</string><key>LANG</key><string>en_US.UTF-8</string><key>HRANESS_NO_UPDATE</key><string>1</string></dict><key>StandardOutPath</key><string>${xml(record.log)}</string><key>StandardErrorPath</key><string>${xml(record.log)}</string></dict></plist>\n`;
|
|
175
|
+
}
|
|
64
176
|
function pinPackage(env) {
|
|
65
177
|
const cli = realpathSync(env.cli), root = dirname(dirname(cli));
|
|
66
178
|
if (cli !== join(root, 'bin', 'sponge.js'))
|
|
67
|
-
throw new
|
|
179
|
+
throw new SpongeError('invalid-input');
|
|
68
180
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
69
181
|
if (pkg.name !== '@hraness/sponge' || pkg.version !== env.version || !/^\d+\.\d+\.\d+$/u.test(env.version))
|
|
70
|
-
throw new
|
|
182
|
+
throw new SpongeError('invalid-input');
|
|
71
183
|
const paths = [env.node, cli, join(root, 'package.json')];
|
|
72
184
|
let total = 0;
|
|
73
|
-
const visit = (directory) => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
185
|
+
const visit = (directory) => {
|
|
186
|
+
safeDirectory(directory);
|
|
187
|
+
for (const name of readdirSync(directory).sort()) {
|
|
188
|
+
const path = join(directory, name), stat = lstatSync(path);
|
|
189
|
+
if (stat.isSymbolicLink())
|
|
190
|
+
throw new SpongeError('integrity');
|
|
191
|
+
if (stat.isDirectory())
|
|
192
|
+
visit(path);
|
|
193
|
+
else {
|
|
194
|
+
regularFile(path);
|
|
195
|
+
if (paths.length >= 10000 || total + stat.size > 64 * 1024 * 1024)
|
|
196
|
+
throw new SpongeError('limit-exceeded');
|
|
197
|
+
total += stat.size;
|
|
198
|
+
paths.push(path);
|
|
199
|
+
}
|
|
85
200
|
}
|
|
86
|
-
}
|
|
201
|
+
};
|
|
87
202
|
visit(join(root, 'dist'));
|
|
88
203
|
const catalog = join(root, 'native', 'runtime-manifest.json');
|
|
89
204
|
let catalogPresent = false;
|
|
@@ -99,44 +214,98 @@ function pinPackage(env) {
|
|
|
99
214
|
safeDirectory(dirname(catalog));
|
|
100
215
|
regularFile(catalog);
|
|
101
216
|
if (lstatSync(catalog).size > 16384 || paths.length >= 10000)
|
|
102
|
-
throw new
|
|
217
|
+
throw new SpongeError('limit-exceeded');
|
|
103
218
|
paths.push(catalog);
|
|
104
219
|
}
|
|
105
|
-
return paths.map(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
220
|
+
return paths.map((path) => {
|
|
221
|
+
const record = describe(path, path, 512 * 1024 ** 2);
|
|
222
|
+
return { path, bytes: record.bytes, sha256: record.sha256 };
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function launcher(record) {
|
|
226
|
+
return `// Sponge pinned service launcher. Reinstall the worker after upgrading its package.\nimport {openSync,readSync,closeSync,fstatSync,lstatSync,renameSync,constants} from 'node:fs';\nimport {createHash} from 'node:crypto';\nimport {pathToFileURL} from 'node:url';\nconst log=${JSON.stringify(record.log)};\ntry{const s=lstatSync(log);if(s.isFile()&&s.size>=1048576)renameSync(log,log+'.1');}catch{}\nconst pins=${JSON.stringify(record.pins)};\nfor(const p of pins){let fd;try{fd=openSync(p.path,constants.O_RDONLY|constants.O_NOFOLLOW);const s=fstatSync(fd);if(!s.isFile()||s.nlink!==1||s.size!==p.bytes)throw Error();const h=createHash('sha256'),b=Buffer.alloc(65536);let n;while((n=readSync(fd,b,0,b.length,null)))h.update(b.subarray(0,n));if(h.digest('hex')!==p.sha256)throw Error();}catch{process.stderr.write(new Date().toISOString()+' Sponge worker installation changed: '+(p===pins[0]?'Node executable':'package files')+'. Run sponge worker install to repair.\\n');process.exit(78);}finally{if(fd!==undefined)closeSync(fd);}}\nprocess.env.SPONGE_SERVICE_INSTALLATION=${JSON.stringify(record.id)};process.env.HRANESS_NO_UPDATE='1';process.argv=[process.execPath,${JSON.stringify(record.cli)},'--library',${JSON.stringify(record.library)},'worker','serve',...${JSON.stringify(record.language ? ['--language', record.language] : [])}];\nawait import(pathToFileURL(${JSON.stringify(record.cli)}).href);\n`;
|
|
227
|
+
}
|
|
228
|
+
async function loaded(env, target) {
|
|
229
|
+
const result = await env.launchctl(['print', target]);
|
|
230
|
+
if (result.code === 0)
|
|
231
|
+
return true;
|
|
232
|
+
if (result.code === 113)
|
|
233
|
+
return false;
|
|
234
|
+
throw new SpongeError('unavailable');
|
|
235
|
+
}
|
|
236
|
+
async function launchState(env, target) {
|
|
237
|
+
const result = await env.launchctl(['print', target]);
|
|
238
|
+
if (result.code === 113)
|
|
239
|
+
return { loaded: false };
|
|
240
|
+
if (result.code !== 0)
|
|
241
|
+
throw new SpongeError('unavailable');
|
|
242
|
+
const exit = /^\s*last exit code = (-?\d{1,5})\b/mu.exec(result.stdout);
|
|
243
|
+
return { loaded: true, ...(exit ? { lastExit: Number(exit[1]) } : {}) };
|
|
244
|
+
}
|
|
245
|
+
export async function serviceStatus(store, env) {
|
|
246
|
+
const p = paths(store, env), record = installation(store, env);
|
|
247
|
+
if (!record)
|
|
248
|
+
return { installed: false };
|
|
249
|
+
verifyOwnedFiles(record);
|
|
250
|
+
const found = drift(record, env, p.log);
|
|
251
|
+
return {
|
|
252
|
+
installed: true,
|
|
253
|
+
...(await launchState(env, p.target)),
|
|
254
|
+
version: record.version,
|
|
255
|
+
language: record.language,
|
|
256
|
+
stale: found.length > 0,
|
|
257
|
+
...(found.length ? { drift: found, repair: driftRepair } : {}),
|
|
258
|
+
log: p.log,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
113
261
|
export async function installService(store, env, options = {}) {
|
|
114
262
|
if (options.language !== undefined && options.language !== 'en')
|
|
115
|
-
throw new
|
|
263
|
+
throw new SpongeError('invalid-input');
|
|
116
264
|
const p = paths(store, env);
|
|
117
265
|
safeDirectory(p.root, true);
|
|
118
266
|
const previous = installation(store, env);
|
|
267
|
+
// Idempotent: a current registration is kept (and reloaded if it was booted out); a drifted one is uninstalled through
|
|
268
|
+
// the verified path (a foreign plist still fails closed) and installed again from the current package and Node.
|
|
119
269
|
if (previous) {
|
|
120
270
|
if (previous.language !== (options.language ?? null))
|
|
121
|
-
throw new
|
|
122
|
-
|
|
271
|
+
throw new SpongeError('invalid-input');
|
|
272
|
+
verifyOwnedFiles(previous);
|
|
273
|
+
if (!drift(previous, env, p.log).length) {
|
|
274
|
+
if (!(await loaded(env, p.target)) && (await env.launchctl(['bootstrap', `gui/${env.uid}`, p.plist])).code !== 0)
|
|
275
|
+
throw new SpongeError('unavailable');
|
|
276
|
+
return serviceStatus(store, env);
|
|
277
|
+
}
|
|
278
|
+
await uninstallService(store, env);
|
|
123
279
|
}
|
|
124
280
|
if (await loaded(env, p.target))
|
|
125
|
-
throw new
|
|
281
|
+
throw new SpongeError('owner-busy');
|
|
126
282
|
safeDirectory(p.agents, true);
|
|
127
283
|
if (existsSync(p.plist))
|
|
128
|
-
throw new
|
|
284
|
+
throw new SpongeError('integrity');
|
|
129
285
|
const id = randomUUID(), pins = pinPackage(env), launcherPath = join(p.root, 'launchers', env.version + '-' + id, 'run.mjs');
|
|
130
286
|
safeDirectory(dirname(launcherPath), true);
|
|
131
|
-
const base = {
|
|
132
|
-
|
|
287
|
+
const base = {
|
|
288
|
+
language: options.language ?? null,
|
|
289
|
+
schemaVersion: 1,
|
|
290
|
+
id,
|
|
291
|
+
label: p.label,
|
|
292
|
+
uid: env.uid,
|
|
293
|
+
version: env.version,
|
|
294
|
+
node: env.node,
|
|
295
|
+
cli: env.cli,
|
|
296
|
+
library: store.root,
|
|
297
|
+
plist: p.plist,
|
|
298
|
+
launcher: launcherPath,
|
|
299
|
+
pins,
|
|
300
|
+
};
|
|
301
|
+
const code = launcher({ ...base, log: p.log }), plist = agentPlist({ ...base, log: p.log });
|
|
133
302
|
writePrivateExclusive(launcherPath, code);
|
|
134
303
|
writePrivateExclusive(p.plist, plist);
|
|
135
|
-
const record = { ...base, launcherHash:
|
|
304
|
+
const record = { ...base, launcherHash: sha256(code), plistHash: sha256(plist) };
|
|
136
305
|
writePrivateExclusive(p.record, JSON.stringify(record) + '\n');
|
|
137
306
|
const result = await env.launchctl(['bootstrap', `gui/${env.uid}`, p.plist]);
|
|
138
307
|
if (result.code !== 0)
|
|
139
|
-
throw new
|
|
308
|
+
throw new SpongeError('unavailable');
|
|
140
309
|
return serviceStatus(store, env);
|
|
141
310
|
}
|
|
142
311
|
export async function uninstallService(store, env) {
|
|
@@ -147,18 +316,32 @@ export async function uninstallService(store, env) {
|
|
|
147
316
|
if (await loaded(env, p.target)) {
|
|
148
317
|
const result = await env.launchctl(['bootout', p.target]);
|
|
149
318
|
if (result.code !== 0 && result.code !== 113)
|
|
150
|
-
throw new
|
|
319
|
+
throw new SpongeError('unavailable');
|
|
151
320
|
}
|
|
152
321
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
153
322
|
let alive = false;
|
|
154
323
|
const run = join(p.root, 'run.json');
|
|
155
324
|
if (existsSync(run)) {
|
|
156
325
|
const r = exact(readJson(run), ['schemaVersion', 'installationId', 'runId', 'identity', 'state']), identity = exact(r.identity, ['pid', 'uid', 'started']);
|
|
157
|
-
if (r.schemaVersion !== 1 ||
|
|
158
|
-
|
|
326
|
+
if (r.schemaVersion !== 1 ||
|
|
327
|
+
r.installationId !== record.id ||
|
|
328
|
+
typeof r.runId !== 'string' ||
|
|
329
|
+
!/^[a-f0-9-]{36}$/u.test(r.runId) ||
|
|
330
|
+
!['running', 'stopped', 'cleanup-required'].includes(String(r.state)) ||
|
|
331
|
+
!Number.isSafeInteger(identity.pid) ||
|
|
332
|
+
Number(identity.pid) <= 1 ||
|
|
333
|
+
identity.uid !== env.uid ||
|
|
334
|
+
typeof identity.started !== 'string' ||
|
|
335
|
+
!identity.started ||
|
|
336
|
+
identity.started.length > 256)
|
|
337
|
+
throw new SpongeError('integrity');
|
|
159
338
|
alive = await env.alive(identity);
|
|
160
339
|
}
|
|
161
|
-
if (!await loaded(env, p.target)
|
|
340
|
+
if (!(await loaded(env, p.target)) &&
|
|
341
|
+
!alive &&
|
|
342
|
+
store.queue.ownerSnapshot() === null &&
|
|
343
|
+
store.queue.pendingCleanup() === 0 &&
|
|
344
|
+
(await recoverServiceLifetime(store, record.id, env.alive))) {
|
|
162
345
|
verifyOwnedFiles(record);
|
|
163
346
|
unlinkSync(record.plist);
|
|
164
347
|
unlinkSync(record.launcher);
|
|
@@ -172,8 +355,14 @@ export async function uninstallService(store, env) {
|
|
|
172
355
|
}
|
|
173
356
|
await env.pause(100);
|
|
174
357
|
}
|
|
175
|
-
throw Object.assign(new
|
|
358
|
+
throw Object.assign(new SpongeError('owner-busy'), { reason: 'worker-cleanup-required' });
|
|
359
|
+
}
|
|
360
|
+
export function installedServiceId(store, env) {
|
|
361
|
+
const record = installation(store, env);
|
|
362
|
+
if (!record || process.env.SPONGE_SERVICE_INSTALLATION !== record.id)
|
|
363
|
+
throw new SpongeError('invalid-input');
|
|
364
|
+
verifyOwnedFiles(record);
|
|
365
|
+
if (!current(record))
|
|
366
|
+
throw new SpongeError('unavailable');
|
|
367
|
+
return record.id;
|
|
176
368
|
}
|
|
177
|
-
export function installedServiceId(store, env) { const record = installation(store, env); if (!record || process.env.SPONGE_SERVICE_INSTALLATION !== record.id)
|
|
178
|
-
throw new StoreError('invalid-input'); verifyOwnedFiles(record); if (!current(record))
|
|
179
|
-
throw new StoreError('unavailable'); return record.id; }
|
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { lstat, link, unlink } from 'node:fs/promises';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { readJournal, writeJournal
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
import { readJournal, writeJournal } from '../worker/journal.js';
|
|
5
|
+
import { syncDirectoryAsync } from '../util/fsync.js';
|
|
6
|
+
import { exactKeys } from '../util/exact.js';
|
|
7
|
+
import { SpongeError } from '../errors.js';
|
|
8
|
+
const pending = () => new SpongeError('owner-busy');
|
|
9
|
+
function id(value) {
|
|
10
|
+
return typeof value === 'string' && /^[a-f0-9-]{36}$/u.test(value);
|
|
11
|
+
}
|
|
12
|
+
async function same(a, b) {
|
|
13
|
+
const [x, y] = await Promise.all([lstat(a), lstat(b)]).catch((error) => {
|
|
14
|
+
if (error.code === 'ENOENT')
|
|
15
|
+
throw pending();
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
if (!x.isFile() || x.isSymbolicLink() || !y.isFile() || y.isSymbolicLink() || x.dev !== y.dev || x.ino !== y.ino)
|
|
19
|
+
throw pending();
|
|
20
|
+
}
|
|
21
|
+
async function read(path) {
|
|
22
|
+
const busy = () => {
|
|
23
|
+
throw pending();
|
|
24
|
+
}, r = exactKeys(await readJournal(path), ['id', 'identity', 'installationId', 'schemaVersion'], busy), p = exactKeys(r.identity, ['pid', 'started', 'uid'], busy);
|
|
25
|
+
if (r.schemaVersion !== 1 ||
|
|
26
|
+
!id(r.id) ||
|
|
27
|
+
!id(r.installationId) ||
|
|
28
|
+
!Number.isSafeInteger(p.pid) ||
|
|
29
|
+
p.pid <= 1 ||
|
|
30
|
+
!Number.isSafeInteger(p.uid) ||
|
|
31
|
+
p.uid < 0 ||
|
|
32
|
+
typeof p.started !== 'string' ||
|
|
33
|
+
!p.started ||
|
|
34
|
+
p.started.length > 256)
|
|
35
|
+
throw pending();
|
|
36
|
+
return r;
|
|
37
|
+
}
|
|
12
38
|
export async function acquireServiceLifetime(root, installationId, identity) {
|
|
13
39
|
const value = { schemaVersion: 1, id: randomUUID(), installationId, identity }, file = join(root, `lifetime-${value.id}.json`), lock = join(root, 'lifetime.lock');
|
|
14
40
|
await writeJournal(file, value);
|
|
15
41
|
try {
|
|
16
42
|
await link(file, lock);
|
|
17
|
-
await
|
|
43
|
+
await syncDirectoryAsync(root);
|
|
18
44
|
}
|
|
19
45
|
catch (error) {
|
|
20
46
|
await unlink(file);
|
|
@@ -22,7 +48,14 @@ export async function acquireServiceLifetime(root, installationId, identity) {
|
|
|
22
48
|
throw pending();
|
|
23
49
|
throw error;
|
|
24
50
|
}
|
|
25
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
release: async () => {
|
|
53
|
+
await same(file, lock);
|
|
54
|
+
await unlink(lock);
|
|
55
|
+
await unlink(file);
|
|
56
|
+
await syncDirectoryAsync(root);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
26
59
|
}
|
|
27
60
|
/** Called only after bootout or a failed prior run; never signal a recorded PID. */
|
|
28
61
|
export async function recoverServiceLifetime(store, installationId, alive) {
|
|
@@ -36,7 +69,7 @@ export async function recoverServiceLifetime(store, installationId, alive) {
|
|
|
36
69
|
return true;
|
|
37
70
|
throw error;
|
|
38
71
|
}
|
|
39
|
-
if (value.installationId !== installationId || await alive(value.identity))
|
|
72
|
+
if (value.installationId !== installationId || (await alive(value.identity)))
|
|
40
73
|
return false;
|
|
41
74
|
const file = join(root, `lifetime-${value.id}.json`), guard = join(root, 'lifetime-recovery.lock');
|
|
42
75
|
await same(file, lock);
|
|
@@ -50,10 +83,10 @@ export async function recoverServiceLifetime(store, installationId, alive) {
|
|
|
50
83
|
}
|
|
51
84
|
try {
|
|
52
85
|
await same(file, lock);
|
|
53
|
-
if (await alive(value.identity) || store.ownerSnapshot() !== null || store.
|
|
86
|
+
if ((await alive(value.identity)) || store.queue.ownerSnapshot() !== null || store.queue.pendingCleanup() > 0)
|
|
54
87
|
return false;
|
|
55
88
|
await unlink(lock);
|
|
56
|
-
await
|
|
89
|
+
await syncDirectoryAsync(root);
|
|
57
90
|
return true;
|
|
58
91
|
}
|
|
59
92
|
finally {
|
|
@@ -68,6 +101,6 @@ export async function recoverServiceLifetime(store, installationId, alive) {
|
|
|
68
101
|
else
|
|
69
102
|
throw error;
|
|
70
103
|
}
|
|
71
|
-
await
|
|
104
|
+
await syncDirectoryAsync(root);
|
|
72
105
|
}
|
|
73
106
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { LibraryStore } from '../store/index.js';
|
|
2
2
|
import { type ProcessIdentity } from '../worker/identity.js';
|
|
3
3
|
export type ServiceEvent = 'started' | 'idle' | 'drained' | 'busy' | 'repair-required' | 'stopped';
|
|
4
|
-
/** Fixed event vocabulary; source URLs, credentials, content and raw errors never enter logs. */
|
|
5
|
-
export declare function serviceLog(root: string, event: ServiceEvent): void;
|
|
4
|
+
/** Fixed event vocabulary plus a stable error code token; source URLs, credentials, content and raw errors never enter logs. */
|
|
5
|
+
export declare function serviceLog(root: string, event: ServiceEvent, code?: unknown): void;
|
|
6
6
|
export declare function idleWait(ms: number, signal: AbortSignal): Promise<void>;
|
|
7
7
|
export interface ServiceLoopOptions {
|
|
8
8
|
signal: AbortSignal;
|
|
@@ -10,7 +10,7 @@ export interface ServiceLoopOptions {
|
|
|
10
10
|
drain: (signal: AbortSignal) => Promise<readonly unknown[]>;
|
|
11
11
|
idleMs?: number;
|
|
12
12
|
wait?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
13
|
-
log?: (event: ServiceEvent) => void;
|
|
13
|
+
log?: (event: ServiceEvent, code?: unknown) => void;
|
|
14
14
|
onIdle?: () => Promise<void>;
|
|
15
15
|
identity?: () => Promise<ProcessIdentity | null>;
|
|
16
16
|
}
|