@aidra/pmx-io 0.0.0-stage → 9.13.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 +69 -2
- package/bin/pmx-io-node.js +143 -0
- package/index.d.ts +381 -0
- package/index.js +27 -0
- package/index.mjs +47 -0
- package/lib/actions.js +120 -0
- package/lib/activate.js +402 -0
- package/lib/application.js +106 -0
- package/lib/bun.js +155 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +17 -0
- package/lib/debug.js +48 -0
- package/lib/esm-hooks.mjs +111 -0
- package/lib/exit.js +79 -0
- package/lib/features/metrics.js +409 -0
- package/lib/features/notify.js +358 -0
- package/lib/features/profiling.js +208 -0
- package/lib/hooks.js +151 -0
- package/lib/inbound.js +174 -0
- package/lib/integrations.js +372 -0
- package/lib/itg/amqplib.js +169 -0
- package/lib/itg/bullmq.js +141 -0
- package/lib/itg/common.js +657 -0
- package/lib/itg/dbcomment.js +114 -0
- package/lib/itg/index.js +103 -0
- package/lib/itg/ioredis.js +212 -0
- package/lib/itg/kafkajs.js +205 -0
- package/lib/itg/mongodb.js +322 -0
- package/lib/itg/mysql2.js +205 -0
- package/lib/itg/nats.js +531 -0
- package/lib/itg/noderedis.js +236 -0
- package/lib/itg/pg.js +253 -0
- package/lib/itg/pgqueue.js +305 -0
- package/lib/itg/postgres.js +252 -0
- package/lib/itg/redis-common.js +265 -0
- package/lib/itg/sql.js +302 -0
- package/lib/metrics/service.js +286 -0
- package/lib/metrics/types.js +342 -0
- package/lib/modules.js +260 -0
- package/lib/node-options.js +62 -0
- package/lib/otel/bridge.js +797 -0
- package/lib/otel/env.js +114 -0
- package/lib/outbound.js +362 -0
- package/lib/pmx.js +757 -0
- package/lib/relay/hub.js +279 -0
- package/lib/relay/transport.js +253 -0
- package/lib/standalone/activate.js +307 -0
- package/lib/standalone/capture.js +55 -0
- package/lib/standalone/container.js +909 -0
- package/lib/standalone/transport.js +1023 -0
- package/lib/tracing/context.js +90 -0
- package/lib/tracing/control.js +124 -0
- package/lib/tracing/exporter.js +115 -0
- package/lib/tracing/index.js +242 -0
- package/lib/tracing/logs.js +176 -0
- package/lib/tracing/options.js +105 -0
- package/lib/tracing/paths.js +34 -0
- package/lib/tracing/tracer.js +850 -0
- package/lib/tracing/w3c.js +415 -0
- package/lib/transport/ipc.js +97 -0
- package/lib/transport/ws.js +511 -0
- package/lib/util.js +109 -0
- package/package.json +48 -4
- package/preload.cjs +26 -0
- package/preload.mjs +25 -0
- package/standalone.cjs +16 -0
- package/standalone.mjs +17 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Standalone mode (80 B.15): activation (NIO-260), roots and relays (NIO-263), the root's setup
|
|
5
|
+
// (hub, capture, prodman connection, NIO-262..NIO-267), shutdown reasons (NIO-268) and isolation
|
|
6
|
+
// (NIO-269). Everything here is caught and logged under `axm:standalone`; nothing throws into the
|
|
7
|
+
// app and the app always runs.
|
|
8
|
+
|
|
9
|
+
const path = require('path')
|
|
10
|
+
const { PKG_DIR } = require('../util')
|
|
11
|
+
const debug = require('../debug')('axm:standalone')
|
|
12
|
+
|
|
13
|
+
const MISSING_LINE = '[@pm2/io] not reporting to prodman: %s not set (standalone mode needs PM2_PUBLIC_KEY, PM2_SECRET_KEY and PM2_APP_NAME)'
|
|
14
|
+
const UNREACHABLE_LINE = '[@pm2/io] not reporting: the standalone root (pid %s) is not reachable; start the server with exec'
|
|
15
|
+
const TREE_ENV = ['PMX_IO_STANDALONE', 'PMX_IO_ROOT_PID', 'PMX_IO_ROOT_PPID', 'PMX_IO_HUB', 'PMX_IO_HUB_TOKEN', 'PMX_IO_CAPTURE_LOGS']
|
|
16
|
+
const STANDALONE_CJS = path.join(PKG_DIR, 'standalone.cjs')
|
|
17
|
+
const GLOBAL = Symbol.for('@pmx/io:standalone')
|
|
18
|
+
const SIGNAL_MEMORY_MS = 10 * 60 * 1000
|
|
19
|
+
const SIGNAL_NUMBERS = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGTERM: 15 }
|
|
20
|
+
|
|
21
|
+
const printed = new Set()
|
|
22
|
+
function printOnce (line) {
|
|
23
|
+
if (printed.has(line)) return
|
|
24
|
+
printed.add(line)
|
|
25
|
+
try {
|
|
26
|
+
process.stderr.write(line + '\n')
|
|
27
|
+
} catch (_) {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A process started by pmx/pm2 (or a descendant of one): the keys are the agent's (APM-001). */
|
|
31
|
+
function underPmx (env) {
|
|
32
|
+
return env.pm_id !== undefined && env.PMX_IO_STANDALONE !== '1'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function daemonChannel (env) {
|
|
36
|
+
return env.PMX_IO === '1' && typeof process.send === 'function'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function scrubTreeEnv (env) {
|
|
40
|
+
for (const k of TREE_ENV) delete env[k]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** NIO-263: is this process the root of its tree? */
|
|
44
|
+
function isRoot (env) {
|
|
45
|
+
const pid = String(process.pid)
|
|
46
|
+
if (env.PMX_IO_ROOT_PID === pid) return true
|
|
47
|
+
if (env.PMX_IO_ROOT_PPID && env.PMX_IO_ROOT_PPID === String(process.ppid)) return true
|
|
48
|
+
return !env.PMX_IO_ROOT_PID && !env.PMX_IO_ROOT_PPID && !env.PMX_IO_HUB
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Adds `--require <abs standalone.cjs>` to NODE_OPTIONS once (NIO-261). */
|
|
52
|
+
function ensureNodeOptions (env) {
|
|
53
|
+
const cur = env.NODE_OPTIONS || ''
|
|
54
|
+
if (cur.includes(STANDALONE_CJS)) return
|
|
55
|
+
const token = `--require "${STANDALONE_CJS.replace(/"/g, '\\"')}"`
|
|
56
|
+
env.NODE_OPTIONS = cur ? token + ' ' + cur : token
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function childrenWanted (config) {
|
|
60
|
+
try {
|
|
61
|
+
return require('../tracing/options').childrenEnabled(config, process.env)
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decide what this process is (NIO-260, NIO-263). Returns
|
|
69
|
+
* { role: 'root', transport, handle } | { role: 'relay', factory } | null (inert).
|
|
70
|
+
*/
|
|
71
|
+
function decide (config) {
|
|
72
|
+
const env = process.env
|
|
73
|
+
if (daemonChannel(env)) return null
|
|
74
|
+
if (underPmx(env)) {
|
|
75
|
+
debug('started by pmx (pm_id set): standalone mode not used')
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
if (!isRoot(env)) {
|
|
79
|
+
const hub = env.PMX_IO_HUB
|
|
80
|
+
const rootPid = env.PMX_IO_ROOT_PID || env.PMX_IO_ROOT_PPID
|
|
81
|
+
if (!hub) {
|
|
82
|
+
if (rootPid) printOnce(UNREACHABLE_LINE.replace('%s', rootPid))
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
const token = env.PMX_IO_HUB_TOKEN || ''
|
|
86
|
+
const { HubRelayTransport } = require('../relay/transport')
|
|
87
|
+
const factory = () => new HubRelayTransport(hub, token, {
|
|
88
|
+
onUnreachable: () => {
|
|
89
|
+
if (rootPid) printOnce(UNREACHABLE_LINE.replace('%s', rootPid))
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
return { role: 'relay', factory }
|
|
93
|
+
}
|
|
94
|
+
const container = require('./container')
|
|
95
|
+
const { resolveConfig } = require('./transport')
|
|
96
|
+
const info = container.globalInfo()
|
|
97
|
+
const cfg = resolveConfig(config && config.apmOptions, env, info)
|
|
98
|
+
if (cfg.missing.length) {
|
|
99
|
+
printOnce(MISSING_LINE.replace('%s', cfg.missing.join(', ')))
|
|
100
|
+
scrubTreeEnv(env)
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
return { role: 'root', handle: setupRoot(cfg, info, config) }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// -- the root ---------------------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
let ROOT = null
|
|
109
|
+
|
|
110
|
+
function setupRoot (cfg, info, config) {
|
|
111
|
+
if (ROOT) return ROOT
|
|
112
|
+
const env = process.env
|
|
113
|
+
const { ProdmanTransport } = require('./transport')
|
|
114
|
+
env.PMX_IO_ROOT_PID = String(process.pid)
|
|
115
|
+
delete env.PMX_IO_ROOT_PPID
|
|
116
|
+
env.PMX_IO_STANDALONE = '1'
|
|
117
|
+
// NIO-262: descendants report through the hub and never need the bucket keys
|
|
118
|
+
delete env.PM2_SECRET_KEY
|
|
119
|
+
delete env.PM2_PUBLIC_KEY
|
|
120
|
+
const handle = {
|
|
121
|
+
transport: null,
|
|
122
|
+
hub: null,
|
|
123
|
+
capture: null,
|
|
124
|
+
closeThreads: null,
|
|
125
|
+
instance: null,
|
|
126
|
+
signal: null,
|
|
127
|
+
signalAt: 0,
|
|
128
|
+
crashed: false,
|
|
129
|
+
observers: new Map(),
|
|
130
|
+
destroyed: false
|
|
131
|
+
}
|
|
132
|
+
const children = childrenWanted(config)
|
|
133
|
+
if (children) {
|
|
134
|
+
ensureNodeOptions(env)
|
|
135
|
+
try {
|
|
136
|
+
const { Hub } = require('../relay/hub')
|
|
137
|
+
const hub = new Hub((msg) => onRelay(handle, msg))
|
|
138
|
+
env.PMX_IO_HUB = hub.start()
|
|
139
|
+
env.PMX_IO_HUB_TOKEN = hub.token
|
|
140
|
+
handle.hub = hub
|
|
141
|
+
} catch (err) {
|
|
142
|
+
debug('hub not started: %s', err && err.message)
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
handle.closeThreads = require('../relay/transport').listenThreads((msg) => onRelay(handle, msg))
|
|
146
|
+
} catch (_) {}
|
|
147
|
+
} else {
|
|
148
|
+
delete env.PMX_IO_HUB
|
|
149
|
+
delete env.PMX_IO_HUB_TOKEN
|
|
150
|
+
}
|
|
151
|
+
env.PMX_IO_CAPTURE_LOGS = cfg.captureLogs
|
|
152
|
+
const transport = new ProdmanTransport(cfg, info, { relayPids: () => (handle.hub ? handle.hub.relayPids() : []) })
|
|
153
|
+
handle.transport = transport
|
|
154
|
+
if (cfg.captureLogs === 'stdio') {
|
|
155
|
+
try {
|
|
156
|
+
const { Capture } = require('./capture')
|
|
157
|
+
handle.capture = new Capture((stream, text) => transport.onLogChunk(stream, text))
|
|
158
|
+
handle.capture.start()
|
|
159
|
+
} catch (err) {
|
|
160
|
+
debug('log capture failed: %s', err && err.message)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
installShutdown(handle)
|
|
164
|
+
handle.destroy = () => destroyRoot(handle)
|
|
165
|
+
Object.defineProperty(globalThis, GLOBAL, {
|
|
166
|
+
configurable: true,
|
|
167
|
+
enumerable: false,
|
|
168
|
+
value: { onUncaught: () => { handle.crashed = true }, handle }
|
|
169
|
+
})
|
|
170
|
+
transport.start()
|
|
171
|
+
ROOT = handle
|
|
172
|
+
return handle
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onRelay (handle, msg) {
|
|
176
|
+
const inst = handle.instance
|
|
177
|
+
if (inst && typeof inst._onRelayMessage === 'function') {
|
|
178
|
+
inst._onRelayMessage(msg)
|
|
179
|
+
} else if (msg && msg.__pmx_relay__ === 'log' && typeof msg.d === 'string') {
|
|
180
|
+
handle.transport.onLogChunk(msg.s === 'err' ? 'err' : 'out', msg.d)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// -- shutdown (NIO-268) -----------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
function exitReason (handle, code) {
|
|
187
|
+
if (handle.crashed) return { reason: 'crash', signal: null, planned: false }
|
|
188
|
+
if (handle.signal && Date.now() - handle.signalAt < SIGNAL_MEMORY_MS) return { reason: 'signal', signal: handle.signal, planned: true }
|
|
189
|
+
if (typeof code === 'number' && code !== 0) return { reason: 'crash', signal: null, planned: false }
|
|
190
|
+
return { reason: 'exit', signal: null, planned: true }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function installShutdown (handle) {
|
|
194
|
+
for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
195
|
+
const observer = function pmxIoSignalObserver () {
|
|
196
|
+
if (handle.destroyed || process.pid !== Number(process.env.PMX_IO_ROOT_PID || process.pid)) return
|
|
197
|
+
handle.signal = sig
|
|
198
|
+
handle.signalAt = Date.now()
|
|
199
|
+
// the app's own exit hooks (onExit) run through exit.js's listener; ours only reports
|
|
200
|
+
const others = process.listeners(sig).filter((l) => l !== observer)
|
|
201
|
+
handle.transport.shutdown('signal', sig, true, 1000).then(() => {
|
|
202
|
+
if (others.length || process.listeners(sig).some((l) => l !== observer)) return
|
|
203
|
+
process.removeListener(sig, observer)
|
|
204
|
+
handle.observers.delete(sig)
|
|
205
|
+
if (process.pid === 1) process.exit(128 + SIGNAL_NUMBERS[sig])
|
|
206
|
+
else process.kill(process.pid, sig)
|
|
207
|
+
}, () => {})
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
process.on(sig, observer)
|
|
211
|
+
handle.observers.set(sig, observer)
|
|
212
|
+
} catch (_) {}
|
|
213
|
+
}
|
|
214
|
+
handle.onExit = (code) => {
|
|
215
|
+
if (handle.destroyed) return
|
|
216
|
+
const r = exitReason(handle, code)
|
|
217
|
+
try {
|
|
218
|
+
handle.transport.shutdown(r.reason, r.signal, r.planned, 0)
|
|
219
|
+
} catch (_) {}
|
|
220
|
+
}
|
|
221
|
+
process.on('exit', handle.onExit)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function destroyRoot (handle) {
|
|
225
|
+
if (handle.destroyed) return
|
|
226
|
+
handle.destroyed = true
|
|
227
|
+
for (const [sig, fn] of handle.observers) process.removeListener(sig, fn)
|
|
228
|
+
handle.observers.clear()
|
|
229
|
+
if (handle.onExit) process.removeListener('exit', handle.onExit)
|
|
230
|
+
if (handle.capture) handle.capture.stop()
|
|
231
|
+
if (handle.hub) handle.hub.stop()
|
|
232
|
+
if (handle.closeThreads) handle.closeThreads()
|
|
233
|
+
try {
|
|
234
|
+
handle.transport.destroyAll()
|
|
235
|
+
} catch (_) {}
|
|
236
|
+
if (ROOT === handle) ROOT = null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// -- entry points -----------------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
/** Called from PMX.init when standalone mode is wanted (NIO-260 a/c). Returns a transport or null. */
|
|
242
|
+
function fromInit (instance, config) {
|
|
243
|
+
try {
|
|
244
|
+
const d = decide(config)
|
|
245
|
+
if (!d) return null
|
|
246
|
+
if (d.role === 'root') {
|
|
247
|
+
instance._mode = 'standalone'
|
|
248
|
+
instance._standalone = d.handle
|
|
249
|
+
d.handle.instance = instance
|
|
250
|
+
return d.handle.transport
|
|
251
|
+
}
|
|
252
|
+
instance._mode = 'relay'
|
|
253
|
+
instance._relayFactory = d.factory
|
|
254
|
+
startRelayCapture(instance)
|
|
255
|
+
return d.factory()
|
|
256
|
+
} catch (err) {
|
|
257
|
+
debug('activation failed: %s', err && err.message)
|
|
258
|
+
try {
|
|
259
|
+
process.stderr.write('[@pm2/io] standalone mode disabled: ' + (err && err.message) + '\n')
|
|
260
|
+
} catch (_) {}
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** NIO-267: relays capture too (their chunks go to the root) when the root captures. */
|
|
266
|
+
function startRelayCapture (instance) {
|
|
267
|
+
if (process.env.PMX_IO_CAPTURE_LOGS !== 'stdio' || instance._relayCapture) return
|
|
268
|
+
const { Capture } = require('./capture')
|
|
269
|
+
instance._relayCapture = new Capture((stream, text) => {
|
|
270
|
+
const t = instance._transport
|
|
271
|
+
if (t && typeof t.sendLog === 'function') t.sendLog(stream, text)
|
|
272
|
+
})
|
|
273
|
+
instance._relayCapture.start()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The standalone preload entries (NIO-261 b): `@aidra/pmx-io/standalone` and `standalone.mjs`. */
|
|
277
|
+
function fromPreload () {
|
|
278
|
+
const env = process.env
|
|
279
|
+
const activate = require('../activate')
|
|
280
|
+
const s = activate.state()
|
|
281
|
+
if (s.active) return s
|
|
282
|
+
if (daemonChannel(env) || underPmx(env)) return null
|
|
283
|
+
if (env.pmx === 'false' || env.automation === 'false') return null
|
|
284
|
+
env.PMX_IO_STANDALONE = '1'
|
|
285
|
+
if (!env.PMX_IO_ROOT_PID && !env.PMX_IO_ROOT_PPID) env.PMX_IO_ROOT_PID = String(process.pid)
|
|
286
|
+
ensureNodeOptions(env)
|
|
287
|
+
const { GLOBAL_SYMBOL } = require('../constants')
|
|
288
|
+
if (Object.prototype.hasOwnProperty.call(globalThis, GLOBAL_SYMBOL)) return null
|
|
289
|
+
const { PMX } = require('../pmx')
|
|
290
|
+
let instance
|
|
291
|
+
if (!activate.isMainThread()) {
|
|
292
|
+
instance = new PMX({ mode: 'thread' })
|
|
293
|
+
s.role = 'thread'
|
|
294
|
+
} else {
|
|
295
|
+
instance = new PMX()
|
|
296
|
+
s.role = 'standalone'
|
|
297
|
+
}
|
|
298
|
+
s.instance = instance
|
|
299
|
+
s.active = true
|
|
300
|
+
activate.claimSingleton(instance)
|
|
301
|
+
instance.init({ standalone: true })
|
|
302
|
+
const conf = activate.secondInitConfig(env)
|
|
303
|
+
if (conf) instance.init(Object.assign({ standalone: true }, conf))
|
|
304
|
+
return s
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { decide, fromInit, fromPreload, isRoot, underPmx, ensureNodeOptions, exitReason, MISSING_LINE, UNREACHABLE_LINE, STANDALONE_CJS, TREE_ENV }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Log capture for standalone mode (80 NIO-267, `captureLogs: 'stdio'`): process.stdout.write and
|
|
5
|
+
// process.stderr.write are wrapped in the root and in relays. Output written by native addons or
|
|
6
|
+
// by child processes with inherited stdio directly to fds 1/2 is not captured (Node has no dup2,
|
|
7
|
+
// so Python's `fd` mode has no equivalent, Part G.1). Capture never throws, never blocks the
|
|
8
|
+
// writer and is re-entrancy guarded; the app's output is always written unchanged.
|
|
9
|
+
|
|
10
|
+
class Capture {
|
|
11
|
+
/** @param {(stream: 'out'|'err', text: string) => void} sink */
|
|
12
|
+
constructor (sink) {
|
|
13
|
+
this._sink = sink
|
|
14
|
+
this._restore = null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
start () {
|
|
18
|
+
if (this._restore) return
|
|
19
|
+
const originals = {}
|
|
20
|
+
let busy = false
|
|
21
|
+
const sink = this._sink
|
|
22
|
+
for (const [type, stream] of [['out', process.stdout], ['err', process.stderr]]) {
|
|
23
|
+
if (!stream || typeof stream.write !== 'function') continue
|
|
24
|
+
const orig = stream.write
|
|
25
|
+
originals[type] = { stream, orig }
|
|
26
|
+
stream.write = function write (chunk, encoding) {
|
|
27
|
+
if (!busy) {
|
|
28
|
+
busy = true
|
|
29
|
+
try {
|
|
30
|
+
let text
|
|
31
|
+
if (typeof chunk === 'string') text = chunk
|
|
32
|
+
// the encoding argument applies to strings only: bytes are written as they are
|
|
33
|
+
else if (chunk instanceof Uint8Array) text = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength).toString('utf8')
|
|
34
|
+
else text = String(chunk)
|
|
35
|
+
if (text) sink(type, text)
|
|
36
|
+
} catch (_) {
|
|
37
|
+
} finally {
|
|
38
|
+
busy = false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return orig.apply(this, arguments)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
this._restore = () => {
|
|
45
|
+
for (const k of Object.keys(originals)) originals[k].stream.write = originals[k].orig
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
stop () {
|
|
50
|
+
if (this._restore) this._restore()
|
|
51
|
+
this._restore = null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { Capture }
|