Skip to content

Commit

Permalink
Use XTerm.js buffer to display container logs
Browse files Browse the repository at this point in the history
  • Loading branch information
astefanutti committed Nov 19, 2020
1 parent c4f6778 commit 6744e05
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 59 deletions.
65 changes: 37 additions & 28 deletions lib/ui/blessed-xterm/blessed-xterm.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,19 @@ class XTerm extends blessed.ScrollableBox {

// This code executes in the jsdom global scope
this.term = new Terminal({
convertEol: true,
cols: this.width - this.iwidth - 1,
rows: this.height - this.iheight,
scrollback: this.options.scrollback !== 'none'
? this.options.scrollback
: this.height - this.iheight,
scrollback: this.options.scrollback !== 'none' ? this.options.scrollback : this.height - this.iheight,
});

// monkey-patch XTerm to prevent it from effectively rendering
// anything to the Virtual DOM, as we just grab its character buffer.
// The alternative would be to listen on the XTerm 'refresh' event,
// but this way XTerm would uselessly render the DOM elements.
this.term._core.refresh = (start, end) => {
this.screen.render();
// repositions the label given scrolling
if (this._label) {
if (!this.screen.autoPadding) {
this._label.rtop = this.childBase || 0;
} else {
this._label.rtop = (this.childBase || 0) - this.itop;
}
if (!this.detached) {
this.screen.render();
}
}

Expand All @@ -78,6 +71,8 @@ class XTerm extends blessed.ScrollableBox {
const resize = () => this.term.resize(this.width - this.iwidth - 1, this.height - this.iheight);
// pass-through Blessed resize events to XTerm
this.on('resize', debounce(resize, 150, { trailing: true }));
// in case the terminal was resized while being detached
this.on('attach', resize);
// perform an initial resizing once
this.once('render', resize);

Expand Down Expand Up @@ -148,6 +143,17 @@ class XTerm extends blessed.ScrollableBox {
return 'terminal';
}

clear({ firstLine } = { firstLine: false }) {
this.clearSelection();
this.term.clear();
if (firstLine) {
// work-around the above method call that just returns when there is only one line
const buffer = this.term.buffer._buffer;
buffer.lines.set(0, buffer.getBlankLine());
buffer.x = 0;
}
}

write(data) {
this.term.write(data);
}
Expand All @@ -156,6 +162,10 @@ class XTerm extends blessed.ScrollableBox {
this.term.writeln(data);
}

writeSync(data) {
this.term._core._writeBuffer.writeSync(data);
}

hasSelection() {
return !!this._selection;
}
Expand Down Expand Up @@ -214,6 +224,15 @@ class XTerm extends blessed.ScrollableBox {
}

render() {
// repositions the label given scrolling
if (this._label) {
if (!this.screen.autoPadding) {
this._label.rtop = this.childBase || 0;
} else {
this._label.rtop = (this.childBase || 0) - this.itop;
}
}

// call the underlying Element's rendering function
let ret = this._render();
if (!ret) return;
Expand All @@ -229,14 +248,6 @@ class XTerm extends blessed.ScrollableBox {
const yl = ret.yl - this.ibottom;

// selection
if (this._selection) {
if (this._selection.m1 === undefined) {
this._selection.m1 = {};
}
if (this._selection.m2 === undefined) {
this._selection.m2 = {};
}
}
let { x1: xs1, x2: xs2, m1: { line: ys1 }, m2: { line: ys2 } } = this._selection || { m1: {}, m2: {} };
if (this._selection) {
// back to wrapped lines coordinates
Expand All @@ -254,7 +265,9 @@ class XTerm extends blessed.ScrollableBox {
ys2 += yi - ydisp;
}

let cursor;
const cursor = this.screen.focused === this && ydisp === buffer.baseY && !this.term.cursorHidden;
const cursorY = yi + buffer.cursorY;

// iterate over all lines
for (let y = Math.max(yi, 0); y < yl; y++) {
// fetch Blessed Screen and XTerm lines
Expand All @@ -264,13 +277,9 @@ class XTerm extends blessed.ScrollableBox {
break;

// determine cursor column position
if (y === yi + buffer.cursorY
&& this.screen.focused === this
&& (ydisp === buffer.baseY)
&& !this.term.cursorHidden) {
cursor = xi + buffer.cursorX;
} else {
cursor = -1;
let cursorX = -1;
if (cursor && y === cursorY) {
cursorX = xi + buffer.cursorX;
}

// iterate over all columns
Expand All @@ -284,7 +293,7 @@ class XTerm extends blessed.ScrollableBox {
let x1 = cell[1] || ' ';

// handle cursor
if (x === cursor) {
if (x === cursorX) {
if (this.blinking) {
if (this.options.cursorType === 'line') {
x0 = this.dattr;
Expand Down
6 changes: 5 additions & 1 deletion lib/ui/blessed/patches.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ blessed.with = function (...fns) {
return function (...args) {
const el = Reflect.apply(target[method], target, args);
return fns.reduce((e, fn) => fn.call(null, e) || e, el);
}
};
}
})
};

blessed.element.prototype.with = function (...fns) {
return fns.reduce((e, fn) => fn.call(null, e) || e, this);
};
64 changes: 36 additions & 28 deletions lib/ui/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const { pause } = require('../promise');
const { focus: { focusIndicator }, setLabel, spinner: { until } } = require('./ui');
const { scroll, throttle } = require('./blessed/scroll');

const XTerm = require('./blessed-xterm/blessed-xterm');

const statsPollRateMs = 10000;

class Dashboard extends EventEmitter {
Expand Down Expand Up @@ -133,7 +135,7 @@ class Dashboard extends EventEmitter {
const graphs = [memory_graph, cpu_graph, net_graph, fs_graph];
graphs.slice(1).forEach(g => g.toggle());

const pod_log = blessed.with(focusIndicator, scroll, throttle).log({
const pod_log = new XTerm({
parent : screen,
label : 'Logs',
top : '50%',
Expand All @@ -146,20 +148,21 @@ class Dashboard extends EventEmitter {
style : {
label : { bold: true },
},
scrollable : true,
scrollbar : {
scrollbar : {
ch : ' ',
style : { bg: 'white' },
track : {
style : { bg: 'grey' },
},
},
});
}).with(focusIndicator, scroll, throttle);

pod_log.reset = function () {
this.setLabel('Logs');
this.clear();
}
this.clear({ firstLine: true });
this.setScrollPerc(0);
this._userScrolled = false;
};

pods_table.key(['e'], () => {
// no selection
Expand Down Expand Up @@ -323,38 +326,43 @@ class Dashboard extends EventEmitter {

function selectContainer(pod, container) {
const namespace = pod.metadata.namespace;
let tail = [];
const logger = function* (sinceTime) {
let data, timestamp;
const lines = [];
let data, timestamp, retry = !!sinceTime;
const chunks = [];
const log = debounce(() => {
pod_log.log(lines);
lines.length = 0;
pod_log.writeSync(chunks.join(''));
chunks.length = 0;
}, 100, { trailing: true });
cancellations.add('dashboard.pod.logs', () => log.cancel());
try {
let line = '';
while (data = yield) {
// an initial ping frame with 0-length data is being sent
if (data.length === 0) continue;

data = data.toString('utf8');
let l = data;
// the current logic is affected by https:/kubernetes/kubernetes/issues/77603
if (line === '') {
const i = data.indexOf(' ');
timestamp = data.substring(0, i);
l = data.substring(i + 1);
const i = data.indexOf(' ');
timestamp = data.substring(0, i);

// maintain a cache for the lastest timestamp to de-duplicate entries on retry,
// as sub-second info from the 'sinceTime' parameter are not taken into account
const ts = timestamp.substring(0, timestamp.indexOf('.'));
if (retry) {
if (ts === sinceTime) {
if (tail.includes(data)) continue;
} else {
retry = false;
}
}
const n = l.indexOf('\n');
if (n < 0) {
line += l;
} else {
line += l.substring(0, n);
// avoid scanning the whole buffer if the timestamp differs from the since time
if (timestamp.startsWith(sinceTime) && pod_log._clines.fake.includes(line)) continue;
lines.push(line);
line = '';
log();
const j = tail.findIndex(l => !l.startsWith(ts));
if (j > 0) {
tail.length = j;
}
tail.unshift(data);

const chunk = data.substring(i + 1);
chunks.push(chunk);
log();
}
} catch (e) {
// HTTP chunked transfer-encoding / streaming requests abort on timeout instead of being ended.
Expand Down Expand Up @@ -396,7 +404,7 @@ class Dashboard extends EventEmitter {
.cancel(c => cancellations.add('dashboard.pod.logs', c))
.catch(error => {
pod_log.setLabel(`Logs {grey-fg}[${container.name}]{/grey-fg}`);
pod_log.log(`\x1b[31mError: ${error.message}\x1b[m`);
pod_log.writeSync(`\x1b[31mError: ${error.message}\x1b[m\n`);
screen.render();
return Promise.reject();
})
Expand Down
2 changes: 0 additions & 2 deletions lib/ui/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ class Exec extends Duplex {
terminal.term.onResize(sendResize);
// In case the terminal was resized while the connection was opening
terminal.once('render', resize);
// In case the terminal was resized while being blurred
terminal.on('focus', resize);

const onScreenInput = function (data) {
if (screen.focused !== terminal) return;
Expand Down

0 comments on commit 6744e05

Please sign in to comment.