From 2ca0ac114349a53b1bdae2be8e30d587bbefe20a Mon Sep 17 00:00:00 2001 From: yeseol Date: Mon, 29 Jun 2026 15:17:42 +0900 Subject: [PATCH] =?UTF-8?q?-=20=ED=8E=8C=EC=9B=A8=EC=96=B4=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=9D=B4=ED=9B=84=EC=97=90=20?= =?UTF-8?q?=EC=8B=B1=ED=81=AC=20=EB=AC=B8=EC=A0=9C=EB=A1=9C=20db=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=EA=B0=80=20=EC=9D=B4=EB=A3=A8?= =?UTF-8?q?=EC=96=B4=EC=A7=90.=20=ED=95=B4=EB=8B=B9=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=20disable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/migrations.py | 50 ++++++++++++++++++++++++++----------- src/network/apply_engine.py | 12 ++++++--- src/server.py | 31 +++++++++++++++++------ 3 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/migrations.py b/src/migrations.py index ea5e909..4109941 100644 --- a/src/migrations.py +++ b/src/migrations.py @@ -14,14 +14,25 @@ _CAN_BAUDRATE_OLD_TO_NEW = { } +def _existing_config(db, key): + cfg = db.get_config(key) + if not isinstance(cfg, dict) or not cfg: + return None + return cfg + + def migrate_can_baudrate_units(db) -> bool: """Migrate device_config.can_baudrate from old units to new (5/22) units. Returns True if the migration ran, False if previously applied.""" if db.get_schema_meta("can_baudrate_units_migrated") == "true": return False + if _existing_config(db, "device_config") is None: + return False def _mut(cur): - cfg = dict(cur) if isinstance(cur, dict) else {} + if not isinstance(cur, dict) or not cur: + return cur + cfg = dict(cur) new = _CAN_BAUDRATE_OLD_TO_NEW.get(cfg.get("can_baudrate")) if new is not None: cfg["can_baudrate"] = new @@ -33,7 +44,7 @@ def migrate_can_baudrate_units(db) -> bool: cfg["can_baudrate"] = int(v.strip()) return cfg - db.update_config("device_config", _mut, default={}) + db.update_config("device_config", _mut) db.set_schema_meta("can_baudrate_units_migrated", "true") return True @@ -53,10 +64,9 @@ def migrate_log_compress_split(db) -> bool: # write, so a brief read before the atomic updates is not a race surface; # the race that mattered was clobbering OTHER device_config keys on write, # which the atomic pop below now avoids). - dev = db.get_config("device_config") or {} - if not isinstance(dev, dict): - db.set_schema_meta("log_compress_split_migrated", "true") - return True + dev = _existing_config(db, "device_config") + if dev is None: + return False moved = {k: dev[k] for k in _LOG_COMPRESS_KEYS if k in dev} @@ -81,9 +91,8 @@ def migrate_log_compress_split(db) -> bool: # are preserved — only the two log_compress_* keys are removed). db.update_config( "device_config", - lambda cur: {k: v for k, v in (cur if isinstance(cur, dict) else {}).items() - if k not in _LOG_COMPRESS_KEYS}, - default={}, + lambda cur: {k: v for k, v in cur.items() + if k not in _LOG_COMPRESS_KEYS} if isinstance(cur, dict) and cur else cur, ) db.set_schema_meta("log_compress_split_migrated", "true") @@ -103,16 +112,20 @@ def migrate_port_types(db) -> bool: dha baseline confirms Java schema Integer (2026-06-04 verify).""" if db.get_schema_meta("port_types_migrated") == "true": return False + if _existing_config(db, "device_config") is None: + return False def _mut(cur): - cfg = dict(cur) if isinstance(cur, dict) else {} + if not isinstance(cur, dict) or not cur: + return cur + cfg = dict(cur) for k in _PORT_KEYS: v = cfg.get(k) if isinstance(v, str) and v.strip().lstrip("-").isdigit(): cfg[k] = int(v) return cfg - db.update_config("device_config", _mut, default={}) + db.update_config("device_config", _mut) db.set_schema_meta("port_types_migrated", "true") return True @@ -131,21 +144,28 @@ def migrate_contract_canonical(db) -> bool: """ if db.get_schema_meta("contract_canonical_migrated") == "true": return False + if (_existing_config(db, "device_config") is None or + _existing_config(db, "protocol_config") is None): + return False # device_config: rs485_parity "no" → "none" def _mut_device(cur): - cfg = dict(cur) if isinstance(cur, dict) else {} + if not isinstance(cur, dict) or not cur: + return cur + cfg = dict(cur) if cfg.get("rs485_parity") == "no": cfg["rs485_parity"] = "none" return cfg - db.update_config("device_config", _mut_device, default={}) + db.update_config("device_config", _mut_device) # protocol_config: byte_order camelCase → space + idt float64 → float _BYTE_ORDER_MAP = {"littleSwap": "little swap", "bigSwap": "big swap"} def _mut_protocol(cur): - cfg = dict(cur) if isinstance(cur, dict) else {} + if not isinstance(cur, dict) or not cur: + return cur + cfg = dict(cur) for bo_key in ("two_byte_order", "four_byte_order"): v = cfg.get(bo_key) if v in _BYTE_ORDER_MAP: @@ -164,7 +184,7 @@ def migrate_contract_canonical(db) -> bool: cfg[arr_key] = new_arr return cfg - db.update_config("protocol_config", _mut_protocol, default={}) + db.update_config("protocol_config", _mut_protocol) db.set_schema_meta("contract_canonical_migrated", "true") return True diff --git a/src/network/apply_engine.py b/src/network/apply_engine.py index 435a9d6..fbf8368 100644 --- a/src/network/apply_engine.py +++ b/src/network/apply_engine.py @@ -130,20 +130,24 @@ class ApplyEngine: def _write_db(self, fields): def mut(cur): - base = cur if isinstance(cur, dict) else {} + if not isinstance(cur, dict) or not cur: + raise RuntimeError("device_config is not initialized") + base = cur out = dict(base); out.update(fields) # partial-merge (§4.1) return out - self.db.update_config("device_config", mut, default={}) + self.db.update_config("device_config", mut) def _write_db_restore(self, present, absent): """I1: 롤백 전용 — present 키 복원 + 스냅샷 시점 부재 키 pop.""" def mut(cur): - base = cur if isinstance(cur, dict) else {} + if not isinstance(cur, dict) or not cur: + raise RuntimeError("device_config is not initialized") + base = cur out = dict(base); out.update(present) for k in absent: out.pop(k, None) return out - self.db.update_config("device_config", mut, default={}) + self.db.update_config("device_config", mut) def _run(self, argv, timeout, apply_id, must_ok=False): t0 = self.clock() diff --git a/src/server.py b/src/server.py index 60362a0..06613a8 100644 --- a/src/server.py +++ b/src/server.py @@ -809,6 +809,14 @@ class ConfigHandler(BaseHTTPRequestHandler): # v1.4.0.3: case-only alias normalization (e.g. "ON"→"on", "WPA/WPA2"→"wpa/wpa2") # must run before enum validation so aliases pass validation correctly + current_device = db.get_config("device_config") + if not isinstance(current_device, dict) or not current_device: + self._send_json_response( + {"ok": False, "errors": ["device_config is not initialized"]}, + 409, + ) + return + data = normalize_device_input(data) # v1.4.4: Super_Relay 소유 키 strict reject (silent passthrough 방지) @@ -890,10 +898,12 @@ class ConfigHandler(BaseHTTPRequestHandler): _cleaned_device = cleaned # closure capture def _merge_device(current): - base = current if isinstance(current, dict) else {} + if not isinstance(current, dict) or not current: + return current + base = current return {**base, **_cleaned_device} - merged_device = db.update_config("device_config", _merge_device, default={}) + merged_device = db.update_config("device_config", _merge_device) if log_part: _log_part = log_part # closure capture @@ -938,6 +948,14 @@ class ConfigHandler(BaseHTTPRequestHandler): return # v1.4.0.3: case-only alias normalization (e.g. can_input "ON"→"on") + current_protocol = db.get_config("protocol_config") + if not isinstance(current_protocol, dict) or not current_protocol: + self._send_json_response( + {"ok": False, "errors": ["protocol_config is not initialized"]}, + 409, + ) + return + data = normalize_protocol_input(data) # v1.4.4: Super_Relay 소유 키 strict reject (silent passthrough 방지) @@ -1001,10 +1019,9 @@ class ConfigHandler(BaseHTTPRequestHandler): _cleaned_protocol = cleaned # closure capture def _merge_protocol(current): - base = current if isinstance(current, dict) else {} - if not isinstance(base, dict): - print(f"[WARN] non-dict protocol_config detected (type={type(base).__name__}), falling back to empty") - base = {} + if not isinstance(current, dict) or not current: + return current + base = current result = {**base, **_cleaned_protocol} # v1.4.6.8 C1: partial-merge AFTER process_protocol_config to preserve arrays result = process_protocol_config(result) @@ -1014,7 +1031,7 @@ class ConfigHandler(BaseHTTPRequestHandler): result.pop("odo_direction", None) return result - merged = db.update_config("protocol_config", _merge_protocol, default={}) + merged = db.update_config("protocol_config", _merge_protocol) self._send_json_response({ "success": True,