diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index b32b4be7..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "runtimeExecutable": "/home/nuskooler/.local/share/rtx/installs/nodejs/16.20.2/bin/node", - "request": "launch", - "name": "Launch Program", - "skipFiles": ["/**"], - "program": "${workspaceFolder}/main.js" - } - ] -} diff --git a/core/ansi_escape_parser.js b/core/ansi_escape_parser.js index 21636e21..907f1a19 100644 --- a/core/ansi_escape_parser.js +++ b/core/ansi_escape_parser.js @@ -24,7 +24,7 @@ function ANSIEscapeParser(options) { this.graphicRendition = {}; this.parseState = { - re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsutEFGST])/g, // eslint-disable-line no-control-regex + re: /(?:\x1b)(?:(?:\x5b([?=;0-9]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex }; options = miscUtil.valueWithDefault(options, { @@ -77,10 +77,24 @@ function ANSIEscapeParser(options) { self.clearScreen = function () { self.column = 1; self.row = 1; + self.positionUpdated(); self.emit('clear screen'); }; self.positionUpdated = function () { + if (self.row > self.termHeight) { + if (this.savedPosition) { + this.savedPosition.row -= self.row - self.termHeight; + } + self.emit('scroll', self.row - self.termHeight); + self.row = self.termHeight; + } else if (self.row < 1) { + if (this.savedPosition) { + this.savedPosition.row -= self.row - 1; + } + self.emit('scroll', -(self.row - 1)); + self.row = 1; + } self.emit('position update', self.row, self.column); }; @@ -231,7 +245,7 @@ function ANSIEscapeParser(options) { self.parseState = { // ignore anything past EOF marker, if any buffer: input.split(String.fromCharCode(0x1a), 1)[0], - re: /(?:\x1b\x5b)([?=;0-9]*?)([ABCDHJKfhlmnpsutEFGST])/g, // eslint-disable-line no-control-regex + re: /(?:\x1b)(?:(?:\x5b([?=;0-9]*?)([ABCDEFGfHJKLmMsSTuUYZt@PXhlnpt]))|([78DEHM]))/g, // eslint-disable-line no-control-regex stop: false, }; }; @@ -271,9 +285,46 @@ function ANSIEscapeParser(options) { opCode = match[2]; args = match[1].split(';').map(v => parseInt(v, 10)); // convert to array of ints - escape(opCode, args); + // Handle the case where there is no bracket + if (!_.isNil(match[3])) { + opCode = match[3]; + args = []; + // no bracket + switch (opCode) { + // save cursor position + case '7': + escape('s', args); + break; + // restore cursor position + case '8': + escape('u', args); + break; + + // scroll up + case 'D': + escape('S', args); + break; + + // move to next line + case 'E': + // functonality is the same as ESC [ E + escape(opCode, args); + break; + + // create a tab at current cursor position + case 'H': + literal('\t'); + break; + + // scroll down + case 'M': + escape('T', args); + break; + } + } else { + escape(opCode, args); + } - //self.emit('chunk', match[0]); self.emit('control', match[0], opCode, args); } } while (0 !== re.lastIndex); @@ -281,8 +332,8 @@ function ANSIEscapeParser(options) { if (pos < buffer.length) { var lastBit = buffer.slice(pos); - // :TODO: check for various ending LF's, not just DOS \r\n - if ('\r\n' === lastBit.slice(-2).toString()) { + // handles either \r\n or \n + if ('\n' === lastBit.slice(-1).toString()) { switch (self.trailingLF) { case 'default': // @@ -290,14 +341,14 @@ function ANSIEscapeParser(options) { // if we're going to end on termHeight // if (this.termHeight === self.row) { - lastBit = lastBit.slice(0, -2); + lastBit = lastBit.slice(0, -1); } break; case 'omit': case 'no': case false: - lastBit = lastBit.slice(0, -2); + lastBit = lastBit.slice(0, -1); break; } } @@ -308,48 +359,6 @@ function ANSIEscapeParser(options) { self.emit('complete'); }; - /* - self.parse = function(buffer, savedRe) { - // :TODO: ensure this conforms to ANSI-BBS / CTerm / bansi.txt for movement/etc. - // :TODO: move this to "constants" section @ top - var re = /(?:\x1b\x5b)([\?=;0-9]*?)([ABCDHJKfhlmnpsu])/g; - var pos = 0; - var match; - var opCode; - var args; - - // ignore anything past EOF marker, if any - buffer = buffer.split(String.fromCharCode(0x1a), 1)[0]; - - do { - pos = re.lastIndex; - match = re.exec(buffer); - - if(null !== match) { - if(match.index > pos) { - parseMCI(buffer.slice(pos, match.index)); - } - - opCode = match[2]; - args = getArgArray(match[1].split(';')); - - escape(opCode, args); - - self.emit('chunk', match[0]); - } - - - - } while(0 !== re.lastIndex); - - if(pos < buffer.length) { - parseMCI(buffer.slice(pos)); - } - - self.emit('complete'); - }; - */ - function escape(opCode, args) { let arg; @@ -382,6 +391,35 @@ function ANSIEscapeParser(options) { self.moveCursor(-arg, 0); break; + // line feed + case 'E': + arg = isNaN(args[0]) ? 1 : args[0]; + if (this.row + arg > this.termHeight) { + this.emit('scroll', arg - (this.termHeight - this.row)); + self.moveCursor(0, this.termHeight); + } else { + self.moveCursor(0, arg); + } + break; + + // reverse line feed + case 'F': + arg = isNaN(args[0]) ? 1 : args[0]; + if (this.row - arg < 1) { + this.emit('scroll', -(arg - this.row)); + self.moveCursor(0, 1 - this.row); + } else { + self.moveCursor(0, -arg); + } + break; + + // absolute horizontal cursor position + case 'G': + arg = isNaN(args[0]) ? 1 : args[0]; + self.column = Math.max(1, arg); + self.positionUpdated(); + break; + case 'f': // horiz & vertical case 'H': // cursor position //self.row = args[0] || 1; @@ -392,14 +430,32 @@ function ANSIEscapeParser(options) { self.positionUpdated(); break; - // save position - case 's': - self.saveCursorPosition(); + // erase display/screen + case 'J': + if (isNaN(args[0]) || 0 === args[0]) { + self.emit('erase rows', self.row, self.termHeight); + } else if (1 === args[0]) { + self.emit('erase rows', 1, self.row); + } else if (2 === args[0]) { + self.clearScreen(); + } break; - // restore position - case 'u': - self.restoreCursorPosition(); + // erase text in line + case 'K': + if (isNaN(args[0]) || 0 === args[0]) { + self.emit('erase columns', self.row, self.column, self.termWidth); + } else if (1 === args[0]) { + self.emit('erase columns', self.row, 1, self.column); + } else if (2 === args[0]) { + self.emit('erase columns', self.row, 1, self.termWidth); + } + break; + + // insert line + case 'L': + arg = isNaN(args[0]) ? 1 : args[0]; + self.emit('insert line', self.row, arg); break; // set graphic rendition @@ -471,14 +527,50 @@ function ANSIEscapeParser(options) { self.emit('sgr update', self.graphicRendition); break; // m - // :TODO: s, u, K + // save position + case 's': + self.saveCursorPosition(); + break; - // erase display/screen - case 'J': - // :TODO: Handle other 'J' types! - if (2 === args[0]) { - self.clearScreen(); - } + // Scroll up + case 'S': + arg = isNaN(args[0]) ? 1 : args[0]; + self.emit('scroll', arg); + break; + + // Scroll down + case 'T': + arg = isNaN(args[0]) ? 1 : args[0]; + self.emit('scroll', -arg); + break; + + // restore position + case 'u': + self.restoreCursorPosition(); + break; + + // clear + case 'U': + self.clearScreen(); + break; + + // delete line + // TODO: how should we handle 'M'? + case 'Y': + arg = isNaN(args[0]) ? 1 : args[0]; + self.emit('delete line', self.row, arg); + break; + + // back tab + case 'Z': + // calculate previous tabstop + self.column = Math.max(1, self.column - (self.column % 8 || 8)); + self.positionUpdated(); + break; + case '@': + // insert column(s) + arg = isNaN(args[0]) ? 1 : args[0]; + self.emit('insert columns', self.row, self.column, arg); break; } } diff --git a/core/archive_util.js b/core/archive_util.js index a142584b..9ffb94cc 100644 --- a/core/archive_util.js +++ b/core/archive_util.js @@ -208,17 +208,17 @@ module.exports = class ArchiveUtil { // pty.js doesn't currently give us a error when things fail, // so we have this horrible, horrible hack: let err; - proc.once('data', d => { + proc.onData(d => { if (_.isString(d) && d.startsWith('execvp(3) failed.')) { err = Errors.ExternalProcess(`${action} failed: ${d.trim()}`); } }); - proc.once('exit', exitCode => { + proc.onExit(exitEvent => { return cb( - exitCode + exitEvent.exitCode ? Errors.ExternalProcess( - `${action} failed with exit code: ${exitCode}` + `${action} failed with exit code: ${exitEvent.exitCode}` ) : err ); @@ -358,10 +358,12 @@ module.exports = class ArchiveUtil { output += data; }); - proc.once('exit', exitCode => { - if (exitCode) { + proc.onExit(exitEvent => { + if (exitEvent.exitCode) { return cb( - Errors.ExternalProcess(`List failed with exit code: ${exitCode}`) + Errors.ExternalProcess( + `List failed with exit code: ${exitEvent.exitCode}` + ) ); } diff --git a/core/art.js b/core/art.js index f99373b4..b0897ee5 100644 --- a/core/art.js +++ b/core/art.js @@ -316,6 +316,74 @@ function display(client, art, options, cb) { } }); + // Remove any MCI's that are in erased rows + ansiParser.on('erase row', (startRow, endRow) => { + _.forEach(mciMap, (mciInfo, mapKey) => { + if (mciInfo.position[0] >= startRow && mciInfo.position[0] <= endRow) { + delete mciMap[mapKey]; + } + }); + }); + + // Remove any MCI's that are in erased columns + ansiParser.on('erase columns', (row, startCol, endCol) => { + _.forEach(mciMap, (mciInfo, mapKey) => { + if ( + mciInfo.position[0] === row && + mciInfo.position[1] >= startCol && + mciInfo.position[1] <= endCol + ) { + delete mciMap[mapKey]; + } + }); + }); + + ansiParser.on('insert columns', (row, startCol, numCols) => { + _.forEach(mciMap, (mciInfo, mapKey) => { + if (mciInfo.position[0] === row && mciInfo.position[1] >= startCol) { + mciInfo.position[1] += numCols; + if (mciInfo.position[1] > client.term.termWidth) { + delete mciMap[mapKey]; + } + } + }); + }); + + // Clear the screen, removing any MCI's + ansiParser.on('clear screen', () => { + _.forEach(mciMap, (mciInfo, mapKey) => { + delete mciMap[mapKey]; + }); + }); + + ansiParser.on('scroll', scrollY => { + _.forEach(mciMap, mciInfo => { + mciInfo.position[0] -= scrollY; + }); + }); + + ansiParser.on('insert line', (row, numLines) => { + _.forEach(mciMap, mciInfo => { + if (mciInfo.position[0] >= row) { + mciInfo.position[0] += numLines; + } + }); + }); + + ansiParser.on('delete line', (row, numLines) => { + _.forEach(mciMap, (mciInfo, mapKey) => { + if (mciInfo.position[0] >= row) { + if (mciInfo.position[0] < row + numLines) { + // unlike scrolling, the rows are actually gone, + // so we need to delete any MCI's that are in them + delete mciMap[mapKey]; + } else { + mciInfo.position[0] -= numLines; + } + } + }); + }); + ansiParser.on('literal', literal => client.term.write(literal, false)); ansiParser.on('control', control => client.term.rawWrite(control)); diff --git a/core/door.js b/core/door.js index 8574cf39..0aae5da1 100644 --- a/core/door.js +++ b/core/door.js @@ -115,9 +115,10 @@ module.exports = class Door { spawnOptions ); - prePty.once('exit', exitCode => { + prePty.onExit(exitEvent => { + const { exitCode, signal } = exitEvent; this.client.log.info( - { exitCode: exitCode }, + { exitCode, signal }, 'Door pre-command exited' ); return callback(null); @@ -167,7 +168,7 @@ module.exports = class Door { this.doorPty.onData(this.doorDataHandler.bind(this)); - this.doorPty.once('close', () => { + this.doorPty.onExit((/*exitEvent*/) => { return this.restoreIo(this.doorPty); }); } else if ('socket' === this.io) { @@ -180,8 +181,9 @@ module.exports = class Door { ); } - this.doorPty.once('exit', exitCode => { - this.client.log.info({ exitCode: exitCode }, 'Door exited'); + this.doorPty.onExit(exitEvent => { + const { exitCode, signal } = exitEvent; + this.client.log.info({ exitCode, signal }, 'Door exited'); if (this.sockServer) { this.sockServer.close(); diff --git a/core/event_scheduler.js b/core/event_scheduler.js index 076e116f..ebce9178 100644 --- a/core/event_scheduler.js +++ b/core/event_scheduler.js @@ -167,17 +167,21 @@ class ScheduledEvent { return cb(e); } - proc.once('exit', exitCode => { - if (exitCode) { + proc.onExit(exitEvent => { + if (exitEvent.exitCode) { Log.warn( - { eventName: this.name, action: this.action, exitCode: exitCode }, + { + eventName: this.name, + action: this.action, + exitCode: exitEvent.exitCode, + }, 'Bad exit code while performing scheduled event action' ); } return cb( - exitCode + exitEvent.exitCode ? Errors.ExternalProcess( - `Bad exit code while performing scheduled event action: ${exitCode}` + `Bad exit code while performing scheduled event action: ${exitEvent.exitCode}` ) : null ); diff --git a/core/file_transfer.js b/core/file_transfer.js index 01eec9b8..d18d581b 100644 --- a/core/file_transfer.js +++ b/core/file_transfer.js @@ -485,13 +485,10 @@ exports.getModule = class TransferFileModule extends MenuModule { } }); - externalProc.once('close', () => { - return this.restorePipeAfterExternalProc(); - }); - - externalProc.once('exit', exitCode => { + externalProc.onExit(exitEvent => { + const { exitCode, signal } = exitEvent; this.client.log.debug( - { cmd: cmd, args: args, exitCode: exitCode }, + { cmd: cmd, args: args, exitCode, signal }, 'Process exited' ); diff --git a/package.json b/package.json index 560563ed..b574e745 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "minimist": "^1.2.6", "moment": "2.29.4", "nntp-server": "3.1.0", - "node-pty": "0.10.1", + "node-pty": "1.0.0", "nodemailer": "6.7.7", "otplib": "11.0.1", "qrcode-generator": "^1.4.4", @@ -65,8 +65,8 @@ "sanitize-filename": "^1.6.3", "sqlite3": "5.1.6", "sqlite3-trans": "1.3.0", - "string-strip-html": "8.4.0", "ssh2": "1.14.0", + "string-strip-html": "8.4.0", "systeminformation": "5.21.7", "telnet-socket": "0.2.4", "temptmp": "^1.1.0", diff --git a/yarn.lock b/yarn.lock index ad5601ea..e6272e82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1921,12 +1921,12 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-pty@0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.1.tgz#cd05d03a2710315ec40221232ec04186f6ac2c6d" - integrity sha512-JTdtUS0Im/yRsWJSx7yiW9rtpfmxqxolrtnyKwPLI+6XqTAPW/O2MjS8FYL4I5TsMbH2lVgDb2VMjp+9LoQGNg== +node-pty@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.0.0.tgz#7daafc0aca1c4ca3de15c61330373af4af5861fd" + integrity sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA== dependencies: - nan "^2.14.0" + nan "^2.17.0" nodemailer@6.7.7: version "6.7.7"