@hraness/slopcamera 3.2.8 → 3.3.2
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 +29 -9
- package/apps/desktop/application/context.ts +6 -0
- package/apps/desktop/application/default-registry.ts +26 -0
- package/apps/desktop/application/operation.ts +14 -1
- package/apps/desktop/application/operations/index.ts +4 -0
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +514 -11
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +29 -1
- package/apps/desktop/cli/commands.ts +1188 -4
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +172 -6
- package/apps/desktop/cli/main.ts +3 -3
- package/apps/desktop/cli/portable-surface.ts +6 -1
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +271 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/semantic-builder.ts +24 -0
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +725 -273
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +25 -429
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-gs48afpt.js +7 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index-zhg36tf9.js +3 -0
- package/dist/index.js +1 -226
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +12 -0
- package/package.json +7 -5
- package/skills/slopcamera/SKILL.md +7 -1
- package/skills/slopcamera/references/directed-scenes.md +48 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +4 -0
- package/skills/slopcamera/references/native-studio.md +19 -0
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/scene-building.md +172 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +222 -4
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -0
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1649 -132
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +232 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +108 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +617 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/version.ts +1 -1
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7131pg1c.js +0 -1820
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-fava6pge.js +0 -192
- package/dist/index-h1k0fnjq.js +0 -2363
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-zfnddgay.js +0 -2029
- package/dist/skill-install-dh0ntqba.js +0 -10
package/dist/index-sh6xbav6.js
DELETED
|
@@ -1,1219 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/host-resources.ts
|
|
3
|
-
import { createHash, randomBytes } from "crypto";
|
|
4
|
-
import {
|
|
5
|
-
chmodSync,
|
|
6
|
-
closeSync,
|
|
7
|
-
constants,
|
|
8
|
-
fchmodSync,
|
|
9
|
-
fstatSync,
|
|
10
|
-
ftruncateSync,
|
|
11
|
-
fsyncSync,
|
|
12
|
-
lstatSync,
|
|
13
|
-
mkdirSync,
|
|
14
|
-
openSync,
|
|
15
|
-
readSync,
|
|
16
|
-
readdirSync,
|
|
17
|
-
realpathSync,
|
|
18
|
-
renameSync,
|
|
19
|
-
statSync,
|
|
20
|
-
unlinkSync,
|
|
21
|
-
writeSync
|
|
22
|
-
} from "fs";
|
|
23
|
-
import { availableParallelism, homedir } from "os";
|
|
24
|
-
import { isAbsolute, join } from "path";
|
|
25
|
-
import { performance } from "perf_hooks";
|
|
26
|
-
|
|
27
|
-
// src/host-resource-posix.ts
|
|
28
|
-
import { readFileSync } from "fs";
|
|
29
|
-
import { createRequire } from "module";
|
|
30
|
-
var lockExclusive = 2;
|
|
31
|
-
var lockNonblocking = 4;
|
|
32
|
-
var lockUnlock = 8;
|
|
33
|
-
var interruptedErrno = 4;
|
|
34
|
-
var nativeLocking;
|
|
35
|
-
var nativeLibraries = [];
|
|
36
|
-
var runtimeRequire = createRequire(import.meta.url);
|
|
37
|
-
function bunFfi() {
|
|
38
|
-
return runtimeRequire("bun:ffi");
|
|
39
|
-
}
|
|
40
|
-
function mappedLinuxLibcCandidates(processMaps, architecture = process.arch) {
|
|
41
|
-
const candidates = [];
|
|
42
|
-
const append = (candidate) => {
|
|
43
|
-
if (!candidates.includes(candidate))
|
|
44
|
-
candidates.push(candidate);
|
|
45
|
-
};
|
|
46
|
-
for (const line of processMaps.split(`
|
|
47
|
-
`)) {
|
|
48
|
-
const pathStart = line.indexOf("/");
|
|
49
|
-
if (pathStart < 0 || line.endsWith(" (deleted)"))
|
|
50
|
-
continue;
|
|
51
|
-
const path = line.slice(pathStart).replace(/\\([0-7]{3})/gu, (_match, octal) => String.fromCodePoint(Number.parseInt(octal, 8)));
|
|
52
|
-
if (/\/(?:libc(?:-[^/]+)?\.so(?:\.[0-9]+)*|libc\.musl-[^/]+\.so(?:\.[0-9]+)*|ld-musl-[^/]+\.so(?:\.[0-9]+)*)$/u.test(path)) {
|
|
53
|
-
append(path);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
append("libc.so.6");
|
|
57
|
-
if (architecture === "x64")
|
|
58
|
-
append("/lib/ld-musl-x86_64.so.1");
|
|
59
|
-
if (architecture === "arm64")
|
|
60
|
-
append("/lib/ld-musl-aarch64.so.1");
|
|
61
|
-
return candidates;
|
|
62
|
-
}
|
|
63
|
-
function linuxLibcCandidates() {
|
|
64
|
-
try {
|
|
65
|
-
return mappedLinuxLibcCandidates(readFileSync("/proc/self/maps", "utf8"));
|
|
66
|
-
} catch {
|
|
67
|
-
return mappedLinuxLibcCandidates("");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function initializeNativeLocking() {
|
|
71
|
-
if (nativeLocking !== undefined)
|
|
72
|
-
return nativeLocking;
|
|
73
|
-
const { dlopen, FFIType, read } = bunFfi();
|
|
74
|
-
if (process.platform === "darwin") {
|
|
75
|
-
const library = dlopen("/usr/lib/libSystem.B.dylib", {
|
|
76
|
-
__error: { args: [], returns: FFIType.ptr },
|
|
77
|
-
flock: {
|
|
78
|
-
args: [FFIType.i32, FFIType.i32],
|
|
79
|
-
returns: FFIType.i32
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
nativeLibraries.push(library);
|
|
83
|
-
nativeLocking = {
|
|
84
|
-
errnoLocation: library.symbols.__error,
|
|
85
|
-
flock: library.symbols.flock,
|
|
86
|
-
readInt32: read.i32
|
|
87
|
-
};
|
|
88
|
-
return nativeLocking;
|
|
89
|
-
}
|
|
90
|
-
if (process.platform === "linux") {
|
|
91
|
-
const failures = [];
|
|
92
|
-
for (const candidate of linuxLibcCandidates()) {
|
|
93
|
-
try {
|
|
94
|
-
const library = dlopen(candidate, {
|
|
95
|
-
__errno_location: { args: [], returns: FFIType.ptr },
|
|
96
|
-
flock: {
|
|
97
|
-
args: [FFIType.i32, FFIType.i32],
|
|
98
|
-
returns: FFIType.i32
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
nativeLibraries.push(library);
|
|
102
|
-
nativeLocking = {
|
|
103
|
-
errnoLocation: library.symbols.__errno_location,
|
|
104
|
-
flock: library.symbols.flock,
|
|
105
|
-
readInt32: read.i32
|
|
106
|
-
};
|
|
107
|
-
return nativeLocking;
|
|
108
|
-
} catch (cause) {
|
|
109
|
-
failures.push(`${candidate}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
throw new Error("Slopcamera could not load host flock support" + ` (${failures.join("; ")}).`);
|
|
113
|
-
}
|
|
114
|
-
throw new Error(`Slopcamera host-resource coordination is unsupported on ${process.platform}.`);
|
|
115
|
-
}
|
|
116
|
-
function currentErrno(locking) {
|
|
117
|
-
const pointer = locking.errnoLocation();
|
|
118
|
-
if (pointer === null) {
|
|
119
|
-
throw new Error("Slopcamera could not read the host flock error.");
|
|
120
|
-
}
|
|
121
|
-
return locking.readInt32(pointer);
|
|
122
|
-
}
|
|
123
|
-
function isBusyErrno(errno) {
|
|
124
|
-
return process.platform === "darwin" ? errno === 35 : errno === 11;
|
|
125
|
-
}
|
|
126
|
-
var slopcameraHostResourcePlatforms = Object.freeze([
|
|
127
|
-
"darwin",
|
|
128
|
-
"linux"
|
|
129
|
-
]);
|
|
130
|
-
function isHostResourcePlatformSupported(platform) {
|
|
131
|
-
return slopcameraHostResourcePlatforms.some((candidate) => candidate === platform);
|
|
132
|
-
}
|
|
133
|
-
function tryLockHostResourceDescriptor(descriptor) {
|
|
134
|
-
const locking = initializeNativeLocking();
|
|
135
|
-
for (;; ) {
|
|
136
|
-
if (locking.flock(descriptor, lockExclusive | lockNonblocking) === 0) {
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
const errno = currentErrno(locking);
|
|
140
|
-
if (errno === interruptedErrno)
|
|
141
|
-
continue;
|
|
142
|
-
if (isBusyErrno(errno))
|
|
143
|
-
return false;
|
|
144
|
-
throw new Error(`Slopcamera host-resource flock failed with errno ${errno}.`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function unlockHostResourceDescriptor(descriptor) {
|
|
148
|
-
const locking = initializeNativeLocking();
|
|
149
|
-
for (;; ) {
|
|
150
|
-
if (locking.flock(descriptor, lockUnlock) === 0)
|
|
151
|
-
return;
|
|
152
|
-
const errno = currentErrno(locking);
|
|
153
|
-
if (errno === interruptedErrno)
|
|
154
|
-
continue;
|
|
155
|
-
throw new Error(`Slopcamera host-resource unlock failed with errno ${errno}.`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// src/host-resources.ts
|
|
160
|
-
var profileIdPattern = /^[a-z0-9][a-z0-9._/-]{0,119}$/u;
|
|
161
|
-
var resourceNamePattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
|
|
162
|
-
var ownerIdPattern = /^[0-9a-f]{32}$/u;
|
|
163
|
-
var ticketPattern = /^[1-9][0-9]{0,19}$/u;
|
|
164
|
-
var markerNamePattern = /^lease-([0-9]{20})-([0-9a-f]{32})\.lock$/u;
|
|
165
|
-
var temporaryNamePattern = /^\.host-resources-[0-9a-f]{32}\.tmp$/u;
|
|
166
|
-
var stateFileName = "state.json";
|
|
167
|
-
var controlFileName = "control.lock";
|
|
168
|
-
var initializedFileName = "initialized.lock";
|
|
169
|
-
var maximumResources = 64;
|
|
170
|
-
var maximumProcessLocalProfiles = 64;
|
|
171
|
-
var maximumDirectoryEntries = 1024;
|
|
172
|
-
var maximumFileBytes = 64 * 1024;
|
|
173
|
-
var maximumPathBytes = 4096;
|
|
174
|
-
var maximumResourceAmount = 1e6;
|
|
175
|
-
var maximumTicket = 0xffff_ffff_ffff_ffffn;
|
|
176
|
-
var defaultPollIntervalMilliseconds = 25;
|
|
177
|
-
var defaultWaitTimeoutMilliseconds = 35000;
|
|
178
|
-
var maximumAdmissionBackoffMilliseconds = 250;
|
|
179
|
-
var smallPollIntervalMilliseconds = 4;
|
|
180
|
-
var maximumAdmissionBackoffMultiplier = 8;
|
|
181
|
-
var HOST_RESOURCE_MAX_WAIT_MILLISECONDS = 24 * 60 * 60000;
|
|
182
|
-
var slopcameraHostResourceNames = Object.freeze([
|
|
183
|
-
"cpu",
|
|
184
|
-
"local-io",
|
|
185
|
-
"ffmpeg",
|
|
186
|
-
"video-encode",
|
|
187
|
-
"vision",
|
|
188
|
-
"whisper",
|
|
189
|
-
"network",
|
|
190
|
-
"paid-call",
|
|
191
|
-
"browser",
|
|
192
|
-
"capture-device"
|
|
193
|
-
]);
|
|
194
|
-
|
|
195
|
-
class HostResourceError extends Error {
|
|
196
|
-
code;
|
|
197
|
-
constructor(code, message, cause) {
|
|
198
|
-
super(`[${code}] ${message}`, cause === undefined ? undefined : { cause });
|
|
199
|
-
this.name = "HostResourceError";
|
|
200
|
-
this.code = code;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
function isRecord(value) {
|
|
204
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
205
|
-
}
|
|
206
|
-
function hasExactKeys(value, expected) {
|
|
207
|
-
const actual = Object.keys(value).sort();
|
|
208
|
-
const normalizedExpected = [...expected].sort();
|
|
209
|
-
return actual.length === normalizedExpected.length && actual.every((key, index) => key === normalizedExpected[index]);
|
|
210
|
-
}
|
|
211
|
-
function boundedPositiveInteger(value, maximum) {
|
|
212
|
-
return Number.isSafeInteger(value) && value >= 1 && value <= maximum;
|
|
213
|
-
}
|
|
214
|
-
function deepFreeze(value) {
|
|
215
|
-
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
216
|
-
return value;
|
|
217
|
-
}
|
|
218
|
-
for (const nested of Object.values(value))
|
|
219
|
-
deepFreeze(nested);
|
|
220
|
-
return Object.freeze(value);
|
|
221
|
-
}
|
|
222
|
-
function resourceEntries(value, amountKey, errorCode) {
|
|
223
|
-
const minimumEntries = errorCode === "INVALID_CLAIMS" ? 0 : 1;
|
|
224
|
-
if (!Array.isArray(value) || value.length < minimumEntries || value.length > maximumResources) {
|
|
225
|
-
throw new HostResourceError(errorCode, `Host-resource ${amountKey === "limit" ? "capacities" : "claims"} must contain ${minimumEntries} through ${maximumResources} entries.`);
|
|
226
|
-
}
|
|
227
|
-
const seen = new Set;
|
|
228
|
-
const result = value.map((candidate) => {
|
|
229
|
-
if (!isRecord(candidate) || !hasExactKeys(candidate, ["resource", amountKey]) || typeof candidate.resource !== "string" || !resourceNamePattern.test(candidate.resource) || !boundedPositiveInteger(candidate[amountKey], maximumResourceAmount) || seen.has(candidate.resource)) {
|
|
230
|
-
throw new HostResourceError(errorCode, `Host-resource ${amountKey === "limit" ? "capacities" : "claims"} are malformed or duplicated.`);
|
|
231
|
-
}
|
|
232
|
-
seen.add(candidate.resource);
|
|
233
|
-
return { resource: candidate.resource, value: candidate[amountKey] };
|
|
234
|
-
});
|
|
235
|
-
return result.sort((left, right) => left.resource < right.resource ? -1 : left.resource > right.resource ? 1 : 0);
|
|
236
|
-
}
|
|
237
|
-
function normalizeHostResourceProfile(value) {
|
|
238
|
-
if (!isRecord(value) || !hasExactKeys(value, ["id", "capacities"]) || typeof value.id !== "string" || !profileIdPattern.test(value.id)) {
|
|
239
|
-
throw new HostResourceError("INVALID_PROFILE", "Host-resource profile identity is invalid.");
|
|
240
|
-
}
|
|
241
|
-
const capacities = resourceEntries(value.capacities, "limit", "INVALID_PROFILE").map(({ resource, value: limit }) => ({ resource, limit }));
|
|
242
|
-
return deepFreeze({ id: value.id, capacities });
|
|
243
|
-
}
|
|
244
|
-
function normalizeHostResourceClaims(value, profile) {
|
|
245
|
-
const normalizedProfile = normalizeHostResourceProfile(profile);
|
|
246
|
-
const limits = new Map(normalizedProfile.capacities.map(({ resource, limit }) => [resource, limit]));
|
|
247
|
-
const claims = resourceEntries(value, "amount", "INVALID_CLAIMS").map(({ resource, value: amount }) => {
|
|
248
|
-
const limit = limits.get(resource);
|
|
249
|
-
if (limit === undefined || amount > limit) {
|
|
250
|
-
throw new HostResourceError("INVALID_CLAIMS", `Host-resource claim ${resource} is unavailable or exceeds its profile limit.`);
|
|
251
|
-
}
|
|
252
|
-
return { resource, amount };
|
|
253
|
-
});
|
|
254
|
-
return deepFreeze(claims);
|
|
255
|
-
}
|
|
256
|
-
function defaultSlopcameraHostResourceProfile(hostParallelism = availableParallelism()) {
|
|
257
|
-
if (!boundedPositiveInteger(hostParallelism, maximumResourceAmount)) {
|
|
258
|
-
throw new HostResourceError("INVALID_PROFILE", "Host parallelism must be a positive safe integer.");
|
|
259
|
-
}
|
|
260
|
-
const reserve = hostParallelism >= 6 ? 2 : hostParallelism >= 2 ? 1 : 0;
|
|
261
|
-
return normalizeHostResourceProfile({
|
|
262
|
-
id: "slopcamera.host-resources/v1",
|
|
263
|
-
capacities: [
|
|
264
|
-
{ resource: "cpu", limit: Math.max(1, hostParallelism - reserve) },
|
|
265
|
-
{ resource: "local-io", limit: 2 },
|
|
266
|
-
{ resource: "ffmpeg", limit: 2 },
|
|
267
|
-
{ resource: "video-encode", limit: 1 },
|
|
268
|
-
{ resource: "vision", limit: 1 },
|
|
269
|
-
{ resource: "whisper", limit: 1 },
|
|
270
|
-
{ resource: "network", limit: 4 },
|
|
271
|
-
{ resource: "paid-call", limit: 1 },
|
|
272
|
-
{ resource: "browser", limit: 1 },
|
|
273
|
-
{ resource: "capture-device", limit: 1 }
|
|
274
|
-
]
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
function defaultSlopcameraHostResourceStateRoot(platform = process.platform, environment = process.env, userHome = homedir()) {
|
|
278
|
-
if (!isHostResourcePlatformSupported(platform)) {
|
|
279
|
-
throw new HostResourceError("UNSUPPORTED_PLATFORM", "Slopcamera host-resource coordination requires Darwin or Linux.");
|
|
280
|
-
}
|
|
281
|
-
if (platform === "darwin") {
|
|
282
|
-
return join(userHome, "Library", "Application Support", "Slopcamera", "cli", "host-resources-v1");
|
|
283
|
-
}
|
|
284
|
-
const configuredStateHome = environment.XDG_STATE_HOME;
|
|
285
|
-
const stateHome = configuredStateHome !== undefined && isAbsolute(configuredStateHome) ? configuredStateHome : join(userHome, ".local", "state");
|
|
286
|
-
return join(stateHome, "slopcamera", "host-resources-v1");
|
|
287
|
-
}
|
|
288
|
-
function canonicalProfile(profile) {
|
|
289
|
-
return JSON.stringify(profile);
|
|
290
|
-
}
|
|
291
|
-
function sha256(value) {
|
|
292
|
-
return createHash("sha256").update(value).digest("hex");
|
|
293
|
-
}
|
|
294
|
-
function stateFailure(message, cause) {
|
|
295
|
-
return new HostResourceError("UNSAFE_STATE", message, cause);
|
|
296
|
-
}
|
|
297
|
-
function isErrorCode(value, code) {
|
|
298
|
-
return isRecord(value) && value.code === code;
|
|
299
|
-
}
|
|
300
|
-
function ownerUid() {
|
|
301
|
-
const uid = process.geteuid?.() ?? process.getuid?.();
|
|
302
|
-
if (uid === undefined)
|
|
303
|
-
throw stateFailure("Host-resource owner identity is unavailable.");
|
|
304
|
-
return uid;
|
|
305
|
-
}
|
|
306
|
-
function identity(value) {
|
|
307
|
-
return { device: value.dev, inode: value.ino };
|
|
308
|
-
}
|
|
309
|
-
function sameIdentity(left, right) {
|
|
310
|
-
const device = "device" in right ? right.device : right.dev;
|
|
311
|
-
const inode = "inode" in right ? right.inode : right.ino;
|
|
312
|
-
return left.device === device && left.inode === inode;
|
|
313
|
-
}
|
|
314
|
-
function privateDirectory(metadata) {
|
|
315
|
-
return metadata.isDirectory() && !metadata.isSymbolicLink() && metadata.uid === ownerUid() && (metadata.mode & 511) === 448;
|
|
316
|
-
}
|
|
317
|
-
function privateRegularFile(metadata) {
|
|
318
|
-
return metadata.isFile() && !metadata.isSymbolicLink() && metadata.nlink === 1 && metadata.uid === ownerUid() && (metadata.mode & 511) === 384;
|
|
319
|
-
}
|
|
320
|
-
function metadataOrNull(path) {
|
|
321
|
-
try {
|
|
322
|
-
return lstatSync(path);
|
|
323
|
-
} catch (cause) {
|
|
324
|
-
if (isErrorCode(cause, "ENOENT"))
|
|
325
|
-
return null;
|
|
326
|
-
throw cause;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
function ensureStateRoot(path) {
|
|
330
|
-
if (!isAbsolute(path) || path.includes("\x00") || Buffer.byteLength(path, "utf8") > maximumPathBytes) {
|
|
331
|
-
throw stateFailure("Host-resource state root must be a bounded absolute path.");
|
|
332
|
-
}
|
|
333
|
-
let created = false;
|
|
334
|
-
let metadata = metadataOrNull(path);
|
|
335
|
-
if (metadata === null) {
|
|
336
|
-
try {
|
|
337
|
-
mkdirSync(path, { recursive: true, mode: 448 });
|
|
338
|
-
created = true;
|
|
339
|
-
} catch (cause) {
|
|
340
|
-
if (!isErrorCode(cause, "EEXIST")) {
|
|
341
|
-
throw stateFailure("Host-resource state root could not be created.", cause);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
metadata = lstatSync(path);
|
|
345
|
-
if (created)
|
|
346
|
-
chmodSync(path, 448);
|
|
347
|
-
}
|
|
348
|
-
if (!privateDirectory(metadata)) {
|
|
349
|
-
throw stateFailure("Host-resource state root is not one private owned directory.");
|
|
350
|
-
}
|
|
351
|
-
const canonical = realpathSync(path);
|
|
352
|
-
if (!privateDirectory(statSync(canonical))) {
|
|
353
|
-
throw stateFailure("Host-resource state root canonicalization is unsafe.");
|
|
354
|
-
}
|
|
355
|
-
return { path: canonical };
|
|
356
|
-
}
|
|
357
|
-
function openPrivateFile(path, exclusive = false) {
|
|
358
|
-
const existing = metadataOrNull(path);
|
|
359
|
-
if (existing !== null && !privateRegularFile(existing)) {
|
|
360
|
-
throw stateFailure("Host-resource state contains an unsafe file.");
|
|
361
|
-
}
|
|
362
|
-
const flags = constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW | (exclusive ? constants.O_EXCL : 0);
|
|
363
|
-
const descriptor = openSync(path, flags, 384);
|
|
364
|
-
try {
|
|
365
|
-
fchmodSync(descriptor, 384);
|
|
366
|
-
const opened = fstatSync(descriptor);
|
|
367
|
-
const published = lstatSync(path);
|
|
368
|
-
if (!privateRegularFile(opened) || !privateRegularFile(published) || opened.dev !== published.dev || opened.ino !== published.ino) {
|
|
369
|
-
throw stateFailure("Host-resource file identity changed while opening.");
|
|
370
|
-
}
|
|
371
|
-
return descriptor;
|
|
372
|
-
} catch (cause) {
|
|
373
|
-
closeSync(descriptor);
|
|
374
|
-
throw cause;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
function readDescriptor(descriptor) {
|
|
378
|
-
const metadata = fstatSync(descriptor);
|
|
379
|
-
if (!privateRegularFile(metadata) || metadata.size > maximumFileBytes) {
|
|
380
|
-
throw stateFailure("Host-resource state file is malformed.");
|
|
381
|
-
}
|
|
382
|
-
const bytes = Buffer.alloc(metadata.size);
|
|
383
|
-
let offset = 0;
|
|
384
|
-
while (offset < bytes.length) {
|
|
385
|
-
const count = readSync(descriptor, bytes, offset, bytes.length - offset, offset);
|
|
386
|
-
if (count === 0)
|
|
387
|
-
break;
|
|
388
|
-
offset += count;
|
|
389
|
-
}
|
|
390
|
-
if (offset !== bytes.length)
|
|
391
|
-
throw stateFailure("Host-resource state read was incomplete.");
|
|
392
|
-
return bytes.toString("utf8");
|
|
393
|
-
}
|
|
394
|
-
function writeDescriptor(descriptor, value) {
|
|
395
|
-
const bytes = Buffer.from(value, "utf8");
|
|
396
|
-
if (bytes.length > maximumFileBytes) {
|
|
397
|
-
throw stateFailure("Host-resource state write exceeds its bound.");
|
|
398
|
-
}
|
|
399
|
-
ftruncateSync(descriptor, 0);
|
|
400
|
-
let offset = 0;
|
|
401
|
-
while (offset < bytes.length) {
|
|
402
|
-
const written = writeSync(descriptor, bytes, offset, bytes.length - offset, offset);
|
|
403
|
-
if (written < 1)
|
|
404
|
-
throw stateFailure("Host-resource state write was incomplete.");
|
|
405
|
-
offset += written;
|
|
406
|
-
}
|
|
407
|
-
fsyncSync(descriptor);
|
|
408
|
-
}
|
|
409
|
-
function stateJson(state) {
|
|
410
|
-
return JSON.stringify({
|
|
411
|
-
version: state.version,
|
|
412
|
-
profile: state.profile,
|
|
413
|
-
nextTicket: state.nextTicket
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
function markerJson(marker) {
|
|
417
|
-
return JSON.stringify({
|
|
418
|
-
version: marker.version,
|
|
419
|
-
owner: marker.owner,
|
|
420
|
-
profileSha256: marker.profileSha256,
|
|
421
|
-
ticket: marker.ticket,
|
|
422
|
-
phase: marker.phase,
|
|
423
|
-
claims: marker.claims
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
function syncDirectory(directory) {
|
|
427
|
-
const descriptor = openSync(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
428
|
-
try {
|
|
429
|
-
if (!privateDirectory(fstatSync(descriptor))) {
|
|
430
|
-
throw stateFailure("Host-resource state root changed identity.");
|
|
431
|
-
}
|
|
432
|
-
fsyncSync(descriptor);
|
|
433
|
-
} finally {
|
|
434
|
-
closeSync(descriptor);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
function atomicWriteState(directory, state) {
|
|
438
|
-
const temporaryName = `.host-resources-${randomBytes(16).toString("hex")}.tmp`;
|
|
439
|
-
const temporaryPath = join(directory, temporaryName);
|
|
440
|
-
const statePath = join(directory, stateFileName);
|
|
441
|
-
let descriptor;
|
|
442
|
-
try {
|
|
443
|
-
descriptor = openPrivateFile(temporaryPath, true);
|
|
444
|
-
writeDescriptor(descriptor, stateJson(state));
|
|
445
|
-
closeSync(descriptor);
|
|
446
|
-
descriptor = undefined;
|
|
447
|
-
const existing = metadataOrNull(statePath);
|
|
448
|
-
if (existing !== null && !privateRegularFile(existing)) {
|
|
449
|
-
throw stateFailure("Host-resource profile state is unsafe.");
|
|
450
|
-
}
|
|
451
|
-
renameSync(temporaryPath, statePath);
|
|
452
|
-
const published = lstatSync(statePath);
|
|
453
|
-
if (!privateRegularFile(published)) {
|
|
454
|
-
throw stateFailure("Host-resource profile state was not published safely.");
|
|
455
|
-
}
|
|
456
|
-
syncDirectory(directory);
|
|
457
|
-
} finally {
|
|
458
|
-
if (descriptor !== undefined)
|
|
459
|
-
closeSync(descriptor);
|
|
460
|
-
try {
|
|
461
|
-
unlinkSync(temporaryPath);
|
|
462
|
-
} catch (cause) {
|
|
463
|
-
if (!isErrorCode(cause, "ENOENT")) {}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
function parseState(value) {
|
|
468
|
-
let parsed;
|
|
469
|
-
try {
|
|
470
|
-
parsed = JSON.parse(value);
|
|
471
|
-
} catch (cause) {
|
|
472
|
-
throw stateFailure("Host-resource profile state is not valid JSON.", cause);
|
|
473
|
-
}
|
|
474
|
-
if (!isRecord(parsed) || !hasExactKeys(parsed, ["version", "profile", "nextTicket"]) || parsed.version !== 1 || typeof parsed.nextTicket !== "string" || !ticketPattern.test(parsed.nextTicket)) {
|
|
475
|
-
throw stateFailure("Host-resource profile state is malformed.");
|
|
476
|
-
}
|
|
477
|
-
let profile;
|
|
478
|
-
try {
|
|
479
|
-
profile = normalizeHostResourceProfile(parsed.profile);
|
|
480
|
-
} catch (cause) {
|
|
481
|
-
throw stateFailure("Host-resource profile state is malformed.", cause);
|
|
482
|
-
}
|
|
483
|
-
const nextTicket = BigInt(parsed.nextTicket);
|
|
484
|
-
if (nextTicket > maximumTicket + 1n) {
|
|
485
|
-
throw stateFailure("Host-resource ticket state exceeds its bound.");
|
|
486
|
-
}
|
|
487
|
-
const state = {
|
|
488
|
-
version: 1,
|
|
489
|
-
profile,
|
|
490
|
-
nextTicket: parsed.nextTicket
|
|
491
|
-
};
|
|
492
|
-
if (stateJson(state) !== value) {
|
|
493
|
-
throw stateFailure("Host-resource profile state is not normalized.");
|
|
494
|
-
}
|
|
495
|
-
return state;
|
|
496
|
-
}
|
|
497
|
-
function readState(directory, profile) {
|
|
498
|
-
const path = join(directory, stateFileName);
|
|
499
|
-
const initializedPath = join(directory, initializedFileName);
|
|
500
|
-
if (metadataOrNull(path) === null) {
|
|
501
|
-
if (metadataOrNull(initializedPath) !== null || readdirSync(directory).some((name) => markerNamePattern.test(name))) {
|
|
502
|
-
throw stateFailure("Host-resource profile state is missing.");
|
|
503
|
-
}
|
|
504
|
-
const initial = {
|
|
505
|
-
version: 1,
|
|
506
|
-
profile,
|
|
507
|
-
nextTicket: "1"
|
|
508
|
-
};
|
|
509
|
-
atomicWriteState(directory, initial);
|
|
510
|
-
const initialized = openPrivateFile(initializedPath, true);
|
|
511
|
-
closeSync(initialized);
|
|
512
|
-
syncDirectory(directory);
|
|
513
|
-
return initial;
|
|
514
|
-
}
|
|
515
|
-
const descriptor = openPrivateFile(path);
|
|
516
|
-
try {
|
|
517
|
-
const state = parseState(readDescriptor(descriptor));
|
|
518
|
-
const initializedWasMissing = metadataOrNull(initializedPath) === null;
|
|
519
|
-
const initialized = initializedWasMissing ? openPrivateFile(initializedPath, true) : openPrivateFile(initializedPath);
|
|
520
|
-
try {
|
|
521
|
-
if (fstatSync(initialized).size !== 0) {
|
|
522
|
-
throw stateFailure("Host-resource initialization state is malformed.");
|
|
523
|
-
}
|
|
524
|
-
} finally {
|
|
525
|
-
closeSync(initialized);
|
|
526
|
-
}
|
|
527
|
-
if (initializedWasMissing)
|
|
528
|
-
syncDirectory(directory);
|
|
529
|
-
return state;
|
|
530
|
-
} finally {
|
|
531
|
-
closeSync(descriptor);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
function assertMatchingProfile(state, profile) {
|
|
535
|
-
if (canonicalProfile(state.profile) === canonicalProfile(profile))
|
|
536
|
-
return;
|
|
537
|
-
throw new HostResourceError("PROFILE_MISMATCH", "The machine-global Slopcamera host-resource profile does not match this process.");
|
|
538
|
-
}
|
|
539
|
-
function reserveTicket(directory, state) {
|
|
540
|
-
const ticket = BigInt(state.nextTicket);
|
|
541
|
-
if (ticket > maximumTicket) {
|
|
542
|
-
throw stateFailure("Host-resource tickets are exhausted.");
|
|
543
|
-
}
|
|
544
|
-
atomicWriteState(directory, {
|
|
545
|
-
...state,
|
|
546
|
-
nextTicket: String(ticket + 1n)
|
|
547
|
-
});
|
|
548
|
-
return String(ticket);
|
|
549
|
-
}
|
|
550
|
-
function duration(value, fallback, maximum, code, label) {
|
|
551
|
-
const resolved = value ?? fallback;
|
|
552
|
-
if (!boundedPositiveInteger(resolved, maximum)) {
|
|
553
|
-
throw new HostResourceError(code, `${label} is outside its supported bound.`);
|
|
554
|
-
}
|
|
555
|
-
return resolved;
|
|
556
|
-
}
|
|
557
|
-
function throwIfWaitEnded(signal, deadline) {
|
|
558
|
-
if (signal?.aborted === true) {
|
|
559
|
-
throw new HostResourceError("WAIT_ABORTED", "Host-resource admission was cancelled before execution.");
|
|
560
|
-
}
|
|
561
|
-
if (performance.now() >= deadline) {
|
|
562
|
-
throw new HostResourceError("WAIT_TIMEOUT", "Host-resource admission exceeded its bounded wait.");
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
async function waitForRetry(pollIntervalMilliseconds, signal, deadline) {
|
|
566
|
-
throwIfWaitEnded(signal, deadline);
|
|
567
|
-
const remaining = Math.max(1, Math.ceil(deadline - performance.now()));
|
|
568
|
-
await new Promise((resolve, reject) => {
|
|
569
|
-
let settled = false;
|
|
570
|
-
const finish = () => {
|
|
571
|
-
if (settled)
|
|
572
|
-
return;
|
|
573
|
-
settled = true;
|
|
574
|
-
signal?.removeEventListener("abort", cancel);
|
|
575
|
-
resolve();
|
|
576
|
-
};
|
|
577
|
-
const cancel = () => {
|
|
578
|
-
if (settled)
|
|
579
|
-
return;
|
|
580
|
-
settled = true;
|
|
581
|
-
clearTimeout(timer);
|
|
582
|
-
signal?.removeEventListener("abort", cancel);
|
|
583
|
-
reject(new HostResourceError("WAIT_ABORTED", "Host-resource admission was cancelled before execution."));
|
|
584
|
-
};
|
|
585
|
-
const timer = setTimeout(finish, Math.min(pollIntervalMilliseconds, remaining));
|
|
586
|
-
signal?.addEventListener("abort", cancel, { once: true });
|
|
587
|
-
if (signal?.aborted === true)
|
|
588
|
-
cancel();
|
|
589
|
-
});
|
|
590
|
-
throwIfWaitEnded(signal, deadline);
|
|
591
|
-
}
|
|
592
|
-
function mixRetrySeed(ticket, retry) {
|
|
593
|
-
const mask = 0xffff_ffff_ffff_ffffn;
|
|
594
|
-
let value = BigInt(ticket) + BigInt(retry + 1) * 0x9e37_79b9_7f4a_7c15n & mask;
|
|
595
|
-
value = (value ^ value >> 30n) * 0xbf58_476d_1ce4_e5b9n & mask;
|
|
596
|
-
value = (value ^ value >> 27n) * 0x94d0_49bb_1331_11ebn & mask;
|
|
597
|
-
return (value ^ value >> 31n) & mask;
|
|
598
|
-
}
|
|
599
|
-
function admissionRetryDelayMilliseconds(pollIntervalMilliseconds, ticket, unchangedRetryCount) {
|
|
600
|
-
const adaptiveLimit = pollIntervalMilliseconds <= smallPollIntervalMilliseconds ? pollIntervalMilliseconds : Math.max(pollIntervalMilliseconds, Math.min(maximumAdmissionBackoffMilliseconds, pollIntervalMilliseconds * maximumAdmissionBackoffMultiplier));
|
|
601
|
-
const exponent = Math.min(unchangedRetryCount, Math.ceil(Math.log2(maximumAdmissionBackoffMultiplier)));
|
|
602
|
-
const ceiling = Math.min(adaptiveLimit, pollIntervalMilliseconds * 2 ** exponent);
|
|
603
|
-
const floor = Math.max(1, Math.ceil(ceiling / 2));
|
|
604
|
-
const width = ceiling - floor + 1;
|
|
605
|
-
const offset = Number(mixRetrySeed(ticket, unchangedRetryCount) % BigInt(width));
|
|
606
|
-
return floor + offset;
|
|
607
|
-
}
|
|
608
|
-
async function withControlLock(directory, pollIntervalMilliseconds, signal, deadline, callback) {
|
|
609
|
-
const descriptor = openPrivateFile(join(directory, controlFileName));
|
|
610
|
-
let locked = false;
|
|
611
|
-
try {
|
|
612
|
-
while (!(locked = tryLockHostResourceDescriptor(descriptor))) {
|
|
613
|
-
await waitForRetry(pollIntervalMilliseconds, signal, deadline);
|
|
614
|
-
}
|
|
615
|
-
throwIfWaitEnded(signal, deadline);
|
|
616
|
-
return callback();
|
|
617
|
-
} catch (cause) {
|
|
618
|
-
if (cause instanceof HostResourceError)
|
|
619
|
-
throw cause;
|
|
620
|
-
throw stateFailure("Host-resource control locking failed.", cause);
|
|
621
|
-
} finally {
|
|
622
|
-
if (locked) {
|
|
623
|
-
try {
|
|
624
|
-
unlockHostResourceDescriptor(descriptor);
|
|
625
|
-
} catch {}
|
|
626
|
-
}
|
|
627
|
-
closeSync(descriptor);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
function normalizedTicketName(ticket) {
|
|
631
|
-
return ticket.padStart(20, "0");
|
|
632
|
-
}
|
|
633
|
-
function parseMarker(name, source, profile, profileSha256) {
|
|
634
|
-
const nameMatch = markerNamePattern.exec(name);
|
|
635
|
-
let parsed;
|
|
636
|
-
try {
|
|
637
|
-
parsed = JSON.parse(source);
|
|
638
|
-
} catch (cause) {
|
|
639
|
-
throw stateFailure("A live host-resource marker is not valid JSON.", cause);
|
|
640
|
-
}
|
|
641
|
-
if (nameMatch === null || !isRecord(parsed) || !hasExactKeys(parsed, [
|
|
642
|
-
"version",
|
|
643
|
-
"owner",
|
|
644
|
-
"profileSha256",
|
|
645
|
-
"ticket",
|
|
646
|
-
"phase",
|
|
647
|
-
"claims"
|
|
648
|
-
]) || parsed.version !== 1 || typeof parsed.owner !== "string" || !ownerIdPattern.test(parsed.owner) || typeof parsed.profileSha256 !== "string" || !/^[0-9a-f]{64}$/u.test(parsed.profileSha256) || typeof parsed.ticket !== "string" || !ticketPattern.test(parsed.ticket) || parsed.phase !== "A" && parsed.phase !== "W" || normalizedTicketName(parsed.ticket) !== nameMatch[1] || parsed.owner !== nameMatch[2]) {
|
|
649
|
-
throw stateFailure("A live host-resource marker is malformed.");
|
|
650
|
-
}
|
|
651
|
-
if (parsed.profileSha256 !== profileSha256) {
|
|
652
|
-
throw new HostResourceError("PROFILE_MISMATCH", "A live lease belongs to a different host-resource profile.");
|
|
653
|
-
}
|
|
654
|
-
let claims;
|
|
655
|
-
try {
|
|
656
|
-
claims = normalizeHostResourceClaims(parsed.claims, profile);
|
|
657
|
-
} catch (cause) {
|
|
658
|
-
throw stateFailure("A live host-resource marker has invalid claims.", cause);
|
|
659
|
-
}
|
|
660
|
-
const marker = {
|
|
661
|
-
version: 1,
|
|
662
|
-
owner: parsed.owner,
|
|
663
|
-
profileSha256: parsed.profileSha256,
|
|
664
|
-
ticket: parsed.ticket,
|
|
665
|
-
phase: parsed.phase,
|
|
666
|
-
claims
|
|
667
|
-
};
|
|
668
|
-
if (markerJson(marker) !== source) {
|
|
669
|
-
throw stateFailure("A live host-resource marker is not normalized.");
|
|
670
|
-
}
|
|
671
|
-
return marker;
|
|
672
|
-
}
|
|
673
|
-
function unlinkExactMarker(path, markerIdentity) {
|
|
674
|
-
const published = lstatSync(path);
|
|
675
|
-
if (!privateRegularFile(published) || !sameIdentity(markerIdentity, published)) {
|
|
676
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Host-resource marker identity changed before cleanup.");
|
|
677
|
-
}
|
|
678
|
-
unlinkSync(path);
|
|
679
|
-
}
|
|
680
|
-
function removeTemporaryFile(path) {
|
|
681
|
-
const descriptor = openPrivateFile(path);
|
|
682
|
-
try {
|
|
683
|
-
const opened = fstatSync(descriptor);
|
|
684
|
-
const published = lstatSync(path);
|
|
685
|
-
if (opened.dev !== published.dev || opened.ino !== published.ino) {
|
|
686
|
-
throw stateFailure("Host-resource temporary state changed identity.");
|
|
687
|
-
}
|
|
688
|
-
unlinkSync(path);
|
|
689
|
-
} finally {
|
|
690
|
-
closeSync(descriptor);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
function scanLiveMarkers(directory, profile, profileSha256) {
|
|
694
|
-
const entries = readdirSync(directory, { withFileTypes: true });
|
|
695
|
-
if (entries.length > maximumDirectoryEntries) {
|
|
696
|
-
throw stateFailure("Host-resource state contains too many entries.");
|
|
697
|
-
}
|
|
698
|
-
const live = [];
|
|
699
|
-
const tickets = new Set;
|
|
700
|
-
const owners = new Set;
|
|
701
|
-
for (const entry of entries) {
|
|
702
|
-
if (entry.name === controlFileName || entry.name === stateFileName || entry.name === initializedFileName)
|
|
703
|
-
continue;
|
|
704
|
-
const path = join(directory, entry.name);
|
|
705
|
-
if (temporaryNamePattern.test(entry.name)) {
|
|
706
|
-
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
707
|
-
throw stateFailure("Host-resource temporary state is unsafe.");
|
|
708
|
-
}
|
|
709
|
-
removeTemporaryFile(path);
|
|
710
|
-
continue;
|
|
711
|
-
}
|
|
712
|
-
if (!markerNamePattern.test(entry.name)) {
|
|
713
|
-
throw stateFailure("Host-resource state contains an unknown entry.");
|
|
714
|
-
}
|
|
715
|
-
const descriptor = openPrivateFile(path);
|
|
716
|
-
let locked = false;
|
|
717
|
-
try {
|
|
718
|
-
const opened = fstatSync(descriptor);
|
|
719
|
-
const markerIdentity = identity(opened);
|
|
720
|
-
locked = tryLockHostResourceDescriptor(descriptor);
|
|
721
|
-
if (locked) {
|
|
722
|
-
unlinkExactMarker(path, markerIdentity);
|
|
723
|
-
continue;
|
|
724
|
-
}
|
|
725
|
-
const document = parseMarker(entry.name, readDescriptor(descriptor), profile, profileSha256);
|
|
726
|
-
if (tickets.has(document.ticket) || owners.has(document.owner)) {
|
|
727
|
-
throw stateFailure("Live host-resource marker identity is duplicated.");
|
|
728
|
-
}
|
|
729
|
-
tickets.add(document.ticket);
|
|
730
|
-
owners.add(document.owner);
|
|
731
|
-
live.push({
|
|
732
|
-
document,
|
|
733
|
-
identity: markerIdentity,
|
|
734
|
-
name: entry.name,
|
|
735
|
-
path
|
|
736
|
-
});
|
|
737
|
-
} finally {
|
|
738
|
-
if (locked) {
|
|
739
|
-
try {
|
|
740
|
-
unlockHostResourceDescriptor(descriptor);
|
|
741
|
-
} catch {}
|
|
742
|
-
}
|
|
743
|
-
closeSync(descriptor);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return live.sort((left, right) => {
|
|
747
|
-
const leftTicket = BigInt(left.document.ticket);
|
|
748
|
-
const rightTicket = BigInt(right.document.ticket);
|
|
749
|
-
return leftTicket < rightTicket ? -1 : leftTicket > rightTicket ? 1 : 0;
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
function assertOwnedMarkerIdentity(marker) {
|
|
753
|
-
if (marker.released) {
|
|
754
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Host-resource lease is no longer active.");
|
|
755
|
-
}
|
|
756
|
-
let held;
|
|
757
|
-
let published;
|
|
758
|
-
try {
|
|
759
|
-
held = fstatSync(marker.descriptor);
|
|
760
|
-
published = lstatSync(marker.path);
|
|
761
|
-
} catch (cause) {
|
|
762
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Host-resource marker is no longer reachable.", cause);
|
|
763
|
-
}
|
|
764
|
-
if (!privateRegularFile(held) || !privateRegularFile(published) || !sameIdentity(marker.identity, held) || !sameIdentity(marker.identity, published)) {
|
|
765
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Host-resource descriptor no longer owns its published inode.");
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
function publishWaitingMarker(directory, state, claims, profileSha256) {
|
|
769
|
-
const ticket = reserveTicket(directory, state);
|
|
770
|
-
const owner = randomBytes(16).toString("hex");
|
|
771
|
-
const name = `lease-${normalizedTicketName(ticket)}-${owner}.lock`;
|
|
772
|
-
const path = join(directory, name);
|
|
773
|
-
let descriptor;
|
|
774
|
-
try {
|
|
775
|
-
descriptor = openPrivateFile(path, true);
|
|
776
|
-
if (!tryLockHostResourceDescriptor(descriptor)) {
|
|
777
|
-
throw stateFailure("A new host-resource marker could not be locked.");
|
|
778
|
-
}
|
|
779
|
-
const document = {
|
|
780
|
-
version: 1,
|
|
781
|
-
owner,
|
|
782
|
-
profileSha256,
|
|
783
|
-
ticket,
|
|
784
|
-
phase: "W",
|
|
785
|
-
claims
|
|
786
|
-
};
|
|
787
|
-
writeDescriptor(descriptor, markerJson(document));
|
|
788
|
-
return {
|
|
789
|
-
descriptor,
|
|
790
|
-
document,
|
|
791
|
-
identity: identity(fstatSync(descriptor)),
|
|
792
|
-
name,
|
|
793
|
-
path,
|
|
794
|
-
released: false
|
|
795
|
-
};
|
|
796
|
-
} catch (cause) {
|
|
797
|
-
if (descriptor !== undefined) {
|
|
798
|
-
try {
|
|
799
|
-
unlockHostResourceDescriptor(descriptor);
|
|
800
|
-
} catch {}
|
|
801
|
-
closeSync(descriptor);
|
|
802
|
-
}
|
|
803
|
-
try {
|
|
804
|
-
unlinkSync(path);
|
|
805
|
-
} catch (cleanupCause) {
|
|
806
|
-
if (!isErrorCode(cleanupCause, "ENOENT")) {}
|
|
807
|
-
}
|
|
808
|
-
throw cause;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
function sameMarker(left, right) {
|
|
812
|
-
return left.name === right.name && left.document.owner === right.document.owner && left.document.ticket === right.document.ticket && sameIdentity(left.identity, right.identity);
|
|
813
|
-
}
|
|
814
|
-
function claimsOverlap(left, right) {
|
|
815
|
-
const resources = new Set(left.map(({ resource }) => resource));
|
|
816
|
-
return right.some(({ resource }) => resources.has(resource));
|
|
817
|
-
}
|
|
818
|
-
function canFitClaims(profile, active, requested) {
|
|
819
|
-
const used = new Map;
|
|
820
|
-
for (const marker of active) {
|
|
821
|
-
for (const claim of marker.document.claims) {
|
|
822
|
-
used.set(claim.resource, (used.get(claim.resource) ?? 0) + claim.amount);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
for (const { resource, limit } of profile.capacities) {
|
|
826
|
-
const current = used.get(resource) ?? 0;
|
|
827
|
-
if (current > limit) {
|
|
828
|
-
throw stateFailure("Live host-resource leases exceed the declared profile.");
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
return requested.every(({ resource, amount }) => {
|
|
832
|
-
const limit = profile.capacities.find((capacity) => capacity.resource === resource)?.limit;
|
|
833
|
-
return limit !== undefined && (used.get(resource) ?? 0) + amount <= limit;
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
function admissionQueueState(marker, live) {
|
|
837
|
-
const ticket = BigInt(marker.document.ticket);
|
|
838
|
-
const blockers = live.filter((candidate) => {
|
|
839
|
-
if (sameMarker(candidate, marker))
|
|
840
|
-
return false;
|
|
841
|
-
if (!claimsOverlap(candidate.document.claims, marker.document.claims)) {
|
|
842
|
-
return false;
|
|
843
|
-
}
|
|
844
|
-
return candidate.document.phase === "A" || BigInt(candidate.document.ticket) < ticket;
|
|
845
|
-
});
|
|
846
|
-
return sha256(blockers.map((candidate) => `${candidate.name}\x00${markerJson(candidate.document)}`).join(`
|
|
847
|
-
`));
|
|
848
|
-
}
|
|
849
|
-
function attemptAdmission(marker, live, profile) {
|
|
850
|
-
assertOwnedMarkerIdentity(marker);
|
|
851
|
-
const current = live.find((candidate) => sameMarker(candidate, marker));
|
|
852
|
-
if (current === undefined || current.document.phase !== marker.document.phase || markerJson(current.document) !== markerJson(marker.document)) {
|
|
853
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Host-resource lease marker no longer matches its descriptor.");
|
|
854
|
-
}
|
|
855
|
-
const ticket = BigInt(marker.document.ticket);
|
|
856
|
-
const earlierOverlap = live.some((candidate) => candidate.document.phase === "W" && BigInt(candidate.document.ticket) < ticket && claimsOverlap(candidate.document.claims, marker.document.claims));
|
|
857
|
-
if (earlierOverlap) {
|
|
858
|
-
return {
|
|
859
|
-
admitted: false,
|
|
860
|
-
queueState: admissionQueueState(marker, live)
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
const active = live.filter((candidate) => candidate.document.phase === "A");
|
|
864
|
-
if (!canFitClaims(profile, active, marker.document.claims)) {
|
|
865
|
-
return {
|
|
866
|
-
admitted: false,
|
|
867
|
-
queueState: admissionQueueState(marker, live)
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
const admitted = { ...marker.document, phase: "A" };
|
|
871
|
-
writeDescriptor(marker.descriptor, markerJson(admitted));
|
|
872
|
-
marker.document = admitted;
|
|
873
|
-
return { admitted: true, queueState: "" };
|
|
874
|
-
}
|
|
875
|
-
function markerMatchesOwnedActive(candidate, marker) {
|
|
876
|
-
return sameMarker(candidate, marker) && candidate.document.phase === "A" && marker.document.phase === "A" && markerJson(candidate.document) === markerJson(marker.document);
|
|
877
|
-
}
|
|
878
|
-
async function admitMarker(marker, root, options, signal, deadline, state) {
|
|
879
|
-
let previousQueueState;
|
|
880
|
-
let unchangedRetryCount = 0;
|
|
881
|
-
for (;; ) {
|
|
882
|
-
const attempt = await withControlLock(root, options.pollIntervalMilliseconds, signal, deadline, () => {
|
|
883
|
-
const currentState = state();
|
|
884
|
-
assertMatchingProfile(currentState, options.profile);
|
|
885
|
-
const live = scanLiveMarkers(root, options.profile, options.profileSha256);
|
|
886
|
-
return attemptAdmission(marker, live, options.profile);
|
|
887
|
-
});
|
|
888
|
-
if (attempt.admitted)
|
|
889
|
-
return;
|
|
890
|
-
if (attempt.queueState === previousQueueState) {
|
|
891
|
-
unchangedRetryCount += 1;
|
|
892
|
-
} else {
|
|
893
|
-
previousQueueState = attempt.queueState;
|
|
894
|
-
unchangedRetryCount = 0;
|
|
895
|
-
}
|
|
896
|
-
await waitForRetry(admissionRetryDelayMilliseconds(options.pollIntervalMilliseconds, marker.document.ticket, unchangedRetryCount), signal, deadline);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
async function assertMarkerOwned(marker, root, options, state) {
|
|
900
|
-
assertOwnedMarkerIdentity(marker);
|
|
901
|
-
const deadline = performance.now() + options.waitTimeoutMilliseconds;
|
|
902
|
-
await withControlLock(root, options.pollIntervalMilliseconds, undefined, deadline, () => {
|
|
903
|
-
const currentState = state();
|
|
904
|
-
assertMatchingProfile(currentState, options.profile);
|
|
905
|
-
const live = scanLiveMarkers(root, options.profile, options.profileSha256);
|
|
906
|
-
if (!live.some((candidate) => markerMatchesOwnedActive(candidate, marker))) {
|
|
907
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Host-resource lease is not backed by its exact active kernel lock.");
|
|
908
|
-
}
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
async function releaseMarker(marker, root, options, state) {
|
|
912
|
-
if (marker.released)
|
|
913
|
-
return;
|
|
914
|
-
marker.released = true;
|
|
915
|
-
try {
|
|
916
|
-
closeSync(marker.descriptor);
|
|
917
|
-
} catch (cause) {
|
|
918
|
-
if (!isErrorCode(cause, "EBADF"))
|
|
919
|
-
throw cause;
|
|
920
|
-
}
|
|
921
|
-
const deadline = performance.now() + options.waitTimeoutMilliseconds;
|
|
922
|
-
await withControlLock(root, options.pollIntervalMilliseconds, undefined, deadline, () => {
|
|
923
|
-
const currentState = state();
|
|
924
|
-
assertMatchingProfile(currentState, options.profile);
|
|
925
|
-
if (metadataOrNull(marker.path) === null)
|
|
926
|
-
return;
|
|
927
|
-
const descriptor = openPrivateFile(marker.path);
|
|
928
|
-
let locked = false;
|
|
929
|
-
try {
|
|
930
|
-
const opened = fstatSync(descriptor);
|
|
931
|
-
if (!sameIdentity(marker.identity, opened)) {
|
|
932
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Host-resource marker inode changed before release.");
|
|
933
|
-
}
|
|
934
|
-
locked = tryLockHostResourceDescriptor(descriptor);
|
|
935
|
-
if (!locked) {
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
unlinkExactMarker(marker.path, marker.identity);
|
|
939
|
-
} finally {
|
|
940
|
-
if (locked) {
|
|
941
|
-
try {
|
|
942
|
-
unlockHostResourceDescriptor(descriptor);
|
|
943
|
-
} catch {}
|
|
944
|
-
}
|
|
945
|
-
closeSync(descriptor);
|
|
946
|
-
}
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
function resolveCoordinatorOptions(options) {
|
|
950
|
-
if (!isHostResourcePlatformSupported(process.platform)) {
|
|
951
|
-
throw new HostResourceError("UNSUPPORTED_PLATFORM", "Slopcamera host-resource coordination requires Darwin or Linux.");
|
|
952
|
-
}
|
|
953
|
-
const profile = normalizeHostResourceProfile(options.profile ?? defaultSlopcameraHostResourceProfile());
|
|
954
|
-
const stateRoot = options.stateRoot ?? defaultSlopcameraHostResourceStateRoot();
|
|
955
|
-
if (typeof stateRoot !== "string" || !isAbsolute(stateRoot) || stateRoot.includes("\x00") || Buffer.byteLength(stateRoot, "utf8") > maximumPathBytes) {
|
|
956
|
-
throw stateFailure("Host-resource state root must be a bounded absolute path.");
|
|
957
|
-
}
|
|
958
|
-
return {
|
|
959
|
-
pollIntervalMilliseconds: duration(options.pollIntervalMilliseconds, defaultPollIntervalMilliseconds, 1000, "INVALID_PROFILE", "Host-resource poll interval"),
|
|
960
|
-
profile,
|
|
961
|
-
profileSha256: sha256(canonicalProfile(profile)),
|
|
962
|
-
stateRoot,
|
|
963
|
-
waitTimeoutMilliseconds: duration(options.waitTimeoutMilliseconds, defaultWaitTimeoutMilliseconds, HOST_RESOURCE_MAX_WAIT_MILLISECONDS, "INVALID_PROFILE", "Host-resource wait timeout")
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
function createHostResourceCoordinator(coordinatorOptions = {}) {
|
|
967
|
-
const options = resolveCoordinatorOptions(coordinatorOptions);
|
|
968
|
-
let resolvedRoot;
|
|
969
|
-
const root = () => {
|
|
970
|
-
if (resolvedRoot === undefined) {
|
|
971
|
-
resolvedRoot = ensureStateRoot(options.stateRoot);
|
|
972
|
-
}
|
|
973
|
-
return resolvedRoot.path;
|
|
974
|
-
};
|
|
975
|
-
const state = () => {
|
|
976
|
-
return readState(root(), options.profile);
|
|
977
|
-
};
|
|
978
|
-
const coordinator = {
|
|
979
|
-
profile: options.profile,
|
|
980
|
-
scope: "machine",
|
|
981
|
-
async withLease(claimValue, callback, leaseOptions = {}) {
|
|
982
|
-
const claims = normalizeHostResourceClaims(claimValue, options.profile);
|
|
983
|
-
if (typeof callback !== "function") {
|
|
984
|
-
throw new HostResourceError("INVALID_CLAIMS", "Host-resource lease callback must be a function.");
|
|
985
|
-
}
|
|
986
|
-
const waitTimeoutMilliseconds = duration(leaseOptions.waitTimeoutMilliseconds, options.waitTimeoutMilliseconds, HOST_RESOURCE_MAX_WAIT_MILLISECONDS, "INVALID_CLAIMS", "Host-resource lease wait timeout");
|
|
987
|
-
const signal = leaseOptions.signal;
|
|
988
|
-
const deadline = performance.now() + waitTimeoutMilliseconds;
|
|
989
|
-
throwIfWaitEnded(signal, deadline);
|
|
990
|
-
const directory = root();
|
|
991
|
-
let marker;
|
|
992
|
-
let callbackSettledSuccessfully = false;
|
|
993
|
-
let callbackResult;
|
|
994
|
-
let callbackFailure;
|
|
995
|
-
try {
|
|
996
|
-
marker = await withControlLock(directory, options.pollIntervalMilliseconds, signal, deadline, () => {
|
|
997
|
-
const currentState = state();
|
|
998
|
-
assertMatchingProfile(currentState, options.profile);
|
|
999
|
-
scanLiveMarkers(directory, options.profile, options.profileSha256);
|
|
1000
|
-
return publishWaitingMarker(directory, currentState, claims, options.profileSha256);
|
|
1001
|
-
});
|
|
1002
|
-
await admitMarker(marker, directory, options, signal, deadline, state);
|
|
1003
|
-
const ownedMarker = marker;
|
|
1004
|
-
const lease = Object.freeze({
|
|
1005
|
-
claims,
|
|
1006
|
-
inheritedFileDescriptor: ownedMarker.descriptor,
|
|
1007
|
-
profile: options.profile,
|
|
1008
|
-
ticket: ownedMarker.document.ticket,
|
|
1009
|
-
assertOwned: async () => {
|
|
1010
|
-
await assertMarkerOwned(ownedMarker, directory, options, state);
|
|
1011
|
-
}
|
|
1012
|
-
});
|
|
1013
|
-
await lease.assertOwned();
|
|
1014
|
-
try {
|
|
1015
|
-
callbackResult = await callback(lease);
|
|
1016
|
-
callbackSettledSuccessfully = true;
|
|
1017
|
-
await lease.assertOwned();
|
|
1018
|
-
} catch (cause) {
|
|
1019
|
-
callbackFailure = cause;
|
|
1020
|
-
}
|
|
1021
|
-
} finally {
|
|
1022
|
-
if (marker !== undefined) {
|
|
1023
|
-
try {
|
|
1024
|
-
await releaseMarker(marker, directory, options, state);
|
|
1025
|
-
} catch (releaseFailure) {
|
|
1026
|
-
if (callbackFailure === undefined)
|
|
1027
|
-
callbackFailure = releaseFailure;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
if (!callbackSettledSuccessfully || callbackFailure !== undefined) {
|
|
1032
|
-
throw callbackFailure;
|
|
1033
|
-
}
|
|
1034
|
-
return callbackResult;
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
|
-
return Object.freeze(coordinator);
|
|
1038
|
-
}
|
|
1039
|
-
var processLocalCoordinatorStates = new Map;
|
|
1040
|
-
function createProcessLocalHostResourceCoordinator(coordinatorOptions = {}) {
|
|
1041
|
-
const profile = normalizeHostResourceProfile(coordinatorOptions.profile ?? defaultSlopcameraHostResourceProfile());
|
|
1042
|
-
const defaultWait = duration(coordinatorOptions.waitTimeoutMilliseconds, defaultWaitTimeoutMilliseconds, HOST_RESOURCE_MAX_WAIT_MILLISECONDS, "INVALID_PROFILE", "Host-resource wait timeout");
|
|
1043
|
-
if (coordinatorOptions.pollIntervalMilliseconds !== undefined) {
|
|
1044
|
-
duration(coordinatorOptions.pollIntervalMilliseconds, defaultPollIntervalMilliseconds, 1000, "INVALID_PROFILE", "Host-resource poll interval");
|
|
1045
|
-
}
|
|
1046
|
-
const stateKey = canonicalProfile(profile);
|
|
1047
|
-
let localState = processLocalCoordinatorStates.get(stateKey);
|
|
1048
|
-
if (localState === undefined) {
|
|
1049
|
-
if (processLocalCoordinatorStates.size >= maximumProcessLocalProfiles) {
|
|
1050
|
-
throw new HostResourceError("INVALID_PROFILE", "This process already coordinates the maximum number of host-resource profiles.");
|
|
1051
|
-
}
|
|
1052
|
-
localState = {
|
|
1053
|
-
active: new Map,
|
|
1054
|
-
nextTicket: 1n,
|
|
1055
|
-
waiting: []
|
|
1056
|
-
};
|
|
1057
|
-
processLocalCoordinatorStates.set(stateKey, localState);
|
|
1058
|
-
}
|
|
1059
|
-
const { active, waiting } = localState;
|
|
1060
|
-
const removeWaitHooks = (request) => {
|
|
1061
|
-
if (request.timeout !== undefined)
|
|
1062
|
-
clearTimeout(request.timeout);
|
|
1063
|
-
if (request.abortListener !== undefined) {
|
|
1064
|
-
request.signal?.removeEventListener("abort", request.abortListener);
|
|
1065
|
-
}
|
|
1066
|
-
};
|
|
1067
|
-
const activeMarkers = () => [...active.values()].map((request) => ({
|
|
1068
|
-
document: {
|
|
1069
|
-
version: 1,
|
|
1070
|
-
owner: request.ticket.padStart(32, "0").slice(-32),
|
|
1071
|
-
profileSha256: sha256(canonicalProfile(profile)),
|
|
1072
|
-
ticket: request.ticket,
|
|
1073
|
-
phase: "A",
|
|
1074
|
-
claims: request.claims
|
|
1075
|
-
},
|
|
1076
|
-
identity: { device: 0, inode: 0 },
|
|
1077
|
-
name: request.ticket,
|
|
1078
|
-
path: ""
|
|
1079
|
-
}));
|
|
1080
|
-
const pump = () => {
|
|
1081
|
-
for (let index = 0;index < waiting.length; ) {
|
|
1082
|
-
const request = waiting[index];
|
|
1083
|
-
if (request === undefined)
|
|
1084
|
-
break;
|
|
1085
|
-
const earlierOverlap = waiting.slice(0, index).some((earlier) => claimsOverlap(earlier.claims, request.claims));
|
|
1086
|
-
if (earlierOverlap || !canFitClaims(profile, activeMarkers(), request.claims)) {
|
|
1087
|
-
index += 1;
|
|
1088
|
-
continue;
|
|
1089
|
-
}
|
|
1090
|
-
waiting.splice(index, 1);
|
|
1091
|
-
removeWaitHooks(request);
|
|
1092
|
-
let descriptor;
|
|
1093
|
-
try {
|
|
1094
|
-
descriptor = openSync(process.platform === "win32" ? "NUL" : "/dev/null", constants.O_RDONLY);
|
|
1095
|
-
} catch (cause) {
|
|
1096
|
-
request.settled = true;
|
|
1097
|
-
request.reject(new HostResourceError("UNSAFE_STATE", "Process-local host-resource admission could not open its lease descriptor.", cause));
|
|
1098
|
-
continue;
|
|
1099
|
-
}
|
|
1100
|
-
request.active = true;
|
|
1101
|
-
active.set(request.ticket, request);
|
|
1102
|
-
request.descriptor = descriptor;
|
|
1103
|
-
const assertOwned = async () => {
|
|
1104
|
-
if (request.settled || !request.active || active.get(request.ticket) !== request || request.descriptor !== descriptor) {
|
|
1105
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Process-local host-resource lease is no longer active.");
|
|
1106
|
-
}
|
|
1107
|
-
try {
|
|
1108
|
-
fstatSync(descriptor);
|
|
1109
|
-
} catch (cause) {
|
|
1110
|
-
throw new HostResourceError("OWNERSHIP_LOST", "Process-local host-resource descriptor is no longer open.", cause);
|
|
1111
|
-
}
|
|
1112
|
-
};
|
|
1113
|
-
const lease = Object.freeze({
|
|
1114
|
-
assertOwned,
|
|
1115
|
-
claims: request.claims,
|
|
1116
|
-
inheritedFileDescriptor: descriptor,
|
|
1117
|
-
profile,
|
|
1118
|
-
ticket: request.ticket
|
|
1119
|
-
});
|
|
1120
|
-
(async () => {
|
|
1121
|
-
let succeeded = false;
|
|
1122
|
-
let result;
|
|
1123
|
-
let failure;
|
|
1124
|
-
try {
|
|
1125
|
-
await lease.assertOwned();
|
|
1126
|
-
result = await request.callback(lease);
|
|
1127
|
-
await lease.assertOwned();
|
|
1128
|
-
succeeded = true;
|
|
1129
|
-
} catch (cause) {
|
|
1130
|
-
failure = cause;
|
|
1131
|
-
} finally {
|
|
1132
|
-
request.active = false;
|
|
1133
|
-
request.settled = true;
|
|
1134
|
-
active.delete(request.ticket);
|
|
1135
|
-
try {
|
|
1136
|
-
closeSync(descriptor);
|
|
1137
|
-
} catch (cause) {
|
|
1138
|
-
if (failure === undefined)
|
|
1139
|
-
failure = cause;
|
|
1140
|
-
}
|
|
1141
|
-
pump();
|
|
1142
|
-
}
|
|
1143
|
-
if (succeeded && failure === undefined)
|
|
1144
|
-
request.resolve(result);
|
|
1145
|
-
else
|
|
1146
|
-
request.reject(failure);
|
|
1147
|
-
})();
|
|
1148
|
-
}
|
|
1149
|
-
};
|
|
1150
|
-
const coordinator = {
|
|
1151
|
-
profile,
|
|
1152
|
-
scope: "process",
|
|
1153
|
-
async withLease(claimValue, callback, leaseOptions = {}) {
|
|
1154
|
-
const claims = normalizeHostResourceClaims(claimValue, profile);
|
|
1155
|
-
if (typeof callback !== "function") {
|
|
1156
|
-
return Promise.reject(new HostResourceError("INVALID_CLAIMS", "Host-resource lease callback must be a function."));
|
|
1157
|
-
}
|
|
1158
|
-
if (localState.nextTicket > maximumTicket) {
|
|
1159
|
-
return Promise.reject(stateFailure("Host-resource tickets are exhausted."));
|
|
1160
|
-
}
|
|
1161
|
-
const waitTimeoutMilliseconds = duration(leaseOptions.waitTimeoutMilliseconds, defaultWait, HOST_RESOURCE_MAX_WAIT_MILLISECONDS, "INVALID_CLAIMS", "Host-resource lease wait timeout");
|
|
1162
|
-
const ticket = String(localState.nextTicket);
|
|
1163
|
-
localState.nextTicket += 1n;
|
|
1164
|
-
return await new Promise((resolve, reject) => {
|
|
1165
|
-
const request = {
|
|
1166
|
-
active: false,
|
|
1167
|
-
callback,
|
|
1168
|
-
claims,
|
|
1169
|
-
reject,
|
|
1170
|
-
resolve,
|
|
1171
|
-
settled: false,
|
|
1172
|
-
signal: leaseOptions.signal,
|
|
1173
|
-
ticket
|
|
1174
|
-
};
|
|
1175
|
-
const removeWaiting = (cause) => {
|
|
1176
|
-
if (request.active || request.settled)
|
|
1177
|
-
return;
|
|
1178
|
-
request.settled = true;
|
|
1179
|
-
const index = waiting.indexOf(request);
|
|
1180
|
-
if (index >= 0)
|
|
1181
|
-
waiting.splice(index, 1);
|
|
1182
|
-
removeWaitHooks(request);
|
|
1183
|
-
reject(cause);
|
|
1184
|
-
pump();
|
|
1185
|
-
};
|
|
1186
|
-
request.abortListener = () => removeWaiting(new HostResourceError("WAIT_ABORTED", "Host-resource admission was cancelled before execution."));
|
|
1187
|
-
request.timeout = setTimeout(() => removeWaiting(new HostResourceError("WAIT_TIMEOUT", "Host-resource admission exceeded its bounded wait.")), waitTimeoutMilliseconds);
|
|
1188
|
-
request.timeout.unref?.();
|
|
1189
|
-
leaseOptions.signal?.addEventListener("abort", request.abortListener, {
|
|
1190
|
-
once: true
|
|
1191
|
-
});
|
|
1192
|
-
if (leaseOptions.signal?.aborted === true) {
|
|
1193
|
-
request.abortListener();
|
|
1194
|
-
return;
|
|
1195
|
-
}
|
|
1196
|
-
waiting.push(request);
|
|
1197
|
-
pump();
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
};
|
|
1201
|
-
return Object.freeze(coordinator);
|
|
1202
|
-
}
|
|
1203
|
-
function createDefaultHostResourceCoordinator(options = {}, platform = process.platform) {
|
|
1204
|
-
if (isHostResourcePlatformSupported(platform)) {
|
|
1205
|
-
return createHostResourceCoordinator(options);
|
|
1206
|
-
}
|
|
1207
|
-
const {
|
|
1208
|
-
profile,
|
|
1209
|
-
pollIntervalMilliseconds,
|
|
1210
|
-
waitTimeoutMilliseconds
|
|
1211
|
-
} = options;
|
|
1212
|
-
return createProcessLocalHostResourceCoordinator({
|
|
1213
|
-
...profile === undefined ? {} : { profile },
|
|
1214
|
-
...pollIntervalMilliseconds === undefined ? {} : { pollIntervalMilliseconds },
|
|
1215
|
-
...waitTimeoutMilliseconds === undefined ? {} : { waitTimeoutMilliseconds }
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
export { HOST_RESOURCE_MAX_WAIT_MILLISECONDS, slopcameraHostResourceNames, HostResourceError, normalizeHostResourceProfile, normalizeHostResourceClaims, defaultSlopcameraHostResourceProfile, defaultSlopcameraHostResourceStateRoot, createHostResourceCoordinator, createProcessLocalHostResourceCoordinator, createDefaultHostResourceCoordinator };
|