import importlib import json import queue import threading import time import traceback import sys from pathlib import Path import logging log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) from flask import Flask from flask_cors import CORS build_folder = 'web_interface/build' app = Flask(__name__, template_folder=build_folder, static_folder=build_folder, static_url_path='') CORS(app) from munch import Munch from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from mosfet import connect global_state = Munch() g = global_state g.connection = False g.connected = False g.connection_status = 'disconnected' g.player_position = None g.last_chat = None g.name = None g.mcdata = False g.pos = False g.dimension = None g.item_lock = False g.inv = {} g.objects = {} g.mobs = {} g.players = {} g.player_names = {} g.window = None g.job = None g.correction_count = 0 g.holding = 0 g.afk = False g.health = 20 g.food = 20 g.sand_origin = None g.reload_lock = threading.RLock() @app.route('/api/global') def api_global(): data = json.dumps(g, default=lambda o: str(o), indent=4) response = app.response_class( response=data, status=200, mimetype='application/json' ) return response @app.route('/') def root(): return app.send_static_file('index.html') def main(): """Run the 26.3 client with in-place development hot reload. Application modules are refreshed while the current socket and player session remain alive. Connection/bootstrap and packet-schema edits are reported as deferred because their listeners cannot safely be replaced without reconnecting. """ g.running = True changes = queue.Queue() observer = Observer() source_root = Path(__file__).resolve().parent / 'mosfet' def enqueue_change(event): if event.is_directory: return path = getattr(event, 'dest_path', None) if event.event_type == 'moved' else event.src_path if not path or Path(path).suffix != '.py' or '__pycache__' in path: return changes.put(str(Path(path).resolve())) def module_name(path): source = Path(path) try: relative = source.relative_to(source_root) except ValueError: return None parts = list(relative.with_suffix('').parts) if parts and parts[-1] == '__init__': parts.pop() return '.'.join(('mosfet', *parts)) def reload_changed(paths): paths = sorted(set(path for path in paths if Path(path).is_file())) if not paths: return # Don't execute a half-written editor save. If any file is syntactically # invalid, keep the currently running code untouched. try: for path in paths: source = Path(path) compile(source.read_text(encoding='utf-8'), str(source), 'exec') except (OSError, SyntaxError, UnicodeError): print('[hot reload] waiting for a valid save', flush=True) return deferred = [] modules = [] for path in paths: name = module_name(path) if not name: continue if name in ('mosfet.connect', 'mosfet') or name == 'mosfet.monkey_patch': deferred.append(name) else: modules.append(name) if deferred: print('[hot reload] {} changed; connection/bootstrap changes are ' 'deferred until the next start (no disconnect performed)'.format( ', '.join(sorted(set(deferred)))), flush=True) if not modules: return if not g.get('connected') or not g.get('commands'): deadline = time.monotonic() + 15.0 while (g.running and time.monotonic() < deadline and (not g.get('connected') or not g.get('commands'))): time.sleep(0.1) if not g.get('connected') or not g.get('commands'): print('[hot reload] runtime not ready; change left for next save', flush=True) return managers_changed = bool( {'mosfet.protocol.managers', 'mosfet.protocol.types'} & set(modules)) core_modules = { 'mosfet.bot', 'mosfet.commands', 'mosfet.game', 'mosfet.job', 'mosfet.path', 'mosfet.print_help', 'mosfet.utils', 'mosfet.vector', 'mosfet.world', 'mosfet.info.blocks', 'mosfet.info.items', 'mosfet.info.mcdata', 'mosfet.info.mobs', } # bot.py reloads the legacy gameplay modules and job state classes as # one coherent group. Reload protocol adapters first so rebinding sees # their latest definitions. direct_modules = [name for name in modules if name not in core_modules and not name.startswith('mosfet.jobs.')] if managers_changed and 'mosfet.protocol.managers' not in direct_modules: direct_modules.append('mosfet.protocol.managers') priority = { 'mosfet.protocol.types': 0, 'mosfet.protocol.packets': 1, 'mosfet.protocol.managers': 2, 'mosfet.protocol.modern': 3, } direct_modules.sort(key=lambda name: (priority.get(name, 10), name)) connection = g.connection packet_lock = getattr(connection, 'packet_listener_lock', None) try: if packet_lock is None: with g.reload_lock: apply_reload(direct_modules, managers_changed) else: # Match packet dispatch's lock order (listener lock, then the # runtime lock) so callbacks cannot race listener replacement. with packet_lock: with g.reload_lock: apply_reload(direct_modules, managers_changed) except Exception: traceback.print_exc() print('[hot reload] failed; connection left running', flush=True) def apply_reload(direct_modules, managers_changed): for name in direct_modules: module = sys.modules.get(name) if module is not None: importlib.reload(module) bot_module = sys.modules.get('mosfet.bot') if bot_module is None: bot_module = importlib.import_module('mosfet.bot') bot_module = importlib.reload(bot_module) bot_module.refresh_runtime(global_state, reload_managers=managers_changed) print('[hot reload] refreshed {} (session preserved)'.format( ', '.join(direct_modules) if direct_modules else 'bot runtime'), flush=True) def reload_worker(): while True: path = changes.get() if path is None: return batch = {path} stopping = False # Coalesce the burst of modified/moved events from editor saves. while True: try: candidate = changes.get(timeout=0.4) except queue.Empty: break if candidate is None: stopping = True break batch.add(candidate) time.sleep(0.15) reload_changed(batch) if stopping: return event_handler = FileSystemEventHandler() event_handler.on_modified = enqueue_change event_handler.on_created = enqueue_change event_handler.on_moved = enqueue_change observer.schedule(event_handler, str(source_root), recursive=True) observer.start() worker = threading.Thread(target=reload_worker, name='mosfet-hot-reloader', daemon=True) worker.start() try: connect.main(global_state) except KeyboardInterrupt: pass except BaseException: traceback.print_exc() finally: g.running = False changes.put(None) observer.stop() observer.join() worker.join(timeout=2.0) def run_api(): host = '0.0.0.0' port = 3300 while True: print('Trying to run web interface on port:', port) print('If it works, go to http://localhost:' + str(port)) try: app.run(host=host, port=port) except OSError: print() print('Error: Port already taken.') port += 1 if __name__ == '__main__': threading.Thread(target=run_api, daemon=True).start() main()