Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Template for new versions:
- `stocks`: the overlay's ``collapse all`` hotkey now toggles, expanding all categories again when everything is collapsed

## Fixes
- Plugin commands that throw uncaught C++ exceptions now report an error instead of crashing the game
- Fix broken weather lookup in ``World::ReadCurrentWeather``
- Fixed a possible hang or assertion failure when pressing a hotkey while a DFHack GUI window was open but unfocused
- In ``Screen`` module, Fix out-of-bounds color table access when Lua pens use ``COLOR_RESET``
Expand Down
19 changes: 16 additions & 3 deletions library/PluginManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,19 @@ command_result Plugin::invoke(color_ostream &out, const std::string & command, s
if (auto cmdIt = std::ranges::find_if(commands, [&](const PluginCommand &cmd) { return cmd.name == command; });
commands.end() != cmdIt)
{
// an uncaught exception from a plugin command would propagate
// through runCommand and terminate the game; report it instead
auto call = [&]() -> command_result {
try {
return cmdIt->function(out, parameters);
} catch (const std::exception &e) {
out.printerr("Exception in {} command '{}': {}\n", name, command, e.what());
return CR_FAILURE;
} catch (...) {
out.printerr("Exception in {} command '{}'\n", name, command);
return CR_FAILURE;
}
};
// running interactive things from some other source than the console would break it
if (!out.is_console() && cmdIt->interactive)
cr = CR_NEEDS_CONSOLE;
Expand All @@ -504,15 +517,15 @@ command_result Plugin::invoke(color_ostream &out, const std::string & command, s
cr = CR_WRONG_USAGE;
}
else {
cr = cmdIt->function(out, parameters);
cr = call();
}
}
else if (cmdIt->unlocked) {
cr = cmdIt->function(out, parameters);
cr = call();
}
else {
CoreSuspender suspend;
cr = cmdIt->function(out, parameters);
cr = call();
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions plugins/devel/kittens.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ uint64_t timeLast = 0;

command_result kittens (color_ostream &out, vector <string> & parameters);
command_result ktimer (color_ostream &out, vector <string> & parameters);
command_result throwtest (color_ostream &out, vector <string> & parameters);
command_result trackmenu (color_ostream &out, vector <string> & parameters);
command_result trackpos (color_ostream &out, vector <string> & parameters);
command_result trackstate (color_ostream &out, vector <string> & parameters);
Expand All @@ -68,6 +69,7 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
commands.push_back(PluginCommand("trackstate","Track world and map state (toggle).",trackstate));
commands.push_back(PluginCommand("colormods","Dump colormod vectors.",colormods));
commands.push_back(PluginCommand("sharedsignal","Test Signal with signal_shared_tag",sharedsignal));
commands.push_back(PluginCommand("throwtest","Throw an exception to test command error handling.",throwtest));
return CR_OK;
}

Expand Down Expand Up @@ -377,6 +379,13 @@ command_result sharedsignal (color_ostream &out, vector <string> & parameters)
return CR_OK;
}

command_result throwtest (color_ostream &out, vector <string> & parameters)
{
if (!parameters.empty() && parameters[0] == "nonstd")
throw 42;
throw std::runtime_error("throwtest: test exception");
}

command_result kittens (color_ostream &out, vector <string> & parameters)
{
if (parameters.size() >= 1)
Expand Down
31 changes: 31 additions & 0 deletions test/plugins/kittens.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
config.mode = 'fortress'
config.target = 'kittens'

-- kittens is a developer plugin and is not built unless BUILD_DEV_PLUGINS is
-- on, so these tests are skipped on builds where throwtest is unavailable
local function has_throwtest()
for _, plugin in ipairs(dfhack.internal.listPlugins()) do
if plugin == 'kittens' then
for _, command in ipairs(dfhack.internal.listCommands(plugin)) do
if command == 'throwtest' then
return true
end
end
end
end
return false
end

function test.exception_returns_failure()
if not has_throwtest() then return end
local output, status = dfhack.run_command_silent('throwtest')
expect.eq(CR_FAILURE, status)
expect.str_find('test exception', output)
end

function test.nonstd_exception_returns_failure()
if not has_throwtest() then return end
local output, status = dfhack.run_command_silent('throwtest nonstd')
expect.eq(CR_FAILURE, status)
expect.str_find("Exception in kittens command", output)
end
Loading