Skip to content

Instantly share code, notes, and snippets.

@djalilhebal
Created July 24, 2020 22:18
Show Gist options
  • Save djalilhebal/0d3b036d1e9c099f1a975029e96a9b6a to your computer and use it in GitHub Desktop.
Save djalilhebal/0d3b036d1e9c099f1a975029e96a9b6a to your computer and use it in GitHub Desktop.
A programmatic interface for Node that wraps MASM and LINK using DOSEMU
/**
* 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