v1.8.66: fix 7 Redis bugs — shlex parser, GUI stats, SSL, error handling, dedup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
229
REDIS_AUDIT.md
Normal file
229
REDIS_AUDIT.md
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
# Аудит Redis — полный отчёт
|
||||||
|
|
||||||
|
**Дата:** 2026-02-28
|
||||||
|
**Версия:** v1.8.65
|
||||||
|
**Тестовый сервер:** Reddis main ovh (Redis 8.2.1, 27M+ ключей, 2.82GB RAM)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Живое тестирование через CLI
|
||||||
|
|
||||||
|
### Работает корректно
|
||||||
|
|
||||||
|
| Команда | Результат |
|
||||||
|
|---------|-----------|
|
||||||
|
| `--redis-info ALIAS` | Версия, память, клиенты, keyspace |
|
||||||
|
| `--redis-keys ALIAS "*"` | SCAN до 1000 ключей, сортировка |
|
||||||
|
| `--redis ALIAS "PING"` | `True` |
|
||||||
|
| `--redis ALIAS "DBSIZE"` | `27199166` |
|
||||||
|
| `--redis ALIAS "GET key"` | Возвращает значение |
|
||||||
|
| `--redis ALIAS "SET key value"` | `True` |
|
||||||
|
| `--redis ALIAS "DEL key1 key2"` | Число удалённых |
|
||||||
|
| `--redis ALIAS "KEYS pattern"` | Список с нумерацией |
|
||||||
|
| `--redis ALIAS "INFO memory"` | Полный вывод секции |
|
||||||
|
| `--redis ALIAS "TYPE key"` | Тип данных |
|
||||||
|
| `--redis ALIAS "TTL key"` | Секунды / -1 |
|
||||||
|
| `--redis ALIAS "SELECT 1"` | `True` |
|
||||||
|
| `--redis ALIAS "MSET k1 v1 k2 v2"` | `True` |
|
||||||
|
| `--status` (redis) | `ONLINE` |
|
||||||
|
|
||||||
|
### Сломано
|
||||||
|
|
||||||
|
| Команда | Ошибка | Баг |
|
||||||
|
|---------|--------|-----|
|
||||||
|
| `SET key "hello world"` | `syntax error` | #1 |
|
||||||
|
| `SET key '{"a":1, "b":2}'` | `syntax error` | #1 |
|
||||||
|
| Любое значение с пробелами | `syntax error` | #1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Найденные баги
|
||||||
|
|
||||||
|
### БАГ #1 — КРИТИЧНЫЙ: Парсер команд ломает значения с пробелами
|
||||||
|
|
||||||
|
**Файлы:** `tools/ssh.py:958`, `core/redis_client.py:78`
|
||||||
|
|
||||||
|
**Проблема:** Используется `command.split()` вместо `shlex.split()`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Текущий код:
|
||||||
|
parts = command.split()
|
||||||
|
# "SET key 'hello world'" → ['SET', 'key', "'hello", "world'"]
|
||||||
|
# Redis получает 4 аргумента вместо 3 → syntax error
|
||||||
|
```
|
||||||
|
|
||||||
|
**Воздействие:**
|
||||||
|
- Невозможно SET/GET значения с пробелами
|
||||||
|
- Невозможно работать с JSON-данными содержащими пробелы
|
||||||
|
- Невозможно выполнить EVAL с Lua-скриптами
|
||||||
|
- Сломаны команды с multi-word аргументами (SORT, CONFIG SET и др.)
|
||||||
|
|
||||||
|
**Затронуты:** CLI (`tools/ssh.py`) И GUI (`core/redis_client.py` → `redis_tab.py`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### БАГ #2 — СРЕДНИЙ: GUI stats показывает отформатированную строку
|
||||||
|
|
||||||
|
**Файл:** `gui/tabs/redis_tab.py:192-208`
|
||||||
|
|
||||||
|
**Проблема:** `_refresh_stats()` вызывает `client.execute("DBSIZE")`, который возвращает строку `"(integer) 27199166"` (уже отформатированную через `_format()`). Эта строка отображается как есть.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# redis_tab.py:198
|
||||||
|
keys_count = client.execute("DBSIZE") # Returns "(integer) 27199166"
|
||||||
|
keys_text = str(keys_count) # → "(integer) 27199166" in label
|
||||||
|
```
|
||||||
|
|
||||||
|
**Для INFO memory проблема другая:**
|
||||||
|
```python
|
||||||
|
# redis_tab.py:201
|
||||||
|
info = client.execute("INFO memory")
|
||||||
|
# _format() превращает dict в строку, но без \r\n
|
||||||
|
# Поиск по \r\n ничего не найдёт → memory = "—" всегда
|
||||||
|
for line in info.split("\r\n"): # \r\n не найдётся!
|
||||||
|
if line.startswith("used_memory_human:"):
|
||||||
|
memory = line.split(":")[1].strip()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Воздействие:** Статистика в GUI:
|
||||||
|
- Keys показывает `(integer) 27199166` вместо `27199166`
|
||||||
|
- Memory всегда показывает `—` (парсинг не находит `\r\n`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### БАГ #3 — СРЕДНИЙ: GUI RedisClient не поддерживает SSL
|
||||||
|
|
||||||
|
**Файл:** `core/redis_client.py:35-43`
|
||||||
|
|
||||||
|
**Проблема:** В конструкторе `redis.Redis()` отсутствует параметр `ssl`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# redis_client.py:35-43
|
||||||
|
self._conn = r.Redis(
|
||||||
|
host=self._host,
|
||||||
|
port=self._port,
|
||||||
|
password=self._password,
|
||||||
|
db=self._db,
|
||||||
|
decode_responses=True,
|
||||||
|
socket_timeout=5,
|
||||||
|
socket_connect_timeout=5,
|
||||||
|
# ❌ ОТСУТСТВУЕТ: ssl=...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
При этом CLI (`tools/ssh.py:955-956`) SSL поддерживает:
|
||||||
|
```python
|
||||||
|
ssl_enabled = server.get("ssl", False)
|
||||||
|
r = redis_lib.Redis(..., ssl=ssl_enabled)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Воздействие:** GUI не может подключиться к Redis с TLS/SSL.
|
||||||
|
|
||||||
|
**Связанная проблема:** В `server_dialog.py` поле `use_ssl`/`ssl` НЕ включено в `FIELD_MAP["redis"]`:
|
||||||
|
```python
|
||||||
|
"redis": ["password", "db_index"] # нет ssl!
|
||||||
|
```
|
||||||
|
|
||||||
|
Даже если добавить SSL в RedisClient, пользователь не сможет включить его в UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### БАГ #4 — НИЗКИЙ: CLI Redis ошибки выдают сырой traceback
|
||||||
|
|
||||||
|
**Файл:** `tools/ssh.py:957-962`
|
||||||
|
|
||||||
|
**Проблема:** `r.execute_command()` не обёрнут в try-except.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Текущий код:
|
||||||
|
result = r.execute_command(*parts) # При ошибке → необработанное исключение
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример:** `HGETALL` на string-ключе:
|
||||||
|
```
|
||||||
|
ERROR: ResponseError: WRONGTYPE Operation against a key holding the wrong kind of value
|
||||||
|
```
|
||||||
|
Выводится как traceback с двойным повтором вместо читаемого сообщения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### БАГ #5 — НИЗКИЙ: Двойной PING при status check
|
||||||
|
|
||||||
|
**Файл:** `core/status_checker.py:98-110`
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _check_redis(self, server: dict) -> bool:
|
||||||
|
client = RedisClient(server)
|
||||||
|
result = client.connect() # ← PING #1 внутри connect()
|
||||||
|
if result:
|
||||||
|
ok = client.check_connection() # ← PING #2
|
||||||
|
client.disconnect()
|
||||||
|
return ok
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
`connect()` уже делает `self._conn.ping()` → `True`. Потом `check_connection()` вызывает `self._conn.ping()` ещё раз.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### БАГ #6 — НИЗКИЙ: CLI status для Redis без try-except
|
||||||
|
|
||||||
|
**Файл:** `tools/ssh.py:698-703`
|
||||||
|
|
||||||
|
```python
|
||||||
|
if stype == "redis":
|
||||||
|
r = redis_lib.Redis(...)
|
||||||
|
r.ping() # ← Нет try-except! Если offline → traceback
|
||||||
|
r.close()
|
||||||
|
return "ONLINE"
|
||||||
|
```
|
||||||
|
|
||||||
|
Сравни с SQL, где `conn.close()` в finally. Здесь если `ping()` упадёт, `r.close()` не вызовется.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ПРОБЛЕМА #7 — Дублирование кода подключения
|
||||||
|
|
||||||
|
**Файлы:** `tools/ssh.py:946-956`, `tools/ssh.py:978-986`, `tools/ssh.py:1009-1017`
|
||||||
|
|
||||||
|
Три функции (`run_redis_cmd`, `redis_info`, `redis_keys`) создают одинаковое подключение к Redis копипастом:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import redis as redis_lib
|
||||||
|
host = server["ip"]
|
||||||
|
port = server.get("port", 6379)
|
||||||
|
password = server.get("password", "") or None
|
||||||
|
db_index = server.get("db_index", 0)
|
||||||
|
ssl_enabled = server.get("ssl", False)
|
||||||
|
r = redis_lib.Redis(host=host, port=port, ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот блок повторяется 3 раза. Вынести в хелпер.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Карта проблем по файлам
|
||||||
|
|
||||||
|
| Файл | Строки | Баги |
|
||||||
|
|------|--------|------|
|
||||||
|
| `tools/ssh.py` | 958 | #1 (split), #4 (error handling), #6 (status), #7 (copypaste) |
|
||||||
|
| `core/redis_client.py` | 78, 35-43 | #1 (split), #3 (no SSL) |
|
||||||
|
| `gui/tabs/redis_tab.py` | 192-208 | #2 (stats parsing) |
|
||||||
|
| `core/status_checker.py` | 98-110 | #5 (double ping) |
|
||||||
|
| `gui/server_dialog.py` | 22 | #3 (no ssl field for redis) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Влияние на пользователей
|
||||||
|
|
||||||
|
| Сценарий | Статус | Причина |
|
||||||
|
|----------|--------|---------|
|
||||||
|
| Простые команды (GET/SET/DEL) | ✅ Работает | Нет пробелов |
|
||||||
|
| Команды со значениями с пробелами | ❌ Сломано | Баг #1 |
|
||||||
|
| JSON-данные с пробелами | ❌ Сломано | Баг #1 |
|
||||||
|
| GUI статистика Keys | ⚠️ Некорректно | Баг #2 |
|
||||||
|
| GUI статистика Memory | ❌ Всегда "—" | Баг #2 |
|
||||||
|
| SSL-подключение через GUI | ❌ Невозможно | Баг #3 |
|
||||||
|
| SSL-подключение через CLI | ✅ Работает | — |
|
||||||
|
| Ошибки в CLI | ⚠️ Нечитаемые | Баг #4 |
|
||||||
|
| Status check | ✅ Работает (с оверхедом) | Баг #5 |
|
||||||
@@ -23,6 +23,7 @@ class RedisClient:
|
|||||||
self._port = int(server.get("port", 6379))
|
self._port = int(server.get("port", 6379))
|
||||||
self._password = server.get("password") or None
|
self._password = server.get("password") or None
|
||||||
self._db = int(server.get("db_index", 0))
|
self._db = int(server.get("db_index", 0))
|
||||||
|
self._ssl = server.get("ssl", False) or server.get("use_ssl", False)
|
||||||
self._conn = None
|
self._conn = None
|
||||||
|
|
||||||
# -- lifecycle --------------------------------------------------------
|
# -- lifecycle --------------------------------------------------------
|
||||||
@@ -38,6 +39,7 @@ class RedisClient:
|
|||||||
decode_responses=True,
|
decode_responses=True,
|
||||||
socket_timeout=5,
|
socket_timeout=5,
|
||||||
socket_connect_timeout=5,
|
socket_connect_timeout=5,
|
||||||
|
ssl=self._ssl,
|
||||||
)
|
)
|
||||||
self._conn.ping()
|
self._conn.ping()
|
||||||
log.info("Redis connected %s:%s db=%s", self._host, self._port, self._db)
|
log.info("Redis connected %s:%s db=%s", self._host, self._port, self._db)
|
||||||
@@ -75,7 +77,11 @@ class RedisClient:
|
|||||||
"""Parse a raw command string, execute via redis-py, return formatted."""
|
"""Parse a raw command string, execute via redis-py, return formatted."""
|
||||||
if not self._conn:
|
if not self._conn:
|
||||||
return "[not connected]"
|
return "[not connected]"
|
||||||
parts = command.split()
|
import shlex
|
||||||
|
try:
|
||||||
|
parts = shlex.split(command)
|
||||||
|
except ValueError:
|
||||||
|
parts = command.split() # fallback для незакрытых кавычек
|
||||||
if not parts:
|
if not parts:
|
||||||
return ""
|
return ""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -100,12 +100,9 @@ class StatusChecker:
|
|||||||
try:
|
try:
|
||||||
from core.redis_client import RedisClient
|
from core.redis_client import RedisClient
|
||||||
client = RedisClient(server)
|
client = RedisClient(server)
|
||||||
result = client.connect()
|
result = client.connect() # connect() уже делает ping()
|
||||||
if result:
|
client.disconnect()
|
||||||
ok = client.check_connection()
|
return result
|
||||||
client.disconnect()
|
|
||||||
return ok
|
|
||||||
return False
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ FIELD_MAP = {
|
|||||||
"mariadb": ["user", "password", "database"],
|
"mariadb": ["user", "password", "database"],
|
||||||
"mssql": ["user", "password", "database"],
|
"mssql": ["user", "password", "database"],
|
||||||
"postgresql": ["user", "password", "database"],
|
"postgresql": ["user", "password", "database"],
|
||||||
"redis": ["password", "db_index"],
|
"redis": ["password", "db_index", "use_ssl"],
|
||||||
"grafana": ["api_token", "use_ssl"],
|
"grafana": ["api_token", "use_ssl"],
|
||||||
"prometheus": ["use_ssl"],
|
"prometheus": ["use_ssl"],
|
||||||
"rdp": ["user", "password", "rdp_resolution", "rdp_quality", "rdp_clipboard", "rdp_drives", "rdp_printers"],
|
"rdp": ["user", "password", "rdp_resolution", "rdp_quality", "rdp_clipboard", "rdp_drives", "rdp_printers"],
|
||||||
|
|||||||
@@ -188,18 +188,14 @@ class RedisTab(ctk.CTkFrame):
|
|||||||
client = self._get_client()
|
client = self._get_client()
|
||||||
db = int(self._db_var.get())
|
db = int(self._db_var.get())
|
||||||
client.select_db(db)
|
client.select_db(db)
|
||||||
keys_count = client.execute("DBSIZE")
|
|
||||||
info = client.execute("INFO memory")
|
|
||||||
|
|
||||||
# Parse memory from INFO output
|
# Прямые методы возвращают int и dict, не форматированные строки
|
||||||
memory = "—"
|
keys_count = client.dbsize()
|
||||||
if isinstance(info, str):
|
info = client.info("memory")
|
||||||
for line in info.split("\r\n"):
|
|
||||||
if line.startswith("used_memory_human:"):
|
memory = info.get("used_memory_human", "—") if info else "—"
|
||||||
memory = line.split(":")[1].strip()
|
keys_text = f"{keys_count:,}" if keys_count is not None else "—"
|
||||||
break
|
|
||||||
|
|
||||||
keys_text = str(keys_count) if keys_count is not None else "—"
|
|
||||||
self.after(0, lambda: self._keys_label.configure(
|
self.after(0, lambda: self._keys_label.configure(
|
||||||
text=t("redis_keys") + f": {keys_text}"))
|
text=t("redis_keys") + f": {keys_text}"))
|
||||||
self.after(0, lambda: self._memory_label.configure(
|
self.after(0, lambda: self._memory_label.configure(
|
||||||
|
|||||||
561
plans/redis-fixes.md
Normal file
561
plans/redis-fixes.md
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
# План исправления Redis — диффы с объяснениями
|
||||||
|
|
||||||
|
**Дата:** 2026-02-28
|
||||||
|
**Аудит:** [`REDIS_AUDIT.md`](../REDIS_AUDIT.md)
|
||||||
|
**Статус:** Верифицирован, готов к реализации
|
||||||
|
**Верификация:** 2026-02-28 — все 7 диффов проверены против кодовой базы
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DIFF 1: Парсер команд — shlex вместо split (БАГ #1, КРИТИЧНЫЙ)
|
||||||
|
|
||||||
|
### Почему
|
||||||
|
|
||||||
|
`command.split()` разбивает строку по ВСЕМ пробелам, игнорируя кавычки.
|
||||||
|
`shlex.split()` понимает одинарные, двойные кавычки и экранирование — стандарт
|
||||||
|
для shell-подобного парсинга.
|
||||||
|
|
||||||
|
```
|
||||||
|
split(): "SET key 'hello world'" → ['SET', 'key', "'hello", "world'"] ← СЛОМАНО
|
||||||
|
shlex.split(): "SET key 'hello world'" → ['SET', 'key', 'hello world'] ← ПРАВИЛЬНО
|
||||||
|
```
|
||||||
|
|
||||||
|
### Файл: `core/redis_client.py` строка 78
|
||||||
|
|
||||||
|
**ДО:**
|
||||||
|
```python
|
||||||
|
def execute(self, command: str) -> str:
|
||||||
|
"""Parse a raw command string, execute via redis-py, return formatted."""
|
||||||
|
if not self._conn:
|
||||||
|
return "[not connected]"
|
||||||
|
parts = command.split()
|
||||||
|
if not parts:
|
||||||
|
return ""
|
||||||
|
```
|
||||||
|
|
||||||
|
**ПОСЛЕ:**
|
||||||
|
```python
|
||||||
|
def execute(self, command: str) -> str:
|
||||||
|
"""Parse a raw command string, execute via redis-py, return formatted."""
|
||||||
|
if not self._conn:
|
||||||
|
return "[not connected]"
|
||||||
|
import shlex
|
||||||
|
try:
|
||||||
|
parts = shlex.split(command)
|
||||||
|
except ValueError:
|
||||||
|
parts = command.split() # fallback для незакрытых кавычек
|
||||||
|
if not parts:
|
||||||
|
return ""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Файл: `tools/ssh.py` строка 958
|
||||||
|
|
||||||
|
**ДО:**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
parts = command.split()
|
||||||
|
if not parts:
|
||||||
|
print("ERROR: Empty Redis command")
|
||||||
|
sys.exit(1)
|
||||||
|
result = r.execute_command(*parts)
|
||||||
|
```
|
||||||
|
|
||||||
|
**ПОСЛЕ:**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
import shlex
|
||||||
|
try:
|
||||||
|
parts = shlex.split(command)
|
||||||
|
except ValueError:
|
||||||
|
parts = command.split()
|
||||||
|
if not parts:
|
||||||
|
print("ERROR: Empty Redis command")
|
||||||
|
sys.exit(1)
|
||||||
|
result = r.execute_command(*parts)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Результат
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Было:
|
||||||
|
--redis ALIAS "SET key 'hello world'" → syntax error
|
||||||
|
# Стало:
|
||||||
|
--redis ALIAS "SET key 'hello world'" → True
|
||||||
|
--redis ALIAS "GET key" → hello world
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DIFF 2: GUI stats — прямые методы вместо execute (БАГ #2, СРЕДНИЙ)
|
||||||
|
|
||||||
|
### Почему
|
||||||
|
|
||||||
|
`client.execute("DBSIZE")` возвращает `"(integer) 27199166"` — уже отформатированную строку.
|
||||||
|
Этот текст отображается в label как есть, включая `(integer)`.
|
||||||
|
|
||||||
|
`client.execute("INFO memory")` возвращает строку где `_format()` заменяет `\r\n` на `\n`,
|
||||||
|
но парсер ищет `\r\n` → ничего не находит → memory всегда `"—"`.
|
||||||
|
|
||||||
|
**Решение:** Использовать `client.dbsize()` (int) и `client.info("memory")` (dict) напрямую.
|
||||||
|
|
||||||
|
### Файл: `gui/tabs/redis_tab.py` метод `_refresh_stats()` строки 188-210
|
||||||
|
|
||||||
|
**ДО:**
|
||||||
|
```python
|
||||||
|
def _refresh_stats(self):
|
||||||
|
if not self._current_alias:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _do():
|
||||||
|
try:
|
||||||
|
client = self._get_client()
|
||||||
|
db = int(self._db_var.get())
|
||||||
|
client.select_db(db)
|
||||||
|
keys_count = client.execute("DBSIZE")
|
||||||
|
info = client.execute("INFO memory")
|
||||||
|
|
||||||
|
# Parse memory from INFO output
|
||||||
|
memory = "—"
|
||||||
|
if isinstance(info, str):
|
||||||
|
for line in info.split("\r\n"):
|
||||||
|
if line.startswith("used_memory_human:"):
|
||||||
|
memory = line.split(":")[1].strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
keys_text = str(keys_count) if keys_count is not None else "—"
|
||||||
|
self.after(0, lambda: self._keys_label.configure(
|
||||||
|
text=t("redis_keys") + f": {keys_text}"))
|
||||||
|
self.after(0, lambda: self._memory_label.configure(
|
||||||
|
text=t("redis_memory") + f": {memory}"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
threading.Thread(target=_do, daemon=True).start()
|
||||||
|
```
|
||||||
|
|
||||||
|
**ПОСЛЕ:**
|
||||||
|
```python
|
||||||
|
def _refresh_stats(self):
|
||||||
|
if not self._current_alias:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _do():
|
||||||
|
try:
|
||||||
|
client = self._get_client()
|
||||||
|
db = int(self._db_var.get())
|
||||||
|
client.select_db(db)
|
||||||
|
|
||||||
|
# Прямые методы возвращают int и dict, не форматированные строки
|
||||||
|
keys_count = client.dbsize()
|
||||||
|
info = client.info("memory")
|
||||||
|
|
||||||
|
memory = info.get("used_memory_human", "—") if info else "—"
|
||||||
|
keys_text = f"{keys_count:,}" if keys_count is not None else "—"
|
||||||
|
|
||||||
|
self.after(0, lambda: self._keys_label.configure(
|
||||||
|
text=t("redis_keys") + f": {keys_text}"))
|
||||||
|
self.after(0, lambda: self._memory_label.configure(
|
||||||
|
text=t("redis_memory") + f": {memory}"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
threading.Thread(target=_do, daemon=True).start()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Результат
|
||||||
|
|
||||||
|
```
|
||||||
|
# Было:
|
||||||
|
Keys: (integer) 27199166 Memory: —
|
||||||
|
|
||||||
|
# Стало:
|
||||||
|
Keys: 27,199,166 Memory: 2.82G
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DIFF 3: SSL поддержка в GUI RedisClient (БАГ #3, СРЕДНИЙ)
|
||||||
|
|
||||||
|
### Почему
|
||||||
|
|
||||||
|
CLI (`tools/ssh.py`) передаёт `ssl=server.get("ssl", False)` при создании подключения.
|
||||||
|
GUI (`redis_client.py`) этот параметр НЕ передаёт → невозможно подключиться к Redis с TLS.
|
||||||
|
|
||||||
|
Дополнительно: `server_dialog.py` не показывает поле `ssl` для Redis в UI.
|
||||||
|
|
||||||
|
### Файл: `core/redis_client.py` конструктор и connect()
|
||||||
|
|
||||||
|
**ДО:**
|
||||||
|
```python
|
||||||
|
class RedisClient:
|
||||||
|
def __init__(self, server: dict):
|
||||||
|
self._host = server["ip"]
|
||||||
|
self._port = int(server.get("port", 6379))
|
||||||
|
self._password = server.get("password") or None
|
||||||
|
self._db = int(server.get("db_index", 0))
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
def connect(self) -> bool:
|
||||||
|
try:
|
||||||
|
r = _get_redis()
|
||||||
|
self._conn = r.Redis(
|
||||||
|
host=self._host,
|
||||||
|
port=self._port,
|
||||||
|
password=self._password,
|
||||||
|
db=self._db,
|
||||||
|
decode_responses=True,
|
||||||
|
socket_timeout=5,
|
||||||
|
socket_connect_timeout=5,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**ПОСЛЕ:**
|
||||||
|
```python
|
||||||
|
class RedisClient:
|
||||||
|
def __init__(self, server: dict):
|
||||||
|
self._host = server["ip"]
|
||||||
|
self._port = int(server.get("port", 6379))
|
||||||
|
self._password = server.get("password") or None
|
||||||
|
self._db = int(server.get("db_index", 0))
|
||||||
|
self._ssl = server.get("ssl", False) or server.get("use_ssl", False)
|
||||||
|
self._conn = None
|
||||||
|
|
||||||
|
def connect(self) -> bool:
|
||||||
|
try:
|
||||||
|
r = _get_redis()
|
||||||
|
self._conn = r.Redis(
|
||||||
|
host=self._host,
|
||||||
|
port=self._port,
|
||||||
|
password=self._password,
|
||||||
|
db=self._db,
|
||||||
|
decode_responses=True,
|
||||||
|
socket_timeout=5,
|
||||||
|
socket_connect_timeout=5,
|
||||||
|
ssl=self._ssl,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Файл: `gui/server_dialog.py` строка 22 — FIELD_MAP
|
||||||
|
|
||||||
|
**ДО:**
|
||||||
|
```python
|
||||||
|
"redis": ["password", "db_index"],
|
||||||
|
```
|
||||||
|
|
||||||
|
**ПОСЛЕ:**
|
||||||
|
```python
|
||||||
|
"redis": ["password", "db_index", "use_ssl"],
|
||||||
|
```
|
||||||
|
|
||||||
|
### Почему `ssl` и `use_ssl`
|
||||||
|
|
||||||
|
В проекте используются оба ключа в разных местах:
|
||||||
|
- `tools/ssh.py` читает `server.get("ssl", False)`
|
||||||
|
- `gui/server_dialog.py` сохраняет `use_ssl` (для WinRM)
|
||||||
|
|
||||||
|
Поэтому в `RedisClient` проверяем оба: `server.get("ssl", False) or server.get("use_ssl", False)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DIFF 4: CLI error handling (БАГ #4, НИЗКИЙ)
|
||||||
|
|
||||||
|
### Почему
|
||||||
|
|
||||||
|
При ошибке Redis (WRONGTYPE, NOSCRIPT, синтаксис) выводится полный Python traceback
|
||||||
|
с двойным повтором. Пользователь видит нечитаемое сообщение.
|
||||||
|
|
||||||
|
### Файл: `tools/ssh.py` функция `run_redis_cmd()` строки 957-973
|
||||||
|
|
||||||
|
**ДО:**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
parts = command.split()
|
||||||
|
if not parts:
|
||||||
|
print("ERROR: Empty Redis command")
|
||||||
|
sys.exit(1)
|
||||||
|
result = r.execute_command(*parts)
|
||||||
|
if isinstance(result, list):
|
||||||
|
for i, item in enumerate(result):
|
||||||
|
print(f"{i + 1}) {item}")
|
||||||
|
print(f"\n({len(result)} items)")
|
||||||
|
elif isinstance(result, dict):
|
||||||
|
for k, v in result.items():
|
||||||
|
print(f"{k}: {v}")
|
||||||
|
elif isinstance(result, bytes):
|
||||||
|
print(result.decode("utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
finally:
|
||||||
|
r.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
**ПОСЛЕ:**
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
import shlex
|
||||||
|
try:
|
||||||
|
parts = shlex.split(command)
|
||||||
|
except ValueError:
|
||||||
|
parts = command.split()
|
||||||
|
if not parts:
|
||||||
|
print("ERROR: Empty Redis command")
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
result = r.execute_command(*parts)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"(error) {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
if isinstance(result, list):
|
||||||
|
for i, item in enumerate(result):
|
||||||
|
print(f"{i + 1}) {item}")
|
||||||
|
print(f"\n({len(result)} items)")
|
||||||
|
elif isinstance(result, dict):
|
||||||
|
for k, v in result.items():
|
||||||
|
print(f"{k}: {v}")
|
||||||
|
elif isinstance(result, bytes):
|
||||||
|
print(result.decode("utf-8", errors="replace"))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
finally:
|
||||||
|
r.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Результат
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Было:
|
||||||
|
ERROR: ResponseError: WRONGTYPE Operation against a key holding the wrong kind of value
|
||||||
|
ERROR: ResponseError: WRONGTYPE Operation against a key holding the wrong kind of value
|
||||||
|
|
||||||
|
# Стало:
|
||||||
|
(error) WRONGTYPE Operation against a key holding the wrong kind of value
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DIFF 5: Двойной PING → одинарный (БАГ #5, НИЗКИЙ)
|
||||||
|
|
||||||
|
### Почему
|
||||||
|
|
||||||
|
`connect()` делает `self._conn.ping()`. Затем `check_connection()` делает ещё один
|
||||||
|
`self._conn.ping()`. Два network round-trip вместо одного при каждом status check.
|
||||||
|
При 10 серверах × каждые 30 секунд = 20 лишних пингов.
|
||||||
|
|
||||||
|
### Файл: `core/status_checker.py` строки 98-110
|
||||||
|
|
||||||
|
**ДО:**
|
||||||
|
```python
|
||||||
|
def _check_redis(self, server: dict) -> bool:
|
||||||
|
"""Check Redis via PING."""
|
||||||
|
try:
|
||||||
|
from core.redis_client import RedisClient
|
||||||
|
client = RedisClient(server)
|
||||||
|
result = client.connect()
|
||||||
|
if result:
|
||||||
|
ok = client.check_connection()
|
||||||
|
client.disconnect()
|
||||||
|
return ok
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
**ПОСЛЕ:**
|
||||||
|
```python
|
||||||
|
def _check_redis(self, server: dict) -> bool:
|
||||||
|
"""Check Redis via PING."""
|
||||||
|
try:
|
||||||
|
from core.redis_client import RedisClient
|
||||||
|
client = RedisClient(server)
|
||||||
|
result = client.connect() # connect() уже делает ping()
|
||||||
|
client.disconnect()
|
||||||
|
return result
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DIFF 6: CLI status — добавить try-except и close (БАГ #6, НИЗКИЙ)
|
||||||
|
|
||||||
|
### Почему
|
||||||
|
|
||||||
|
Если Redis offline, `r.ping()` бросает исключение. `r.close()` не вызывается (утечка).
|
||||||
|
Ошибка не перехвачена → traceback вместо `OFFLINE`.
|
||||||
|
|
||||||
|
### Файл: `tools/ssh.py` строки 698-703
|
||||||
|
|
||||||
|
**ДО:**
|
||||||
|
```python
|
||||||
|
if stype == "redis":
|
||||||
|
import redis as redis_lib
|
||||||
|
r = redis_lib.Redis(host=server["ip"], port=server.get("port", 6379),
|
||||||
|
password=server.get("password", "") or None,
|
||||||
|
db=server.get("db_index", 0), socket_timeout=10,
|
||||||
|
ssl=server.get("ssl", False))
|
||||||
|
r.ping()
|
||||||
|
r.close()
|
||||||
|
return "ONLINE"
|
||||||
|
```
|
||||||
|
|
||||||
|
**ПОСЛЕ:**
|
||||||
|
```python
|
||||||
|
if stype == "redis":
|
||||||
|
import redis as redis_lib
|
||||||
|
r = redis_lib.Redis(host=server["ip"], port=server.get("port", 6379),
|
||||||
|
password=server.get("password", "") or None,
|
||||||
|
db=server.get("db_index", 0), socket_timeout=10,
|
||||||
|
ssl=server.get("ssl", False))
|
||||||
|
try:
|
||||||
|
r.ping()
|
||||||
|
return "ONLINE"
|
||||||
|
finally:
|
||||||
|
r.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DIFF 7: Дедупликация подключения в CLI (БАГ #7, УЛУЧШЕНИЕ)
|
||||||
|
|
||||||
|
### Почему
|
||||||
|
|
||||||
|
Три функции (`run_redis_cmd`, `redis_info`, `redis_keys`) содержат одинаковый блок
|
||||||
|
из 6 строк для создания Redis-подключения. При изменении параметров (например, добавление
|
||||||
|
`ssl_cert_reqs`) нужно менять в 3 местах.
|
||||||
|
|
||||||
|
### Файл: `tools/ssh.py` — добавить хелпер перед строкой 946
|
||||||
|
|
||||||
|
**ДОБАВИТЬ:**
|
||||||
|
```python
|
||||||
|
def _get_redis_client(server: dict):
|
||||||
|
"""Create a Redis client from server config. Single source of truth."""
|
||||||
|
import redis as redis_lib
|
||||||
|
return redis_lib.Redis(
|
||||||
|
host=server["ip"],
|
||||||
|
port=server.get("port", 6379),
|
||||||
|
password=server.get("password", "") or None,
|
||||||
|
db=server.get("db_index", 0),
|
||||||
|
decode_responses=True,
|
||||||
|
socket_timeout=10,
|
||||||
|
ssl=server.get("ssl", False),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Затем заменить во всех трёх функциях:**
|
||||||
|
```python
|
||||||
|
# Было (в каждой из 3 функций):
|
||||||
|
import redis as redis_lib
|
||||||
|
host = server["ip"]
|
||||||
|
port = server.get("port", 6379)
|
||||||
|
password = server.get("password", "") or None
|
||||||
|
db_index = server.get("db_index", 0)
|
||||||
|
ssl_enabled = server.get("ssl", False)
|
||||||
|
r = redis_lib.Redis(host=host, port=port, password=password, db=db_index,
|
||||||
|
decode_responses=True, socket_timeout=10, ssl=ssl_enabled)
|
||||||
|
|
||||||
|
# Стало:
|
||||||
|
r = _get_redis_client(server)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Порядок применения
|
||||||
|
|
||||||
|
| # | Дифф | Файлы | Строк | Приоритет |
|
||||||
|
|---|------|-------|-------|-----------|
|
||||||
|
| 1 | Парсер shlex | `redis_client.py`, `ssh.py` | 8 | КРИТИЧНЫЙ |
|
||||||
|
| 2 | GUI stats | `redis_tab.py` | 10 | СРЕДНИЙ |
|
||||||
|
| 3 | SSL в GUI | `redis_client.py`, `server_dialog.py` | 4 | СРЕДНИЙ |
|
||||||
|
| 4 | CLI error handling | `ssh.py` | 6 | НИЗКИЙ |
|
||||||
|
| 5 | Double PING | `status_checker.py` | 2 | НИЗКИЙ |
|
||||||
|
| 6 | CLI status fix | `ssh.py` | 4 | НИЗКИЙ |
|
||||||
|
| 7 | Дедупликация | `ssh.py` | -12 (убрано дублей) | УЛУЧШЕНИЕ |
|
||||||
|
|
||||||
|
**Всего:** ~20 строк нового кода, -12 строк дублей = +8 строк нетто.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Схема: что сломано и что фиксит каждый дифф
|
||||||
|
|
||||||
|
```
|
||||||
|
Пользователь вводит: SET mykey "hello world"
|
||||||
|
│
|
||||||
|
├─ CLI (ssh.py) ├─ GUI (redis_tab.py)
|
||||||
|
│ │ │ │
|
||||||
|
│ command.split() │ client.execute()
|
||||||
|
│ ❌ DIFF 1 фиксит │ │
|
||||||
|
│ │ command.split() ← redis_client.py
|
||||||
|
│ │ ❌ DIFF 1 фиксит
|
||||||
|
│ │
|
||||||
|
│ ├─ _refresh_stats()
|
||||||
|
│ │ execute("DBSIZE") → "(integer) N"
|
||||||
|
│ │ ❌ DIFF 2 фиксит → dbsize() → int
|
||||||
|
│ │
|
||||||
|
│ │ execute("INFO memory") → строка без \r\n
|
||||||
|
│ │ ❌ DIFF 2 фиксит → info("memory") → dict
|
||||||
|
│ │
|
||||||
|
├─ SSL подключение ├─ SSL подключение
|
||||||
|
│ ✅ Работает │ ❌ DIFF 3 фиксит
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Результаты верификации (аудит плана)
|
||||||
|
|
||||||
|
Каждый дифф проверен тремя независимыми аудиторами против текущей кодовой базы.
|
||||||
|
|
||||||
|
### Сводная таблица
|
||||||
|
|
||||||
|
| DIFF | Точное совпадение ДО↔код | Риски | Статус |
|
||||||
|
|------|--------------------------|-------|--------|
|
||||||
|
| 1 — shlex | ✅ redis_client.py:78 + ssh.py:958 | Нулевой. 11 команд протестированы | ОДОБРЕН |
|
||||||
|
| 2 — stats | ✅ redis_tab.py:191-208 | Нулевой. dbsize()→int, info()→dict подтверждено | ОДОБРЕН |
|
||||||
|
| 3 — SSL | ✅ redis_client.py:21-43 + dialog:22 | Нулевой. use_ssl чекбокс уже существует в UI | ОДОБРЕН |
|
||||||
|
| 4 — errors | ✅ ssh.py:957-975 | Нулевой. finally вызывается даже при sys.exit | ОДОБРЕН |
|
||||||
|
| 5 — ping | ✅ status_checker.py:98-110 | Нулевой. connect() содержит ping() | ОДОБРЕН |
|
||||||
|
| 6 — status | ✅ ssh.py:698-703 | Нулевой. try-finally корректно | ОДОБРЕН |
|
||||||
|
| 7 — dedup | ✅ 3 копии по 9 строк | **НИЗКИЙ** — см. замечание ниже | ОДОБРЕН с замечанием |
|
||||||
|
|
||||||
|
### Замечания аудиторов
|
||||||
|
|
||||||
|
**DIFF 1:**
|
||||||
|
- shlex.split() протестирован на: PING, GET, SET, HGETALL, INFO, SCAN, SELECT, MSET, CONFIG SET, значения с кавычками, Windows-пути
|
||||||
|
- Все 11 типов команд парсятся корректно
|
||||||
|
- Fallback на split() при ValueError (незакрытые кавычки) — корректный
|
||||||
|
- Inline import shlex соответствует паттерну проекта (lazy imports)
|
||||||
|
|
||||||
|
**DIFF 2:**
|
||||||
|
- `dbsize()` всегда возвращает int (0 при ошибке, никогда None)
|
||||||
|
- `info("memory")` возвращает dict с ключом `"used_memory_human"`
|
||||||
|
- `f"{keys_count:,}"` безопасен для 0 и любого int
|
||||||
|
- Thread race condition существует но **pre-existing** — DIFF 2 не вводит новых
|
||||||
|
|
||||||
|
**DIFF 3:**
|
||||||
|
- `use_ssl` — согласовано с паттерном проекта (winrm, grafana, prometheus используют то же)
|
||||||
|
- UI чекбокс `use_ssl` уже существует в server_dialog.py, нужно только добавить в FIELD_MAP
|
||||||
|
- Dual-key fallback `server.get("ssl") or server.get("use_ssl")` покрывает CLI и GUI пути
|
||||||
|
- `ssl=False` по умолчанию — не ломает существующие подключения
|
||||||
|
|
||||||
|
**DIFF 5:**
|
||||||
|
- `connect()` вызывает `self._conn.ping()` на строке 42 redis_client.py — подтверждено
|
||||||
|
- Второй ping через `check_connection()` — гарантированно дубликат
|
||||||
|
|
||||||
|
**DIFF 7 — ЗАМЕЧАНИЕ:**
|
||||||
|
- Status check в ssh.py:698-703 содержит **4-й дубликат** подключения Redis
|
||||||
|
- План НЕ включает его в дедупликацию
|
||||||
|
- **Рекомендация:** Обновить status check тоже при реализации DIFF 7
|
||||||
|
- **Примечание:** decode_responses=True в хелпере безопасен для status check (ping() не зависит от декодирования)
|
||||||
|
|
||||||
|
### Итог верификации
|
||||||
|
|
||||||
|
**7 из 7 диффов ОДОБРЕНЫ.** Ни один не сломает существующую функциональность.
|
||||||
|
Единственное замечание: DIFF 7 стоит расширить на status check (4-й дубликат).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Связанные документы
|
||||||
|
|
||||||
|
- [`REDIS_AUDIT.md`](../REDIS_AUDIT.md) — полный отчёт аудита
|
||||||
|
- [`plans/reliable-sftp-upload.md`](reliable-sftp-upload.md) — план по SFTP
|
||||||
|
- [`SFTP_UPLOAD_AUDIT.md`](../SFTP_UPLOAD_AUDIT.md) — аудит загрузки
|
||||||
|
- [`SFTP_UPLOAD_DIFFS.md`](../SFTP_UPLOAD_DIFFS.md) — диффы загрузки
|
||||||
BIN
releases/ServerManager-v1.8.66-win-x64.exe
Normal file
BIN
releases/ServerManager-v1.8.66-win-x64.exe
Normal file
Binary file not shown.
70
tools/ssh.py
70
tools/ssh.py
@@ -694,14 +694,12 @@ def _check_status_one(server: dict) -> str:
|
|||||||
return "ONLINE"
|
return "ONLINE"
|
||||||
|
|
||||||
if stype == "redis":
|
if stype == "redis":
|
||||||
import redis as redis_lib
|
r = _get_redis_client(server)
|
||||||
r = redis_lib.Redis(host=server["ip"], port=server.get("port", 6379),
|
try:
|
||||||
password=server.get("password", "") or None,
|
r.ping()
|
||||||
db=server.get("db_index", 0), socket_timeout=10,
|
return "ONLINE"
|
||||||
ssl=server.get("ssl", False))
|
finally:
|
||||||
r.ping()
|
r.close()
|
||||||
r.close()
|
|
||||||
return "ONLINE"
|
|
||||||
|
|
||||||
if stype == "grafana":
|
if stype == "grafana":
|
||||||
import requests
|
import requests
|
||||||
@@ -991,23 +989,37 @@ def sql_tables(server: dict, database: str = None):
|
|||||||
|
|
||||||
# ── Redis commands ────────────────────────────────────
|
# ── Redis commands ────────────────────────────────────
|
||||||
|
|
||||||
|
def _get_redis_client(server: dict):
|
||||||
|
"""Create a Redis client from server config. Single source of truth."""
|
||||||
|
import redis as redis_lib
|
||||||
|
return redis_lib.Redis(
|
||||||
|
host=server["ip"],
|
||||||
|
port=server.get("port", 6379),
|
||||||
|
password=server.get("password", "") or None,
|
||||||
|
db=server.get("db_index", 0),
|
||||||
|
decode_responses=True,
|
||||||
|
socket_timeout=10,
|
||||||
|
ssl=server.get("ssl", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_redis_cmd(server: dict, command: str):
|
def run_redis_cmd(server: dict, command: str):
|
||||||
"""Execute a Redis command."""
|
"""Execute a Redis command."""
|
||||||
import redis as redis_lib
|
r = _get_redis_client(server)
|
||||||
host = server["ip"]
|
|
||||||
port = server.get("port", 6379)
|
|
||||||
password = server.get("password", "") or None
|
|
||||||
db_index = server.get("db_index", 0)
|
|
||||||
ssl_enabled = server.get("ssl", False)
|
|
||||||
|
|
||||||
r = redis_lib.Redis(host=host, port=port, password=password, db=db_index,
|
|
||||||
decode_responses=True, socket_timeout=10, ssl=ssl_enabled)
|
|
||||||
try:
|
try:
|
||||||
parts = command.split()
|
import shlex
|
||||||
|
try:
|
||||||
|
parts = shlex.split(command)
|
||||||
|
except ValueError:
|
||||||
|
parts = command.split()
|
||||||
if not parts:
|
if not parts:
|
||||||
print("ERROR: Empty Redis command")
|
print("ERROR: Empty Redis command")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
result = r.execute_command(*parts)
|
try:
|
||||||
|
result = r.execute_command(*parts)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"(error) {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
if isinstance(result, list):
|
if isinstance(result, list):
|
||||||
for i, item in enumerate(result):
|
for i, item in enumerate(result):
|
||||||
print(f"{i + 1}) {item}")
|
print(f"{i + 1}) {item}")
|
||||||
@@ -1025,15 +1037,7 @@ def run_redis_cmd(server: dict, command: str):
|
|||||||
|
|
||||||
def redis_info(server: dict):
|
def redis_info(server: dict):
|
||||||
"""Show Redis INFO."""
|
"""Show Redis INFO."""
|
||||||
import redis as redis_lib
|
r = _get_redis_client(server)
|
||||||
host = server["ip"]
|
|
||||||
port = server.get("port", 6379)
|
|
||||||
password = server.get("password", "") or None
|
|
||||||
db_index = server.get("db_index", 0)
|
|
||||||
ssl_enabled = server.get("ssl", False)
|
|
||||||
|
|
||||||
r = redis_lib.Redis(host=host, port=port, password=password, db=db_index,
|
|
||||||
decode_responses=True, socket_timeout=10, ssl=ssl_enabled)
|
|
||||||
try:
|
try:
|
||||||
info = r.info()
|
info = r.info()
|
||||||
# Print key sections
|
# Print key sections
|
||||||
@@ -1056,15 +1060,7 @@ def redis_info(server: dict):
|
|||||||
|
|
||||||
def redis_keys(server: dict, pattern: str):
|
def redis_keys(server: dict, pattern: str):
|
||||||
"""SCAN keys matching a pattern."""
|
"""SCAN keys matching a pattern."""
|
||||||
import redis as redis_lib
|
r = _get_redis_client(server)
|
||||||
host = server["ip"]
|
|
||||||
port = server.get("port", 6379)
|
|
||||||
password = server.get("password", "") or None
|
|
||||||
db_index = server.get("db_index", 0)
|
|
||||||
ssl_enabled = server.get("ssl", False)
|
|
||||||
|
|
||||||
r = redis_lib.Redis(host=host, port=port, password=password, db=db_index,
|
|
||||||
decode_responses=True, socket_timeout=10, ssl=ssl_enabled)
|
|
||||||
try:
|
try:
|
||||||
keys = []
|
keys = []
|
||||||
cursor = 0
|
cursor = 0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Version info for ServerManager."""
|
"""Version info for ServerManager."""
|
||||||
|
|
||||||
__version__ = "1.8.65"
|
__version__ = "1.8.66"
|
||||||
__app_name__ = "ServerManager"
|
__app_name__ = "ServerManager"
|
||||||
__author__ = "aibot777"
|
__author__ = "aibot777"
|
||||||
__description__ = "Desktop GUI for managing remote servers"
|
__description__ = "Desktop GUI for managing remote servers"
|
||||||
|
|||||||
Reference in New Issue
Block a user