1const { 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} 508