@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
package/lib/actions.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Custom, internal and scoped actions (80 A.8.1, A.8.2, NIO-040..042; 40 §7.4, §7.5, §7.8, §8.3).
|
|
5
|
+
|
|
6
|
+
const debug = require('./debug')('axm:services:actions')
|
|
7
|
+
|
|
8
|
+
function actionType (name) {
|
|
9
|
+
return name.startsWith('km:') || name.startsWith('internal:') ? 'internal' : 'custom'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class ActionService {
|
|
13
|
+
/**
|
|
14
|
+
* @param {object} transport
|
|
15
|
+
* @param {(err: any) => void} notifyError used for rejected async handlers (NIO-042)
|
|
16
|
+
*/
|
|
17
|
+
constructor (transport, notifyError) {
|
|
18
|
+
this._transport = transport
|
|
19
|
+
this._notifyError = notifyError
|
|
20
|
+
this._actions = new Map()
|
|
21
|
+
transport.onMessage((msg) => this.dispatch(msg))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
register (name, opts, handler) {
|
|
25
|
+
if (typeof name !== 'string') {
|
|
26
|
+
console.error('You must define an name when registering an action')
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
if (typeof handler !== 'function') {
|
|
30
|
+
console.error('You must define an callback when registering an action')
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
const arity = handler.length
|
|
34
|
+
this._actions.set(name, { name, handler, arity, scoped: false })
|
|
35
|
+
const data = { action_name: name, action_type: actionType(name), arity }
|
|
36
|
+
if (opts !== undefined) data.opts = opts
|
|
37
|
+
this._transport.send('axm:action', data)
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
registerScoped (name, handler) {
|
|
42
|
+
if (typeof name !== 'string') {
|
|
43
|
+
console.error('You must define an name when registering an action')
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
if (typeof handler !== 'function') {
|
|
47
|
+
console.error('You must define an callback when registering an action')
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
this._actions.set(name, { name, handler, arity: handler.length, scoped: true })
|
|
51
|
+
this._transport.send('axm:action', { action_name: name, action_type: 'scoped', arity: handler.length, opts: null })
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
has (name) {
|
|
56
|
+
return this._actions.has(name)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
list () {
|
|
60
|
+
return Array.from(this._actions.values()).map((a) => ({
|
|
61
|
+
action_name: a.name,
|
|
62
|
+
action_type: a.scoped ? 'scoped' : actionType(a.name),
|
|
63
|
+
arity: a.arity
|
|
64
|
+
}))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Handle a daemon → app message (40 §8.3). Unknown names are ignored silently. */
|
|
68
|
+
dispatch (msg) {
|
|
69
|
+
let name
|
|
70
|
+
let opts
|
|
71
|
+
let uuid
|
|
72
|
+
if (typeof msg === 'string') {
|
|
73
|
+
name = msg
|
|
74
|
+
} else if (msg && typeof msg === 'object') {
|
|
75
|
+
name = msg.msg || msg.action_name
|
|
76
|
+
opts = msg.opts
|
|
77
|
+
uuid = msg.uuid
|
|
78
|
+
}
|
|
79
|
+
if (typeof name !== 'string') return
|
|
80
|
+
const action = this._actions.get(name)
|
|
81
|
+
if (!action) return
|
|
82
|
+
debug('invoking action %s', name)
|
|
83
|
+
|
|
84
|
+
if (action.scoped) {
|
|
85
|
+
if (!uuid) return
|
|
86
|
+
const send = (type) => (data) => this._transport.send(type, { data, uuid, action_name: name })
|
|
87
|
+
const stream = {
|
|
88
|
+
send: send('axm:scoped_action:stream'),
|
|
89
|
+
error: send('axm:scoped_action:error'),
|
|
90
|
+
end: send('axm:scoped_action:end')
|
|
91
|
+
}
|
|
92
|
+
action.handler(opts || {}, stream)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let replied = false
|
|
97
|
+
const reply = (data) => {
|
|
98
|
+
replied = true
|
|
99
|
+
this._transport.send('axm:reply', { at: Date.now(), action_name: name, return: data })
|
|
100
|
+
}
|
|
101
|
+
const params = opts === undefined || opts === null ? {} : opts // NIO-041
|
|
102
|
+
const ret = action.arity === 2 ? action.handler(params, reply) : action.handler(reply)
|
|
103
|
+
if (ret && typeof ret.then === 'function') {
|
|
104
|
+
ret.then((value) => {
|
|
105
|
+
if (!replied) reply(value)
|
|
106
|
+
}, (err) => {
|
|
107
|
+
if (!replied) reply({ success: false, err: err && err.message !== undefined ? err.message : String(err) })
|
|
108
|
+
try {
|
|
109
|
+
this._notifyError(err)
|
|
110
|
+
} catch (_) {}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
destroy () {
|
|
116
|
+
this._actions.clear()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { ActionService, actionType }
|
package/lib/activate.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Preload activation (80 B.3 NIO-020..NIO-025, B.16 NIO-290..NIO-296) and coexistence with an
|
|
5
|
+
// app's own @pm2/io (B.4 NIO-030..NIO-036, 02 D-05).
|
|
6
|
+
//
|
|
7
|
+
// A process that loads the preload becomes one of:
|
|
8
|
+
// * the root (env PMX_IO=1 from the daemon, IPC channel, main thread): reports over IPC and,
|
|
9
|
+
// with `children` (default true), opens the local hub for its Node descendants (NIO-291);
|
|
10
|
+
// * a daemon-mode relay (PMX_IO unset, PMX_IO_HUB set, PMX_IO_STANDALONE unset): reports
|
|
11
|
+
// through the root's hub;
|
|
12
|
+
// * a thread relay (a worker thread of an instrumented process): reports through the main
|
|
13
|
+
// thread (NIO-293).
|
|
14
|
+
|
|
15
|
+
const path = require('path')
|
|
16
|
+
const fs = require('fs')
|
|
17
|
+
const { fileURLToPath } = require('url')
|
|
18
|
+
const { GLOBAL_SYMBOL } = require('./constants')
|
|
19
|
+
const { PKG_DIR } = require('./util')
|
|
20
|
+
const debug = require('./debug')('axm:main')
|
|
21
|
+
|
|
22
|
+
const STATE = Symbol.for('@pmx/io:preload-state')
|
|
23
|
+
|
|
24
|
+
function state () {
|
|
25
|
+
let s = globalThis[STATE]
|
|
26
|
+
if (!s) {
|
|
27
|
+
s = { active: false, instance: null, joined: false, conflict: false, esmHooked: false, role: null, hub: null, caps: [], closeThreads: null }
|
|
28
|
+
Object.defineProperty(globalThis, STATE, { value: s, enumerable: false, configurable: true })
|
|
29
|
+
}
|
|
30
|
+
return s
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const OWN_PRELOADS = ['preload.cjs', 'preload.mjs', 'standalone.cjs', 'standalone.mjs'].map((f) => path.join(PKG_DIR, f))
|
|
34
|
+
|
|
35
|
+
function realpath (p) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.realpathSync(p)
|
|
38
|
+
} catch (_) {
|
|
39
|
+
return p
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isOurPreload (spec) {
|
|
44
|
+
if (typeof spec !== 'string' || spec === '') return false
|
|
45
|
+
let file = spec
|
|
46
|
+
if (file.startsWith('file:')) {
|
|
47
|
+
try {
|
|
48
|
+
file = fileURLToPath(file)
|
|
49
|
+
} catch (_) {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const abs = path.resolve(file)
|
|
54
|
+
if (OWN_PRELOADS.includes(abs)) return true
|
|
55
|
+
const real = realpath(abs)
|
|
56
|
+
return OWN_PRELOADS.some((own) => realpath(own) === real)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isMainThread () {
|
|
60
|
+
// A worker thread's bootstrap always loads worker_threads: when it is not loaded yet this is
|
|
61
|
+
// the main thread, without paying for loading the module (start-up cost, NIO-024).
|
|
62
|
+
const list = process.moduleLoadList
|
|
63
|
+
if (Array.isArray(list) && !list.includes('NativeModule worker_threads')) return true
|
|
64
|
+
try {
|
|
65
|
+
return require('worker_threads').isMainThread !== false
|
|
66
|
+
} catch (_) {
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function optedOut (env) {
|
|
72
|
+
return env.pmx === 'false' || env.automation === 'false'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** NIO-020 activation conditions (the root under the daemon). */
|
|
76
|
+
function shouldActivate (env) {
|
|
77
|
+
env = env || process.env
|
|
78
|
+
return env.PMX_IO === '1' &&
|
|
79
|
+
!optedOut(env) &&
|
|
80
|
+
isMainThread() &&
|
|
81
|
+
typeof process.send === 'function'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** NIO-291: a descendant of an instrumented root, under the daemon. */
|
|
85
|
+
function isDaemonRelay (env) {
|
|
86
|
+
env = env || process.env
|
|
87
|
+
return !env.PMX_IO && Boolean(env.PMX_IO_HUB) && env.PMX_IO_STANDALONE !== '1' && !optedOut(env)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** INJ-024: the capabilities the daemon advertised (read once, then removed). */
|
|
91
|
+
function readCaps (env) {
|
|
92
|
+
const raw = env.PMX_IO_CAPS
|
|
93
|
+
if (typeof raw !== 'string') return []
|
|
94
|
+
return raw.split(',').map((s) => s.trim()).filter((s) => s)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* NIO-021 (amended by NIO-291): PMX_IO and PMX_IO_CAPS are never inherited; the preload tokens
|
|
99
|
+
* leave NODE_OPTIONS only when descendants are not instrumented (`children: false`).
|
|
100
|
+
*/
|
|
101
|
+
function scrubEnvironment (keepPreloads) {
|
|
102
|
+
delete process.env.PMX_IO
|
|
103
|
+
delete process.env.PMX_IO_CAPS
|
|
104
|
+
if (keepPreloads) return
|
|
105
|
+
const opts = process.env.NODE_OPTIONS
|
|
106
|
+
if (typeof opts === 'string') {
|
|
107
|
+
const stripped = require('./node-options').stripPreloads(opts, isOurPreload)
|
|
108
|
+
if (stripped.trim() === '') delete process.env.NODE_OPTIONS
|
|
109
|
+
else process.env.NODE_OPTIONS = stripped
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function sendOption (instance, data) {
|
|
114
|
+
try {
|
|
115
|
+
if (instance._transport && instance._initialized) instance._transport.send('axm:option:configuration', data)
|
|
116
|
+
else if (typeof process.send === 'function' && process.connected !== false && instance._mode === 'managed') {
|
|
117
|
+
process.send({ type: 'axm:option:configuration', data }, () => {})
|
|
118
|
+
}
|
|
119
|
+
} catch (_) {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** NIO-030/031: define the global symbol as an accessor so foreign reads/writes are observable. */
|
|
123
|
+
function claimSingleton (instance) {
|
|
124
|
+
const s = state()
|
|
125
|
+
Object.defineProperty(globalThis, GLOBAL_SYMBOL, {
|
|
126
|
+
configurable: true,
|
|
127
|
+
enumerable: false,
|
|
128
|
+
get () {
|
|
129
|
+
if (!s.joined) {
|
|
130
|
+
s.joined = true
|
|
131
|
+
debug('an app copy of @pm2/io joined the injected instance')
|
|
132
|
+
sendOption(instance, { pm2_io_joined: true })
|
|
133
|
+
}
|
|
134
|
+
return instance
|
|
135
|
+
},
|
|
136
|
+
set (value) {
|
|
137
|
+
if (value === instance) return
|
|
138
|
+
if (!s.conflict) {
|
|
139
|
+
s.conflict = true
|
|
140
|
+
sendOption(instance, { pm2_io_conflict: true })
|
|
141
|
+
try {
|
|
142
|
+
process.stderr.write('[pmx] another @pm2/io instance tried to replace the injected one; it was ignored\n')
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** NIO-034: warn once when the legacy `pmx` module is required. Never alters it. */
|
|
150
|
+
function watchLegacyPmx () {
|
|
151
|
+
let Module
|
|
152
|
+
try {
|
|
153
|
+
Module = require('./util').cjsModule()
|
|
154
|
+
} catch (_) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const proto = Module.prototype
|
|
158
|
+
if (!proto || typeof proto.require !== 'function') return
|
|
159
|
+
const orig = proto.require
|
|
160
|
+
let warned = false
|
|
161
|
+
proto.require = function require (id) {
|
|
162
|
+
if (!warned && id === 'pmx') {
|
|
163
|
+
warned = true
|
|
164
|
+
try {
|
|
165
|
+
process.stderr.write("[pmx] legacy 'pmx' module detected: start the app with --no-pmx to avoid duplicate metrics\n")
|
|
166
|
+
} catch (_) {}
|
|
167
|
+
}
|
|
168
|
+
return orig.apply(this, arguments)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A.2: the second init from env `io`. (Env `trace` and the sampling variables are applied by
|
|
174
|
+
* every init, NIO-201, so `trace=true` alone needs no second init.)
|
|
175
|
+
*/
|
|
176
|
+
function secondInitConfig (env) {
|
|
177
|
+
const { envIoConf } = require('./tracing/options')
|
|
178
|
+
const conf = envIoConf(env)
|
|
179
|
+
return conf ? Object.assign({}, conf) : null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** NIO-291: open the hub and export its variables before the app can start children. */
|
|
183
|
+
function openHub (s, instanceRef) {
|
|
184
|
+
const { Hub } = require('./relay/hub')
|
|
185
|
+
const hub = new Hub((msg) => {
|
|
186
|
+
const inst = instanceRef()
|
|
187
|
+
if (inst) inst._onRelayMessage(msg)
|
|
188
|
+
})
|
|
189
|
+
process.env.PMX_IO_HUB = hub.start()
|
|
190
|
+
process.env.PMX_IO_HUB_TOKEN = hub.token
|
|
191
|
+
process.env.PMX_IO_ROOT_PID = String(process.pid)
|
|
192
|
+
s.hub = hub
|
|
193
|
+
// worker threads start after the app's first line: listen for them off the start-up path
|
|
194
|
+
const t = setImmediate(() => {
|
|
195
|
+
s.closeThreads = require('./relay/transport').listenThreads((msg) => {
|
|
196
|
+
const inst = instanceRef()
|
|
197
|
+
if (inst) inst._onRelayMessage(msg)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
if (t.unref) t.unref()
|
|
201
|
+
return hub
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function activateRoot (s) {
|
|
205
|
+
const { childrenEnabled } = require('./tracing/options')
|
|
206
|
+
s.caps = readCaps(process.env)
|
|
207
|
+
const children = childrenEnabled(null, process.env)
|
|
208
|
+
scrubEnvironment(children)
|
|
209
|
+
if (Object.prototype.hasOwnProperty.call(globalThis, GLOBAL_SYMBOL)) {
|
|
210
|
+
debug('global @pm2/io instance already present; not injecting')
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
// (a) node_version (40 §7.1)
|
|
214
|
+
try {
|
|
215
|
+
process.send({ node_version: process.versions.node }, () => {})
|
|
216
|
+
} catch (_) {}
|
|
217
|
+
// (b) claim the singleton
|
|
218
|
+
const { PMX } = require('./pmx')
|
|
219
|
+
const instance = new PMX({ managed: true, caps: s.caps })
|
|
220
|
+
s.instance = instance
|
|
221
|
+
s.active = true
|
|
222
|
+
s.role = 'root'
|
|
223
|
+
claimSingleton(instance)
|
|
224
|
+
watchLegacyPmx()
|
|
225
|
+
if (children) {
|
|
226
|
+
try {
|
|
227
|
+
openHub(s, () => s.instance)
|
|
228
|
+
} catch (err) {
|
|
229
|
+
debug('hub not started: %s', err && err.message)
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
delete process.env.PMX_IO_HUB
|
|
233
|
+
delete process.env.PMX_IO_HUB_TOKEN
|
|
234
|
+
}
|
|
235
|
+
// (c) default init, (d) second init from env
|
|
236
|
+
instance.init()
|
|
237
|
+
const conf = secondInitConfig(process.env)
|
|
238
|
+
if (conf) instance.init(conf)
|
|
239
|
+
return s
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function activateRelay (s, kind) {
|
|
243
|
+
if (Object.prototype.hasOwnProperty.call(globalThis, GLOBAL_SYMBOL)) return null
|
|
244
|
+
const { PMX } = require('./pmx')
|
|
245
|
+
let instance
|
|
246
|
+
if (kind === 'thread') {
|
|
247
|
+
instance = new PMX({ mode: 'thread' })
|
|
248
|
+
} else {
|
|
249
|
+
const env = process.env
|
|
250
|
+
const hub = env.PMX_IO_HUB
|
|
251
|
+
const token = env.PMX_IO_HUB_TOKEN || ''
|
|
252
|
+
const { HubRelayTransport } = require('./relay/transport')
|
|
253
|
+
instance = new PMX({ mode: 'relay', relayFactory: () => new HubRelayTransport(hub, token) })
|
|
254
|
+
}
|
|
255
|
+
s.instance = instance
|
|
256
|
+
s.active = true
|
|
257
|
+
s.role = kind
|
|
258
|
+
claimSingleton(instance)
|
|
259
|
+
instance.init()
|
|
260
|
+
const conf = secondInitConfig(process.env)
|
|
261
|
+
if (conf) instance.init(conf)
|
|
262
|
+
return s
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Activate instrumentation from a preload. Idempotent. Returns the state, or null when the
|
|
267
|
+
* activation conditions do not hold (then nothing at all is done).
|
|
268
|
+
*/
|
|
269
|
+
function activate () {
|
|
270
|
+
const s = state()
|
|
271
|
+
if (s.active) return s
|
|
272
|
+
const env = process.env
|
|
273
|
+
if (!isMainThread()) {
|
|
274
|
+
// NIO-293: a worker thread of an instrumented process (daemon root with children, or a
|
|
275
|
+
// standalone tree) becomes a thread relay
|
|
276
|
+
if (optedOut(env) || !(env.PMX_IO_HUB || env.PMX_IO_STANDALONE === '1')) return null
|
|
277
|
+
if (require('./tracing/options').childrenEnabled(null, env) === false) return null
|
|
278
|
+
return activateRelay(s, 'thread')
|
|
279
|
+
}
|
|
280
|
+
if (shouldActivate(env)) return activateRoot(s)
|
|
281
|
+
if (isDaemonRelay(env)) {
|
|
282
|
+
if (require('./tracing/options').childrenEnabled(null, env) === false) return null
|
|
283
|
+
return activateRelay(s, 'relay')
|
|
284
|
+
}
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const ESM_SYNTAX = /^[ \t]*(?:import[ \t]*[\w*{'"]|export[ \t]+(?:\{|\*|default\b|const\b|let\b|var\b|function\b|class\b|async\b))/m
|
|
289
|
+
|
|
290
|
+
/** The `type` of the package.json nearest to `dir`, or undefined. */
|
|
291
|
+
function packageType (dir) {
|
|
292
|
+
for (let i = 0; i < 64; i++) {
|
|
293
|
+
let text = null
|
|
294
|
+
try {
|
|
295
|
+
text = fs.readFileSync(path.join(dir, 'package.json'), 'utf8')
|
|
296
|
+
} catch (_) {}
|
|
297
|
+
if (text !== null) {
|
|
298
|
+
try {
|
|
299
|
+
const t = JSON.parse(text).type
|
|
300
|
+
return typeof t === 'string' ? t : undefined
|
|
301
|
+
} catch (_) {
|
|
302
|
+
return undefined
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const up = path.dirname(dir)
|
|
306
|
+
if (up === dir) return undefined
|
|
307
|
+
dir = up
|
|
308
|
+
}
|
|
309
|
+
return undefined
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Whether the app's entry point is (or may be) an ES module: `.mjs`/`.mts`; `.js`/`.ts`/no
|
|
314
|
+
* extension under a package.json with `"type": "module"`, or containing import/export syntax
|
|
315
|
+
* (Node's syntax detection); `--input-type=module` / `--experimental-default-type=module`. Errs
|
|
316
|
+
* towards true: a wrong "true" only costs the hooks thread, a wrong "false" leaves ES-module
|
|
317
|
+
* builds of hono/h3 unwrapped (their CommonJS builds are always covered).
|
|
318
|
+
*/
|
|
319
|
+
function entryIsEsm (argv, execArgv) {
|
|
320
|
+
argv = argv || process.argv
|
|
321
|
+
const flags = (execArgv || process.execArgv || []).concat(String(process.env.NODE_OPTIONS || '').split(/\s+/))
|
|
322
|
+
if (flags.some((f) => f === '--input-type=module' || f === '--experimental-default-type=module')) return true
|
|
323
|
+
const entry = argv[1]
|
|
324
|
+
if (typeof entry !== 'string' || entry === '') return false
|
|
325
|
+
let file = path.resolve(entry)
|
|
326
|
+
const ext = path.extname(file).toLowerCase()
|
|
327
|
+
if (ext === '.mjs' || ext === '.mts') return true
|
|
328
|
+
if (ext === '.cjs' || ext === '.cts') return false
|
|
329
|
+
if (ext === '' && !fs.existsSync(file) && fs.existsSync(file + '.js')) file += '.js'
|
|
330
|
+
try {
|
|
331
|
+
file = fs.realpathSync(file)
|
|
332
|
+
} catch (_) {}
|
|
333
|
+
if (packageType(path.dirname(file)) === 'module') return true
|
|
334
|
+
try {
|
|
335
|
+
const fd = fs.openSync(file, 'r')
|
|
336
|
+
try {
|
|
337
|
+
const buf = Buffer.alloc(64 * 1024)
|
|
338
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0)
|
|
339
|
+
return ESM_SYNTAX.test(buf.toString('utf8', 0, n))
|
|
340
|
+
} finally {
|
|
341
|
+
fs.closeSync(fd)
|
|
342
|
+
}
|
|
343
|
+
} catch (_) {
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* NIO-085 / NIO-210: register the module customization hooks that wrap ES-module libraries,
|
|
350
|
+
* only when tracing is enabled (called from preload.mjs / standalone.mjs).
|
|
351
|
+
*/
|
|
352
|
+
function registerEsmHooks () {
|
|
353
|
+
const s = state()
|
|
354
|
+
if (!s.active || s.esmHooked || !s.instance || !s.instance._tracingEnabled()) return false
|
|
355
|
+
let mod
|
|
356
|
+
try {
|
|
357
|
+
mod = require('module')
|
|
358
|
+
} catch (_) {
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
const file = path.join(__dirname, 'esm-hooks.mjs')
|
|
362
|
+
try {
|
|
363
|
+
if (typeof mod.registerHooks === 'function') {
|
|
364
|
+
const hooks = require(file)
|
|
365
|
+
mod.registerHooks({ load: hooks.load })
|
|
366
|
+
} else if (typeof mod.register === 'function') {
|
|
367
|
+
// off-thread hooks (Node < 22.15 / < 23.5) start a worker thread, ~100 ms of start-up on
|
|
368
|
+
// Node 18/20: only when the entry point is an ES module (a CommonJS app loads the
|
|
369
|
+
// CommonJS builds, which the require interception covers)
|
|
370
|
+
if (!entryIsEsm()) {
|
|
371
|
+
debug('CommonJS entry point: module hooks not registered (ES-module libraries imported later are not instrumented)')
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
mod.register(require('url').pathToFileURL(file).href)
|
|
375
|
+
} else {
|
|
376
|
+
debug('module hooks unavailable on this Node: ES-module libraries are not instrumented')
|
|
377
|
+
return false
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
debug('module hooks failed: %s', err && err.message)
|
|
381
|
+
return false
|
|
382
|
+
}
|
|
383
|
+
s.esmHooked = true
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
module.exports = {
|
|
388
|
+
activate,
|
|
389
|
+
registerEsmHooks,
|
|
390
|
+
shouldActivate,
|
|
391
|
+
isDaemonRelay,
|
|
392
|
+
scrubEnvironment,
|
|
393
|
+
isOurPreload,
|
|
394
|
+
secondInitConfig,
|
|
395
|
+
state,
|
|
396
|
+
readCaps,
|
|
397
|
+
openHub,
|
|
398
|
+
claimSingleton,
|
|
399
|
+
isMainThread,
|
|
400
|
+
entryIsEsm,
|
|
401
|
+
OWN_PRELOADS
|
|
402
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Applications (pmx 97 PAP-001/002/050/051, prodman 02 §6.20): the application this process
|
|
5
|
+
// belongs to. Under a pmx daemon the daemon decides (`PMX_APPLICATION`, PAP-022) and an init
|
|
6
|
+
// value that differs is ignored with one warning; standalone, init({ application }) >
|
|
7
|
+
// PMX_APPLICATION > `default`, an invalid value giving `default` and one warning.
|
|
8
|
+
|
|
9
|
+
const DEFAULT = 'default'
|
|
10
|
+
const ENV = 'PMX_APPLICATION'
|
|
11
|
+
|
|
12
|
+
const NAME = /^[a-z0-9](?:[a-z0-9._-]{0,61}[a-z0-9])?$/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* PAP-001/002: `{ value }` with the normalised name (`null` = unset: empty after trimming), or
|
|
16
|
+
* `{ error }` with the failing rule: 'type', 'charset', 'length' or 'reserved'.
|
|
17
|
+
*/
|
|
18
|
+
function normalize (raw) {
|
|
19
|
+
if (raw === undefined || raw === null) return { value: null }
|
|
20
|
+
if (typeof raw !== 'string') return { error: 'type' }
|
|
21
|
+
const n = raw.replace(/^[\t\n\f\r ]+|[\t\n\f\r ]+$/g, '').replace(/[A-Z]/g, (c) => c.toLowerCase())
|
|
22
|
+
if (n === '') return { value: null }
|
|
23
|
+
if (!/^[a-z0-9._-]+$/.test(n) || !/^[a-z0-9]/.test(n) || !/[a-z0-9]$/.test(n)) return { error: 'charset' }
|
|
24
|
+
if (n.length > 63) return { error: 'length' }
|
|
25
|
+
if (!NAME.test(n)) return { error: 'charset' }
|
|
26
|
+
if (n === 'all' || n === 'none' || n.startsWith('pmx')) return { error: 'reserved' }
|
|
27
|
+
return { value: n }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** PAP-004 wording. */
|
|
31
|
+
function message (raw, reason) {
|
|
32
|
+
const shown = typeof raw === 'string' ? raw : JSON.stringify(raw)
|
|
33
|
+
if (reason === 'reserved') return `Application name "${shown}" is reserved`
|
|
34
|
+
return `Invalid application name "${shown}": use 1-63 lower-case letters, digits, '.', '_' or '-', starting and ending with a letter or digit`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function warnDefault (line) {
|
|
38
|
+
try {
|
|
39
|
+
process.stderr.write(line + '\n')
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let resolved = null
|
|
44
|
+
const warned = new Set()
|
|
45
|
+
|
|
46
|
+
function warnOnce (warn, line) {
|
|
47
|
+
if (warned.has(line)) return
|
|
48
|
+
warned.add(line)
|
|
49
|
+
;(warn || warnDefault)(line)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* PAP-050: resolve and remember the application. `opts`: { managed (under a pmx daemon), env,
|
|
54
|
+
* warn (line printer) }. Returns the name (`default` included).
|
|
55
|
+
*/
|
|
56
|
+
function resolve (initValue, opts) {
|
|
57
|
+
opts = opts || {}
|
|
58
|
+
const env = opts.env || process.env
|
|
59
|
+
const warn = opts.warn
|
|
60
|
+
const fromEnv = normalize(env[ENV])
|
|
61
|
+
if (opts.managed) {
|
|
62
|
+
const a = fromEnv.value || DEFAULT
|
|
63
|
+
if (initValue !== undefined && initValue !== null) {
|
|
64
|
+
const n = normalize(initValue)
|
|
65
|
+
if (n.error || (n.value || DEFAULT) !== a) {
|
|
66
|
+
warnOnce(warn, `[pmx-io] application is set by pmx (${a}); ignoring init application ${typeof initValue === 'string' ? initValue : JSON.stringify(initValue)}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
resolved = a
|
|
70
|
+
return a
|
|
71
|
+
}
|
|
72
|
+
resolved = DEFAULT
|
|
73
|
+
for (const raw of [initValue, env[ENV]]) {
|
|
74
|
+
const n = normalize(raw)
|
|
75
|
+
if (n.error) {
|
|
76
|
+
warnOnce(warn, `[pmx-io] ${message(raw, n.error)}; using application ${DEFAULT}`)
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
if (n.value) {
|
|
80
|
+
resolved = n.value
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return resolved
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The application resolved by the last init, else the environment's (`default` when unset). */
|
|
88
|
+
function current (env) {
|
|
89
|
+
if (resolved !== null) return resolved
|
|
90
|
+
return normalize((env || process.env)[ENV]).value || DEFAULT
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** PAP-051: the `service.namespace` value, or null: the application, else a non-default pm2 namespace. */
|
|
94
|
+
function serviceNamespace (application, namespace) {
|
|
95
|
+
if (application && application !== DEFAULT) return application
|
|
96
|
+
if (typeof namespace === 'string' && namespace !== '' && namespace !== DEFAULT) return namespace
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Tests only. */
|
|
101
|
+
function _reset () {
|
|
102
|
+
resolved = null
|
|
103
|
+
warned.clear()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { DEFAULT, ENV, normalize, message, resolve, current, serviceNamespace, _reset }
|