@roflsec/fail2scan 0.0.1 → 0.0.3
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/bin/daemon.js +282 -91
- package/package.json +1 -1
package/bin/daemon.js
CHANGED
|
@@ -2,21 +2,26 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Fail2Scan
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Fail2Scan daemon - integrated version
|
|
6
|
+
* - watches fail2ban log for "Ban" events (handles rotation)
|
|
7
|
+
* - extracts IPv4/IPv6
|
|
8
|
+
* - queue with persistence and rescan TTL
|
|
9
|
+
* - fallback output dir if /var/log/fail2scan not writable
|
|
10
|
+
* - single-ip CLI --scan-ip
|
|
8
11
|
*
|
|
9
|
-
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* fail2scan-daemon --log /var/log/fail2ban.log --out /var/log/fail2scan --concurrency 2 --nmap-args "-sS -Pn -p- -T4 -sV" --quiet
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
const fs = require('fs');
|
|
13
17
|
const path = require('path');
|
|
14
|
-
const
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const { execFile, spawn } = require('child_process');
|
|
15
20
|
const { promisify } = require('util');
|
|
16
|
-
|
|
17
21
|
const execFileP = promisify(execFile);
|
|
18
|
-
const argv = process.argv.slice(2);
|
|
19
22
|
|
|
23
|
+
// -------------------- CLI / CONFIG --------------------
|
|
24
|
+
const argv = process.argv.slice(2);
|
|
20
25
|
function getArg(key, def) {
|
|
21
26
|
for (let i = 0; i < argv.length; i++) {
|
|
22
27
|
const a = argv[i];
|
|
@@ -25,9 +30,8 @@ function getArg(key, def) {
|
|
|
25
30
|
}
|
|
26
31
|
return def;
|
|
27
32
|
}
|
|
28
|
-
|
|
29
33
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
30
|
-
console.log('
|
|
34
|
+
console.log('Fail2Scan daemon\n--log PATH (default /var/log/fail2ban.log)\n--out PATH (default /var/log/fail2scan)\n--concurrency N (default 1)\n--nmap-args "args" (default "-sS -Pn -p- -T4 -sV")\n--scan-ip IP (do one scan and exit)\n--quiet');
|
|
31
35
|
process.exit(0);
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -35,121 +39,308 @@ const LOG_PATH = getArg('--log', '/var/log/fail2ban.log');
|
|
|
35
39
|
const OUT_ROOT = getArg('--out', '/var/log/fail2scan');
|
|
36
40
|
const CONCURRENCY = Math.max(1, parseInt(getArg('--concurrency', '1'), 10) || 1);
|
|
37
41
|
const NMAP_ARGS_STR = getArg('--nmap-args', '-sS -Pn -p- -T4 -sV');
|
|
42
|
+
const SINGLE_IP = getArg('--scan-ip', null);
|
|
38
43
|
const QUIET = argv.includes('--quiet');
|
|
39
44
|
|
|
40
|
-
function log(...args) { if (!QUIET) console.log(new Date().toISOString(), ...args); }
|
|
45
|
+
function log(...args) { if (!QUIET) console.log(new Date().toISOString(), ...args); appendLog(args.join(' ')); }
|
|
46
|
+
|
|
47
|
+
// -------------------- small logger to file --------------------
|
|
48
|
+
const STATE_FILE = path.join(os.homedir(), '.fail2scan_state.json');
|
|
49
|
+
const LOG_FILE = path.join(os.homedir(), '.fail2scan.log');
|
|
50
|
+
function appendLog(msg) {
|
|
51
|
+
try { fs.appendFileSync(LOG_FILE, new Date().toISOString() + ' ' + msg + '\n'); } catch (e) {}
|
|
52
|
+
}
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
// -------------------- utilities --------------------
|
|
55
|
+
function sanitizeFilename(s) { return String(s).replace(/[:\/\\<>?"|* ]+/g, '_'); }
|
|
43
56
|
async function which(bin) { try { await execFileP('which', [bin]); return true; } catch { return false; } }
|
|
44
|
-
async function
|
|
57
|
+
async function runCmdCapture(cmd, args, opts = {}) {
|
|
45
58
|
try {
|
|
46
59
|
const { stdout, stderr } = await execFileP(cmd, args, { maxBuffer: 1024 * 1024 * 32, ...opts });
|
|
47
|
-
return { ok: true, stdout, stderr };
|
|
60
|
+
return { ok: true, stdout: stdout || '', stderr: stderr || '' };
|
|
48
61
|
} catch (e) {
|
|
49
|
-
return { ok: false, stdout: e.stdout || '', stderr: e.stderr || e.message };
|
|
62
|
+
return { ok: false, stdout: (e.stdout || '') + '', stderr: (e.stderr || e.message) + '' };
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.error(`Missing required binary: ${t}. Please install it (e.g. apt install ${t}).`);
|
|
59
|
-
process.exit(2);
|
|
65
|
+
function safeMkdirSyncWithFallback(p) {
|
|
66
|
+
try { fs.mkdirSync(p, { recursive: true, mode: 0o750 }); return p; }
|
|
67
|
+
catch (e) {
|
|
68
|
+
const fallback = path.join('/tmp', 'fail2scan');
|
|
69
|
+
try { fs.mkdirSync(fallback, { recursive: true, mode: 0o750 }); return fallback; }
|
|
70
|
+
catch (ee) { throw e; }
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
|
-
checkPrereqs();
|
|
63
73
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async start() {
|
|
76
|
-
try {
|
|
77
|
-
const st = fs.statSync(this.filePath);
|
|
78
|
-
this.inode = st.ino;
|
|
79
|
-
this.position = st.size;
|
|
80
|
-
} catch {}
|
|
81
|
-
this.watch = fs.watch(this.filePath, { persistent: true }, async () => {
|
|
82
|
-
try {
|
|
83
|
-
const st = fs.statSync(this.filePath);
|
|
84
|
-
if (st.size < this.position) this.position = 0;
|
|
85
|
-
if (st.size === this.position) return;
|
|
86
|
-
const stream = fs.createReadStream(this.filePath, { start: this.position, end: st.size - 1, encoding: 'utf8' });
|
|
87
|
-
for await (const chunk of stream) {
|
|
88
|
-
this.buffer += chunk;
|
|
89
|
-
let idx;
|
|
90
|
-
while ((idx = this.buffer.indexOf('\n')) >= 0) {
|
|
91
|
-
const line = this.buffer.slice(0, idx);
|
|
92
|
-
this.buffer = this.buffer.slice(idx + 1);
|
|
93
|
-
if (line.trim()) this.onLine(line);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
this.position = st.size;
|
|
97
|
-
} catch {}
|
|
98
|
-
});
|
|
74
|
+
// -------------------- prerequisites --------------------
|
|
75
|
+
(async function checkTools() {
|
|
76
|
+
const tools = ['nmap', 'dig', 'whois', 'which'];
|
|
77
|
+
for (const t of tools) {
|
|
78
|
+
if (t === 'which') continue;
|
|
79
|
+
if (!(await which(t))) {
|
|
80
|
+
console.error(`Missing required binary: ${t}. Please install it (eg: apt install ${t}).`);
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
99
83
|
}
|
|
84
|
+
})().catch(e => { console.error('Prereq check failed', e); process.exit(2); });
|
|
100
85
|
|
|
101
|
-
|
|
86
|
+
// -------------------- persistence state --------------------
|
|
87
|
+
function loadState() {
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
90
|
+
const raw = fs.readFileSync(STATE_FILE, 'utf8');
|
|
91
|
+
const j = JSON.parse(raw);
|
|
92
|
+
return {
|
|
93
|
+
seen: new Set(Array.isArray(j.seen) ? j.seen : []),
|
|
94
|
+
retryAfter: typeof j.retryAfter === 'object' ? j.retryAfter : {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {}
|
|
98
|
+
return { seen: new Set(), retryAfter: {} };
|
|
99
|
+
}
|
|
100
|
+
function saveState(state) {
|
|
101
|
+
try {
|
|
102
|
+
const obj = { seen: Array.from(state.seen || []), retryAfter: state.retryAfter || {} };
|
|
103
|
+
const dir = path.dirname(STATE_FILE);
|
|
104
|
+
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch (e) {}
|
|
105
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(obj, null, 2));
|
|
106
|
+
} catch (e) {}
|
|
102
107
|
}
|
|
108
|
+
const STATE = loadState();
|
|
109
|
+
const RESCAN_TTL_SEC = 60 * 60; // default 1 hour
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
try { log('Scanning', ip); await performScan(ip); log('Done', ip); }
|
|
114
|
-
catch (e) { console.error('Error scanning', ip, e); }
|
|
115
|
-
}
|
|
111
|
+
// -------------------- IP extraction --------------------
|
|
112
|
+
function extractIpFromLine(line) {
|
|
113
|
+
// prefer strict IPv4
|
|
114
|
+
const ipv4 = line.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/);
|
|
115
|
+
if (ipv4 && ipv4[0]) return ipv4[0];
|
|
116
|
+
// simple IPv6 match
|
|
117
|
+
const ipv6 = line.match(/\b([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b/);
|
|
118
|
+
if (ipv6 && ipv6[0]) return ipv6[0];
|
|
119
|
+
return null;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
+
// -------------------- Scan implementation --------------------
|
|
123
|
+
function spawnNmap(ip, outDir, nmapArgs) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const outNmap = path.join(outDir, 'nmap.txt');
|
|
126
|
+
const args = nmapArgs.concat([ip]);
|
|
127
|
+
const proc = spawn('nmap', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
128
|
+
const outStream = fs.createWriteStream(outNmap, { flags: 'w' });
|
|
129
|
+
proc.stdout.pipe(outStream);
|
|
130
|
+
let stderr = '';
|
|
131
|
+
proc.stderr.on('data', c => { stderr += c.toString(); });
|
|
132
|
+
proc.on('close', code => {
|
|
133
|
+
if (stderr) fs.appendFileSync(outNmap, '\n\nSTDERR:\n' + stderr);
|
|
134
|
+
resolve({ code, ok: code === 0 });
|
|
135
|
+
});
|
|
136
|
+
proc.on('error', err => reject(err));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
122
139
|
|
|
123
140
|
async function performScan(ip) {
|
|
124
141
|
const now = new Date();
|
|
125
142
|
const dateDir = now.toISOString().slice(0, 10);
|
|
126
143
|
const safeIp = sanitizeFilename(ip);
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
let outDir = path.join(OUT_ROOT, dateDir, `${safeIp}_${now.toISOString().replace(/[:.]/g, '-')}`);
|
|
145
|
+
try {
|
|
146
|
+
outDir = path.join(safeMkdirSyncWithFallback(path.dirname(outDir)), path.basename(outDir));
|
|
147
|
+
fs.mkdirSync(outDir, { recursive: true, mode: 0o750 });
|
|
148
|
+
} catch (e) {
|
|
149
|
+
log('Cannot create out dir for', ip, '-', e.message);
|
|
150
|
+
// schedule quick retry and exit
|
|
151
|
+
STATE.retryAfter[ip] = Math.floor(Date.now() / 1000) + 60;
|
|
152
|
+
saveState(STATE);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
129
155
|
|
|
130
156
|
const summary = { ip, ts: now.toISOString(), cmds: {} };
|
|
131
157
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
158
|
+
// choose nmap args and adapt if not root
|
|
159
|
+
const requested = NMAP_ARGS_STR.trim().split(/\s+/).filter(Boolean);
|
|
160
|
+
const isRoot = (typeof process.getuid === 'function' && process.getuid() === 0);
|
|
161
|
+
const nmapArgs = requested.map(a => (!isRoot && a === '-sS') ? '-sT' : a);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
log('Running nmap on', ip, 'args:', nmapArgs.join(' '));
|
|
165
|
+
await spawnNmap(ip, outDir, nmapArgs);
|
|
166
|
+
summary.cmds.nmap = { ok: true, args: nmapArgs.join(' '), path: 'nmap.txt' };
|
|
167
|
+
} catch (e) {
|
|
168
|
+
log('nmap failed for', ip, e.message);
|
|
169
|
+
summary.cmds.nmap = { ok: false, err: e.message };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// dig
|
|
173
|
+
try {
|
|
174
|
+
const dig = await runCmdCapture('dig', ['-x', ip, '+short']);
|
|
175
|
+
fs.writeFileSync(path.join(outDir, 'dig.txt'), (dig.stdout || '') + (dig.stderr ? '\n\nSTDERR:\n' + dig.stderr : ''));
|
|
176
|
+
summary.cmds.dig = { ok: dig.ok, path: 'dig.txt' };
|
|
177
|
+
} catch (e) { summary.cmds.dig = { ok: false, err: e.message }; }
|
|
135
178
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
179
|
+
// whois
|
|
180
|
+
try {
|
|
181
|
+
const who = await runCmdCapture('whois', [ip]);
|
|
182
|
+
fs.writeFileSync(path.join(outDir, 'whois.txt'), (who.stdout || '') + (who.stderr ? '\n\nSTDERR:\n' + who.stderr : ''));
|
|
183
|
+
summary.cmds.whois = { ok: who.ok, path: 'whois.txt' };
|
|
184
|
+
} catch (e) { summary.cmds.whois = { ok: false, err: e.message }; }
|
|
139
185
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
186
|
+
// minimal parse for open ports
|
|
187
|
+
try {
|
|
188
|
+
const nmapTxt = fs.readFileSync(path.join(outDir, 'nmap.txt'), 'utf8');
|
|
189
|
+
const open = nmapTxt.split(/\r?\n/).filter(l => /^\d+\/tcp\s+open/.test(l)).map(l => l.trim());
|
|
190
|
+
summary.open_ports = open;
|
|
191
|
+
} catch (e) { summary.open_ports = []; }
|
|
143
192
|
|
|
144
193
|
fs.writeFileSync(path.join(outDir, 'summary.json'), JSON.stringify(summary, null, 2));
|
|
194
|
+
try { fs.chmodSync(outDir, 0o750); } catch (e) {}
|
|
195
|
+
log('Scan written for', ip, '->', outDir);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// -------------------- Queue with concurrency and TTL --------------------
|
|
199
|
+
class ScanQueue {
|
|
200
|
+
constructor(concurrency = 1) {
|
|
201
|
+
this.concurrency = concurrency;
|
|
202
|
+
this.running = 0;
|
|
203
|
+
this.q = [];
|
|
204
|
+
this.set = STATE.seen; // persistent set
|
|
205
|
+
}
|
|
206
|
+
push(ip) {
|
|
207
|
+
const now = Math.floor(Date.now() / 1000);
|
|
208
|
+
const next = STATE.retryAfter[ip] || 0;
|
|
209
|
+
if (this.set.has(ip) && next > now) {
|
|
210
|
+
log('IP already queued or running (TTL not expired):', ip);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (this.set.has(ip) && next <= now) {
|
|
214
|
+
log('Re-queueing after TTL:', ip);
|
|
215
|
+
this.set.delete(ip);
|
|
216
|
+
}
|
|
217
|
+
this.set.add(ip);
|
|
218
|
+
STATE.seen = this.set;
|
|
219
|
+
saveState(STATE);
|
|
220
|
+
this.q.push(ip);
|
|
221
|
+
this._next();
|
|
222
|
+
}
|
|
223
|
+
_next() {
|
|
224
|
+
if (this.running >= this.concurrency) return;
|
|
225
|
+
const ip = this.q.shift();
|
|
226
|
+
if (!ip) return;
|
|
227
|
+
this.running++;
|
|
228
|
+
(async () => {
|
|
229
|
+
try {
|
|
230
|
+
log('Scanning', ip);
|
|
231
|
+
await performScan(ip);
|
|
232
|
+
log('Done', ip);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
log('Error scanning', ip, e && e.message ? e.message : e);
|
|
235
|
+
} finally {
|
|
236
|
+
STATE.retryAfter[ip] = Math.floor(Date.now() / 1000) + RESCAN_TTL_SEC;
|
|
237
|
+
saveState(STATE);
|
|
238
|
+
this.set.delete(ip); // allow future re-queue after TTL (state still records retryAfter)
|
|
239
|
+
this.running--;
|
|
240
|
+
setImmediate(() => this._next());
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// -------------------- File tail (watch + read new lines, handle rotation) --------------------
|
|
247
|
+
class FileTail {
|
|
248
|
+
constructor(filePath, onLine) {
|
|
249
|
+
this.filePath = filePath;
|
|
250
|
+
this.onLine = onLine;
|
|
251
|
+
this.pos = 0;
|
|
252
|
+
this.inode = null;
|
|
253
|
+
this.buf = '';
|
|
254
|
+
this.watch = null;
|
|
255
|
+
this.start();
|
|
256
|
+
}
|
|
257
|
+
start() {
|
|
258
|
+
try {
|
|
259
|
+
const st = fs.statSync(this.filePath);
|
|
260
|
+
this.inode = st.ino;
|
|
261
|
+
this.pos = st.size;
|
|
262
|
+
} catch (e) {
|
|
263
|
+
this.inode = null;
|
|
264
|
+
this.pos = 0;
|
|
265
|
+
}
|
|
266
|
+
this._watch();
|
|
267
|
+
// try initial read (if file exists and appended)
|
|
268
|
+
this._readNew().catch(()=>{});
|
|
269
|
+
}
|
|
270
|
+
_watch() {
|
|
271
|
+
try {
|
|
272
|
+
this.watch = fs.watch(this.filePath, { persistent: true }, async () => {
|
|
273
|
+
try {
|
|
274
|
+
let st;
|
|
275
|
+
try { st = fs.statSync(this.filePath); } catch { st = null; }
|
|
276
|
+
if (!st) { this.inode = null; this.pos = 0; return; }
|
|
277
|
+
if (this.inode !== null && st.ino !== this.inode) { // rotated
|
|
278
|
+
this.inode = st.ino;
|
|
279
|
+
this.pos = 0;
|
|
280
|
+
} else if (this.inode === null) {
|
|
281
|
+
this.inode = st.ino;
|
|
282
|
+
this.pos = 0;
|
|
283
|
+
}
|
|
284
|
+
await this._readNew();
|
|
285
|
+
} catch (e) { /* ignore transient */ }
|
|
286
|
+
});
|
|
287
|
+
} catch (e) { log('fs.watch failed:', e.message); }
|
|
288
|
+
}
|
|
289
|
+
async _readNew() {
|
|
290
|
+
try {
|
|
291
|
+
const st = fs.statSync(this.filePath);
|
|
292
|
+
if (st.size < this.pos) this.pos = 0;
|
|
293
|
+
if (st.size === this.pos) return;
|
|
294
|
+
const stream = fs.createReadStream(this.filePath, { start: this.pos, end: st.size - 1, encoding: 'utf8' });
|
|
295
|
+
for await (const chunk of stream) {
|
|
296
|
+
this.buf += chunk;
|
|
297
|
+
let idx;
|
|
298
|
+
while ((idx = this.buf.indexOf('\n')) >= 0) {
|
|
299
|
+
const line = this.buf.slice(0, idx);
|
|
300
|
+
this.buf = this.buf.slice(idx + 1);
|
|
301
|
+
if (line.trim()) this.onLine(line);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
this.pos = st.size;
|
|
305
|
+
} catch (e) { /* ignore */ }
|
|
306
|
+
}
|
|
307
|
+
close() { try { if (this.watch) this.watch.close(); } catch (e) {} }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// -------------------- Main startup --------------------
|
|
311
|
+
if (SINGLE_IP) {
|
|
312
|
+
(async () => {
|
|
313
|
+
const q = new ScanQueue(CONCURRENCY);
|
|
314
|
+
q.push(SINGLE_IP);
|
|
315
|
+
})();
|
|
316
|
+
return;
|
|
145
317
|
}
|
|
146
318
|
|
|
147
319
|
const q = new ScanQueue(CONCURRENCY);
|
|
148
|
-
log('Fail2Scan
|
|
320
|
+
log('Fail2Scan started. Watching', LOG_PATH, ' -> output', OUT_ROOT, 'concurrency', CONCURRENCY);
|
|
321
|
+
|
|
322
|
+
// On each new line, extract IP and push to queue if Ban detected
|
|
323
|
+
const BAN_RE = /\bBan\b/i;
|
|
149
324
|
const tail = new FileTail(LOG_PATH, (line) => {
|
|
150
|
-
|
|
151
|
-
|
|
325
|
+
try {
|
|
326
|
+
if (!BAN_RE.test(line)) return;
|
|
327
|
+
const ip = extractIpFromLine(line);
|
|
328
|
+
if (!ip) return;
|
|
329
|
+
q.push(ip);
|
|
330
|
+
} catch (e) { log('onLine handler error', e && e.message ? e.message : e); }
|
|
152
331
|
});
|
|
153
332
|
|
|
154
|
-
|
|
155
|
-
|
|
333
|
+
// graceful shutdown
|
|
334
|
+
function shutdown() {
|
|
335
|
+
log('Shutting down Fail2Scan...');
|
|
336
|
+
tail.close();
|
|
337
|
+
// wait briefly for running tasks
|
|
338
|
+
const start = Date.now();
|
|
339
|
+
const wait = () => {
|
|
340
|
+
if (q.running === 0 || Date.now() - start > 10000) process.exit(0);
|
|
341
|
+
setTimeout(wait, 500);
|
|
342
|
+
};
|
|
343
|
+
wait();
|
|
344
|
+
}
|
|
345
|
+
process.on('SIGINT', shutdown);
|
|
346
|
+
process.on('SIGTERM', shutdown);
|