UNPKG

stmux

Version:

Simple Terminal Multiplexing for Node Environments

180 lines (164 loc) 7.63 kB
/* ** stmux -- Simple Terminal Multiplexing for Node Environments ** Copyright (c) 2017-2024 Dr. Ralf S. Engelschall <rse@engelschall.com> ** ** Permission is hereby granted, free of charge, to any person obtaining ** a copy of this software and associated documentation files (the ** "Software"), to deal in the Software without restriction, including ** without limitation the rights to use, copy, modify, merge, publish, ** distribute, sublicense, and/or sell copies of the Software, and to ** permit persons to whom the Software is furnished to do so, subject to ** the following conditions: ** ** The above copyright notice and this permission notice shall be included ** in all copies or substantial portions of the Software. ** ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import notifier from "node-notifier" import stripAnsi from "strip-ansi" export default class stmuxErrors { handleErrors () { /* determine error patterns */ const parseErrorPatterns = (patterns) => { const result = [] while (patterns) { let [ , pattern, rest ] = patterns.match(/^((?:\\,|.)+?)(?:,(.+))?$/) const m = pattern.match(/^!(.+)$/) let negate = false if (m) { negate = true pattern = m[1] } result.push({ negate, regexp: new RegExp(pattern) }) patterns = rest } return result } const globalErrorPatterns = parseErrorPatterns(this.argv.error) this.terms.forEach((term) => { const patterns = term.node.get("error") term.stmuxErrorPatterns = patterns ? parseErrorPatterns(patterns) : [] term.stmuxError = false }) /* handle error detection */ let notifyLocked = false const notifyStateOld = [] const notifyStateNew = [] this.terms.forEach((term) => { notifyStateOld[term.stmuxNumber - 1] = "" notifyStateNew[term.stmuxNumber - 1] = "" }) setInterval(() => { let dirty = false const notify = [] this.terms.forEach((term) => { /* act only if an update exists */ if (!term.stmuxUpdate) return term.stmuxUpdate = false /* short-circuit processing */ if ( globalErrorPatterns.length === 0 && term.stmuxErrorPatterns.length === 0) return /* take screenshot */ let screenshot = term.screenshot() screenshot = stripAnsi(screenshot) const lines = screenshot.split(/\r?\n/) /* match errors in screenshot */ const matches = (string, patterns) => { for (let i = 0; i < patterns.length; i++) { const matched = patterns[i].regexp.test(string) if (!( ( matched && !patterns[i].negate) || (!matched && patterns[i].negate))) return false } return true } term.stmuxError = false for (let i = 0; i < lines.length; i++) { if ( (globalErrorPatterns.length > 0 && matches(lines[i], globalErrorPatterns)) || (term.stmuxErrorPatterns.length > 0 && matches(lines[i], term.stmuxErrorPatterns))) { term.stmuxError = true break } } /* record notification state */ if (term.stmuxError) { notifyStateNew[term.stmuxNumber - 1] = screenshot notify.push(term) } else notifyStateNew[term.stmuxNumber - 1] = "" /* determine and record screen updates */ if (term.stmuxError && term.style.border.fg === "default") { term.style.border.fg = "red" dirty = true } else if (!term.stmuxError && term.style.border.fg === "red") { term.style.border.fg = "default" dirty = true } if (term.stmuxError && term.style.focus.border.fg === "green") { term.style.border.fg = "red" dirty = true } else if (!term.stmuxError && term.style.focus.border.fg === "red") { term.style.border.fg = "green" dirty = true } if (term.stmuxError && !term.stmuxTitle.match(/-\[ERROR\]/)) { this.setTerminalTitle(term) dirty = true } else if (!term.stmuxError && term.stmuxTitle.match(/-\[ERROR\]/)) { this.setTerminalTitle(term) dirty = true } }) /* update screen */ if (dirty) this.screen.render() /* raise notification */ if (this.argv.method !== "" && notify.length > 0 && !notifyLocked) { const stateOld = notifyStateOld.reduce((a, v) => a + v, "") const stateNew = notifyStateNew.reduce((a, v) => a + v, "") if (stateOld !== stateNew) { /* determine message */ const notifyMsg = `${this.my.name}: ERROR situation${notify.length > 1 ? "s" : ""} ` + `detected in terminal${notify.length > 1 ? "s" : ""} ` + notify.map((term) => "#" + term.stmuxNumber).join(", ") /* determine method(s) */ const methods = {} this.argv.method.split(",").forEach((method) => { methods[method] = true }) /* send notification(s) */ if (methods.beep) this.screen.program.output.write("\x07") if (methods.system) { notifier.notify({ title: `${this.my.name}: Detected new ERROR situation${notify.length > 1 ? "s" : ""}`, message: notifyMsg, wait: false }) } /* swap notification state */ this.terms.forEach((term) => { notifyStateOld[term.stmuxNumber - 1] = notifyStateNew[term.stmuxNumber - 1] notifyStateNew[term.stmuxNumber - 1] = "" }) /* lock notification for some time to not bug the user too much */ notifyLocked = true setTimeout(() => { notifyLocked = false }, 5 * 1000) } } }, 500) } }