chromium/chrome/updater/tools/o4logtool.html

<html>
  <head>
    <script>
      function loadFromClipboard(hideCrashHandlers) {
        for (let b of document.querySelectorAll("button")) b.style.display = "none";
        navigator.clipboard.readText().then(text => {
          makeRows(computeGrid(tagWithFPID(parseLines(text)), hideCrashHandlers));
          document.querySelector("#controls").style.display = "block";
        });
      }

      function parseLines(str) {
        let lines = [];
        const logLineHeaderRegex = /^\[(?<process>[^:]*):(?<thread>[^:]*):(?<time>[^:]*):(?<level>[^:]*):(?<location>[^:]*)\] (?<text>.*)/;
        for (let l of str.split("\n")) {
          let match = logLineHeaderRegex.exec(l);
          if (match !== null) {
            lines.push({"process": match.groups.process, "thread": match.groups.thread, "time": match.groups.time, "level": match.groups.level, "location": match.groups.location, "text": match.groups.text + "\n"});
          } else {
            if (lines.length == 0) lines.push({"process": 0, "thread": 0, "time": "", "level": 0, "location": "", "text": l + "\n"});
            else lines[lines.length - 1].text += l + "\n";
          }
        }
        let linesEqual = (a, b) => {
          return (
            a.process == b.process
            && a.thread == b.thread
            && a.time == b.time
            && a.level == b.level
            && a.location == b.location
            && a.text == b.text);
        }
        let lines2 = [];
        outer: for (let l of lines) {
          for (let ll of lines2) {
            if (linesEqual(l, ll)) continue outer;
          }
          lines2.push(l);
        }
        lines = lines2;
        lines.sort((a, b) => a.time.localeCompare(b.time));

        return lines;
      }

      // PID reuse is common in longer logs. Split processes at
      // "UpdaterMain (.*) returned .*."
      function tagWithFPID(lines) {
        let fpid = 0;
        let openPids = {};
        const finalLine = /UpdaterMain \(--[a-zA-Z-]+\) returned .+/
        for (let l of lines) {
          if (!openPids.hasOwnProperty(l.process)) {
            openPids[l.process] = fpid++;
          }
          l.fpid = openPids[l.process];
          if (l.text.match(finalLine) != null) delete(openPids[l.process]);
        }
        return lines;
      }

      function computeGrid(lines, hideCrashHandlers) {
        let grid = [];
        let firstLineByProcess = {};
        let lastLineByProcess = {};

        // Crash handlers are recognized by having all logs from updater.cc
        // contain --crash-handler.
        if (hideCrashHandlers) {
          let crashHandlers = {};
          let notCrashHandlers = {};
          for (let l of lines) {
            if (l.location.indexOf("updater.cc") == 0 && l.text.indexOf("command line:") > 0) {
              if (l.text.indexOf("--crash-handler") >= 0) {
                crashHandlers[l.fpid] = true;
              } else {
                notCrashHandlers[l.fpid] = true;
              }
            }
          }
          for (let p in notCrashHandlers) delete(crashHandlers[p]);
          lines = lines.filter(l => !crashHandlers.hasOwnProperty(l.fpid));
        }

        for (let i = 0; i < lines.length; i++) {
          let p = lines[i].fpid;
          if (!firstLineByProcess.hasOwnProperty(p)) firstLineByProcess[p] = i;
          lastLineByProcess[p] = i;
        }

        let procColumns = [];
        const BLANK = {"type": "blank"};
        for (let i = 0; i < lines.length;) {
          for (let j = 0; j < procColumns.length; j++) {
            if (procColumns[j] != -1 && lastLineByProcess[procColumns[j]] < i) procColumns[j] = -1;
          }
          let p = lines[i].fpid;
          let x = procColumns.indexOf(p);
          outer: if (x == -1) {
            for (let j = 0; j < procColumns.length; j++) {
              if (procColumns[j] == -1) {
                procColumns[j] = p;
                x = j;
                break outer;
              }
            }
            procColumns.push(p);
            x = procColumns.length - 1;
          }

          let start_line = i;
          let llines = [];
          while (i < lines.length && lines[i].fpid == lines[start_line].fpid && lines[i].time.substring(0, 4) == lines[start_line].time.substring(0, 4)) {
            llines.push(lines[i]);
            i++;
          }

          let gridRow = [];
          for (let j = 0; j < procColumns.length; j++) {
            if (j == x) gridRow.push({"type": "line", "lines": llines});
            else if (procColumns[j] == -1) gridRow.push(BLANK);
            else gridRow.push({"type": "processBlank", "fpid": procColumns[j]});
          }
          grid.push(gridRow);
        }

        for (let r of grid) while (r.length < procColumns.length) r.push(BLANK);
        return grid;
      }

      function makeRows(grid) {
        let colors_hints = [
          {"hint": "--windows-service --service=update-internal ", "color": [168, 218, 181]},
          {"hint": "--windows-service --service=update ",          "color": [138, 180, 248]},
          {"hint": "--server --service=update-internal ",          "color": [168, 218, 181]},
          {"hint": "--server --service=update ",                   "color": [138, 180, 248]},
          {"hint": "--crash-handler",                              "color": [240, 240, 240]},
          {"hint": "--install",                                    "color": [255, 255, 200]},
          {"hint": "--update",                                     "color": [200, 255, 200]},
          {"hint": "--uninstall",                                  "color": [255, 120, 120]},
          {"hint": "--uninstall-self",                             "color": [255, 180, 180]},
          {"hint": "--uninstall-if-unused",                        "color": [255, 150, 150]},
          {"hint": "--wake ",                                      "color": [254, 239, 195]},
          {"hint": "--wake-all",                                   "color": [255, 235, 215]},
          {"hint": "--test",                                       "color": [220, 220, 220]},
        ];

        let colorBaseMap = {};
        let colorMap = {};

        let isFirstFromProcess = (process) => {
          return !colorMap.hasOwnProperty(process);
        }

        for (let r of grid) {
          for (let c of r) {
            if (c.type == "line") {
              for (let l of c.lines) {
                if (colorBaseMap.hasOwnProperty(l.fpid)) break;
                if (l.location.indexOf("updater.cc") == 0) {
                  for (let ch of colors_hints) {
                    if (l.text.indexOf(ch.hint) > 0) {
                      colorBaseMap[l.fpid] = ch.color;
                      break;
                    }
                  }
                }
              }
            }
          }
        }

        let getColor = (fpid) => {
          if (!colorMap.hasOwnProperty(fpid)) {
            let c = [220, 80, 80];
            if (colorBaseMap.hasOwnProperty(fpid)) {
              c = colorBaseMap[fpid];
            }
            let r = Math.random() * 10 - 5;
            colorMap[fpid] = c.map(c => Math.max(0, Math.min(255, c + r)));
          }
          return "rgb(" + colorMap[fpid].join(",") + ")";
        }

        let t = document.querySelector("table");
        let lastdate = "";
        for (let r of grid) {
          let tr = document.createElement("tr");
          let rdate = lastdate;
          let wrotedate = false;
          for (let i = 0; i < r.length; i++) {
            if (r[i].type == "line") rdate = r[i].lines[0].time.substring(0, 4);
          }
          for (let i = 0; i < r.length; i++) {
            let c = r[i];
            let td = document.createElement("td");
            if (c.type == "blank") {
              td.appendChild(document.createElement("div"));
              td.addEventListener("click", () => expandColumn(-1));
              if (rdate != lastdate) {
                td.style.borderTop = "1px dotted #aaa";
                if (!wrotedate) {
                  td.appendChild(document.createTextNode(rdate.substring(0, 2) + "/" + rdate.substring(2, 4)));
                  td.style.textAlign = "center";
                  td.style.color = "#aaa";
                  td.style.verticalAlign = "top";
                  wrotedate = true;
                }
              }
            } else if (c.type == "processBlank") {
              td.style.background = getColor(c.fpid);
              td.appendChild(document.createElement("div"));
              td.addEventListener("click", () => expandColumn(i));
            } else if (c.type == "line") {
              if (isFirstFromProcess(c.lines[0].fpid)) {
                td.style.borderRadius = "8px 8px 0 0";
              }
              td.style.background = getColor(c.lines[0].fpid);
              for (let l of c.lines) td.appendChild(makeDiv(l));
              td.addEventListener("click", () => expandColumn(i));
            }
            tr.appendChild(td);
          }
          t.appendChild(tr);
          lastdate = rdate;
        }
      }

      function makeDiv(line) {
        let div = document.createElement("div");

        let makeInfo = (type) => {
          let s = document.createElement("span");
          s.className = "lineinfo_" + type;
          s.appendChild(document.createTextNode(line[type]));
          div.appendChild(s);
        }
        makeInfo("process");
        makeInfo("thread");
        makeInfo("time");
        makeInfo("level");
        makeInfo("location");
        div.appendChild(document.createTextNode(line.text));
        return div;
      }

      function expandColumn(i) {
        for (let td of document.querySelectorAll("tr:first-child > td")) {
          td.className = i == 0 ? "expanded" : "";
          i--;
        }
      }

      function toggle(type) {
        for (let e of document.querySelectorAll(".lineinfo_" + type)) {
          e.style.display = event.target.checked ? "inline-block" : "none";
        }
      }
    </script>
    <style>
      body {
        margin: 0;
        padding: 0;
        font-family: sans-serif;
      }
      table {
        width: 100%;
        border-collapse: collapse;
      }
      td {
        max-width: 4vw;
        overflow: hidden;
        transition: max-width 0.2s;
      }
      td.expanded {
        max-width: 80vw;
      }
      td > div {
        width: 80vw;
        white-space: pre-wrap;
        font-family: monospace;
      }
      button {
        margin: 2em;
        width: 30%;
        font-size: 200%;
      }
      .lineinfo_process, .lineinfo_thread, .lineinfo_time, .lineinfo_level, .lineinfo_location {
        margin-right: 1em;
        display: none;
      }
      #controls {
        position: fixed;
        right: 0;
        text-align: right;
        display: none;
        color: #fff;
        padding: 0.1em 0 0 0.3em;
        background-color: #000;
        border-radius: 0 0 0 1em;
      }
      #controls label {
        display: block;
      }
    </style>
  </head>
  <body>
    <div id="controls">
      <label>Process ID<input type="checkbox" onchange="toggle('process')"></input></label>
      <label>Thread ID<input type="checkbox" onchange="toggle('thread')"></input></label>
      <label>Time<input type="checkbox" onchange="toggle('time')"></input></label>
      <label>Level<input type="checkbox" onchange="toggle('level')"></input></label>
      <label>Location<input type="checkbox" onchange="toggle('location')"></input></label>
    </div>
    <button onclick="loadFromClipboard(false)">Load from clipboard</button>
    <button onclick="loadFromClipboard(true)">Load from clipboard (hide crash handlers)</button>
    <table></table>
  </body>
</html>