webstation-broker
DeveloperPython referenceemulatorsemulators.base

emulators.base.Emulator

Contract every launcher implements, plus the process plumbing they share.

Contract every launcher implements, plus the process plumbing they share.

A subclass describes itself through the class attributes below and must override launch and resolve_rom_file; both raise NotImplementedError here. Everything else is an optional hook with a safe default:

  • save_state, load_state, state_path, state_target and wait_for_state are the save-state hooks. The broker only calls the first two when supports_states is on; the defaults report an empty slot. The frame RomM shows beside a state is not the emulator's to produce: the broker captures the streamed desktop on the way into a save and keeps it on state_screenshot.
  • swap_disc is only called when supports_disc_swap is on.
  • toggle_menu is only called when supports_menu is on.
  • memory_card_path pairs with memory_card_subtree for emulators whose whole memory card travels on its own routes.
  • clear_working_slot and prepare_restore both run at every activate. Any subclass with real save data has to clear the previous session's saves in one of them and declare clears_stale_saves; see clear_working_slot for the contract.
  • save_and_exit is the exit path; the default writes no state.

The lifecycle as the broker drives it:

  1. Activate: clear_working_slot drops the previous session's leftover saves and states, prepare_restore runs (always, archive or not), and the incoming save archive is extracted into save_root, scoped to save_subtrees.
  2. launch spawns the process through _spawn, which captures output to log_path, starts it in its own session and records its pid so a later broker process can reap it (see reap_orphan).
  3. Mid-session: save_state and load_state work the one slot in state_slot. The state-file routes serve state_path and write through state_target, with wait_for_state bridging a resume state that arrives after launch.
  4. save_and_exit saves when asked and stops. stop sends SIGTERM to the process group, escalates to SIGKILL after term_timeout, and _forget drops both the handle and the pid record once the process is confirmed gone.

Attributes

attributenamestr
= 'base'

Registry key and log label for the emulator.

attributedisplay_namestr
= 'Webstation'

Human-readable name the UI shows.

attributeplatformOptional[str]
= None

The RomM platform slug the session was activated for, or None.

attributecoreOptional[str]
= None

The core RomM asked for (rom.core), for a launcher that fronts many; None for its default.

Set by the activate route and the import-spec route, like platform.

attributeexperimental_coresbool
= False

Whether RomM opted this launch in to known-broken cores (rom.experimental_cores).

attributelanguageOptional[str]
= None

The language the rom was activated for, or None.

attributegui_languageOptional[str]
= None

The player's own interface language, or None.

attributerequires_rombool
= True

Whether a launch needs a ROM; the desktop session does not.

attributerom_cacheablebool
= False

Whether the ROM cache may boot this emulator from a local copy of its ROM.

attributesave_rootPath
= Path('/config')

Root of the emulator's writable data.

attributesave_subtreestuple[str, ...]
= ()

Subtrees under save_root that hold save data; save restore and dump are scoped to these.

attributestate_subtreestuple[str, ...]
= ()

The subset of save_subtrees holding savestates, for labelling an archive's members.

attributerom_extensionstuple[str, ...]
= ()

File extensions the emulator will boot, in preference order.

attributesupports_statesbool
= False

Whether the emulator can save and load state mid-session.

attributesupports_disc_swapbool
= False

Whether the emulator can change the mounted disc without restarting.

attributesupports_menubool
= False

Whether the room can open and close the emulator's own menu.

attributestate_slotint
= 0

The one slot the broker saves into.

attributestate_dirPath
= Path('/config')

Where that slot's file lives.

attributelog_pathPath
= Path('/config/broker-app.log')

Where the emulator's stdout and stderr are appended.

attributeterm_timeoutfloat
= _DEFAULT_TERM_TIMEOUT

Seconds SIGTERM gets before escalating to SIGKILL.

attributekill_timeoutfloat
= _DEFAULT_KILL_TIMEOUT

Seconds SIGKILL gets before the process is written off.

attributeclears_stale_savesbool
= False

The subclass's declaration that it wipes the previous session's save data before a restore.

attributememory_card_subtreeOptional[str]
= None

The save subtree holding the whole memory card, or None for emulators without one.

attributememory_card_markerOptional[str]
= None

A file the emulator needs inside the card directory before it treats it as a card, or None.

attribute_procOptional[subprocess.Popen[bytes]]
= None
attributeboot_failedbool
= False

Set by an emulator that can tell its process is alive but never reached a running game.

attributeextraction_phaseOptional[str]
= None

Set while a slow pre-launch extraction is running, else None.

attributestate_screenshotOptional[bytes]
= None

The frame captured just before the working slot's state was written, as PNG bytes.

Set by the broker on every confirmed save and replaced, or cleared, by the next one, so it never outlives the state it was taken with.

attributeexit_codeOptional[int]

The spawned process's exit code once it has exited, else None.

Negative for a process killed by a signal, as subprocess reports it: -11 is a segfault.

attributeimport_identityOptional[imports.SessionIdentity]
= None

The game id this session runs as, set on every activate.

Preflight sets it when the archive has imports; resolve_activate_identity sets it when it has none. The push route's identity checks read it.

attributelink_rootstuple[Path, ...]

Directories a save subtree may be a symlink to although they lie outside save_root.

The chain check and the write check refuse a path through a link that resolves outside the save root. An emulator whose data is split across two trees declares the second tree here, and a subtree link that resolves exactly to it is allowed.

attributerestore_subtreestuple[str, ...]

The subtrees an archive may restore into, read before the working slot is cleared.

Functions

func__init__(self) -> None

Start with no process handle, no boot failure, no extraction running, and no frame.

paramself

Returns

None
func_spawn(self, cmd, env, stdin_pipe=False, stdin_tty=False) -> None

Start the app in its own process group with output captured.

A launch banner and then the child's stdout and stderr are appended to log_path; if the log cannot be opened the output is discarded. The pid is recorded through _record_pid once the process is up.

Raises

  • ValueError: When both stdin_pipe and stdin_tty are set.
  • OSError: When the process started but its pid could not be recorded. The process is killed first: an emulator no record names outlives the next broker restart with nothing able to find it, so the launch fails rather than leaving one behind.
paramself
paramcmdlist[str]

The argv to run.

paramenvdict[str, str]

The environment to run it in, normally base_launch_env().

paramstdin_pipebool
= False

Keep the child's stdin as a pipe so emulators with a stdin control protocol (shadPS4 IPC) can be driven headlessly.

paramstdin_ttybool
= False

Give the child a pseudo-terminal for stdin, for apps that print an error to stdout and exit when stdin is a terminal but raise a blocking dialog when it is not (Xenia).

Returns

None
funcalive(self) -> bool

Whether a spawned process exists and has not exited.

paramself

Returns

bool
func_forget(self) -> None

Drop the handle on the emulator and the record of it on disk.

Every path that ends with the process gone has to go through here. A graceful exit that only clears _proc leaves a record pointing at a pid nobody owns, and the next broker start would hunt it.

paramself

Returns

None
funcstop(self) -> None

Terminate the running emulator, if any, and forget it.

The process group gets SIGTERM, escalating to SIGKILL once term_timeout passes, and SIGKILL gets kill_timeout to land. A process that is already gone is a no-op.

The handle and the pid record are only dropped once the process is confirmed gone. An emulator that outlived SIGKILL, or that refused the signal outright, keeps both: dropping the record while it still runs would leave it with nothing able to find it, which is the orphan PID_FILE exists to prevent, and every subclass launch opens with a stop that would then be its next chance to try again.

This reaches the process and its group, which is every emulator here. An app that starts other apps and detaches them needs more than a signal to one group, and overrides this (see Desktop.stop).

paramself

Returns

None
funcprepare_restore(self) -> None

Hook run at activate, after clear_working_slot and before any extract.

Called on every activate, whether or not there is an archive to restore: a session that starts with no archive is exactly the one where a stale save left in place would be picked up as the player's own.

Default: nothing. Override to clear anything that would block or outrank the restore: a process holding a save file open, or an existing file the newer-file guard would wrongly keep over the archived one.

paramself

Returns

None
funclaunch(self, rom_path, resume_slot) -> None

Start the emulator on rom_path, optionally resuming a state.

Raises

  • NotImplementedError: Always; every subclass overrides this.
paramself
paramrom_pathOptional[Path]

The file to boot, as returned by resolve_rom_file, or None for an emulator that does not require a ROM.

paramresume_slotOptional[int]

The slot to load once the game is up, or None for a fresh start.

Returns

None
funcsave_state(self, slot) -> bool

Save the running game to slot.

Only called when supports_states.

Raises

  • NotImplementedError: When the emulator does not support states.
paramself
paramslotint

The slot RomM asked for; implementations may resolve it to state_slot.

Returns

bool

True once the state is confirmed written.

funcload_state(self, slot) -> bool

Load slot into the running game.

Only called when supports_states.

Raises

  • NotImplementedError: When the emulator does not support states.
paramself
paramslotint

The slot RomM asked for; implementations may resolve it to state_slot.

Returns

bool

True once the load was delivered to the emulator.

funcstate_path(self) -> Optional[Path]

The file the working slot holds right now, or None if it is empty.

This is what the state-file GET serves, so it has to be the file the emulator just wrote, not the newest state in the directory: another slot or another game's state would otherwise be filed in RomM as this save.

paramself

Returns

typing.Optional

The state file's path, or None. The default reports an empty slot.

funcnote_state_handout(self) -> None

Record that the state-file GET route is about to read the file state_path returned.

That read moves the file's access time, which some implementations also watch to confirm a load actually happened. Called before the route reads the file's bytes, so the window it opens covers that read instead of starting after the access time has already moved; callers of load_state can then tell their own load's read from this one. The default does nothing, for implementations that confirm loads some other way.

paramself

Returns

None
funcnote_broker_state_write(self, path) -> None

Record that the broker itself just wrote path as a state file.

Called right after a mid-session state push (the state-file PUT route) lands the file on disk. An implementation that infers where a core actually saves by watching the filesystem needs to tell its own writes apart from the core's, since a push the broker made itself proves nothing about the core's own behavior. The default does nothing, for implementations with no such inference to protect.

paramself
parampathPath

The state file's path, as returned by state_target.

Returns

None
funclock_for_state_write(self) -> bool

Take whatever lock a pushed state file should hold before it overwrites the working slot.

A push lands as a raw file replace with no confirmation step of its own, so nothing else stops it landing in the middle of another implementation's own confirmation window for that same file (a resume retry, an in-flight load) and being mistaken for whatever that other read or write was watching for. The default has no such window to protect and always succeeds.

paramself

Returns

bool

True once locked (or when there is nothing to lock); False when

funcunlock_state_write(self) -> None

Release whatever lock_for_state_write took. No-op by default.

Only called after a lock_for_state_write call that returned True.

paramself

Returns

None
func_clear_subtree(self, subtree, keep=None) -> None

Empty one of save_subtrees without removing the directory itself.

The directory stays because an emulator that finds its save directory missing at launch writes its saves somewhere else entirely, or refuses to boot at all.

paramself
paramsubtreestr

A path relative to save_root, as save_subtrees names it.

paramkeepOptional[Callable[[Path], bool]]
= None

Called with each top-level entry; returning True leaves that entry in place. The default clears everything.

Returns

None
func_clear_save_subtrees(self, excluded=(), keep=None) -> None

Empty every save subtree this session is responsible for.

paramself
paramexcludedtuple[str, ...]
= ()

Subtrees the whole-card routes carry, which this session must leave alone; the card is laid down before activate runs, so clearing one here would delete what RomM just synced.

paramkeepOptional[Callable[[Path], bool]]
= None

Passed through to _clear_subtree for each subtree.

Returns

None
funcclear_working_slot(self, excluded=()) -> None

Drop what an earlier session left in the save tree, before the restore.

Every subclass that carries real save data MUST empty the save tree here (or in prepare_restore) and declare clears_stale_saves. This is not an optimisation and not optional. The restore only overwrites the members the incoming archive happens to carry, so anything the previous session wrote and this one's archive does not name survives untouched: on a pooled or shared container that is one player's saves left readable, and mixed into the dump, in the next player's session. A clear scoped to a state slot or to one title is not enough on its own unless nothing else under save_subtrees can hold another player's data.

Deleting more than the container's own leftovers is the failure in the other direction, so the clear is scoped to save_subtrees and runs before the archive is extracted, never after. _clear_save_subtrees is the usual implementation; an emulator with app data, DLC or quarantined files sharing a subtree passes a keep predicate.

The default only reports the gap: the base class cannot know which paths are safe to delete, so an emulator with save data that reaches this warns rather than silently starting a session on the last player's files.

paramself
paramexcludedtuple[str, ...]
= ()

Save subtrees the whole-card routes carry for this session, which the clear must leave alone.

Returns

None
funcmemory_card_path(self, platform=None) -> Optional[Path]

The directory holding the card the memory-card routes sync, or None.

The broker names the card rather than reading the name out of the emulator's own config, because RomM lays a card down before the first launch has written that config. platform is the ROM's platform slug, for an emulator whose card exists on only one of several platforms it serves (GameCube vs Wii on Dolphin); most emulators ignore it.

paramself
paramplatformOptional[str]
= None

Returns

typing.Optional

The card directory, or None for emulators without a memory card.

funcarrange_card(self, heads) -> memcard.Placement

Where each member of a card pushed on the memory-card route goes inside the card.

The default writes every member where the image put it. An emulator that reads only one layout overrides it to move a member the player packed differently to where the emulator will look for it.

paramself
paramheadsdict[str, bytes]

Each member's zip name mapped to its first memcard.HEAD_BYTES bytes.

Returns

webstation_broker.memcard.Placement

Every member name mapped to its destination relative to the card

funcselect_core(self) -> None

Resolve core for this launch, before anything is cleared.

Default: a launcher that is its own backend takes no core.

Raises

  • CoreRejectedError: When core is set.
paramself

Returns

None
funccore_identity(self) -> dict[str, Any]

Extra manifest and activate-response fields describing the running core.

paramself

Returns

dict

Nothing by default.

funcstate_library(self, path) -> Optional[str]

The folder RomM should file the state at path under.

paramself
parampathPath

The state file the state GET is serving.

Returns

typing.Optional

None by default: only RetroArch sorts states into per-core folders.

funcadopt_archive_identity(self, identity) -> None

Learn from the restored archive's session identity, before the launch.

Default: nothing. A launcher that fronts many cores reads the previous session's core here.

paramself
paramidentityOptional[Mapping[str, Any]]

The restored archive's manifest session, or None.

Returns

None
funcarchive_core(self) -> Optional[str]

The core or backend actually running the game, or None.

Only meaningful for a launcher that is one shell over many cores; it goes in the archive manifest so the parent can tell a RetroArch PSP archive from a standalone PPSSPP one.

paramself

Returns

typing.Optional

The core name, or None for emulators that are their own backend.

funccarry_save_across_cores(self, identity, rom_file) -> None

Move a battery save into the new core's save dir on a core switch.

Default: nothing. A launcher that fronts many cores, each sorting its saves under its own dir, overrides this to carry the file across.

paramself
paramidentityOptional[Mapping[str, Any]]

The restored archive's manifest session, or None.

paramrom_fileOptional[Path]

The file being booted, or None.

Returns

None
funcsave_file_kind(self, rel) -> str

What an archive member holds, for the manifest the parent reads.

Every emulator lays its save directories out differently, so this is what lets the parent sort an archive without a table of those layouts. The default classifies by subtree; emulators whose states and saves share a directory override it.

paramself
paramrelstr

The member path, relative to save_root and posix-separated.

Returns

str

One of state, state_screenshot, memcard or save.

funcalways_restore(self, rel) -> bool

Whether an archive member outranks whatever sits on disk at activate.

The restore skips a member the disk already holds a newer copy of, so it can never roll back progress made since the archive was taken. That reasoning only holds for files whose mtime records this player's own saving. An emulator with a file the container writes on behalf of whoever is using it (a signed-in profile package, an account store) overrides this: the freshest copy of one of those is the last player's, and passing over the incoming one silently runs the session under their identity.

paramself
paramrelstr

The member path, relative to save_root and posix-separated.

Returns

bool

True to restore the member unconditionally. The default exempts

funcimport_spec(self) -> imports.ImportSpec

What this emulator accepts as a declared import, on self.platform.

paramself

Returns

webstation_broker.imports.ImportSpec

The spec. The default accepts nothing, so every import member is

funcplace_import(self, member, spec, ctx) -> Union[imports.Placement, imports.ImportRefusal]

Place one member that passed hygiene and the kind gate.

Must be pure: nothing is written, nothing is deleted, and the only reads allowed are member.head() and the rom through ctx.

paramself
parammemberimports.ImportMember

The member.

paramspecimports.ImportSpec

This emulator's spec, as import_spec answered it.

paramctximports.ImportCtx

The launch context.

Returns

typing.Union

Where the member lands, or why it cannot. The default refuses.

funcvalidate_import_plan(self, plan, ctx) -> list[imports.ImportRefusal]

Check the placements together, after the shared checks.

paramself
paramplanlist[imports.Placement]

Every placement.

paramctximports.ImportCtx

The launch context.

Returns

list

Any refusals. The default has none.

funcidentity_source(self) -> Optional[imports.IdentitySource]

Where this emulator's session identity comes from.

paramself

Returns

typing.Optional

The source, or None when the emulator checks no identity.

funcstate_target(self, filename) -> Optional[Path]

Where a pushed state called filename belongs.

Validating the name against the emulator's own convention is what keeps a caller from dropping arbitrary files into the save tree. The slot in it is not part of that test: RomM holds the library, so a stored state carries whatever slot it was captured in and lands in this broker's own working slot regardless.

paramself
paramfilenamestr

The name the pushed state was stored under.

Returns

typing.Optional

The path to write it to, or None if the name is not one this

funccheck_state_bytes(self, head) -> bool

Tell whether a pushed state may be written for this session, going by its first bytes.

The push route calls this once the whole body is in, before the state replaces the slot's. The base takes every state: only an emulator whose states open with the id of the game they belong to can tell.

paramself
paramheadbytes

Up to STATE_HEAD_BYTES bytes from the start of the pushed state.

Returns

bool

False when the state provably belongs to another game.

funcwait_for_state(self, deadline, poll=0.5) -> bool

Block until the working slot holds a state file, or deadline passes.

A resume state can turn up after launch: the state-file routes only answer while a session is up, so RomM pushes its pick once activate has returned and the game is already booting. Waiting for it here is what keeps a deferred resume load from firing on a slot that is still empty and reporting a fresh start.

paramself
paramdeadlinefloat

A time.monotonic() value to give up at.

parampollfloat
= 0.5

Seconds between checks of state_path.

Returns

bool

True if the slot holds a state file by the time this returns.

funcsave_and_exit(self, slot) -> dict[str, Any]

Save state (best effort) and stop.

Default: nothing to save. A slot of None is an exit that writes no state. The game's own save data is still flushed and shipped: not writing a state is the whole of "exit without saving", and discarding an in-game save the player made at a save point would be losing real progress.

paramself
paramslotOptional[int]

The slot to save into before stopping, or None to skip the state save.

Returns

dict

A dict with `{"state_saved", "state_slot", "state_file",

funcresolve_rom_file(self, path) -> Optional[Path]

File the emulator should boot for path (folder or file).

Raises

  • NotImplementedError: Always; every subclass overrides this.
paramself
parampathPath

The ROM as RomM delivered it, either a single file or a folder holding the game's files.

Returns

typing.Optional

The file to hand to launch, or None if nothing bootable is there.

funcswap_disc(self, path) -> bool

Mount path in place of the running disc.

Only called when supports_disc_swap.

Raises

  • NotImplementedError: When the emulator has no tray.
paramself
parampathPath

The disc image to mount.

Returns

bool

True once the new disc is in the tray.

functoggle_menu(self) -> bool

Open the emulator's menu, or close it when it is already open.

Only called when supports_menu.

Raises

  • NotImplementedError: When the emulator has no menu to reach.
paramself

Returns

bool

True once the toggle reached the emulator.

On this page