diff --git a/MultiServer.py b/MultiServer.py index e68c1437d..5c7ccaa1b 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -3077,7 +3077,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument('multidata', nargs="?", default=defaults["multidata"]) parser.add_argument('--host', default=defaults["host"]) parser.add_argument('--port', default=defaults["port"], type=int) - parser.add_argument('--admin-password', default=defaults["admin_password"]) + parser.add_argument('--admin-password') parser.add_argument('--server-password', dest="admin_password", default=argparse.SUPPRESS, help=argparse.SUPPRESS) # pre-rename alias parser.add_argument('--password', default=defaults["password"]) @@ -3146,6 +3146,10 @@ def parse_args() -> argparse.Namespace: """) parser.add_argument('--log_network', default=defaults["log_network"], action="store_true") args = parser.parse_args() + # a command-line admin password outranks the one a save restores; host.yaml's does not + args.admin_password_given = args.admin_password is not None + if not args.admin_password_given: + args.admin_password = defaults["admin_password"] return args @@ -3237,6 +3241,8 @@ async def main(args: argparse.Namespace): raise ctx.init_save(not args.disable_save) + if args.admin_password_given: + ctx.admin_password = args.admin_password # Set up signal handler for Ctrl+C (SIGINT) - cross-platform compatible def shutdown_handler(): diff --git a/setup.py b/setup.py index 0ca446415..51d4d12a7 100644 --- a/setup.py +++ b/setup.py @@ -93,7 +93,9 @@ def _find_libmtdev() -> str | None: "mwgg_gui", "mwgg_tui", "mwgg_splash", - "worlds" + "worlds", + # Submodules like cached_world are only imported by out-of-tree worlds. + "rule_builder", ], "includes": [ "ModuleUpdate", diff --git a/test/programs/test_server_password_alias.py b/test/programs/test_server_password_alias.py index 832f19d96..bda4b1852 100644 --- a/test/programs/test_server_password_alias.py +++ b/test/programs/test_server_password_alias.py @@ -1,14 +1,21 @@ """server_password is the pre-rename spelling of admin_password; multidata, saves and command lines written before the rename must still reach Context.admin_password.""" +import asyncio import logging +import os +import pickle +import signal import sys +import tempfile import types import unittest +import zlib from unittest import mock import MultiServer from MultiServer import Context +from NetUtils import NetworkSlot, SlotType def _stub() -> types.SimpleNamespace: @@ -34,5 +41,69 @@ def test_command_line_alias(self) -> None: self.assertFalse(hasattr(args, "server_password")) +class _Stop(Exception): + pass + + +class TestCommandLinePasswordOutranksSave(unittest.TestCase): + """The launcher's host dialog passes --admin-password; a save written with another + password must not replace it, or remote admin answers "disabled".""" + + def setUp(self) -> None: + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.data = os.path.join(tmp.name, "AP_TEST.archipelago") + multidata = { + "slot_info": {1: NetworkSlot("Player1", "Game A", SlotType.player)}, + "connect_names": {"Player1": (0, 1)}, + "locations": {1: {}}, + "slot_data": {1: {}}, + "er_hint_data": {}, + "precollected_items": {1: []}, + "precollected_hints": {1: set()}, + "version": (0, 6, 2), + "minimum_versions": {"server": (0, 1, 6), "clients": {}}, + "seed_name": "TESTSEED", + "datapackage": {"Game A": {"item_name_to_id": {}, "location_name_to_id": {}, "item_name_groups": {}}}, + } + with open(self.data, "wb") as f: + f.write(bytes([3]) + zlib.compress(pickle.dumps(multidata))) + no_autosave = mock.patch.object(Context, "_start_async_saving") + no_autosave.start() + self.addCleanup(no_autosave.stop) + + ctx = Context("", 0, "saved", None, 1, 10, True) + ctx.load(self.data) + ctx.init_save() + self.assertTrue(ctx._save()) + + def _run_main(self, *flags: str) -> Context: + created = [] + init_save = Context.init_save + + def recording_init_save(ctx: Context, *args, **kwargs) -> None: + created.append(ctx) + init_save(ctx, *args, **kwargs) + + with mock.patch.object(sys, "argv", ["MultiServer", self.data, *flags]): + args = MultiServer.parse_args() + sigint = signal.getsignal(signal.SIGINT) + try: + with mock.patch.object(Context, "init_save", recording_init_save), \ + mock.patch.object(MultiServer.Utils, "init_logging"), \ + mock.patch.object(MultiServer.websockets, "serve", side_effect=_Stop): + with self.assertRaises(_Stop): + asyncio.run(MultiServer.main(args)) + finally: + signal.signal(signal.SIGINT, sigint) + return created[0] + + def test_command_line_password_beats_save(self) -> None: + self.assertEqual(self._run_main("--admin-password", "typed").admin_password, "typed") + + def test_save_beats_host_yaml_default(self) -> None: + self.assertEqual(self._run_main().admin_password, "saved") + + if __name__ == "__main__": unittest.main()