@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,1023 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// The standalone root's prodman transport (80 NIO-262..NIO-269; prodman spec 11 APM-001…053;
|
|
5
|
+
// 95 §1–§4): configuration, handshake, WebSocket session, status, channels, commands, logs and
|
|
6
|
+
// shutdown. The instance keeps sending IPC-shaped messages (`send(type, data)`); `axm:monitor`,
|
|
7
|
+
// `axm:action` and `axm:option:configuration` feed the next `status`, other types become packets.
|
|
8
|
+
|
|
9
|
+
const os = require('os')
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const crypto = require('crypto')
|
|
13
|
+
const { VERSION } = require('../constants')
|
|
14
|
+
const container = require('./container')
|
|
15
|
+
const debug = require('../debug')('axm:standalone')
|
|
16
|
+
|
|
17
|
+
const IO_VERSION = '@pm2/io ' + VERSION
|
|
18
|
+
const STATUS_INTERVAL = 1000
|
|
19
|
+
const PING_INTERVAL = 5000
|
|
20
|
+
const PONG_TIMEOUT = 20000
|
|
21
|
+
const OFFLINE_QUEUE = 200
|
|
22
|
+
const SEND_QUEUE = 1000
|
|
23
|
+
// bytes the socket may hold unsent before packets wait in the (bounded) queue instead
|
|
24
|
+
const MAX_BUFFERED = 1024 * 1024
|
|
25
|
+
const RING = 200
|
|
26
|
+
const LOG_BATCH_MS = 250
|
|
27
|
+
const LOG_BATCH_BYTES = 64 * 1024
|
|
28
|
+
const LOG_BATCH_MAX = 4 * 1024 * 1024
|
|
29
|
+
const LOG_LINES_PER_SECOND = 2000
|
|
30
|
+
const LOG_DROP_REPORT_MS = 60000
|
|
31
|
+
const NOT_QUEUED = ['status', 'logs', 'trace-span', 'trace-spans']
|
|
32
|
+
const REFUSAL = '%s is not available: %s runs without PM2 (@pm2/io standalone)'
|
|
33
|
+
|
|
34
|
+
// -- configuration (NIO-262) ----------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const IPV4 = /^\d+\.\d+\.\d+\.\d+(:\d+)?(\/|$)/
|
|
37
|
+
|
|
38
|
+
function resolveUrl (configured, env) {
|
|
39
|
+
env = env || process.env
|
|
40
|
+
let raw = null
|
|
41
|
+
for (const v of [configured, env.PRODMAN_URL, env.KEYMETRICS_NODE]) {
|
|
42
|
+
if (typeof v === 'string' && v.trim()) {
|
|
43
|
+
raw = v.trim()
|
|
44
|
+
break
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (raw === null) raw = 'https://pmx.aidra.io'
|
|
48
|
+
if (!raw.includes('://')) {
|
|
49
|
+
const host = raw.split('/')[0]
|
|
50
|
+
const plain = host === 'localhost' || host.startsWith('localhost:') || IPV4.test(raw) || host.startsWith('[')
|
|
51
|
+
raw = (plain ? 'http://' : 'https://') + raw
|
|
52
|
+
}
|
|
53
|
+
return raw.replace(/\/+$/, '')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function serverName (cfg, info, host) {
|
|
57
|
+
const app = cfg.appName || null
|
|
58
|
+
const h = host || container.hostname()
|
|
59
|
+
info = info || new container.ContainerInfo({ inContainer: false, runtime: 'unknown', orchestrator: 'none' })
|
|
60
|
+
if (cfg.explicitServerName) return container.expand(String(cfg.explicitServerName), info.nameContext(app, h))
|
|
61
|
+
const dflt = info.defaultMachineName(app, h)
|
|
62
|
+
if (dflt) return dflt
|
|
63
|
+
if (!app) return null
|
|
64
|
+
return container.truncateName(`${app}@${h}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Environment (APM-010) overlaid by `apmOptions` (options win); `base` = a resolved config. */
|
|
68
|
+
function resolveConfig (apmOptions, env, info, base) {
|
|
69
|
+
env = env || process.env
|
|
70
|
+
const o = apmOptions && typeof apmOptions === 'object' ? apmOptions : {}
|
|
71
|
+
const b = base || null
|
|
72
|
+
const opt = (k) => (o[k] !== undefined && o[k] !== null && o[k] !== '' ? o[k] : undefined)
|
|
73
|
+
const pick = (k, envName) => {
|
|
74
|
+
const v = opt(k)
|
|
75
|
+
if (v !== undefined) return v
|
|
76
|
+
if (b && b[k]) return b[k]
|
|
77
|
+
return env[envName] || null
|
|
78
|
+
}
|
|
79
|
+
const cfg = {}
|
|
80
|
+
cfg.url = resolveUrl(opt('url') || (b ? b.url : undefined), env)
|
|
81
|
+
cfg.publicKey = pick('publicKey', 'PM2_PUBLIC_KEY')
|
|
82
|
+
cfg.secretKey = pick('secretKey', 'PM2_SECRET_KEY')
|
|
83
|
+
cfg.appName = pick('appName', 'PM2_APP_NAME')
|
|
84
|
+
let explicit = opt('serverName')
|
|
85
|
+
if (explicit === undefined && b) explicit = b.explicitServerName
|
|
86
|
+
if (explicit === undefined || explicit === null) explicit = env.PM2_MACHINE_NAME || null
|
|
87
|
+
cfg.explicitServerName = explicit
|
|
88
|
+
cfg.sendLogs = opt('sendLogs') !== undefined ? Boolean(o.sendLogs) : Boolean(b && b.sendLogs)
|
|
89
|
+
cfg.disableLogs = opt('disableLogs') !== undefined ? Boolean(o.disableLogs) : Boolean(b && b.disableLogs)
|
|
90
|
+
let cap = opt('captureLogs')
|
|
91
|
+
if (cap === undefined) cap = b ? b.captureLogs : (env.PMX_IO_CAPTURE_LOGS || 'stdio')
|
|
92
|
+
cap = String(cap).toLowerCase()
|
|
93
|
+
if (cap === 'false' || cap === 'none' || cap === '0') cap = 'off'
|
|
94
|
+
if (cap === 'sys') cap = 'stdio'
|
|
95
|
+
if (cap !== 'stdio' && cap !== 'off') {
|
|
96
|
+
debug('unknown captureLogs %s, using stdio', cap)
|
|
97
|
+
cap = 'stdio'
|
|
98
|
+
}
|
|
99
|
+
if (cfg.disableLogs) cap = 'off'
|
|
100
|
+
cfg.captureLogs = cap
|
|
101
|
+
const lf = opt('logFilter')
|
|
102
|
+
cfg.logFilter = lf !== undefined ? lf : (b ? b.logFilter : undefined)
|
|
103
|
+
let proxy = opt('proxy')
|
|
104
|
+
if (proxy === undefined) proxy = b ? b.proxy : (env.PMX_IO_PROXY || null)
|
|
105
|
+
cfg.proxy = proxy || null
|
|
106
|
+
cfg.missing = [['PM2_PUBLIC_KEY', 'publicKey'], ['PM2_SECRET_KEY', 'secretKey'], ['PM2_APP_NAME', 'appName']].filter(([, k]) => !cfg[k]).map(([n]) => n)
|
|
107
|
+
for (const k of ['publicKey', 'secretKey', 'appName']) if (cfg[k] !== null) cfg[k] = String(cfg[k])
|
|
108
|
+
cfg.serverName = serverName(cfg, info)
|
|
109
|
+
return cfg
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function matchesFilter (flt, text) {
|
|
113
|
+
if (flt === undefined || flt === null || flt === '') return false
|
|
114
|
+
if (typeof flt === 'string') return text.includes(flt)
|
|
115
|
+
if (flt instanceof RegExp) {
|
|
116
|
+
flt.lastIndex = 0
|
|
117
|
+
return flt.test(text)
|
|
118
|
+
}
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function redact (url) {
|
|
123
|
+
try {
|
|
124
|
+
const u = new URL(url)
|
|
125
|
+
if (u.username || u.password) {
|
|
126
|
+
u.username = '***'
|
|
127
|
+
u.password = ''
|
|
128
|
+
}
|
|
129
|
+
return u.href
|
|
130
|
+
} catch (_) {
|
|
131
|
+
return '<url>'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// -- footprint (NIO-263) --------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
function procCpuTicks (pid) {
|
|
138
|
+
let data
|
|
139
|
+
try {
|
|
140
|
+
data = fs.readFileSync(`/proc/${pid}/stat`, 'latin1')
|
|
141
|
+
} catch (_) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
const rp = data.lastIndexOf(')')
|
|
145
|
+
if (rp < 0) return null
|
|
146
|
+
const f = data.slice(rp + 2).split(' ')
|
|
147
|
+
if (f[0] === 'Z') return null
|
|
148
|
+
const u = Number(f[11])
|
|
149
|
+
const s = Number(f[12])
|
|
150
|
+
return Number.isFinite(u) && Number.isFinite(s) ? u + s : null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function procMemory (pid) {
|
|
154
|
+
try {
|
|
155
|
+
const t = fs.readFileSync(`/proc/${pid}/smaps_rollup`, 'latin1')
|
|
156
|
+
const m = /^Pss:\s+(\d+)/m.exec(t)
|
|
157
|
+
if (m) return Number(m[1]) * 1024
|
|
158
|
+
} catch (_) {}
|
|
159
|
+
try {
|
|
160
|
+
const t = fs.readFileSync(`/proc/${pid}/statm`, 'latin1').split(' ')
|
|
161
|
+
return Number(t[1]) * 4096
|
|
162
|
+
} catch (_) {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class Footprint {
|
|
168
|
+
constructor (relayPids) {
|
|
169
|
+
this.relayPids = relayPids
|
|
170
|
+
this.linux = fs.existsSync(`/proc/${process.pid}/stat`)
|
|
171
|
+
this.tck = 100
|
|
172
|
+
this._prev = new Map()
|
|
173
|
+
this._prevT = null
|
|
174
|
+
this._prevSelf = null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
sample () {
|
|
178
|
+
const now = Date.now() / 1000
|
|
179
|
+
const dt = this._prevT !== null ? now - this._prevT : 0
|
|
180
|
+
this._prevT = now
|
|
181
|
+
if (!this.linux) {
|
|
182
|
+
const u = process.cpuUsage()
|
|
183
|
+
const t = (u.user + u.system) / 1e6
|
|
184
|
+
const prev = this._prevSelf
|
|
185
|
+
this._prevSelf = t
|
|
186
|
+
const cpu = prev !== null && dt > 0 ? Math.max(0, (t - prev) / dt * 100) : 0
|
|
187
|
+
return { cpu: Math.round(cpu * 10) / 10, memory: process.memoryUsage().rss }
|
|
188
|
+
}
|
|
189
|
+
const pids = [process.pid]
|
|
190
|
+
try {
|
|
191
|
+
for (const p of this.relayPids()) if (!pids.includes(p)) pids.push(p)
|
|
192
|
+
} catch (_) {}
|
|
193
|
+
let ticks = 0
|
|
194
|
+
let mem = 0
|
|
195
|
+
const seen = new Map()
|
|
196
|
+
for (const pid of pids) {
|
|
197
|
+
const t = procCpuTicks(pid)
|
|
198
|
+
if (t === null) continue
|
|
199
|
+
const m = procMemory(pid)
|
|
200
|
+
if (m !== null) mem += m
|
|
201
|
+
const prev = this._prev.get(pid)
|
|
202
|
+
if (prev !== undefined) ticks += Math.max(0, t - prev)
|
|
203
|
+
seen.set(pid, t)
|
|
204
|
+
}
|
|
205
|
+
this._prev = seen
|
|
206
|
+
const cpu = dt > 0 ? (ticks / this.tck) / dt * 100 : 0
|
|
207
|
+
return { cpu: Math.round(cpu * 10) / 10, memory: mem }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// -- host facts -----------------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function username () {
|
|
214
|
+
for (const k of ['SUDO_USER', 'C9_USER', 'LOGNAME', 'USER', 'LNAME', 'USERNAME']) if (process.env[k]) return process.env[k]
|
|
215
|
+
try {
|
|
216
|
+
return os.userInfo().username
|
|
217
|
+
} catch (_) {
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function cpuModel () {
|
|
223
|
+
const c = os.cpus()
|
|
224
|
+
return c && c[0] ? c[0].model : null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function appVersion () {
|
|
228
|
+
if (process.env.PMX_APP_VERSION) return process.env.PMX_APP_VERSION
|
|
229
|
+
try {
|
|
230
|
+
const { readPackageJsonSync } = require('../util')
|
|
231
|
+
const pkg = readPackageJsonSync()
|
|
232
|
+
if (pkg && typeof pkg.version === 'string') return pkg.version
|
|
233
|
+
} catch (_) {}
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function backoff (baseMs, attempt) {
|
|
238
|
+
const ms = Math.min(60000, baseMs * Math.pow(2, attempt))
|
|
239
|
+
return Math.round(ms * (0.8 + Math.random() * 0.4))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function countLines (text) {
|
|
243
|
+
const n = text.split('\n').length - (text.endsWith('\n') ? 1 : 0)
|
|
244
|
+
return n || 1
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// -- transport ------------------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
class ProdmanTransport {
|
|
250
|
+
/**
|
|
251
|
+
* @param {object} cfg resolveConfig() result
|
|
252
|
+
* @param {object} info ContainerInfo
|
|
253
|
+
* @param {{relayPids?: Function}} [opts]
|
|
254
|
+
*/
|
|
255
|
+
constructor (cfg, info, opts) {
|
|
256
|
+
opts = opts || {}
|
|
257
|
+
this.kind = 'standalone'
|
|
258
|
+
this.cfg = cfg
|
|
259
|
+
this.info = info
|
|
260
|
+
this._listeners = []
|
|
261
|
+
this._pending = [] // [channel, text, cb]
|
|
262
|
+
this.tracingLayer = null // 80 NIO-298 (prodman `tracing` key for this app)
|
|
263
|
+
this._ws = null
|
|
264
|
+
this._connecting = false
|
|
265
|
+
this._stopping = false
|
|
266
|
+
this._closed = false
|
|
267
|
+
this._timers = { status: null, ping: null, retry: null, logs: null }
|
|
268
|
+
this._hsAttempt = 0
|
|
269
|
+
this._wsAttempt = 0
|
|
270
|
+
this._lastRefusal = null
|
|
271
|
+
this.collectLogs = false
|
|
272
|
+
this._batchSupported = false
|
|
273
|
+
this.ring = { out: [], err: [] }
|
|
274
|
+
this._batch = { out: [], err: [] }
|
|
275
|
+
this._batchBytes = 0
|
|
276
|
+
this._batchSince = null
|
|
277
|
+
this._windowStart = 0
|
|
278
|
+
this._windowLines = 0
|
|
279
|
+
this._droppedLines = 0
|
|
280
|
+
this._dropReportedAt = null
|
|
281
|
+
this.monitor = {}
|
|
282
|
+
this.actions = new Map()
|
|
283
|
+
this.options = {}
|
|
284
|
+
this.startedAt = Date.now()
|
|
285
|
+
this.uniqueId = crypto.randomBytes(12).toString('hex')
|
|
286
|
+
this.serverUniqueId = crypto.randomBytes(16).toString('hex')
|
|
287
|
+
this.footprint = new Footprint(opts.relayPids || (() => []))
|
|
288
|
+
this.hostMetrics = new container.HostMetrics(info)
|
|
289
|
+
this._shutdownPayload = null
|
|
290
|
+
this._shutdownSentOn = null
|
|
291
|
+
this._ip = null
|
|
292
|
+
this.statusCount = 0
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
get server () {
|
|
296
|
+
return this.cfg.serverName
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
get appName () {
|
|
300
|
+
return this.cfg.appName
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** pmx 97 PAP-050: the application (init option > PMX_APPLICATION > default). */
|
|
304
|
+
get application () {
|
|
305
|
+
return require('../application').current()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** PAP-050: init decided the application; the tracing layer is resolved again for it. */
|
|
309
|
+
setApplication () {
|
|
310
|
+
if (this._tracingBlock !== undefined) this._applyTracing(this._tracingBlock)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
ref () {
|
|
314
|
+
return { pm_id: 0, name: this.cfg.appName, application: this.application, rev: null, server: this.cfg.serverName }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
batchSupported () {
|
|
318
|
+
return this._batchSupported
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
init () {
|
|
322
|
+
return this
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
start () {
|
|
326
|
+
this._schedule(() => this._run(), 0)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
reconfigure (cfg) {
|
|
330
|
+
const reconnect = cfg.url !== this.cfg.url || cfg.publicKey !== this.cfg.publicKey || cfg.secretKey !== this.cfg.secretKey ||
|
|
331
|
+
cfg.serverName !== this.cfg.serverName || cfg.proxy !== this.cfg.proxy
|
|
332
|
+
this.cfg = cfg
|
|
333
|
+
if (reconnect && this._ws) this._ws.terminate()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// -- connection -----------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
_schedule (fn, ms) {
|
|
339
|
+
if (this._stopping) return
|
|
340
|
+
clearTimeout(this._timers.retry)
|
|
341
|
+
this._timers.retry = setTimeout(fn, ms)
|
|
342
|
+
if (this._timers.retry.unref) this._timers.retry.unref()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async _run () {
|
|
346
|
+
if (this._stopping || this._ws || this._connecting) return
|
|
347
|
+
this._connecting = true
|
|
348
|
+
try {
|
|
349
|
+
if (this.info.ecsPending()) {
|
|
350
|
+
try {
|
|
351
|
+
await this.info.fetchEcs()
|
|
352
|
+
const name = serverName(this.cfg, this.info)
|
|
353
|
+
if (name) this.cfg = Object.assign({}, this.cfg, { serverName: name })
|
|
354
|
+
} catch (err) {
|
|
355
|
+
debug('ECS metadata failed: %s', err && err.message)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
debug('prodman %s as %s (app %s)', redact(this.cfg.url), this.server, this.appName)
|
|
359
|
+
let hs
|
|
360
|
+
try {
|
|
361
|
+
hs = await this._handshake()
|
|
362
|
+
} catch (err) {
|
|
363
|
+
this._connecting = false
|
|
364
|
+
if (err && err.refused) {
|
|
365
|
+
if (err.message !== this._lastRefusal) {
|
|
366
|
+
this._lastRefusal = err.message
|
|
367
|
+
try {
|
|
368
|
+
process.stderr.write(`[@pm2/io] prodman refused the connection: ${err.message}\n`)
|
|
369
|
+
} catch (_) {}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const delay = backoff(1000, this._hsAttempt++)
|
|
373
|
+
debug('handshake failed (%s), retrying in %d ms', err && err.message, delay)
|
|
374
|
+
this._schedule(() => this._run(), delay)
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
this._hsAttempt = 0
|
|
378
|
+
this._lastRefusal = null
|
|
379
|
+
if (hs.collectLogs) this.collectLogs = true
|
|
380
|
+
this._batchSupported = hs.traceSpansBatch
|
|
381
|
+
this._applyTracing(hs.tracing)
|
|
382
|
+
this._connect(hs.endpoint)
|
|
383
|
+
} catch (err) {
|
|
384
|
+
this._connecting = false
|
|
385
|
+
debug('connection failed: %s', err && err.message)
|
|
386
|
+
this._schedule(() => this._run(), backoff(5000, this._wsAttempt++))
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
handshakeBody () {
|
|
391
|
+
const limits = this.hostMetrics.limits()
|
|
392
|
+
const n = this.info.inContainer() ? limits.cpusRoundedUp() : (os.cpus() || []).length || 1
|
|
393
|
+
const model = cpuModel() || 'unknown'
|
|
394
|
+
const cpus = []
|
|
395
|
+
for (let i = 0; i < n; i++) cpus.push({ model, speed: 0 })
|
|
396
|
+
return {
|
|
397
|
+
public_id: this.cfg.publicKey,
|
|
398
|
+
private_id: this.cfg.secretKey,
|
|
399
|
+
data: {
|
|
400
|
+
MACHINE_NAME: this.server,
|
|
401
|
+
PUBLIC_KEY: this.cfg.publicKey,
|
|
402
|
+
RECYCLE: false,
|
|
403
|
+
PM2_VERSION: null,
|
|
404
|
+
IO_VERSION,
|
|
405
|
+
MEMORY: Math.floor(limits.memoryBytes / 1048576),
|
|
406
|
+
HOSTNAME: container.hostname(),
|
|
407
|
+
CPUS: cpus
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async _handshake () {
|
|
413
|
+
const { httpRequest } = require('../transport/ws')
|
|
414
|
+
const body = JSON.stringify(this.handshakeBody())
|
|
415
|
+
const res = await httpRequest('POST', this.cfg.url + '/api/node/verifyPM2', body, {
|
|
416
|
+
'Content-Type': 'application/json',
|
|
417
|
+
'User-Agent': '@pmx/io/' + VERSION,
|
|
418
|
+
Accept: 'application/json'
|
|
419
|
+
}, { timeout: 15000, proxy: this.cfg.proxy || undefined })
|
|
420
|
+
let doc
|
|
421
|
+
try {
|
|
422
|
+
doc = JSON.parse(res.body.toString('utf8'))
|
|
423
|
+
} catch (_) {
|
|
424
|
+
throw new Error(`handshake: HTTP ${res.status}, not JSON`)
|
|
425
|
+
}
|
|
426
|
+
const ws = doc && doc.endpoints && typeof doc.endpoints.ws === 'string' ? doc.endpoints.ws : null
|
|
427
|
+
if (!doc || doc.active !== true || !ws) {
|
|
428
|
+
const e = new Error((doc && typeof doc.msg === 'string' && doc.msg) || 'bucket not active')
|
|
429
|
+
e.refused = true
|
|
430
|
+
throw e
|
|
431
|
+
}
|
|
432
|
+
const p = doc.prodman && typeof doc.prodman === 'object' ? doc.prodman : {}
|
|
433
|
+
return { endpoint: ws, collectLogs: p.collect_logs === true, traceSpansBatch: p.trace_spans_batch === true, tracing: p.tracing }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_pongTimeout () {
|
|
437
|
+
const v = Number(process.env.PRODMAN_WS_TIMEOUT)
|
|
438
|
+
return Number.isFinite(v) && v > 0 ? v : PONG_TIMEOUT
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
_connect (endpoint) {
|
|
442
|
+
if (this._stopping) {
|
|
443
|
+
this._connecting = false
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
const { WebSocketClient } = require('../transport/ws')
|
|
447
|
+
const ws = new WebSocketClient(endpoint, {
|
|
448
|
+
'X-KM-PUBLIC': this.cfg.publicKey,
|
|
449
|
+
'X-KM-SECRET': this.cfg.secretKey,
|
|
450
|
+
'X-KM-SERVER': this.server,
|
|
451
|
+
'X-PM2-VERSION': IO_VERSION,
|
|
452
|
+
'X-PROTOCOL-VERSION': '1',
|
|
453
|
+
'User-Agent': '@pmx/io/' + VERSION
|
|
454
|
+
}, { proxy: this.cfg.proxy || undefined })
|
|
455
|
+
let lastSeen = Date.now()
|
|
456
|
+
let opened = false
|
|
457
|
+
ws.on('open', () => {
|
|
458
|
+
opened = true
|
|
459
|
+
this._ws = ws
|
|
460
|
+
this._connecting = false
|
|
461
|
+
this._wsAttempt = 0
|
|
462
|
+
debug('connected to %s', redact(endpoint))
|
|
463
|
+
try {
|
|
464
|
+
const addr = ws._socket && ws._socket.localAddress
|
|
465
|
+
if (typeof addr === 'string' && addr.includes('.') && !addr.startsWith('127.')) this._ip = addr
|
|
466
|
+
} catch (_) {}
|
|
467
|
+
this._sendStatus(true)
|
|
468
|
+
if (this._shutdownPayload && this._shutdownSentOn !== ws) {
|
|
469
|
+
// CTR-022: re-sent on a new connection while the shutdown is in progress
|
|
470
|
+
this._pending.unshift(['agent:shutdown', JSON.stringify({ channel: 'agent:shutdown', payload: this._shutdownPayload }), null])
|
|
471
|
+
}
|
|
472
|
+
this._drain()
|
|
473
|
+
this._timers.status = setInterval(() => this._sendStatus(false), STATUS_INTERVAL)
|
|
474
|
+
if (this._timers.status.unref) this._timers.status.unref()
|
|
475
|
+
this._timers.ping = setInterval(() => {
|
|
476
|
+
if (Date.now() - lastSeen > this._pongTimeout()) return ws.terminate()
|
|
477
|
+
ws.ping()
|
|
478
|
+
}, PING_INTERVAL)
|
|
479
|
+
if (this._timers.ping.unref) this._timers.ping.unref()
|
|
480
|
+
this._timers.logs = setInterval(() => this._flushLogs(false), LOG_BATCH_MS)
|
|
481
|
+
if (this._timers.logs.unref) this._timers.logs.unref()
|
|
482
|
+
})
|
|
483
|
+
ws.on('drain', () => this._drain())
|
|
484
|
+
ws.on('pong', () => { lastSeen = Date.now() })
|
|
485
|
+
ws.on('ping', () => { lastSeen = Date.now() })
|
|
486
|
+
ws.on('message', (text) => {
|
|
487
|
+
lastSeen = Date.now()
|
|
488
|
+
this._onText(text)
|
|
489
|
+
})
|
|
490
|
+
ws.on('error', (err) => debug('socket error: %s', err && err.message))
|
|
491
|
+
ws.on('close', () => {
|
|
492
|
+
if (!opened) this._connecting = false
|
|
493
|
+
if (this._ws === ws) this._disconnected()
|
|
494
|
+
if (this._stopping) return
|
|
495
|
+
const delay = backoff(5000, this._wsAttempt++)
|
|
496
|
+
debug('connection closed, reconnecting in %d ms', delay)
|
|
497
|
+
this._schedule(() => this._run(), delay)
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
_disconnected () {
|
|
502
|
+
this._ws = null
|
|
503
|
+
clearInterval(this._timers.status)
|
|
504
|
+
clearInterval(this._timers.ping)
|
|
505
|
+
clearInterval(this._timers.logs)
|
|
506
|
+
this._trimOffline()
|
|
507
|
+
this._batch = { out: [], err: [] }
|
|
508
|
+
this._batchBytes = 0
|
|
509
|
+
this._batchSince = null
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_connected () {
|
|
513
|
+
return Boolean(this._ws && this._ws.readyState === 1)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// -- queue ----------------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
_lost (entry) {
|
|
519
|
+
if (entry && entry[2]) {
|
|
520
|
+
try {
|
|
521
|
+
entry[2](new Error('not delivered'))
|
|
522
|
+
} catch (_) {}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
_trimOffline () {
|
|
527
|
+
const kept = []
|
|
528
|
+
for (const e of this._pending) {
|
|
529
|
+
if (NOT_QUEUED.includes(e[0])) this._lost(e)
|
|
530
|
+
else kept.push(e)
|
|
531
|
+
}
|
|
532
|
+
while (kept.length > OFFLINE_QUEUE) kept.shift()
|
|
533
|
+
this._pending = kept
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
_evict () {
|
|
537
|
+
const q = this._pending
|
|
538
|
+
for (const wanted of ['status', 'logs', 'trace-span', 'trace-spans']) {
|
|
539
|
+
const i = q.findIndex((e) => e[0] === wanted)
|
|
540
|
+
if (i !== -1) return this._lost(q.splice(i, 1)[0])
|
|
541
|
+
}
|
|
542
|
+
const i = q.findIndex((e) => e[0] !== 'process:exception' && e[0] !== 'agent:shutdown')
|
|
543
|
+
if (i !== -1) this._lost(q.splice(i, 1)[0])
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
_drain () {
|
|
547
|
+
// a slow prodman leaves data in the socket: stop at MAX_BUFFERED (resumed on 'drain') so the
|
|
548
|
+
// queue's bounds and eviction apply instead of an unbounded socket buffer
|
|
549
|
+
while (this._pending.length && this._connected() && this._ws.bufferedAmount() < MAX_BUFFERED) {
|
|
550
|
+
const [ch, text, cb] = this._pending.shift()
|
|
551
|
+
if (ch === 'agent:shutdown') this._shutdownSentOn = this._ws
|
|
552
|
+
if (!this._ws.send(text, cb ? (err) => cb(err) : undefined)) {
|
|
553
|
+
this._pending.unshift([ch, text, cb])
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Queue or write one packet. While disconnected only APM-026 channels are kept (≤ 200). */
|
|
560
|
+
packet (channel, payload, cb, front) {
|
|
561
|
+
let text
|
|
562
|
+
try {
|
|
563
|
+
text = JSON.stringify({ channel, payload })
|
|
564
|
+
} catch (err) {
|
|
565
|
+
debug('cannot encode %s: %s', channel, err && err.message)
|
|
566
|
+
if (cb) process.nextTick(cb, err)
|
|
567
|
+
return false
|
|
568
|
+
}
|
|
569
|
+
if (this._closed) {
|
|
570
|
+
if (cb) process.nextTick(cb, new Error('closed'))
|
|
571
|
+
return false
|
|
572
|
+
}
|
|
573
|
+
if (!this._connected()) {
|
|
574
|
+
if (NOT_QUEUED.includes(channel)) {
|
|
575
|
+
if (cb) process.nextTick(cb, new Error('not connected'))
|
|
576
|
+
return false
|
|
577
|
+
}
|
|
578
|
+
this._pending.push([channel, text, cb])
|
|
579
|
+
this._trimOffline()
|
|
580
|
+
return true
|
|
581
|
+
}
|
|
582
|
+
if (this._pending.length >= SEND_QUEUE) this._evict()
|
|
583
|
+
// only exceptions and agent:shutdown are left: drop the oldest exception (hard bound)
|
|
584
|
+
if (this._pending.length >= SEND_QUEUE * 2) this._lost(this._pending.splice(this._pending.findIndex((e) => e[0] !== 'agent:shutdown'), 1)[0])
|
|
585
|
+
if (front) this._pending.unshift([channel, text, cb])
|
|
586
|
+
else this._pending.push([channel, text, cb])
|
|
587
|
+
this._drain()
|
|
588
|
+
return true
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// -- the instance's messages ----------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
send (type, data, cb) {
|
|
594
|
+
try {
|
|
595
|
+
return this._route(type, data, cb)
|
|
596
|
+
} catch (err) {
|
|
597
|
+
debug('send failed: %s', err && err.message)
|
|
598
|
+
if (cb) process.nextTick(cb, err)
|
|
599
|
+
return false
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
_route (type, data, cb) {
|
|
604
|
+
const done = () => {
|
|
605
|
+
if (cb) process.nextTick(cb)
|
|
606
|
+
return true
|
|
607
|
+
}
|
|
608
|
+
switch (type) {
|
|
609
|
+
case 'axm:monitor':
|
|
610
|
+
if (data && typeof data === 'object') this.monitor = data
|
|
611
|
+
return done()
|
|
612
|
+
case 'axm:action':
|
|
613
|
+
if (data && typeof data.action_name === 'string') this.actions.set(data.action_name, data)
|
|
614
|
+
return done()
|
|
615
|
+
case 'axm:option:configuration':
|
|
616
|
+
if (data && typeof data === 'object') Object.assign(this.options, data)
|
|
617
|
+
return done()
|
|
618
|
+
case 'human:event': {
|
|
619
|
+
const d = Object.assign({}, data)
|
|
620
|
+
const name = d.__name
|
|
621
|
+
delete d.__name
|
|
622
|
+
return this.packet('human:event', { at: Date.now(), name: typeof name === 'string' ? name : 'event', data: d, process: this.ref() }, cb)
|
|
623
|
+
}
|
|
624
|
+
case 'process:exception': {
|
|
625
|
+
const payload = Object.assign({}, data)
|
|
626
|
+
if (payload.last_logs === undefined && this.cfg.captureLogs !== 'off') {
|
|
627
|
+
payload.last_logs = this.ring.err.slice(-20).concat(this.ring.out.slice(-20))
|
|
628
|
+
}
|
|
629
|
+
return this.packet('process:exception', { at: Date.now(), data: payload, process: this.ref() }, cb)
|
|
630
|
+
}
|
|
631
|
+
case 'trace-span':
|
|
632
|
+
case 'trace-spans':
|
|
633
|
+
case 'axm:reply':
|
|
634
|
+
case 'profilings':
|
|
635
|
+
case 'application:dependencies':
|
|
636
|
+
return this.packet(type, { at: Date.now(), data, process: this.ref() }, cb)
|
|
637
|
+
case 'ready':
|
|
638
|
+
return this.packet('ready', { at: Date.now(), data: {}, process: this.ref() }, cb)
|
|
639
|
+
default:
|
|
640
|
+
if (typeof type === 'string' && type.startsWith('axm:scoped_action:')) return this.packet(type, { at: Date.now(), data, process: this.ref() }, cb)
|
|
641
|
+
debug('not forwarded to prodman: %s', type)
|
|
642
|
+
return done()
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
sendRaw (msg, cb) {
|
|
647
|
+
if (msg === 'ready') return this.send('ready', {}, cb)
|
|
648
|
+
if (msg && typeof msg === 'object' && typeof msg.type === 'string') return this.send(msg.type, msg.data, cb)
|
|
649
|
+
if (cb) process.nextTick(cb)
|
|
650
|
+
return false
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
onMessage (fn) {
|
|
654
|
+
this._listeners.push(fn)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
offMessage (fn) {
|
|
658
|
+
this._listeners = this._listeners.filter((f) => f !== fn)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
isConnected () {
|
|
662
|
+
return true
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** IPC transports are destroyed on re-init; the prodman connection is not (see destroyAll). */
|
|
666
|
+
destroy () {
|
|
667
|
+
this._listeners = []
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// -- status (NIO-265) -----------------------------------------------------------------------
|
|
671
|
+
|
|
672
|
+
statusPayload () {
|
|
673
|
+
const fp = this.footprint.sample()
|
|
674
|
+
let options = this.options
|
|
675
|
+
try {
|
|
676
|
+
if (JSON.stringify(options).length > 8192) options = {}
|
|
677
|
+
} catch (_) {
|
|
678
|
+
options = {}
|
|
679
|
+
}
|
|
680
|
+
const limits = this.hostMetrics.limits()
|
|
681
|
+
const host = container.hostname()
|
|
682
|
+
const user = username()
|
|
683
|
+
const bun = typeof process.versions.bun === 'string'
|
|
684
|
+
const script = process.argv[1] || null
|
|
685
|
+
const runtime = {
|
|
686
|
+
kind: bun ? 'bun' : 'node',
|
|
687
|
+
version: bun ? process.versions.bun : process.versions.node,
|
|
688
|
+
versionSource: 'process',
|
|
689
|
+
interpreter: bun ? 'bun' : 'node',
|
|
690
|
+
interpreterPath: process.execPath,
|
|
691
|
+
interpreterArgs: [],
|
|
692
|
+
nodeArgs: process.execArgv,
|
|
693
|
+
virtualenv: null,
|
|
694
|
+
script,
|
|
695
|
+
args: process.argv.slice(2),
|
|
696
|
+
cwd: safeCwd(),
|
|
697
|
+
execMode: 'standalone',
|
|
698
|
+
instances: 1,
|
|
699
|
+
user,
|
|
700
|
+
nodeEnv: process.env.NODE_ENV || null,
|
|
701
|
+
watch: false,
|
|
702
|
+
autorestart: false,
|
|
703
|
+
maxMemoryRestart: null,
|
|
704
|
+
killTimeout: null,
|
|
705
|
+
cronRestart: null,
|
|
706
|
+
mergeLogs: false
|
|
707
|
+
}
|
|
708
|
+
if (this._version === undefined) this._version = appVersion()
|
|
709
|
+
const proc = {
|
|
710
|
+
pid: process.pid,
|
|
711
|
+
name: this.appName,
|
|
712
|
+
pm_id: 0,
|
|
713
|
+
status: 'online',
|
|
714
|
+
exec_mode: 'fork_mode',
|
|
715
|
+
restart_time: 0,
|
|
716
|
+
instances: 1,
|
|
717
|
+
pm_uptime: this.startedAt,
|
|
718
|
+
created_at: this.startedAt,
|
|
719
|
+
unique_id: this.uniqueId,
|
|
720
|
+
cpu: fp.cpu,
|
|
721
|
+
memory: fp.memory,
|
|
722
|
+
axm_monitor: this.monitor,
|
|
723
|
+
axm_actions: Array.from(this.actions.values()),
|
|
724
|
+
axm_options: options,
|
|
725
|
+
axm_dynamic: {},
|
|
726
|
+
version: this._version,
|
|
727
|
+
node_version: process.versions.node,
|
|
728
|
+
node_env: process.env.NODE_ENV || null,
|
|
729
|
+
pm_cwd: safeCwd(),
|
|
730
|
+
path: script,
|
|
731
|
+
args: process.argv.slice(2),
|
|
732
|
+
versioning: null,
|
|
733
|
+
runtime,
|
|
734
|
+
// prodman 02 PROTO-170 (pmx 97 PAP-050)
|
|
735
|
+
application: this.application
|
|
736
|
+
}
|
|
737
|
+
const inContainer = this.info.inContainer()
|
|
738
|
+
const cpus = os.cpus() || []
|
|
739
|
+
const server = {
|
|
740
|
+
username: user,
|
|
741
|
+
hostname: host,
|
|
742
|
+
uptime: os.uptime(),
|
|
743
|
+
platform: os.platform(),
|
|
744
|
+
pm2_version: null,
|
|
745
|
+
pm2_agent_version: IO_VERSION,
|
|
746
|
+
node_version: process.version,
|
|
747
|
+
unique_id: this.serverUniqueId,
|
|
748
|
+
loadavg: os.loadavg(),
|
|
749
|
+
total_mem: limits.memoryBytes,
|
|
750
|
+
type: os.type(),
|
|
751
|
+
arch: os.arch(),
|
|
752
|
+
cpus: inContainer ? limits.cpusRoundedUp() : cpus.length || 1,
|
|
753
|
+
cpu_model: cpuModel(),
|
|
754
|
+
system: this.hostMetrics.collect(),
|
|
755
|
+
agent_kind: '@pm2/io',
|
|
756
|
+
ephemeral: this.info.ephemeral(),
|
|
757
|
+
// prodman 02 PROTO-164: the `tracing` settings key is applied (80 NIO-298)
|
|
758
|
+
tracing_control: 1,
|
|
759
|
+
// prodman 02 PROTO-171: applications support, protocol 1
|
|
760
|
+
applications: 1
|
|
761
|
+
}
|
|
762
|
+
if (inContainer) server.container = this.info.block(this.appName || null, host, limits)
|
|
763
|
+
if (this._ip === null) this._ip = internalIp()
|
|
764
|
+
this.statusCount++
|
|
765
|
+
return { at: Date.now(), server_name: this.server, internal_ip: this._ip, data: { process: [proc], server } }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
_sendStatus (front) {
|
|
769
|
+
try {
|
|
770
|
+
this.packet('status', this.statusPayload(), null, front)
|
|
771
|
+
} catch (err) {
|
|
772
|
+
debug('status failed: %s', err && err.message)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// -- incoming (NIO-266) ---------------------------------------------------------------------
|
|
777
|
+
|
|
778
|
+
_onText (text) {
|
|
779
|
+
let msg
|
|
780
|
+
try {
|
|
781
|
+
msg = JSON.parse(typeof text === 'string' ? text : text.toString('utf8'))
|
|
782
|
+
} catch (_) {
|
|
783
|
+
debug('ignoring a malformed message')
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
if (!msg || typeof msg !== 'object' || typeof msg.channel !== 'string') return
|
|
787
|
+
const ch = msg.channel
|
|
788
|
+
const p = msg.payload
|
|
789
|
+
if (!p) return
|
|
790
|
+
try {
|
|
791
|
+
if (ch === 'trigger:action' || ch === 'trigger:scoped_action') this._triggerAction(p)
|
|
792
|
+
else if (ch === 'trigger:pm2:action') this._pm2Action(p)
|
|
793
|
+
else if (ch === 'status:resend') this._sendStatus(true)
|
|
794
|
+
else if (ch === 'prodman:settings') {
|
|
795
|
+
if (typeof p.collect_logs === 'boolean') this.collectLogs = p.collect_logs
|
|
796
|
+
this._applyTracing(p.tracing)
|
|
797
|
+
} else debug('ignoring channel %s', ch)
|
|
798
|
+
} catch (err) {
|
|
799
|
+
debug('handling %s failed: %s', ch, err && err.message)
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* 80 NIO-298, prodman 02 PROTO-160/161: the `tracing` key of the handshake / prodman:settings,
|
|
805
|
+
* resolved for this app; a change goes to the instance as a `pmx:tracing` message.
|
|
806
|
+
*/
|
|
807
|
+
_applyTracing (block) {
|
|
808
|
+
const { layerFromBlock } = require('../tracing/control')
|
|
809
|
+
this._tracingBlock = block
|
|
810
|
+
const layer = layerFromBlock(block, this.appName, this.application)
|
|
811
|
+
const prev = this.tracingLayer ? JSON.stringify(this.tracingLayer) : '{}'
|
|
812
|
+
this.tracingLayer = layer
|
|
813
|
+
if (JSON.stringify(layer) === prev) return
|
|
814
|
+
const m = { type: 'pmx:tracing', data: layer }
|
|
815
|
+
for (const fn of this._listeners.slice()) {
|
|
816
|
+
try {
|
|
817
|
+
fn(m)
|
|
818
|
+
} catch (_) {}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
_triggerAction (p) {
|
|
823
|
+
const name = p.action_name
|
|
824
|
+
const id = p.process_id !== undefined ? p.process_id : 0
|
|
825
|
+
if (typeof name !== 'string') return
|
|
826
|
+
if (!this.actions.has(name)) {
|
|
827
|
+
this.packet('trigger:action:failure', { success: false, err: 'unknown action ' + name, id, action_name: name })
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
this.packet('trigger:action:success', { success: true, id, action_name: name })
|
|
831
|
+
const m = { msg: name, action_name: name, opts: p.opts !== undefined ? p.opts : p.options, uuid: p.uuid }
|
|
832
|
+
for (const fn of this._listeners.slice()) {
|
|
833
|
+
try {
|
|
834
|
+
fn(m)
|
|
835
|
+
} catch (_) {}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
_pm2Action (p) {
|
|
840
|
+
const method = p.method_name
|
|
841
|
+
if (typeof method !== 'string') return
|
|
842
|
+
const params = p.parameters && typeof p.parameters === 'object' ? p.parameters : {}
|
|
843
|
+
let err = null
|
|
844
|
+
let data = null
|
|
845
|
+
if (method === 'ping') data = { msg: 'pong' }
|
|
846
|
+
else if (method === 'getLogs') data = this.getLogs(params.lines, params.type)
|
|
847
|
+
else if (method === 'startLogging') {
|
|
848
|
+
this.collectLogs = true
|
|
849
|
+
data = { success: true }
|
|
850
|
+
} else if (method === 'stopLogging') {
|
|
851
|
+
if (!this.cfg.sendLogs) this.collectLogs = false
|
|
852
|
+
data = { success: true }
|
|
853
|
+
} else {
|
|
854
|
+
err = { message: REFUSAL.replace('%s', method).replace('%s', this.appName) }
|
|
855
|
+
}
|
|
856
|
+
this.packet('trigger:pm2:result', {
|
|
857
|
+
ret: { err, data },
|
|
858
|
+
meta: {
|
|
859
|
+
method_name: method,
|
|
860
|
+
app_name: this.appName,
|
|
861
|
+
machine_name: this.server,
|
|
862
|
+
public_key: this.cfg.publicKey,
|
|
863
|
+
request_id: params.request_id
|
|
864
|
+
}
|
|
865
|
+
})
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// -- logs (NIO-267) -------------------------------------------------------------------------
|
|
869
|
+
|
|
870
|
+
getLogs (lines, kind) {
|
|
871
|
+
let n = Number(lines)
|
|
872
|
+
if (!Number.isFinite(n) || lines === undefined || lines === null) n = RING
|
|
873
|
+
n = Math.max(0, Math.min(RING, Math.floor(n)))
|
|
874
|
+
kind = kind === 'out' || kind === 'err' || kind === 'both' ? kind : 'both'
|
|
875
|
+
const out = n && kind !== 'err' ? this.ring.out.slice(-n).join('') : ''
|
|
876
|
+
const err = n && kind !== 'out' ? this.ring.err.slice(-n).join('') : ''
|
|
877
|
+
return [{ pm_id: 0, name: this.appName, out, err }]
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
forwardingLogs () {
|
|
881
|
+
return Boolean(this.cfg.sendLogs) || this.collectLogs
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** The capture sink (the root's own chunks and relayed ones). Never throws. */
|
|
885
|
+
onLogChunk (stream, text) {
|
|
886
|
+
try {
|
|
887
|
+
if (!text || this.cfg.captureLogs === 'off' || matchesFilter(this.cfg.logFilter, text)) return
|
|
888
|
+
stream = stream === 'err' ? 'err' : 'out'
|
|
889
|
+
const ring = this.ring[stream]
|
|
890
|
+
ring.push(text)
|
|
891
|
+
if (ring.length > RING) ring.shift()
|
|
892
|
+
if (this._connected() && this.forwardingLogs()) {
|
|
893
|
+
if (this._batchBytes + text.length > LOG_BATCH_MAX) {
|
|
894
|
+
this._droppedLines += countLines(text)
|
|
895
|
+
} else {
|
|
896
|
+
this._batch[stream].push(text)
|
|
897
|
+
this._batchBytes += text.length
|
|
898
|
+
if (this._batchSince === null) this._batchSince = Date.now()
|
|
899
|
+
if (this._batchBytes >= LOG_BATCH_BYTES) setImmediate(() => this._flushLogs(false))
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
} catch (_) {}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
_flushLogs (force) {
|
|
906
|
+
const now = Date.now()
|
|
907
|
+
const due = this._batchSince !== null && (force || this._batchBytes >= LOG_BATCH_BYTES || now - this._batchSince >= LOG_BATCH_MS)
|
|
908
|
+
const batches = []
|
|
909
|
+
if (due) {
|
|
910
|
+
for (const stream of ['out', 'err']) if (this._batch[stream].length) batches.push([stream, this._batch[stream].join('')])
|
|
911
|
+
this._batch = { out: [], err: [] }
|
|
912
|
+
this._batchBytes = 0
|
|
913
|
+
this._batchSince = null
|
|
914
|
+
}
|
|
915
|
+
if (now - this._windowStart >= 1000) {
|
|
916
|
+
this._windowStart = now
|
|
917
|
+
this._windowLines = 0
|
|
918
|
+
}
|
|
919
|
+
const out = []
|
|
920
|
+
for (const [stream, text] of batches) {
|
|
921
|
+
let lines = text.split(/(?<=\n)/)
|
|
922
|
+
const allowed = Math.max(0, LOG_LINES_PER_SECOND - this._windowLines)
|
|
923
|
+
if (lines.length > allowed) {
|
|
924
|
+
this._droppedLines += lines.length - allowed
|
|
925
|
+
lines = lines.slice(0, allowed)
|
|
926
|
+
}
|
|
927
|
+
this._windowLines += lines.length
|
|
928
|
+
if (lines.length) out.push([stream, lines.join('')])
|
|
929
|
+
}
|
|
930
|
+
let report = 0
|
|
931
|
+
if (this._droppedLines && (this._dropReportedAt === null || now - this._dropReportedAt >= LOG_DROP_REPORT_MS)) {
|
|
932
|
+
report = this._droppedLines
|
|
933
|
+
this._droppedLines = 0
|
|
934
|
+
this._dropReportedAt = now
|
|
935
|
+
}
|
|
936
|
+
for (const [stream, text] of out) this.packet('logs', { at: now, data: text, log_type: stream, process: this.ref() })
|
|
937
|
+
if (report) {
|
|
938
|
+
this.packet('logs', { at: now, data: `[@pm2/io] ${report} log lines not sent (rate limit)\n`, log_type: 'err', process: this.ref() })
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// -- shutdown (NIO-268, 95 CTR-022) ---------------------------------------------------------
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Sends agent:shutdown (once), flushes for at most `flushMs` and closes with 1000. Resolves
|
|
946
|
+
* when done. Synchronous callers (process 'exit') get a best-effort immediate write.
|
|
947
|
+
*/
|
|
948
|
+
shutdown (reason, signal, planned, flushMs) {
|
|
949
|
+
if (this._shutdownPayload || this._closed) return Promise.resolve(false)
|
|
950
|
+
this._shutdownPayload = { at: Date.now(), reason, signal: signal || null, planned: planned === true }
|
|
951
|
+
try {
|
|
952
|
+
this._flushLogs(true)
|
|
953
|
+
} catch (_) {}
|
|
954
|
+
this.packet('agent:shutdown', this._shutdownPayload, null, false)
|
|
955
|
+
return this.flush(flushMs === undefined ? 1000 : flushMs).then(() => {
|
|
956
|
+
this.close(1000, 'Shutting down')
|
|
957
|
+
return true
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** Waits until the queue and the socket's buffer are empty (connected or connecting only). */
|
|
962
|
+
flush (ms) {
|
|
963
|
+
const deadline = Date.now() + ms
|
|
964
|
+
return new Promise((resolve) => {
|
|
965
|
+
const check = () => {
|
|
966
|
+
const empty = this._pending.length === 0 && (!this._ws || this._ws.bufferedAmount() === 0)
|
|
967
|
+
if (empty || Date.now() >= deadline || (!this._ws && !this._connecting)) return resolve()
|
|
968
|
+
const t = setTimeout(check, 20)
|
|
969
|
+
if (t.unref) t.unref()
|
|
970
|
+
}
|
|
971
|
+
check()
|
|
972
|
+
})
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
close (code, reason) {
|
|
976
|
+
this._stopping = true
|
|
977
|
+
this._closed = true
|
|
978
|
+
clearTimeout(this._timers.retry)
|
|
979
|
+
this._disconnectTimers()
|
|
980
|
+
if (this._ws) {
|
|
981
|
+
const ws = this._ws
|
|
982
|
+
try {
|
|
983
|
+
ws.close(code || 1000, reason)
|
|
984
|
+
} catch (_) {}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
_disconnectTimers () {
|
|
989
|
+
clearInterval(this._timers.status)
|
|
990
|
+
clearInterval(this._timers.ping)
|
|
991
|
+
clearInterval(this._timers.logs)
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
destroyAll () {
|
|
995
|
+
this.close(1000, 'Shutting down')
|
|
996
|
+
if (this._ws) {
|
|
997
|
+
const ws = this._ws
|
|
998
|
+
const t = setTimeout(() => ws.terminate(), 100)
|
|
999
|
+
if (t.unref) t.unref()
|
|
1000
|
+
}
|
|
1001
|
+
this._listeners = []
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function internalIp () {
|
|
1006
|
+
const ifaces = os.networkInterfaces()
|
|
1007
|
+
for (const name of Object.keys(ifaces)) {
|
|
1008
|
+
for (const a of ifaces[name] || []) {
|
|
1009
|
+
if ((a.family === 'IPv4' || a.family === 4) && !a.internal) return a.address
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return null
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function safeCwd () {
|
|
1016
|
+
try {
|
|
1017
|
+
return process.cwd()
|
|
1018
|
+
} catch (_) {
|
|
1019
|
+
return null
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
module.exports = { ProdmanTransport, resolveConfig, resolveUrl, serverName, matchesFilter, Footprint, IO_VERSION, REFUSAL, NOT_QUEUED }
|