Перейти до вмісту

Logging and diagnostics

Цей контент ще не доступний вашою мовою.

The loader writes one set of logs for itself and every mod, keeps a status summary of what runs, and turns a failing mod off with a reason in the log. This chapter covers what a mod writes there and where to look.

enshwick::error!, warn!, info! and debug! write a line under the mod’s id, formatted like format!:

enshwick::info!("{} altars merged", 3);
enshwick::debug!("bed {} checked", 1210);

For lines that may repeat:

MacroLogs
info_once!, warn_once!, error_once!the first time this call site runs in the mod’s life, never again
info_every!(period, ...), warn_every!(period, ...), error_every!(...)at most once per period from this call site, adding how many lines it skipped since the last
use std::time::Duration;
for bed in 0..100 {
enshwick::warn_every!(Duration::from_secs(60), "bed {bed} has no owner");
}

A line above the mod’s level costs one atomic load: its arguments are neither evaluated nor formatted. Log milestones and failures, not every call of a hook; a line that is kept is formatted on the calling thread.

The loader writes to logs/enshwick/ beside the game:

FileHolds
all.logevery line, the loader’s and every mod’s
loader.logthe loader’s own lines
mods/<id>.logone mod’s lines
status.logthe latest status summary
crash.logeach crash, blamed on a mod, the loader or the game
report.mdthe report a player attaches to an issue

A line reads hh:mm:ss.mmmZ level [source] message, in UTC. At start the files of the two sessions before are kept as <name>.1.log (the last) and <name>.2.log, each cut to its last 8 MiB, and older ones are deleted. A background thread writes them and flushes every line, so a hook never waits on the disk and the last lines before a crash are kept. Up to 4096 lines wait for it; a line that finds the queue full is dropped rather than making the game wait, and the next line written says how many were dropped.

report.md gathers what an issue needs in one file: the loader version and the stub’s contract, Windows or the Wine version with WINEDLLOVERRIDES, and the status summary. It lists the DLLs in mods/, in mods/chain/ and other mods’ proxies beside the game, with their sizes, and the latest 200 warnings and errors of the loader and every mod. The loader writes it once its log opens, again when it cannot serve the game, and with every status summary, each time after the log has taken the lines before it. Paths show the game’s folder as <game>, the user’s profile folder as %USERPROFILE% and the name in a Linux home folder as <user>, in any case and with either slash.

The writer keeps the latest 1024 lines in memory. The console reads them with enshwick log [mod] [level] [n], the words in any order:

  • mod: one mod’s lines, or the loader’s with enshwick; every source when left out;
  • level: error, warn, info or debug, the lines at that level or more severe; every kept line when left out;
  • n: how many of the latest lines, 10 by default and at most 16.

enshwick log you.trading-post warn 5 shows the last five warnings and errors of you.trading-post. Only lines the levels keep are there to read.

The player picks how much is kept in mod-configs/enshwick.toml, the loader’s own settings:

# error, warn, info or debug; info by default
log_level = "info"
# The same per mod id, over log_level; "enshwick" is the loader itself.
log_levels = { "you.lockers" = "debug" }

The loader watches this file like a mod’s settings: a change applies about a second later, and every mod’s level follows without a restart.

The keys of mod-configs/enshwick.toml:

KeyDefaultMeaning
log_level"info"the most detailed lines kept
log_levelsemptylog_level per mod id
status_minutes10minutes between status summaries; 0 writes one only on request
diagnosticsfalsecount the calls of every hooked function; read at start
diagnostics_timingfalsewith diagnostics, also time every 256th call; read at start
migrations_dry_runfalseonly log what mods’ migrations would change
build_patternstruerun a build without its table on patterns; read at start
dev_reloadfalsereload changed mods that allow it, on the server or the client; read at start
knowledge_guard"watch"watch, enforce or turn off the knowledge guard; read at start
require_enshwicktruekick players whose game lacks the join mods; read at start
join_hello_seconds30seconds a joining player’s game has to name its join mods; read at start
join_note_seconds10seconds between the chat line and the disconnect; read at start
allowlistfalseon a dedicated server, let in only the allowlist’s accounts and administrators; read at start
language""on a dedicated server, language code of the console’s and other texts no player’s game names; empty for English
motd""the message of the day players read in chat; at most 4 lines and 500 bytes

The file rejects unknown keys. A file that does not parse at start is logged and the loader runs with the defaults; a change that does not parse is logged and skipped.

The loader writes a status summary to logs/enshwick/status.log: a minute after start, then every status_minutes, and whenever the console runs enshwick status. Each write replaces the last. It lists:

  • the loader and game versions, and whether this process hosts a game;
  • every mod with its name, version and whether it runs, its authors, licence and homepage, notes such as a recommended mod that is missing, and the lines the mod adds;
  • the mods turned off, and why;
  • what mods serve and patch, every service, mod event and extension point with the mods on it, and the links left inert with the reason;
  • the number of hooked functions, and with diagnostics their calls;
  • with diagnostics, what each mod costs (Per-mod costs);
  • where mods meet (Conflicts).

enshwick status also answers the console with a short form of it.

A mod adds its own lines with load.add_status(|| ...). The closure returns a String; each non-empty line shows under the mod. It runs only when the summary is written, on a loader thread or the game thread, never inside a hook, and only while the mod runs. Keep what it reports in atomics and read them there:

use std::sync::atomic::{AtomicU64, Ordering};
static MERGED: AtomicU64 = AtomicU64::new(0);
fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
load.add_status(|| format!("{} zones merged", MERGED.load(Ordering::Relaxed)))
}

A mod made of parts that can fail apart wraps each part’s setup in load.feature(name, |load| ...). When the closure returns an Off, only that part is off: the mod keeps running, its log names the part and the reason, and the status summary and the Mods screen show it. The closure’s value comes back as Some when the part runs. Look up everything the part needs before it links hooks or adds content, since what it registered before it failed stays:

fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
if let Some(glow) = load.feature("glow", |load| load.symbol("lantern.glow")) {
let _ = glow;
}
Ok(())
}

A reason made with Off::text(key, args) names a text of the mod’s texts. The Mods screen, the status summary and the log show it in the reader’s language, with each {name} filled from args. A key with no text shows as the key and its values.

fn init(load: &mut enshwick::Load<'_>) -> Result<(), enshwick::Off> {
let _texts = load.texts("[en]\n\"glow.off\" = \"No glow: {symbol} is missing.\"\n")?;
let glow = load.feature("glow", |load| {
load.symbol("lantern.glow")
.map_err(|_| enshwick::Off::text("glow.off", &[("symbol", "lantern.glow")]))
});
let _ = glow;
Ok(())
}

With diagnostics = true the loader counts the calls of every hooked function, one locked increment in its thunk, and the summary lists the 40 busiest, with the mods linked on each. Add diagnostics_timing = true and every 256th call is timed as well; the summary shows the mean time of the whole chain with the original.

Both are read at start, since the thunks are written once. Without them the thunks count nothing and a hooked call runs as before.

With diagnostics = true the loader also times every call it makes into a mod: event, game event and mod event handlers, actions, messages, interface frames, filters and vetoes, survivor and altar callbacks, stat changes, inventory results, status lines, resource patches, migrations and links on extension points, each link apart. A callback’s time leaves out the callbacks it causes in other mods, which count for those. With diagnostics_timing = true as well, every link and every original has a timed thunk in front of it, and a link’s own time is its thunk’s less the rest of the chain’s, so a mod pays only for its own link even when several mods hook one function. Timing a callback costs about 14 ns; with diagnostics off it costs nothing measurable.

The summary lists the ten costliest mods, each with its work per second of time and its four costliest items; the loader’s own links count under enshwick, with the callbacks they make into mods inside them:

per-mod costs over 1m 00s (since start), work per second of time:
enshwick 0.92 ms/s: hook comfort.base_register 0.39 ms/s in 366110 calls; hook server.post_step 0.18 ms/s in 1456 calls
you.lanterns 52.9 µs/s: progress filters 51.8 µs/s in 366110 calls; events 1.1 µs/s in 1 calls

The console asks for the five costliest with enshwick profile, and starts counting anew with enshwick profile reset, such as before trying something in game.

The summary and enshwick conflicts on the console list where mods meet:

LineMeans
hook <function>: a (priority 0) then b (priority 5)both link the function, in that running order, each calling on
hook <function>: ...; b refused, a holds it exclusivelya linked it with EXCLUSIVE, and b’s link was refused
code patch at <address>: a holds it; refused: bb asked to patch bytes a patches already
<filter>: a, b, chained in load orderboth filter it, such as interactions or damage; each may refuse
skill override: a, b, asked in load orderthe first answer decides
<feature>: a holds it; refused: bone mod at a time, such as the altar limit
the item registry: patched by a then bboth change one resource, each patch on the one before
max_stack of <item> is 999 from b, which overrides 100 from aboth set one vanilla value; the later in load order wins
component <type>: written by a, b, the last write winsboth write that component of entities
point <name>: a then b, each may change the answerboth extend one extension point, in that order
...; b changed the answer a had set, 3 timesb overturned a’s answer that often since start
game code at <place>, 5 bytes: changed outside the API by a.dll while it loadedsee below

conflicts: none means no two mods share anything.

A mod that writes game code itself, with a hooking library of its own or by hand, passes none of the checks above. The loader finds such changes anyway: it keeps a copy of the game’s code section while it starts and compares it after each DLL in mods/ loads, after each mod’s init and after each ready. For a change it finds, the loader names the DLL or mod that ran last. It logs the change as a warning in the loader’s log and lists it with the conflicts:

Line ends withThe change came
by another program before Enshwick startedbefore the loader, from a DLL in mods/chain/ or another program
by a.dll while it loadedwhile that DLL loaded, from its own start-up code
by another program while Enshwick unloaded the DLLs it skipswhile the loader unloaded a mod that is off or an older copy
by you.mod in its init or in its readywhile that mod’s init or ready ran
by another program after every init, before Enshwick turned on its hooksbetween the last init and arming, from a thread of some mod or another program

The copy starts from the executable on disk, so code changed before the loader started shows too; when the file’s code does not read as the code in memory, as for an encrypted executable, the copy starts when the loader does. Two names on one line mean two mods wrote the same bytes, and the later one won.

The loader’s own writes are never listed: its hooks and the code patches mods ask for through the API, and the jump that holds the server’s entry point until the loader is ready. For a thread’s write, the loader names whatever ran last before the next comparison. Changes made after the last ready, and changes to game data rather than code, are not seen.

A mod is turned off when:

  • init returns Err(Off): the log has an error off: <reason>, and the Mods screen shows the reason under the mod, in the reader’s language for an Off::text;
  • init panics: an error off: init panicked;
  • a hook, event handler, action, status closure or ready panics later: an error off: <what panicked>.

The panic is caught at the boundary, the loader unlinks every hook of the mod, and the game goes on vanilla where the mod acted. Its event handlers, actions and status lines stop with it. The summary’s list of mods turned off keeps the reason, and enshwick on the console marks the mod (off).

A hardware fault, such as an access violation in a mod’s code, is not a panic and cannot be caught. When it ends the game, the loader writes crash.log, naming the mod whose code faulted, whose callback it was running, or whose code was on the stack, before the game’s own crash handling runs; the next start shows the line in its log and at the top of status.log. See What is in crash.log?.