feat: автообновление через отдельные shell-скрипты на Windows, Linux и macOS
Добавлено механизм самообновления: приложение скачивает и заменяет текущий бинарник через отдельный скрипт PowerShell (.ps1) или Bash (.sh), обходя ограничение на замену запущенного исполняемого файла без отдельного updater-приложения. utils/updater.py (новый): определение платформенных файлов, проверка прав на запись, генерация скриптов для Windows/Linux/macOS; на macOS — монтирование DMG, копирование .app с откатом при ошибке, снятие quarantine, перезапуск. utils/update_check.py: кэширует assets релиза локально, чтобы при часовом rate limit они были доступны для отложенного обновления. utils/default_config.py: auto_update = True по умолчанию. utils/tray_common.py: maybe_notify_update теперь запускает автоустановку при auto_update=True и is_supported(), иначе показывает ручной диалог; добавлены колбэки on_update_available / on_exit. windows.py / linux.py: _on_update_available добавляет пункт трея «Обновить до X», _on_do_update запускает обновление и корректный выход. macos.py: переход на общий maybe_notify_update вместо ad-hoc логики; добавлен пункт меню обновления, переключатель автообновления и безопасный выход через главный поток с принудительным завершением через 3 секунды. ui/ctk_tray_ui.py: чекбокс «Устанавливать обновления автоматически» в разделе Updates, виден только при is_supported().
This commit is contained in:
parent
28be00ea9e
commit
c47beab064
8 changed files with 491 additions and 45 deletions
40
linux.py
40
linux.py
|
|
@ -34,6 +34,7 @@ from ui.ctk_theme import (
|
|||
_tray_icon: Optional[object] = None
|
||||
_config: dict = {}
|
||||
_exiting = False
|
||||
_update_version: Optional[str] = None
|
||||
|
||||
# dialogs (tkinter messagebox)
|
||||
|
||||
|
|
@ -121,6 +122,28 @@ def _on_open_logs(icon=None, item=None) -> None:
|
|||
_show_info("Файл логов ещё не создан.")
|
||||
|
||||
|
||||
def _on_update_available(version: str) -> None:
|
||||
global _update_version
|
||||
_update_version = version
|
||||
if _tray_icon:
|
||||
_tray_icon.menu = _build_menu()
|
||||
|
||||
|
||||
def _on_do_update(icon=None, item=None) -> None:
|
||||
from utils.updater import run_update
|
||||
from utils.update_check import get_status
|
||||
st = get_status()
|
||||
assets = st.get("assets") or []
|
||||
ver = st.get("latest") or "?"
|
||||
if run_update(assets, ver):
|
||||
_on_exit(_tray_icon)
|
||||
else:
|
||||
_show_error(
|
||||
"Не удалось запустить автообновление.\n"
|
||||
"Скачайте новую версию вручную на странице релиза."
|
||||
)
|
||||
|
||||
|
||||
def _on_exit(icon=None, item=None) -> None:
|
||||
global _exiting
|
||||
if _exiting:
|
||||
|
|
@ -228,7 +251,13 @@ def _build_menu():
|
|||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||
link_host = get_link_host(host)
|
||||
return pystray.Menu(
|
||||
items = []
|
||||
if _update_version:
|
||||
items += [
|
||||
pystray.MenuItem(f"Обновить до {_update_version}", _on_do_update),
|
||||
pystray.Menu.SEPARATOR,
|
||||
]
|
||||
items += [
|
||||
pystray.MenuItem(f"Открыть в Telegram ({link_host}:{port})", _on_open_in_telegram, default=True),
|
||||
pystray.MenuItem("Скопировать ссылку", _on_copy_link),
|
||||
pystray.Menu.SEPARATOR,
|
||||
|
|
@ -237,7 +266,8 @@ def _build_menu():
|
|||
pystray.MenuItem("Открыть логи", _on_open_logs),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("Выход", _on_exit),
|
||||
)
|
||||
]
|
||||
return pystray.Menu(*items)
|
||||
|
||||
|
||||
# entry point
|
||||
|
|
@ -260,7 +290,11 @@ def run_tray() -> None:
|
|||
return
|
||||
|
||||
start_proxy(_config, _show_error)
|
||||
maybe_notify_update(_config, lambda: _exiting, _ask_yes_no)
|
||||
maybe_notify_update(
|
||||
_config, lambda: _exiting, _ask_yes_no,
|
||||
on_update_available=_on_update_available,
|
||||
on_exit=lambda: _on_exit(_tray_icon),
|
||||
)
|
||||
_show_first_run()
|
||||
check_ipv6_warning(_show_info)
|
||||
|
||||
|
|
|
|||
140
macos.py
140
macos.py
|
|
@ -30,7 +30,8 @@ from proxy.tg_ws_proxy import _run
|
|||
from utils.tray_common import (
|
||||
APP_DIR, APP_NAME, DEFAULT_CONFIG, FIRST_RUN_MARKER, IPV6_WARN_MARKER,
|
||||
LOG_FILE, acquire_lock, apply_proxy_config, ensure_dirs, load_config,
|
||||
log, release_lock, save_config, setup_logging, stop_proxy, tg_proxy_url,
|
||||
log, maybe_notify_update, release_lock, save_config, setup_logging,
|
||||
stop_proxy, tg_proxy_url,
|
||||
)
|
||||
|
||||
MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png"
|
||||
|
|
@ -40,6 +41,7 @@ _async_stop: Optional[object] = None
|
|||
_app: Optional[object] = None
|
||||
_config: dict = {}
|
||||
_exiting: bool = False
|
||||
_update_version: Optional[str] = None
|
||||
|
||||
# osascript dialogs
|
||||
|
||||
|
|
@ -280,6 +282,19 @@ def _toggle_check_updates(_=None) -> None:
|
|||
_app._check_updates_item.title = _check_updates_menu_title()
|
||||
|
||||
|
||||
def _auto_update_menu_title() -> str:
|
||||
on = bool(_config.get("auto_update", True))
|
||||
return "✓ Устанавливать обновления автоматически" if on else "Устанавливать обновления автоматически (выкл)"
|
||||
|
||||
|
||||
def _toggle_auto_update(_=None) -> None:
|
||||
global _config
|
||||
_config["auto_update"] = not bool(_config.get("auto_update", True))
|
||||
save_config(_config)
|
||||
if _app is not None and getattr(_app, "_auto_update_item", None) is not None:
|
||||
_app._auto_update_item.title = _auto_update_menu_title()
|
||||
|
||||
|
||||
def _on_open_release_page(_=None) -> None:
|
||||
from utils.update_check import RELEASES_PAGE_URL
|
||||
webbrowser.open(RELEASES_PAGE_URL)
|
||||
|
|
@ -288,30 +303,65 @@ def _on_open_release_page(_=None) -> None:
|
|||
# update check
|
||||
|
||||
|
||||
def _maybe_notify_update_async() -> None:
|
||||
def _work():
|
||||
time.sleep(1.5)
|
||||
if _exiting:
|
||||
return
|
||||
if not _config.get("check_updates", True):
|
||||
return
|
||||
try:
|
||||
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
|
||||
run_check(__version__)
|
||||
st = get_status()
|
||||
if not st.get("has_update"):
|
||||
return
|
||||
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
ver = st.get("latest") or "?"
|
||||
if _ask_yes_no(
|
||||
f"Доступна новая версия: {ver}\n\nОткрыть страницу релиза в браузере?",
|
||||
"TG WS Proxy — обновление",
|
||||
):
|
||||
webbrowser.open(url)
|
||||
except Exception as exc:
|
||||
log.warning("Update check failed: %s", exc)
|
||||
def _run_on_main(fn) -> None:
|
||||
try:
|
||||
from PyObjCTools import AppHelper
|
||||
AppHelper.callAfter(fn)
|
||||
except Exception as exc:
|
||||
log.warning("Main-thread dispatch failed: %s", exc)
|
||||
|
||||
threading.Thread(target=_work, daemon=True, name="update-check").start()
|
||||
|
||||
def _insert_update_menu_item() -> None:
|
||||
if _app is None or _update_version is None:
|
||||
return
|
||||
try:
|
||||
title = f"Обновить до {_update_version}"
|
||||
if title in _app.menu:
|
||||
return
|
||||
item = rumps.MenuItem(title, callback=_on_do_update)
|
||||
keys = list(_app.menu.keys())
|
||||
if keys:
|
||||
_app.menu.insert_before(keys[0], item)
|
||||
else:
|
||||
_app.menu[title] = item
|
||||
except Exception as exc:
|
||||
log.warning("Failed to insert update menu item: %s", exc)
|
||||
|
||||
|
||||
def _on_update_available(version: str) -> None:
|
||||
global _update_version
|
||||
_update_version = version
|
||||
if _app is not None:
|
||||
_run_on_main(_insert_update_menu_item)
|
||||
|
||||
|
||||
def _on_do_update(_=None) -> None:
|
||||
from utils.update_check import get_status
|
||||
from utils.updater import run_update
|
||||
st = get_status()
|
||||
assets = st.get("assets") or []
|
||||
ver = st.get("latest") or "?"
|
||||
if run_update(assets, ver):
|
||||
_quit_rumps()
|
||||
else:
|
||||
_show_error(
|
||||
"Не удалось запустить автообновление.\n"
|
||||
"Скачайте новую версию вручную на странице релиза."
|
||||
)
|
||||
|
||||
|
||||
def _quit_rumps() -> None:
|
||||
global _exiting
|
||||
if _exiting:
|
||||
os._exit(0)
|
||||
return
|
||||
_exiting = True
|
||||
log.info("Quitting menubar app")
|
||||
_run_on_main(rumps.quit_application)
|
||||
threading.Thread(
|
||||
target=lambda: (time.sleep(3), os._exit(0)),
|
||||
daemon=True, name="force-exit",
|
||||
).start()
|
||||
|
||||
|
||||
# settings dialog
|
||||
|
|
@ -536,24 +586,34 @@ class TgWsProxyApp(_TgWsProxyAppBase):
|
|||
)
|
||||
self._version_item = rumps.MenuItem(f"Версия {__version__}", callback=lambda _: None)
|
||||
|
||||
from utils.updater import is_supported as _upd_supported
|
||||
self._auto_update_item = None
|
||||
if _upd_supported():
|
||||
self._auto_update_item = rumps.MenuItem(
|
||||
_auto_update_menu_title(), callback=_toggle_auto_update
|
||||
)
|
||||
|
||||
menu_items = [
|
||||
self._open_tg_item,
|
||||
self._copy_link_item,
|
||||
None,
|
||||
self._restart_item,
|
||||
self._settings_item,
|
||||
self._logs_item,
|
||||
None,
|
||||
self._release_page_item,
|
||||
self._check_updates_item,
|
||||
]
|
||||
if self._auto_update_item is not None:
|
||||
menu_items.append(self._auto_update_item)
|
||||
menu_items.extend([None, self._version_item])
|
||||
|
||||
super().__init__(
|
||||
"TG WS Proxy",
|
||||
icon=icon_path,
|
||||
template=False,
|
||||
quit_button="Выход",
|
||||
menu=[
|
||||
self._open_tg_item,
|
||||
self._copy_link_item,
|
||||
None,
|
||||
self._restart_item,
|
||||
self._settings_item,
|
||||
self._logs_item,
|
||||
None,
|
||||
self._release_page_item,
|
||||
self._check_updates_item,
|
||||
None,
|
||||
self._version_item,
|
||||
],
|
||||
menu=menu_items,
|
||||
)
|
||||
|
||||
def update_menu_title(self) -> None:
|
||||
|
|
@ -597,7 +657,11 @@ def run_menubar() -> None:
|
|||
return
|
||||
|
||||
_start_proxy()
|
||||
_maybe_notify_update_async()
|
||||
maybe_notify_update(
|
||||
_config, lambda: _exiting, _ask_yes_no,
|
||||
on_update_available=_on_update_available,
|
||||
on_exit=_quit_rumps,
|
||||
)
|
||||
_show_first_run()
|
||||
_check_ipv6_warning()
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ _TIP_AUTOSTART = (
|
|||
"Если вы переместите программу в другую папку, автозапуск сбросится"
|
||||
)
|
||||
_TIP_CHECK_UPDATES = "При запуске проверять наличие обновлений"
|
||||
_TIP_AUTO_UPDATE = (
|
||||
"Автоматически скачивать и устанавливать обновления без подтверждения.\n"
|
||||
"Если выключено — при наличии обновления появится кнопка в трее."
|
||||
)
|
||||
_TIP_CFPROXY = (
|
||||
"Использовать Cloudflare прокси для недоступных датацентров"
|
||||
)
|
||||
|
|
@ -295,6 +299,7 @@ class TrayConfigFormWidgets:
|
|||
adv_keys: Tuple[str, ...]
|
||||
autostart_var: Optional[Any]
|
||||
check_updates_var: Optional[Any]
|
||||
auto_update_var: Optional[Any] = None
|
||||
cfproxy_var: Optional[Any] = None
|
||||
cfproxy_priority_var: Optional[Any] = None
|
||||
cfproxy_user_domain_var: Optional[Any] = None
|
||||
|
|
@ -540,6 +545,16 @@ def install_tray_config_form(
|
|||
upd_cb.pack(anchor="w", pady=(0, 6))
|
||||
attach_ctk_tooltip(upd_cb, _TIP_CHECK_UPDATES)
|
||||
|
||||
from utils.updater import is_supported as _upd_supported
|
||||
auto_update_var = None
|
||||
if _upd_supported():
|
||||
auto_update_var = ctk.BooleanVar(
|
||||
value=bool(cfg.get("auto_update", default_config.get("auto_update", True)))
|
||||
)
|
||||
auto_upd_cb = _checkbox(ctk, upd_inner, theme, "Устанавливать обновления автоматически", auto_update_var)
|
||||
auto_upd_cb.pack(anchor="w", pady=(0, 6))
|
||||
attach_ctk_tooltip(auto_upd_cb, _TIP_AUTO_UPDATE)
|
||||
|
||||
if st.get("error"):
|
||||
upd_status = "Не удалось связаться с GitHub. Проверьте сеть."
|
||||
elif not st.get("checked"):
|
||||
|
|
@ -589,6 +604,7 @@ def install_tray_config_form(
|
|||
dc_textbox=dc_textbox, verbose_var=verbose_var,
|
||||
adv_entries=adv_entries, adv_keys=adv_keys,
|
||||
autostart_var=autostart_var, check_updates_var=check_updates_var,
|
||||
auto_update_var=auto_update_var,
|
||||
cfproxy_var=cfproxy_var,
|
||||
cfproxy_priority_var=cfproxy_priority_var,
|
||||
cfproxy_user_domain_var=cfproxy_user_domain_var,
|
||||
|
|
@ -669,6 +685,8 @@ def validate_config_form(
|
|||
merge_adv_from_form(widgets, new_cfg, default_config)
|
||||
if widgets.check_updates_var is not None:
|
||||
new_cfg["check_updates"] = bool(widgets.check_updates_var.get())
|
||||
if widgets.auto_update_var is not None:
|
||||
new_cfg["auto_update"] = bool(widgets.auto_update_var.get())
|
||||
if widgets.cfproxy_var is not None:
|
||||
new_cfg["cfproxy"] = bool(widgets.cfproxy_var.get())
|
||||
if widgets.cfproxy_priority_var is not None:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
|||
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
||||
"verbose": False,
|
||||
"check_updates": True,
|
||||
"auto_update": True,
|
||||
"log_max_mb": 5,
|
||||
"buf_kb": 256,
|
||||
"pool_size": 4,
|
||||
|
|
|
|||
|
|
@ -367,6 +367,9 @@ def maybe_notify_update(
|
|||
cfg: dict,
|
||||
is_exiting: Callable[[], bool],
|
||||
ask_open: Callable[[str, str], bool],
|
||||
*,
|
||||
on_update_available: Optional[Callable[[str], None]] = None,
|
||||
on_exit: Optional[Callable[[], None]] = None,
|
||||
) -> None:
|
||||
if not cfg.get("check_updates", True):
|
||||
return
|
||||
|
|
@ -377,14 +380,29 @@ def maybe_notify_update(
|
|||
return
|
||||
try:
|
||||
from utils.update_check import RELEASES_PAGE_URL, get_status, run_check
|
||||
from utils.updater import is_supported, run_update
|
||||
import webbrowser
|
||||
|
||||
run_check(__version__)
|
||||
st = get_status()
|
||||
if not st.get("has_update"):
|
||||
return
|
||||
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
|
||||
ver = st.get("latest") or "?"
|
||||
assets = st.get("assets") or []
|
||||
url = (st.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
auto_update = cfg.get("auto_update", True)
|
||||
|
||||
if auto_update and is_supported() and assets:
|
||||
if run_update(assets, ver):
|
||||
if on_exit:
|
||||
on_exit()
|
||||
return
|
||||
# launch failed — fall through to dialog
|
||||
|
||||
if not auto_update and on_update_available:
|
||||
on_update_available(ver)
|
||||
|
||||
if ask_open(
|
||||
f"Доступна новая версия: {ver}\n\nОткрыть страницу релиза в браузере?",
|
||||
"TG WS Proxy — обновление",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ _state: Dict[str, Any] = {
|
|||
"latest": None,
|
||||
"html_url": None,
|
||||
"error": None,
|
||||
"assets": [],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -162,6 +163,7 @@ def run_check(current_version: str) -> None:
|
|||
tag = (cache.get("tag_name") or "").strip()
|
||||
if tag:
|
||||
_apply_release_tag(tag, cache.get("html_url") or "", current_version)
|
||||
_state["assets"] = cache.get("assets") or []
|
||||
return
|
||||
err = cache.get("last_error")
|
||||
_state["error"] = (
|
||||
|
|
@ -181,6 +183,7 @@ def run_check(current_version: str) -> None:
|
|||
tag = (cache.get("tag_name") or "").strip()
|
||||
url = (cache.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
_apply_release_tag(tag, url, current_version)
|
||||
_state["assets"] = cache.get("assets") or []
|
||||
if new_etag:
|
||||
cache["etag"] = new_etag
|
||||
_save_cache(cache_path, cache)
|
||||
|
|
@ -189,6 +192,7 @@ def run_check(current_version: str) -> None:
|
|||
assert data is not None
|
||||
tag = (data.get("tag_name") or "").strip()
|
||||
html_url = (data.get("html_url") or "").strip() or RELEASES_PAGE_URL
|
||||
assets = data.get("assets") or []
|
||||
if not tag:
|
||||
_state["has_update"] = False
|
||||
_state["ahead_of_release"] = False
|
||||
|
|
@ -196,10 +200,12 @@ def run_check(current_version: str) -> None:
|
|||
_state["html_url"] = html_url
|
||||
else:
|
||||
_apply_release_tag(tag, html_url, current_version)
|
||||
_state["assets"] = assets
|
||||
if new_etag:
|
||||
cache["etag"] = new_etag
|
||||
cache["tag_name"] = tag
|
||||
cache["html_url"] = html_url
|
||||
cache["assets"] = assets
|
||||
cache.pop("last_error", None)
|
||||
_save_cache(cache_path, cache)
|
||||
except (HTTPError, URLError, OSError, TimeoutError, ValueError, json.JSONDecodeError) as e:
|
||||
|
|
|
|||
271
utils/updater.py
Normal file
271
utils/updater.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
log = logging.getLogger('tg-mtproto-proxy')
|
||||
|
||||
|
||||
def _find_app_bundle() -> Optional[Path]:
|
||||
"""On macOS, walk up sys.executable to find the enclosing .app bundle."""
|
||||
if sys.platform != 'darwin':
|
||||
return None
|
||||
try:
|
||||
p = Path(sys.executable).resolve()
|
||||
except OSError:
|
||||
return None
|
||||
for _ in range(8):
|
||||
if p.suffix == '.app' and p.is_dir():
|
||||
return p
|
||||
if p == p.parent:
|
||||
return None
|
||||
p = p.parent
|
||||
return None
|
||||
|
||||
|
||||
def _can_write_target() -> bool:
|
||||
"""Check if we have write permission for the install target."""
|
||||
try:
|
||||
if sys.platform == 'darwin':
|
||||
app = _find_app_bundle()
|
||||
if app is None:
|
||||
return False
|
||||
return os.access(str(app), os.W_OK) and os.access(str(app.parent), os.W_OK)
|
||||
exe = Path(sys.executable)
|
||||
return os.access(str(exe), os.W_OK) and os.access(str(exe.parent), os.W_OK)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def is_supported() -> bool:
|
||||
"""Auto-update is supported on frozen builds where we can write to the install target."""
|
||||
if not getattr(sys, 'frozen', False):
|
||||
return False
|
||||
if sys.platform not in ('win32', 'linux', 'darwin'):
|
||||
return False
|
||||
return _can_write_target()
|
||||
|
||||
|
||||
def find_asset_url(assets: list) -> Optional[str]:
|
||||
name = _asset_name()
|
||||
if name is None:
|
||||
return None
|
||||
for asset in assets:
|
||||
if asset.get('name') == name:
|
||||
return asset.get('browser_download_url')
|
||||
return None
|
||||
|
||||
|
||||
def _asset_name() -> Optional[str]:
|
||||
if sys.platform == 'win32':
|
||||
ver = sys.getwindowsversion()
|
||||
if ver.major >= 10:
|
||||
return 'TgWsProxy_windows.exe'
|
||||
bits = struct.calcsize('P') * 8
|
||||
return f'TgWsProxy_windows_7_{"64bit" if bits == 64 else "32bit"}.exe'
|
||||
if sys.platform == 'linux':
|
||||
return 'TgWsProxy_linux_amd64'
|
||||
if sys.platform == 'darwin':
|
||||
return 'TgWsProxy_macos_universal.dmg'
|
||||
return None
|
||||
|
||||
|
||||
def run_update(assets: list, new_version: str) -> bool:
|
||||
"""Generate a platform updater script and launch it detached. Returns True on success."""
|
||||
url = find_asset_url(assets)
|
||||
if not url:
|
||||
log.warning("Auto-update: no matching asset found for this platform")
|
||||
return False
|
||||
|
||||
if not _can_write_target():
|
||||
log.warning("Auto-update: no write permission for install target")
|
||||
return False
|
||||
|
||||
pid = os.getpid()
|
||||
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
script = _ps1(pid, sys.executable, url, new_version)
|
||||
tmp = Path(tempfile.mktemp(suffix='.ps1'))
|
||||
tmp.write_text(script, encoding='utf-8')
|
||||
subprocess.Popen(
|
||||
['powershell', '-ExecutionPolicy', 'Bypass',
|
||||
'-WindowStyle', 'Normal', '-File', str(tmp)],
|
||||
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
)
|
||||
elif sys.platform == 'darwin':
|
||||
app = _find_app_bundle()
|
||||
if app is None:
|
||||
log.warning("Auto-update: cannot locate .app bundle")
|
||||
return False
|
||||
script = _sh_macos(pid, str(app), url, new_version)
|
||||
tmp = Path(tempfile.mktemp(suffix='.sh'))
|
||||
tmp.write_text(script, encoding='utf-8')
|
||||
tmp.chmod(0o755)
|
||||
subprocess.Popen(['/bin/bash', str(tmp)], start_new_session=True)
|
||||
else:
|
||||
script = _sh_linux(pid, sys.executable, url, new_version)
|
||||
tmp = Path(tempfile.mktemp(suffix='.sh'))
|
||||
tmp.write_text(script, encoding='utf-8')
|
||||
tmp.chmod(0o755)
|
||||
subprocess.Popen(['/bin/bash', str(tmp)], start_new_session=True)
|
||||
log.info("Auto-update: updater launched for v%s, shutting down", new_version)
|
||||
return True
|
||||
except Exception as exc:
|
||||
log.error("Auto-update: failed to launch updater: %s", repr(exc))
|
||||
return False
|
||||
|
||||
|
||||
def _ps1(pid: int, exe: str, url: str, version: str) -> str:
|
||||
exe_esc = exe.replace("'", "''")
|
||||
return f"""\
|
||||
$pid_target = {pid}
|
||||
$exe = '{exe_esc}'
|
||||
$url = '{url}'
|
||||
$ver = '{version}'
|
||||
|
||||
Write-Host "TG WS Proxy updater: installing $ver..."
|
||||
|
||||
$proc = Get-Process -Id $pid_target -ErrorAction SilentlyContinue
|
||||
if ($proc) {{
|
||||
Write-Host "Waiting for process to exit..."
|
||||
if (-not $proc.WaitForExit(15000)) {{
|
||||
$proc | Stop-Process -Force
|
||||
Start-Sleep -Milliseconds 500
|
||||
}}
|
||||
}}
|
||||
|
||||
$tmp = $exe + '.update'
|
||||
Write-Host "Downloading..."
|
||||
try {{
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
(New-Object Net.WebClient).DownloadFile($url, $tmp)
|
||||
}} catch {{
|
||||
Write-Host "Download failed: $_"
|
||||
Start-Sleep 3
|
||||
exit 1
|
||||
}}
|
||||
|
||||
try {{
|
||||
Move-Item -LiteralPath $tmp -Destination $exe -Force
|
||||
}} catch {{
|
||||
Write-Host "Replace failed: $_"
|
||||
Start-Sleep 3
|
||||
exit 1
|
||||
}}
|
||||
|
||||
Write-Host "Done. Restarting..."
|
||||
Start-Process -FilePath $exe
|
||||
Start-Sleep -Milliseconds 500
|
||||
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue
|
||||
"""
|
||||
|
||||
|
||||
def _sh_linux(pid: int, exe: str, url: str, version: str) -> str:
|
||||
exe_esc = exe.replace("'", "'\\''")
|
||||
return f"""\
|
||||
#!/bin/bash
|
||||
PID={pid}
|
||||
EXE='{exe_esc}'
|
||||
URL='{url}'
|
||||
VER='{version}'
|
||||
|
||||
echo "TG WS Proxy updater: installing $VER..."
|
||||
echo "Waiting for process to exit..."
|
||||
for i in $(seq 1 30); do
|
||||
kill -0 $PID 2>/dev/null || break
|
||||
sleep 0.5
|
||||
done
|
||||
kill -9 $PID 2>/dev/null || true
|
||||
sleep 0.3
|
||||
|
||||
TMP="$EXE.update"
|
||||
echo "Downloading..."
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -fsSL -o "$TMP" "$URL" || {{ echo "Download failed"; sleep 3; exit 1; }}
|
||||
else
|
||||
wget -q -O "$TMP" "$URL" || {{ echo "Download failed"; sleep 3; exit 1; }}
|
||||
fi
|
||||
chmod +x "$TMP"
|
||||
mv -f "$TMP" "$EXE" || {{ echo "Replace failed"; sleep 3; exit 1; }}
|
||||
|
||||
echo "Done. Restarting..."
|
||||
nohup "$EXE" >/dev/null 2>&1 &
|
||||
rm -f "$0"
|
||||
"""
|
||||
|
||||
|
||||
def _sh_macos(pid: int, app_path: str, url: str, version: str) -> str:
|
||||
app_esc = app_path.replace("'", "'\\''")
|
||||
return f"""\
|
||||
#!/bin/bash
|
||||
PID={pid}
|
||||
APP='{app_esc}'
|
||||
URL='{url}'
|
||||
VER='{version}'
|
||||
|
||||
echo "TG WS Proxy updater: installing $VER..."
|
||||
echo "Waiting for process to exit..."
|
||||
for i in $(seq 1 30); do
|
||||
kill -0 $PID 2>/dev/null || break
|
||||
sleep 0.5
|
||||
done
|
||||
kill -9 $PID 2>/dev/null || true
|
||||
sleep 0.3
|
||||
|
||||
TMP_DMG="$(mktemp -t tgwsproxy).dmg"
|
||||
echo "Downloading..."
|
||||
if ! curl -fsSL -o "$TMP_DMG" "$URL"; then
|
||||
echo "Download failed"
|
||||
rm -f "$TMP_DMG"
|
||||
sleep 3
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Mounting..."
|
||||
MOUNT=$(hdiutil attach -nobrowse -readonly "$TMP_DMG" 2>/dev/null | grep -oE '/Volumes/.*$' | tail -1)
|
||||
if [ -z "$MOUNT" ]; then
|
||||
echo "Mount failed"
|
||||
rm -f "$TMP_DMG"
|
||||
sleep 3
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC_APP=$(find "$MOUNT" -maxdepth 2 -name "*.app" -type d 2>/dev/null | head -1)
|
||||
if [ -z "$SRC_APP" ]; then
|
||||
hdiutil detach -quiet "$MOUNT" 2>/dev/null
|
||||
rm -f "$TMP_DMG"
|
||||
echo "No .app found in DMG"
|
||||
sleep 3
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing..."
|
||||
BACKUP="$APP.bak"
|
||||
rm -rf "$BACKUP"
|
||||
mv -f "$APP" "$BACKUP" 2>/dev/null || true
|
||||
if ! cp -R "$SRC_APP" "$APP"; then
|
||||
mv -f "$BACKUP" "$APP" 2>/dev/null || true
|
||||
hdiutil detach -quiet "$MOUNT" 2>/dev/null
|
||||
rm -f "$TMP_DMG"
|
||||
echo "Install failed"
|
||||
sleep 3
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$BACKUP"
|
||||
|
||||
hdiutil detach -quiet "$MOUNT" 2>/dev/null
|
||||
rm -f "$TMP_DMG"
|
||||
|
||||
xattr -dr com.apple.quarantine "$APP" 2>/dev/null || true
|
||||
|
||||
echo "Done. Restarting..."
|
||||
open "$APP"
|
||||
rm -f "$0"
|
||||
"""
|
||||
40
windows.py
40
windows.py
|
|
@ -57,6 +57,7 @@ _tray_icon: Optional[object] = None
|
|||
_config: dict = {}
|
||||
_exiting = False
|
||||
_win_mutex_handle = None
|
||||
_update_version: Optional[str] = None
|
||||
|
||||
_ERROR_ALREADY_EXISTS = 183
|
||||
|
||||
|
|
@ -217,6 +218,28 @@ def _on_open_logs(icon=None, item=None) -> None:
|
|||
_show_info("Файл логов ещё не создан.")
|
||||
|
||||
|
||||
def _on_update_available(version: str) -> None:
|
||||
global _update_version
|
||||
_update_version = version
|
||||
if _tray_icon:
|
||||
_tray_icon.menu = _build_menu()
|
||||
|
||||
|
||||
def _on_do_update(icon=None, item=None) -> None:
|
||||
from utils.updater import run_update
|
||||
from utils.update_check import get_status
|
||||
st = get_status()
|
||||
assets = st.get("assets") or []
|
||||
ver = st.get("latest") or "?"
|
||||
if run_update(assets, ver):
|
||||
_on_exit(_tray_icon)
|
||||
else:
|
||||
_show_error(
|
||||
"Не удалось запустить автообновление.\n"
|
||||
"Скачайте новую версию вручную на странице релиза."
|
||||
)
|
||||
|
||||
|
||||
def _on_exit(icon=None, item=None) -> None:
|
||||
global _exiting
|
||||
if _exiting:
|
||||
|
|
@ -335,7 +358,13 @@ def _build_menu():
|
|||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||
link_host = get_link_host(host)
|
||||
return pystray.Menu(
|
||||
items = []
|
||||
if _update_version:
|
||||
items += [
|
||||
pystray.MenuItem(f"Обновить до {_update_version}", _on_do_update),
|
||||
pystray.Menu.SEPARATOR,
|
||||
]
|
||||
items += [
|
||||
pystray.MenuItem(f"Открыть в Telegram ({link_host}:{port})", _on_open_in_telegram, default=True),
|
||||
pystray.MenuItem("Скопировать ссылку", _on_copy_link),
|
||||
pystray.Menu.SEPARATOR,
|
||||
|
|
@ -344,7 +373,8 @@ def _build_menu():
|
|||
pystray.MenuItem("Открыть логи", _on_open_logs),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("Выход", _on_exit),
|
||||
)
|
||||
]
|
||||
return pystray.Menu(*items)
|
||||
|
||||
|
||||
# entry point
|
||||
|
|
@ -370,7 +400,11 @@ def run_tray() -> None:
|
|||
return
|
||||
|
||||
start_proxy(_config, _show_error)
|
||||
maybe_notify_update(_config, lambda: _exiting, _ask_yes_no)
|
||||
maybe_notify_update(
|
||||
_config, lambda: _exiting, _ask_yes_no,
|
||||
on_update_available=_on_update_available,
|
||||
on_exit=lambda: _on_exit(_tray_icon),
|
||||
)
|
||||
_show_first_run()
|
||||
check_ipv6_warning(_show_info)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue