@shotkit/shotium 0.0.1 → 0.1.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 +169 -1
- package/dist/daemon_main.d.ts +1 -0
- package/dist/daemon_main.js +322 -0
- package/dist/daemon_main.js.map +1 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/dist/native.d.ts +66 -0
- package/dist/native.js +127 -0
- package/dist/native.js.map +1 -0
- package/dist/platform-DU8DYqmA.js +32 -0
- package/dist/platform-DU8DYqmA.js.map +1 -0
- package/dist/pool-BSgS6vkr.js +356 -0
- package/dist/pool-BSgS6vkr.js.map +1 -0
- package/dist/request-qZXS3N9f.js +43 -0
- package/dist/request-qZXS3N9f.js.map +1 -0
- package/dist/types-x9HtkzeE.d.ts +156 -0
- package/native/binding.cc +283 -0
- package/native/binding.gyp +54 -0
- package/native/stage_header.js +53 -0
- package/package.json +60 -3
- package/src/daemon_main.ts +76 -0
- package/src/index.ts +161 -0
- package/src/lib/client.ts +377 -0
- package/src/lib/config.ts +86 -0
- package/src/lib/daemon.ts +370 -0
- package/src/lib/endpoint.ts +63 -0
- package/src/lib/platform.ts +75 -0
- package/src/lib/pool.ts +243 -0
- package/src/lib/protocol.ts +53 -0
- package/src/lib/request.ts +108 -0
- package/src/lib/worker.ts +220 -0
- package/src/native.ts +234 -0
- package/src/types.ts +169 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { a as resolveStartOptions, i as endpointFor, n as FrameReader, r as encodeFrame, t as Pool } from "./pool-BSgS6vkr.js";
|
|
2
|
+
import { n as timeoutFor, r as toRequest, t as SUPERVISOR_MARGIN_MS } from "./request-qZXS3N9f.js";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import net from "node:net";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
//#region src/lib/client.ts
|
|
11
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const DAEMON_MAIN = path.join(HERE, "daemon_main.js");
|
|
13
|
+
const START_TIMEOUT_MS = 2e4;
|
|
14
|
+
const CONNECT_RETRY_MS = 20;
|
|
15
|
+
var DaemonClient = class extends EventEmitter {
|
|
16
|
+
socket;
|
|
17
|
+
endpointPath;
|
|
18
|
+
pending = /* @__PURE__ */ new Map();
|
|
19
|
+
nextId = 1;
|
|
20
|
+
header = null;
|
|
21
|
+
reader = new FrameReader();
|
|
22
|
+
constructor(socket, endpoint) {
|
|
23
|
+
super();
|
|
24
|
+
this.socket = socket;
|
|
25
|
+
this.endpointPath = endpoint;
|
|
26
|
+
socket.on("data", (chunk) => this.onData(chunk));
|
|
27
|
+
socket.on("error", (error) => this.failAll(error));
|
|
28
|
+
socket.on("close", () => {
|
|
29
|
+
this.failAll(/* @__PURE__ */ new Error("shotium: the daemon closed the connection"));
|
|
30
|
+
this.emit("close", {});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
get endpoint() {
|
|
34
|
+
return this.endpointPath;
|
|
35
|
+
}
|
|
36
|
+
get closed() {
|
|
37
|
+
return this.socket.destroyed;
|
|
38
|
+
}
|
|
39
|
+
onData(chunk) {
|
|
40
|
+
this.reader.push(chunk);
|
|
41
|
+
for (;;) {
|
|
42
|
+
const frame = this.reader.next();
|
|
43
|
+
if (frame === null) return;
|
|
44
|
+
if (this.header === null) {
|
|
45
|
+
try {
|
|
46
|
+
this.header = JSON.parse(frame.toString("utf8"));
|
|
47
|
+
} catch {
|
|
48
|
+
this.failAll(/* @__PURE__ */ new Error("shotium: the daemon sent a header that is not JSON"));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const header = this.header;
|
|
54
|
+
this.header = null;
|
|
55
|
+
this.settle(header, frame);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
settle(header, payload) {
|
|
59
|
+
const pending = this.pending.get(header.id);
|
|
60
|
+
if (!pending) return;
|
|
61
|
+
this.pending.delete(header.id);
|
|
62
|
+
if (header.ok) pending.resolve({
|
|
63
|
+
header,
|
|
64
|
+
image: header.path ? null : payload
|
|
65
|
+
});
|
|
66
|
+
else pending.reject(new Error(header.error || "shotium: request failed"));
|
|
67
|
+
}
|
|
68
|
+
failAll(error) {
|
|
69
|
+
for (const [, pending] of this.pending) pending.reject(error);
|
|
70
|
+
this.pending.clear();
|
|
71
|
+
}
|
|
72
|
+
send(message) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
if (this.socket.destroyed) {
|
|
75
|
+
reject(/* @__PURE__ */ new Error("shotium: not connected to a daemon"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const id = this.nextId++;
|
|
79
|
+
this.pending.set(id, {
|
|
80
|
+
resolve,
|
|
81
|
+
reject
|
|
82
|
+
});
|
|
83
|
+
this.socket.write(encodeFrame(Buffer.from(JSON.stringify({
|
|
84
|
+
...message,
|
|
85
|
+
id
|
|
86
|
+
}), "utf8")));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Resolves to the image, or to null when `path` was given. */
|
|
90
|
+
async screenshot(options) {
|
|
91
|
+
const request = toRequest(options);
|
|
92
|
+
const retry = typeof options.retry === "number" ? options.retry : 0;
|
|
93
|
+
return (await this.send({
|
|
94
|
+
op: "screenshot",
|
|
95
|
+
request,
|
|
96
|
+
timeout: timeoutFor(options),
|
|
97
|
+
retry
|
|
98
|
+
})).image;
|
|
99
|
+
}
|
|
100
|
+
async status() {
|
|
101
|
+
const { header } = await this.send({ op: "status" });
|
|
102
|
+
return header;
|
|
103
|
+
}
|
|
104
|
+
async shutdown() {
|
|
105
|
+
const { header } = await this.send({ op: "shutdown" });
|
|
106
|
+
return { ok: header.ok === true };
|
|
107
|
+
}
|
|
108
|
+
close() {
|
|
109
|
+
this.socket.end();
|
|
110
|
+
this.socket.destroy();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function connectOnly(endpoint) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const socket = net.connect(endpoint);
|
|
116
|
+
const onError = (error) => {
|
|
117
|
+
socket.destroy();
|
|
118
|
+
reject(error);
|
|
119
|
+
};
|
|
120
|
+
socket.once("error", onError);
|
|
121
|
+
socket.once("connect", () => {
|
|
122
|
+
socket.removeListener("error", onError);
|
|
123
|
+
resolve(new DaemonClient(socket, endpoint));
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function resolveDaemonOptions(options = {}) {
|
|
128
|
+
const resolved = resolveStartOptions(options);
|
|
129
|
+
return {
|
|
130
|
+
...resolved,
|
|
131
|
+
name: options.name,
|
|
132
|
+
endpoint: endpointFor({
|
|
133
|
+
...resolved,
|
|
134
|
+
name: options.name,
|
|
135
|
+
endpoint: options.endpoint
|
|
136
|
+
}),
|
|
137
|
+
idleTimeoutMs: options.idleTimeoutMs,
|
|
138
|
+
prewarm: options.prewarm,
|
|
139
|
+
logFile: options.logFile || process.env.SHOTIUM_DAEMON_LOG || null
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function spawnDaemon(options) {
|
|
143
|
+
const config = {
|
|
144
|
+
binary: options.binary,
|
|
145
|
+
workers: options.workers,
|
|
146
|
+
cacheDir: options.cacheDir,
|
|
147
|
+
args: options.args,
|
|
148
|
+
endpoint: options.endpoint,
|
|
149
|
+
idleTimeoutMs: options.idleTimeoutMs,
|
|
150
|
+
prewarm: options.prewarm
|
|
151
|
+
};
|
|
152
|
+
const encoded = Buffer.from(JSON.stringify(config), "utf8").toString("base64");
|
|
153
|
+
let stdio = "ignore";
|
|
154
|
+
let logFd = null;
|
|
155
|
+
if (options.logFile) {
|
|
156
|
+
logFd = fs.openSync(options.logFile, "a");
|
|
157
|
+
stdio = [
|
|
158
|
+
"ignore",
|
|
159
|
+
logFd,
|
|
160
|
+
logFd
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
spawn(process.execPath, [DAEMON_MAIN, encoded], {
|
|
164
|
+
detached: true,
|
|
165
|
+
stdio,
|
|
166
|
+
windowsHide: true
|
|
167
|
+
}).unref();
|
|
168
|
+
if (logFd !== null) fs.closeSync(logFd);
|
|
169
|
+
}
|
|
170
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
171
|
+
async function ensureClient(options = {}) {
|
|
172
|
+
const resolved = resolveDaemonOptions(options);
|
|
173
|
+
try {
|
|
174
|
+
return {
|
|
175
|
+
client: await connectOnly(resolved.endpoint),
|
|
176
|
+
spawned: false,
|
|
177
|
+
endpoint: resolved.endpoint
|
|
178
|
+
};
|
|
179
|
+
} catch {
|
|
180
|
+
if (options.spawn === false) throw new Error(`shotium: no daemon at ${resolved.endpoint}`);
|
|
181
|
+
}
|
|
182
|
+
spawnDaemon(resolved);
|
|
183
|
+
const deadline = Date.now() + (options.startTimeoutMs === void 0 ? START_TIMEOUT_MS : options.startTimeoutMs);
|
|
184
|
+
for (;;) try {
|
|
185
|
+
return {
|
|
186
|
+
client: await connectOnly(resolved.endpoint),
|
|
187
|
+
spawned: true,
|
|
188
|
+
endpoint: resolved.endpoint
|
|
189
|
+
};
|
|
190
|
+
} catch {
|
|
191
|
+
if (Date.now() >= deadline) throw new Error(`shotium: the daemon did not come up at ${resolved.endpoint}`);
|
|
192
|
+
await sleep(CONNECT_RETRY_MS);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function connect(options = {}) {
|
|
196
|
+
const { client } = await ensureClient(options);
|
|
197
|
+
return client;
|
|
198
|
+
}
|
|
199
|
+
async function start(options = {}) {
|
|
200
|
+
const { client, spawned, endpoint } = await ensureClient(options);
|
|
201
|
+
try {
|
|
202
|
+
return {
|
|
203
|
+
...await client.status(),
|
|
204
|
+
endpoint,
|
|
205
|
+
spawned
|
|
206
|
+
};
|
|
207
|
+
} finally {
|
|
208
|
+
client.close();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function status(options = {}) {
|
|
212
|
+
const resolved = resolveDaemonOptions(options);
|
|
213
|
+
let client;
|
|
214
|
+
try {
|
|
215
|
+
client = await connectOnly(resolved.endpoint);
|
|
216
|
+
} catch {
|
|
217
|
+
return {
|
|
218
|
+
running: false,
|
|
219
|
+
endpoint: resolved.endpoint
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
return {
|
|
224
|
+
...await client.status(),
|
|
225
|
+
running: true
|
|
226
|
+
};
|
|
227
|
+
} finally {
|
|
228
|
+
client.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function stop(options = {}) {
|
|
232
|
+
const resolved = resolveDaemonOptions(options);
|
|
233
|
+
let client;
|
|
234
|
+
try {
|
|
235
|
+
client = await connectOnly(resolved.endpoint);
|
|
236
|
+
} catch {
|
|
237
|
+
return {
|
|
238
|
+
stopped: false,
|
|
239
|
+
endpoint: resolved.endpoint
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
await client.shutdown();
|
|
244
|
+
return {
|
|
245
|
+
stopped: true,
|
|
246
|
+
endpoint: resolved.endpoint
|
|
247
|
+
};
|
|
248
|
+
} finally {
|
|
249
|
+
client.close();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function screenshot$1(options) {
|
|
253
|
+
const { daemon, ...rest } = options;
|
|
254
|
+
const client = await connect(daemon || {});
|
|
255
|
+
try {
|
|
256
|
+
return await client.screenshot(rest);
|
|
257
|
+
} finally {
|
|
258
|
+
client.close();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/index.ts
|
|
264
|
+
/**
|
|
265
|
+
* The library's one runtime: a pool of worker processes plus its lifecycle.
|
|
266
|
+
*
|
|
267
|
+
* `runtime` below is the singleton, because the expensive part is the
|
|
268
|
+
* processes and a second runtime would double them for no gain. Anyone who
|
|
269
|
+
* genuinely wants two constructs a Runtime directly.
|
|
270
|
+
*
|
|
271
|
+
* Its pool lives and dies with this process. `daemon` is the same pool behind
|
|
272
|
+
* a socket, for callers whose process does not live long enough to be worth
|
|
273
|
+
* starting one.
|
|
274
|
+
*/
|
|
275
|
+
var Runtime = class extends EventEmitter {
|
|
276
|
+
pool = null;
|
|
277
|
+
get running() {
|
|
278
|
+
return this.pool !== null;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Starts the pool. Safe to call twice; the second call is a no-op, so that
|
|
282
|
+
* library code can call it defensively.
|
|
283
|
+
*
|
|
284
|
+
* Every option has a default: the binary is `$SHOTIUM_BINARY`, then the
|
|
285
|
+
* platform package, then `./bin/shotium.exe`; the worker count is half the
|
|
286
|
+
* cores, at least one and at most four; the cache root is a directory under
|
|
287
|
+
* the system temp, and `null` disables caching.
|
|
288
|
+
*/
|
|
289
|
+
start(options = {}) {
|
|
290
|
+
if (this.pool) return this;
|
|
291
|
+
const pool = new Pool(resolveStartOptions(options));
|
|
292
|
+
this.pool = pool;
|
|
293
|
+
for (const event of [
|
|
294
|
+
"ready",
|
|
295
|
+
"exit",
|
|
296
|
+
"crash",
|
|
297
|
+
"timeout",
|
|
298
|
+
"worker-restart",
|
|
299
|
+
"worker-error",
|
|
300
|
+
"stderr"
|
|
301
|
+
]) pool.on(event, (payload) => this.emit(event, payload));
|
|
302
|
+
pool.start();
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
/** Stops every worker. The pool can be started again afterwards. */
|
|
306
|
+
async stop() {
|
|
307
|
+
if (!this.pool) return;
|
|
308
|
+
const pool = this.pool;
|
|
309
|
+
this.pool = null;
|
|
310
|
+
await pool.stop();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Renders one screenshot. Resolves to the encoded image, or to `null` when
|
|
314
|
+
* `path` was given and the worker wrote the file itself.
|
|
315
|
+
*/
|
|
316
|
+
async screenshot(options) {
|
|
317
|
+
const request = toRequest(options);
|
|
318
|
+
if (!this.pool) this.start();
|
|
319
|
+
const retry = typeof options.retry === "number" ? options.retry : 0;
|
|
320
|
+
return (await this.pool.submit(request, {
|
|
321
|
+
timeout: timeoutFor(options) + SUPERVISOR_MARGIN_MS,
|
|
322
|
+
retry
|
|
323
|
+
})).image;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
/** The shared pool: one per process, started on first use. */
|
|
327
|
+
const runtime = new Runtime();
|
|
328
|
+
/** One screenshot through the shared pool, starting it if it is not up. */
|
|
329
|
+
const screenshot = (options) => runtime.screenshot(options);
|
|
330
|
+
/**
|
|
331
|
+
* The resident pool: workers that outlive the process that started them,
|
|
332
|
+
* reachable over a named pipe on Windows and a unix socket elsewhere. For
|
|
333
|
+
* callers that are short-lived themselves. See lib/daemon.ts.
|
|
334
|
+
*/
|
|
335
|
+
const daemon = {
|
|
336
|
+
connect,
|
|
337
|
+
screenshot: screenshot$1,
|
|
338
|
+
start,
|
|
339
|
+
status,
|
|
340
|
+
stop
|
|
341
|
+
};
|
|
342
|
+
var src_default = {
|
|
343
|
+
Runtime,
|
|
344
|
+
runtime,
|
|
345
|
+
screenshot,
|
|
346
|
+
daemon
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
//#endregion
|
|
350
|
+
export { Runtime, daemon, src_default as default, runtime, screenshot };
|
|
351
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["screenshot","client.connect","client.screenshot","client.start","client.status","client.stop"],"sources":["../src/lib/client.ts","../src/index.ts"],"sourcesContent":["import {spawn} from 'node:child_process';\nimport {EventEmitter} from 'node:events';\nimport fs from 'node:fs';\nimport net from 'node:net';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nimport type {\n DaemonOptions,\n DaemonStatus,\n ScreenshotOptions,\n} from '../types.js';\n\nimport {resolveStartOptions} from './config.js';\nimport {endpointFor} from './endpoint.js';\nimport {FrameReader, encodeFrame} from './protocol.js';\nimport {timeoutFor, toRequest} from './request.js';\n\n// ESM has no __dirname. This is the same thing, from the module's own URL.\nconst HERE = path.dirname(fileURLToPath(import.meta.url));\n\n// The detached daemon's entry point, which is a build output beside this one.\n// It is spawned as `node <path>`, so it has to be a file on disk with a name\n// that does not move -- see tsdown.config.ts, where it is an entry of its own\n// for exactly that reason.\nconst DAEMON_MAIN = path.join(HERE, 'daemon_main.js');\n// How long to wait for a daemon this process just started to bind its\n// endpoint. Binding happens after the workers are spawned but before they are\n// warm, so this covers process startup and nothing else.\nconst START_TIMEOUT_MS = 20000;\nconst CONNECT_RETRY_MS = 20;\n\ninterface ClientReply {\n id: number;\n ok?: boolean;\n error?: string;\n path?: string;\n}\n\ninterface ClientResult {\n header: ClientReply;\n image: Buffer|null;\n}\n\ninterface Pending {\n resolve: (result: ClientResult) => void;\n reject: (error: Error) => void;\n}\n\ninterface ResolvedDaemonOptions {\n binary: string;\n workers: number;\n cacheDir: string|null;\n args: string[];\n name: string|undefined;\n endpoint: string;\n idleTimeoutMs: number|undefined;\n prewarm: boolean|undefined;\n logFile: string|null;\n}\n\n// The client half of the resident daemon.\n//\n// One connection can carry several requests at once, which is the difference\n// between this and the worker protocol underneath: every message carries an\n// `id` and the answers are matched back by it, so a caller can fire ten\n// screenshots down one socket and let the pool on the other side spread them\n// across workers.\nclass DaemonClient extends EventEmitter {\n private readonly socket: net.Socket;\n private readonly endpointPath: string;\n private readonly pending = new Map<number, Pending>();\n private nextId = 1;\n private header: ClientReply|null = null;\n private reader = new FrameReader();\n\n constructor(socket: net.Socket, endpoint: string) {\n super();\n this.socket = socket;\n this.endpointPath = endpoint;\n\n socket.on('data', (chunk: Buffer) => this.onData(chunk));\n socket.on('error', (error: Error) => this.failAll(error));\n socket.on('close', () => {\n this.failAll(new Error('shotium: the daemon closed the connection'));\n this.emit('close', {});\n });\n }\n\n get endpoint(): string {\n return this.endpointPath;\n }\n\n get closed(): boolean {\n return this.socket.destroyed;\n }\n\n private onData(chunk: Buffer): void {\n this.reader.push(chunk);\n for (;;) {\n const frame = this.reader.next();\n if (frame === null) {\n return;\n }\n if (this.header === null) {\n try {\n this.header = JSON.parse(frame.toString('utf8')) as ClientReply;\n } catch {\n this.failAll(\n new Error('shotium: the daemon sent a header that is not JSON'));\n return;\n }\n continue;\n }\n const header = this.header;\n this.header = null;\n this.settle(header, frame);\n }\n }\n\n private settle(header: ClientReply, payload: Buffer): void {\n const pending = this.pending.get(header.id);\n if (!pending) {\n return;\n }\n this.pending.delete(header.id);\n if (header.ok) {\n pending.resolve({header, image: header.path ? null : payload});\n } else {\n pending.reject(new Error(header.error || 'shotium: request failed'));\n }\n }\n\n private failAll(error: Error): void {\n for (const [, pending] of this.pending) {\n pending.reject(error);\n }\n this.pending.clear();\n }\n\n // Sends one message and resolves with {header, image}.\n send(message: Record<string, unknown>): Promise<ClientResult> {\n return new Promise<ClientResult>((resolve, reject) => {\n if (this.socket.destroyed) {\n reject(new Error('shotium: not connected to a daemon'));\n return;\n }\n const id = this.nextId++;\n this.pending.set(id, {resolve, reject});\n this.socket.write(\n encodeFrame(Buffer.from(JSON.stringify({...message, id}), 'utf8')));\n });\n }\n\n /** Resolves to the image, or to null when `path` was given. */\n async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n const request = toRequest(options);\n const retry = typeof options.retry === 'number' ? options.retry : 0;\n const result = await this.send({\n op: 'screenshot',\n request,\n timeout: timeoutFor(options),\n retry,\n });\n return result.image;\n }\n\n async status(): Promise<DaemonStatus> {\n const {header} = await this.send({op: 'status'});\n return header as unknown as DaemonStatus;\n }\n\n async shutdown(): Promise<{ok: boolean}> {\n const {header} = await this.send({op: 'shutdown'});\n return {ok: header.ok === true};\n }\n\n close(): void {\n this.socket.end();\n this.socket.destroy();\n }\n}\n\n// Opens a connection to a daemon that is already listening, and fails if there\n// is not one. Nothing is spawned here: a caller that wants a daemon started\n// says so, because starting one is a side effect on the machine and not the\n// sort of thing a status query should do.\nfunction connectOnly(endpoint: string): Promise<DaemonClient> {\n return new Promise<DaemonClient>((resolve, reject) => {\n const socket = net.connect(endpoint);\n const onError = (error: Error) => {\n socket.destroy();\n reject(error);\n };\n socket.once('error', onError);\n socket.once('connect', () => {\n socket.removeListener('error', onError);\n resolve(new DaemonClient(socket, endpoint));\n });\n });\n}\n\nfunction resolveDaemonOptions(options: DaemonOptions = {}):\n ResolvedDaemonOptions {\n const resolved = resolveStartOptions(options);\n return {\n ...resolved,\n name: options.name,\n endpoint: endpointFor({\n ...resolved,\n name: options.name,\n endpoint: options.endpoint,\n }),\n idleTimeoutMs: options.idleTimeoutMs,\n prewarm: options.prewarm,\n logFile: options.logFile || process.env.SHOTIUM_DAEMON_LOG || null,\n };\n}\n\nfunction spawnDaemon(options: ResolvedDaemonOptions): void {\n const config = {\n binary: options.binary,\n workers: options.workers,\n cacheDir: options.cacheDir,\n args: options.args,\n endpoint: options.endpoint,\n idleTimeoutMs: options.idleTimeoutMs,\n prewarm: options.prewarm,\n };\n const encoded =\n Buffer.from(JSON.stringify(config), 'utf8').toString('base64');\n\n // Detached, with the standard streams let go of: the daemon has to outlive\n // the process that started it, and a child still holding this process's pipes\n // would keep it from exiting -- the exact failure that makes a \"background\"\n // daemon hang a shell.\n let stdio: 'ignore'|['ignore', number, number] = 'ignore';\n let logFd: number|null = null;\n if (options.logFile) {\n logFd = fs.openSync(options.logFile, 'a');\n stdio = ['ignore', logFd, logFd];\n }\n const child = spawn(process.execPath, [DAEMON_MAIN, encoded], {\n detached: true,\n stdio,\n windowsHide: true,\n });\n child.unref();\n if (logFd !== null) {\n fs.closeSync(logFd);\n }\n}\n\nconst sleep = (ms: number) =>\n new Promise<void>((resolve) => setTimeout(resolve, ms));\n\nexport interface EnsuredClient {\n client: DaemonClient;\n spawned: boolean;\n endpoint: string;\n}\n\n// Connects, starting a daemon if none answers.\n//\n// The endpoint existing is the readiness signal, so this is a connect loop\n// rather than a handshake: a daemon that has bound can be talked to, and one\n// that has not is indistinguishable from one that was never started. Several\n// processes racing here is fine -- the losers' daemons exit on EADDRINUSE and\n// everyone ends up on the winner.\nasync function ensureClient(options: DaemonOptions = {}):\n Promise<EnsuredClient> {\n const resolved = resolveDaemonOptions(options);\n try {\n const client = await connectOnly(resolved.endpoint);\n return {client, spawned: false, endpoint: resolved.endpoint};\n } catch {\n if (options.spawn === false) {\n throw new Error(`shotium: no daemon at ${resolved.endpoint}`);\n }\n }\n\n spawnDaemon(resolved);\n const deadline = Date.now() +\n (options.startTimeoutMs === undefined ? START_TIMEOUT_MS :\n options.startTimeoutMs);\n for (;;) {\n try {\n const client = await connectOnly(resolved.endpoint);\n return {client, spawned: true, endpoint: resolved.endpoint};\n } catch {\n if (Date.now() >= deadline) {\n throw new Error(\n `shotium: the daemon did not come up at ${resolved.endpoint}`);\n }\n await sleep(CONNECT_RETRY_MS);\n }\n }\n}\n\n// The five things a caller does with a daemon. Each opens a connection, does\n// one thing and closes it, which is the shape a short-lived process wants; a\n// service that will send more than one request calls connect() and keeps the\n// client.\nasync function connect(options: DaemonOptions = {}): Promise<DaemonClient> {\n const {client} = await ensureClient(options);\n return client;\n}\n\nasync function start(options: DaemonOptions = {}):\n Promise<DaemonStatus&{spawned: boolean}> {\n const {client, spawned, endpoint} = await ensureClient(options);\n try {\n const status = await client.status();\n return {...status, endpoint, spawned};\n } finally {\n client.close();\n }\n}\n\nasync function status(options: DaemonOptions = {}):\n Promise<Partial<DaemonStatus>&{running: boolean, endpoint: string}> {\n const resolved = resolveDaemonOptions(options);\n let client: DaemonClient;\n try {\n client = await connectOnly(resolved.endpoint);\n } catch {\n return {running: false, endpoint: resolved.endpoint};\n }\n try {\n return {...(await client.status()), running: true};\n } finally {\n client.close();\n }\n}\n\nasync function stop(options: DaemonOptions = {}):\n Promise<{stopped: boolean, endpoint: string}> {\n const resolved = resolveDaemonOptions(options);\n let client: DaemonClient;\n try {\n client = await connectOnly(resolved.endpoint);\n } catch {\n return {stopped: false, endpoint: resolved.endpoint};\n }\n try {\n await client.shutdown();\n return {stopped: true, endpoint: resolved.endpoint};\n } finally {\n client.close();\n }\n}\n\n// One screenshot through the daemon, connection and all. `daemon` carries the\n// pool's configuration -- binary, workers, cache root -- and is stripped out\n// here rather than sent, because it says which daemon to talk to and not what\n// to photograph.\nasync function screenshot(options: ScreenshotOptions&{daemon?: DaemonOptions}):\n Promise<Buffer|null> {\n const {daemon, ...rest} = options;\n const client = await connect(daemon || {});\n try {\n return await client.screenshot(rest);\n } finally {\n client.close();\n }\n}\n\nexport {\n DaemonClient,\n connect,\n ensureClient,\n resolveDaemonOptions,\n screenshot,\n start,\n status,\n stop,\n};\n","import {EventEmitter} from 'node:events';\n\nimport * as client from './lib/client.js';\nimport type {DaemonClient} from './lib/client.js';\nimport {resolveStartOptions} from './lib/config.js';\nimport {Pool} from './lib/pool.js';\nimport {SUPERVISOR_MARGIN_MS, timeoutFor, toRequest} from './lib/request.js';\nimport type {\n DaemonOptions,\n DaemonStatus,\n ScreenshotOptions,\n StartOptions,\n WorkerEvent,\n} from './types.js';\n\nexport type {\n Clip,\n DaemonOptions,\n DaemonStatus,\n PageGotoParams,\n PurgeOptions,\n ScreenshotOptions,\n StartOptions,\n Viewport,\n WorkerEvent,\n} from './types.js';\nexport type {DaemonClient} from './lib/client.js';\n\n/** The five things a caller does with the resident pool. */\nexport interface Daemon {\n /** Connects, starting a daemon if none is listening. */\n connect(options?: DaemonOptions): Promise<DaemonClient>;\n /** One screenshot through the daemon, connection and all. */\n screenshot(options: ScreenshotOptions&{daemon?: DaemonOptions}):\n Promise<Buffer|null>;\n /** Starts one if it is not up, and reports what is there either way. */\n start(options?: DaemonOptions): Promise<DaemonStatus&{spawned: boolean}>;\n status(options?: DaemonOptions):\n Promise<Partial<DaemonStatus>&{running: boolean, endpoint: string}>;\n stop(options?: DaemonOptions): Promise<{stopped: boolean, endpoint: string}>;\n}\n\n// The events the pool forwards, and the only ones. Declared as an interface\n// merged into the class below rather than as a catch-all `on(string, ...)`,\n// so that a listener for an event this runtime never emits is a compile error\n// rather than a callback nobody ever calls.\nexport interface Runtime {\n on(event: 'ready', listener: (info: {workers: number}) => void): this;\n on(event: 'exit', listener: (event: WorkerEvent) => void): this;\n on(event: 'crash', listener: (event: WorkerEvent) => void): this;\n on(event: 'timeout',\n listener: (event: {worker: number, timeout: number}) => void): this;\n on(event: 'worker-restart',\n listener: (event: {worker: number, reason: string, delay: number}) => void):\n this;\n /** A worker could not be started at all -- a missing or unusable binary. */\n on(event: 'worker-error',\n listener: (event: {worker: number, error: Error}) => void): this;\n on(event: 'stderr',\n listener: (event: {worker: number, line: string}) => void): this;\n}\n\n/**\n * The library's one runtime: a pool of worker processes plus its lifecycle.\n *\n * `runtime` below is the singleton, because the expensive part is the\n * processes and a second runtime would double them for no gain. Anyone who\n * genuinely wants two constructs a Runtime directly.\n *\n * Its pool lives and dies with this process. `daemon` is the same pool behind\n * a socket, for callers whose process does not live long enough to be worth\n * starting one.\n */\nexport class Runtime extends EventEmitter {\n private pool: Pool|null = null;\n\n get running(): boolean {\n return this.pool !== null;\n }\n\n /**\n * Starts the pool. Safe to call twice; the second call is a no-op, so that\n * library code can call it defensively.\n *\n * Every option has a default: the binary is `$SHOTIUM_BINARY`, then the\n * platform package, then `./bin/shotium.exe`; the worker count is half the\n * cores, at least one and at most four; the cache root is a directory under\n * the system temp, and `null` disables caching.\n */\n start(options: StartOptions = {}): this {\n if (this.pool) {\n return this;\n }\n const pool = new Pool(resolveStartOptions(options));\n this.pool = pool;\n for (const event\n of ['ready', 'exit', 'crash', 'timeout', 'worker-restart',\n 'worker-error', 'stderr']) {\n pool.on(event, (payload) => this.emit(event, payload));\n }\n pool.start();\n return this;\n }\n\n /** Stops every worker. The pool can be started again afterwards. */\n async stop(): Promise<void> {\n if (!this.pool) {\n return;\n }\n const pool = this.pool;\n this.pool = null;\n await pool.stop();\n }\n\n /**\n * Renders one screenshot. Resolves to the encoded image, or to `null` when\n * `path` was given and the worker wrote the file itself.\n */\n async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n // Validate before starting anything. A malformed request should not cost a\n // pool of worker processes to discover, and toRequest() is the only check\n // that can be made without one.\n const request = toRequest(options);\n if (!this.pool) {\n this.start();\n }\n const retry = typeof options.retry === 'number' ? options.retry : 0;\n const result = await this.pool!.submit(request, {\n timeout: timeoutFor(options) + SUPERVISOR_MARGIN_MS,\n retry,\n });\n return result.image;\n }\n}\n\n/** The shared pool: one per process, started on first use. */\nconst runtime = new Runtime();\n\n/** One screenshot through the shared pool, starting it if it is not up. */\nconst screenshot = (options: ScreenshotOptions): Promise<Buffer|null> =>\n runtime.screenshot(options);\n\n/**\n * The resident pool: workers that outlive the process that started them,\n * reachable over a named pipe on Windows and a unix socket elsewhere. For\n * callers that are short-lived themselves. See lib/daemon.ts.\n */\nconst daemon: Daemon = {\n connect: client.connect,\n screenshot: client.screenshot,\n start: client.start,\n status: client.status,\n stop: client.stop,\n};\n\nexport {runtime, screenshot, daemon};\n\n// A default as well as the names, because `import shotium from` is what a\n// caller coming from `require` writes first, and the two have to be the same\n// object rather than two views that drift.\nexport default {Runtime, runtime, screenshot, daemon};\n"],"mappings":"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"}
|
package/dist/native.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { i as NativeStartOptions, o as PurgeOptions, s as ScreenshotOptions } from "./types-x9HtkzeE.js";
|
|
2
|
+
//#region src/native.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The engine, in this process, and the queue in front of it.
|
|
5
|
+
*
|
|
6
|
+
* Same options and same output as `runtime`, and a different set of trades.
|
|
7
|
+
* There is no worker process, so there is nothing to start, nothing to find on
|
|
8
|
+
* disk and no pipe: a screenshot costs about a third less than through the
|
|
9
|
+
* pool, and the whole thing is one process instead of five.
|
|
10
|
+
*
|
|
11
|
+
* What it gives up is what a separate process was providing for free. One
|
|
12
|
+
* renderer, because blink is a process-wide singleton and `worker_threads`
|
|
13
|
+
* share the process, so requests are serialised however many callers there
|
|
14
|
+
* are. And no crash isolation: a renderer that dies takes the host program
|
|
15
|
+
* with it, where the pool would have retried.
|
|
16
|
+
*
|
|
17
|
+
* The queue is not about fairness. Each capture occupies a libuv thread pool
|
|
18
|
+
* thread for as long as the render takes, and there are four of those by
|
|
19
|
+
* default, shared with fs and dns -- so letting four screenshots go at once
|
|
20
|
+
* would stall the host's file reads for a fifth of a second at a time while
|
|
21
|
+
* gaining nothing, since the engine serialises them anyway.
|
|
22
|
+
*/
|
|
23
|
+
declare class NativeRuntime {
|
|
24
|
+
private engine;
|
|
25
|
+
private tail;
|
|
26
|
+
get running(): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Starts the engine. Safe to call twice; the second call is a no-op.
|
|
29
|
+
*
|
|
30
|
+
* `cacheDir` is the HTTP disk cache and `null` disables it, which is the
|
|
31
|
+
* default here. `resourceDir` is where `shotium_data.pak` and
|
|
32
|
+
* `shotium_strings.pak` are, and defaults to the directory the addon was
|
|
33
|
+
* loaded from, which is where they ship.
|
|
34
|
+
*/
|
|
35
|
+
start(options?: NativeStartOptions): this;
|
|
36
|
+
/** Stops the engine, after whatever is queued. */
|
|
37
|
+
stop(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Hands back what the engine is holding but can rebuild.
|
|
40
|
+
* `releaseWorkingSet` additionally asks the OS for the pages, which the next
|
|
41
|
+
* screenshot pays back in soft faults -- worth it when there may not be a
|
|
42
|
+
* next one soon.
|
|
43
|
+
*
|
|
44
|
+
* The resident worker does this for itself on a timer because it can watch
|
|
45
|
+
* its own request stream go quiet. Here the queue belongs to the caller, so
|
|
46
|
+
* the caller is the one who knows a batch has ended.
|
|
47
|
+
*/
|
|
48
|
+
purge({ releaseWorkingSet }?: PurgeOptions): void;
|
|
49
|
+
/**
|
|
50
|
+
* Renders one screenshot. Resolves to the encoded image, or to `null` when
|
|
51
|
+
* `path` was given and the engine wrote the file itself.
|
|
52
|
+
*/
|
|
53
|
+
screenshot(options: ScreenshotOptions): Promise<Buffer | null>;
|
|
54
|
+
}
|
|
55
|
+
/** The shared in-process engine, started on first use. */
|
|
56
|
+
declare const native: NativeRuntime;
|
|
57
|
+
/** One screenshot through the shared in-process engine. */
|
|
58
|
+
declare const screenshot: (options: ScreenshotOptions) => Promise<Buffer | null>;
|
|
59
|
+
declare const _default: {
|
|
60
|
+
NativeRuntime: typeof NativeRuntime;
|
|
61
|
+
native: NativeRuntime;
|
|
62
|
+
screenshot: (options: ScreenshotOptions) => Promise<Buffer | null>;
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
export { NativeRuntime, type NativeStartOptions, type PurgeOptions, type ScreenshotOptions, _default as default, native, screenshot };
|
|
66
|
+
//# sourceMappingURL=native.d.ts.map
|
package/dist/native.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { n as packageDir, r as packageName } from "./platform-DU8DYqmA.js";
|
|
2
|
+
import { r as toRequest } from "./request-qZXS3N9f.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
//#region src/native.ts
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
function candidates() {
|
|
12
|
+
const found = [];
|
|
13
|
+
const dir = packageDir();
|
|
14
|
+
if (dir) found.push(path.join(dir, "shotium.node"));
|
|
15
|
+
found.push(path.join(HERE, "..", "native", "build", "Release", "shotium.node"));
|
|
16
|
+
return found;
|
|
17
|
+
}
|
|
18
|
+
let binding = null;
|
|
19
|
+
let bindingDir = null;
|
|
20
|
+
function load() {
|
|
21
|
+
if (binding) return binding;
|
|
22
|
+
const tried = candidates();
|
|
23
|
+
for (const candidate of tried) {
|
|
24
|
+
if (!fs.existsSync(candidate)) continue;
|
|
25
|
+
binding = require(candidate);
|
|
26
|
+
bindingDir = path.dirname(candidate);
|
|
27
|
+
return binding;
|
|
28
|
+
}
|
|
29
|
+
const expected = packageName();
|
|
30
|
+
throw new Error(`shotium: no native engine for this platform.
|
|
31
|
+
looked in:\n ${tried.join("\n ")}\n` + (expected ? ` It ships in ${expected}, which npm installs as an optional dependency of this package.
|
|
32
|
+
` : ` There is no build for ${process.platform}-${process.arch}.\n`) + " import(\"@shotkit/shotium\") uses worker processes instead and needs no addon.");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The engine, in this process, and the queue in front of it.
|
|
36
|
+
*
|
|
37
|
+
* Same options and same output as `runtime`, and a different set of trades.
|
|
38
|
+
* There is no worker process, so there is nothing to start, nothing to find on
|
|
39
|
+
* disk and no pipe: a screenshot costs about a third less than through the
|
|
40
|
+
* pool, and the whole thing is one process instead of five.
|
|
41
|
+
*
|
|
42
|
+
* What it gives up is what a separate process was providing for free. One
|
|
43
|
+
* renderer, because blink is a process-wide singleton and `worker_threads`
|
|
44
|
+
* share the process, so requests are serialised however many callers there
|
|
45
|
+
* are. And no crash isolation: a renderer that dies takes the host program
|
|
46
|
+
* with it, where the pool would have retried.
|
|
47
|
+
*
|
|
48
|
+
* The queue is not about fairness. Each capture occupies a libuv thread pool
|
|
49
|
+
* thread for as long as the render takes, and there are four of those by
|
|
50
|
+
* default, shared with fs and dns -- so letting four screenshots go at once
|
|
51
|
+
* would stall the host's file reads for a fifth of a second at a time while
|
|
52
|
+
* gaining nothing, since the engine serialises them anyway.
|
|
53
|
+
*/
|
|
54
|
+
var NativeRuntime = class {
|
|
55
|
+
engine = null;
|
|
56
|
+
tail = Promise.resolve();
|
|
57
|
+
get running() {
|
|
58
|
+
return this.engine !== null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Starts the engine. Safe to call twice; the second call is a no-op.
|
|
62
|
+
*
|
|
63
|
+
* `cacheDir` is the HTTP disk cache and `null` disables it, which is the
|
|
64
|
+
* default here. `resourceDir` is where `shotium_data.pak` and
|
|
65
|
+
* `shotium_strings.pak` are, and defaults to the directory the addon was
|
|
66
|
+
* loaded from, which is where they ship.
|
|
67
|
+
*/
|
|
68
|
+
start(options = {}) {
|
|
69
|
+
if (this.engine) return this;
|
|
70
|
+
const native = load();
|
|
71
|
+
const engineOptions = {};
|
|
72
|
+
if (options.cacheDir !== null && options.cacheDir !== void 0) engineOptions.cacheDir = options.cacheDir;
|
|
73
|
+
if (options.userAgent !== void 0) engineOptions.userAgent = options.userAgent;
|
|
74
|
+
engineOptions.resourceDir = options.resourceDir || bindingDir;
|
|
75
|
+
this.engine = native.create(JSON.stringify(engineOptions));
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
/** Stops the engine, after whatever is queued. */
|
|
79
|
+
async stop() {
|
|
80
|
+
if (!this.engine) return;
|
|
81
|
+
const engine = this.engine;
|
|
82
|
+
this.engine = null;
|
|
83
|
+
await this.tail.catch(() => {});
|
|
84
|
+
load().destroy(engine);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Hands back what the engine is holding but can rebuild.
|
|
88
|
+
* `releaseWorkingSet` additionally asks the OS for the pages, which the next
|
|
89
|
+
* screenshot pays back in soft faults -- worth it when there may not be a
|
|
90
|
+
* next one soon.
|
|
91
|
+
*
|
|
92
|
+
* The resident worker does this for itself on a timer because it can watch
|
|
93
|
+
* its own request stream go quiet. Here the queue belongs to the caller, so
|
|
94
|
+
* the caller is the one who knows a batch has ended.
|
|
95
|
+
*/
|
|
96
|
+
purge({ releaseWorkingSet = false } = {}) {
|
|
97
|
+
if (!this.engine) return;
|
|
98
|
+
load().purge(this.engine, releaseWorkingSet);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Renders one screenshot. Resolves to the encoded image, or to `null` when
|
|
102
|
+
* `path` was given and the engine wrote the file itself.
|
|
103
|
+
*/
|
|
104
|
+
async screenshot(options) {
|
|
105
|
+
const request = toRequest(options);
|
|
106
|
+
if (!this.engine) this.start();
|
|
107
|
+
const engine = this.engine;
|
|
108
|
+
const native = load();
|
|
109
|
+
const result = this.tail.catch(() => {}).then(() => native.capture(engine, JSON.stringify(request)));
|
|
110
|
+
this.tail = result.catch(() => {});
|
|
111
|
+
const image = await result;
|
|
112
|
+
return request.path ? null : image;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/** The shared in-process engine, started on first use. */
|
|
116
|
+
const native = new NativeRuntime();
|
|
117
|
+
/** One screenshot through the shared in-process engine. */
|
|
118
|
+
const screenshot = (options) => native.screenshot(options);
|
|
119
|
+
var native_default = {
|
|
120
|
+
NativeRuntime,
|
|
121
|
+
native,
|
|
122
|
+
screenshot
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
export { NativeRuntime, native_default as default, native, screenshot };
|
|
127
|
+
//# sourceMappingURL=native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native.js","names":["platformPackage.packageDir","platformPackage.packageName"],"sources":["../src/native.ts"],"sourcesContent":["import fs from 'node:fs';\nimport {createRequire} from 'node:module';\nimport path from 'node:path';\nimport {fileURLToPath} from 'node:url';\n\nimport * as platformPackage from './lib/platform.js';\nimport {toRequest} from './lib/request.js';\nimport type {\n NativeStartOptions,\n PurgeOptions,\n ScreenshotOptions,\n} from './types.js';\n\nexport type {\n NativeStartOptions,\n PurgeOptions,\n ScreenshotOptions,\n} from './types.js';\n\n// A .node addon is a CommonJS artefact: there is no ESM loader for one.\nconst require = createRequire(import.meta.url);\n\n// ESM has no __dirname. This is the same thing, from the module's own URL.\nconst HERE = path.dirname(fileURLToPath(import.meta.url));\n\n// The engine handle the addon hands back. It is opaque on purpose: everything\n// that can be done with it is a call on the binding below.\ntype Engine = unknown;\n\n// What native/binding.cc exports. See shot/shot_api.h for the C ABI under it.\ninterface NativeBinding {\n create(optionsJson: string): Engine;\n destroy(engine: Engine): void;\n purge(engine: Engine, releaseWorkingSet: boolean): void;\n capture(engine: Engine, requestJson: string): Promise<Buffer>;\n}\n\n// shot in this process, instead of in workers beside it.\n//\n// The difference from `runtime` is not the API, which is the same\n// screenshot(options), and not the request format, which is byte for byte the\n// same JSON. It is where blink is:\n//\n// runtime N worker processes, one screenshot each at a time, a crash is a\n// retry, memory is N copies of an engine\n// native one engine in this process, one screenshot at a time ever, a\n// crash takes the program with it, memory is one copy\n//\n// One at a time is not a limitation of this file. Blink is a process-wide\n// singleton -- it is initialised once and there is no path to a second one --\n// so an in-process engine is one renderer no matter how it is driven, and\n// worker_threads do not change that because they share the process. A caller\n// who wants four screenshots at once wants four processes, which is what the\n// pool is for.\n//\n// What it buys is that there is no process to start, nothing to find on disk,\n// no pipe, and no supervisor: a program that takes a handful of screenshots\n// and exits pays for one engine and talks to it directly.\n\n// Where the addon and the library beside it live.\n//\n// The platform package is what ships -- the .node sits next to the shared\n// library it is linked against, which is the whole reason the two travel in\n// one package rather than two. native/build/Release is where node-gyp puts a\n// local build; it exists in a checkout and not in an install, so the two never\n// compete in practice. Both paths are relative to this file's build output,\n// which is one directory below the package root.\nfunction candidates(): string[] {\n const found: string[] = [];\n const dir = platformPackage.packageDir();\n if (dir) {\n found.push(path.join(dir, 'shotium.node'));\n }\n found.push(\n path.join(HERE, '..', 'native', 'build', 'Release', 'shotium.node'));\n return found;\n}\n\nlet binding: NativeBinding|null = null;\nlet bindingDir: string|null = null;\n\nfunction load(): NativeBinding {\n if (binding) {\n return binding;\n }\n const tried = candidates();\n for (const candidate of tried) {\n if (!fs.existsSync(candidate)) {\n continue;\n }\n // Not wrapped in a try: a .node that is there and will not load is a\n // broken installation, and the loader's own message -- a missing\n // dependency, an architecture mismatch -- says more than anything that\n // could be substituted for it.\n binding = require(candidate) as NativeBinding;\n bindingDir = path.dirname(candidate);\n return binding;\n }\n const expected = platformPackage.packageName();\n throw new Error(\n 'shotium: no native engine for this platform.\\n' +\n ` looked in:\\n ${tried.join('\\n ')}\\n` +\n (expected ?\n ` It ships in ${expected}, which npm installs as an optional ` +\n 'dependency of this package.\\n' :\n ` There is no build for ${process.platform}-${process.arch}.\\n`) +\n ' import(\"@shotkit/shotium\") uses worker processes instead and needs ' +\n 'no addon.');\n}\n\n/**\n * The engine, in this process, and the queue in front of it.\n *\n * Same options and same output as `runtime`, and a different set of trades.\n * There is no worker process, so there is nothing to start, nothing to find on\n * disk and no pipe: a screenshot costs about a third less than through the\n * pool, and the whole thing is one process instead of five.\n *\n * What it gives up is what a separate process was providing for free. One\n * renderer, because blink is a process-wide singleton and `worker_threads`\n * share the process, so requests are serialised however many callers there\n * are. And no crash isolation: a renderer that dies takes the host program\n * with it, where the pool would have retried.\n *\n * The queue is not about fairness. Each capture occupies a libuv thread pool\n * thread for as long as the render takes, and there are four of those by\n * default, shared with fs and dns -- so letting four screenshots go at once\n * would stall the host's file reads for a fifth of a second at a time while\n * gaining nothing, since the engine serialises them anyway.\n */\nexport class NativeRuntime {\n private engine: Engine|null = null;\n private tail: Promise<unknown> = Promise.resolve();\n\n get running(): boolean {\n return this.engine !== null;\n }\n\n /**\n * Starts the engine. Safe to call twice; the second call is a no-op.\n *\n * `cacheDir` is the HTTP disk cache and `null` disables it, which is the\n * default here. `resourceDir` is where `shotium_data.pak` and\n * `shotium_strings.pak` are, and defaults to the directory the addon was\n * loaded from, which is where they ship.\n */\n start(options: NativeStartOptions = {}): this {\n if (this.engine) {\n return this;\n }\n const native = load();\n\n const engineOptions: Record<string, unknown> = {};\n if (options.cacheDir !== null && options.cacheDir !== undefined) {\n engineOptions.cacheDir = options.cacheDir;\n }\n if (options.userAgent !== undefined) {\n engineOptions.userAgent = options.userAgent;\n }\n // The packs sit beside the library, and the library cannot find itself on\n // Linux -- the path the engine resolves for \"this module\" goes through\n // /proc/self/exe, which names node. Saying it here is cheaper than\n // teaching the engine a second way to look. See shot_api.h.\n engineOptions.resourceDir = options.resourceDir || bindingDir;\n\n this.engine = native.create(JSON.stringify(engineOptions));\n return this;\n }\n\n /** Stops the engine, after whatever is queued. */\n async stop(): Promise<void> {\n if (!this.engine) {\n return;\n }\n // After the queue, not before: destroy() waits for a capture in flight\n // anyway, and doing it in order means a caller's last screenshot resolves\n // rather than racing the shutdown.\n const engine = this.engine;\n this.engine = null;\n await this.tail.catch(() => {});\n load().destroy(engine);\n }\n\n /**\n * Hands back what the engine is holding but can rebuild.\n * `releaseWorkingSet` additionally asks the OS for the pages, which the next\n * screenshot pays back in soft faults -- worth it when there may not be a\n * next one soon.\n *\n * The resident worker does this for itself on a timer because it can watch\n * its own request stream go quiet. Here the queue belongs to the caller, so\n * the caller is the one who knows a batch has ended.\n */\n purge({releaseWorkingSet = false}: PurgeOptions = {}): void {\n if (!this.engine) {\n return;\n }\n load().purge(this.engine, releaseWorkingSet);\n }\n\n /**\n * Renders one screenshot. Resolves to the encoded image, or to `null` when\n * `path` was given and the engine wrote the file itself.\n */\n async screenshot(options: ScreenshotOptions): Promise<Buffer|null> {\n // Before anything else, and before the queue: a malformed request should\n // be a rejection now rather than one that waits its turn.\n const request = toRequest(options);\n if (!this.engine) {\n this.start();\n }\n const engine = this.engine;\n const native = load();\n\n // Chain onto the tail so that captures run one at a time. The catch keeps\n // one failure from poisoning everything queued behind it.\n const result = this.tail.catch(() => {}).then(\n () => native.capture(engine, JSON.stringify(request)));\n this.tail = result.catch(() => {});\n const image = await result;\n return request.path ? null : image;\n }\n}\n\n/** The shared in-process engine, started on first use. */\nconst native = new NativeRuntime();\n\n/** One screenshot through the shared in-process engine. */\nconst screenshot = (options: ScreenshotOptions): Promise<Buffer|null> =>\n native.screenshot(options);\n\nexport {native, screenshot};\n\nexport default {NativeRuntime, native, screenshot};\n"],"mappings":";;;;;;;;AAoBA,MAAM,UAAU,cAAc,YAAY,GAAG;AAG7C,MAAM,OAAO,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AA4CxD,SAAS,aAAuB;CAC9B,MAAM,QAAkB,CAAC;CACzB,MAAM,MAAMA,WAA2B;CACvC,IAAI,KACF,MAAM,KAAK,KAAK,KAAK,KAAK,cAAc,CAAC;CAE3C,MAAM,KACF,KAAK,KAAK,MAAM,MAAM,UAAU,SAAS,WAAW,cAAc,CAAC;CACvE,OAAO;AACT;AAEA,IAAI,UAA8B;AAClC,IAAI,aAA0B;AAE9B,SAAS,OAAsB;CAC7B,IAAI,SACF,OAAO;CAET,MAAM,QAAQ,WAAW;CACzB,KAAK,MAAM,aAAa,OAAO;EAC7B,IAAI,CAAC,GAAG,WAAW,SAAS,GAC1B;EAMF,UAAU,QAAQ,SAAS;EAC3B,aAAa,KAAK,QAAQ,SAAS;EACnC,OAAO;CACT;CACA,MAAM,WAAWC,YAA4B;CAC7C,MAAM,IAAI,MACN;oBACqB,MAAM,KAAK,QAAQ,EAAE,OACzC,WACI,iBAAiB,SAAS;IAE1B,2BAA2B,QAAQ,SAAS,GAAG,QAAQ,KAAK,QACjE,kFACW;AACjB;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAa,gBAAb,MAA2B;CACzB,AAAQ,SAAsB;CAC9B,AAAQ,OAAyB,QAAQ,QAAQ;CAEjD,IAAI,UAAmB;EACrB,OAAO,KAAK,WAAW;CACzB;;;;;;;;;CAUA,MAAM,UAA8B,CAAC,GAAS;EAC5C,IAAI,KAAK,QACP,OAAO;EAET,MAAM,SAAS,KAAK;EAEpB,MAAM,gBAAyC,CAAC;EAChD,IAAI,QAAQ,aAAa,QAAQ,QAAQ,aAAa,QACpD,cAAc,WAAW,QAAQ;EAEnC,IAAI,QAAQ,cAAc,QACxB,cAAc,YAAY,QAAQ;EAMpC,cAAc,cAAc,QAAQ,eAAe;EAEnD,KAAK,SAAS,OAAO,OAAO,KAAK,UAAU,aAAa,CAAC;EACzD,OAAO;CACT;;CAGA,MAAM,OAAsB;EAC1B,IAAI,CAAC,KAAK,QACR;EAKF,MAAM,SAAS,KAAK;EACpB,KAAK,SAAS;EACd,MAAM,KAAK,KAAK,YAAY,CAAC,CAAC;EAC9B,KAAK,CAAC,CAAC,QAAQ,MAAM;CACvB;;;;;;;;;;;CAYA,MAAM,EAAC,oBAAoB,UAAuB,CAAC,GAAS;EAC1D,IAAI,CAAC,KAAK,QACR;EAEF,KAAK,CAAC,CAAC,MAAM,KAAK,QAAQ,iBAAiB;CAC7C;;;;;CAMA,MAAM,WAAW,SAAkD;EAGjE,MAAM,UAAU,UAAU,OAAO;EACjC,IAAI,CAAC,KAAK,QACR,KAAK,MAAM;EAEb,MAAM,SAAS,KAAK;EACpB,MAAM,SAAS,KAAK;EAIpB,MAAM,SAAS,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,WAC/B,OAAO,QAAQ,QAAQ,KAAK,UAAU,OAAO,CAAC,CAAC;EACzD,KAAK,OAAO,OAAO,YAAY,CAAC,CAAC;EACjC,MAAM,QAAQ,MAAM;EACpB,OAAO,QAAQ,OAAO,OAAO;CAC/B;AACF;;AAGA,MAAM,SAAS,IAAI,cAAc;;AAGjC,MAAM,cAAc,YAChB,OAAO,WAAW,OAAO;AAI7B,qBAAe;CAAC;CAAe;CAAQ;AAAU"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/platform.ts
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const PACKAGES = {
|
|
7
|
+
"win32-x64": "@shotkit/shotium-win32-x64",
|
|
8
|
+
"win32-arm64": "@shotkit/shotium-win32-arm64",
|
|
9
|
+
"darwin-x64": "@shotkit/shotium-darwin-x64",
|
|
10
|
+
"darwin-arm64": "@shotkit/shotium-darwin-arm64",
|
|
11
|
+
"linux-x64": "@shotkit/shotium-linux-x64",
|
|
12
|
+
"linux-arm64": "@shotkit/shotium-linux-arm64"
|
|
13
|
+
};
|
|
14
|
+
function packageName(platform = process.platform, arch = process.arch) {
|
|
15
|
+
return PACKAGES[`${platform}-${arch}`] ?? null;
|
|
16
|
+
}
|
|
17
|
+
function packageDir() {
|
|
18
|
+
const name = packageName();
|
|
19
|
+
if (!name) return null;
|
|
20
|
+
try {
|
|
21
|
+
return path.dirname(require.resolve(`${name}/package.json`));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function binaryName() {
|
|
27
|
+
return process.platform === "win32" ? "shotium.exe" : "shotium";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { packageDir as n, packageName as r, binaryName as t };
|
|
32
|
+
//# sourceMappingURL=platform-DU8DYqmA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-DU8DYqmA.js","names":[],"sources":["../src/lib/platform.ts"],"sourcesContent":["import {createRequire} from 'node:module';\nimport path from 'node:path';\n\n// require.resolve is the resolver, and ESM has no synchronous equivalent\n// that answers for a package that may not be installed at all.\nconst require = createRequire(import.meta.url);\n\n// Which package carries the engine for this machine.\n//\n// The engine is not in this package and cannot be: it is a Chromium build,\n// 41 MB per platform and architecture, six of them, and `npm install` is never\n// going to produce one. So the bytes live in six packages of their own and\n// this one depends on all six as optionalDependencies with `os` and `cpu` set,\n// which is npm's way of saying \"install the one that matches this machine and\n// skip the other five\". A machine nobody builds for installs none of them and\n// still gets a working package -- it just has to be pointed at an engine.\n//\n// The alternative, a postinstall script that downloads a tarball, was not\n// chosen. It defeats a lockfile, which is supposed to pin what you get; it\n// fails behind a registry mirror, which is the one place a large dependency\n// most needs to work; and it runs code at install time in exchange for saving\n// nothing that npm was not already doing.\n//\n// Key and name are both `${process.platform}-${process.arch}`, so the table is\n// the identity map with a prefix on it. That is deliberate: the value npm\n// matches `os` and `cpu` against is process.platform, and a package named for\n// anything else makes the reader hold two spellings of one machine in their\n// head. It is also what every other package of this shape does -- esbuild,\n// swc, lightningcss all publish darwin-arm64 and win32-x64.\n//\n// The release archives spell it win/mac instead -- shotium-mac-arm64.7z --\n// and that is not going to change either. They are downloaded by people, and\n// `mac` is what people call it. So the two spellings do differ, in the one\n// place where each is right: the registry gets node's, the download page gets\n// the reader's.\nconst PACKAGES: Readonly<Record<string, string>> = {\n 'win32-x64': '@shotkit/shotium-win32-x64',\n 'win32-arm64': '@shotkit/shotium-win32-arm64',\n 'darwin-x64': '@shotkit/shotium-darwin-x64',\n 'darwin-arm64': '@shotkit/shotium-darwin-arm64',\n 'linux-x64': '@shotkit/shotium-linux-x64',\n 'linux-arm64': '@shotkit/shotium-linux-arm64',\n};\n\nfunction packageName(\n platform: string = process.platform,\n arch: string = process.arch): string|null {\n return PACKAGES[`${platform}-${arch}`] ?? null;\n}\n\n// Where the matching platform package unpacked, or null if it is not installed.\n//\n// require.resolve rather than a path built from the module's own location: the\n// package can be hoisted to a workspace root, nested under this one, or left\n// in a pnpm store with a symlink pointing at it, and the resolver is the only\n// thing that knows which of those happened.\nfunction packageDir(): string|null {\n const name = packageName();\n if (!name) {\n return null;\n }\n try {\n return path.dirname(require.resolve(`${name}/package.json`));\n } catch {\n return null;\n }\n}\n\n// What the engine executable is called, which is not what the platform calls\n// it: Windows wants the extension and nothing else does.\nfunction binaryName(): string {\n return process.platform === 'win32' ? 'shotium.exe' : 'shotium';\n}\n\nexport {PACKAGES, binaryName, packageDir, packageName};\n"],"mappings":";;;;AAKA,MAAM,UAAU,cAAc,YAAY,GAAG;AA8B7C,MAAM,WAA6C;CACjD,aAAa;CACb,eAAe;CACf,cAAc;CACd,gBAAgB;CAChB,aAAa;CACb,eAAe;AACjB;AAEA,SAAS,YACL,WAAmB,QAAQ,UAC3B,OAAe,QAAQ,MAAmB;CAC5C,OAAO,SAAS,GAAG,SAAS,GAAG,WAAW;AAC5C;AAQA,SAAS,aAA0B;CACjC,MAAM,OAAO,YAAY;CACzB,IAAI,CAAC,MACH,OAAO;CAET,IAAI;EACF,OAAO,KAAK,QAAQ,QAAQ,QAAQ,GAAG,KAAK,cAAc,CAAC;CAC7D,QAAQ;EACN,OAAO;CACT;AACF;AAIA,SAAS,aAAqB;CAC5B,OAAO,QAAQ,aAAa,UAAU,gBAAgB;AACxD"}
|