@abtnode/util 1.16.53 → 1.16.54-beta-20251016-050817-2fc632b8

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.
@@ -0,0 +1,230 @@
1
+ /* eslint-disable no-console */
2
+ const { execSync } = require('child_process');
3
+ const { ORPHAN_MIN_UPTIME } = require('@abtnode/constant');
4
+ const pm2 = require('./async-pm2');
5
+
6
+ /**
7
+ * Check and kill orphan processes that should have been terminated during PM2 reload
8
+ *
9
+ * Safety measures:
10
+ * 1. Only runs in PM2 environment (unless force=true)
11
+ * 2. Only kills processes older than ORPHAN_MIN_UPTIME seconds (default 60s)
12
+ * 3. Only kills processes not managed by PM2
13
+ * 4. Only kills processes matching the target script name
14
+ *
15
+ * @param {Object} options
16
+ * @param {string} options.scriptName - The script name to check (e.g., 'daemon.js', 'service.js')
17
+ * @param {Object} options.logger - Logger instance
18
+ * @param {boolean} options.force - Force run even outside PM2 environment (for manual cleanup)
19
+ */
20
+ async function checkAndKillOrphanProcesses(options = {}) {
21
+ const { scriptName, logger = console, force = false } = options;
22
+
23
+ // Safety check: Only run in PM2 environment (unless force=true)
24
+ if (!force && (!process.env.PM2_HOME || !process.env.pm_id)) {
25
+ return;
26
+ }
27
+
28
+ if (!scriptName) {
29
+ logger.error('[orphan-check] scriptName is required');
30
+ return;
31
+ }
32
+
33
+ try {
34
+ // Step 1: Get ALL PM2 managed PIDs for this app (including all cluster workers)
35
+ let pm2ManagedPids = [];
36
+
37
+ // In force mode (manual cleanup), include current process as protected
38
+ if (force) {
39
+ pm2ManagedPids = [process.pid];
40
+ } else {
41
+ // In PM2 environment, protect current process
42
+ pm2ManagedPids = [process.pid];
43
+ }
44
+
45
+ try {
46
+ // Try to get all PIDs managed by PM2
47
+ await pm2.connectAsync();
48
+ const pm2Processes = await pm2.listAsync();
49
+ await pm2.disconnect();
50
+
51
+ // Filter by app name only if we're in PM2 environment
52
+ if (force) {
53
+ // In force mode, protect all PM2-managed daemon/service processes
54
+ const targetProcesses = pm2Processes.filter(
55
+ (p) => p.pm2_env.status === 'online' && (p.name === 'abt-node-daemon' || p.name === 'abt-node-service')
56
+ );
57
+ pm2ManagedPids = targetProcesses.map((p) => p.pid).filter(Boolean);
58
+ } else {
59
+ // In PM2 environment, protect same app name processes
60
+ const pm2Name = process.env.name || '';
61
+ const sameAppProcesses = pm2Processes.filter((p) => p.name === pm2Name && p.pm2_env.status === 'online');
62
+ pm2ManagedPids = sameAppProcesses.map((p) => p.pid).filter(Boolean);
63
+ }
64
+
65
+ logger.info(`[orphan-check] found ${pm2ManagedPids.length} PM2 managed instances: ${pm2ManagedPids.join(', ')}`);
66
+ } catch (e) {
67
+ // Fallback: protect all processes running this script that are < 5 minutes old
68
+ // This is safer than only protecting current PID in case of PM2 cluster mode
69
+ logger.warn(
70
+ `[orphan-check] Failed to get PM2 process list (${e.message}), will use conservative protection strategy`
71
+ );
72
+ // Ensure disconnect even on error
73
+ try {
74
+ await pm2.disconnect();
75
+ } catch (disconnectError) {
76
+ // Ignore disconnect errors
77
+ }
78
+ }
79
+
80
+ logger.info(`[orphan-check] checking for orphan processes: ${scriptName}`);
81
+ logger.info(`[orphan-check] protected PIDs (PM2 managed): ${pm2ManagedPids.join(', ')}`);
82
+
83
+ // Step 2: Find all processes running the same script
84
+ let psOutput;
85
+ try {
86
+ // Use ps to find all processes with matching script name
87
+ // Format: PID ELAPSED CMD
88
+ psOutput = execSync(`ps -eo pid,etime,command | grep "${scriptName}" | grep -v grep | grep -v "check-orphan"`, {
89
+ encoding: 'utf8',
90
+ timeout: 5000,
91
+ }).trim();
92
+ } catch (e) {
93
+ // If grep finds nothing, it returns exit code 1
94
+ if (e.status === 1) {
95
+ logger.info(`[orphan-check] no processes found for ${scriptName}`);
96
+ return;
97
+ }
98
+ throw e;
99
+ }
100
+
101
+ if (!psOutput) {
102
+ logger.info(`[orphan-check] no processes found for ${scriptName}`);
103
+ return;
104
+ }
105
+
106
+ const lines = psOutput.split('\n').filter(Boolean);
107
+ logger.info(`[orphan-check] found ${lines.length} process(es) for ${scriptName}`);
108
+
109
+ const orphans = [];
110
+ const conservativeMode = pm2ManagedPids.length === 1; // Only current PID, PM2 list failed
111
+
112
+ for (const line of lines) {
113
+ const match = line.trim().match(/^(\d+)\s+(\S+)\s+(.+)$/);
114
+ if (match) {
115
+ const pid = parseInt(match[1], 10);
116
+ const etime = match[2]; // Format: [[dd-]hh:]mm:ss
117
+ const cmd = match[3];
118
+
119
+ // Skip PM2 managed processes (including all cluster workers)
120
+ if (pm2ManagedPids.includes(pid)) {
121
+ logger.info(`[orphan-check] pid ${pid} is PM2 managed, skipping`);
122
+ } else {
123
+ // Parse elapsed time to seconds
124
+ const uptimeSeconds = parseElapsedTime(etime);
125
+
126
+ logger.info(
127
+ `[orphan-check] found process: pid=${pid}, uptime=${uptimeSeconds}s, cmd=${cmd.substring(0, 80)}...`
128
+ );
129
+
130
+ // Conservative mode: protect processes < 5 minutes (in case of PM2 cluster)
131
+ if (conservativeMode && uptimeSeconds < 300) {
132
+ logger.info(`[orphan-check] CONSERVATIVE MODE: pid ${pid} uptime < 5min, skipping (PM2 list unavailable)`);
133
+ } else if (uptimeSeconds < ORPHAN_MIN_UPTIME) {
134
+ // Safety check: Only consider processes that have been running for > ORPHAN_MIN_UPTIME seconds
135
+ // This prevents killing processes that are in the middle of graceful shutdown
136
+ logger.info(
137
+ `[orphan-check] pid ${pid} uptime < ${ORPHAN_MIN_UPTIME}s, skipping (may be in graceful shutdown)`
138
+ );
139
+ } else {
140
+ // Verify it's the correct script by checking command line more strictly
141
+ // Use regex to match the exact script name with word boundaries
142
+ const scriptPattern = new RegExp(`[/\\s]${scriptName.replace(/\./g, '\\.')}(\\s|$)`);
143
+ if (!scriptPattern.test(cmd)) {
144
+ logger.info(`[orphan-check] pid ${pid} command does not match ${scriptName}, skipping`);
145
+ } else {
146
+ // This is an orphan process
147
+ orphans.push({ pid, uptime: uptimeSeconds, cmd });
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // Step 3: Kill orphan processes
155
+ if (orphans.length === 0) {
156
+ logger.info(`[orphan-check] no orphan processes found for ${scriptName}`);
157
+ return;
158
+ }
159
+
160
+ logger.warn(`[orphan-check] found ${orphans.length} orphan process(es) for ${scriptName}`);
161
+
162
+ for (const orphan of orphans) {
163
+ try {
164
+ logger.warn(`[orphan-check] killing orphan process: pid=${orphan.pid}, uptime=${orphan.uptime}s`);
165
+
166
+ // Try graceful kill first (SIGTERM)
167
+ process.kill(orphan.pid, 'SIGTERM');
168
+
169
+ // Wait 2 seconds, then force kill if still alive
170
+ setTimeout(() => {
171
+ try {
172
+ // Check if process still exists
173
+ process.kill(orphan.pid, 0); // Signal 0 just checks existence
174
+
175
+ // Still alive, force kill
176
+ logger.warn(`[orphan-check] Process ${orphan.pid} did not respond to SIGTERM, forcing SIGKILL`);
177
+ process.kill(orphan.pid, 'SIGKILL');
178
+
179
+ logger.info(`[orphan-check] Successfully killed orphan process ${orphan.pid}`);
180
+ } catch (e) {
181
+ // Process already dead, good
182
+ logger.info(`[orphan-check] Process ${orphan.pid} already terminated`);
183
+ }
184
+ }, 2000); // No unref, ensure kill completes before process exits
185
+ } catch (e) {
186
+ if (e.code === 'ESRCH') {
187
+ logger.info(`[orphan-check] Process ${orphan.pid} already terminated`);
188
+ } else {
189
+ logger.error(`[orphan-check] Failed to kill process ${orphan.pid}:`, e.message);
190
+ }
191
+ }
192
+ }
193
+ } catch (error) {
194
+ logger.error('[orphan-check] Error checking orphan processes:', error.message);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Parse elapsed time from ps output to seconds
200
+ * Format: [[dd-]hh:]mm:ss
201
+ * Examples:
202
+ * "05:23" -> 323 seconds (5 minutes 23 seconds)
203
+ * "01:05:23" -> 3923 seconds (1 hour 5 minutes 23 seconds)
204
+ * "2-03:15:42" -> 185742 seconds (2 days 3 hours 15 minutes 42 seconds)
205
+ */
206
+ function parseElapsedTime(etime) {
207
+ const parts = etime.split(/[-:]/);
208
+ let seconds = 0;
209
+
210
+ if (parts.length === 2) {
211
+ // mm:ss
212
+ seconds = parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
213
+ } else if (parts.length === 3) {
214
+ // hh:mm:ss
215
+ seconds = parseInt(parts[0], 10) * 3600 + parseInt(parts[1], 10) * 60 + parseInt(parts[2], 10);
216
+ } else if (parts.length === 4) {
217
+ // dd-hh:mm:ss
218
+ seconds =
219
+ parseInt(parts[0], 10) * 86400 +
220
+ parseInt(parts[1], 10) * 3600 +
221
+ parseInt(parts[2], 10) * 60 +
222
+ parseInt(parts[3], 10);
223
+ }
224
+
225
+ return seconds;
226
+ }
227
+
228
+ module.exports = {
229
+ checkAndKillOrphanProcesses,
230
+ };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.16.53",
6
+ "version": "1.16.54-beta-20251016-050817-2fc632b8",
7
7
  "description": "ArcBlock's JavaScript utility",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -18,14 +18,14 @@
18
18
  "author": "polunzh <polunzh@gmail.com> (http://github.com/polunzh)",
19
19
  "license": "Apache-2.0",
20
20
  "dependencies": {
21
- "@abtnode/constant": "1.16.53",
22
- "@abtnode/db-cache": "1.16.53",
21
+ "@abtnode/constant": "1.16.54-beta-20251016-050817-2fc632b8",
22
+ "@abtnode/db-cache": "1.16.54-beta-20251016-050817-2fc632b8",
23
23
  "@arcblock/did": "1.25.6",
24
24
  "@arcblock/event-hub": "1.25.6",
25
25
  "@arcblock/pm2": "^6.0.12",
26
- "@blocklet/constant": "1.16.53",
26
+ "@blocklet/constant": "1.16.54-beta-20251016-050817-2fc632b8",
27
27
  "@blocklet/error": "^0.2.5",
28
- "@blocklet/meta": "1.16.53",
28
+ "@blocklet/meta": "1.16.54-beta-20251016-050817-2fc632b8",
29
29
  "@blocklet/xss": "^0.2.12",
30
30
  "@ocap/client": "1.25.6",
31
31
  "@ocap/mcrypto": "1.25.6",
@@ -91,5 +91,5 @@
91
91
  "fs-extra": "^11.2.0",
92
92
  "jest": "^29.7.0"
93
93
  },
94
- "gitHead": "7c37a9bd063192fb0a14921b9d0317de953016ff"
94
+ "gitHead": "6b624f4b7eba2ecee164a57112adda1a93aff513"
95
95
  }