);
});
- var tabs = [];
- var tabIdx;
- var Renderer;
- var rendererData;
- var row;
+ let tabs = [];
+ let tabIdx;
+ let Renderer;
+ let rendererData;
+ let row;
for (tabIdx = 0; tabIdx < this.props.tabRenderers.length; tabIdx++) {
Renderer = this.props.tabRenderers[tabIdx].renderer;
rendererData = this.props.tabRenderers[tabIdx].data;
@@ -231,7 +220,7 @@ var ListingRow = React.createClass({
tabs.push(
{row}
);
}
- var listingDetail;
+ let listingDetail;
if ('listingDetail' in this.props) {
listingDetail = (
@@ -268,8 +257,26 @@ var ListingRow = React.createClass({
);
}
}
-});
+}
+ListingRow.defaultProps = {
+ tabRenderers: [],
+ navigateToItem: null,
+};
+
+ListingRow.propTypes = {
+ rowId: PropTypes.string,
+ columns: PropTypes.array.isRequired,
+ tabRenderers: PropTypes.array,
+ navigateToItem: PropTypes.func,
+ listingDetail: PropTypes.node,
+ listingActions: PropTypes.arrayOf(PropTypes.node),
+ selectChanged: PropTypes.func,
+ selected: PropTypes.bool,
+ initiallyExpanded: PropTypes.bool,
+ expandChanged: PropTypes.func,
+ initiallyActiveTab: PropTypes.bool
+};
/* Implements a PatternFly 'List View' pattern
* https://www.patternfly.org/list-view/
* Properties:
@@ -281,80 +288,77 @@ var ListingRow = React.createClass({
* receives the column index as argument
* - actions: additional listing-wide actions (displayed next to the list's title)
*/
-var Listing = React.createClass({
- propTypes: {
- title: React.PropTypes.string.isRequired,
- fullWidth: React.PropTypes.bool,
- emptyCaption: React.PropTypes.string.isRequired,
- columnTitles: React.PropTypes.arrayOf(React.PropTypes.string),
- columnTitleClick: React.PropTypes.func,
- actions: React.PropTypes.arrayOf(React.PropTypes.node)
- },
- getDefaultProps: function () {
- return {
- fullWidth: true,
- columnTitles: [],
- actions: []
- };
- },
- render: function() {
- var self = this;
- var bodyClasses = ["listing", "listing-ct"];
- if (this.props.fullWidth)
- bodyClasses.push("listing-ct-wide");
- var headerClasses;
- var headerRow;
- var selectableRows;
- if (!this.props.children || this.props.children.length === 0) {
- headerClasses = "listing-ct-empty";
- headerRow =
{this.props.emptyCaption}
;
- } else if (this.props.columnTitles.length) {
- // check if any of the children are selectable
- selectableRows = false;
- this.props.children.forEach(function(r) {
- if (r.props.selected !== undefined)
- selectableRows = true;
+export const Listing = (props) => {
+ let bodyClasses = ["listing", "listing-ct"];
+ if (props.fullWidth)
+ bodyClasses.push("listing-ct-wide");
+ let headerClasses;
+ let headerRow;
+ let selectableRows;
+ if (!props.children || props.children.length === 0) {
+ headerClasses = "listing-ct-empty";
+ headerRow =
{props.emptyCaption}
;
+ } else if (props.columnTitles.length) {
+ // check if any of the children are selectable
+ selectableRows = false;
+ props.children.forEach(function(r) {
+ if (r.props.selected !== undefined)
+ selectableRows = true;
+ });
+
+ if (selectableRows) {
+ // now make sure that if one is set, it's available on all items
+ props.children.forEach(function(r) {
+ if (r.props.selected === undefined)
+ r.props.selected = false;
});
-
- if (selectableRows) {
- // now make sure that if one is set, it's available on all items
- this.props.children.forEach(function(r) {
- if (r.props.selected === undefined)
- r.props.selected = false;
- });
- }
-
- headerRow = (
-
+ );
+};
+
+Listing.defaultProps = {
+ title: '',
+ fullWidth: true,
+ columnTitles: [],
+ actions: []
+};
+
+Listing.propTypes = {
+ title: PropTypes.string,
+ fullWidth: PropTypes.bool,
+ emptyCaption: PropTypes.string.isRequired,
+ columnTitles: PropTypes.arrayOf(
+ PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.element,
+ ])),
+ columnTitleClick: PropTypes.func,
+ actions: PropTypes.arrayOf(PropTypes.node)
};
diff --git a/src/player.jsx b/src/player.jsx
index 53c75c3..d04cb5e 100644
--- a/src/player.jsx
+++ b/src/player.jsx
@@ -1,1132 +1,1121 @@
/*
- * This file is part of Cockpit.
- *
- * Copyright (C) 2017 Red Hat, Inc.
- *
- * Cockpit is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation; either version 2.1 of the License, or
- * (at your option) any later version.
- *
- * Cockpit is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Cockpit; If not, see .
+* This file is part of Cockpit.
+*
+* Copyright (C) 2017 Red Hat, Inc.
+*
+* Cockpit is free software; you can redistribute it and/or modify it
+* under the terms of the GNU Lesser General Public License as published by
+* the Free Software Foundation; either version 2.1 of the License, or
+* (at your option) any later version.
+*
+* Cockpit is distributed in the hope that it will be useful, but
+* WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with Cockpit; If not, see .
+*/
+"use strict";
+import React from 'react';
+let cockpit = require("cockpit");
+let _ = cockpit.gettext;
+let Term = require("term.js-cockpit");
+let Journal = require("journal");
+let $ = require("jquery");
+require("console.css");
+
+/*
+ * Get an object field, verifying its presence and type.
*/
-
-(function() {
- "use strict";
-
- let cockpit = require("cockpit");
- let _ = cockpit.gettext;
- let React = require("react");
- let Term = require("term.js-cockpit");
- let Journal = require("journal");
- let $ = require("jquery");
-
- require("console.css");
-
- /*
- * Get an object field, verifying its presence and type.
- */
- let getValidField = function (object, field, type) {
- let value;
- if (!(field in object)) {
- throw Error("\"" + field + "\" field is missing");
- }
- value = object[field];
- if (typeof (value) != typeof (type)) {
- throw Error("invalid \"" + field + "\" field type: " + typeof (value));
- }
- return value;
+let getValidField = function (object, field, type) {
+ let value;
+ if (!(field in object)) {
+ throw Error("\"" + field + "\" field is missing");
}
+ value = object[field];
+ if (typeof (value) != typeof (type)) {
+ throw Error("invalid \"" + field + "\" field type: " + typeof (value));
+ }
+ return value;
+};
- let scrollToBottom = function(id) {
- const el = document.getElementById(id);
- if (el) {
- el.scrollTop = el.scrollHeight;
- }
+let scrollToBottom = function(id) {
+ const el = document.getElementById(id);
+ if (el) {
+ el.scrollTop = el.scrollHeight;
+ }
+};
+
+/*
+ * An auto-loading buffer of recording's packets.
+ */
+let PacketBuffer = class {
+ /*
+ * Initialize a buffer.
+ */
+ constructor(matchList) {
+ this.handleError = this.handleError.bind(this);
+ this.handleStream = this.handleStream.bind(this);
+ this.handleDone = this.handleDone.bind(this);
+ /* RegExp used to parse message's timing field */
+ this.timingRE = new RegExp(
+ /* Delay (1) */
+ "\\+(\\d+)|" +
+ /* Text input (2) */
+ "<(\\d+)|" +
+ /* Binary input (3, 4) */
+ "\\[(\\d+)/(\\d+)|" +
+ /* Text output (5) */
+ ">(\\d+)|" +
+ /* Binary output (6, 7) */
+ "\\](\\d+)/(\\d+)|" +
+ /* Window (8, 9) */
+ "=(\\d+)x(\\d+)|" +
+ /* End of string */
+ "$",
+ /* Continue after the last match only */
+ /* FIXME Support likely sparse */
+ "y"
+ );
+ /* List of matches to apply when loading the buffer from Journal */
+ this.matchList = matchList;
+ /*
+ * An array of two-element arrays (tuples) each containing a
+ * packet index and a deferred object. The list is kept sorted to
+ * have tuples with lower packet indices first. Once the buffer
+ * receives a packet at the specified index, the matching tuple is
+ * removed from the list, and its deferred object is resolved.
+ * This is used to keep users informed about packets arriving.
+ */
+ this.idxDfdList = [];
+ /* Last seen message ID */
+ this.id = 0;
+ /* Last seen time position */
+ this.pos = 0;
+ /* Last seen window width */
+ this.width = null;
+ /* Last seen window height */
+ this.height = null;
+ /* List of packets read */
+ this.pktList = [];
+ /* Error which stopped the loading */
+ this.error = null;
+ /* The journalctl reading the recording */
+ this.journalctl = Journal.journalctl(
+ this.matchList,
+ {count: "all", follow: false});
+ this.journalctl.fail(this.handleError);
+ this.journalctl.stream(this.handleStream);
+ this.journalctl.done(this.handleDone);
+ /*
+ * Last seen cursor of the first, non-follow, journalctl run.
+ * Null if no entry was received yet, or the second run has
+ * skipped the entry received last by the first run.
+ */
+ this.cursor = null;
+ /* True if the first, non-follow, journalctl run has completed */
+ this.done = false;
}
/*
- * An auto-loading buffer of recording's packets.
+ * Return a promise which is resolved when a packet at a particular
+ * index is received by the buffer. The promise is rejected with a
+ * non-null argument if an error occurs or has occurred previously.
+ * The promise is rejected with null, when the buffer is stopped. If
+ * the packet index is not specified, assume it's the next packet.
*/
- let PacketBuffer = class {
- /*
- * Initialize a buffer.
- */
- constructor(matchList) {
- this.handleError = this.handleError.bind(this);
- this.handleStream = this.handleStream.bind(this);
- this.handleDone = this.handleDone.bind(this);
- /* RegExp used to parse message's timing field */
- this.timingRE = new RegExp(
- /* Delay (1) */
- "\\+(\\d+)|" +
- /* Text input (2) */
- "<(\\d+)|" +
- /* Binary input (3, 4) */
- "\\[(\\d+)/(\\d+)|" +
- /* Text output (5) */
- ">(\\d+)|" +
- /* Binary output (6, 7) */
- "\\](\\d+)/(\\d+)|" +
- /* Window (8, 9) */
- "=(\\d+)x(\\d+)|" +
- /* End of string */
- "$",
- /* Continue after the last match only */
- /* FIXME Support likely sparse */
- "y"
- );
- /* List of matches to apply when loading the buffer from Journal */
- this.matchList = matchList;
- /*
- * An array of two-element arrays (tuples) each containing a
- * packet index and a deferred object. The list is kept sorted to
- * have tuples with lower packet indices first. Once the buffer
- * receives a packet at the specified index, the matching tuple is
- * removed from the list, and its deferred object is resolved.
- * This is used to keep users informed about packets arriving.
- */
- this.idxDfdList = [];
- /* Last seen message ID */
- this.id = 0;
- /* Last seen time position */
- this.pos = 0;
- /* Last seen window width */
- this.width = null;
- /* Last seen window height */
- this.height = null;
- /* List of packets read */
- this.pktList = [];
- /* Error which stopped the loading */
- this.error = null;
- /* The journalctl reading the recording */
- this.journalctl = Journal.journalctl(
- this.matchList,
- {count: "all", follow: false});
- this.journalctl.fail(this.handleError);
- this.journalctl.stream(this.handleStream);
- this.journalctl.done(this.handleDone);
- /*
- * Last seen cursor of the first, non-follow, journalctl run.
- * Null if no entry was received yet, or the second run has
- * skipped the entry received last by the first run.
- */
- this.cursor = null;
- /* True if the first, non-follow, journalctl run has completed */
- this.done = false;
+ awaitPacket(idx) {
+ let i;
+ let idxDfd;
+
+ /* If an error has occurred previously */
+ if (this.error !== null) {
+ /* Reject immediately */
+ return $.Deferred().reject(this.error)
+ .promise();
}
- /*
- * Return a promise which is resolved when a packet at a particular
- * index is received by the buffer. The promise is rejected with a
- * non-null argument if an error occurs or has occurred previously.
- * The promise is rejected with null, when the buffer is stopped. If
- * the packet index is not specified, assume it's the next packet.
- */
- awaitPacket(idx) {
- let i;
- let idxDfd;
+ /* If the buffer was stopped */
+ if (this.journalctl === null) {
+ return $.Deferred().reject(null)
+ .promise();
+ }
- /* If an error has occurred previously */
- if (this.error !== null) {
- /* Reject immediately */
- return $.Deferred().reject(this.error)
+ /* If packet index is not specified */
+ if (idx === undefined) {
+ /* Assume it's the next one */
+ idx = this.pktList.length;
+ } else {
+ /* If it has already been received */
+ if (idx < this.pktList.length) {
+ /* Return resolved promise */
+ return $.Deferred().resolve()
.promise();
}
+ }
- /* If the buffer was stopped */
- if (this.journalctl === null) {
- return $.Deferred().reject(null)
- .promise();
+ /* Try to find an existing, matching tuple */
+ for (i = 0; i < this.idxDfdList.length; i++) {
+ idxDfd = this.idxDfdList[i];
+ if (idxDfd[0] == idx) {
+ return idxDfd[1].promise();
+ } else if (idxDfd[0] > idx) {
+ break;
}
+ }
- /* If packet index is not specified */
- if (idx === undefined) {
- /* Assume it's the next one */
- idx = this.pktList.length;
+ /* Not found, create and insert a new tuple */
+ idxDfd = [idx, $.Deferred()];
+ this.idxDfdList.splice(i, 0, idxDfd);
+
+ /* Return its promise */
+ return idxDfd[1].promise();
+ }
+
+ /*
+ * Return true if the buffer was done loading everything logged to
+ * journal so far and is now waiting for and loading new entries.
+ * Return false if the buffer is loading existing entries so far.
+ */
+ isDone() {
+ return this.done;
+ }
+
+ /*
+ * Stop receiving the entries
+ */
+ stop() {
+ if (this.journalctl === null) {
+ return;
+ }
+ /* Destroy journalctl */
+ this.journalctl.stop();
+ this.journalctl = null;
+ /* Notify everyone we stopped */
+ for (let i = 0; i < this.idxDfdList.length; i++) {
+ this.idxDfdList[i][1].reject(null);
+ }
+ this.idxDfdList = [];
+ }
+
+ /*
+ * Add a packet to the received packet list.
+ */
+ addPacket(pkt) {
+ /* TODO Validate the packet */
+ /* Add the packet */
+ this.pktList.push(pkt);
+ /* Notify any matching listeners */
+ while (this.idxDfdList.length > 0) {
+ let idxDfd = this.idxDfdList[0];
+ if (idxDfd[0] < this.pktList.length) {
+ this.idxDfdList.shift();
+ idxDfd[1].resolve();
} else {
- /* If it has already been received */
- if (idx < this.pktList.length) {
- /* Return resolved promise */
- return $.Deferred().resolve()
- .promise();
- }
+ break;
}
-
- /* Try to find an existing, matching tuple */
- for (i = 0; i < this.idxDfdList.length; i++) {
- idxDfd = this.idxDfdList[i];
- if (idxDfd[0] == idx) {
- return idxDfd[1].promise();
- } else if (idxDfd[0] > idx) {
- break;
- }
- }
-
- /* Not found, create and insert a new tuple */
- idxDfd = [idx, $.Deferred()];
- this.idxDfdList.splice(i, 0, idxDfd);
-
- /* Return its promise */
- return idxDfd[1].promise();
}
+ }
- /*
- * Return true if the buffer was done loading everything logged to
- * journal so far and is now waiting for and loading new entries.
- * Return false if the buffer is loading existing entries so far.
- */
- isDone() {
- return this.done;
- }
-
- /*
- * Stop receiving the entries
- */
- stop() {
- if (this.journalctl === null) {
- return;
- }
- /* Destroy journalctl */
+ /*
+ * Handle an error.
+ */
+ handleError(error) {
+ /* Remember the error */
+ this.error = error;
+ /* Destroy journalctl, don't try to recover */
+ if (this.journalctl !== null) {
this.journalctl.stop();
this.journalctl = null;
- /* Notify everyone we stopped */
- for (let i = 0; i < this.idxDfdList.length; i++) {
- this.idxDfdList[i][1].reject(null);
- }
- this.idxDfdList = [];
}
+ /* Notify everyone we had an error */
+ for (let i = 0; i < this.idxDfdList.length; i++) {
+ this.idxDfdList[i][1].reject(error);
+ }
+ this.idxDfdList = [];
+ }
- /*
- * Add a packet to the received packet list.
- */
- addPacket(pkt) {
- /* TODO Validate the packet */
- /* Add the packet */
- this.pktList.push(pkt)
- /* Notify any matching listeners */
- while (this.idxDfdList.length > 0) {
- let idxDfd = this.idxDfdList[0];
- if (idxDfd[0] < this.pktList.length) {
- this.idxDfdList.shift();
- idxDfd[1].resolve();
- } else {
+ /*
+ * Parse packets out of a tlog message data and add them to the buffer.
+ */
+ parseMessageData(timing, in_txt, out_txt) {
+ let matches;
+ let in_txt_pos = 0;
+ let out_txt_pos = 0;
+ let t;
+ let x;
+ let y;
+ let s;
+ let io = [];
+ let is_output;
+
+ /* While matching entries in timing */
+ this.timingRE.lastIndex = 0;
+ for (;;) {
+ /* Match next timing entry */
+ matches = this.timingRE.exec(timing);
+ if (matches === null) {
+ throw Error("invalid timing string");
+ } else if (matches[0] == "") {
+ break;
+ }
+
+ /* Switch on entry type character */
+ switch (t = matches[0][0]) {
+ /* Delay */
+ case "+":
+ x = parseInt(matches[1], 10);
+ if (x == 0) {
break;
}
- }
- }
-
- /*
- * Handle an error.
- */
- handleError(error) {
- /* Remember the error */
- this.error = error;
- /* Destroy journalctl, don't try to recover */
- if (this.journalctl !== null) {
- this.journalctl.stop();
- this.journalctl = null;
- }
- /* Notify everyone we had an error */
- for (let i = 0; i < this.idxDfdList.length; i++) {
- this.idxDfdList[i][1].reject(error);
- }
- this.idxDfdList = [];
- }
-
- /*
- * Parse packets out of a tlog message data and add them to the buffer.
- */
- parseMessageData(timing, in_txt, out_txt) {
- let matches;
- let in_txt_pos = 0;
- let out_txt_pos = 0;
- let t;
- let x;
- let y;
- let s;
- let io = [];
- let is_output;
-
- /* While matching entries in timing */
- this.timingRE.lastIndex = 0;
- for (;;) {
- /* Match next timing entry */
- matches = this.timingRE.exec(timing);
- if (matches === null) {
- throw Error("invalid timing string");
- } else if (matches[0] == "") {
- break;
- }
-
- /* Switch on entry type character */
- switch (t = matches[0][0]) {
- /* Delay */
- case "+":
- x = parseInt(matches[1], 10);
- if (x == 0) {
- break;
- }
- if (io.length > 0) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
- io = [];
- }
- this.pos += x;
- break;
- /* Text or binary input */
- case "<":
- case "[":
- x = parseInt(matches[(t == "<") ? 2 : 3], 10);
- if (x == 0) {
- break;
- }
- if (io.length > 0 && is_output) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
- io = [];
- }
- is_output = false;
- /* Add (replacement) input characters */
- s = in_txt.slice(in_txt_pos, in_txt_pos += x);
- if (s.length != x) {
- throw Error("timing entry out of input bounds");
- }
- io.push(s);
- break;
- /* Text or binary output */
- case ">":
- case "]":
- x = parseInt(matches[(t == ">") ? 5 : 6], 10);
- if (x == 0) {
- break;
- }
- if (io.length > 0 && !is_output) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
- io = [];
- }
- is_output = true;
- /* Add (replacement) output characters */
- s = out_txt.slice(out_txt_pos, out_txt_pos += x);
- if (s.length != x) {
- throw Error("timing entry out of output bounds");
- }
- io.push(s);
- break;
- /* Window */
- case "=":
- x = parseInt(matches[8], 10);
- y = parseInt(matches[9], 10);
- if (x == this.width && y == this.height) {
- break;
- }
- if (io.length > 0) {
- this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
- io = [];
- }
+ if (io.length > 0) {
this.addPacket({pos: this.pos,
- is_io: false,
- width: x,
- height: y});
- this.width = x;
- this.height = y;
+ is_io: true,
+ is_output: is_output,
+ io: io.join()});
+ io = [];
+ }
+ this.pos += x;
+ break;
+ /* Text or binary input */
+ case "<":
+ case "[":
+ x = parseInt(matches[(t == "<") ? 2 : 3], 10);
+ if (x == 0) {
break;
}
- }
-
- if (in_txt_pos < in_txt.length) {
- throw Error("extra input present");
- }
- if (out_txt_pos < out_txt.length) {
- throw Error("extra output present");
- }
-
- if (io.length > 0) {
+ if (io.length > 0 && is_output) {
+ this.addPacket({pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()});
+ io = [];
+ }
+ is_output = false;
+ /* Add (replacement) input characters */
+ s = in_txt.slice(in_txt_pos, in_txt_pos += x);
+ if (s.length != x) {
+ throw Error("timing entry out of input bounds");
+ }
+ io.push(s);
+ break;
+ /* Text or binary output */
+ case ">":
+ case "]":
+ x = parseInt(matches[(t == ">") ? 5 : 6], 10);
+ if (x == 0) {
+ break;
+ }
+ if (io.length > 0 && !is_output) {
+ this.addPacket({pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()});
+ io = [];
+ }
+ is_output = true;
+ /* Add (replacement) output characters */
+ s = out_txt.slice(out_txt_pos, out_txt_pos += x);
+ if (s.length != x) {
+ throw Error("timing entry out of output bounds");
+ }
+ io.push(s);
+ break;
+ /* Window */
+ case "=":
+ x = parseInt(matches[8], 10);
+ y = parseInt(matches[9], 10);
+ if (x == this.width && y == this.height) {
+ break;
+ }
+ if (io.length > 0) {
+ this.addPacket({pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()});
+ io = [];
+ }
this.addPacket({pos: this.pos,
- is_io: true,
- is_output: is_output,
- io: io.join()});
+ is_io: false,
+ width: x,
+ height: y});
+ this.width = x;
+ this.height = y;
+ break;
}
}
- /*
- * Parse packets out of a tlog message and add them to the buffer.
- */
- parseMessage(message) {
- let matches;
- let ver;
- let id;
- let pos;
-
- const number = Number();
- const string = String();
-
- /* Check version */
- ver = getValidField(message, "ver", string);
- matches = ver.match("^(\\d+)\\.(\\d+)$");
- if (matches === null || matches[1] > 2) {
- throw Error("\"ver\" field has invalid value: " + ver);
- }
-
- /* TODO Perhaps check host, rec, user, term, and session fields */
-
- /* Extract message ID */
- id = getValidField(message, "id", number);
- if (id <= this.id) {
- throw Error("out of order \"id\" field value: " + id);
- }
-
- /* Extract message time position */
- pos = getValidField(message, "pos", number);
- if (pos < this.message_pos) {
- throw Error("out of order \"pos\" field value: " + pos);
- }
-
- /* Update last received message ID and time position */
- this.id = id;
- this.pos = pos;
-
- /* Parse message data */
- this.parseMessageData(
- getValidField(message, "timing", string),
- getValidField(message, "in_txt", string),
- getValidField(message, "out_txt", string));
+ if (in_txt_pos < in_txt.length) {
+ throw Error("extra input present");
+ }
+ if (out_txt_pos < out_txt.length) {
+ throw Error("extra output present");
}
- /*
- * Handle journalctl "stream" event.
- */
- handleStream(entryList) {
- let i;
- let e;
- for (i = 0; i < entryList.length; i++) {
- e = entryList[i];
- /* If this is the second, "follow", run */
- if (this.done) {
- /* Skip the last entry we added on the first run */
- if (this.cursor !== null) {
- this.cursor = null;
- continue;
- }
- } else {
- if (!('__CURSOR' in e)) {
- this.handleError("No cursor in a Journal entry");
- }
- this.cursor = e['__CURSOR'];
+ if (io.length > 0) {
+ this.addPacket({pos: this.pos,
+ is_io: true,
+ is_output: is_output,
+ io: io.join()});
+ }
+ }
+
+ /*
+ * Parse packets out of a tlog message and add them to the buffer.
+ */
+ parseMessage(message) {
+ let matches;
+ let ver;
+ let id;
+ let pos;
+
+ const number = Number();
+ const string = String();
+
+ /* Check version */
+ ver = getValidField(message, "ver", string);
+ matches = ver.match("^(\\d+)\\.(\\d+)$");
+ if (matches === null || matches[1] > 2) {
+ throw Error("\"ver\" field has invalid value: " + ver);
+ }
+
+ /* TODO Perhaps check host, rec, user, term, and session fields */
+
+ /* Extract message ID */
+ id = getValidField(message, "id", number);
+ if (id <= this.id) {
+ throw Error("out of order \"id\" field value: " + id);
+ }
+
+ /* Extract message time position */
+ pos = getValidField(message, "pos", number);
+ if (pos < this.message_pos) {
+ throw Error("out of order \"pos\" field value: " + pos);
+ }
+
+ /* Update last received message ID and time position */
+ this.id = id;
+ this.pos = pos;
+
+ /* Parse message data */
+ this.parseMessageData(
+ getValidField(message, "timing", string),
+ getValidField(message, "in_txt", string),
+ getValidField(message, "out_txt", string));
+ }
+
+ /*
+ * Handle journalctl "stream" event.
+ */
+ handleStream(entryList) {
+ let i;
+ let e;
+ for (i = 0; i < entryList.length; i++) {
+ e = entryList[i];
+ /* If this is the second, "follow", run */
+ if (this.done) {
+ /* Skip the last entry we added on the first run */
+ if (this.cursor !== null) {
+ this.cursor = null;
+ continue;
}
- /* TODO Refer to entry number/cursor in errors */
- if (!('MESSAGE' in e)) {
- this.handleError("No message in Journal entry");
- }
- /* Parse the entry message */
- try {
- this.parseMessage(JSON.parse(e['MESSAGE']));
- } catch (error) {
- this.handleError(error);
- return;
+ } else {
+ if (!('__CURSOR' in e)) {
+ this.handleError("No cursor in a Journal entry");
}
+ this.cursor = e['__CURSOR'];
+ }
+ /* TODO Refer to entry number/cursor in errors */
+ if (!('MESSAGE' in e)) {
+ this.handleError("No message in Journal entry");
+ }
+ /* Parse the entry message */
+ try {
+ this.parseMessage(JSON.parse(e['MESSAGE']));
+ } catch (error) {
+ this.handleError(error);
+ return;
}
}
+ }
- /*
- * Handle journalctl "done" event.
- */
- handleDone() {
- this.done = true;
- this.journalctl.stop();
- /* Continue with the "following" run */
- this.journalctl = Journal.journalctl(
- this.matchList,
- {cursor: this.cursor,
- follow: true, count: "all"});
- this.journalctl.fail(this.handleError);
- this.journalctl.stream(this.handleStream);
- /* NOTE: no "done" handler on purpose */
+ /*
+ * Handle journalctl "done" event.
+ */
+ handleDone() {
+ this.done = true;
+ this.journalctl.stop();
+ /* Continue with the "following" run */
+ this.journalctl = Journal.journalctl(
+ this.matchList,
+ {cursor: this.cursor,
+ follow: true, count: "all"});
+ this.journalctl.fail(this.handleError);
+ this.journalctl.stream(this.handleStream);
+ /* NOTE: no "done" handler on purpose */
+ }
+};
+
+let ProgressBar = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.jumpTo = this.jumpTo.bind(this);
+ }
+
+ jumpTo(e) {
+ if (this.props.fastForwardFunc) {
+ let percent = parseInt((e.nativeEvent.offsetX * 100) / e.currentTarget.clientWidth);
+ let ts = parseInt((this.props.length * percent) / 100);
+ this.props.fastForwardFunc(ts);
}
- };
+ }
- let ProgressBar = class extends React.Component {
- constructor(props) {
- super(props);
- this.jumpTo = this.jumpTo.bind(this);
- }
+ render() {
+ let progress = {
+ "width": parseInt((this.props.mark * 100) / this.props.length) + "%"
+ };
- jumpTo(e) {
- if (this.props.fastForwardFunc) {
- let percent = parseInt((e.offsetX * 100) / e.currentTarget.clientWidth);
- let ts = parseInt((this.props.length * percent) / 100);
- this.props.fastForwardFunc(ts);
- }
- }
+ return (
+