mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-10-21 02:58:45 +00:00
#1223 fixes, timeout for downloads
This commit is contained in:
1 parent
a6177fec87
commit
bf07b42f88
4 files changed
+55
-30
No files matched your search
@@ -1,4 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -71,29 +73,31 @@ class DetectInstallKindTest(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class InstallCmdTest(unittest.TestCase):
|
class InstallCmdTest(unittest.TestCase):
|
||||||
|
package_path = Path(tempfile.gettempdir()) / 'pkg'
|
||||||
|
|
||||||
def _cmd(self, kind, tools):
|
def _cmd(self, kind, tools):
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
linux_update.shutil, 'which',
|
linux_update.shutil, 'which',
|
||||||
side_effect=lambda n: '/usr/bin/' + n if n in tools else None,
|
side_effect=lambda n: '/usr/bin/' + n if n in tools else None,
|
||||||
), mock.patch.object(linux_update, '_is_root', return_value=False):
|
), mock.patch.object(linux_update, '_is_root', return_value=False):
|
||||||
return linux_update.install_cmd(kind, Path('/tmp/pkg'))
|
return linux_update.install_cmd(kind, self.package_path)
|
||||||
|
|
||||||
def test_deb_prefers_apt_get_and_elevates(self):
|
def test_deb_prefers_apt_get_and_elevates(self):
|
||||||
cmd = self._cmd('deb', ('apt-get',))
|
cmd = self._cmd('deb', ('apt-get',))
|
||||||
self.assertEqual(cmd[:3], ['pkexec', 'apt-get', 'install'])
|
self.assertEqual(cmd[:3], ['pkexec', 'apt-get', 'install'])
|
||||||
self.assertEqual(cmd[-1], '/tmp/pkg')
|
self.assertEqual(cmd[-1], str(self.package_path))
|
||||||
|
|
||||||
def test_deb_falls_back_to_dpkg(self):
|
def test_deb_falls_back_to_dpkg(self):
|
||||||
self.assertEqual(self._cmd('deb', ()), ['pkexec', 'dpkg', '-i', '/tmp/pkg'])
|
self.assertEqual(self._cmd('deb', ()), ['pkexec', 'dpkg', '-i', str(self.package_path)])
|
||||||
|
|
||||||
def test_rpm_prefers_dnf_without_removing_packages(self):
|
def test_rpm_prefers_dnf_without_removing_packages(self):
|
||||||
cmd = self._cmd('rpm', ('dnf',))
|
cmd = self._cmd('rpm', ('dnf',))
|
||||||
self.assertEqual(cmd, ['pkexec', 'dnf', 'install', '-y', '/tmp/pkg'])
|
self.assertEqual(cmd, ['pkexec', 'dnf', 'install', '-y', str(self.package_path)])
|
||||||
self.assertNotIn('--allowerasing', cmd)
|
self.assertNotIn('--allowerasing', cmd)
|
||||||
|
|
||||||
def test_rpm_falls_back_to_rpm_binary(self):
|
def test_rpm_falls_back_to_rpm_binary(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self._cmd('rpm', ()), ['pkexec', 'rpm', '-U', '--force', '/tmp/pkg'],
|
self._cmd('rpm', ()), ['pkexec', 'rpm', '-U', '--force', str(self.package_path)],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_unknown_kind_is_rejected(self):
|
def test_unknown_kind_is_rejected(self):
|
||||||
@@ -104,8 +108,8 @@ class InstallCmdTest(unittest.TestCase):
|
|||||||
with mock.patch.object(linux_update.shutil, 'which', return_value=None), \
|
with mock.patch.object(linux_update.shutil, 'which', return_value=None), \
|
||||||
mock.patch.object(linux_update, '_is_root', return_value=True):
|
mock.patch.object(linux_update, '_is_root', return_value=True):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
linux_update.install_cmd('deb', Path('/tmp/pkg')),
|
linux_update.install_cmd('deb', self.package_path),
|
||||||
['dpkg', '-i', '/tmp/pkg'],
|
['dpkg', '-i', str(self.package_path)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -127,9 +131,18 @@ class ApplyUpdateTest(unittest.TestCase):
|
|||||||
self.assertIsNone(linux_update.apply_update('binary', 'https://x.invalid/a'))
|
self.assertIsNone(linux_update.apply_update('binary', 'https://x.invalid/a'))
|
||||||
|
|
||||||
self.assertEqual(exe.read_bytes(), b'new')
|
self.assertEqual(exe.read_bytes(), b'new')
|
||||||
self.assertTrue(os.access(str(exe), os.X_OK))
|
|
||||||
self.assertEqual(list(Path(tmp).iterdir()), [exe])
|
self.assertEqual(list(Path(tmp).iterdir()), [exe])
|
||||||
|
|
||||||
|
@unittest.skipUnless(sys.platform == 'linux', 'Linux executable permissions')
|
||||||
|
def test_replaced_binary_has_executable_permissions(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
exe = Path(tmp) / 'TgWsProxy'
|
||||||
|
exe.write_bytes(b'old')
|
||||||
|
with mock.patch.object(linux_update, 'exe_path', return_value=exe), \
|
||||||
|
mock.patch('utils.update_check.download_asset', self._download()):
|
||||||
|
self.assertIsNone(linux_update.apply_update('binary', 'https://x.invalid/a'))
|
||||||
|
self.assertEqual(stat.S_IMODE(exe.stat().st_mode), 0o755)
|
||||||
|
|
||||||
def test_read_only_dir_binary_is_installed_with_elevation(self):
|
def test_read_only_dir_binary_is_installed_with_elevation(self):
|
||||||
exe = Path('/usr/local/bin/TgWsProxy')
|
exe = Path('/usr/local/bin/TgWsProxy')
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
@@ -205,6 +218,32 @@ class ApplyUpdateTest(unittest.TestCase):
|
|||||||
self.assertEqual(linux_update.apply_update('binary', 'https://x.invalid/a'), 'boom')
|
self.assertEqual(linux_update.apply_update('binary', 'https://x.invalid/a'), 'boom')
|
||||||
|
|
||||||
|
|
||||||
|
class ExternalCommandsEnvTest(unittest.TestCase):
|
||||||
|
def test_commands_use_system_libraries_without_changing_parent_environment(self):
|
||||||
|
bundle = str(Path(tempfile.gettempdir()) / '_MEItest')
|
||||||
|
probe = (
|
||||||
|
'import os, sys; '
|
||||||
|
'assert os.environ.get("LD_LIBRARY_PATH", "") == sys.argv[1]; '
|
||||||
|
'assert "_PYI_APPLICATION_HOME_DIR" not in os.environ'
|
||||||
|
)
|
||||||
|
for original in ('/usr/lib', None):
|
||||||
|
for runner, expected in ((linux_update._run_quiet, True),
|
||||||
|
(linux_update._run_install, None)):
|
||||||
|
with self.subTest(original=original, runner=runner.__name__):
|
||||||
|
env = dict(os.environ)
|
||||||
|
env.update(LD_LIBRARY_PATH=bundle, _PYI_APPLICATION_HOME_DIR=bundle)
|
||||||
|
env.pop('LD_LIBRARY_PATH_ORIG', None)
|
||||||
|
if original is not None:
|
||||||
|
env['LD_LIBRARY_PATH_ORIG'] = original
|
||||||
|
with mock.patch.dict(os.environ, env, clear=True), \
|
||||||
|
mock.patch.object(sys, '_MEIPASS', bundle, create=True):
|
||||||
|
self.assertEqual(
|
||||||
|
runner([sys.executable, '-c', probe, original or '']), expected,
|
||||||
|
)
|
||||||
|
self.assertEqual(os.environ['LD_LIBRARY_PATH'], bundle)
|
||||||
|
self.assertEqual(os.environ['_PYI_APPLICATION_HOME_DIR'], bundle)
|
||||||
|
|
||||||
|
|
||||||
class RelaunchEnvTest(unittest.TestCase):
|
class RelaunchEnvTest(unittest.TestCase):
|
||||||
def _env(self, env, meipass='/tmp/_MEI1'):
|
def _env(self, env, meipass='/tmp/_MEI1'):
|
||||||
with mock.patch.dict(linux_update.os.environ, env, clear=True), \
|
with mock.patch.dict(linux_update.os.environ, env, clear=True), \
|
||||||
@@ -218,7 +257,7 @@ class RelaunchEnvTest(unittest.TestCase):
|
|||||||
'LD_LIBRARY_PATH': '/tmp/_MEI1',
|
'LD_LIBRARY_PATH': '/tmp/_MEI1',
|
||||||
'LD_LIBRARY_PATH_ORIG': '/usr/lib',
|
'LD_LIBRARY_PATH_ORIG': '/usr/lib',
|
||||||
'GI_TYPELIB_PATH': '/tmp/_MEI1',
|
'GI_TYPELIB_PATH': '/tmp/_MEI1',
|
||||||
'PATH': '/tmp/_MEI1:/usr/bin',
|
'PATH': os.pathsep.join(('/tmp/_MEI1', '/usr/bin')),
|
||||||
'HOME': '/home/u',
|
'HOME': '/home/u',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ def _run_quiet(cmd: List[str]) -> bool:
|
|||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
timeout=15,
|
timeout=15,
|
||||||
|
env=relaunch_env(),
|
||||||
).returncode == 0
|
).returncode == 0
|
||||||
except (OSError, subprocess.SubprocessError):
|
except (OSError, subprocess.SubprocessError):
|
||||||
return False
|
return False
|
||||||
@@ -107,6 +108,7 @@ def _run_install(cmd: List[str], log: Optional[logging.Logger] = None) -> Option
|
|||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
timeout=_INSTALL_TIMEOUT_SEC,
|
timeout=_INSTALL_TIMEOUT_SEC,
|
||||||
|
env=relaunch_env(),
|
||||||
)
|
)
|
||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -271,12 +271,14 @@ def _expected_length(resp: Any) -> Optional[int]:
|
|||||||
|
|
||||||
def download_asset(
|
def download_asset(
|
||||||
url: str, dest_dir: Path, suffix: str = ".tmp", digest: str = "",
|
url: str, dest_dir: Path, suffix: str = ".tmp", digest: str = "",
|
||||||
|
timeout: float = 30.0,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""Download a release asset into a temporary file inside dest_dir.
|
"""Download a release asset into a temporary file inside dest_dir.
|
||||||
|
|
||||||
The size is checked against Content-Length and, when the GitHub API
|
The size is checked against Content-Length and, when the GitHub API
|
||||||
provides a "sha256:..." digest, the checksum too. A truncated or
|
provides a "sha256:..." digest, the checksum too. A truncated or
|
||||||
corrupted file is removed instead of being returned.
|
corrupted file is removed instead of being returned.
|
||||||
|
timeout limits blocking network operations, including response reads.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
OSError: network error, truncated download or checksum mismatch.
|
OSError: network error, truncated download or checksum mismatch.
|
||||||
@@ -287,7 +289,7 @@ def download_asset(
|
|||||||
try:
|
try:
|
||||||
sha = hashlib.sha256()
|
sha = hashlib.sha256()
|
||||||
written = 0
|
written = 0
|
||||||
with build_github_opener().open(url) as resp:
|
with build_github_opener().open(url, timeout=timeout) as resp:
|
||||||
expected = _expected_length(resp)
|
expected = _expected_length(resp)
|
||||||
with open(str(path), "wb") as out:
|
with open(str(path), "wb") as out:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
+2
-20
@@ -8,11 +8,9 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import winreg
|
import winreg
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from proxy.utils import build_github_opener
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pyperclip
|
import pyperclip
|
||||||
@@ -48,7 +46,7 @@ from utils.tray_common import (
|
|||||||
save_config, start_proxy, stop_proxy, tg_proxy_url,
|
save_config, start_proxy, stop_proxy, tg_proxy_url,
|
||||||
)
|
)
|
||||||
from utils.update_check import (
|
from utils.update_check import (
|
||||||
get_status, get_update_asset, run_check, RELEASES_PAGE_URL,
|
download_asset, get_status, get_update_asset, run_check, RELEASES_PAGE_URL,
|
||||||
)
|
)
|
||||||
from ui.ctk_tray_ui import (
|
from ui.ctk_tray_ui import (
|
||||||
install_tray_config_buttons, install_tray_config_form,
|
install_tray_config_buttons, install_tray_config_form,
|
||||||
@@ -245,27 +243,11 @@ def _perform_update(download_url: str, set_status=None) -> None:
|
|||||||
_step(t("update.downloading"))
|
_step(t("update.downloading"))
|
||||||
cur_exe = Path(sys.executable)
|
cur_exe = Path(sys.executable)
|
||||||
old_exe = cur_exe.with_name(cur_exe.stem + "_oldtgws.exe")
|
old_exe = cur_exe.with_name(cur_exe.stem + "_oldtgws.exe")
|
||||||
tmp_path = None
|
|
||||||
try:
|
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)
|
log.info("Downloading update from %s", download_url)
|
||||||
opener = build_github_opener()
|
tmp_path = download_asset(download_url, cur_exe.parent)
|
||||||
with opener.open(download_url) as _resp:
|
|
||||||
with open(str(tmp_path), "wb") as _fout:
|
|
||||||
while True:
|
|
||||||
_chunk = _resp.read(65536)
|
|
||||||
if not _chunk:
|
|
||||||
break
|
|
||||||
_fout.write(_chunk)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_err(t("update.download_fail", error=exc))
|
_err(t("update.download_fail", error=exc))
|
||||||
if tmp_path:
|
|
||||||
try:
|
|
||||||
tmp_path.unlink(missing_ok=True)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
return
|
return
|
||||||
|
|
||||||
_step(t("update.replacing"))
|
_step(t("update.replacing"))
|
||||||
|
|||||||
Reference in new issue
Block a user