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