1218 lines
47 KiB
Bash
Executable file
1218 lines
47 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# joey-run: run a built JoeyLib program in the right emulator (or, for the
|
|
# SDL3 port, natively, for the web port, in a browser, for the Win32 port,
|
|
# under Wine, and for the Win16 port, on Windows 3.11 in DOSBox; the PSP
|
|
# port's in headless PPSSPP; the Haiku and BeOS ports' in their QEMU VMs).
|
|
#
|
|
# Usage:
|
|
# ./joey-run [options] PROGRAM [-- ARGS...]
|
|
#
|
|
# PROGRAM is any of:
|
|
# a program file build/dos/bin/DRAW.EXE, build/atarist/bin/DRAW.PRG (or build/ste),
|
|
# build/amiga/bin/Draw, build/x68000/bin/DRAW.X,
|
|
# build/iigs/bin/DRAW, build/dvx/bin/apps/joeylib/draw/draw.app,
|
|
# build/sdl3/bin/DRAW, build/web/bin/DRAW/DRAW.html,
|
|
# build/psp/bin/DRAW/EBOOT.PBP, build/haiku/bin/DRAW
|
|
# (or build/haiku32, build/beos)
|
|
# an app folder a program in a folder of its own with its DATA:
|
|
# build/dvx/bin/apps/joeylib/audio
|
|
# an IIgs disk image build/iigs/bin/DRAW.2mg (make iigs-game-disk), or
|
|
# a data disk such as joey.2mg (boots GS/OS with it)
|
|
# an example name draw, audio, uber, ... together with --port
|
|
# The machine is recognised from the file itself. A program built outside
|
|
# JoeyLib (make/project.mk) runs the same way: name its file, folder or disk
|
|
# image in the project's build/, or give its name with --project and --port.
|
|
#
|
|
# Options:
|
|
# --port PORT iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, win32, win16,
|
|
# psp, haiku, haiku32 or beos
|
|
# --project DIR look a program name up in the project at DIR (its
|
|
# build/PORT/bin) instead of JoeyLib's examples; DVX runs
|
|
# keep their DVX in DIR's build/dvx/run
|
|
# --data DIR copy DIR's files into the program's DATA folder for
|
|
# this run (for example the game an interpreter plays)
|
|
# --headless no window and no sound, for scripts and CI; stops
|
|
# after 60 seconds unless --seconds says otherwise
|
|
# --seconds N stop after N seconds
|
|
# --snapshot FILE save a PNG of the screen when the run stops
|
|
# (needs --seconds or --headless)
|
|
# --machine rom01 IIgs: the ROM 01 machine instead of ROM 03
|
|
# --machine ste Atari: the machine Hatari emulates: st, megast, ste
|
|
# or megaste. By default an ST-build program (make
|
|
# atarist) gets a plain ST and an STe-build one (make
|
|
# ste) an STe; the ST build runs on all four, the STe
|
|
# build only on ste and megaste
|
|
# --machine a1200 Amiga: the machine FS-UAE emulates: a500 (the
|
|
# default), a1200 (68EC020, AGA) or a4000 (68040,
|
|
# AGA), each on its own Kickstart (install emulator:aga
|
|
# for the AGA ones)
|
|
# --ram SIZE IIgs: memory size for MAME (default 4M)
|
|
# --debug IIgs and X68000: start MAME's debugger
|
|
# --serial HOST:PORT connect the machine's serial port to the TCP server
|
|
# at HOST:PORT: a BBS, a program of your own, another
|
|
# run's --serial-listen, or `nc -l PORT`
|
|
# --serial-listen PORT
|
|
# the serial port waits for a TCP connection on PORT
|
|
# (on every network interface) instead
|
|
# --serial-baud N IIgs and X68000: the rate MAME's end of the wire runs
|
|
# at, which must match the program's (default 9600,
|
|
# always 8N1); the other emulators pass bytes at any rate
|
|
# -h, --help this text
|
|
#
|
|
# Arguments after -- are passed to the program (DOS, SDL3, Win32 and Win16
|
|
# only: the other machines' autostart cannot pass arguments).
|
|
#
|
|
# An SDL3 program is a native program: it runs on this machine directly, in
|
|
# a window (JOEYLIB_SCALE, JOEYLIB_ASPECT and JOEYLIB_FULLSCREEN in the
|
|
# environment size it; see docs). --headless runs it on a private virtual
|
|
# display at one pixel per stage pixel, so --snapshot is the 320x200 stage.
|
|
# It has no serial port.
|
|
#
|
|
# A web program is a page: joey-run serves its folder over HTTP on this
|
|
# machine (127.0.0.1 only) and opens it in your browser; stop with Ctrl+C.
|
|
# --headless runs it in headless Chromium instead, at one canvas pixel per
|
|
# stage pixel, so --snapshot is the stage. Its DATA is packed in when it is
|
|
# built, so --data does not apply, and it has no serial port.
|
|
#
|
|
# A Win16 program runs on Windows for Workgroups 3.11 in DOSBox Staging (a
|
|
# fresh copy, each run, of the one the installer set up from your own disc:
|
|
# install emulator:win16), as Windows' shell, so Windows ends when the
|
|
# program does: in DOSBox's window, at 800x600, with sound, DOSBox's game
|
|
# port through KPJOY.DRV and COM1 through KPCOMM.DRV (JoeyLib's joystick and
|
|
# communications drivers for Windows 3.1). Its folder is
|
|
# drive D:. JOEYLIB_SCALE, JOEYLIB_ASPECT and JOEYLIB_FULLSCREEN in the
|
|
# environment go into Windows' DOS environment, so they size it as they do
|
|
# an SDL3 or Win32 program; Alt with the keypad's Enter switches full screen
|
|
# (DOSBox keeps Alt+Enter). --headless runs it on a private virtual display
|
|
# at one window pixel per stage pixel, and --snapshot is DOSBox's own
|
|
# picture of the 800x600 screen.
|
|
#
|
|
# A PSP program runs only with --headless here: under PPSSPPHeadless (the
|
|
# installer's patched build, emulator:psp), from its folder on a private
|
|
# memory stick (PSP/GAME/NAME, as on a PSP), with every memory access checked
|
|
# as a PSP checks it; a bad one stops the run and joey-run names the line.
|
|
# --snapshot is the PSP's 480x272 screen. To play it, copy its folder into
|
|
# PSP/GAME/ on a PSP's memory stick, or open its EBOOT.PBP in PPSSPP. It has
|
|
# no serial port.
|
|
#
|
|
# A Haiku (haiku: x86_64; haiku32: x86) or BeOS R5 (beos) program runs in
|
|
# its VM under QEMU (install emulator:PORT; BeOS's is made from your own
|
|
# BeOS 5 Professional CD), from a copy of its folder on a second disk: in
|
|
# QEMU's window, with sound (Haiku's; BeOS's VM has none). The machine
|
|
# powers off when the program ends, and what the program wrote in its
|
|
# folder (saves, its log) is copied back. JOEYLIB_SCALE, JOEYLIB_ASPECT and
|
|
# JOEYLIB_FULLSCREEN in the environment size its window, as they do an SDL3
|
|
# program's. --headless runs it at one window pixel per stage pixel without
|
|
# a window; --seconds counts from when the job starts, after the boot, and
|
|
# --snapshot is QEMU's picture of the whole screen. They have no serial
|
|
# port here.
|
|
#
|
|
# A DVX program runs in DOSBox Staging on a copy of the DVX the installer
|
|
# unpacked from a DVX tree's dist/dvx.zip (install dvx), which boots
|
|
# straight into it: the program is DVX's desktop program, in its window on a
|
|
# 1024x768 desktop (640x480 with --headless). Game > Full Screen, or Alt
|
|
# with the keypad's Enter (DOSBox keeps Alt+Enter for its own full screen),
|
|
# puts it on the whole screen. Interactive runs keep that DVX in
|
|
# build/dvx/run (the project's with --project), so what the program saves
|
|
# (in DVX's CONFIG folder), and whether it was left on the whole screen, is
|
|
# still there next time.
|
|
#
|
|
# The serial port is the machine's usual one: the IIgs modem port (the
|
|
# printer port and a Super Serial Card are not connected), the X68000's
|
|
# RS-232C port, the ST's Modem port, the Amiga's serial port (serial.device
|
|
# unit 0) and COM1 on DOS, DVX, Win32 and Win16.
|
|
#
|
|
# The serial link is raw TCP, without telnet negotiation. socat relays it
|
|
# where the emulator cannot make it itself: the ST, Win32, the Amiga with
|
|
# --serial, and the IIgs and X68000 with --serial-listen. When
|
|
# --serial's server is not there, joey-run stops, except on the IIgs and
|
|
# X68000, where MAME prints "Unable to load image" and runs with the port
|
|
# unplugged. Listening, what the program sends before the connection
|
|
# arrives waits for it, except in DOSBox (DOS, DVX and Win16), which drops
|
|
# it; the Amiga holds the whole machine until the connection arrives.
|
|
#
|
|
# The emulators and firmware come from ./install.sh
|
|
# (install emulator:PORT). Interactive runs use the program's own folder
|
|
# or disk image, so saves and logs persist; --headless and --data runs
|
|
# work on a private copy. Nothing is written to your home directory.
|
|
|
|
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
# See LICENSE.md for license information.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
set -euo pipefail
|
|
|
|
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=scripts/lib/joeyEnv.sh
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
|
|
port=""
|
|
projectRoot="$repo"
|
|
dataDir=""
|
|
headless=0
|
|
snapshotWindow=0 # --snapshot takes the program's window, not the screen
|
|
seconds=""
|
|
snapshot=""
|
|
machine="apple2gs"
|
|
stMachine="" # Hatari's --machine for an Atari program (default: by build)
|
|
amigaModel="" # an Amiga program's machine (scripts/lib/amigaGate.sh; default A500)
|
|
ramSize="4M"
|
|
debug=0
|
|
program=""
|
|
progArgs=()
|
|
emuPid=""
|
|
xvfbPid=""
|
|
work=""
|
|
serialMode="" # connect (--serial), listen (--serial-listen) or off
|
|
serialHost=""
|
|
serialPort=""
|
|
serialBaud=9600 # MAME's null modem's own default rate
|
|
serialArgs=() # MAME's options for the link (mameSerial)
|
|
serialPty="" # the pty socat hands Hatari, FS-UAE and Wine
|
|
relayPid=""
|
|
tcpPortMax=65535
|
|
# How long socat keeps trying to reach --serial's server before the run
|
|
# stops; an unreachable host otherwise hangs for the kernel's SYN timeout.
|
|
serialConnectSeconds=10
|
|
# DOSBox logs whether COM1 reached --serial's server as it starts.
|
|
dosSerialLogSeconds=5
|
|
# The key a headless Win16 run takes DOSBox's snapshot with (xdotool's name
|
|
# and SDL's scancode for it), and how long DOSBox gets to write it.
|
|
dosboxShotKey=F11
|
|
dosboxShotScancode=68
|
|
dosboxShotSeconds=5
|
|
# The settings a hosted program's window reads from its environment.
|
|
joeyWindowSettings=(JOEYLIB_SCALE JOEYLIB_ASPECT JOEYLIB_FULLSCREEN)
|
|
# A Be API VM: how long its boot may take before the job starts, how much
|
|
# longer the job runs than --seconds (for the snapshot), how long the
|
|
# machine takes to power off, and the job's limit when nothing sets one.
|
|
beBootSeconds=120
|
|
beSnapshotSeconds=5
|
|
beShutdownSeconds=30
|
|
beForeverSeconds=86400
|
|
# The mouse move that takes a VM's pointer to the screen's top left
|
|
# corner from anywhere.
|
|
bePointerCorner=4000
|
|
pollSeconds=0.1
|
|
|
|
|
|
usage() {
|
|
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
|
|
}
|
|
|
|
|
|
fail() {
|
|
echo "joey-run: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
|
|
cleanup() {
|
|
joeyStop "$emuPid"
|
|
joeyStop "$relayPid"
|
|
if [ -n "$xvfbPid" ]; then
|
|
kill "$xvfbPid" 2>/dev/null || true
|
|
fi
|
|
if [ -n "$work" ]; then
|
|
rm -rf "$work"
|
|
fi
|
|
}
|
|
|
|
|
|
# ---- finding the program ----------------------------------------------------
|
|
|
|
# magicPort FILE -- print the machine FILE was built for, or nothing.
|
|
magicPort() {
|
|
local file="$1"
|
|
local magic
|
|
case "$file" in
|
|
*.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV)
|
|
printf 'iigs\n'
|
|
return 0
|
|
;;
|
|
esac
|
|
magic="$(head -c 16 "$file" | od -An -tx1 | tr -d ' \n')"
|
|
case "$magic" in
|
|
000003f3*) printf 'amiga\n' ;;
|
|
601a*)
|
|
# The ST and STe builds share the .PRG format; an STe build is
|
|
# the one whose platform name (jlPlatformName) is "Atari STe".
|
|
if LC_ALL=C grep -q -a -P 'Atari STe\x00' "$file"; then
|
|
printf 'ste\n'
|
|
else
|
|
printf 'atarist\n'
|
|
fi
|
|
;;
|
|
4d5a*)
|
|
# A Windows program is an MZ file whose header (e_lfanew, at
|
|
# 0x3C) points at a "PE\0\0" signature (Win32) or an "NE" one
|
|
# (Windows 3.x); a DOS one has neither.
|
|
case "$(od -An -tx1 -j "$(od -An -tu4 -j 60 -N 4 "$file" | tr -d ' ')" -N 4 "$file" 2> /dev/null | tr -d ' \n')" in
|
|
50450000) printf 'win32\n' ;;
|
|
4e45*) printf 'win16\n' ;;
|
|
*) printf 'dos\n' ;;
|
|
esac
|
|
;;
|
|
4855*) printf 'x68000\n' ;;
|
|
44584531*) printf 'dvx\n' ;; # "DXE1": a DVX module (.app)
|
|
7f454c46*)
|
|
# ELF: a native program (the SDL3 port), or a Be API one, which
|
|
# names its system (jlPlatformName): BeOS, or Haiku, 64-bit
|
|
# (ELF class 2) or 32-bit.
|
|
if LC_ALL=C grep -q -a -P 'BeOS\x00' "$file"; then
|
|
printf 'beos\n'
|
|
elif LC_ALL=C grep -q -a -P 'Haiku\x00' "$file"; then
|
|
if [ "${magic:8:2}" = "02" ]; then
|
|
printf 'haiku\n'
|
|
else
|
|
printf 'haiku32\n'
|
|
fi
|
|
else
|
|
printf 'sdl3\n'
|
|
fi
|
|
;;
|
|
3c21646f*) printf 'web\n' ;; # "<!do(ctype html>": a web program's page
|
|
00504250*) printf 'psp\n' ;; # "\0PBP": a PSP program's EBOOT.PBP
|
|
*)
|
|
# Apple IIgs OMF: segment header byte 14 (NUMLEN) is 4 and
|
|
# byte 15 (VERSION) is 2.
|
|
if [ "${magic:28:4}" = "0402" ]; then
|
|
printf 'iigs\n'
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
|
|
# folderProgram DIR -- the one program file inside an app folder.
|
|
folderProgram() {
|
|
local file
|
|
local found=""
|
|
while IFS= read -r -d '' file; do
|
|
if [ -n "$(magicPort "$file")" ]; then
|
|
[ -z "$found" ] || fail "$1 holds more than one program; name the file"
|
|
found="$file"
|
|
fi
|
|
done < <(find "$1" -maxdepth 1 -type f -print0)
|
|
[ -n "$found" ] || fail "no program found in $1"
|
|
printf '%s\n' "$found"
|
|
}
|
|
|
|
|
|
# ---- shared run plumbing ------------------------------------------------------
|
|
|
|
# stageFolder DEST -- copy the program's folder (its files and DATA tree)
|
|
# plus --data into DEST, for runs that must not touch the build output.
|
|
stageFolder() {
|
|
local dest="$1"
|
|
local progDir
|
|
progDir="$(dirname "$program")"
|
|
mkdir -p "$dest"
|
|
find "$progDir" -maxdepth 1 -type f -exec cp -t "$dest" {} +
|
|
if [ -d "$progDir/DATA" ]; then
|
|
cp -r "$progDir/DATA" "$dest/"
|
|
fi
|
|
if [ -n "$dataDir" ]; then
|
|
mkdir -p "$dest/DATA"
|
|
cp -r "$dataDir/." "$dest/DATA/"
|
|
fi
|
|
}
|
|
|
|
|
|
# startDisplay -- a virtual X display for --headless SDL emulators.
|
|
startDisplay() {
|
|
local display
|
|
display=":$((300 + RANDOM % 500))"
|
|
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 &
|
|
xvfbPid=$!
|
|
sleep 1
|
|
export DISPLAY="$display"
|
|
export SDL_AUDIODRIVER=dummy
|
|
}
|
|
|
|
|
|
# waitOrStop -- wait for the emulator, or stop it after --seconds (taking
|
|
# the --snapshot first).
|
|
waitOrStop() {
|
|
local waited=0
|
|
local win
|
|
if [ -z "$seconds" ]; then
|
|
wait "$emuPid" || true
|
|
emuPid=""
|
|
return 0
|
|
fi
|
|
while [ "$waited" -lt "$seconds" ] && kill -0 "$emuPid" 2>/dev/null; do
|
|
sleep 1
|
|
waited=$((waited + 1))
|
|
done
|
|
if [ -n "$snapshot" ]; then
|
|
if [ "$headless" = "1" ] && [ "$snapshotWindow" = "0" ]; then
|
|
import -window root "$snapshot"
|
|
elif win="$(joeyFindWindow "$DISPLAY" "$emuPid")"; then
|
|
import -window "$win" "$snapshot"
|
|
else
|
|
echo "joey-run: no emulator window to snapshot" >&2
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
# mameStopScript FILE -- MAME Lua that snapshots (if asked) and exits after
|
|
# --seconds of emulated time.
|
|
mameStopScript() {
|
|
cat > "$1" <<LUA
|
|
local done = false
|
|
emu.register_frame_done(function()
|
|
if not done and manager.machine.time:as_double() >= $seconds then
|
|
done = true
|
|
if $([ -n "$snapshot" ] && echo true || echo false) then
|
|
manager.machine.video:snapshot()
|
|
end
|
|
manager.machine:exit()
|
|
end
|
|
end)
|
|
LUA
|
|
}
|
|
|
|
|
|
# runMame DRIVER ARGS... -- start MAME with the per-run directories, the
|
|
# headless/debug/stop options, then collect the snapshot.
|
|
runMame() {
|
|
local driver="$1"
|
|
shift
|
|
local args=(-skip_gameinfo -cfg_directory "$work/cfg" -nvram_directory "$work/nvram"
|
|
-snapshot_directory "$work/snap" -snapname "shot")
|
|
if [ "$headless" = "1" ]; then
|
|
args+=(-video none -sound none -nothrottle)
|
|
export SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy QT_QPA_PLATFORM=offscreen
|
|
else
|
|
args+=(-window)
|
|
fi
|
|
if [ "$debug" = "1" ]; then
|
|
args+=(-debug)
|
|
fi
|
|
if [ -n "$seconds" ]; then
|
|
mameStopScript "$work/stop.lua"
|
|
args+=(-autoboot_script "$work/stop.lua")
|
|
fi
|
|
(cd "$work" && exec "$MAME" "$driver" "$@" "${args[@]}") &
|
|
emuPid=$!
|
|
wait "$emuPid" || true
|
|
emuPid=""
|
|
if [ -n "$snapshot" ]; then
|
|
if [ -f "$work/snap/shot.png" ]; then
|
|
cp "$work/snap/shot.png" "$snapshot"
|
|
else
|
|
echo "joey-run: MAME saved no snapshot" >&2
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
# ---- the serial link ----------------------------------------------------------
|
|
|
|
# portFree PORT -- nothing on this machine listens on TCP PORT yet. Checked
|
|
# up front because an emulator that cannot take the port only says so in a
|
|
# log nobody reads, and runs with the serial port unplugged.
|
|
portFree() {
|
|
python3 - "$1" <<'PY'
|
|
import socket
|
|
import sys
|
|
|
|
probe = socket.socket()
|
|
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
try:
|
|
probe.bind(("", int(sys.argv[1])))
|
|
except OSError:
|
|
sys.exit(1)
|
|
PY
|
|
}
|
|
|
|
|
|
# serialNote NAME -- tell the user where the serial port (NAME) is wired.
|
|
serialNote() {
|
|
case "$serialMode" in
|
|
connect) echo "joey-run: $1 connects to $serialHost:$serialPort." ;;
|
|
listen) echo "joey-run: $1 waits for a connection on TCP port $serialPort." ;;
|
|
esac
|
|
}
|
|
|
|
|
|
# relaySerial NEAR PATH -- run socat between the emulator's end of the link
|
|
# (NEAR, a pty or unix socket that appears at PATH) and the network, and
|
|
# wait for PATH. socat opens its first address before its second, so the
|
|
# order matters. Connecting, the TCP connection goes first: an unreachable
|
|
# server stops the run before the machine boots. Listening, NEAR goes
|
|
# first: the emulator can open it at once, and what the program sends
|
|
# waits in the kernel until a client connects.
|
|
relaySerial() {
|
|
local near="$1"
|
|
local path="$2"
|
|
local deadline
|
|
command -v socat >/dev/null 2>&1 ||
|
|
fail "the serial link on this machine goes through socat, which is not installed (package socat: apt install socat, dnf install socat)"
|
|
if [ "$serialMode" = "connect" ]; then
|
|
(cd "$work" && exec socat "tcp:$serialHost:$serialPort,connect-timeout=$serialConnectSeconds" "$near") 2>"$work/socat.log" &
|
|
else
|
|
(cd "$work" && exec socat "$near" "tcp-listen:$serialPort,reuseaddr") 2>"$work/socat.log" &
|
|
fi
|
|
relayPid=$!
|
|
deadline=$((SECONDS + serialConnectSeconds + 1))
|
|
while [ ! -e "$path" ]; do
|
|
if ! joeyAlive "$relayPid" || [ "$SECONDS" -ge "$deadline" ]; then
|
|
cat "$work/socat.log" >&2
|
|
if [ "$serialMode" = "connect" ]; then
|
|
fail "cannot connect to $serialHost:$serialPort"
|
|
fi
|
|
fail "socat cannot listen on TCP port $serialPort"
|
|
fi
|
|
sleep "$pollSeconds"
|
|
done
|
|
}
|
|
|
|
|
|
# mameSerial DRIVER SLOT -- set serialArgs to the MAME options that plug a
|
|
# null modem into SLOT and wire it up. MAME's bitbanger connects out by
|
|
# itself; to listen, socat takes the TCP port and MAME connects to socat
|
|
# through a unix socket. The null modem clocks the wire itself, at
|
|
# --serial-baud, which cfg/DRIVER.cfg sets.
|
|
mameSerial() {
|
|
local driver="$1"
|
|
local slot="$2"
|
|
local bitb="socket.$serialHost:$serialPort"
|
|
[ -n "$serialMode" ] || return 0
|
|
mkdir -p "$work/cfg"
|
|
python3 "$repo/scripts/lib/mameNullModemCfg.py" "$driver" ":$slot:null_modem" "$serialBaud" \
|
|
> "$work/cfg/$driver.cfg" || fail "--serial-baud $serialBaud is not a rate MAME's null modem has"
|
|
if [ "$serialMode" = "listen" ]; then
|
|
relaySerial "unix-listen:serial.sock" "$work/serial.sock"
|
|
bitb="domain.serial.sock"
|
|
fi
|
|
serialArgs=(-"$slot" null_modem -bitb "$bitb")
|
|
}
|
|
|
|
|
|
# dosSerialCheck -- DOSBox tries --serial's server as it starts and, when
|
|
# that fails, only says so in its log and runs with COM1 unplugged; stop
|
|
# with the reason instead.
|
|
dosSerialCheck() {
|
|
local deadline=$((SECONDS + dosSerialLogSeconds))
|
|
local line
|
|
[ "$serialMode" = "connect" ] || return 0
|
|
while [ "$SECONDS" -lt "$deadline" ]; do
|
|
line="$(grep -m 1 'SERIAL: Port 1 ' "$work/dosbox.log" 2>/dev/null)" || true
|
|
case "$line" in
|
|
*"connection failed"*) fail "DOSBox cannot connect COM1 to $serialHost:$serialPort" ;;
|
|
?*) return 0 ;;
|
|
esac
|
|
sleep "$pollSeconds"
|
|
done
|
|
}
|
|
|
|
|
|
# ---- one function per machine -------------------------------------------------
|
|
|
|
runIigs() {
|
|
local name
|
|
local img
|
|
local bytes
|
|
local media=()
|
|
joeyNeed MAME mame
|
|
joeyNeed IIGS_ROMPATH iigsFirmware
|
|
[ -z "$dataDir" ] || fail "--data is not supported on the IIgs; build a disk with the data on it (a game disk)"
|
|
case "$program" in
|
|
*.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV)
|
|
joeyNeed CADIUS cadius
|
|
img="$program"
|
|
if [ "$headless" = "1" ]; then
|
|
img="$work/$(basename "$program")"
|
|
cp "$program" "$img"
|
|
fi
|
|
bytes="$(stat -c %s "$img")"
|
|
if "$CADIUS" CATALOG "$img" 2>/dev/null | grep -qE '^ ProDOS +SYS'; then
|
|
# A bootable game disk: it launches its program by itself.
|
|
if [ "$bytes" -le 900000 ]; then
|
|
media=(-flop3 "$img")
|
|
else
|
|
media=(-sl7 cffa2 -hard1 "$img")
|
|
fi
|
|
else
|
|
# A data disk: boot GS/OS beside it and pick in the Finder.
|
|
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
|
|
cp "$GSOS_SYSTEM_DISK" "$work/boot.po"
|
|
if [ "$bytes" -le 900000 ]; then
|
|
media=(-flop3 "$work/boot.po" -flop4 "$img")
|
|
else
|
|
media=(-flop3 "$work/boot.po" -sl7 cffa2 -hard1 "$img")
|
|
fi
|
|
echo "joey-run: $(basename "$img") is not bootable; GS/OS boots beside it -- open it in the Finder."
|
|
fi
|
|
;;
|
|
*)
|
|
# A bare program: pack it onto a data disk and boot a GS/OS
|
|
# whose startup program launches it (the developer path; needs
|
|
# the IIgs toolchain).
|
|
joeyNeed LLVM816_ROOT toolchain:iigs
|
|
joeyNeed CADIUS cadius
|
|
joeyNeed IIGS_CLANG_BUILD toolchain:iigs
|
|
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
|
|
name="$(basename "$program")"
|
|
BINDIR="$(dirname "$program")" JOEY_DISK_EXAMPLES="$name" \
|
|
"$repo/scripts/make-iigs-disk.sh" "$work/data.2mg" >/dev/null
|
|
"$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$name" >/dev/null
|
|
media=(-flop3 "$work/boot.po" -flop4 "$work/data.2mg")
|
|
;;
|
|
esac
|
|
mameSerial "$machine" modem
|
|
echo "joey-run: booting the IIgs ($machine); the program comes up in about a minute. Close MAME to stop."
|
|
serialNote "the modem port"
|
|
runMame "$machine" -ramsize "$ramSize" -rompath "$IIGS_ROMPATH" "${media[@]}" "${serialArgs[@]}"
|
|
}
|
|
|
|
|
|
# amigaBootFile HD VAR PATH COMPONENT LOSS -- the installed file named by
|
|
# $VAR at PATH on the boot drive HD, or say that without it LOSS.
|
|
amigaBootFile() {
|
|
local file="${!2:-}"
|
|
if [ -n "$file" ] && [ -f "$file" ]; then
|
|
mkdir -p "$1/$(dirname "$3")"
|
|
cp "$file" "$1/$3"
|
|
else
|
|
echo "joey-run: no $(basename "$3") installed (./install.sh install $4); $5 on the Amiga" >&2
|
|
fi
|
|
}
|
|
|
|
|
|
runAmiga() {
|
|
local name
|
|
local hd="$work/hd"
|
|
local args=()
|
|
joeyNeed FSUAE fsuae
|
|
name="$(basename "$program")"
|
|
# FS-UAE boots the folder as a hard drive and writes to it, so it is
|
|
# always staged. s/startup-sequence runs the program.
|
|
stageFolder "$hd"
|
|
mkdir -p "$hd/s"
|
|
printf ':%s\n' "$name" > "$hd/s/startup-sequence"
|
|
# Kickstart 3.1 has neither in ROM; AmigaOS loads them from this boot
|
|
# drive. Harmless for programs that use neither.
|
|
amigaBootFile "$hd" AMIGA_SERIAL_DEVICE Devs/serial.device amigaSerial "serial will not open"
|
|
amigaBootFile "$hd" AMIGA_LOWLEVEL_LIBRARY Libs/lowlevel.library amigaLowlevel "no joystick will be seen"
|
|
args=(--fast_memory=2048
|
|
--hard_drive_0="$hd" --hard_drive_0_label=JOEYLIB
|
|
--automatic_input_grab=0 --middle_click_ungrab=1 --mouse_integration=1
|
|
--floppy_drive_speed=800 --base_dir="$work/fs-uae")
|
|
if [ -n "$amigaModel" ]; then
|
|
# shellcheck source=scripts/lib/amigaGate.sh
|
|
source "$repo/scripts/lib/amigaGate.sh"
|
|
AMIGA_MODEL="$amigaModel"
|
|
amigaMachine
|
|
args+=("${AMIGA_MACHINE_ARGS[@]}")
|
|
elif [ -n "${KICKSTART_ROM:-}" ] && [ -f "$KICKSTART_ROM" ]; then
|
|
args+=(--amiga_model=A500 --kickstart_file="$KICKSTART_ROM")
|
|
else
|
|
args+=(--amiga_model=A500)
|
|
echo "joey-run: no Kickstart installed (./install.sh install kickstart); booting FS-UAE's AROS, which may not run every program" >&2
|
|
fi
|
|
if [ "$headless" = "1" ]; then
|
|
startDisplay
|
|
# A real gamepad crashes FS-UAE when there is no screen to grab.
|
|
args+=(--joystick_port_0=none --joystick_port_1=none)
|
|
export ALSOFT_DRIVERS=null
|
|
fi
|
|
case "$serialMode" in
|
|
connect)
|
|
# FS-UAE's tcp:// serial port only ever listens, so socat makes
|
|
# the connection and hands FS-UAE a pty.
|
|
relaySerial "pty,rawer,link=$serialPty" "$serialPty"
|
|
args+=(--amiga_enable_serial_port=1 --serial_port="$serialPty")
|
|
;;
|
|
listen)
|
|
# /wait holds the machine until the client connects, so nothing
|
|
# the program sends goes to nobody.
|
|
args+=(--amiga_enable_serial_port=1 --serial_port="tcp://0.0.0.0:$serialPort/wait")
|
|
;;
|
|
esac
|
|
echo "joey-run: booting the Amiga; $name starts from the JOEYLIB hard drive. Close FS-UAE to stop."
|
|
serialNote "the serial port"
|
|
if [ "$serialMode" = "listen" ]; then
|
|
echo "joey-run: the Amiga does not start until that connection arrives."
|
|
fi
|
|
"$FSUAE" "${args[@]}" >"$work/fs-uae.log" 2>&1 &
|
|
emuPid=$!
|
|
waitOrStop
|
|
# Keep the program's log next to it, as a direct run would.
|
|
find "$hd" -maxdepth 1 -iname 'joeylog.txt' -exec cp {} "$(dirname "$program")/" \; 2>/dev/null || true
|
|
}
|
|
|
|
|
|
runAtarist() {
|
|
local dir
|
|
local args=()
|
|
joeyNeed HATARI hatari
|
|
joeyNeed EMUTOS_IMG emutos
|
|
dir="$(dirname "$program")"
|
|
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
|
|
stageFolder "$work/hd"
|
|
dir="$work/hd"
|
|
fi
|
|
if [ -z "$stMachine" ]; then
|
|
stMachine="$([ "$detected" = "ste" ] && echo ste || echo st)"
|
|
fi
|
|
args=(--machine "$stMachine" --tos "$EMUTOS_IMG" --harddrive "$dir" --gemdos-drive C --auto "C:\\$(basename "$program")")
|
|
if [ "$headless" = "1" ]; then
|
|
startDisplay
|
|
args+=(--sound off)
|
|
fi
|
|
if [ -n "$serialMode" ]; then
|
|
# Hatari's serial port can only be a file or a tty, so socat
|
|
# stands between a pty and the network.
|
|
relaySerial "pty,rawer,link=$serialPty" "$serialPty"
|
|
args+=(--rs232-in "$serialPty" --rs232-out "$serialPty")
|
|
fi
|
|
echo "joey-run: booting the Atari ($stMachine, EmuTOS); $(basename "$program") starts from drive C. Close Hatari to stop."
|
|
serialNote "the Modem port"
|
|
"$HATARI" "${args[@]}" >"$work/hatari.log" 2>&1 &
|
|
emuPid=$!
|
|
waitOrStop
|
|
}
|
|
|
|
|
|
runDos() {
|
|
local dir
|
|
local command
|
|
local args=()
|
|
joeyNeed DOSBOX dosbox
|
|
dir="$(dirname "$program")"
|
|
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
|
|
stageFolder "$work/hd"
|
|
dir="$work/hd"
|
|
fi
|
|
command="$(basename "$program")"
|
|
if [ ${#progArgs[@]} -gt 0 ]; then
|
|
command="$command ${progArgs[*]}"
|
|
fi
|
|
args=("${JOEY_DOSBOX_ISOLATE[@]}" --conf "$repo/scripts/dosbox-386sx16.conf"
|
|
--set "mouse_capture=seamless" -c "MOUNT C \"$dir\"" -c "C:" -c "$command")
|
|
# transparent:1 -- data bytes only: DOSBox's in-band escapes for the
|
|
# control lines would reach a plain TCP peer as data.
|
|
case "$serialMode" in
|
|
connect) args+=(--set "serial1=nullmodem server:$serialHost port:$serialPort transparent:1") ;;
|
|
listen) args+=(--set "serial1=nullmodem port:$serialPort transparent:1") ;;
|
|
esac
|
|
if [ "$headless" = "1" ]; then
|
|
startDisplay
|
|
else
|
|
# Leave the program's last screen up until a key is pressed.
|
|
args+=(-c "pause")
|
|
fi
|
|
args+=(--exit)
|
|
echo "joey-run: booting DOS (386SX-16); $command runs from drive C. Close DOSBox to stop."
|
|
serialNote "COM1"
|
|
XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${args[@]}" >"$work/dosbox.log" 2>&1 &
|
|
emuPid=$!
|
|
dosSerialCheck
|
|
waitOrStop
|
|
}
|
|
|
|
|
|
runDvx() {
|
|
local dir
|
|
local app
|
|
joeyNeed DOSBOX dosbox
|
|
joeyNeed DVX_ROOT dvx
|
|
app="$(dirname "$program")"
|
|
# Built as apps/<vendor>/<name>/: install it under the same vendor.
|
|
if [ "$(basename "$(dirname "$(dirname "$app")")")" = apps ]; then
|
|
DVX_VENDOR="$(basename "$(dirname "$app")")"
|
|
fi
|
|
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
|
|
dir="$work/dvx"
|
|
else
|
|
dir="$projectRoot/build/dvx/run"
|
|
# DVX's own desktop size, where a program's window is the stage at 3x.
|
|
DVX_VIDEO_W=1024
|
|
DVX_VIDEO_H=768
|
|
fi
|
|
# shellcheck source=scripts/lib/dvxGate.sh
|
|
source "$repo/scripts/lib/dvxGate.sh"
|
|
if [ -n "$dataDir" ]; then
|
|
stageFolder "$work/app/$(basename "$app")"
|
|
app="$work/app/$(basename "$app")"
|
|
fi
|
|
dvxStage "$dir" "$app"
|
|
dvxDosboxConf "$work/dosbox.conf" "$dir" sb16
|
|
# transparent:1 -- data bytes only, as for DOS.
|
|
case "$serialMode" in
|
|
connect) printf '\n[serial]\nserial1 = nullmodem server:%s port:%s transparent:1\n' "$serialHost" "$serialPort" >> "$work/dosbox.conf" ;;
|
|
listen) printf '\n[serial]\nserial1 = nullmodem port:%s transparent:1\n' "$serialPort" >> "$work/dosbox.conf" ;;
|
|
esac
|
|
if [ "$headless" = "1" ]; then
|
|
startDisplay
|
|
fi
|
|
echo "joey-run: booting DVX in DOSBox; $(basename "$program") is its desktop program. Close DOSBox to stop."
|
|
serialNote "COM1"
|
|
XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" --conf "$work/dosbox.conf" \
|
|
--set "mouse_capture=seamless" > "$work/dosbox.log" 2>&1 &
|
|
emuPid=$!
|
|
dosSerialCheck
|
|
waitOrStop
|
|
}
|
|
|
|
|
|
runX68000() {
|
|
local name
|
|
local stage="$work/files"
|
|
joeyNeed MAME mame
|
|
joeyNeed X68K_DIR x68kFirmware
|
|
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
|
|
# shellcheck source=scripts/lib/x68kDisk.sh
|
|
source "$repo/scripts/lib/x68kDisk.sh"
|
|
name="$(basename "$program")"
|
|
# Human68k boots from a trimmed copy of the system disk whose
|
|
# AUTOEXEC.BAT runs the program from the data disk in drive B.
|
|
x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$name" || fail "cannot build the boot disk"
|
|
x68kDataDisk "$work/data.xdf" || fail "cannot build the data disk"
|
|
x68kXdf add "$work/data.xdf" "$program" "$name" >/dev/null || fail "cannot copy $name onto the data disk"
|
|
rm -rf "$stage"
|
|
mkdir -p "$stage"
|
|
if [ -d "$(dirname "$program")/DATA" ]; then
|
|
cp -r "$(dirname "$program")/DATA" "$stage/"
|
|
fi
|
|
if [ -n "$dataDir" ]; then
|
|
mkdir -p "$stage/DATA"
|
|
cp -r "$dataDir/." "$stage/DATA/"
|
|
fi
|
|
if [ -d "$stage/DATA" ]; then
|
|
x68kAddTree "$work/data.xdf" "$stage/DATA" DATA || fail "the DATA files do not fit on the 1232 KB data disk"
|
|
fi
|
|
mameSerial x68000 rs232c
|
|
echo "joey-run: booting the X68000 (Human68k 3.02); $name runs from drive B. Close MAME to stop."
|
|
serialNote "the RS-232C port"
|
|
runMame x68000 -bios ipl10 -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf" "${serialArgs[@]}"
|
|
}
|
|
|
|
|
|
# runSdl3 -- the program natively. Interactive runs use its own folder, so
|
|
# its saves and log persist; --headless and --data runs a private copy.
|
|
runSdl3() {
|
|
local dir
|
|
[ -z "$serialMode" ] || fail "the SDL3 port has no serial port"
|
|
dir="$(dirname "$program")"
|
|
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
|
|
stageFolder "$work/run"
|
|
dir="$work/run"
|
|
fi
|
|
if [ "$headless" = "1" ]; then
|
|
startDisplay
|
|
export SDL_VIDEO_DRIVER=x11 SDL_AUDIO_DRIVER=dummy JOEYLIB_SCALE=1 JOEYLIB_ASPECT=square
|
|
snapshotWindow=1
|
|
fi
|
|
echo "joey-run: running $(basename "$program") natively (SDL3). Close its window to stop."
|
|
"$dir/$(basename "$program")" "${progArgs[@]}" &
|
|
emuPid=$!
|
|
waitOrStop
|
|
}
|
|
|
|
|
|
# runWindows -- the Win32 program under Wine, in a window on your display;
|
|
# with --headless on a private virtual display at one pixel per stage pixel,
|
|
# so --snapshot is the stage. The Wine prefix (its C: drive and settings) is
|
|
# kept in the project's build/win32/prefix for interactive runs, so it is
|
|
# set up once; --headless and serial runs use a private one, so the kept
|
|
# prefix's COM1 stays whatever device of this machine it was.
|
|
runWindows() {
|
|
local dir
|
|
local title
|
|
local win
|
|
# shellcheck source=scripts/lib/wineGate.sh
|
|
source "$repo/scripts/lib/wineGate.sh"
|
|
wineNeed
|
|
dir="$(dirname "$program")"
|
|
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
|
|
stageFolder "$work/run"
|
|
dir="$work/run"
|
|
fi
|
|
title="$(basename "$program")"
|
|
title="${title%.*}"
|
|
if [ -n "$serialMode" ]; then
|
|
# Wine's COM1 can only be a device, so socat stands between a pty
|
|
# and the network; before Wine, so an unreachable server stops the
|
|
# run before Wine starts.
|
|
relaySerial "pty,rawer,link=$serialPty" "$serialPty"
|
|
fi
|
|
if [ "$headless" = "1" ]; then
|
|
wineStart "$work"
|
|
xvfbPid="$WINE_XVFB_PID"
|
|
elif [ -n "$serialMode" ]; then
|
|
winePrefix "$work"
|
|
else
|
|
wineEnv "$projectRoot/build/$detected/prefix"
|
|
fi
|
|
if [ -n "$serialMode" ]; then
|
|
wineComPort 1 "$serialPty"
|
|
fi
|
|
if [ "$headless" = "1" ]; then
|
|
echo "joey-run: running $title under Wine (headless)."
|
|
serialNote "COM1"
|
|
JOEYLIB_SCALE=1 JOEYLIB_ASPECT=square wineRun "$dir/$(basename "$program")" "${progArgs[@]}"
|
|
emuPid="$WINE_PID"
|
|
sleep "$seconds"
|
|
if [ -n "$snapshot" ]; then
|
|
if win="$(wineWindow "$title")"; then
|
|
import -window "$win" "$snapshot"
|
|
else
|
|
echo "joey-run: no $title window to snapshot" >&2
|
|
fi
|
|
fi
|
|
wineStop
|
|
emuPid=""
|
|
xvfbPid=""
|
|
return 0
|
|
fi
|
|
echo "joey-run: running $title under Wine. Close its window to stop."
|
|
serialNote "COM1"
|
|
"$WINE" "$dir/$(basename "$program")" "${progArgs[@]}" &
|
|
emuPid=$!
|
|
waitOrStop
|
|
if [ -n "$serialMode" ]; then
|
|
# The private prefix's Wine, before its folder goes.
|
|
wineStop
|
|
emuPid=""
|
|
fi
|
|
}
|
|
|
|
|
|
# runWin16 -- the program on Windows 3.11 in DOSBox (scripts/lib/win16Gate.sh),
|
|
# as Windows' shell, its folder drive D:. Interactive runs use the program's
|
|
# own folder, so saves and logs persist; --headless and --data runs a copy.
|
|
runWin16() {
|
|
local dir
|
|
local name
|
|
local win
|
|
local png=""
|
|
local found
|
|
local size=""
|
|
local deadline
|
|
local setting
|
|
local settings=()
|
|
# shellcheck source=scripts/lib/win16Gate.sh
|
|
source "$repo/scripts/lib/win16Gate.sh"
|
|
win16Need
|
|
dir="$(dirname "$program")"
|
|
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
|
|
stageFolder "$work/run"
|
|
dir="$work/run"
|
|
fi
|
|
name="$(basename "$program")"
|
|
# transparent:1 -- data bytes only, as for DOS.
|
|
case "$serialMode" in
|
|
connect) WIN16_DOSBOX_ARGS+=(--set "serial1=nullmodem server:$serialHost port:$serialPort transparent:1") ;;
|
|
listen) WIN16_DOSBOX_ARGS+=(--set "serial1=nullmodem port:$serialPort transparent:1") ;;
|
|
esac
|
|
win16Stage "$work" "$dir" "$name" "${progArgs[@]}"
|
|
# The program reads its settings from the DOS environment Windows
|
|
# started from: this one's go there, as a Win32 or SDL3 program inherits
|
|
# them.
|
|
for setting in "${joeyWindowSettings[@]}"; do
|
|
[ -z "${!setting:-}" ] || settings+=("$setting=${!setting}")
|
|
done
|
|
if [ "$headless" = "0" ]; then
|
|
echo "joey-run: booting Windows 3.11 in DOSBox; ${name%.*} runs from drive D:. Close it to stop."
|
|
serialNote "COM1"
|
|
WIN16_HEADLESS=0 WIN16_SOUND=1 WIN16_WALL=0 win16Start "$work" "${settings[@]}"
|
|
emuPid="$WIN16_PID"
|
|
dosSerialCheck
|
|
waitOrStop
|
|
return 0
|
|
fi
|
|
mkdir -p "$work/shots"
|
|
# DOSBox's raw snapshot on a key of its own: its Ctrl+F5 would hand the
|
|
# program a Ctrl press, which ends one that waits for any key while the
|
|
# snapshot is being taken. A mapper file replaces every binding, which a
|
|
# headless run, which types nothing, does not miss.
|
|
printf 'hand_rawshot "key %s"\n' "$dosboxShotScancode" > "$work/mapper.txt"
|
|
WIN16_DOSBOX_ARGS+=(--set "capture_dir=$work/shots" --set "mapperfile=$work/mapper.txt")
|
|
echo "joey-run: running ${name%.*} on Windows 3.11 in DOSBox (headless)."
|
|
serialNote "COM1"
|
|
win16Start "$work" "${settings[@]}" JOEYLIB_SCALE=1 JOEYLIB_ASPECT=square
|
|
emuPid="$WIN16_PID"
|
|
xvfbPid="$WIN16_XVFB_PID"
|
|
dosSerialCheck
|
|
sleep "$seconds"
|
|
if [ -n "$snapshot" ]; then
|
|
if win="$(win16Window)"; then
|
|
DISPLAY="$WIN16_DISPLAY" xdotool key --window "$win" "$dosboxShotKey"
|
|
# DOSBox makes the file before it writes it: it is done when it
|
|
# has bytes and one more poll adds none.
|
|
deadline=$((SECONDS + dosboxShotSeconds))
|
|
while [ "$SECONDS" -lt "$deadline" ]; do
|
|
sleep "$pollSeconds"
|
|
found="$(find "$work/shots" -name '*.png' -print -quit)"
|
|
[ -s "$found" ] || continue
|
|
if [ "$(stat -c %s "$found")" = "$size" ]; then
|
|
png="$found"
|
|
break
|
|
fi
|
|
size="$(stat -c %s "$found")"
|
|
done
|
|
if [ -n "$png" ]; then
|
|
cp "$png" "$snapshot"
|
|
else
|
|
echo "joey-run: DOSBox took no snapshot" >&2
|
|
fi
|
|
else
|
|
echo "joey-run: no DOSBox window to snapshot" >&2
|
|
fi
|
|
fi
|
|
win16Stop
|
|
emuPid=""
|
|
xvfbPid=""
|
|
}
|
|
|
|
|
|
# runWeb -- the program's page, served from its folder: in your browser, or
|
|
# with --headless in headless Chromium.
|
|
runWeb() {
|
|
local page
|
|
[ -z "$serialMode" ] || fail "the web port has no serial port"
|
|
[ -z "$dataDir" ] || fail "--data does not apply to the web port: its DATA is packed in when it is built"
|
|
# shellcheck source=scripts/lib/webGate.sh
|
|
source "$repo/scripts/lib/webGate.sh"
|
|
page="$(basename "$program")"
|
|
if [ "$headless" = "1" ]; then
|
|
webNeed
|
|
webServe "$(dirname "$program")"
|
|
echo "joey-run: running $page in headless Chromium."
|
|
webDrive "$page?scale=1&aspect=square" --step "wait:$seconds" ${snapshot:+--screenshot "$snapshot"} || true
|
|
webStop
|
|
return 0
|
|
fi
|
|
webServe "$(dirname "$program")"
|
|
echo "joey-run: serving $page at http://127.0.0.1:$WEB_PORT/$page -- Ctrl+C stops."
|
|
if command -v xdg-open > /dev/null; then
|
|
xdg-open "http://127.0.0.1:$WEB_PORT/$page" > /dev/null 2>&1 || true
|
|
fi
|
|
if [ -n "$seconds" ]; then
|
|
sleep "$seconds"
|
|
webStop
|
|
else
|
|
wait "$WEB_SERVER_PID" || true
|
|
fi
|
|
}
|
|
|
|
|
|
# runPsp -- the program headless under PPSSPPHeadless, from a copy of its
|
|
# folder (and --data) on a private memory stick.
|
|
runPsp() {
|
|
local name
|
|
local folder
|
|
[ -z "$serialMode" ] || fail "the PSP port has no serial port"
|
|
[ "$headless" = "1" ] || fail "the PSP port runs here only with --headless; to play it, copy $(dirname "$program") into PSP/GAME/ on a PSP's memory stick, or open its EBOOT.PBP in PPSSPP"
|
|
# shellcheck source=scripts/lib/pspGate.sh
|
|
source "$repo/scripts/lib/pspGate.sh"
|
|
pspNeed
|
|
name="$(basename "$(dirname "$program")")"
|
|
stageFolder "$work/stage/$name"
|
|
folder="$(pspStage "$work" "$work/stage/$name")"
|
|
echo "joey-run: running $name in PPSSPP (headless)."
|
|
pspRun "$work" "$folder" "$seconds" ${snapshot:+--snapshot="$snapshot"}
|
|
emuPid="$PSP_PID"
|
|
pspWait
|
|
emuPid=""
|
|
pspStrictCheck "$work" "$(dirname "$program")" || true
|
|
}
|
|
|
|
|
|
# runBe -- a Be API program (haiku, haiku32, beos) in its VM under QEMU
|
|
# (scripts/lib/beGate.sh), from a copy of its folder on the VM's job disk:
|
|
# in QEMU's window, with sound; the machine powers off when the program
|
|
# ends, and what it wrote in its folder (saves, its log) is copied back.
|
|
# --headless: no window and no sound, at one window pixel per stage pixel,
|
|
# and --snapshot is QEMU's picture of the whole screen. --seconds counts
|
|
# from when the job starts, not from the boot.
|
|
runBe() {
|
|
local name
|
|
local dir
|
|
local entry
|
|
local settings=()
|
|
local setting
|
|
local jobSeconds
|
|
[ -z "$serialMode" ] || fail "the $detected port has no serial port here"
|
|
# shellcheck source=scripts/lib/beGate.sh
|
|
source "$repo/scripts/lib/beGate.sh"
|
|
beNeed "$detected"
|
|
name="$(basename "$program")"
|
|
dir="$(dirname "$program")"
|
|
stageFolder "$work/job/$name"
|
|
if [ "$headless" = "1" ]; then
|
|
settings=(JOEYLIB_SCALE=1 JOEYLIB_ASPECT=square)
|
|
else
|
|
BE_DISPLAY=sdl
|
|
BE_AUDIODEV="sdl,id=beSound"
|
|
for setting in "${joeyWindowSettings[@]}"; do
|
|
if [ -n "${!setting:-}" ]; then
|
|
settings+=("$setting=${!setting}")
|
|
fi
|
|
done
|
|
fi
|
|
# The job outlasts --seconds a little, so the snapshot finds the
|
|
# program still up.
|
|
jobSeconds=$((${seconds:-$beForeverSeconds} + beSnapshotSeconds))
|
|
beJobScript "$name" "$name" "" "$jobSeconds" "${settings[@]}" > "$work/job/job.sh"
|
|
beJobDisk "$work/job.img" "$work/job" || fail "could not make the VM's job disk"
|
|
echo "joey-run: running $name in the $detected VM (QEMU); it powers off when the program ends."
|
|
beStart "$work" "$work/job.img" || fail "the $detected VM did not start (QEMU's log: $work/qemu.log)"
|
|
emuPid="$BE_PID"
|
|
beWaitJob "$work" "$beBootSeconds" || fail "the $detected VM did not start the job within $beBootSeconds s"
|
|
if [ -n "$snapshot" ]; then
|
|
# Nothing moves the VM's pointer, so it would stay in the middle of
|
|
# the screen, over the window (which hides it only once it moves
|
|
# there): put it in the corner before the window opens.
|
|
beQmp "$work" move "-$bePointerCorner" "-$bePointerCorner" || true
|
|
fi
|
|
if [ -n "$seconds" ]; then
|
|
sleep "$seconds"
|
|
if [ -n "$snapshot" ]; then
|
|
beQmp "$work" screendump "$snapshot" || echo "joey-run: no screen to snapshot" >&2
|
|
fi
|
|
beWaitExit "$work" "$((beSnapshotSeconds + beShutdownSeconds))" || beStop
|
|
else
|
|
wait "$emuPid" || true
|
|
fi
|
|
emuPid=""
|
|
BE_PID=""
|
|
if [ "$headless" = "0" ] && [ -z "$dataDir" ]; then
|
|
if ! beJobGet "$work/job.img" "$name" "$work/back"; then
|
|
echo "joey-run: could not copy the program's files back off the job disk (was QEMU closed before the machine powered off?)" >&2
|
|
return 0
|
|
fi
|
|
# Only what the run made or changed: the folder may hold other
|
|
# programs, which keep their files and times.
|
|
for entry in "$work/back"/*; do
|
|
case "$(basename "$entry")" in
|
|
"$name"|DATA) ;;
|
|
*)
|
|
if [ -d "$entry" ] || ! cmp -s "$entry" "$dir/$(basename "$entry")"; then
|
|
cp -r "$entry" "$dir/"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
fi
|
|
}
|
|
|
|
|
|
# ---- main ---------------------------------------------------------------------
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--port) port="${2:?--port needs iigs, amiga, atarist, ste, dos, x68000, dvx, sdl3, web, win32, win16, psp, haiku, haiku32 or beos}"; shift ;;
|
|
--project)
|
|
[ -d "${2:-}" ] || fail "--project needs a project folder (one built on make/project.mk)"
|
|
projectRoot="$(cd "$2" && pwd)"
|
|
shift
|
|
;;
|
|
--data) dataDir="${2:?--data needs a folder}"; shift ;;
|
|
--headless) headless=1 ;;
|
|
--seconds) seconds="${2:?--seconds needs a number}"; shift ;;
|
|
--snapshot) snapshot="${2:?--snapshot needs a file name}"; shift ;;
|
|
--machine)
|
|
case "${2:-}" in
|
|
rom01) machine="apple2gsr1" ;;
|
|
rom03) machine="apple2gs" ;;
|
|
st|megast|ste|megaste) stMachine="$2" ;;
|
|
a500|a1200|a4000) amigaModel="${2^^}" ;;
|
|
*) fail "--machine takes rom01 or rom03 (IIgs), st, megast, ste or megaste (Atari), or a500, a1200 or a4000 (Amiga)" ;;
|
|
esac
|
|
shift
|
|
;;
|
|
--ram) ramSize="${2:?--ram needs a size such as 4M}"; shift ;;
|
|
--debug) debug=1 ;;
|
|
--serial)
|
|
[ -z "$serialMode" ] || fail "one --serial or --serial-listen per run"
|
|
[[ "${2:-}" =~ ^([^:]+):([0-9]+)$ ]] || fail "--serial takes HOST:PORT (a host name or IPv4 address)"
|
|
serialMode=connect
|
|
serialHost="${BASH_REMATCH[1]}"
|
|
serialPort="${BASH_REMATCH[2]}"
|
|
shift
|
|
;;
|
|
--serial-listen)
|
|
[ -z "$serialMode" ] || fail "one --serial or --serial-listen per run"
|
|
[[ "${2:-}" =~ ^[0-9]+$ ]] || fail "--serial-listen takes a port number"
|
|
serialMode=listen
|
|
serialPort="$2"
|
|
shift
|
|
;;
|
|
--serial-baud) serialBaud="${2:?--serial-baud needs a rate such as 2400}"; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--) shift; progArgs=("$@"); break ;;
|
|
-*) fail "unknown option $1 (see: joey-run --help)" ;;
|
|
*)
|
|
[ -z "$program" ] || fail "one program at a time (got '$program' and '$1')"
|
|
program="$1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -n "$program" ] || { usage; exit 2; }
|
|
[[ "$seconds" =~ ^[0-9]*$ ]] || fail "--seconds takes a whole number"
|
|
if [ "$headless" = "1" ] && [ -z "$seconds" ]; then
|
|
seconds=60
|
|
fi
|
|
if [ -n "$snapshot" ]; then
|
|
[ -n "$seconds" ] || fail "--snapshot needs --seconds (or --headless)"
|
|
snapshot="$(realpath -m "$snapshot")"
|
|
fi
|
|
if [ -n "$dataDir" ]; then
|
|
[ -d "$dataDir" ] || fail "--data: no such folder $dataDir"
|
|
dataDir="$(cd "$dataDir" && pwd)"
|
|
fi
|
|
if [ -n "$serialMode" ]; then
|
|
[ "$serialPort" -ge 1 ] && [ "$serialPort" -le "$tcpPortMax" ] || fail "TCP port $serialPort is not 1 to $tcpPortMax"
|
|
fi
|
|
if [ "$serialMode" = "listen" ]; then
|
|
portFree "$serialPort" || fail "TCP port $serialPort is already in use"
|
|
fi
|
|
|
|
# Resolve PROGRAM to a file and its machine.
|
|
if [ -d "$program" ]; then
|
|
program="$(folderProgram "$program")"
|
|
elif [ ! -e "$program" ]; then
|
|
[ -n "$port" ] || fail "$program is not a file; for a program name add --port"
|
|
program="$(joeyExampleProgram "$program" "$port" "$projectRoot")" || exit 1
|
|
[ -e "$program" ] || fail "$program is not built (make $port in $projectRoot)"
|
|
fi
|
|
program="$(cd "$(dirname "$program")" && pwd)/$(basename "$program")"
|
|
detected="$(magicPort "$program")"
|
|
[ -n "$detected" ] || fail "cannot tell which machine $program is for"
|
|
if [ -n "$port" ] && [ "$port" != "$detected" ]; then
|
|
fail "$program is a $detected program, not $port"
|
|
fi
|
|
if [ ${#progArgs[@]} -gt 0 ] && [ "$detected" != "dos" ] && [ "$detected" != "sdl3" ] && [ "$detected" != "win32" ] && [ "$detected" != "win16" ]; then
|
|
fail "program arguments only work on DOS, SDL3, Win32 and Win16"
|
|
fi
|
|
|
|
work="$(mktemp -d -t joey-run.XXXXXX)"
|
|
serialPty="$work/serial.pty"
|
|
trap cleanup EXIT INT TERM
|
|
|
|
case "$detected" in
|
|
iigs) runIigs ;;
|
|
amiga) runAmiga ;;
|
|
atarist|ste) runAtarist ;;
|
|
dos) runDos ;;
|
|
x68000) runX68000 ;;
|
|
dvx) runDvx ;;
|
|
sdl3) runSdl3 ;;
|
|
web) runWeb ;;
|
|
win32) runWindows ;;
|
|
win16) runWin16 ;;
|
|
psp) runPsp ;;
|
|
haiku|haiku32|beos) runBe ;;
|
|
esac
|
|
if [ -n "$snapshot" ] && [ -f "$snapshot" ]; then
|
|
echo "joey-run: snapshot saved to $snapshot"
|
|
fi
|