======================
== fabulous.systems ==
======================
Welcome to the world of fabulous.systems

The Making of the MS-DOS Command Reference Bot

#retrocomputing #msdos #mastodon #fediverse

I launched my Daily MS-DOS command bot a few months ago.

The initial idea was straightforward: Pick a random command or program that ships with MS-DOS 6.22 and post a brief description.

With a bit of help from some websites that archived the MS-DOS reference manual, I finished the initial revision of the JSON-based data file including all the comments with a short description in a matter of hours. I wrote a simple Bash script to pick a random command, fetch the description from the JSON file, and publish a post through the Mastodon API.

Easy. But shortly after that, a question arose: “What about screenshots?”

At first, I thought about going through every command in my data file, spinning up a VM, executing the command, taking a screenshot, and linking to it in the data file itself. While this approach is possible, it is tedious. There’s no real challenge in repeatedly typing in commands and trying to keep track of the screenshots I already took. Moreover, any change in the command list would require spinning up a VM, executing the command, taking a screenshot, and modifying the data file to include the modified screenshot.

But what if I could do all of this dynamically by taking screenshots at the moment my MS-DOS VM is executing the command I requested?

Setting up the MS-DOS 6.22 virtual machine

The first step was to create a virtual machine with QEMU to run the commands from the data file. For this scenario, QEMU works exceptionally well thanks to the built-in monitor interface that will allow interacting with the VM from the host system.

I started by creating an image file that will be the virtual hard drive for the MS-DOS installation.

qemu-img create msdos622.img 512M

A basic installation of MS-DOS 6.22 consists of three floppy images. The first floppy is bootable and will start the installation process. For convenience, I named the floppy images Disk1.img, Disk2.img, and Disk3.img in the same order the setup program will request them.

I went with a very basic 486-based VM with a whopping RAM size of 8 MB - this should be enough for getting all the included tools up and running.

qemu-system-i386 -drive format=raw,file=Disk1.img,index=0,if=floppy -drive format=raw,file=msdos622.img,index=0,if=ide -m 8 -cpu 486

After starting QEMU, the VM booted from the floppy image Disk1.img and initiated the installer.

MS-DOS 6.22: Initial setup screen
MS-DOS 6.22: Initial setup screen

Guided by the installer, I partitioned the virtual hard drive and let the installer start copying the contents from the floppy image to the virtual hard drive. Once the installer is done copying the files from the first floppy, it will request the user to insert the second floppy into the disk drive.

Swapping floppy disks
Swapping floppy disks

To replace Disk1.img with Disk2.img (and Disk3.img afterwards) in the VM, I switched to the QEMU monitor by pressing Ctrl+Alt+2 inside the QEMU window. The QEMU monitor gives us complete control over the VM, so replacing the floppy image is possible with the following command:

change floppy0 Disk2.img

With the new floppy image in place, I switched back to the VM with Ctrl+Alt+1 and let the setup continue. Inserting the third floppy image works in the exact same way.

Once the installer completes processing all three floppies, it asks the user to eject the floppy from the drive, ensuring that the machine will boot from the fresh installation instead of the floppy drive.

Time to reboot!
Time to reboot!

Using the QEMU monitor again, ejecting the floppy image is as easy as

eject floppy0

One reboot later, we have the new installation up and running. Success!

Hello there, DOS!
Hello there, DOS!

Connecting to the monitor

After the initial setup, it is time to hook up the QEMU monitor to the host system.

Thankfully, QEMU provides a way to connect to the QEMU monitor via the network so that I can send arbitrary commands to the VM. Additionally, I switched the virtual console to text-only mode because the bot and thus QEMU is running on a server without a graphical interface.

qemu-system-i386 -hda msdos622.img -m 8 -cpu 486 -monitor tcp:localhost:9999,server,nowait -nographic &

Starting the VM this way will expose the QEMU monitor to port 9999 of the host machine. With the monitor exposed, the host machine can send the commands to port 9999 using tools like nc.

QEMU’s sendkey command will only accept one character at a time, which is not ideal. One way to mitigate this problem would be splitting the DOS commands into single characters. The much more convenient way is using a script called sendkeys.awk by zambonin.

This script accepts full strings and split them into single characters, including some conversion magic:

$ echo "ver" | awk -f sendkeys.awk | timeout 1 nc localhost 9999
QEMU 8.2.0 monitor - type 'help' for more information
(qemu) sendkey v
(qemu) vsendkey e
(qemu) esendkey r
(qemu) rsendkey ret
(qemu)
...
MS-DOS Version 6.22


C:\>

With the DOS VM listening to the input, it is time to take care of the output!

Creating screenshots with the screendump command

QEMU provides the screendump command through the monitor, which does exactly what I needed to create the screenshots. The command creates a bitmap file in the PPM format, which resembles a simple bitmap file.

Screenshot taken with the screendump command
Screenshot taken with the screendump command

Because this format is pretty non-standard nowadays and incompatible with most web applications, the best way to convert the files is with the convert utility provided by ImageMagick.

convert ms-dos-vm-screendump.ppm ms-dos-vm-screendump.webp

The JSON data file

As mentioned earlier, I am using a JSON file to store the commands along with their descriptions. Each entry in the data file follows the same pattern:

  {
    "command": "SYS",
    "description": "Copies MS-DOS system files to a disk's master boot record.",
    "verbose_description": "The SYS command is used to copy MS-DOS system files to a disk's master boot record (MBR), making it bootable. It's often used to create bootable system disks for MS-DOS installations.",
    "show_help": "yes"
  },

Each message the bot posts contains the command, the description and a more verbose description in a second paragraph. If show_help is set to yes, the bot passes the /? argument to command.

I use this option for commands that provide no meaningful screen output when invoked without additional parameters like files or disk drives.

The script

With the VM prepared and the JSON file in place, there is only one step left: Writing a simple script that picks a command, launches the VM, grabs the screenshot, and uploads it through the Mastodon API.

Because the content of the JSON file is dynamic and can change quite often, I want to do as little hard coding as possible. First, I check the length of the JSON file to see how many commands the script can pick from.

After picking a random command, the individual elements for the JSON entry are concatenated into the final string QUOTE, which contains the complete post message.

# Calculate number of entries in json datafile
MAXNUMBER=$(jq '.[].command' dosreference.json | wc -l)
MAXNUMBER="$((MAXNUMBER-1))"
RANDOMNUMBER=$(shuf -i 0-$MAXNUMBER -n 1)

# Fetch random entry from json datafile
COMMAND=$(jq -r --argjson randomnumber "${RANDOMNUMBER}" '.[$randomnumber].command' dosreference.json)
COMMAND_DESCRIPTION="$(jq -r --argjson randomnumber "${RANDOMNUMBER}" '.[$randomnumber].description' dosreference.json) \n\n"
COMMAND_VERBOSE_DESCRIPTION="$(jq -r --argjson randomnumber "${RANDOMNUMBER}" '.[$randomnumber].verbose_description' dosreference.json)"
COMMAND_SHOW_HELP="$(jq -r --argjson randomnumber "${RANDOMNUMBER}" '.[$randomnumber].show_help' dosreference.json)"
QUOTE="status=$(echo "${COMMAND}": "${COMMAND_DESCRIPTION}""${COMMAND_VERBOSE_DESCRIPTION}")"

if [ "$COMMAND_SHOW_HELP" = "yes" ]; then
    COMMAND="${COMMAND} /?"
fi

Then, I start the VM and pass the command crafted from the JSON output to it.

After taking the screenshot, the VM is killed through the monitor, ready to relaunched the next time the cron job responsible for the script is triggered.

qemu-system-i386 -hda msdos622.img -m 8 -cpu 486 -monitor tcp:localhost:9999,server,nowait -nographic &
sleep 30

# Execute command in VM
echo "cls"        | awk -f qemu-sendkeys.awk | timeout 1 nc localhost 9999
echo "${COMMAND}" | awk -f qemu-sendkeys.awk | timeout 5 nc localhost 9999

# Create screenshot of VM
echo "screendump ms-dos-vm-screendump.ppm" | timeout 1 nc localhost 9999

# Kill VM
echo "quit" | timeout 1 nc localhost 9999
convert ms-dos-vm-screendump.ppm ms-dos-vm-screendump.webp

The timing here is completely hard-coded because I cannot determine if the VM has fully launched. Since the server hosting the bot also provides some other services, I went with a safe timeout of 30 seconds - this should be well enough to get a bare MS-DOS VM ready. The timeouts for the calls to nc are required, though - without it, nc would wait ’til eternity for me to hit the “Enter” key.

The final step: Upload the screenshot to Mastodon, wait for it to process, and attach the post message.

# Upload image to Mastodon
MEDIA_ID=$(curl -s -X POST -H "Content-Type: multipart/form-data" https://manitu.social/api/v2/media -H 'Authorization: Bearer XXXXXXXXXXXXXXXX' --form file="@ms-dos-vm-screendump.webp" | jq -r '.id')

# Wait for the image to process...
sleep 10

# Go!
curl -g https://manitu.social/api/v1/statuses -H "Authorization: Bearer XXXXXXXXXXXXXXXX" -F "$QUOTE" -F "media_ids[]=${MEDIA_ID}"

And we are done!

MS-DOS command of the day: SYS!
MS-DOS command of the day: SYS!

Do you have any comments or suggestions regarding this article? Please drop an e-mail to feedback@fabulous.systems!