logs: handle line feed and always use streaming

remove the use of http server sent events, just use the streaming api

handle line feeds by rewriting the current line. This isn't perfect since it
won't handle subtle cases where the colors are change between lines but will
work in all standard cases

Now commands that writes a progess will work correctly without adding a new line
every time and users will see the progress in realtime.
This commit is contained in:
Simone Gotti 2019-05-19 14:17:12 +02:00
parent da8e3b9b5c
commit 449096c0a3
1 changed files with 52 additions and 43 deletions

View File

@ -4,12 +4,15 @@
<div class="stream-line" v-for="(item, index) in items" :key="index"> <div class="stream-line" v-for="(item, index) in items" :key="index">
<div v-html="item"/> <div v-html="item"/>
</div> </div>
<div v-if="lastitem" class="stream-line">
<div v-html="lastitem"/>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { apiurl, apiurlwithtoken, fetch } from "@/util/auth"; import { apiurl, fetch } from "@/util/auth";
import AnsiUp from "ansi_up"; import AnsiUp from "ansi_up";
export default { export default {
@ -29,6 +32,7 @@ export default {
return { return {
items: [], items: [],
lastitem: "",
lines: [], lines: [],
formatter: formatter, formatter: formatter,
es: null, es: null,
@ -40,61 +44,71 @@ export default {
if (this.fetching) { if (this.fetching) {
return; return;
} }
this.fetching = true;
let follow = false;
if (this.stepphase == "running") { if (this.stepphase == "running") {
this.streamLogs(); follow = true;
} }
if (this.stepphase == "success" || this.stepphase == "failed") { this.getLogs(follow);
this.getLogs();
}
}, },
streamLogs() { async getLogs(follow) {
this.items = [];
let path = "/logs?runID=" + this.runid + "&taskID=" + this.taskid; let path = "/logs?runID=" + this.runid + "&taskID=" + this.taskid;
if (this.setup) { if (this.setup) {
path += "&setup"; path += "&setup";
} else { } else {
path += "&step=" + this.step; path += "&step=" + this.step;
} }
path += "&follow&stream"; if (follow) {
path += "&follow";
this.es = new EventSource(apiurlwithtoken(path));
this.es.onmessage = event => {
var data = event.data;
// TODO(sgotti) ansi_up doesn't handle carriage return (\r), find a way to also handle it
this.items.push(this.formatter.ansi_to_html(data));
};
// don't reconnect on error
this.es.onerror = () => {
this.es.close();
};
},
async getLogs() {
let path = "/logs?runID=" + this.runid + "&taskID=" + this.taskid;
if (this.setup) {
path += "&setup";
} else {
path += "&step=" + this.step;
} }
let res = await fetch(apiurl(path)); let res = await fetch(apiurl(path));
if (res.status == 200) { if (res.status == 200) {
const reader = res.body.getReader(); const reader = res.body.getReader();
let items = this.items; let lastline = "";
let formatter = this.formatter; let j = 0;
for (;;) { for (;;) {
let { done, value } = await reader.read(); let { done, value } = await reader.read();
if (done) { if (done) {
return; return;
} }
let data = new TextDecoder("utf-8").decode(value); let data = new TextDecoder("utf-8").decode(value, { stream: true });
let lines = data.split("\n"); let part = "";
lines.forEach(line => { for (var i = 0; i < data.length; i++) {
items.push(formatter.ansi_to_html(line)); let c = data.charAt(i);
}); if (c == "\r") {
// replace lastline from start, simulating line feed (go to start of line)
// this isn't perfect since the previous line contents could have
// been written using different colors and this will lose them but
// in practically all cases this won't happen
lastline =
lastline.slice(0, j) + part + lastline.slice(j + part.length);
j = 0;
this.lastitem = this.formatter.ansi_to_html(lastline);
part = "";
} else if (c == "\n") {
lastline =
lastline.slice(0, j) + part + lastline.slice(j + part.length);
j += part.length;
this.lastitem = this.formatter.ansi_to_html(lastline);
this.items.push(this.lastitem);
this.lastitem = "";
lastline = "";
j = 0;
part = "";
} else {
part += c;
}
}
lastline =
lastline.slice(0, j) + part + lastline.slice(j + part.length);
j += part.length;
this.lastitem = this.formatter.ansi_to_html(lastline);
} }
} }
} }
@ -107,14 +121,9 @@ export default {
}, },
stepphase: function(post, pre) { stepphase: function(post, pre) {
if (pre == "notstarted" && post == "running") { if (pre == "notstarted" && post == "running") {
this.streamLogs(); this.getLogs(true);
} } else {
if (pre == "notstarted" && (post == "success" || post == "failed")) { this.getLogs(false);
this.getLogs();
}
if (pre == "running" && (post == "success" || post == "failed")) {
// TODO(sgotti)
} }
} }
}, },
@ -135,7 +144,7 @@ export default {
.log { .log {
background-color: #222; background-color: #222;
color: #f1f1f1; color: #f1f1f1;
font-family: Cousine, monospace; font-family: Cousine, monospace, "Noto Color Emoji";
font-size: 12px; font-size: 12px;
line-height: 19px; line-height: 19px;
white-space: pre-wrap; white-space: pre-wrap;