Skip to content

Scripts & Native Binaries

The abracadabra module provides a generic solution for launching any local process as a door: native terminal applications, shell scripts, Python scripts, and more. Any process that communicates over stdio works. I/O is bridged through standard I/O (stdio) or a temporary TCP socket server.


The abracadabra config block supports the following fields:

ItemRequiredDescription
nameYesUsed as a key for tracking the number of clients using this door.
dropFileTypeNoType of drop file to generate. See Drop File Types. Can be omitted or none.
cmdYesPath to the executable to launch.
argsNoArray of arguments to pass to cmd. See Argument Variables below.
preCmdNoPath to a pre-command executable or script. Executes before cmd.
preCmdArgsNoArguments to pass to preCmd. See Argument Variables below.
cwdNoWorking directory for cmd. Defaults to the directory containing cmd.
envNoEnvironment variables as a map: { SOME_VAR: "value" }
nodeMaxNoMax concurrent sessions for this door. Uses name as the tracking key.
tooManyArtNoArt spec to display when nodeMax is exceeded.
minTimeLeftMinutesNoRefuse to start the door unless the user has at least this many minutes left in today’s time budget. Unset means no check; a user with no limit always passes.
notEnoughTimeArtNoArt spec to display when minTimeLeftMinutes is not met.
ioNoI/O mode: stdio (default) or socket. When socket, ENiGMA½ spawns a temporary TCP server on 127.0.0.1:{srvPort} that the door process connects back to. See Socket I/O and the Connect-Back Server.
socketBindAddressNoInterface the io: socket server listens on. Defaults to 127.0.0.1 (loopback). Only set this if the door or emulator genuinely runs on another machine — see Socket I/O and the Connect-Back Server.
commTypeNoWhat the drop file tells the door it is talking to: local, serial, or socket, defaulting to socket when io: socket and local otherwise. dropFileType: BBSDEV takes a wider set with a different default — see BBSDEV.DRP.
commParamsNoThe descriptor, handle, UART base and IRQ, or FOSSIL port belonging to commType. Read only for dropFileType: BBSDEV. See BBSDEV.DRP below.
encodingNoThe door process’s text encoding. Defaults to cp437. Linux-native binaries often use utf8.

With io: socket, ENiGMA½ listens on an ephemeral port for the duration of the session and gives it to you as {srvPort}. The door — or the emulator bridging the port onto a guest COM port — dials that port, and ENiGMA½ pipes the caller’s session to it.

The server listens on 127.0.0.1 only. Whatever connects first is handed the caller’s session, so point your launch script, emulator or bridge at 127.0.0.1 and nothing else:

  • Spell it 127.0.0.1, not localhost. localhost can resolve to ::1 first, and the listener is not there. Clients that try every resolved address do get through, but there is no reason to depend on it.
  • A LAN address of the machine, or a container’s address, will not connect. Earlier versions of ENiGMA½ listened on every interface, so a setup that used one of those happened to work; it was also a way for anyone who could reach the box to pick up the caller’s session, and it no longer binds that way.

socketBindAddress exists for the one setup loopback cannot serve — an emulator or bridge on a different machine:

io: socket
socketBindAddress: 10.0.0.5 // only this host's own address, never 0.0.0.0

Anything other than loopback is logged as a warning each time the door starts, and leaves the window above open to that network. Restrict the port with a host firewall if you must use it.

io says how ENiGMA½ talks to the process it spawns; commType says how the door talks to the caller. They are the same thing only when the process ENiGMA½ spawns is the door, which is why the default is derived from io:

commTypeReported asUse when
local (default for the legacy formats)DOOR32.SYS comm type 0, DOOR.SYS COM0:, DORINFO 0The door reads stdin and writes stdout. This covers io: stdio, which is nearly every native or scripted door.
serialDOOR32.SYS comm type 1, DOOR.SYS COM1:, DORINFO COM1An emulator sits between ENiGMA½ and the door and presents it a COM port — QEMU bridging {srvPort} onto isa-serial, for example.
socketDOOR32.SYS comm type 2, DOOR.SYS COM1:, DORINFO COM1Descriptor sharing by way of bivrost!.

Doors that ignore these fields entirely — most DOS-era games under an emulator — are unaffected by any of this.

dropFileType: BBSDEV writes a BBSDEV.DRP instead of one of the legacy formats.

The door is told where the file is through the BBSDEV_DRP environment variable, which ENiGMA½ sets to the full path before it spawns the process. The value is neither quoted nor shell-escaped, and a door reads it from its own environment. An env of your own still replaces ENiGMA½’s environment as it always has; BBSDEV_DRP is added to whichever environment the door gets.

commType names a wider set of mechanisms, and most of them take a parameter in commParams:

commTypecommParamsThe door is handed
localnoneits own local console
stdiononeterminal input on stdin, terminal output on stdout. This is the default under io: stdio
serialfile descriptoran inherited, configured POSIX serial descriptor
winserialWin32 HANDLEan inherited, configured Win32 COM handle
uartHHHH,I — I/O base in four uppercase hex digits, then the IRQdirect DOS UART access
fossilport, 0 through 254an initialized FOSSIL interface

The format also has a socket mode, for a socket the door inherits. ENiGMA½ never has one to pass on — io: socket stands up a listener the door dials — so commType: socket is refused here rather than written, whatever commParams you give it. A value for serial or winserial has to come from the emulator or bridge you put between ENiGMA½ and the door — QEMU, DOSEMU, bivrost!.

A channel ENiGMA½ cannot name stops the door from starting. Where the legacy formats fall back to local, local in this format is a positive claim — the door uses its current local console — so writing it for a door reading a socket would describe a screen nobody sees. Instead the drop file is refused, the door does not run, and the reason is logged. That is what happens under io: socket: the socket ENiGMA½ shares is a server the door dials rather than a descriptor it inherits, and the format has no token for that. A QEMU or DOSEMU setup says what the door really gets — commType: uart with commParams: 03F8,4, or fossil with 0 — and writes a valid file.

Line 12 names the character set of the door’s terminal data, and it is taken from the door’s own encoding rather than the caller’s terminal encoding — that is the value ENiGMA½ decodes the door’s output with, so the two cannot disagree. An encoding it cannot name in the registry’s spelling refuses the file rather than guessing; aliases iconv accepts, such as 437 or win1252, are folded onto the same name.

Line 13 must be a well-formed BCP 47 tag. A general.language that is not one — English (US), say — refuses the file rather than writing something a conforming door must reject.

Line 11, the forced logoff time, states when the caller’s daily time budget runs out, as an absolute UTC instant so a door that pauses cannot arrive at the wrong answer by counting down. It is written empty where nothing will end the session — no limit configured, or an exempt user — which is the format’s way of saying there is no deadline.

The following variables can be used in args and preCmdArgs:

VariableDescriptionExample
{node}Current node number1
{dropFile}Drop file filename onlyDOOR.SYS
{dropFilePath}Full path to the generated drop file/home/enigma/drop/node1/DOOR.SYS
{dropFileDir}Full path to the drop file directory/home/enigma/drop/node1/
{userAreaDir}User-specific save directory/home/enigma/drop/node1/NuSkooler/lord/
{userId}Current user ID42
{userName}Sanitized username (safe for filenames)nuskooler
{userNameRaw}Raw username (may not be filename-safe)\/\/izard
{srvPort}Temporary TCP server port (when io: socket)1234
{cwd}Working directory/home/enigma/doors/foo/
{termHeight}Terminal height25
{termWidth}Terminal width80
args: [
"-D", "{dropFilePath}",
"-N", "{node}",
"-U", "{userId}"
]

A door can produce output faster than its caller takes it in — a sixel game on a slow link, or a terminal that is slow to render. Rather than queue everything in memory while the caller falls further behind, ENiGMA½ stops reading the door’s output once too much is waiting to be sent, and starts again when the caller has caught up. The door’s own writes block in the meantime, so it runs at the caller’s pace. Input to the door is never held back.

This applies to every abracadabra door, under both io: stdio and io: socket, and is set once for the system in config.hjson:

doors: {
outputBackpressure: {
enabled: true
highWaterBytes: 262144 // pause at 256 KiB waiting to be sent
lowWaterBytes: 65536 // resume under 64 KiB
}
}

enabled: false turns it off. lowWaterBytes must be less than highWaterBytes; a pair that is not is logged and the door runs unthrottled. When a door exits, a Door output backpressure summary line in the log gives how many times it was paused, for how long in total, and the largest backlog seen.


A simple wrapper script that launches a native binary:

doorMyGame: {
desc: My Door Game
module: abracadabra
config: {
name: MyGame
dropFileType: DOOR
cmd: /home/enigma/doors/mygame/launch.sh
args: [ "{node}", "{dropFilePath}" ]
nodeMax: 4
tooManyArt: DOORMANY
io: stdio
}
}
doorPythonGame: {
desc: Python Door
module: abracadabra
config: {
name: PythonGame
dropFileType: DORINFO
cmd: /usr/bin/python3
args: [ "/home/enigma/doors/pydoor/main.py", "{node}", "{dropFilePath}" ]
encoding: utf8
nodeMax: 8
io: stdio
}
}

Some doors require a socket connection rather than stdio. ENiGMA½ starts a temporary TCP server on 127.0.0.1 and passes the port to your script, which must connect back to 127.0.0.1:{srvPort}:

doorSocketGame: {
desc: Socket Door
module: abracadabra
config: {
name: SocketGame
dropFileType: DOOR
cmd: /home/enigma/doors/socketgame/launch.sh
args: [ "{node}", "{dropFile}", "{srvPort}" ]
nodeMax: 1
io: socket
}
}

Due to Node.js limitations, ENiGMA½ does not directly support DOOR32.SYS-style socket descriptor sharing. However, bivrost! bridges this gap. bivrost! is available for Windows and Linux x86/x86_64 (and buildable from Rust on other platforms).

doorWithBivrost: {
desc: Bivrost Example
module: abracadabra
config: {
name: BivrostExample
dropFileType: DOOR32
cmd: /home/enigma/utils/bivrost
args: [
"--port", "{srvPort}",
"--dropfile", "{dropFilePath}",
"--out", "/home/enigma/doors/jezebel",
"/home/enigma/doors/jezebel/door.exe /home/enigma/doors/jezebel/door32.sys"
]
nodeMax: 1
tooManyArt: DOORMANY
io: socket
}
}

bivrost! connects to the port on the local machine, so it needs no host argument and the loopback default suits it. It has no way to reach a socketBindAddress elsewhere.

See the bivrost! documentation for details. Pre-built binaries are also available via Phenom Productions on various boards.

Alternative workarounds: Telnet Bridge, or NET2BBS.