blob: eb1ac9e4430aea3caa0ee878dbbbf9e6c570741b [file] [log] [blame]
Sui Chenb65280f2020-06-30 18:14:03 -07001const { spawn } = require('child_process');
2const targz = require('targz');
3
4const DBUS_MONITOR_LEGACY =
5 'dbus-monitor --system | grep "sendMessage\\|ReceivedMessage" -A7 \n';
6const DBUS_MONITOR_NEW =
7 'dbus-monitor --system | grep "member=execute\\|method return" -A7 \n';
8
9// Capture state for all scripts
10var g_capture_state = 'not started';
11var g_capture_mode = 'live';
12
13// For capturing IPMI requests live
14var g_dbus_monitor_cmd = '';
15
16// For tracking transfer
17var g_hexdump = '';
18var g_hexdump_received_size = 0;
19var g_hexdump_total_size = 0;
20
21function currTimestamp() {
22 var tmp = new Date();
23 return (tmp.getTime() + tmp.getTimezoneOffset() * 60000) / 1000;
24}
25
26var g_child;
27var g_rz;
28
29var g_capture_live = true;
30var g_dbus_capture_tarfile_size = 0;
31
32function ParseHexDump(hd) {
33 let ret = [];
34 let lines = hd.split('\n');
35 let tot_size = 0;
36 for (let i = 0; i < lines.length; i++) {
37 const line = lines[i].trimEnd();
38 const sp = line.split(' ');
39 if (line.length < 1) continue;
40 if (sp.length < 1) continue;
41
42 for (let j = 1; j < sp.length; j++) {
43 let b0 = sp[j].slice(2);
44 let b1 = sp[j].slice(0, 2);
45 b0 = parseInt(b0, 16);
46 b1 = parseInt(b1, 16);
47 ret.push(b0);
48 ret.push(b1);
49 }
50
51 console.log('[' + line + ']')
52
53 {
54 tot_size = parseInt(sp[0], 16);
55 console.log('File size: ' + tot_size + ' ' + sp[0]);
56 }
57 }
58 ret = ret.slice(0, tot_size);
59 return new Buffer(ret);
60}
61
62function SaveHexdumpToFile(hd, file_name) {
63 const buf = ParseHexDump(hd);
64 fs.writeFileSync(file_name, buf)
65}
66
67// Delimiters: ">>>>>>" and "<<<<<<"
68function ExtractMyDelimitedStuff(x, parse_as = undefined) {
69 let i0 = x.lastIndexOf('>>>>>>'), i1 = x.lastIndexOf('<<<<<<');
70 if (i0 != -1 && i1 != -1) {
71 let ret = x.substr(i0 + 6, i1 - i0 - 6);
72 if (parse_as == undefined)
73 return ret;
74 else if (parse_as == 'int')
75 return parseInt(ret);
76 } else
77 return null;
78}
79
80function streamWrite(stream, chunk, encoding = 'utf8') {
81 return new Promise((resolve, reject) => {
82 const errListener = (err) => {
83 stream.removeListener('error', errListener);
84 reject(err);
85 };
86 stream.addListener('error', errListener);
87 const callback = () => {
88 stream.removeListener('error', errListener);
89 resolve(undefined);
90 };
91 stream.write(chunk, encoding, callback);
92 });
93}
94
95function ExtractTarFile() {
96 const tar_file = 'DBUS_MONITOR.tar.gz';
97 const target = '.';
98 targz.decompress({src: tar_file, dest: target}, function(err) {
99 if (err) {
100 console.log('Error decompressing .tar.gz file:' + err);
101 }
102 // Attempt to load even if error occurs
103 // example error: "Error decompressing .tar.gz file:Error: incorrect data check"
104 console.log('Done! will load file contents');
105 if (g_capture_mode == 'staged') {
106 fs.readFile('./DBUS_MONITOR', {encoding: 'utf-8'}, (err, data) => {
107 if (err) {
108 console.log('Error in readFile: ' + err);
109 } else {
110 ParseIPMIDump(data);
111 }
112 });
113 } else if (g_capture_mode == 'staged2') {
114 OpenDBusPcapFile('./DBUS_MONITOR');
115 }
116 });
117}
118
119function OnCaptureStart() {
120 switch (g_capture_state) {
121 case 'not started':
122 capture_info.textContent = 'dbus-monitor running on BMC';
123 break;
124 default:
125 break;
126 }
127}
128
129function OnCaptureStop() {
130 btn_start_capture.disabled = false;
131 select_capture_mode.disabled = false;
132 text_hostname.disabled = false;
133 g_capture_state = 'not started';
134}
135
136async function OnTransferCompleted() {
137 setTimeout(function() {
138 console.log('OnTransferCompleted');
139 g_child.kill('SIGINT');
140 }, 5000);
141
142 capture_info.textContent = 'Loaded the capture file';
143 OnCaptureStop();
144 ExtractTarFile();
145}
146
147// Example output from stderr:
148// ^M Bytes received: 2549/ 2549 BPS:6370
149async function LaunchRZ() {
150 // On the Host
151
152 // Remove existing file
153 const file_names = ['DBUS_MONITOR', 'DBUS_MONITOR.tar.gz'];
154 try {
155 for (let i = 0; i < 2; i++) {
156 const fn = file_names[i];
157 if (fs.existsSync(fn)) {
158 fs.unlinkSync(fn); // unlink is basically rm
159 console.log('Removed file: ' + fn);
160 }
161 }
162 } catch (err) {
163 }
164
165 g_rz = spawn(
166 'screen', ['rz', '-a', '-e', '-E', '-r', '-w', '32767'], {shell: false});
167 g_rz.stdout.on('data', (data) => {
168 console.log('[rz] received ' + data.length + ' B');
169 console.log(data);
170 console.log(data + '');
171 // data = MyCorrection(data);
172 if (data != undefined) g_child.stdin.write(data);
173 });
174 g_rz.stderr.on('data', (data) => {
175 console.log('[rz] error: ' + data);
176 let s = data.toString();
177 let idx = s.lastIndexOf('Bytes received:');
178 if (idx != -1) {
179 capture_info.textContent = s.substr(idx);
180 }
181 if (data.indexOf('Transfer complete') != -1) {
182 OnTransferCompleted();
183 } else if (data.indexOf('Transfer incomplete') != -1) {
184 // todo: retry transfer
185 // Bug info
186 // Uncaught Error [ERR_STREAM_WRITE_AFTER_END]: write after end
187 // at writeAfterEnd (_stream_writable.js:253)
188 // at Socket.Writable.write (_stream_writable.js:302)
189 // at Socket.<anonymous> (ipmi_capture.js:317)
190 // at Socket.emit (events.js:210)
191 // at addChunk (_stream_readable.js:308)
192 // at readableAddChunk (_stream_readable.js:289)
193 // at Socket.Readable.push (_stream_readable.js:223)
194 // at Pipe.onStreamRead (internal/stream_base_commons.js:182)
195 capture_info.textContent = 'Transfer incomplete';
196 }
197 });
198 await Promise.all(
199 [g_rz.stdin.pipe(g_child.stdout), g_rz.stdout.pipe(g_child.stdin)]);
200}
201
202function ClearAllPendingTimeouts() {
203 var id = setTimeout(function() {}, 0);
204 for (; id >= 0; id--) clearTimeout(id);
205}
206
207function StartDbusMonitorFileSizePollLoop() {
208 QueueDbusMonitorFileSize(5);
209}
210
211function QueueDbusMonitorFileSize(secs = 5) {
212 setTimeout(function() {
213 g_child.stdin.write(
214 'a=`ls -l /run/initramfs/DBUS_MONITOR | awk \'{print $5}\'` ; echo ">>>>>>$a<<<<<<" \n\n\n\n');
215 QueueDbusMonitorFileSize(secs);
216 }, secs * 1000);
217}
218
219function StopCapture() {
220 switch (g_capture_mode) {
221 case 'live':
222 g_child.stdin.write('\x03 ');
223 g_capture_state = 'stopping';
224 capture_info.textContent = 'Ctrl+C sent to BMC console';
225 break;
226 case 'staged':
227 ClearAllPendingTimeouts();
228 g_child.stdin.write(
229 'echo ">>>>>>" && killall busctl && echo "<<<<<<" \n\n\n\n');
230 g_capture_state = 'stopping';
231 capture_info.textContent = 'Stopping dbus-monitor';
232 case 'staged2':
233 g_hexdump_received_size = 0;
234 g_hexdump_total_size = 0;
235 ClearAllPendingTimeouts();
236 g_child.stdin.write(
237 'echo ">>>>>>" && killall busctl && echo "<<<<<<" \n\n\n\n');
238 g_capture_state = 'stopping';
239 capture_info.textContent = 'Stopping busctl';
240 break;
241 }
242}
243
244function QueueBMCConsoleHello(secs = 3) {
245 setTimeout(function() {
246 try {
247 if (g_capture_state == 'not started') {
248 console.log('Sending hello <cr> to the BMC');
249 g_child.stdin.write('\n');
250 QueueBMCConsoleHello(secs);
251 }
252 } catch (err) {
253 console.log('g_child may have ended as intended');
254 }
255 }, secs * 1000);
256}
257
258// The command line needed to access the BMC. The expectation is
259// executing this command brings the user to the BMC's console.
260function GetCMDLine() {
261 let v = text_hostname.value.split(' ');
262 return [v[0], v.slice(1, v.length)];
263}
264
265async function StartCapture(host) {
266 // Disable buttons
267 HideWelcomeScreen();
268 ShowIPMITimeline();
269 ShowNavigation();
270 let args = GetCMDLine();
271 btn_start_capture.disabled = true;
272 select_capture_mode.disabled = true;
273 text_hostname.disabled = true;
274 capture_info.textContent = 'Contacting BMC console: ' + args.toString();
275
276 // On the B.M.C.
277 let last_t = currTimestamp();
278 let attempt = 0;
279 console.log('Args: ' + args);
280 g_child = spawn(args[0], args[1], {shell: true});
281 g_child.stdout.on('data', async function(data) {
282 QueueBMCConsoleHello();
283
284 var t = currTimestamp();
285 {
286 switch (g_capture_state) {
287 case 'not started': // Do nothing
288 break;
289 case 'started':
290 attempt++;
291 console.log('attempt ' + attempt);
292 g_child.stdin.write('echo "haha" \n');
293 await streamWrite(g_child.stdin, 'whoami \n');
294 let idx = data.indexOf('haha');
295 if (idx != -1) {
296 ClearAllPendingTimeouts();
297 OnCaptureStart(); // Successfully logged on, start
298
299 if (g_capture_mode == 'live') {
300 g_child.stdin.write(
301 '\n\n' +
302 'a=`pidof btbridged`;b=`pidof kcsbridged`;c=`pidof netipmid`;' +
303 'echo ">>>>>>$a,$b,$c<<<<<<"\n\n');
304 g_capture_state = 'determine bridge daemon';
305 } else {
306 g_capture_state = 'dbus monitor start';
307 }
308 capture_info.textContent = 'Reached BMC console';
309
310 } else {
311 console.log('idx=' + idx);
312 }
313 break;
314 case 'determine bridge daemon': {
315 const abc = ExtractMyDelimitedStuff(data.toString());
316 if (abc == null) break;
317 const sp = abc.split(',');
318 if (parseInt(sp[0]) >= 0) { // btbridged, legacy interface
319 g_dbus_monitor_cmd = DBUS_MONITOR_LEGACY;
320 console.log('The BMC is using btbridged.');
321 } else if (parseInt(sp[1]) >= 0) { // new iface
322 g_dbus_monitor_cmd = DBUS_MONITOR_NEW;
323 console.log('The BMC is using kcsbridged.');
324 } else if (parseInt(sp[2]) >= 0) {
325 g_dbus_monitor_cmd = DBUS_MONITOR_NEW;
326 console.log('The BMC is using netipmid.');
327 } else {
328 console.log('Cannot determine the IPMI bridge daemon\n')
329 return;
330 }
331 g_capture_state = 'dbus monitor start';
332 break;
333 }
334 case 'dbus monitor start':
335 if (g_capture_mode == 'live') {
336 // It would be good to make sure the console bit rate is greater
337 // than the speed at which outputs are generated.
338 // g_child.stdin.write("dbus-monitor --system | grep
339 // \"sendMessage\\|ReceivedMessage\" -A7 \n")
340 ClearAllPendingTimeouts();
341 g_child.stdin.write(
342 'dbus-monitor --system | grep "member=execute\\|method return" -A7 \n');
343 capture_info.textContent = 'Started dbus-monitor for live capture';
344 } else {
345 // g_child.stdin.write("dbus-monitor --system | grep
346 // \"sendMessage\\|ReceivedMessage\" -A7 >
347 // /run/initramfs/DBUS_MONITOR & \n\n\n")
348 ClearAllPendingTimeouts();
349 if (g_capture_mode == 'staged') {
350 g_child.stdin.write(
351 'dbus-monitor --system > /run/initramfs/DBUS_MONITOR & \n\n\n');
352 capture_info.textContent =
353 'Started dbus-monitor for staged IPMI capture';
354 } else if (g_capture_mode == 'staged2') {
355 g_child.stdin.write(
356 'busctl capture > /run/initramfs/DBUS_MONITOR & \n\n\n');
357 capture_info.textContent =
358 'Started busctl for staged IPMI + DBus capture';
359 }
360 StartDbusMonitorFileSizePollLoop();
361 }
362 g_capture_state = 'dbus monitor running';
363 break;
364 case 'dbus monitor running':
365 if (g_capture_mode == 'staged' || g_capture_mode == 'staged2') {
366 let s = data.toString();
367 let tmp = ExtractMyDelimitedStuff(s, 'int');
368 if (tmp != undefined) {
369 let sz = Math.floor(parseInt(tmp) / 1024);
370 if (!isNaN(sz)) {
371 capture_info.textContent =
372 'Raw Dbus capture size: ' + sz + ' KiB';
373 } else { // This can happen if the output is cut by half & may be
374 // fixed by queuing console outputs
375 }
376 }
377 } else {
378 AppendToParseBuffer(data.toString());
379 MunchLines();
380 UpdateLayout();
381 ComputeHistogram();
382 }
383 break;
384 case 'dbus monitor end': // Todo: add speed check
385 let s = data.toString();
386 let i0 = s.lastIndexOf('>>>>'), i1 = s.lastIndexOf('<<<<');
387 if (i0 != -1 && i1 != -1) {
388 let tmp = s.substr(i0 + 4, i1 - i0 - 4);
389 let sz = parseInt(tmp);
390 if (isNaN(sz)) {
391 console.log(
392 'Error: the tentative dbus-profile dump is not found!');
393 } else {
394 let bps = sz / 10;
395 console.log('dbus-monitor generates ' + bps + 'B per second');
396 }
397 }
398 g_child.kill('SIGINT');
399 break;
400 case 'sz sending':
401 console.log('[sz] Received a chunk of size ' + data.length);
402 console.log(data);
403 console.log(data + '');
404 // capture_info.textContent = "Received a chunk of size " +
405 // data.length
406 g_rz.stdin.write(data);
407 break;
408 case 'stopping':
409 let t = data.toString();
410 if (g_capture_mode == 'live') {
411 if (t.lastIndexOf('^C') != -1) {
412 // Live mode
413 g_child.kill('SIGINT');
414 g_capture_state = 'not started';
415 OnCaptureStop();
416 capture_info.textContent = 'connection to BMC closed';
417 // Log mode
418 }
419 } else if (
420 g_capture_mode == 'staged' || g_capture_mode == 'staged2') {
421 ClearAllPendingTimeouts();
422 if (t.lastIndexOf('<<<<<<') != -1) {
423 g_capture_state = 'compressing';
424 g_child.stdin.write(
425 'echo ">>>>>>" && cd /run/initramfs && tar cfz DBUS_MONITOR.tar.gz DBUS_MONITOR && echo "<<<<<<" \n\n\n\n');
426 capture_info.textContent = 'Compressing dbus monitor dump on BMC';
427 }
428 }
429 break;
430 case 'compressing':
431 g_child.stdin.write(
432 '\n\na=`ls -l /run/initramfs/DBUS_MONITOR.tar.gz | awk \'{print $5}\'` && echo ">>>>>>$a<<<<<<" \n\n\n\n');
433 g_capture_state = 'dbus_monitor size';
434 capture_info.textContent = 'Obtaining size of compressed dbus dump';
435 break;
436 case 'dbus_monitor size':
437 // Starting RZ
438 let tmp = ExtractMyDelimitedStuff(data.toString(), 'int');
439 if (tmp != null && !isNaN(tmp)) { // Wait until result shows up
440 g_hexdump_total_size = tmp;
441 console.log(
442 'dbus_monitor size tmp=' + tmp + ', ' + data.toString());
443
444 // if (tmp != undefined) {
445 // g_dbus_capture_tarfile_size = tmp;
446 // capture_info.textContent =
447 // 'Starting rz and sz, file size: ' + Math.floor(tmp / 1024)
448 // + ' KiB';
449 // } else {
450 // capture_info.textContent = 'Starting rz and sz';
451 // }
452 // g_capture_state = 'sz start';
453 // g_child.stdin.write(
454 // '\n\n\n\n' +
455 // 'sz -a -e -R -L 512 -w 32767 -y
456 // /run/initramfs/DBUS_MONITOR.tar.gz\n');
457 // g_capture_state = 'sz sending';
458 // LaunchRZ();
459 g_child.stdin.write(
460 'echo ">>>>>>"; hexdump /run/initramfs/DBUS_MONITOR.tar.gz ; echo "<<<<<<"; \n');
461 g_capture_state = 'test hexdump running';
462 g_hexdump = new Buffer([]);
463 }
464
465 break;
466 case 'test hexdump start':
467 g_child.stdin.write(
468 'echo ">>>>>>"; hexdump /run/initramfs/DBUS_MONITOR.tar.gz ; echo "<<<<<<"; \n');
469 g_capture_state = 'test hexdump running';
470 g_hexdump = new Buffer([]);
471 g_hexdump_received_size = 0;
472 break;
473 case 'test hexdump running':
474 g_hexdump += data;
475 const lines = data.toString().split('\n');
476 for (let j = lines.length - 1; j >= 0; j--) {
477 sp = lines[j].trimEnd().split(' ');
478 if (sp.length >= 1) {
479 const sz = parseInt(sp[0], 16)
480 if (!isNaN(sz)) {
481 if (g_hexdump_received_size < sz) {
482 g_hexdump_received_size = sz;
483 capture_info.textContent = 'Receiving capture file: ' + sz +
484 ' / ' + g_hexdump_total_size + ' B';
485 break;
486 }
487 }
488 }
489 }
490 if (data.includes('<<<<<<') && !data.includes('echo')) {
491 g_hexdump = ExtractMyDelimitedStuff(g_hexdump);
492 SaveHexdumpToFile(g_hexdump, 'DBUS_MONITOR.tar.gz');
493 OnTransferCompleted();
494 }
495 break;
496 }
497 last_t = t;
498 }
499 });
500 g_child.stderr.on('data', (data) => {
501 console.log('[bmc] err=' + data);
502 g_child.stdin.write('echo "haha" \n\n');
503 });
504 g_child.on('close', (code) => {
505 console.log('return code: ' + code);
506 });
507}