Compare commits

...

9 Commits

Author SHA1 Message Date
delewer 1728fabfc7 docs(README): update preview image source with better visual (#817) 2026-05-06 17:31:49 +03:00
deexsed bf08bad11e Fix --cfproxy-priority CLI parsing (#797) 2026-04-30 13:29:43 +03:00
Kleshzz a787b4fd6b Update README.md (#796) 2026-04-30 06:12:54 +03:00
Sanfix 2c577c1166 Update README.md (change service name in cli version) (#783) 2026-04-28 17:38:37 +03:00
delewer 05b22fe3ba docs(README): fix little typo bug (#778) 2026-04-26 18:54:02 +03:00
Flowseal cc00c6d040 Version bump 2026-04-26 16:58:48 +03:00
Flowseal b3ed5c09db Windows auto update 2026-04-26 16:58:17 +03:00
Flowseal b8556dc702 fix #775 2026-04-26 16:26:50 +03:00
Flowseal 28be00ea9e docs update 2026-04-19 17:32:54 +03:00
8 changed files with 327 additions and 28 deletions
+1
View File
@@ -5,3 +5,4 @@ zaewayzmplad.com
twdmbzcm.com twdmbzcm.com
awzwsldi.com awzwsldi.com
clngqrflngqin.com clngqrflngqin.com
tjacxbqtj.com
+20 -17
View File
@@ -21,9 +21,12 @@
# TG WS Proxy # TG WS Proxy
**Локальный MTProto-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние сервера. **Локальный MTProto-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние серверы.
<img width="529" height="487" alt="image" src="https://github.com/user-attachments/assets/6a4cf683-0df8-43af-86c1-0e8f08682b62" /> <picture>
<source srcset="https://github.com/user-attachments/assets/17f1d15e-e1c2-41ea-a452-220d13359262" media="(prefers-color-scheme: dark)">
<img src="https://github.com/user-attachments/assets/8d595468-83a1-4e4f-bac4-9ce4a07027bd">
</picture>
## Как это работает ## Как это работает
@@ -34,13 +37,13 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
1. Приложение поднимает MTProto прокси на `127.0.0.1:1443` 1. Приложение поднимает MTProto прокси на `127.0.0.1:1443`
2. Перехватывает подключения к IP-адресам Telegram 2. Перехватывает подключения к IP-адресам Telegram
3. Извлекает DC ID из MTProto obfuscation init-пакета 3. Извлекает DC ID из MTProto obfuscation init-пакета
4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены Telegram 4. Устанавливает WebSocket-соединение (TLS) к соответствующему DC через домены Telegram
5. Если WS недоступен (302 redirect) — автоматически переключается на CfProxy / прямое TCP-соединение 5. Если WS недоступен (302 redirect) — автоматически переключается на CfProxy / прямое TCP-соединение
> [!IMPORTANT] > [!IMPORTANT]
> ### Не грузит фото/видео? > ### Не грузит фото/видео?
> **Удалите в настройках прокси в DC->IP всё, кроме `4:149.154.167.220`** > **Удалите в настройках прокси в DC->IP всё, кроме `4:149.154.167.220`**
> **Если не помогло, то удалите вообще всё из этого поля** > **Если не помогло, то полностью очистите это поле**
> #### > ####
> Подобная проблема встречается на аккаунтах без Premium > Подобная проблема встречается на аккаунтах без Premium
> Если вам не помогло, то настраивайте свой домен по гайду отсюда: https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md > Если вам не помогло, то настраивайте свой домен по гайду отсюда: https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md
@@ -49,20 +52,20 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
### Windows ### Windows
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_windows.exe`**. Он собирается автоматически через [Github Actions](https://github.com/Flowseal/tg-ws-proxy/actions) из открытого исходного кода. Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_windows.exe`**. Он собирается автоматически через [GitHub Actions](https://github.com/Flowseal/tg-ws-proxy/actions) из открытого исходного кода.
При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей. При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей.
**Меню трея:** **Меню трея:**
- **Открыть в Telegram** — автоматически настроить прокси через `tg://proxy` ссылку - **Открыть в Telegram** — автоматически настроить прокси через ссылку `tg://proxy`
- **Скопировать ссылку** — скопировать ссылку для подключения - **Скопировать ссылку** — скопировать ссылку для подключения
- **Перезапустить прокси** — перезапуск без выхода из приложения - **Перезапустить прокси** — перезапуск без выхода из приложения
- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub) - **Настройки...** — GUI-редактор конфигурации (версия приложения, опциональная проверка обновлений с GitHub)
- **Открыть логи** — открыть файл логов - **Открыть логи** — открыть файл логов
- **Выход** — остановить прокси и закрыть приложение - **Выход** — остановить прокси и закрыть приложение
При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (отключается в настройках). При первом запуске после старта может появиться запрос об открытии страницы релиза, если на GitHub вышла новая версия (эту проверку можно отключить в настройках).
### Настройка Telegram Desktop ### Настройка Telegram Desktop
@@ -71,7 +74,7 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
ПКМ по иконке в трее → **«Открыть в Telegram»** ПКМ по иконке в трее → **«Открыть в Telegram»**
Если не сработало (не открылся Telegram с подключением), то: Если не сработало (не открылся Telegram с подключением), то:
1. ПКМ по иконке в трее → **«Скопировать ссылку»** 1. ПКМ по иконке в трее → **«Скопировать ссылку»**
2. Отправьте ссылку себе в избранное в Telegram клиенте и нажмите по ней ЛКМ 2. Отправьте ссылку себе в избранное в Telegram-клиенте и нажмите по ней ЛКМ
3. Подключитесь 3. Подключитесь
### Вручную: ### Вручную:
@@ -96,7 +99,7 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
Для Debian/Ubuntu скачайте со [страницы релизов](https://github.com/Flowseal/tg-ws-proxy/releases) пакет **`TgWsProxy_linux_amd64.deb`**. Для Debian/Ubuntu скачайте со [страницы релизов](https://github.com/Flowseal/tg-ws-proxy/releases) пакет **`TgWsProxy_linux_amd64.deb`**.
Для Arch и Arch-Based дистрибутивов подготовлены пакеты в AUR: [tg-ws-proxy-bin](https://aur.archlinux.org/packages/tg-ws-proxy-bin), [tg-ws-proxy-git](https://aur.archlinux.org/packages/tg-ws-proxy-git), [tg-ws-proxy-cli](https://aur.archlinux.org/packages/tg-ws-proxy-cli) Для Arch и Arch-based дистрибутивов подготовлены пакеты в AUR: [tg-ws-proxy-bin](https://aur.archlinux.org/packages/tg-ws-proxy-bin), [tg-ws-proxy-git](https://aur.archlinux.org/packages/tg-ws-proxy-git), [tg-ws-proxy-cli](https://aur.archlinux.org/packages/tg-ws-proxy-cli)
```shell ```shell
# Установка без AUR-helper # Установка без AUR-helper
@@ -107,9 +110,9 @@ makepkg -si
# При помощи AUR-helper # При помощи AUR-helper
paru -S tg-ws-proxy-bin paru -S tg-ws-proxy-bin
# Если вы установили -cli пакет, то запуск осуществляется через systemctl, где 8888 это номер порта, # Если вы установили -cli пакет, то запуск осуществляется через systemctl, где 8888 - это номер порта,
# разделитель ":" и secret, который можно сгенерировать командой: openssl rand -hex 16 # разделитель ":" и secret, который можно сгенерировать командой: openssl rand -hex 16
sudo systemctl start tg-ws-proxy-cli@8888:3075abe65830f0325116bb0416cadf9f sudo systemctl start tg-ws-proxy@8888:3075abe65830f0325116bb0416cadf9f
``` ```
Для остальных дистрибутивов можно использовать **`TgWsProxy_linux_amd64`** (бинарный файл для x86_64). Для остальных дистрибутивов можно использовать **`TgWsProxy_linux_amd64`** (бинарный файл для x86_64).
@@ -165,16 +168,16 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
|---|---|---| |---|---|---|
| `--port` | `1443` | Порт прокси | | `--port` | `1443` | Порт прокси |
| `--host` | `127.0.0.1` | Хост прокси | | `--host` | `127.0.0.1` | Хост прокси |
| `--secret` | `random` | 32 hex chars secret для авторизации клиентов | | `--secret` | `random` | 32-значный hex-ключ для авторизации клиентов |
| `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (можно указать несколько раз) | | `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (можно указать несколько раз) |
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare]((https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md)) | | `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md) |
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudfalre. [Подробнее тут](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md) | | `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare. [Подробнее тут](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md) |
| `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением | | `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением |
| `--fake-tls-domain` | | Включить Fake TLS (ee-secret) маскировку с указанным SNI-доменом | | `--fake-tls-domain` | | Включить Fake TLS (ee-secret) маскировку с указанным SNI-доменом |
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) | | `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |
| `--buf-kb` | `256` | Размер буфера в КБ | | `--buf-kb` | `256` | Размер буфера в КБ |
| `--pool-size` | `4` | Количество заготовленных соединений на каждый DC | | `--pool-size` | `4` | Количество заготовленных соединений на каждый DC |
| `--log-file` | выкл. | Путь до файла, в который сохранять логи | | `--log-file` | выкл. | Путь к файлу, в который будут сохраняться логи |
| `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после идёт перезапись) | | `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после идёт перезапись) |
| `--log-backups` | `0` | Количество сохранений логов после перезаписи | | `--log-backups` | `0` | Количество сохранений логов после перезаписи |
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) | | `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
@@ -241,7 +244,7 @@ python3 proxy/tg_ws_proxy.py \
--secret <32-hex-chars> --secret <32-hex-chars>
``` ```
Ссылка для подключения будет в формате `ee`-секрета:</p> Ссылка для подключения будет в формате `ee`-секрета:
``` ```
tg://proxy?server=your.domain.com&port=443&secret=ee<secret><domain_hex> tg://proxy?server=your.domain.com&port=443&secret=ee<secret><domain_hex>
+4 -4
View File
@@ -4,8 +4,8 @@
# http://msdn.microsoft.com/en-us/library/ms646997.aspx # http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo( VSVersionInfo(
ffi=FixedFileInfo( ffi=FixedFileInfo(
filevers=(1, 6, 2, 0), filevers=(1, 6, 5, 0),
prodvers=(1, 6, 2, 0), prodvers=(1, 6, 5, 0),
mask=0x3f, mask=0x3f,
flags=0x0, flags=0x0,
OS=0x40004, OS=0x40004,
@@ -21,12 +21,12 @@ VSVersionInfo(
[ [
StringStruct(u'CompanyName', u'Flowseal'), StringStruct(u'CompanyName', u'Flowseal'),
StringStruct(u'FileDescription', u'Telegram Desktop WebSocket Bridge Proxy'), StringStruct(u'FileDescription', u'Telegram Desktop WebSocket Bridge Proxy'),
StringStruct(u'FileVersion', u'1.6.2.0'), StringStruct(u'FileVersion', u'1.6.5.0'),
StringStruct(u'InternalName', u'TgWsProxy'), StringStruct(u'InternalName', u'TgWsProxy'),
StringStruct(u'LegalCopyright', u'Copyright (c) Flowseal. MIT License.'), StringStruct(u'LegalCopyright', u'Copyright (c) Flowseal. MIT License.'),
StringStruct(u'OriginalFilename', u'TgWsProxy.exe'), StringStruct(u'OriginalFilename', u'TgWsProxy.exe'),
StringStruct(u'ProductName', u'TG WS Proxy'), StringStruct(u'ProductName', u'TG WS Proxy'),
StringStruct(u'ProductVersion', u'1.6.2.0'), StringStruct(u'ProductVersion', u'1.6.5.0'),
] ]
) )
] ]
+1 -1
View File
@@ -1,6 +1,6 @@
from .config import parse_dc_ip_list, proxy_config from .config import parse_dc_ip_list, proxy_config
from .utils import get_link_host from .utils import get_link_host
__version__ = "1.6.4" __version__ = "1.6.5"
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list"] __all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list"]
-3
View File
@@ -96,9 +96,6 @@ class RawWebSocket:
f'Sec-WebSocket-Key: {ws_key}\r\n' f'Sec-WebSocket-Key: {ws_key}\r\n'
f'Sec-WebSocket-Version: 13\r\n' f'Sec-WebSocket-Version: 13\r\n'
f'Sec-WebSocket-Protocol: binary\r\n' f'Sec-WebSocket-Protocol: binary\r\n'
f'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
f'AppleWebKit/537.36 (KHTML, like Gecko) '
f'Chrome/131.0.0.0 Safari/537.36\r\n'
f'\r\n' f'\r\n'
) )
writer.write(req.encode()) writer.write(req.encode())
+11 -1
View File
@@ -651,6 +651,16 @@ def run_proxy(stop_event: Optional[asyncio.Event] = None):
def main(): def main():
def _parse_bool(value: str) -> bool:
lowered = value.strip().lower()
if lowered == 'true':
return True
if lowered == 'false':
return False
raise argparse.ArgumentTypeError(
"Expected boolean value: true or false",
)
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(
description='Telegram MTProto WebSocket Bridge Proxy') description='Telegram MTProto WebSocket Bridge Proxy')
ap.add_argument('--port', type=int, default=1443, ap.add_argument('--port', type=int, default=1443,
@@ -679,7 +689,7 @@ def main():
help='User defined Cloudflare-proxied domain for WS fallback') help='User defined Cloudflare-proxied domain for WS fallback')
ap.add_argument('--no-cfproxy', action='store_true', ap.add_argument('--no-cfproxy', action='store_true',
help='Disable Cloudflare proxy fallback') help='Disable Cloudflare proxy fallback')
ap.add_argument('--cfproxy-priority', type=bool, default=True, ap.add_argument('--cfproxy-priority', type=_parse_bool, default=True,
help='Try cfproxy before tcp fallback (default: true)') help='Try cfproxy before tcp fallback (default: true)')
ap.add_argument('--fake-tls-domain', type=str, default='', ap.add_argument('--fake-tls-domain', type=str, default='',
metavar='DOMAIN', metavar='DOMAIN',
+52
View File
@@ -30,6 +30,7 @@ _state: Dict[str, Any] = {
"latest": None, "latest": None,
"html_url": None, "html_url": None,
"error": None, "error": None,
"assets": [],
} }
@@ -162,6 +163,7 @@ def run_check(current_version: str) -> None:
tag = (cache.get("tag_name") or "").strip() tag = (cache.get("tag_name") or "").strip()
if tag: if tag:
_apply_release_tag(tag, cache.get("html_url") or "", current_version) _apply_release_tag(tag, cache.get("html_url") or "", current_version)
_state["assets"] = cache.get("assets") or []
return return
err = cache.get("last_error") err = cache.get("last_error")
_state["error"] = ( _state["error"] = (
@@ -181,6 +183,7 @@ def run_check(current_version: str) -> None:
tag = (cache.get("tag_name") or "").strip() tag = (cache.get("tag_name") or "").strip()
url = (cache.get("html_url") or "").strip() or RELEASES_PAGE_URL url = (cache.get("html_url") or "").strip() or RELEASES_PAGE_URL
_apply_release_tag(tag, url, current_version) _apply_release_tag(tag, url, current_version)
_state["assets"] = cache.get("assets") or []
if new_etag: if new_etag:
cache["etag"] = new_etag cache["etag"] = new_etag
_save_cache(cache_path, cache) _save_cache(cache_path, cache)
@@ -200,6 +203,13 @@ def run_check(current_version: str) -> None:
cache["etag"] = new_etag cache["etag"] = new_etag
cache["tag_name"] = tag cache["tag_name"] = tag
cache["html_url"] = html_url cache["html_url"] = html_url
assets = [
{"name": a.get("name", ""), "url": a.get("browser_download_url", ""), "digest": a.get("digest", "")}
for a in (data.get("assets") or [])
if a.get("name") and a.get("browser_download_url")
]
_state["assets"] = assets
cache["assets"] = assets
cache.pop("last_error", None) cache.pop("last_error", None)
_save_cache(cache_path, cache) _save_cache(cache_path, cache)
except (HTTPError, URLError, OSError, TimeoutError, ValueError, json.JSONDecodeError) as e: except (HTTPError, URLError, OSError, TimeoutError, ValueError, json.JSONDecodeError) as e:
@@ -221,3 +231,45 @@ def run_check(current_version: str) -> None:
def get_status() -> Dict[str, Any]: def get_status() -> Dict[str, Any]:
"""Снимок состояния после run_check (для подписей в настройках).""" """Снимок состояния после run_check (для подписей в настройках)."""
return dict(_state) return dict(_state)
def get_update_asset(exe_path: Path) -> Optional[Tuple[str, str]]:
assets = _state.get("assets") or []
if not assets:
return None
# Try SHA256 match against release asset digests
try:
import hashlib
h = hashlib.sha256()
with open(exe_path, "rb") as f:
while True:
chunk = f.read(65536)
if not chunk:
break
h.update(chunk)
exe_sha = h.hexdigest().lower()
for a in assets:
d = (a.get("digest") or "").lower()
if d.startswith("sha256:") and d[7:] == exe_sha:
return a["url"], a["name"]
except Exception:
pass
# Fallback
import struct
is_64 = struct.calcsize("P") * 8 == 64
try:
is_modern = sys.getwindowsversion().major >= 10
except Exception:
is_modern = True
if is_modern:
name = "TgWsProxy_windows.exe"
elif is_64:
name = "TgWsProxy_windows_7_64bit.exe"
else:
name = "TgWsProxy_windows_7_32bit.exe"
for a in assets:
if a.get("name") == name:
return a["url"], a["name"]
return None
+238 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import ctypes import ctypes
import os import os
import subprocess
import sys import sys
import threading import threading
import time import time
@@ -40,7 +41,7 @@ from utils.tray_common import (
APP_NAME, DEFAULT_CONFIG, FIRST_RUN_MARKER, IS_FROZEN, LOG_FILE, APP_NAME, DEFAULT_CONFIG, FIRST_RUN_MARKER, IS_FROZEN, LOG_FILE,
acquire_lock, bootstrap, check_ipv6_warning, ctk_run_dialog, acquire_lock, bootstrap, check_ipv6_warning, ctk_run_dialog,
ensure_ctk_thread, ensure_dirs, load_config, load_icon, log, ensure_ctk_thread, ensure_dirs, load_config, load_icon, log,
maybe_notify_update, quit_ctk, release_lock, restart_proxy, quit_ctk, release_lock, restart_proxy,
save_config, start_proxy, stop_proxy, tg_proxy_url, save_config, start_proxy, stop_proxy, tg_proxy_url,
) )
from ui.ctk_tray_ui import ( from ui.ctk_tray_ui import (
@@ -101,7 +102,9 @@ _u32.MessageBoxW.restype = ctypes.c_int
_MB_OK_ERR = 0x10 _MB_OK_ERR = 0x10
_MB_OK_INFO = 0x40 _MB_OK_INFO = 0x40
_MB_YESNO_Q = 0x24 _MB_YESNO_Q = 0x24
_MB_YESNOCANCEL_Q = 0x23
_IDYES = 6 _IDYES = 6
_IDNO = 7
def _show_error(text: str, title: str = "TG WS Proxy — Ошибка") -> None: def _show_error(text: str, title: str = "TG WS Proxy — Ошибка") -> None:
@@ -116,6 +119,227 @@ def _ask_yes_no(text: str, title: str = "TG WS Proxy") -> bool:
return _u32.MessageBoxW(None, text, title, _MB_YESNO_Q) == _IDYES return _u32.MessageBoxW(None, text, title, _MB_YESNO_Q) == _IDYES
def update_ctk_form(
text: str, title: str = "TG WS Proxy", download_url: Optional[str] = None,
release_url: Optional[str] = None,
) -> str:
if ctk is None or not ensure_ctk_thread(ctk, _config.get("appearance", "auto")):
result = _u32.MessageBoxW(None, text, title, _MB_YESNOCANCEL_Q)
if result == _IDYES:
return "update"
if result == _IDNO:
return "open"
return "close"
result = {"value": "close"}
def _build(done: threading.Event) -> None:
theme = ctk_theme_for_platform()
root = create_ctk_toplevel(
ctk,
title=title,
width=310 if IS_FROZEN else 210,
height=130 if IS_FROZEN else 100,
theme=theme,
after_create=lambda r: r.iconbitmap(ICON_PATH),
)
frame = main_content_frame(ctk, root, theme, padx=16, pady=14)
ctk.CTkLabel(
frame,
text=text,
justify="left",
anchor="w",
wraplength=270,
font=(theme.ui_font_family, 12),
text_color=theme.text_primary,
).pack(fill="x", pady=(0, 10))
row = ctk.CTkFrame(frame, fg_color="transparent")
row.pack(fill="x")
status_label = ctk.CTkLabel(
frame, text="", justify="left", anchor="w", wraplength=270,
font=(theme.ui_font_family, 11), text_color=theme.text_secondary,
)
status_label.pack(fill="x", pady=(6, 0))
btns: list = []
def _set_status(msg: str) -> None:
root.after(0, lambda: status_label.configure(text=msg))
def _close_with(value: str) -> None:
result["value"] = value
root.destroy()
done.set()
def _on_update() -> None:
if not download_url:
if release_url:
webbrowser.open(release_url)
_close_with("open")
return
for b in btns:
b.configure(state="disabled")
root.protocol("WM_DELETE_WINDOW", lambda: None)
def _run():
_perform_update(download_url, set_status=_set_status)
root.after(0, lambda: [b.configure(state="normal") for b in btns])
root.after(0, lambda: root.protocol("WM_DELETE_WINDOW", lambda: _close_with("close")))
threading.Thread(target=_run, daemon=True).start()
if IS_FROZEN:
btn_upd = ctk.CTkButton(
row, text="Обновить", width=88, height=34,
font=(theme.ui_font_family, 13), command=_on_update,
)
btn_upd.pack(side="left", padx=(0, 6))
btns.append(btn_upd)
btn_pg = ctk.CTkButton(
row, text="Страница", width=88, height=34,
font=(theme.ui_font_family, 13), command=lambda: _close_with("open"),
)
btn_pg.pack(side="left", padx=(0, 6))
btns.append(btn_pg)
btn_cl = ctk.CTkButton(
row, text="Закрыть", width=88, height=34,
font=(theme.ui_font_family, 13),
fg_color=theme.field_bg, hover_color=theme.field_border,
text_color=theme.text_primary, border_width=1, border_color=theme.field_border,
command=lambda: _close_with("close"),
)
btn_cl.pack(side="left")
btns.append(btn_cl)
root.protocol("WM_DELETE_WINDOW", lambda: _close_with("close"))
ctk_run_dialog(_build)
return result["value"]
def _perform_update(download_url: str, set_status=None) -> None:
import tempfile
import urllib.request
def _step(msg: str) -> None:
log.info("Update: %s", msg)
if set_status:
set_status(msg)
time.sleep(0.8)
def _err(msg: str) -> None:
log.error("Update error: %s", msg)
if set_status:
set_status(f"Ошибка: {msg}")
else:
_show_error(msg)
_step("Скачивание...")
cur_exe = Path(sys.executable)
old_exe = cur_exe.with_name(cur_exe.stem + "_oldtgws.exe")
tmp_path = None
try:
fd, tmp_name = tempfile.mkstemp(dir=cur_exe.parent, suffix=".tmp")
os.close(fd)
tmp_path = Path(tmp_name)
log.info("Downloading update from %s", download_url)
urllib.request.urlretrieve(download_url, str(tmp_path))
except Exception as exc:
_err(f"Не удалось скачать:\n{exc}")
if tmp_path:
try:
tmp_path.unlink(missing_ok=True)
except OSError:
pass
return
_step("Замена файла...")
try:
if old_exe.exists():
old_exe.unlink()
cur_exe.rename(old_exe)
except Exception as exc:
_err(f"Не удалось переименовать файл:\n{exc}")
try:
tmp_path.unlink(missing_ok=True)
except OSError:
pass
return
try:
tmp_path.rename(cur_exe)
except Exception as exc:
_err(f"Не удалось переместить файл:\n{exc}")
try:
old_exe.rename(cur_exe)
except OSError:
pass
try:
tmp_path.unlink(missing_ok=True)
except OSError:
pass
return
_step("Перезапуск...")
_release_win_mutex()
stop_proxy()
# Don't reuse existing _MEI* dir
env = os.environ.copy()
for _k in [k for k in env if k.startswith("_PYI_") or k == "_MEIPASS"]:
del env[_k]
if hasattr(sys, "_MEIPASS"):
_mei = os.path.normcase(sys._MEIPASS.rstrip("\\/"))
env["PATH"] = os.pathsep.join(
p for p in env.get("PATH", "").split(os.pathsep)
if os.path.normcase(p.rstrip("\\/")) != _mei
)
try:
subprocess.Popen(
[str(cur_exe)],
env=env,
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
)
except Exception as exc:
log.error("Failed to launch updated exe: %s", exc)
time.sleep(0.5)
os._exit(0)
def _maybe_do_update(cfg: dict, is_exiting) -> None:
if not cfg.get("check_updates", True):
return
def _work():
time.sleep(1.5)
if is_exiting():
return
try:
from proxy import __version__
from utils.update_check import RELEASES_PAGE_URL, get_status, get_update_asset, run_check
run_check(__version__)
st = get_status()
if not st.get("has_update") or is_exiting():
return
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
ver = st.get("latest") or "?"
asset = get_update_asset(Path(sys.executable)) if IS_FROZEN else None
choice = update_ctk_form(
f"Доступна новая версия: {ver}",
download_url=asset[0] if asset else None,
release_url=url,
)
if choice == "open":
webbrowser.open(url)
except Exception as exc:
log.warning("Update check failed: %s", repr(exc))
threading.Thread(target=_work, daemon=True, name="update-check").start()
# autostart (registry) # autostart (registry)
_RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run" _RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
@@ -370,7 +594,7 @@ def run_tray() -> None:
return return
start_proxy(_config, _show_error) start_proxy(_config, _show_error)
maybe_notify_update(_config, lambda: _exiting, _ask_yes_no) _maybe_do_update(_config, lambda: _exiting)
_show_first_run() _show_first_run()
check_ipv6_warning(_show_info) check_ipv6_warning(_show_info)
@@ -387,6 +611,18 @@ def main() -> None:
_show_info("Приложение уже запущено.", os.path.basename(sys.argv[0])) _show_info("Приложение уже запущено.", os.path.basename(sys.argv[0]))
return return
if IS_FROZEN:
def _cleanup_old_exes():
exe_dir = Path(sys.executable).parent
time.sleep(3)
for _f in exe_dir.glob("*_oldtgws.exe"):
try:
_f.unlink()
log.info("Deleted leftover: %s", _f)
except OSError:
pass
threading.Thread(target=_cleanup_old_exes, daemon=True, name="cleanup-old").start()
try: try:
run_tray() run_tray()
finally: finally: