diff --git a/api/db.py b/api/db.py index 60a11f6..0dbaeef 100644 --- a/api/db.py +++ b/api/db.py @@ -10,6 +10,7 @@ import json import sqlite3 +import threading from datetime import datetime, timezone from pathlib import Path @@ -21,6 +22,7 @@ class Database: self.conn.row_factory = sqlite3.Row self.conn.execute("PRAGMA journal_mode=WAL") self.conn.execute("PRAGMA busy_timeout=30000") + self._lock = threading.Lock() self._init_schema() def __enter__(self): @@ -175,65 +177,65 @@ class Database: Если request_id передан и уже существует — silently return (идемпотентность). """ - ci = client_info + with self._lock: + ci = client_info - # Подсчёт DTC/PID из ответов - dtc_count = 0 - pid_count = 0 - for r in responses: - dec = (r.get("decoded") or "").lower() - if dec.startswith("dtc"): - dtc_count += 1 - elif ":" in dec and not dec.startswith(("vin", "dtc", "elm", "protocol")): - pid_count += 1 + # Подсчёт DTC/PID из ответов + dtc_count = 0 + pid_count = 0 + for r in responses: + dec = (r.get("decoded") or "").lower() + if dec.startswith("dtc"): + dtc_count += 1 + elif ":" in dec and not dec.startswith(("vin", "dtc", "elm", "protocol")): + pid_count += 1 - # VIN из ответов - vin = None - for r in responses: - dec = (r.get("decoded") or "") - if dec.startswith("VIN:"): - vin = dec[4:].strip() - if len(vin) != 17: - vin = None - break + # VIN из ответов + vin = None + for r in responses: + dec = (r.get("decoded") or "") + if dec.startswith("VIN:"): + vin = dec[4:].strip() + if len(vin) != 17: + vin = None + break - resp_json_str = json.dumps(response_json, ensure_ascii=False) if response_json else None + resp_json_str = json.dumps(response_json, ensure_ascii=False) if response_json else None - self.conn.execute(""" - INSERT OR IGNORE INTO sessions ( - client_ip, real_ip, user_agent, content_length, - phone_model, phone_maker, android_version, android_sdk, - app_version, android_id, device_uuid, phone_lang, phone_tz, phone_display, - elm_mac, elm_bt_name, obd_protocol, + self.conn.execute(""" + INSERT OR IGNORE INTO sessions ( + client_ip, real_ip, user_agent, content_length, + phone_model, phone_maker, android_version, android_sdk, + app_version, android_id, device_uuid, phone_lang, phone_tz, phone_display, + elm_mac, elm_bt_name, obd_protocol, + vin, dtc_count, pid_count, + duration_ms, response_count, error_count, + retry_count, timeout_count, script_mode, + transport, mock_mode, car_info, + diagnosis_text, diagnosis_len, llm_model, + llm_duration_ms, llm_success, + raw_responses, request_id, response_json + ) VALUES (?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?, ?,?,?,?, ?,?,?,?,?) + """, ( + ci.get("client_ip"), ci.get("real_ip"), ci.get("user_agent"), + ci.get("content_length"), + ci.get("phone_model"), ci.get("phone_maker"), ci.get("android_version"), + ci.get("android_sdk"), ci.get("app_version"), ci.get("android_id"), + ci.get("device_uuid"), + ci.get("phone_lang"), ci.get("phone_tz"), ci.get("phone_display"), + ci.get("elm_mac"), ci.get("elm_bt_name"), ci.get("obd_protocol"), vin, dtc_count, pid_count, - duration_ms, response_count, error_count, - retry_count, timeout_count, script_mode, - transport, mock_mode, car_info, - diagnosis_text, diagnosis_len, llm_model, - llm_duration_ms, llm_success, - raw_responses, request_id, response_json - ) VALUES (?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?,?, ?,?, ?,?,?,?, ?,?,?,?,?) - - """, ( - ci.get("client_ip"), ci.get("real_ip"), ci.get("user_agent"), - ci.get("content_length"), - ci.get("phone_model"), ci.get("phone_maker"), ci.get("android_version"), - ci.get("android_sdk"), ci.get("app_version"), ci.get("android_id"), - ci.get("device_uuid"), - ci.get("phone_lang"), ci.get("phone_tz"), ci.get("phone_display"), - ci.get("elm_mac"), ci.get("elm_bt_name"), ci.get("obd_protocol"), - vin, dtc_count, pid_count, - ci.get("duration_ms"), len(responses), ci.get("error_count", 0), - ci.get("retry_count", 0), ci.get("timeout_count", 0), - ci.get("script_mode"), ci.get("transport"), ci.get("mock_mode", 0), - ci.get("car_info", ""), - diagnosis, len(diagnosis), llm_model, - llm_duration_ms, 1 if llm_success else 0, - json.dumps(responses, ensure_ascii=False) if responses else None, - request_id if request_id else None, - resp_json_str, - )) - self.conn.commit() + ci.get("duration_ms"), len(responses), ci.get("error_count", 0), + ci.get("retry_count", 0), ci.get("timeout_count", 0), + ci.get("script_mode"), ci.get("transport"), ci.get("mock_mode", 0), + ci.get("car_info", ""), + diagnosis, len(diagnosis), llm_model, + llm_duration_ms, 1 if llm_success else 0, + json.dumps(responses, ensure_ascii=False) if responses else None, + request_id if request_id else None, + resp_json_str, + )) + self.conn.commit() def get_recent_sessions(self, limit: int = 50) -> list[dict]: """Последние N сессий.""" @@ -244,27 +246,28 @@ class Database: def save_dtc_scan(self, client_info: dict, dtc_codes: list[str]): """Сохраняет быстрый скан кодов ошибок.""" - self.conn.execute(""" - INSERT INTO sessions ( - client_ip, real_ip, user_agent, - phone_model, phone_maker, android_version, android_sdk, - app_version, android_id, device_uuid, - elm_mac, elm_bt_name, - dtc_count, response_count, - script_mode, transport, - raw_responses - ) VALUES (?,?,?, ?,?,?,?, ?,?,?, ?,?, ?,?, ?,?,?) - """, ( - client_info.get("client_ip"), client_info.get("real_ip"), client_info.get("user_agent"), - client_info.get("phone_model"), client_info.get("phone_maker"), client_info.get("android_version"), - client_info.get("android_sdk"), client_info.get("app_version"), client_info.get("android_id"), - client_info.get("device_uuid"), - client_info.get("elm_mac"), client_info.get("elm_bt_name"), - len(dtc_codes), 0, - "dtc_scan", client_info.get("transport", "bt"), - json.dumps([{"decoded": f"DTC stored: {c}"} for c in dtc_codes], ensure_ascii=False) - )) - self.conn.commit() + with self._lock: + self.conn.execute(""" + INSERT INTO sessions ( + client_ip, real_ip, user_agent, + phone_model, phone_maker, android_version, android_sdk, + app_version, android_id, device_uuid, + elm_mac, elm_bt_name, + dtc_count, response_count, + script_mode, transport, + raw_responses + ) VALUES (?,?,?, ?,?,?,?, ?,?,?, ?,?, ?,?, ?,?,?) + """, ( + client_info.get("client_ip"), client_info.get("real_ip"), client_info.get("user_agent"), + client_info.get("phone_model"), client_info.get("phone_maker"), client_info.get("android_version"), + client_info.get("android_sdk"), client_info.get("app_version"), client_info.get("android_id"), + client_info.get("device_uuid"), + client_info.get("elm_mac"), client_info.get("elm_bt_name"), + len(dtc_codes), 0, + "dtc_scan", client_info.get("transport", "bt"), + json.dumps([{"decoded": f"DTC stored: {c}"} for c in dtc_codes], ensure_ascii=False) + )) + self.conn.commit() # ── device_profiles ────────────────────────────────── @@ -285,33 +288,34 @@ class Database: profile — результат obd.probe.probe(). """ - now = datetime.now(timezone.utc).isoformat() - self.conn.execute(""" - INSERT INTO device_profiles - (mac, level, elm_version, elm_desc, protocol, voltage, - supported, unsupported, errors, first_seen, last_seen) - VALUES (?,?,?,?,?,?, ?,?,?, ?,?) - ON CONFLICT(mac) DO UPDATE SET - level = excluded.level, - elm_version = excluded.elm_version, - elm_desc = excluded.elm_desc, - protocol = excluded.protocol, - voltage = excluded.voltage, - supported = excluded.supported, - unsupported = excluded.unsupported, - errors = excluded.errors, - last_seen = excluded.last_seen - """, ( - mac, - profile.get("level", -1), - profile.get("elm_version"), - profile.get("elm_desc"), - profile.get("protocol"), - profile.get("voltage"), - json.dumps(profile.get("supported", []), ensure_ascii=False), - json.dumps(profile.get("unsupported", []), ensure_ascii=False), - json.dumps(profile.get("errors", []), ensure_ascii=False), - now, now, - )) - self.conn.commit() + with self._lock: + now = datetime.now(timezone.utc).isoformat() + self.conn.execute(""" + INSERT INTO device_profiles + (mac, level, elm_version, elm_desc, protocol, voltage, + supported, unsupported, errors, first_seen, last_seen) + VALUES (?,?,?,?,?,?, ?,?,?, ?,?) + ON CONFLICT(mac) DO UPDATE SET + level = excluded.level, + elm_version = excluded.elm_version, + elm_desc = excluded.elm_desc, + protocol = excluded.protocol, + voltage = excluded.voltage, + supported = excluded.supported, + unsupported = excluded.unsupported, + errors = excluded.errors, + last_seen = excluded.last_seen + """, ( + mac, + profile.get("level", -1), + profile.get("elm_version"), + profile.get("elm_desc"), + profile.get("protocol"), + profile.get("voltage"), + json.dumps(profile.get("supported", []), ensure_ascii=False), + json.dumps(profile.get("unsupported", []), ensure_ascii=False), + json.dumps(profile.get("errors", []), ensure_ascii=False), + now, now, + )) + self.conn.commit()