18 Commits

Author SHA1 Message Date
Flowseal
d84b9eadc4 version fix 2026-04-16 18:20:47 +03:00
Flowseal
c1b4cb0204 docs update 2026-04-16 18:01:48 +03:00
Flowseal
5d08e16e5d removed repeated annotation 2026-04-16 17:56:48 +03:00
Flowseal
a844a88f38 docs update 2026-04-16 17:52:58 +03:00
Flowseal
e5f1d02737 docs links update 2026-04-16 17:51:41 +03:00
Flowseal
3a6e82c2a8 docs update 2026-04-16 17:50:32 +03:00
Flowseal
e56ada1a34 CF domains balancer 2026-04-16 17:08:03 +03:00
Flowseal
b44d79a933 docs update 2026-04-16 17:08:03 +03:00
Aksarin Mikhail
77723d875f Update README.md (#711)
Fix relative links
2026-04-16 00:29:58 +03:00
Flowseal
548ec05fc5 docs update 2026-04-14 21:56:14 +03:00
Flowseal
03c7719c39 mutex check simplify 2026-04-14 16:58:54 +03:00
Flowseal
db4cebe0b2 build test 2026-04-14 16:51:26 +03:00
Flowseal
ca81d037f7 docs update 2026-04-14 03:11:13 +03:00
Flowseal
07615af49c bootloader build fix 2026-04-14 02:44:15 +03:00
Flowseal
f8ee37370d Version bump 2026-04-14 00:27:27 +03:00
Flowseal
4cbb9e555c windows mutex-lock 2026-04-14 00:27:27 +03:00
Flowseal
25ae4b0a24 build version changes 2026-04-14 00:27:27 +03:00
Kleshzz
8af1bc8c89 Add .gitattributes & Update .gitignore (#690) 2026-04-13 19:30:57 +03:00
17 changed files with 149 additions and 54 deletions

9
.gitattributes vendored Normal file
View File

@@ -0,0 +1,9 @@
* text=auto eol=lf
*.py text diff=python
*.spec text linguist-language=Python
*.toml text
*.txt text
*.ico binary

View File

@@ -3,3 +3,5 @@ vmmzovy.com
mkuosckvso.com mkuosckvso.com
zaewayzmplad.com zaewayzmplad.com
twdmbzcm.com twdmbzcm.com
awzwsldi.com
clngqrflngqin.com

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.12" python-version: "3.11"
cache: "pip" cache: "pip"
- name: Setup MSVC 14.40 toolset - name: Setup MSVC 14.40 toolset
@@ -38,10 +38,11 @@ jobs:
run: pip install . run: pip install .
- name: Build PyInstaller bootloader from source - name: Build PyInstaller bootloader from source
run: |
pip install "pyinstaller==6.16.0" --no-binary pyinstaller
env: env:
PYINSTALLER_COMPILE_BOOTLOADER: 1 PYINSTALLER_COMPILE_BOOTLOADER: "1"
run: |
pip download --no-binary pyinstaller --no-deps --no-cache-dir -d pyinstaller_src "pyinstaller==6.10.0"
pip install (Get-ChildItem pyinstaller_src\*.tar.gz).FullName
- name: Build EXE with PyInstaller - name: Build EXE with PyInstaller
run: pyinstaller packaging/windows.spec --noconfirm run: pyinstaller packaging/windows.spec --noconfirm
@@ -193,7 +194,7 @@ jobs:
python3.12 -m pip install --no-deps wheelhouse/universal2/*.whl python3.12 -m pip install --no-deps wheelhouse/universal2/*.whl
python3.12 -m pip install . python3.12 -m pip install .
python3.12 -m pip install pyinstaller==6.16.0 python3.12 -m pip install pyinstaller==6.13.0
- name: Create macOS icon from ICO - name: Create macOS icon from ICO
run: | run: |
@@ -295,7 +296,7 @@ jobs:
run: | run: |
.venv/bin/pip install --upgrade pip .venv/bin/pip install --upgrade pip
.venv/bin/pip install . .venv/bin/pip install .
.venv/bin/pip install "pyinstaller==6.16.0" .venv/bin/pip install "pyinstaller==6.13.0"
- name: Build binary with PyInstaller - name: Build binary with PyInstaller
run: .venv/bin/pyinstaller packaging/linux.spec --noconfirm run: .venv/bin/pyinstaller packaging/linux.spec --noconfirm
@@ -383,7 +384,8 @@ jobs:
tag_name: ${{ github.event.inputs.version }} tag_name: ${{ github.event.inputs.version }}
name: "TG WS Proxy ${{ github.event.inputs.version }}" name: "TG WS Proxy ${{ github.event.inputs.version }}"
body: | body: |
## TG WS Proxy ${{ github.event.inputs.version }} ##
### [❤️ Поддержать развитие проекта](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/Funding.md)
files: | files: |
dist/TgWsProxy_windows.exe dist/TgWsProxy_windows.exe
dist/TgWsProxy_windows_7_64bit.exe dist/TgWsProxy_windows_7_64bit.exe

2
.gitignore vendored
View File

@@ -6,6 +6,8 @@ __pycache__/
dist/ dist/
build/ build/
*.spec.bak *.spec.bak
venv/
.venv/
# PyInstaller # PyInstaller
*.manifest *.manifest

14
docs/Funding.md Normal file
View File

@@ -0,0 +1,14 @@
> [!TIP]
>
> ### 🎉 Поддержать меня
>
> **USDT (TRC20)**: `TXPnKs2Ww1RD8JN6nChFUVmi5r2hqrWjuu`
> **BTC**: `bc1qr8vd6jelkyyry3m4mq6z5txdx4pl856fu6ss0w`
> **ETH**: `0x1417878fdc5047E670a77748B34819b9A49C72F1`
> **Другие монеты**: https://nowpayments.io/donation/flowseal
##
### Проект полностью бесплатен для использования всеми.
### Однако его развитие и стабильная работа при росте числа пользователей требуют от меня определённых вложений.
### Буду благодарен за любую форму поддержки! Спасибо ❤️

View File

@@ -1,6 +1,6 @@
> [!TIP] > [!TIP]
> >
> ### 🎉 Поддержать меня > ### [🎉 Поддержать меня](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/Funding.md)
> >
> **USDT (TRC20)**: `TXPnKs2Ww1RD8JN6nChFUVmi5r2hqrWjuu` > **USDT (TRC20)**: `TXPnKs2Ww1RD8JN6nChFUVmi5r2hqrWjuu`
> **BTC**: `bc1qr8vd6jelkyyry3m4mq6z5txdx4pl856fu6ss0w` > **BTC**: `bc1qr8vd6jelkyyry3m4mq6z5txdx4pl856fu6ss0w`
@@ -11,13 +11,13 @@
> >
> ### Реакция антивирусов > ### Реакция антивирусов
> >
> Windows Defender часто ошибочно помечает приложение как **Wacatac**. > Антивирусы часто ошибочно помечают приложение как вирус из-за упаковщика.
> Если вы не можете скачать из-за блокировки, то: > Если вы не можете скачать из-за блокировки антивирусом, то:
> >
> 1) Попробуйте скачать версию win7 (она ничем не отличается в плане функционала) > 1) **Попробуйте скачать версию win7 (она ничем не отличается в плане функционала)**
> 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно > 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно
> >
> **Всегда проверяйте, что скачиваете из интернета, тем более из непроверенных источников. Всегда лучше смотреть на детекты широко известных антивирусов на VirusTotal** > Всегда проверяйте, что скачиваете из интернета, тем более из непроверенных источников. Всегда лучше смотреть на детекты широко известных антивирусов на VirusTotal
# TG WS Proxy # TG WS Proxy
@@ -280,7 +280,7 @@ Tray-приложение хранит данные в:
## Автоматическая сборка ## Автоматическая сборка
Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec), [`packaging/linux.spec`](packaging/linux.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки. Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](../packaging/windows.spec), [`packaging/macos.spec`](../packaging/macos.spec), [`packaging/linux.spec`](../packaging/linux.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](../.github/workflows/build.yml)) для автоматической сборки.
Минимально поддерживаемые версии ОС для текущих бинарных сборок: Минимально поддерживаемые версии ОС для текущих бинарных сборок:
@@ -293,4 +293,4 @@ Tray-приложение хранит данные в:
## Лицензия ## Лицензия
[MIT License](LICENSE) [MIT License](https://github.com/Flowseal/tg-ws-proxy/blob/main/LICENSE)

View File

@@ -273,7 +273,7 @@ def run_tray() -> None:
def main() -> None: def main() -> None:
if not acquire_lock("linux.py"): if not acquire_lock():
_show_info("Приложение уже запущено.", os.path.basename(sys.argv[0])) _show_info("Приложение уже запущено.", os.path.basename(sys.argv[0]))
return return
try: try:

View File

@@ -610,7 +610,7 @@ def run_menubar() -> None:
def main() -> None: def main() -> None:
if not acquire_lock("macos.py"): if not acquire_lock():
_show_info("Приложение уже запущено.") _show_info("Приложение уже запущено.")
return return
try: try:

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, 0, 0, 0), filevers=(1, 6, 2, 0),
prodvers=(1, 0, 0, 0), prodvers=(1, 6, 2, 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.0.0.0'), StringStruct(u'FileVersion', u'1.6.2.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.0.0.0'), StringStruct(u'ProductVersion', u'1.6.2.0'),
] ]
) )
] ]

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.1" __version__ = "1.6.3"
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list"] __all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list"]

42
proxy/balancer.py Normal file
View File

@@ -0,0 +1,42 @@
import random
from collections import Counter
from typing import Dict, List, Iterator
class _Balancer:
def __init__(self):
self.domains: List[str] = []
self._dc_to_domain: Dict[int, str] = {}
def update_domains_list(self, domains_list: List[str]) -> None:
if Counter(self.domains) == Counter(domains_list):
return
self.domains = domains_list[:]
self._dc_to_domain = {
dc_id: random.choice(self.domains)
for dc_id in (1, 2, 3, 4, 5, 203)
}
def update_domain_for_dc(self, dc_id: int, domain: str) -> bool:
if self._dc_to_domain.get(dc_id) == domain:
return False
self._dc_to_domain[dc_id] = domain
return True
def get_domains_for_dc(self, dc_id: int) -> Iterator[str]:
current_domain = self._dc_to_domain.get(dc_id)
yield current_domain
shuffled_domains = self.domains[:]
random.shuffle(shuffled_domains)
for domain in shuffled_domains:
if domain != current_domain:
yield domain
balancer = _Balancer()

View File

@@ -7,6 +7,7 @@ from typing import Dict, List, Optional
from .utils import * from .utils import *
from .stats import stats from .stats import stats
from .balancer import balancer
from .config import proxy_config from .config import proxy_config
from .raw_websocket import RawWebSocket from .raw_websocket import RawWebSocket
@@ -160,17 +161,13 @@ async def _cfproxy_fallback(reader, writer, relay_init, label,
dc=None, is_media=False, dc=None, is_media=False,
ctx: CryptoCtx = None, splitter=None): ctx: CryptoCtx = None, splitter=None):
media_tag = ' media' if is_media else '' media_tag = ' media' if is_media else ''
active = proxy_config.active_cfproxy_domain
others = [d for d in proxy_config.cfproxy_domains if d != active]
ws = None ws = None
chosen_domain = None chosen_domain = None
log.info("[%s] DC%d%s -> trying CF proxy", log.info("[%s] DC%d%s -> trying CF proxy",
label, dc, media_tag) label, dc, media_tag)
for base_domain in ([active] + others): for base_domain in balancer.get_domains_for_dc(dc):
domain = f'kws{dc}.{base_domain}' domain = f'kws{dc}.{base_domain}'
try: try:
ws = await RawWebSocket.connect(domain, domain, timeout=10.0) ws = await RawWebSocket.connect(domain, domain, timeout=10.0)
@@ -183,9 +180,8 @@ async def _cfproxy_fallback(reader, writer, relay_init, label,
if ws is None: if ws is None:
return False return False
if chosen_domain and chosen_domain != proxy_config.active_cfproxy_domain: if chosen_domain and balancer.update_domain_for_dc(dc, chosen_domain):
log.info("[%s] Switching active CF domain", label) log.info("[%s] Switched active CF domain", label)
proxy_config.active_cfproxy_domain = chosen_domain
stats.connections_cfproxy += 1 stats.connections_cfproxy += 1
await ws.send(relay_init) await ws.send(relay_init)

View File

@@ -9,6 +9,8 @@ from dataclasses import dataclass, field
from typing import Dict, List from typing import Dict, List
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from .balancer import balancer
log = logging.getLogger('tg-mtproto-proxy') log = logging.getLogger('tg-mtproto-proxy')
CFPROXY_DOMAINS_URL = ( CFPROXY_DOMAINS_URL = (
@@ -45,8 +47,6 @@ class ProxyConfig:
fallback_cfproxy: bool = True fallback_cfproxy: bool = True
fallback_cfproxy_priority: bool = True fallback_cfproxy_priority: bool = True
cfproxy_user_domain: str = '' cfproxy_user_domain: str = ''
cfproxy_domains: List[str] = field(default_factory=lambda: list(CFPROXY_DEFAULT_DOMAINS))
active_cfproxy_domain: str = field(default_factory=lambda: random.choice(CFPROXY_DEFAULT_DOMAINS))
fake_tls_domain: str = '' fake_tls_domain: str = ''
proxy_protocol: bool = False proxy_protocol: bool = False
@@ -79,12 +79,8 @@ def refresh_cfproxy_domains() -> None:
if fetched: if fetched:
seen = set() seen = set()
pool = [d for d in fetched if not (d in seen or seen.add(d))] pool = [d for d in fetched if not (d in seen or seen.add(d))]
balancer.update_domains_list(pool)
log.info("CF proxy domain pool updated from GitHub (%d domains)", len(pool)) log.info("CF proxy domain pool updated from GitHub (%d domains)", len(pool))
else:
pool = list(proxy_config.cfproxy_domains) or list(CFPROXY_DEFAULT_DOMAINS)
proxy_config.cfproxy_domains = pool
proxy_config.active_cfproxy_domain = random.choice(pool)
_refresh_stop: threading.Event = threading.Event() _refresh_stop: threading.Event = threading.Event()
@@ -96,6 +92,8 @@ def start_cfproxy_domain_refresh() -> None:
_refresh_stop = threading.Event() _refresh_stop = threading.Event()
stop = _refresh_stop stop = _refresh_stop
balancer.update_domains_list(CFPROXY_DEFAULT_DOMAINS)
def _loop(): def _loop():
refresh_cfproxy_domains() refresh_cfproxy_domains()
while not stop.wait(timeout=3600): while not stop.wait(timeout=3600):

View File

@@ -4,7 +4,6 @@ import os
import sys import sys
import time import time
import struct import struct
import random
import asyncio import asyncio
import hashlib import hashlib
import argparse import argparse
@@ -25,10 +24,11 @@ if __name__ == '__main__' and (__package__ is None or __package__ == ''):
from .utils import * from .utils import *
from .stats import stats from .stats import stats
from .config import proxy_config, parse_dc_ip_list, start_cfproxy_domain_refresh, CFPROXY_DEFAULT_DOMAINS from .config import proxy_config, parse_dc_ip_list, start_cfproxy_domain_refresh
from .bridge import MsgSplitter, CryptoCtx, do_fallback, bridge_ws_reencrypt from .bridge import MsgSplitter, CryptoCtx, do_fallback, bridge_ws_reencrypt
from .raw_websocket import RawWebSocket, WsHandshakeError, set_sock_opts from .raw_websocket import RawWebSocket, WsHandshakeError, set_sock_opts
from .fake_tls import proxy_to_masking_domain, verify_client_hello, build_server_hello, FakeTlsStream, TLS_RECORD_HANDSHAKE from .fake_tls import proxy_to_masking_domain, verify_client_hello, build_server_hello, FakeTlsStream, TLS_RECORD_HANDSHAKE
from .balancer import balancer
log = logging.getLogger('tg-mtproto-proxy') log = logging.getLogger('tg-mtproto-proxy')
@@ -535,11 +535,8 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
if proxy_config.fallback_cfproxy: if proxy_config.fallback_cfproxy:
user = proxy_config.cfproxy_user_domain user = proxy_config.cfproxy_user_domain
if user: if user:
proxy_config.cfproxy_domains = [user] balancer.update_domains_list([user])
proxy_config.active_cfproxy_domain = user
else: else:
proxy_config.cfproxy_domains = list(CFPROXY_DEFAULT_DOMAINS)
proxy_config.active_cfproxy_domain = random.choice(CFPROXY_DEFAULT_DOMAINS)
start_cfproxy_domain_refresh() start_cfproxy_domain_refresh()
secret_bytes = bytes.fromhex(proxy_config.secret) secret_bytes = bytes.fromhex(proxy_config.secret)
@@ -585,12 +582,11 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
user_domain = "user" if proxy_config.cfproxy_user_domain else "auto" user_domain = "user" if proxy_config.cfproxy_user_domain else "auto"
log.info(" CF proxy: enabled (%s | %s)", prio, user_domain) log.info(" CF proxy: enabled (%s | %s)", prio, user_domain)
log.info("=" * 60) log.info("=" * 60)
log.info(" Connect links:") log.info(" Connect:")
if ftls: if ftls:
log.info(" ee (Fake TLS): %s", ee_link) log.info(" %s", ee_link)
else: else:
log.info(" (standard): %s", proxy_config.secret) log.info(" %s", dd_link)
log.info(" dd (random padding): %s", dd_link)
log.info("=" * 60) log.info("=" * 60)
async def log_stats(): async def log_stats():
@@ -716,7 +712,6 @@ def main():
proxy_config.pool_size = max(0, args.pool_size) proxy_config.pool_size = max(0, args.pool_size)
proxy_config.fallback_cfproxy = not args.no_cfproxy proxy_config.fallback_cfproxy = not args.no_cfproxy
proxy_config.fallback_cfproxy_priority = args.cfproxy_priority proxy_config.fallback_cfproxy_priority = args.cfproxy_priority
proxy_config.cfproxy_user_domain = args.cfproxy_domain
proxy_config.fake_tls_domain = args.fake_tls_domain.strip() proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
proxy_config.proxy_protocol = args.proxy_protocol proxy_config.proxy_protocol = args.proxy_protocol

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Tuple, Union from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from proxy import __version__, get_link_host, parse_dc_ip_list from proxy import __version__, get_link_host, parse_dc_ip_list
from proxy.config import proxy_config from proxy.balancer import balancer
from utils.update_check import RELEASES_PAGE_URL, get_status from utils.update_check import RELEASES_PAGE_URL, get_status
@@ -358,7 +358,7 @@ def install_tray_config_form(
text_color="#ffffff", border_width=0, text_color="#ffffff", border_width=0,
command=lambda: ( command=lambda: (
header.winfo_toplevel().iconify(), header.winfo_toplevel().iconify(),
webbrowser.open("https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/README.md"), webbrowser.open("https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/Funding.md"),
), ),
).pack(side="right", padx=(0, 6)) ).pack(side="right", padx=(0, 6))
@@ -451,7 +451,7 @@ def install_tray_config_form(
_threading.Thread(target=_worker, daemon=True).start() _threading.Thread(target=_worker, daemon=True).start()
else: else:
def _worker_auto(): def _worker_auto():
ok_domain, res = _run_cfproxy_auto_test(proxy_config.cfproxy_domains) ok_domain, res = _run_cfproxy_auto_test(balancer.domains)
if btn: if btn:
btn.after(0, lambda: btn.configure(text="Тест", state="normal")) btn.after(0, lambda: btn.configure(text="Тест", state="normal"))
btn.after(0, lambda: _cfproxy_show_auto_test_results(ok_domain, res)) btn.after(0, lambda: _cfproxy_show_auto_test_results(ok_domain, res))

View File

@@ -51,7 +51,7 @@ def ensure_dirs() -> None:
_lock_file_path: Optional[Path] = None _lock_file_path: Optional[Path] = None
def _same_process(meta: dict, proc: psutil.Process, script_hint: str) -> bool: def _same_process(meta: dict, proc: psutil.Process) -> bool:
try: try:
lock_ct = float(meta.get("create_time", 0.0)) lock_ct = float(meta.get("create_time", 0.0))
if lock_ct > 0 and abs(lock_ct - proc.create_time()) > 1.0: if lock_ct > 0 and abs(lock_ct - proc.create_time()) > 1.0:
@@ -63,7 +63,7 @@ def _same_process(meta: dict, proc: psutil.Process, script_hint: str) -> bool:
return False return False
def acquire_lock(script_hint: str = "") -> bool: def acquire_lock() -> bool:
global _lock_file_path global _lock_file_path
ensure_dirs() ensure_dirs()
for f in list(APP_DIR.glob("*.lock")): for f in list(APP_DIR.glob("*.lock")):
@@ -84,7 +84,7 @@ def acquire_lock(script_hint: str = "") -> bool:
pass pass
is_running = False is_running = False
try: try:
is_running = _same_process(meta, psutil.Process(pid), script_hint) is_running = _same_process(meta, psutil.Process(pid))
except Exception: except Exception:
pass pass
if is_running: if is_running:

View File

@@ -56,6 +56,39 @@ from ui.ctk_theme import (
_tray_icon: Optional[object] = None _tray_icon: Optional[object] = None
_config: dict = {} _config: dict = {}
_exiting = False _exiting = False
_win_mutex_handle = None
_ERROR_ALREADY_EXISTS = 183
def _acquire_win_mutex() -> bool | None:
global _win_mutex_handle
try:
kernel32 = ctypes.windll.kernel32
kernel32.CreateMutexW.restype = ctypes.c_void_p
kernel32.CreateMutexW.argtypes = [ctypes.c_void_p, ctypes.c_bool, ctypes.c_wchar_p]
handle = kernel32.CreateMutexW(None, True, "Local\\TgWsProxy_SingleInstance")
if kernel32.GetLastError() == _ERROR_ALREADY_EXISTS:
kernel32.CloseHandle(ctypes.c_void_p(handle))
return False
if not handle:
return None
_win_mutex_handle = handle
return True
except Exception:
return None
def _release_win_mutex() -> None:
global _win_mutex_handle
if _win_mutex_handle:
try:
kernel32 = ctypes.windll.kernel32
kernel32.ReleaseMutex(ctypes.c_void_p(_win_mutex_handle))
kernel32.CloseHandle(ctypes.c_void_p(_win_mutex_handle))
except Exception:
pass
_win_mutex_handle = None
ICON_PATH = str(Path(__file__).parent / "icon.ico") ICON_PATH = str(Path(__file__).parent / "icon.ico")
@@ -350,13 +383,15 @@ def run_tray() -> None:
def main() -> None: def main() -> None:
if not acquire_lock("windows.py"): if (mutex_result := _acquire_win_mutex()) is False or mutex_result is None and not acquire_lock():
_show_info("Приложение уже запущено.", os.path.basename(sys.argv[0])) _show_info("Приложение уже запущено.", os.path.basename(sys.argv[0]))
return return
try: try:
run_tray() run_tray()
finally: finally:
release_lock() release_lock()
_release_win_mutex()
if __name__ == "__main__": if __name__ == "__main__":