CliRemote 1.4.6__py3-none-any.whl → 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cliremote-1.4.6.dist-info → cliremote-1.6.0.dist-info}/METADATA +1 -1
- {cliremote-1.4.6.dist-info → cliremote-1.6.0.dist-info}/RECORD +7 -6
- remote/client_manager.py +149 -141
- remote/utils/sqlite_utils.py +45 -0
- {cliremote-1.4.6.dist-info → cliremote-1.6.0.dist-info}/WHEEL +0 -0
- {cliremote-1.4.6.dist-info → cliremote-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {cliremote-1.4.6.dist-info → cliremote-1.6.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
cliremote-1.
|
1
|
+
cliremote-1.6.0.dist-info/licenses/LICENSE,sha256=O-0zMbcEi6wXz1DiSdVgzMlQjJcNqNe5KDv08uYzqR0,1055
|
2
2
|
remote/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
remote/account_manager.py,sha256=u8yqcyq7Vl8LZ1rNlrEfwlJEbJiBzoSAlL3z2hhTzyc,9829
|
4
4
|
remote/account_viewer.py,sha256=MQoH5lOz24651EjhlzVwi6O5hVUAT7Q5fFU6ZHjBlsM,4243
|
@@ -8,7 +8,7 @@ remote/batch_manager.py,sha256=jVGhYVwHMKJd7f7JxcWjKlwr03dq0RaGD1KdkyYdb00,1051
|
|
8
8
|
remote/block_manager.py,sha256=R7UaQigr-hTRtjxjG3OvJdKhvp0mDpLaESp3Of1AYhs,5692
|
9
9
|
remote/caption_manager.py,sha256=ekgcZ_D1q8C24WP18TXxlM5eWTknJmw-KNXDfqlsnEw,966
|
10
10
|
remote/cleaner.py,sha256=gvPtkosGsf7Eb3zmYyeC44xB4HtCxlzKH3e3qLuVos4,5742
|
11
|
-
remote/client_manager.py,sha256=
|
11
|
+
remote/client_manager.py,sha256=EP2pVasUOUwjr4h-vO-LWWf9_rMCGAsFrzVvnGXJcNI,8250
|
12
12
|
remote/client_picker.py,sha256=3QYi1UQ8pZjqR5YU2oKgQlCYV7-1pSPWknCyRFMcFak,4428
|
13
13
|
remote/config.py,sha256=VK0e96gEINRViKIq99CYYuYyaVZTLtlWlPKKkBd41Cg,2377
|
14
14
|
remote/device_manager.py,sha256=SUCONe1qa5jMHOMqqS27ATtv3CaqAT8cN9jNi7AI_Go,5813
|
@@ -31,7 +31,8 @@ remote/speed_manager.py,sha256=fIWSQAP9qW8AHZtMZq0MrC4_nvxcTFU1SBU75kpRzB8,1115
|
|
31
31
|
remote/stop_manager.py,sha256=UXzKJTblEyQqCjp7fenvQ51Q96Unx05WeOiuFMdj25M,1151
|
32
32
|
remote/text_manager.py,sha256=C2wNSXPSCDu8NSD3RsfbKmUQMWOYd1B5N4tzy-Jsriw,2195
|
33
33
|
remote/username_manager.py,sha256=nMNdke-2FIv86xR1Y6rR-43oUoQu_3Khw8wEo54noXI,3388
|
34
|
-
|
35
|
-
cliremote-1.
|
36
|
-
cliremote-1.
|
37
|
-
cliremote-1.
|
34
|
+
remote/utils/sqlite_utils.py,sha256=5i0oUXsBgKC_8qHZPJ-Gyhp9D1TwqKHVvuZRIhKpS6w,1260
|
35
|
+
cliremote-1.6.0.dist-info/METADATA,sha256=TVNkkckVN52498m8gUoc5SFnEYyZ1cKFE6wDUZh6GTk,1202
|
36
|
+
cliremote-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
37
|
+
cliremote-1.6.0.dist-info/top_level.txt,sha256=yBZidJ6zCix_a2ubGlYaewvlzBFXWbckQt20dudxJ1E,7
|
38
|
+
cliremote-1.6.0.dist-info/RECORD,,
|
remote/client_manager.py
CHANGED
@@ -1,223 +1,231 @@
|
|
1
|
+
|
1
2
|
import os
|
2
3
|
import json
|
3
4
|
import asyncio
|
4
5
|
import logging
|
5
6
|
import random
|
6
7
|
import traceback
|
7
|
-
from typing import Optional, Dict, List, Set
|
8
|
+
from typing import Optional, Dict, List, Set
|
8
9
|
from pyrogram import Client, errors
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
from .utils.sqlite_utils import (
|
12
|
+
ensure_dir, chmod_rw, cleanup_sqlite_sidecars, probe_sqlite
|
13
|
+
)
|
14
|
+
|
15
|
+
# =========================
|
16
|
+
# Logging
|
17
|
+
# =========================
|
13
18
|
os.makedirs("logs", exist_ok=True)
|
14
|
-
|
19
|
+
LOG_FILE = "logs/client_debug_log.txt"
|
15
20
|
logger = logging.getLogger(__name__)
|
16
21
|
logger.setLevel(logging.DEBUG)
|
17
|
-
|
18
|
-
|
19
|
-
fh = logging.FileHandler(log_file, encoding="utf-8")
|
22
|
+
if not any(isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", "").endswith(LOG_FILE) for h in logger.handlers):
|
23
|
+
fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
|
20
24
|
fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S")
|
21
25
|
fh.setFormatter(fmt)
|
22
26
|
logger.addHandler(fh)
|
27
|
+
logger.info("🧩 client_manager loaded (v1.6.0, DEBUG).")
|
28
|
+
|
29
|
+
# =========================
|
30
|
+
# Paths
|
31
|
+
# =========================
|
32
|
+
BASE_DIR = os.path.abspath(os.getcwd())
|
33
|
+
ACCOUNTS_FOLDER = os.path.join(BASE_DIR, "acc")
|
34
|
+
ACCOUNTS_DATA_FOLDER = os.path.join(BASE_DIR, "acc_data")
|
35
|
+
ACC_TEMP = os.path.join(BASE_DIR, "acc_temp")
|
36
|
+
for p in (ACCOUNTS_FOLDER, ACCOUNTS_DATA_FOLDER, ACC_TEMP):
|
37
|
+
ensure_dir(p)
|
23
38
|
|
24
|
-
logger.info("🧩 Client Manager started in DEBUG MODE.")
|
25
|
-
|
26
|
-
# ============================================================
|
27
|
-
# ⚙️ ساختارهای ذخیرهسازی
|
28
|
-
# ============================================================
|
29
39
|
client_pool: Dict[str, Client] = {}
|
30
40
|
client_locks: Dict[str, asyncio.Lock] = {}
|
31
41
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
42
|
+
def _strip_session_ext(x: str) -> str:
|
43
|
+
return x[:-8] if x.endswith(".session") else x
|
44
|
+
|
45
|
+
# =========================
|
46
|
+
# JSON helpers
|
47
|
+
# =========================
|
48
|
+
def get_account_data(phone_number: str) -> Optional[Dict]:
|
49
|
+
fp = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
50
|
+
if not os.path.exists(fp):
|
51
|
+
logger.warning("%s: account JSON not found: %s", phone_number, fp)
|
52
|
+
return None
|
53
|
+
try:
|
54
|
+
with open(fp, "r", encoding="utf-8") as f:
|
55
|
+
return json.load(f)
|
56
|
+
except Exception as e:
|
57
|
+
logger.error("%s: error reading JSON: %s: %s", phone_number, type(e).__name__, e)
|
58
|
+
return None
|
36
59
|
|
37
|
-
|
38
|
-
|
39
|
-
|
60
|
+
def save_account_data(phone_number: str, data: Dict) -> None:
|
61
|
+
fp = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
62
|
+
ensure_dir(ACCOUNTS_DATA_FOLDER)
|
63
|
+
try:
|
64
|
+
with open(fp, "w", encoding="utf-8") as f:
|
65
|
+
json.dump(data, f, ensure_ascii=False, indent=4)
|
66
|
+
logger.info("%s: JSON saved: %s", phone_number, fp)
|
67
|
+
except Exception as e:
|
68
|
+
logger.error("%s: error saving JSON: %s: %s", phone_number, type(e).__name__, e)
|
69
|
+
|
70
|
+
# =========================
|
71
|
+
# Client builder
|
72
|
+
# =========================
|
73
|
+
def _make_client_from_json(phone_number: str) -> Optional[Client]:
|
74
|
+
try:
|
75
|
+
data = get_account_data(phone_number)
|
76
|
+
if not data:
|
77
|
+
return None
|
78
|
+
session_base = data.get("session")
|
79
|
+
if not session_base:
|
80
|
+
logger.error("%s: JSON missing 'session'", phone_number)
|
81
|
+
return None
|
82
|
+
|
83
|
+
# absolute name WITHOUT .session
|
84
|
+
if os.path.isabs(session_base) or os.path.dirname(session_base):
|
85
|
+
session_name = _strip_session_ext(session_base)
|
86
|
+
else:
|
87
|
+
session_name = os.path.join(ACCOUNTS_FOLDER, _strip_session_ext(session_base))
|
88
|
+
|
89
|
+
ensure_dir(os.path.dirname(session_name) or BASE_DIR)
|
90
|
+
|
91
|
+
api_id = data.get("api_id")
|
92
|
+
api_hash = data.get("api_hash")
|
93
|
+
if not api_id or not api_hash:
|
94
|
+
logger.error("%s: JSON missing api_id/api_hash", phone_number)
|
95
|
+
return None
|
96
|
+
|
97
|
+
workdir = os.path.join(ACC_TEMP, phone_number)
|
98
|
+
ensure_dir(workdir)
|
99
|
+
|
100
|
+
cli = Client(
|
101
|
+
name=session_name, # NO .session here
|
102
|
+
api_id=int(api_id),
|
103
|
+
api_hash=str(api_hash),
|
104
|
+
sleep_threshold=30,
|
105
|
+
workdir=workdir,
|
106
|
+
no_updates=True,
|
107
|
+
)
|
108
|
+
if data.get("2fa_password"):
|
109
|
+
setattr(cli, "_twofa_password", data["2fa_password"])
|
110
|
+
logger.debug("%s: Prepared Client(name=%s)", phone_number, session_name)
|
111
|
+
return cli
|
112
|
+
except Exception as e:
|
113
|
+
tb = traceback.format_exc(limit=3)
|
114
|
+
logger.critical("%s: client build failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
115
|
+
return None
|
116
|
+
|
117
|
+
# =========================
|
118
|
+
# Start client (safe)
|
119
|
+
# =========================
|
40
120
|
async def get_or_start_client(phone_number: str) -> Optional[Client]:
|
41
|
-
"""
|
42
|
-
ساخت یا گرفتن کلاینت از فایل سشن (با لاگهای بسیار دقیق برای تشخیص ارورهای SQLite)
|
43
|
-
"""
|
44
121
|
cli = client_pool.get(phone_number)
|
45
122
|
try:
|
46
123
|
if cli is not None and getattr(cli, "is_connected", False):
|
47
|
-
logger.debug(
|
124
|
+
logger.debug("%s: already connected (%s)", phone_number, getattr(cli, "name", "?"))
|
48
125
|
return cli
|
49
126
|
|
50
127
|
cli = _make_client_from_json(phone_number)
|
51
128
|
if cli is None:
|
52
|
-
logger.error(f"{phone_number}: ❌ Client creation failed (make_client_from_json returned None)")
|
53
129
|
return None
|
54
130
|
|
55
|
-
|
56
|
-
|
57
|
-
|
131
|
+
session_file = f"{cli.name}.session"
|
132
|
+
parent = os.path.dirname(session_file) or BASE_DIR
|
133
|
+
ensure_dir(parent)
|
58
134
|
|
59
|
-
#
|
60
|
-
if
|
61
|
-
|
135
|
+
# sanitize permissions + sidecars
|
136
|
+
if os.path.exists(session_file):
|
137
|
+
chmod_rw(session_file)
|
138
|
+
cleanup_sqlite_sidecars(cli.name)
|
62
139
|
else:
|
63
|
-
|
64
|
-
|
140
|
+
try:
|
141
|
+
os.chmod(parent, 0o777)
|
142
|
+
except Exception:
|
143
|
+
pass
|
65
144
|
|
66
|
-
|
67
|
-
|
68
|
-
|
145
|
+
if os.path.exists(session_file) and not probe_sqlite(session_file):
|
146
|
+
logger.warning("%s: sqlite probe failed; retrying after cleanup", phone_number)
|
147
|
+
chmod_rw(session_file)
|
148
|
+
cleanup_sqlite_sidecars(cli.name)
|
69
149
|
|
70
150
|
try:
|
71
151
|
await cli.start()
|
72
152
|
await asyncio.sleep(0.4)
|
73
|
-
logger.info(
|
153
|
+
logger.info("%s: client started", phone_number)
|
74
154
|
except errors.SessionPasswordNeeded:
|
75
155
|
twofa = getattr(cli, "_twofa_password", None)
|
76
156
|
if twofa:
|
77
157
|
await cli.check_password(twofa)
|
78
|
-
logger.info(
|
158
|
+
logger.info("%s: 2FA applied", phone_number)
|
79
159
|
else:
|
80
|
-
logger.error(
|
160
|
+
logger.error("%s: 2FA required but missing", phone_number)
|
81
161
|
return None
|
162
|
+
except errors.AuthKeyDuplicated:
|
163
|
+
logger.error("%s: AuthKeyDuplicated (invalid session)", phone_number)
|
164
|
+
return None
|
82
165
|
except Exception as e:
|
83
166
|
tb = traceback.format_exc(limit=3)
|
84
|
-
logger.error(
|
167
|
+
logger.error("%s: start failed: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
85
168
|
return None
|
86
169
|
|
87
170
|
client_pool[phone_number] = cli
|
88
171
|
client_locks.setdefault(phone_number, asyncio.Lock())
|
89
172
|
return cli
|
90
|
-
|
91
|
-
except Exception as e:
|
92
|
-
tb = traceback.format_exc(limit=3)
|
93
|
-
logger.critical(f"{phone_number}: 💥 Fatal error in get_or_start_client() → {type(e).__name__}: {e}\nTraceback:\n{tb}")
|
94
|
-
return None
|
95
|
-
|
96
|
-
|
97
|
-
# ============================================================
|
98
|
-
# 🧱 ساخت کلاینت از JSON
|
99
|
-
# ============================================================
|
100
|
-
def _make_client_from_json(phone_number: str) -> Optional[Client]:
|
101
|
-
try:
|
102
|
-
data_path = os.path.join(ACCOUNTS_DATA_FOLDER, f"{phone_number}.json")
|
103
|
-
if not os.path.exists(data_path):
|
104
|
-
logger.error(f"{phone_number}: ⚠️ JSON file not found → {data_path}")
|
105
|
-
return None
|
106
|
-
|
107
|
-
with open(data_path, "r", encoding="utf-8") as f:
|
108
|
-
account_data = json.load(f)
|
109
|
-
|
110
|
-
session_base = account_data.get("session")
|
111
|
-
if not session_base:
|
112
|
-
logger.error(f"{phone_number}: Missing 'session' in JSON → {data_path}")
|
113
|
-
return None
|
114
|
-
|
115
|
-
# مسیر دقیق سشن
|
116
|
-
session_path = os.path.join(ACCOUNTS_FOLDER, session_base)
|
117
|
-
if not session_path.endswith(".session"):
|
118
|
-
session_path += ".session"
|
119
|
-
|
120
|
-
# مسیر SQLite
|
121
|
-
db_dir = os.path.dirname(os.path.abspath(session_path))
|
122
|
-
if not os.path.exists(db_dir):
|
123
|
-
logger.warning(f"{phone_number}: Creating missing folder for session → {db_dir}")
|
124
|
-
os.makedirs(db_dir, exist_ok=True)
|
125
|
-
|
126
|
-
logger.debug(f"{phone_number}: Final session path → {session_path}")
|
127
|
-
|
128
|
-
# بررسی وجود فایل SQLite (قبل از start)
|
129
|
-
if os.path.exists(session_path):
|
130
|
-
logger.debug(f"{phone_number}: Session file already present ({os.path.getsize(session_path)} bytes)")
|
131
|
-
else:
|
132
|
-
logger.debug(f"{phone_number}: Session file will be created by Pyrogram")
|
133
|
-
|
134
|
-
api_id = account_data.get("api_id")
|
135
|
-
api_hash = account_data.get("api_hash")
|
136
|
-
if not api_id or not api_hash:
|
137
|
-
logger.error(f"{phone_number}: Missing api_id/api_hash in JSON → {data_path}")
|
138
|
-
return None
|
139
|
-
|
140
|
-
cli = Client(
|
141
|
-
name=session_path,
|
142
|
-
api_id=int(api_id),
|
143
|
-
api_hash=str(api_hash),
|
144
|
-
sleep_threshold=30,
|
145
|
-
workdir=os.path.join("acc_temp", phone_number),
|
146
|
-
no_updates=True
|
147
|
-
)
|
148
|
-
|
149
|
-
if account_data.get("2fa_password"):
|
150
|
-
setattr(cli, "_twofa_password", account_data["2fa_password"])
|
151
|
-
|
152
|
-
return cli
|
153
|
-
|
154
173
|
except Exception as e:
|
155
174
|
tb = traceback.format_exc(limit=3)
|
156
|
-
logger.critical(
|
175
|
+
logger.critical("%s: fatal in get_or_start_client: %s: %s\n%s", phone_number, type(e).__name__, e, tb)
|
157
176
|
return None
|
158
177
|
|
159
|
-
|
160
|
-
#
|
161
|
-
#
|
162
|
-
# ============================================================
|
178
|
+
# =========================
|
179
|
+
# Preload
|
180
|
+
# =========================
|
163
181
|
async def preload_clients(limit: Optional[int] = None) -> None:
|
164
182
|
phones = list(get_active_accounts())
|
165
183
|
if limit is not None:
|
166
184
|
phones = phones[:max(0, int(limit))]
|
185
|
+
if not phones:
|
186
|
+
logger.info("no accounts to preload")
|
187
|
+
return
|
167
188
|
|
168
|
-
logger.info(
|
169
|
-
|
170
|
-
ok, bad = 0, 0
|
189
|
+
logger.info("preloading %d client(s)...", len(phones))
|
190
|
+
ok = bad = 0
|
171
191
|
for idx, phone in enumerate(phones, 1):
|
172
|
-
logger.info(
|
192
|
+
logger.info("[%d/%d] preload %s", idx, len(phones), phone)
|
173
193
|
try:
|
174
194
|
cli = await get_or_start_client(phone)
|
175
195
|
if cli and getattr(cli, "is_connected", False):
|
176
196
|
ok += 1
|
177
|
-
logger.info(f"{phone}: ✅ Connected OK.")
|
178
197
|
else:
|
179
198
|
bad += 1
|
180
|
-
logger.warning(f"{phone}: ❌ Not connected after start().")
|
181
199
|
except Exception as e:
|
182
200
|
bad += 1
|
183
|
-
|
184
|
-
|
201
|
+
logger.error("%s: exception during preload: %s: %s", phone, type(e).__name__, e)
|
202
|
+
await asyncio.sleep(0.8 + random.uniform(0.1, 0.3))
|
203
|
+
logger.info("preload done: ok=%d fail=%d", ok, bad)
|
185
204
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
logger.info(f"🎯 Preload completed: OK={ok} | FAIL={bad}")
|
190
|
-
|
191
|
-
|
192
|
-
# ============================================================
|
193
|
-
# 🧹 Stop all
|
194
|
-
# ============================================================
|
205
|
+
# =========================
|
206
|
+
# Stop all
|
207
|
+
# =========================
|
195
208
|
async def stop_all_clients() -> None:
|
196
|
-
logger.info("
|
209
|
+
logger.info("stopping all clients...")
|
197
210
|
for phone, cli in list(client_pool.items()):
|
198
211
|
try:
|
199
212
|
await cli.stop()
|
200
|
-
logger.info(
|
213
|
+
logger.info("%s: stopped", phone)
|
201
214
|
except Exception as e:
|
202
|
-
|
203
|
-
logger.warning(f"{phone}: ⚠️ Error stopping client → {type(e).__name__}: {e}\n{tb}")
|
215
|
+
logger.warning("%s: stop error: %s: %s", phone, type(e).__name__, e)
|
204
216
|
finally:
|
205
217
|
client_pool.pop(phone, None)
|
206
|
-
|
207
|
-
|
208
|
-
|
218
|
+
client_locks.pop(phone, None)
|
219
|
+
await asyncio.sleep(0.2)
|
220
|
+
logger.info("all clients stopped")
|
209
221
|
|
210
|
-
#
|
211
|
-
#
|
212
|
-
#
|
222
|
+
# =========================
|
223
|
+
# Enumerate accounts
|
224
|
+
# =========================
|
213
225
|
def accounts() -> List[str]:
|
214
|
-
accs: Set[str] = set()
|
215
226
|
if not os.path.isdir(ACCOUNTS_FOLDER):
|
216
227
|
return []
|
217
|
-
for
|
218
|
-
if acc.endswith(".session"):
|
219
|
-
accs.add(acc.split(".")[0])
|
220
|
-
return list(accs)
|
228
|
+
return [f[:-8] for f in os.listdir(ACCOUNTS_FOLDER) if f.endswith(".session")]
|
221
229
|
|
222
230
|
def get_active_accounts() -> Set[str]:
|
223
231
|
return set(accounts())
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
import os
|
3
|
+
import sqlite3
|
4
|
+
import logging
|
5
|
+
|
6
|
+
logger = logging.getLogger(__name__)
|
7
|
+
|
8
|
+
SIDE_SUFFIXES = (".session-wal", ".session-shm", ".session-journal")
|
9
|
+
|
10
|
+
def ensure_dir(path: str):
|
11
|
+
try:
|
12
|
+
os.makedirs(path, exist_ok=True)
|
13
|
+
try:
|
14
|
+
os.chmod(path, 0o777)
|
15
|
+
except Exception:
|
16
|
+
pass
|
17
|
+
except Exception as e:
|
18
|
+
logger.error("Cannot create directory %s: %s: %s", path, type(e).__name__, e)
|
19
|
+
|
20
|
+
def chmod_rw(path: str):
|
21
|
+
try:
|
22
|
+
os.chmod(path, 0o666)
|
23
|
+
except Exception:
|
24
|
+
pass
|
25
|
+
|
26
|
+
def cleanup_sqlite_sidecars(db_without_ext: str):
|
27
|
+
base = f"{db_without_ext}.session"
|
28
|
+
for suf in ("-wal", "-shm", "-journal"):
|
29
|
+
f = f"{base}{suf}"
|
30
|
+
if os.path.exists(f):
|
31
|
+
try:
|
32
|
+
os.remove(f)
|
33
|
+
logger.debug("Removed sqlite sidecar: %s", f)
|
34
|
+
except Exception as e:
|
35
|
+
logger.debug("Cannot remove sidecar %s: %s", f, e)
|
36
|
+
|
37
|
+
def probe_sqlite(db_file: str) -> bool:
|
38
|
+
try:
|
39
|
+
conn = sqlite3.connect(db_file, timeout=1)
|
40
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
41
|
+
conn.close()
|
42
|
+
return True
|
43
|
+
except Exception as e:
|
44
|
+
logger.debug("SQLite probe failed for %s: %s: %s", db_file, type(e).__name__, e)
|
45
|
+
return False
|
File without changes
|
File without changes
|
File without changes
|