Compare commits
No commits in common. "879013782f3211dde139bed86806cd67a8e0716d" and "bb2813f0edc9664f37e0ceb5a7f979ceda6a0d5a" have entirely different histories.
879013782f
...
bb2813f0ed
3
.gitignore
vendored
@ -6,8 +6,5 @@
|
|||||||
*.zip
|
*.zip
|
||||||
*.7z
|
*.7z
|
||||||
|
|
||||||
# иконки трея — часть кода, а не личные картинки
|
|
||||||
!icons/*.png
|
|
||||||
|
|
||||||
vpn_config.json
|
vpn_config.json
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
27
README.md
@ -9,31 +9,6 @@
|
|||||||
5. Положить секреты в keyring: `bash init.sh <secret> <static_pass>`.
|
5. Положить секреты в keyring: `bash init.sh <secret> <static_pass>`.
|
||||||
6. Запуск: `python launch_vpn_gui.py` (запросит права администратора).
|
6. Запуск: `python launch_vpn_gui.py` (запросит права администратора).
|
||||||
|
|
||||||
Ярлык без окна консоли — `launch_vpn.bat`: ищет `pythonw.exe` сначала в `.venv` проекта, затем в PATH, затем через `pyw.exe -3`.
|
|
||||||
|
|
||||||
## Автозапуск при входе в систему
|
|
||||||
|
|
||||||
Через Планировщик заданий, а не папку «Автозагрузка»: оттуда процесс стартует непривилегированным, и `ensure_admin()` дёргает UAC-окно при каждом входе. Задача с «Запускать с наивысшими правами» отдаёт админский токен сразу — промпта нет.
|
|
||||||
|
|
||||||
Регистрация (один раз, нужен админ):
|
|
||||||
|
|
||||||
```
|
|
||||||
schtasks /create /tn VPN-TOTP-Tray /xml vpn_tray_task.xml /f
|
|
||||||
```
|
|
||||||
|
|
||||||
`vpn_tray_task.xml` лежит в корне репозитория (в UTF-16 — этого требует `schtasks /xml`). Пути к `launch_vpn.bat` и имя пользователя в нём захардкожены абсолютными — при переносе проекта или на другой машине правь `<Arguments>`, `<WorkingDirectory>` и оба `<UserId>`.
|
|
||||||
|
|
||||||
Управление:
|
|
||||||
|
|
||||||
```
|
|
||||||
schtasks /query /tn VPN-TOTP-Tray /v /fo list # статус
|
|
||||||
schtasks /run /tn VPN-TOTP-Tray # запустить сейчас
|
|
||||||
schtasks /change /tn VPN-TOTP-Tray /disable # временно выключить автозапуск
|
|
||||||
schtasks /delete /tn VPN-TOTP-Tray /f # снести (нужен админ)
|
|
||||||
```
|
|
||||||
|
|
||||||
Или через GUI: `taskschd.msc` → Библиотека планировщика → `VPN-TOTP-Tray`.
|
|
||||||
|
|
||||||
## Индикатор в трее
|
## Индикатор в трее
|
||||||
|
|
||||||
- 🔘 серый — openvpn не запущен;
|
- 🔘 серый — openvpn не запущен;
|
||||||
@ -42,8 +17,6 @@ schtasks /delete /tn VPN-TOTP-Tray /f # снести (нужен ад
|
|||||||
|
|
||||||
Проверку связи можно отключить (`"ping_check_enabled": false`) — тогда только серый/зелёный.
|
Проверку связи можно отключить (`"ping_check_enabled": false`) — тогда только серый/зелёный.
|
||||||
|
|
||||||
Картинки берутся из `icons/` (`v-gray-256.png`, `v-yellow-256.png`, `v-green-256.png`). Если файла нет — рисуется кружок того же цвета.
|
|
||||||
|
|
||||||
## Настройки (`vpn_config.json`)
|
## Настройки (`vpn_config.json`)
|
||||||
|
|
||||||
Правятся вручную или через пункт меню «Открыть настройки»; перечитываются на лету.
|
Правятся вручную или через пункт меню «Открыть настройки»; перечитываются на лету.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB |
BIN
icons/v-gray.ico
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 11 KiB |
BIN
icons/v-red.ico
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 28 KiB |
@ -1,36 +0,0 @@
|
|||||||
@echo off
|
|
||||||
:: launch launch_vpn_gui.py without a console window via pythonw
|
|
||||||
setlocal
|
|
||||||
cd /d "%~dp0"
|
|
||||||
|
|
||||||
set "SCRIPT=%~dp0launch_vpn_gui.py"
|
|
||||||
|
|
||||||
if not exist "%SCRIPT%" (
|
|
||||||
echo launch_vpn_gui.py not found next to this .bat
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
:: 1) project venv, if present
|
|
||||||
if exist "%~dp0.venv\Scripts\pythonw.exe" (
|
|
||||||
start "" "%~dp0.venv\Scripts\pythonw.exe" "%SCRIPT%"
|
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
:: 2) system pythonw from PATH
|
|
||||||
where pythonw.exe >nul 2>&1
|
|
||||||
if not errorlevel 1 (
|
|
||||||
start "" pythonw.exe "%SCRIPT%"
|
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
:: 3) py launcher
|
|
||||||
where pyw.exe >nul 2>&1
|
|
||||||
if not errorlevel 1 (
|
|
||||||
start "" pyw.exe -3 "%SCRIPT%"
|
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
echo pythonw.exe not found: install Python or create .venv
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
@ -71,15 +71,6 @@ STATE_COLORS = {
|
|||||||
STATE_WAIT: (230, 200, 40, 255),
|
STATE_WAIT: (230, 200, 40, 255),
|
||||||
STATE_ON: (40, 200, 80, 255),
|
STATE_ON: (40, 200, 80, 255),
|
||||||
}
|
}
|
||||||
# Готовые иконки в icons/ (цвета те же, что в STATE_COLORS). Если файла нет —
|
|
||||||
# make_icon() рисует кружок соответствующего цвета, как раньше.
|
|
||||||
ICONS_DIR = os.path.join(
|
|
||||||
os.path.dirname(os.path.abspath(__file__)), "icons")
|
|
||||||
STATE_ICON_FILES = {
|
|
||||||
STATE_OFF: "v-gray-256.png",
|
|
||||||
STATE_WAIT: "v-yellow-256.png",
|
|
||||||
STATE_ON: "v-green-256.png",
|
|
||||||
}
|
|
||||||
STATE_TITLES = {
|
STATE_TITLES = {
|
||||||
STATE_OFF: "VPN: отключено",
|
STATE_OFF: "VPN: отключено",
|
||||||
STATE_WAIT: "VPN: запущен, нет связи с сервером",
|
STATE_WAIT: "VPN: запущен, нет связи с сервером",
|
||||||
@ -181,31 +172,12 @@ def current_state() -> str:
|
|||||||
return STATE_ON if ping_alive else STATE_WAIT
|
return STATE_ON if ping_alive else STATE_WAIT
|
||||||
|
|
||||||
|
|
||||||
_icon_cache = {}
|
|
||||||
|
|
||||||
|
|
||||||
def make_icon(state: str) -> Image.Image:
|
def make_icon(state: str) -> Image.Image:
|
||||||
"""Иконка состояния: серый = выкл, жёлтый = нет связи, зелёный = связь есть.
|
"""Кружок: серый = выкл, жёлтый = нет связи, зелёный = связь есть."""
|
||||||
Берём картинку из icons/, при её отсутствии — рисуем кружок того же цвета."""
|
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
|
||||||
if state in _icon_cache:
|
d = ImageDraw.Draw(img)
|
||||||
return _icon_cache[state]
|
color = STATE_COLORS.get(state, STATE_COLORS[STATE_OFF])
|
||||||
|
d.ellipse((12, 12, 52, 52), fill=color)
|
||||||
name = STATE_ICON_FILES.get(state, STATE_ICON_FILES[STATE_OFF])
|
|
||||||
path = os.path.join(ICONS_DIR, name)
|
|
||||||
img = None
|
|
||||||
try:
|
|
||||||
with Image.open(path) as f:
|
|
||||||
img = f.convert("RGBA").resize((64, 64), Image.LANCZOS)
|
|
||||||
except (OSError, ValueError):
|
|
||||||
img = None
|
|
||||||
|
|
||||||
if img is None:
|
|
||||||
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
|
|
||||||
d = ImageDraw.Draw(img)
|
|
||||||
color = STATE_COLORS.get(state, STATE_COLORS[STATE_OFF])
|
|
||||||
d.ellipse((12, 12, 52, 52), fill=color)
|
|
||||||
|
|
||||||
_icon_cache[state] = img
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
|||||||