Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: more informative app log handling in admin #1584

Merged
merged 5 commits into from
Apr 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 127 additions & 68 deletions packages/xarc-app-dev/lib/dev-admin/admin-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ const boxen = require("boxen");
const ck = require("chalker");
const chokidar = require("chokidar");
const readline = require("readline");
const { parse: parseLog } = require("./log-parser");
const WebpackDevRelay = require("./webpack-dev-relay");
const { parse } = require("./log-parser");
const { displayLogs } = require("./log-reader");
const { fork } = require("child_process");
const ConsoleIO = require("./console-io");
const logger = require("@xarc/app/lib/logger");
const xaa = require("xaa");
const { formUrl } = require("../utils");
const {
settings: { useDevProxy: DEV_PROXY_ENABLED }
settings: { useDevProxy: DEV_PROXY_ENABLED },
fullDevServer,
controlPaths
} = require("../../config/dev-proxy");

const APP_SERVER_NAME = "your app server";
Expand All @@ -40,13 +43,20 @@ class AdminServer {
this._saveWebpackReportData = undefined;
this._webpackDevRelay = new WebpackDevRelay();
this._servers = {};
//
// All output to terminal must be done through this IO.
// Any out-of-band writes to the terminal with process.stdout or console
// will mess up the in place progress display that log-update handles
//
this._io = (options && options.inputOutput) || new ConsoleIO();
this._shutdown = false;
this._fullAppLogUrl = formUrl({ ...fullDevServer, path: controlPaths.appLog });
}

async start() {
this._wds = ck`<gray.inverse>[wds]</> `;
this._proxy = ck`<green.inverse>[proxy]</> `;
this._app = ck`<cyan.inverse>[app]</> `;
this._io.setup();
this.handleUserInput();

Expand Down Expand Up @@ -74,7 +84,7 @@ class AdminServer {
<magenta>W</> - Restart <magenta>E</> - <cyan>inspect-brk</> mode <magenta>R</> - <cyan>inspect</> mode <magenta>X</> - Kill&nbsp;
<magenta>L</> - Show All Logs <magenta>0-6</> - Show Logs by Winston level
${proxyItem}<magenta>M</> - Show this menu <magenta>Q</> - Shutdown`;
this._io.show(boxen(menu, { margin: { left: 5 } }));
this._io.show("\n" + boxen(menu, { margin: { left: 5 } }));
}

getServer(name) {
Expand Down Expand Up @@ -134,6 +144,7 @@ class AdminServer {

async _quit() {
this._shutdown = true;
this._io.clearStatusMessage(true);
this._io.show(ck`<magenta>admin server exit, shutting down servers</magenta>`);
if (this._appWatcher) {
this._appWatcher.close();
Expand Down Expand Up @@ -169,6 +180,7 @@ class AdminServer {
e: () => this.startWebpackDevServer("--inspect-brk"),
r: () => this.startWebpackDevServer("--inspect"),
x: () => this.kill(DEV_SERVER_NAME, "SIGINT"),
z: () => this.toggleFullLogUrlMessage(APP_SERVER_NAME),
// dev proxy server
p: () => DEV_PROXY_ENABLED && this.sendMsg(PROXY_SERVER_NAME, { name: "restart" })
};
Expand Down Expand Up @@ -260,49 +272,34 @@ class AdminServer {
// start webpack dev server
//
async startWebpackDevServer(debug) {
let currentStatusMessage;
let hasStatusMessage = false;

const clearStatusMessage = out => {
if (hasStatusMessage) {
out.write("\x1b[2K\r");
hasStatusMessage = false;
}
};

const writeStatusMessage = out => {
if (!currentStatusMessage) return;
const l = out.columns;
const str = l ? currentStatusMessage.substr(0, l - 6) : currentStatusMessage;
const coloredStr = `\u001b[1m${str}\u001b[39m\u001b[22m`;
out.write(`\x1b[2K\r${this._wds}${coloredStr}`);
hasStatusMessage = true;
};

const progSig = `<s> [webpack.Progress] `;
const waitStart = async info => {
const cwdRegex = new RegExp(process.cwd(), "g");

let progLine = "";
const log = (out, data) => {
data
.toString()
.split("\n")
// kill empty blank lines but preserve spaces
.map(x => x.trim() && x)
.filter(x => x)
.forEach(l => {
if (l.startsWith(progSig)) {
currentStatusMessage = l.substring(progSig.length).replace(cwdRegex, ".");
writeStatusMessage(out);
.forEach(line => {
if (line.startsWith(progSig)) {
progLine = line.substring(progSig.length).replace(cwdRegex, ".");
out.writeStatusMessage(this._wds, progLine);
} else {
clearStatusMessage(out);
out.write(this._wds + l.replace(cwdRegex, ".") + "\n");
if (progLine) {
out.clearStatusMessage(this._wds);
progLine = "";
}
out.write(this._wds + line.replace(cwdRegex, ".") + "\n");
}
});
};

info._child.stdout.on("data", data => log(process.stdout, data));
info._child.stderr.on("data", data => log(process.stderr, data));
info._child.stdout.on("data", data => log(this._io, data));
info._child.stderr.on("data", data => log(this._io, data));

this._webpackDevRelay.setWebpackServer(info._child);

Expand Down Expand Up @@ -334,9 +331,99 @@ class AdminServer {
});
}

toggleFullLogUrlMessage(serverName) {
const server = this.getServer(serverName);
if (server && server.options.logSaver) {
const { options } = server;
const { logSaver } = options;
logSaver._toggle = !logSaver._toggle;
if (!logSaver._toggle) {
this._io.clearStatusMessage(options.tag);
} else {
this.showFullLogUrlMessage(options.tag, options.fullLogUrl);
}
}
}

showFullLogUrlMessage(tag, url) {
const time = new Date().toLocaleTimeString().replace(/ /g, "");
this._io.writeStatusMessage(
tag,
[
ck`${time} - <orange>There may be logs from your app server that requires your attention.</>`,
ck`<orange>View full logs at: <cyan.underline>${url}</></>`
],
true,
ck`<green>Press Z to hide or show this message.</>`
);
}

deferLogsOutput(context, showFullLink = true, delay = 999) {
const { tag, store } = context;

if (context._deferTimer) {
clearTimeout(context._deferTimer);
} else {
context._deferIx = store.length - 1;
}

if (!context._showFullLink) {
context._showFullLink = showFullLink;
}

context._deferTimer = setTimeout(() => {
context._deferTimer = undefined;
for (let ix = context._deferIx; ix < store.length; ix++) {
if (typeof store[ix] === "string") {
this._io.write(tag + store[ix] + "\n");
} else if (store[ix]) {
const json = store[ix];
this._io.write(tag + (json.msg || json.message || JSON.stringify(json)) + "\n");
}
}
if (context._showFullLink === true) {
context._toggle = true;
this.showFullLogUrlMessage(tag, context.fullLogUrl);
}
context._showFullLink = undefined;
if (store.length > 19999) {
context.store = store.slice(store.length - 9999);
}
}, delay);
}

saveLineOutput(context) {
const { inputs, store } = context;

const handler = data => {
let str = data.toString();
if (!str.trim()) {
store.push("");
logger.info("");
} else {
const entry = parseLog(str.trimRight());
store.push(entry.json || entry.message);
if (entry.show) {
this.deferLogsOutput(context, entry.show > 1);
}
logger[entry.level](str);
}
};

inputs.forEach(input => {
const reader = readline.createInterface({ input });
reader.on("line", handler);
return reader;
});
}

async startAppServer(debug) {
const skipWatch = debug === "--inspect-brk";

this._io.clearStatusMessage(this._app);

const logSaver = { tag: this._app, store: [], fullLogUrl: this._fullAppLogUrl };

await this.startServer({
name: APP_SERVER_NAME,
debug: debug || false,
Expand All @@ -346,45 +433,22 @@ class AdminServer {
skipWatch,
noTimeoutCheck: skipWatch,
passThruArgs: this._passThru,
logSaver,
waitStart: async info => {
const readStdout = readline.createInterface({
input: info._child.stdout
});

readStdout.on("line", data => {
const { level, message } = parse(data.toString().trim());
logger[level](message);
});

const readStderr = readline.createInterface({
input: info._child.stderr
});

readStderr.on("line", data => {
const { level, message } = parse(data.toString().trim());
logger[level](message);
});

logSaver.inputs = [info._child.stdout, info._child.stderr];
this.saveLineOutput(logSaver);
await this.waitForAppServerStart(info);
this._webpackDevRelay.setAppServer(info._child);
}
});
}

writeMultiLine(tag, data, out) {
const lines = data
.toString()
.replace(/\r/g, "")
.split("\n");

const last = lines.length - 1;
lines.forEach((l, ix) => {
if (ix < last) {
out.write(tag + l + "\n");
} else if (l) {
out.write(tag + l);
}
passThruLineOutput(tag, input, output) {
const reader = readline.createInterface({ input });
reader.on("line", data => {
output.write(tag + data.toString() + "\n");
});
return reader;
}

async startProxyServer(debug) {
Expand All @@ -394,13 +458,8 @@ class AdminServer {
debug,
exec: Path.join(__dirname, "redbird-spawn"),
waitStart: async info => {
info._child.stdout.on("data", data => {
this.writeMultiLine(this._proxy, data, process.stdout);
});

info._child.stderr.on("data", data => {
this.writeMultiLine(this._proxy, data, process.stderr);
});
this.passThruLineOutput(this._proxy, info._child.stdout, process.stdout);
this.passThruLineOutput(this._proxy, info._child.stderr, process.stderr);
}
});
}
Expand Down
59 changes: 58 additions & 1 deletion packages/xarc-app-dev/lib/dev-admin/console-io.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
/* eslint-disable no-console, no-process-exit */

const readline = require("readline");
const logUpdate = require("log-update");

class ConsoleIO {
constructor() {}
constructor() {
this._out = process.stdout;
this.currentStatus = {};
}

setup() {
readline.emitKeypressEvents(process.stdin);
Expand All @@ -26,10 +30,63 @@ class ConsoleIO {
}

show(...args) {
this.clearStatusMessage();
console.log(...args);
this.writeStatusMessage();
}

write(str) {
this.clearStatusMessage();
this._out.write(str);
this.writeStatusMessage();
}

clearStatusMessage(resetTag) {
const { showing, preserve } = this.currentStatus;
if (showing) {
if (resetTag && resetTag !== true && resetTag !== this.currentStatus.tag) {
this.preserveStatusMsg();
}
logUpdate.clear();
this.currentStatus.showing = false;
}
if (resetTag) {
this.currentStatus = {};
}
}

preserveStatusMsg() {
if (this.currentStatus.msg && this.currentStatus.preserve) {
const { tag, msg } = this.currentStatus;
this.showStatusMessage(tag, msg);
logUpdate.done();
}
}

showStatusMessage(tag, msg, clearMsg = "") {
const lineTxt = []
.concat(msg, clearMsg)
.filter(x => x)
.map(line => `${tag}${line}`);
logUpdate(lineTxt.join("\n"));
this.currentStatus.showing = true;
}

writeStatusMessage(tag, msg, preserve = false, clearMsg = "") {
if (tag && msg) {
if (this.currentStatus.tag && this.currentStatus.tag !== tag) {
this.preserveStatusMsg();
}
this.currentStatus = { tag, msg, preserve, clearMsg };
}
if (!this.currentStatus.msg) return;

const { tag: xtag, msg: xmsg } = this.currentStatus;
this.showStatusMessage(xtag, xmsg, clearMsg);
}

exit() {
this._out.write("\n");
process.exit();
}
}
Expand Down
Loading