Sui Chen | b65280f | 2020-06-30 18:14:03 -0700 | [diff] [blame] | 1 | const { spawn } = require('child_process'); |
| 2 | const targz = require('targz'); |
| 3 | |
| 4 | const DBUS_MONITOR_LEGACY = |
| 5 | 'dbus-monitor --system | grep "sendMessage\\|ReceivedMessage" -A7 \n'; |
| 6 | const DBUS_MONITOR_NEW = |
| 7 | 'dbus-monitor --system | grep "member=execute\\|method return" -A7 \n'; |
| 8 | |
| 9 | // Capture state for all scripts |
| 10 | var g_capture_state = 'not started'; |
| 11 | var g_capture_mode = 'live'; |
| 12 | |
| 13 | // For capturing IPMI requests live |
| 14 | var g_dbus_monitor_cmd = ''; |
| 15 | |
| 16 | // For tracking transfer |
| 17 | var g_hexdump = ''; |
| 18 | var g_hexdump_received_size = 0; |
| 19 | var g_hexdump_total_size = 0; |
| 20 | |
| 21 | function currTimestamp() { |
| 22 | var tmp = new Date(); |
| 23 | return (tmp.getTime() + tmp.getTimezoneOffset() * 60000) / 1000; |
| 24 | } |
| 25 | |
| 26 | var g_child; |
| 27 | var g_rz; |
| 28 | |
| 29 | var g_capture_live = true; |
| 30 | var g_dbus_capture_tarfile_size = 0; |
| 31 | |
| 32 | function 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 | |
| 62 | function SaveHexdumpToFile(hd, file_name) { |
| 63 | const buf = ParseHexDump(hd); |
| 64 | fs.writeFileSync(file_name, buf) |
| 65 | } |
| 66 | |
| 67 | // Delimiters: ">>>>>>" and "<<<<<<" |
| 68 | function 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 | |
| 80 | function 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 | |
| 95 | function 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 | |
| 119 | function 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 | |
| 129 | function 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 | |
| 136 | async 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 |
| 149 | async 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 | |
| 202 | function ClearAllPendingTimeouts() { |
| 203 | var id = setTimeout(function() {}, 0); |
| 204 | for (; id >= 0; id--) clearTimeout(id); |
| 205 | } |
| 206 | |
| 207 | function StartDbusMonitorFileSizePollLoop() { |
| 208 | QueueDbusMonitorFileSize(5); |
| 209 | } |
| 210 | |
| 211 | function 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 | |
| 219 | function 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 | |
| 244 | function 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. |
| 260 | function GetCMDLine() { |
| 261 | let v = text_hostname.value.split(' '); |
| 262 | return [v[0], v.slice(1, v.length)]; |
| 263 | } |
| 264 | |
| 265 | async 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 | } |