Created
July 24, 2020 22:18
-
-
Save djalilhebal/0d3b036d1e9c099f1a975029e96a9b6a to your computer and use it in GitHub Desktop.
A programmatic interface for Node that wraps MASM and LINK using DOSEMU
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* masmo-dosemu.js | |
* | |
* @fileoverview A programmatic interface that wraps MASM and LINK using DOSEMU. | |
* It uses DOSEMU in the `-dumb` mode to be able to interact with it through the `stdin` and `stdout` streams. | |
* Currenty it only parses errors and warning that are causes by a provided code (the `lint` method). | |
* Also, it currently works only on Linux. | |
* | |
* This is basically a test of Node Steams and a "proof of concept" of last year's idea Masmo.js (which I should rename to `masmo-v86`): | |
* - See https://github.com/djalilhebal/trash/blob/master/2019-04/Masmo.js | |
* - See https://djalil.me/shit/2018-11/retardedAO/app/ | |
* | |
* - TESTED WITH: Node v12.16.1, DOSEMU v1.4.0.8, MASM v5.10, and LINK v3.69. | |
* | |
* - It is not "thread-safe": | |
* I could add something like a queue or a semaphore/lock, but my use case does not require concurrency, really. | |
* "Linting" the code happens after the user stops writing (`debounce`, that is). | |
* Adding an `isBusy` flag should be enough. | |
* | |
* - This code expects the directory structure and files: | |
* ``` | |
* . | |
* ├── bin | |
* │ ├── dosemu | |
* │ ├── dosemu.bin | |
* │ ├── KAI-ETX.EXE | |
* │ ├── KAI-GO.BAT | |
* │ ├── LINK.EXE | |
* │ ├── MASM.EXE | |
* │ └── PROGRAM.ASM | |
* └── masmo-dosemu.js | |
* ``` | |
* | |
* NOTES: | |
* - `PROGRAM.ASM` is automatically created | |
* - "dosemu.bin is the binary wrapped by the script dosemu (1) which invokes the Linux dos emulator, also known as DOSEMU." | |
* - `dosemu`, if installed, can be directly used as a global command, | |
* I have copied it to bin/ only to make the project standalone and not require the user to install it manually. | |
*/ | |
const {writeFile} = require('fs').promises; | |
const {spawn} = require('child_process'); | |
const split2 = require('split2'); | |
/** | |
* A programmatic interface for MASM and LINK using DOSEMU | |
*/ | |
class MasmoDosemu { | |
constructor() { | |
this._worker = null; | |
this._pending = null; | |
this.isReady = new Promise((resolve, _reject) => { | |
this.setReady = () => resolve(true); | |
}); | |
} | |
/** | |
* @return {Promise<boolean>} A promise that fulfills once Masmo is ready | |
* @public | |
*/ | |
init() { | |
// The 'ETX' (End of Text) control character is printed via KAI-ETX.EXE | |
// after KAI-GO completes a "linting loop" and reaches the KAI-END section. | |
// TODO: Instead of ETX, consider using "Unit Separator", "Group Separator", or "Record Separator" | |
// See https://en.wikipedia.org/wiki/C0_and_C1_control_codes | |
const OUTPUT_SEPARATOR_REGEX = /\x03/; | |
this._worker = spawn('./dosemu', ['-dumb', '"KAI-GO.BAT"'], {cwd: './bin/'}); | |
this._worker | |
.stdout | |
.pipe( split2(OUTPUT_SEPARATOR_REGEX, MasmoDosemu.parseMASMOutput) ) | |
.on('data', (arr) => { | |
if (this._pending) { | |
this._pending.resolve(arr); | |
this._pending = null; | |
} else { | |
// Assuming it's the first run | |
this.setReady(); | |
} | |
}); | |
return this.isReady; | |
} | |
/** | |
* @param {string} code Assembly source code | |
* @returns {Promise<any>} A promise that resolves to an array of errors and warnings as reported by MASM and LINK | |
* @public | |
*/ | |
async lint(code) { | |
// 1. Save the code that MASM/LINK will work on. | |
await writeFile('./bin/PROGRAM.ASM', code); | |
// 2. "Register" a promise that will resolve when the code is assembled and the output is parsed | |
const pending = new Promise((resolve, reject) => { | |
this._pending = {code, resolve, reject}; | |
}); | |
// 3. "Press any key to continue..." | |
this._worker.stdin.write('c'); | |
return pending; | |
} | |
/** | |
* @todo Should specify a signal? (https://en.wikipedia.org/wiki/Signal_(IPC)#SIGTERM) | |
* @returns {void} | |
* @public | |
*/ | |
destroy() { | |
this._worker.kill(); | |
} | |
/** | |
* @param {String} log | |
* @return {Object[]} Errors and warnings | |
* @private | |
*/ | |
static parseMASMOutput(log) { | |
// yo.asm(18): error A2009: Symbol not defined: KH | |
// X.ASM(28): warning A4031: Operand types must match | |
// (filename).asm(lineNum): (type:error|warning) (code): (message) | |
const rLine = /^(\w+)\.asm\((\d+)\): (error|warning) (.+?): (.+)$/i; | |
return log.split('\r\n').filter( line => rLine.test(line)).map((line) => { | |
const [, filename, lineNum, type, code, message] = line.match(rLine); | |
return { type, message, line: Number(lineNum), code, filename }; | |
}); | |
} | |
} | |
async function main() { | |
const masmo = new MasmoDosemu(); | |
await masmo.init(); | |
console.log('[main] Running code with errors'); | |
const errorAndWarnings = await masmo.lint(` | |
; PROGRAM: Print the character 'K'. K is for Kaito. | |
pile segment para stack 'pile' | |
db 256 dup(0) | |
pile ends | |
data segment | |
; no variables | |
data ends | |
code segment | |
main proc far | |
assume cs:code | |
assume ds:data | |
assume ss:pile | |
mov ax, data | |
mov ds, ax | |
mov ah, 02h ; output character | |
mov dl, 'K' ; or 75 | |
int 21h, 13 | |
pop Nicki | |
mov al, 0 ; exit code success | |
mov ah, 4Ch | |
int 21h | |
main endp | |
code ends | |
end main | |
`); | |
console.log(errorAndWarnings); | |
console.log('[main] Running after fixing issues'); | |
console.log(await masmo.lint(` | |
; PROGRAM: Print the character 'K'. K is for Kaito. | |
pile segment para stack 'pile' | |
db 256 dup(0) | |
pile ends | |
data segment | |
; no variables | |
data ends | |
code segment | |
main proc far | |
assume cs:code | |
assume ds:data | |
assume ss:pile | |
mov ax, data | |
mov ds, ax | |
mov ah, 02h ; output character | |
mov dl, 'K' ; or 75 | |
int 21h | |
mov al, 0 ; exit code success | |
mov ah, 4Ch | |
int 21h | |
main endp | |
code ends | |
end main | |
`)); | |
console.log('[main] Destroying masmo'); | |
masmo.destroy(); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment