mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-12 01:08:17 +03:00
Add sing-tun settings UI
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -309,7 +309,7 @@ class ExternalCoreApplicationTun2socksInput(GuiEditorItemTextSwitch):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the application tun2socks opt-in control."""
|
||||
super().__init__(title=_('Use Application Tun2socks'))
|
||||
super().__init__(title=_('Use Application TUN'))
|
||||
|
||||
def connectToggled(self, receiver, methodName: str):
|
||||
"""Connect a field-state callback without retaining its binding."""
|
||||
@@ -536,7 +536,7 @@ class ExternalCoreEditor(GuiEditorWidgetQDialog):
|
||||
and not self._tunRemoteAddressInput.text()
|
||||
):
|
||||
validationMessage = _(
|
||||
'TUN remote address is required when application tun2socks is enabled'
|
||||
'TUN remote address is required when application TUN is enabled'
|
||||
)
|
||||
|
||||
if validationMessage:
|
||||
|
||||
Vendored
+285
-32
@@ -1425,6 +1425,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Primary Adapter Interface Name": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog",
|
||||
"Furious.Window.TunSettingsDialog"
|
||||
],
|
||||
"RU": "Имя основного сетевого адаптера",
|
||||
@@ -1433,6 +1434,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Primary Adapter Interface IP": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog",
|
||||
"Furious.Window.TunSettingsDialog"
|
||||
],
|
||||
"RU": "IP-адрес основного сетевого адаптера",
|
||||
@@ -1447,24 +1449,9 @@ TRANSLATION = {
|
||||
"ZH": "默认主网关IP",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Tun2socks Adapter Interface DNS": {
|
||||
"source": [
|
||||
"Furious.Window.TunSettingsDialog"
|
||||
],
|
||||
"RU": "DNS-адрес интерфейса адаптера Tun2socks",
|
||||
"ZH": "Tun2socks适配器接口DNS",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Bypass Tun2socks Adapter Interface IP (separated by commas)": {
|
||||
"source": [
|
||||
"Furious.Window.TunSettingsDialog"
|
||||
],
|
||||
"RU": "Обход IP-адреса интерфейса адаптера Tun2socks (через запятую)",
|
||||
"ZH": "绕过Tun2socks适配器接口IP(以逗号分隔)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Disable Primary Adapter Interface DNS (Mitigating DNS leaks on Windows)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog",
|
||||
"Furious.Window.TunSettingsDialog"
|
||||
],
|
||||
"RU": "Отключить DNS основного сетевого адаптера (для предотвращения утечек DNS в Windows)",
|
||||
@@ -2760,14 +2747,6 @@ TRANSLATION = {
|
||||
"ZH": "SOCKS代理",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Use Application Tun2socks": {
|
||||
"source": [
|
||||
"Furious.Backends.ExternalCore.Editor"
|
||||
],
|
||||
"RU": "Использовать Tun2socks приложения",
|
||||
"ZH": "使用应用程序的Tun2socks",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"TUN Remote Address": {
|
||||
"source": [
|
||||
"Furious.Backends.ExternalCore.Editor"
|
||||
@@ -2936,14 +2915,6 @@ TRANSLATION = {
|
||||
"ZH": "代理出口信息与隐私",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"TUN remote address is required when application tun2socks is enabled": {
|
||||
"source": [
|
||||
"Furious.Backends.ExternalCore.Editor"
|
||||
],
|
||||
"RU": "При включённом Tun2socks приложения необходимо указать удалённый адрес TUN",
|
||||
"ZH": "启用应用程序的Tun2socks时必须填写TUN远程地址",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Data usage": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
@@ -3375,5 +3346,287 @@ TRANSLATION = {
|
||||
"RU": "Тесты",
|
||||
"ZH": "测试",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Application TUN Engine": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Движок TUN приложения",
|
||||
"ZH": "应用 TUN 引擎",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Native proxy-core TUN takes priority. This engine is used when Furious provides application TUN. Changes apply to the next connection.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Встроенный TUN прокси-ядра имеет приоритет. Этот движок используется, когда TUN предоставляет Furious. Изменения применяются при следующем подключении.",
|
||||
"ZH": "代理内核的原生 TUN 优先。由 Furious 提供应用 TUN 时才使用此引擎。更改将在下次连接时生效。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Customize sing-tun Settings...": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Настроить sing-tun...",
|
||||
"ZH": "自定义 sing-tun 设置...",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Customize sing-tun Settings": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Настройка sing-tun",
|
||||
"ZH": "自定义 sing-tun 设置",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"SOCKS Transit": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Передача через SOCKS",
|
||||
"ZH": "SOCKS 转发",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Stack": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Сетевой стек",
|
||||
"ZH": "网络协议栈",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Log Level": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Уровень журналирования",
|
||||
"ZH": "日志级别",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"SOCKS Interface Binding (empty for automatic)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Интерфейс SOCKS (пусто — автоматически)",
|
||||
"ZH": "SOCKS 绑定接口(留空自动选择)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Maximum Sessions": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Максимум сеансов",
|
||||
"ZH": "最大会话数",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Interface and Stack": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Интерфейс и сетевой стек",
|
||||
"ZH": "接口与协议栈",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Device Name (empty for automatic)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Имя устройства (пусто — автоматически)",
|
||||
"ZH": "设备名称(留空自动选择)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"MTU (bytes)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "MTU (байты)",
|
||||
"ZH": "MTU(字节)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"IPv4 Address Prefixes (comma separated)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Префиксы IPv4 (через запятую)",
|
||||
"ZH": "IPv4 地址前缀(以逗号分隔)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"IPv6 Address Prefixes (comma separated)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Префиксы IPv6 (через запятую)",
|
||||
"ZH": "IPv6 地址前缀(以逗号分隔)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Native UDP Timeout (Go duration, e.g. 1m)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Тайм-аут UDP стека (длительность Go, например 1m)",
|
||||
"ZH": "原生 UDP 超时(Go 时长,例如 1m)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Native ICMP Timeout (Go duration, 0 for default)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Тайм-аут ICMP стека (длительность Go, 0 — по умолчанию)",
|
||||
"ZH": "原生 ICMP 超时(Go 时长,0 使用默认值)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Native UDP NAT Limit (0 for default)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Лимит UDP NAT стека (0 — по умолчанию)",
|
||||
"ZH": "原生 UDP NAT 上限(0 使用默认值)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Advanced Options": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Дополнительные параметры",
|
||||
"ZH": "高级选项",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Use exact native JSON field names. Supported: route address/exclusion lists, StrictRoute, InterfaceScope, ForwarderBindInterface and IncludeAllNetworks. AutoRoute, external configuration and DNS ownership are managed. Unsupported fields are rejected.": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Используйте точные имена полей JSON. Поддерживаются списки маршрутов и исключений, StrictRoute, InterfaceScope, ForwarderBindInterface и IncludeAllNetworks. AutoRoute, внешняя настройка и управление DNS контролируются приложением. Неподдерживаемые поля отклоняются.",
|
||||
"ZH": "请使用准确的原生 JSON 字段名。支持路由地址及排除列表、StrictRoute、InterfaceScope、ForwarderBindInterface 和 IncludeAllNetworks。AutoRoute、外部配置模式和 DNS 归属由应用管理。不支持的字段会被拒绝。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Connect Timeout (seconds)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Тайм-аут подключения (секунды)",
|
||||
"ZH": "连接超时(秒)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"SOCKS UDP Timeout (seconds)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Тайм-аут UDP SOCKS (секунды)",
|
||||
"ZH": "SOCKS UDP 超时(秒)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"TCP Idle Timeout (seconds)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Тайм-аут простоя TCP (секунды)",
|
||||
"ZH": "TCP 空闲超时(秒)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"UDP Mapping": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Сопоставление UDP",
|
||||
"ZH": "UDP 映射方式",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"UDP Filtering": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Фильтрация UDP",
|
||||
"ZH": "UDP 过滤方式",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Automatic": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Автоматический выбор",
|
||||
"ZH": "自动选择",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Endpoint Independent": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Независимо от конечной точки",
|
||||
"ZH": "与端点无关",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Address Dependent": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Зависит от адреса",
|
||||
"ZH": "依赖地址",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Address and Port Dependent": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Зависит от адреса и порта",
|
||||
"ZH": "依赖地址和端口",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"TUN remote address is required when application TUN is enabled": {
|
||||
"source": [
|
||||
"Furious.Backends.ExternalCore.Editor"
|
||||
],
|
||||
"RU": "При включённом TUN приложения требуется адрес удалённого сервера",
|
||||
"ZH": "启用应用程序 TUN 时必须填写 TUN 远程地址",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Use Application TUN": {
|
||||
"source": [
|
||||
"Furious.Backends.ExternalCore.Editor"
|
||||
],
|
||||
"RU": "Использовать TUN приложения",
|
||||
"ZH": "使用应用程序 TUN",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Configure sing-tun interface, stack, SOCKS transit and host settings.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Настройте интерфейс, стек, SOCKS-передачу и параметры хоста sing-tun.",
|
||||
"ZH": "配置 sing-tun 的接口、协议栈、SOCKS 转发和主机设置。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Host Settings": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Настройки хоста",
|
||||
"ZH": "主机设置",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"TCP Congestion Control (Go stack)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog"
|
||||
],
|
||||
"RU": "Управление перегрузкой TCP (стек Go)",
|
||||
"ZH": "TCP 拥塞控制(Go 协议栈)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"TUN Adapter Interface DNS": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog",
|
||||
"Furious.Window.TunSettingsDialog"
|
||||
],
|
||||
"RU": "DNS-адрес интерфейса адаптера TUN",
|
||||
"ZH": "TUN适配器接口DNS",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Bypass TUN Adapter Interface IP (separated by commas)": {
|
||||
"source": [
|
||||
"Furious.Window.SingTUNSettingsDialog",
|
||||
"Furious.Window.TunSettingsDialog"
|
||||
],
|
||||
"RU": "IP-адреса в обход интерфейса TUN (через запятую)",
|
||||
"ZH": "绕过TUN适配器接口的IP地址(以逗号分隔)",
|
||||
"isReviewed": "True"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ from Furious.Window.ProxyBypassDialog import *
|
||||
from Furious.Window.QRCodeWindow import QRCodeWindow
|
||||
from Furious.Window.TextEditorWindow import TextEditorWindow
|
||||
from Furious.Window.TunSettingsDialog import *
|
||||
from Furious.Window.SingTUNSettingsDialog import SingTUNSettingsDialog
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
@@ -1130,6 +1131,10 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
self.updatesManager.configureHttpProxy(Storage.Extras.UserHttpProxy())
|
||||
self.updatesManager.checkForUpdates(**kwargs)
|
||||
|
||||
def getGuiSingTUNSettings(self, **kwargs):
|
||||
"""Transfer a fresh managed transient editor to the Settings page."""
|
||||
return SingTUNSettingsDialog(parent=kwargs.pop('parent', self), **kwargs)
|
||||
|
||||
def resetNetworkState(self):
|
||||
"""Reset network state."""
|
||||
self.networkState.clearStatus()
|
||||
|
||||
@@ -94,6 +94,7 @@ class MainWindow(AppQMainWindow):
|
||||
self.logPage = AppLogPage()
|
||||
self.settingsPage = SettingsPage(
|
||||
tunSettingsDialogFactory=self.homePage.getGuiTUNSettings,
|
||||
singTunSettingsDialogFactory=self.homePage.getGuiSingTUNSettings,
|
||||
proxyBypassDialog=self.homePage.customizeProxyBypassDialog,
|
||||
networkTestDialog=self.homePage.customizeNetworkTestDialog,
|
||||
checkForUpdates=self.homePage.checkForUpdates,
|
||||
|
||||
@@ -39,6 +39,7 @@ from Furious.Service.TrafficStatsManager import (
|
||||
METRICS_COLLECTION_SETTING,
|
||||
)
|
||||
from Furious.Window.TunSettingsDialog import tunModeTitle
|
||||
from Furious.Qt.Signals import connectWeakly
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import *
|
||||
@@ -555,6 +556,44 @@ class _SystemProxySettingsCard(_SettingsCard):
|
||||
AppSettingsController().setSystemProxyMode(mode)
|
||||
|
||||
|
||||
class _TUNBackendSettingsCard(_SettingsCard):
|
||||
"""Select an application fallback without changing native-core policy."""
|
||||
|
||||
def __init__(self):
|
||||
self.comboBox = AppQComboBox()
|
||||
self.comboBox.setObjectName('SettingsComboBox')
|
||||
self.comboBox.addItem('sing-tun', 'sing-tun')
|
||||
self.comboBox.addItem('tun2socks', 'tun2socks')
|
||||
|
||||
super().__init__(
|
||||
'diagram-3.svg',
|
||||
self.comboBox,
|
||||
_('Application TUN Engine'),
|
||||
_(
|
||||
'Native proxy-core TUN takes priority. This engine is used when Furious provides application TUN. Changes apply to the next connection.'
|
||||
),
|
||||
)
|
||||
|
||||
self.sync()
|
||||
|
||||
connectWeakly(self.comboBox.currentIndexChanged, self, '_selectionChanged')
|
||||
connectWeakly(AppSettingsController().tunBackendChanged, self, 'sync')
|
||||
|
||||
def sync(self, backend=None):
|
||||
blocker = QtCore.QSignalBlocker(self.comboBox)
|
||||
value = (
|
||||
backend
|
||||
if isinstance(backend, str)
|
||||
else AppSettings.get(APPLICATION_TUN_BACKEND)
|
||||
)
|
||||
|
||||
self.comboBox.setCurrentIndex(max(self.comboBox.findData(value), 0))
|
||||
del blocker
|
||||
|
||||
def _selectionChanged(self, _index):
|
||||
AppSettingsController().setTUNBackend(self.comboBox.currentData())
|
||||
|
||||
|
||||
class _SettingsSection(QWidget):
|
||||
"""Group a translated heading and a stack of settings cards."""
|
||||
|
||||
@@ -601,6 +640,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
self,
|
||||
*,
|
||||
tunSettingsDialogFactory,
|
||||
singTunSettingsDialogFactory=None,
|
||||
proxyBypassDialog,
|
||||
networkTestDialog,
|
||||
checkForUpdates,
|
||||
@@ -613,6 +653,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName('SettingsPage')
|
||||
self._singTunSettingsDialogFactory = singTunSettingsDialogFactory
|
||||
|
||||
(
|
||||
self._tunSettingsDialogFactory,
|
||||
@@ -636,6 +677,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
self.pageTitleLabel.setObjectName('SettingsPageTitle')
|
||||
|
||||
self.generalSection = _SettingsSection(_('General'))
|
||||
self.tunSection = _SettingsSection('TUN', translatable=False)
|
||||
self.connectionSection = _SettingsSection(_('Connection and Interface'))
|
||||
self.applicationSection = _SettingsSection(_('Application'))
|
||||
self.pluginSettingsTitleLabel = AppQLabel(_('Plugin Settings'))
|
||||
@@ -644,6 +686,14 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
self._pluginActions = []
|
||||
|
||||
self._tunModeAvailable = AppSettingsController().tunModeAvailable()
|
||||
self.tunBackendCard = _TUNBackendSettingsCard()
|
||||
self.singTunSettingsCard = _ActionSettingsCard(
|
||||
None,
|
||||
self._openSingTUNSettings,
|
||||
_('Customize sing-tun Settings...'),
|
||||
_('Configure sing-tun interface, stack, SOCKS transit and host settings.'),
|
||||
_('Open'),
|
||||
)
|
||||
|
||||
self.tunModeCard = _ToggleSettingsCard(
|
||||
'shield-check.svg',
|
||||
@@ -729,7 +779,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
self.editorWhitespaceCard,
|
||||
) = (
|
||||
_ActionSettingsCard(
|
||||
'diagram-3.svg',
|
||||
None,
|
||||
self._openTUNSettings,
|
||||
_('Customize Tun2socks Settings...'),
|
||||
_('Configure the external Tun2socks network interface and routing.'),
|
||||
@@ -802,8 +852,10 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
self.systemProxyCard.sync
|
||||
)
|
||||
|
||||
if not SystemRuntime.flatpakID():
|
||||
self.connectionSection.addCard(self.tunSettingsCard)
|
||||
self.tunSection.addCard(self.tunBackendCard)
|
||||
self.tunSection.addCard(self.singTunSettingsCard)
|
||||
self.tunSection.addCard(self.tunSettingsCard)
|
||||
self.tunSection.setVisible(not SystemRuntime.flatpakID())
|
||||
|
||||
self.connectionSection.addCard(self.systemProxyCard)
|
||||
self.connectionSection.addCard(self.proxyBypassCard)
|
||||
@@ -884,6 +936,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
contentLayout.setSpacing(22)
|
||||
contentLayout.addWidget(self.pageTitleLabel)
|
||||
contentLayout.addWidget(self.generalSection)
|
||||
contentLayout.addWidget(self.tunSection)
|
||||
contentLayout.addWidget(self.connectionSection)
|
||||
|
||||
if self.pluginSections:
|
||||
@@ -1124,10 +1177,19 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
"""Show the end-user disclosure for the active endpoint providers."""
|
||||
_endpointPrivacyMessageBox(self).open()
|
||||
|
||||
def _openSingTUNSettings(self):
|
||||
if self._singTunSettingsDialogFactory is not None:
|
||||
self._singTunSettingsDialogFactory(parent=self).open()
|
||||
|
||||
def setConnectionControlsEnabled(self, enabled: bool):
|
||||
"""Disable connection-sensitive settings during a transition."""
|
||||
self.tunModeCard.setEnabled(bool(enabled) and self._tunModeAvailable)
|
||||
self.systemProxyCard.comboBox.setEnabled(bool(enabled))
|
||||
self.tunBackendCard.setEnabled(bool(enabled) and not SystemRuntime.flatpakID())
|
||||
self.tunSettingsCard.setEnabled(bool(enabled))
|
||||
self.singTunSettingsCard.setEnabled(
|
||||
bool(enabled) and self._singTunSettingsDialogFactory is not None
|
||||
)
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Synchronize controls in case a legacy action changed a setting."""
|
||||
@@ -1135,6 +1197,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
|
||||
for section in (
|
||||
self.generalSection,
|
||||
self.tunSection,
|
||||
self.connectionSection,
|
||||
self.applicationSection,
|
||||
*self.pluginSections,
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""A staged, transient editor for sing-tun's native and host configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Models.SingTUN import (
|
||||
SING_TUN_DEFAULTS,
|
||||
SING_TUN_HOST_DEFAULTS,
|
||||
prepareSingTUNSettings,
|
||||
)
|
||||
from Furious.Repository import Storage
|
||||
from Furious.Frozenlib import PLATFORM, GOLDEN_RATIO, AppFontName
|
||||
from Furious.Qt import (
|
||||
AppQTransientDialog,
|
||||
AppQLabel,
|
||||
AppQLineEdit,
|
||||
AppQComboBox,
|
||||
AppQSpinBox,
|
||||
AppQSwitch,
|
||||
AppQTabWidget,
|
||||
AppQDialogButtonBox,
|
||||
DraculaJSONTextEditor,
|
||||
)
|
||||
from Furious.Qt.Signals import connectWeakly
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import (
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QScrollArea,
|
||||
)
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
|
||||
class SingTUNSettingsDialog(AppQTransientDialog):
|
||||
"""Never import the native engine or mutate storage while loading widgets."""
|
||||
|
||||
FIXED_DIALOG_SIZE = (
|
||||
QtCore.QSize(int(690 * GOLDEN_RATIO), 690)
|
||||
if PLATFORM in ('Darwin', 'Linux')
|
||||
else QtCore.QSize(int(620 * GOLDEN_RATIO), 620)
|
||||
)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle(_('Customize sing-tun Settings'))
|
||||
self._original = copy.deepcopy(Storage.UserSingTUNSettings())
|
||||
self.fields = {}
|
||||
|
||||
self.tabs = AppQTabWidget(self)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
common = copy.deepcopy(SING_TUN_DEFAULTS)
|
||||
|
||||
for key, value in self._original.items():
|
||||
if key in ('tun_options', 'stack_options', 'host_options') and isinstance(
|
||||
value, dict
|
||||
):
|
||||
common[key].update(value)
|
||||
elif key in common:
|
||||
common[key] = value
|
||||
|
||||
bridge = self._page(_('SOCKS Transit'))
|
||||
|
||||
self._choice(
|
||||
bridge,
|
||||
'stack',
|
||||
_('Stack'),
|
||||
[
|
||||
('go', 'go'),
|
||||
('gvisor', 'gvisor'),
|
||||
('system', 'system'),
|
||||
('mixed', 'mixed'),
|
||||
(_('Automatic'), ''),
|
||||
],
|
||||
common.get('stack'),
|
||||
)
|
||||
self._choice(
|
||||
bridge,
|
||||
'log_level',
|
||||
_('Log Level'),
|
||||
[
|
||||
(value, value)
|
||||
for value in ('trace', 'debug', 'info', 'warn', 'error', 'silent')
|
||||
],
|
||||
common.get('log_level'),
|
||||
)
|
||||
self._text(
|
||||
bridge,
|
||||
'network_interface',
|
||||
_('SOCKS Interface Binding (empty for automatic)'),
|
||||
common.get('network_interface'),
|
||||
)
|
||||
self._number(
|
||||
bridge,
|
||||
'max_sessions',
|
||||
_('Maximum Sessions'),
|
||||
common.get('max_sessions'),
|
||||
1,
|
||||
16384,
|
||||
)
|
||||
|
||||
for key, label in (
|
||||
('connect_timeout', _('Connect Timeout (seconds)')),
|
||||
('udp_timeout', _('SOCKS UDP Timeout (seconds)')),
|
||||
('tcp_idle_timeout', _('TCP Idle Timeout (seconds)')),
|
||||
):
|
||||
self._text(bridge, key, label, common.get(key))
|
||||
|
||||
native = self._page(_('Interface and Stack'))
|
||||
tun = (
|
||||
common['tun_options']
|
||||
if isinstance(common.get('tun_options'), dict)
|
||||
else SING_TUN_DEFAULTS['tun_options']
|
||||
)
|
||||
|
||||
self._text(
|
||||
native, 'Name', _('Device Name (empty for automatic)'), tun.get('Name', '')
|
||||
)
|
||||
self._number(native, 'MTU', _('MTU (bytes)'), tun.get('MTU', 1500), 68, 65535)
|
||||
self._text(
|
||||
native,
|
||||
'Inet4Address',
|
||||
_('IPv4 Address Prefixes (comma separated)'),
|
||||
(
|
||||
', '.join(str(value) for value in tun.get('Inet4Address', []))
|
||||
if isinstance(tun.get('Inet4Address'), list)
|
||||
else ''
|
||||
),
|
||||
)
|
||||
self._text(
|
||||
native,
|
||||
'Inet6Address',
|
||||
_('IPv6 Address Prefixes (comma separated)'),
|
||||
(
|
||||
', '.join(str(value) for value in tun.get('Inet6Address', []))
|
||||
if isinstance(tun.get('Inet6Address'), list)
|
||||
else ''
|
||||
),
|
||||
)
|
||||
|
||||
stack = (
|
||||
common['stack_options']
|
||||
if isinstance(common.get('stack_options'), dict)
|
||||
else {}
|
||||
)
|
||||
|
||||
self._choice(
|
||||
native,
|
||||
'TCPCongestionControl',
|
||||
_('TCP Congestion Control (Go stack)'),
|
||||
[
|
||||
(_('Automatic'), ''),
|
||||
('cubic', 'cubic'),
|
||||
('reno', 'reno'),
|
||||
('bbr', 'bbr'),
|
||||
],
|
||||
stack.get('TCPCongestionControl', ''),
|
||||
)
|
||||
self._text(
|
||||
native,
|
||||
'UDPTimeout',
|
||||
_('Native UDP Timeout (Go duration, e.g. 1m)'),
|
||||
stack.get('UDPTimeout', '1m'),
|
||||
)
|
||||
self._text(
|
||||
native,
|
||||
'ICMPTimeout',
|
||||
_('Native ICMP Timeout (Go duration, 0 for default)'),
|
||||
stack.get('ICMPTimeout', '0'),
|
||||
)
|
||||
self._text(
|
||||
native,
|
||||
'UDPNATMax',
|
||||
_('Native UDP NAT Limit (0 for default)'),
|
||||
stack.get('UDPNATMax', 0),
|
||||
)
|
||||
for key, label in (
|
||||
('UDPMapping', _('UDP Mapping')),
|
||||
('UDPFiltering', _('UDP Filtering')),
|
||||
):
|
||||
self._choice(
|
||||
native,
|
||||
key,
|
||||
label,
|
||||
[
|
||||
(_('Endpoint Independent'), 0),
|
||||
(_('Address Dependent'), 1),
|
||||
(_('Address and Port Dependent'), 2),
|
||||
],
|
||||
stack.get(key, 0),
|
||||
)
|
||||
|
||||
host = (
|
||||
common['host_options']
|
||||
if isinstance(common.get('host_options'), dict)
|
||||
else SING_TUN_HOST_DEFAULTS
|
||||
)
|
||||
hostForm = self._page(_('Host Settings'))
|
||||
|
||||
for key, label in (
|
||||
('primaryAdapterInterfaceName', _('Primary Adapter Interface Name')),
|
||||
('primaryAdapterInterfaceIP', _('Primary Adapter Interface IP')),
|
||||
('tunAdapterInterfaceDNS', _('TUN Adapter Interface DNS')),
|
||||
(
|
||||
'bypassTUNAdapterInterfaceIP',
|
||||
_('Bypass TUN Adapter Interface IP (separated by commas)'),
|
||||
),
|
||||
):
|
||||
self._text(hostForm, key, label, host.get(key, ''))
|
||||
|
||||
self.disableDNS = AppQSwitch()
|
||||
self.disableDNS.setChecked(
|
||||
host.get('disablePrimaryAdapterInterfaceDNS', True) is True
|
||||
)
|
||||
|
||||
disableDNSRow = QHBoxLayout()
|
||||
disableDNSRow.setContentsMargins(0, 0, 0, 0)
|
||||
disableDNSRow.addWidget(
|
||||
AppQLabel(
|
||||
_(
|
||||
'Disable Primary Adapter Interface DNS (Mitigating DNS leaks on Windows)'
|
||||
)
|
||||
)
|
||||
)
|
||||
disableDNSRow.addWidget(self.disableDNS)
|
||||
disableDNSRow.addStretch()
|
||||
hostForm.addRow(disableDNSRow)
|
||||
|
||||
advanced = self._page(_('Advanced Options'))
|
||||
|
||||
self.advanced = DraculaJSONTextEditor(fontFamily=AppFontName())
|
||||
self.advanced.setLineWrapMode(DraculaJSONTextEditor.LineWrapMode.NoWrap)
|
||||
|
||||
extra = copy.deepcopy(self._original)
|
||||
|
||||
for key in SING_TUN_DEFAULTS:
|
||||
if key not in ('tun_options', 'stack_options', 'host_options'):
|
||||
extra.pop(key, None)
|
||||
|
||||
for key, represented in (
|
||||
('tun_options', ('Name', 'MTU', 'Inet4Address', 'Inet6Address')),
|
||||
('host_options', tuple(SING_TUN_HOST_DEFAULTS)),
|
||||
(
|
||||
'stack_options',
|
||||
(
|
||||
'UDPTimeout',
|
||||
'ICMPTimeout',
|
||||
'UDPNATMax',
|
||||
'UDPMapping',
|
||||
'UDPFiltering',
|
||||
'TCPCongestionControl',
|
||||
),
|
||||
),
|
||||
):
|
||||
if isinstance(extra.get(key), dict):
|
||||
for field in represented:
|
||||
extra[key].pop(field, None)
|
||||
|
||||
self.advanced.setPlainText(json.dumps(extra, indent=2, ensure_ascii=False))
|
||||
|
||||
advanced.addRow(self.advanced)
|
||||
instructions = AppQLabel(
|
||||
_(
|
||||
'Use exact native JSON field names. Supported: route address/exclusion lists, StrictRoute, InterfaceScope, ForwarderBindInterface and IncludeAllNetworks. AutoRoute, external configuration and DNS ownership are managed. Unsupported fields are rejected.'
|
||||
)
|
||||
)
|
||||
instructions.setWordWrap(True)
|
||||
advanced.addRow(instructions)
|
||||
|
||||
self.errorLabel = AppQLabel('')
|
||||
self.errorLabel.setWordWrap(True)
|
||||
|
||||
layout.addWidget(self.errorLabel)
|
||||
|
||||
buttons = AppQDialogButtonBox(
|
||||
AppQDialogButtonBox.StandardButton.Ok
|
||||
| AppQDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
connectWeakly(buttons.accepted, self, 'accept')
|
||||
connectWeakly(buttons.rejected, self, 'reject')
|
||||
|
||||
layout.addWidget(buttons)
|
||||
|
||||
def _page(self, title):
|
||||
page = QWidget()
|
||||
|
||||
form = QFormLayout(page)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(page)
|
||||
|
||||
self.tabs.addTab(scroll, title)
|
||||
|
||||
return form
|
||||
|
||||
def _text(self, form, key, label, value):
|
||||
widget = AppQLineEdit()
|
||||
widget.setText(str(value) if value is not None else '')
|
||||
|
||||
self.fields[key] = widget
|
||||
|
||||
form.addRow(AppQLabel(label), widget)
|
||||
|
||||
def _number(self, form, key, label, value, minimum, maximum):
|
||||
widget = AppQSpinBox()
|
||||
widget.setRange(minimum, maximum)
|
||||
widget.setValue(value if type(value) is int else minimum)
|
||||
|
||||
self.fields[key] = widget
|
||||
|
||||
form.addRow(AppQLabel(label), widget)
|
||||
|
||||
def _choice(self, form, key, label, choices, value):
|
||||
widget = AppQComboBox()
|
||||
|
||||
for text, data in choices:
|
||||
widget.addItem(text, data)
|
||||
|
||||
widget.setCurrentIndex(max(widget.findData(value), 0))
|
||||
|
||||
self.fields[key] = widget
|
||||
|
||||
form.addRow(AppQLabel(label), widget)
|
||||
|
||||
def document(self):
|
||||
"""Prepare sing-tun's complete candidate before its live collection changes."""
|
||||
document = json.loads(self.advanced.toPlainText())
|
||||
|
||||
if not isinstance(document, dict):
|
||||
raise ValueError('Advanced options must be a JSON object')
|
||||
|
||||
tun = document.setdefault('tun_options', {})
|
||||
stack = document.setdefault('stack_options', {})
|
||||
|
||||
if not isinstance(tun, dict) or not isinstance(stack, dict):
|
||||
raise ValueError('Native options must be JSON objects')
|
||||
|
||||
for key in ('stack', 'log_level'):
|
||||
document[key] = self.fields[key].currentData()
|
||||
|
||||
document['network_interface'] = self.fields['network_interface'].text().strip()
|
||||
document['max_sessions'] = self.fields['max_sessions'].value()
|
||||
|
||||
for key in ('connect_timeout', 'udp_timeout', 'tcp_idle_timeout'):
|
||||
document[key] = float(self.fields[key].text())
|
||||
|
||||
tun['Name'], tun['MTU'] = (
|
||||
self.fields['Name'].text().strip(),
|
||||
self.fields['MTU'].value(),
|
||||
)
|
||||
|
||||
for key in ('Inet4Address', 'Inet6Address'):
|
||||
tun[key] = [
|
||||
value.strip()
|
||||
for value in self.fields[key].text().split(',')
|
||||
if value.strip()
|
||||
]
|
||||
|
||||
for key in ('UDPTimeout', 'ICMPTimeout'):
|
||||
value = self.fields[key].text().strip()
|
||||
stack[key] = int(value) if value.isdigit() else value
|
||||
|
||||
stack['UDPNATMax'] = int(self.fields['UDPNATMax'].text())
|
||||
|
||||
for key in ('UDPMapping', 'UDPFiltering', 'TCPCongestionControl'):
|
||||
stack[key] = self.fields[key].currentData()
|
||||
|
||||
host = document.setdefault('host_options', {})
|
||||
|
||||
if not isinstance(host, dict):
|
||||
raise ValueError('sing-tun host options must be a JSON object')
|
||||
|
||||
for key in (
|
||||
'primaryAdapterInterfaceName',
|
||||
'primaryAdapterInterfaceIP',
|
||||
'tunAdapterInterfaceDNS',
|
||||
'bypassTUNAdapterInterfaceIP',
|
||||
):
|
||||
host[key] = self.fields[key].text().strip()
|
||||
|
||||
host['disablePrimaryAdapterInterfaceDNS'] = self.disableDNS.isChecked()
|
||||
|
||||
prepareSingTUNSettings(document, platform=PLATFORM)
|
||||
|
||||
return document
|
||||
|
||||
def accept(self):
|
||||
try:
|
||||
document = self.document()
|
||||
|
||||
Storage.replaceSingTUNSettings(document)
|
||||
except (ValueError, TypeError, OSError) as ex:
|
||||
self.errorLabel.setText(str(ex))
|
||||
|
||||
return
|
||||
|
||||
super().accept()
|
||||
@@ -264,11 +264,11 @@ class GuiTUNSettingsGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
key='defaultPrimaryGatewayIP',
|
||||
),
|
||||
GuiTUNSettingsItemXXX(
|
||||
title=_('Tun2socks Adapter Interface DNS'),
|
||||
title=_('TUN Adapter Interface DNS'),
|
||||
key='tunAdapterInterfaceDNS',
|
||||
),
|
||||
GuiTUNSettingsItemXXX(
|
||||
title=_('Bypass Tun2socks Adapter Interface IP (separated by commas)'),
|
||||
title=_('Bypass TUN Adapter Interface IP (separated by commas)'),
|
||||
key='bypassTUNAdapterInterfaceIP',
|
||||
),
|
||||
GuiTUNSettingsItemSwitchXXX(
|
||||
|
||||
+2
-1
@@ -94,6 +94,7 @@ worker. Choose tests by the changed contract rather than by filename alone.
|
||||
| [test_external_core.py](test_external_core.py) | Harmless real process launch/output/shutdown, partial thread-start rollback, non-finite timeout rejection, failed reap/thread-join retry, readiness/TUN metadata, Windows paths with spaces, subscription rejection of executable profiles, bounded DNS references. |
|
||||
| [test_frozenlib.py](test_frozenlib.py) | Nested state guards, cleanup isolation, bounded caches/throttling, dual-stack probe selection, mocked proxy/DNS/routes/startup/session boundaries and failure handling. |
|
||||
| [test_native_tun_semantics.py](test_native_tun_semantics.py) | Xray/Hysteria2 runtime-copy TUN preservation/replacement, managed-TUN failures, download-test stripping, prevention of a second tun2socks owner. |
|
||||
| [test_sing_tun.py](test_sing_tun.py) | Engine choice/Go default, development-stack and congestion-control compatibility, independent backend settings and host defaults, selected-only snapshots, captured TUN log sources in both startup paths, staged settings and malformed storage, SOCKS authentication/UDP, harmless real spawn/status/stop, native readiness, cancellation, synchronous compatibility, mocked DNS/routing ownership including development IPv6 rule ordering and refused cleanup. |
|
||||
| [test_subscription_sync.py](test_subscription_sync.py) | Group-local preparation/commit, stable duplicate identity, atomic failure, preservation of newer local metadata, rejection of changed source state. |
|
||||
| [test_subscription_manager.py](test_subscription_manager.py) | Stable request generations, provider metadata, batch/partial failures, timer policy, targeted updates, real Stop Updates input, worker responsiveness, cancellation and synchronous shutdown ownership. |
|
||||
| [test_subscription_scalability.py](test_subscription_scalability.py) | Deterministic 1/3/8-group preparation and commit with 1,500 profiles per group and bounded workers; uses the offline benchmark helper. |
|
||||
@@ -140,7 +141,7 @@ regular cases run by default while its heavy class remains gated.
|
||||
python -m unittest tests.test_interface tests.test_models_and_services tests.test_repository_contracts tests.test_plugin_architecture tests.test_public_api tests.test_hysteria1_protocol tests.test_hysteria2_compatibility tests.test_backend_editor_contract tests.test_shadowsocks_uri tests.test_socks_uri -v
|
||||
|
||||
# Application, connection, runtime, and mocked host boundaries
|
||||
python -m unittest tests.test_application_process tests.test_architecture_refactors tests.test_connection_startup_async tests.test_controllers tests.test_runtime_lifecycle tests.test_external_core tests.test_frozenlib tests.test_native_tun_semantics -v
|
||||
python -m unittest tests.test_application_process tests.test_architecture_refactors tests.test_connection_startup_async tests.test_controllers tests.test_runtime_lifecycle tests.test_external_core tests.test_frozenlib tests.test_native_tun_semantics tests.test_sing_tun -v
|
||||
|
||||
# Subscription, profile tests, HTTP/assets, endpoint information, metrics, and logs
|
||||
python -m unittest tests.test_subscription_sync tests.test_subscription_manager tests.test_subscription_scalability tests.test_profile_test_jobs tests.test_service_runtime tests.test_xray_asset_download tests.test_endpoint_info tests.test_metrics_behavior tests.test_log_manager_generation tests.test_log_manager_benchmark -v
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Harmless source/Nuitka probe for the new transient settings editor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Qt import AppQDialog
|
||||
from Furious.Repository import Storage
|
||||
from Furious.Window.SingTUNSettingsDialog import SingTUNSettingsDialog
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from shiboken6 import isValid
|
||||
|
||||
from tests.support import application, isolatedSettings, processQtEvents
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import gc
|
||||
import json
|
||||
import sys
|
||||
import weakref
|
||||
import importlib
|
||||
|
||||
|
||||
def run(iterations, method):
|
||||
application()
|
||||
destroyed = []
|
||||
references = []
|
||||
errors = []
|
||||
|
||||
originalHook = sys.excepthook
|
||||
sys.excepthook = lambda kind, error, traceback: errors.append(
|
||||
(kind.__name__, str(error))
|
||||
)
|
||||
singGetter, replace = (
|
||||
Storage.UserSingTUNSettings,
|
||||
Storage.replaceSingTUNSettings,
|
||||
)
|
||||
sing = {}
|
||||
|
||||
Storage.UserSingTUNSettings = staticmethod(lambda: copy.deepcopy(sing))
|
||||
Storage.replaceSingTUNSettings = staticmethod(lambda document: None)
|
||||
baseline = len(AppQDialog._openDialogs)
|
||||
|
||||
try:
|
||||
with isolatedSettings():
|
||||
for index in range(iterations):
|
||||
owner = QWidget()
|
||||
dialog = SingTUNSettingsDialog(owner)
|
||||
dialog.destroyed.connect(lambda *_args: destroyed.append(True))
|
||||
references.append(weakref.ref(dialog))
|
||||
|
||||
dialog.open()
|
||||
if method == 'owner':
|
||||
owner.deleteLater()
|
||||
else:
|
||||
getattr(dialog, method)()
|
||||
processQtEvents()
|
||||
|
||||
assert not isValid(dialog), index
|
||||
|
||||
if isValid(owner):
|
||||
owner.deleteLater()
|
||||
processQtEvents()
|
||||
del dialog, owner
|
||||
|
||||
gc.collect()
|
||||
processQtEvents()
|
||||
|
||||
assert len(destroyed) == iterations
|
||||
assert all(reference() is None for reference in references)
|
||||
assert len(AppQDialog._openDialogs) == baseline
|
||||
assert not errors, errors
|
||||
finally:
|
||||
sys.excepthook = originalHook
|
||||
Storage.UserSingTUNSettings = staticmethod(singGetter)
|
||||
Storage.replaceSingTUNSettings = staticmethod(replace)
|
||||
|
||||
return {
|
||||
'method': method,
|
||||
'destroyed': len(destroyed),
|
||||
'wrapperReleased': iterations,
|
||||
'callbackErrors': len(errors),
|
||||
'compiledDialog': hasattr(
|
||||
importlib.import_module('Furious.Window.SingTUNSettingsDialog'),
|
||||
'__compiled__',
|
||||
),
|
||||
'compiledSignals': hasattr(
|
||||
importlib.import_module('Furious.Qt.Signals'), '__compiled__'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--iterations', type=int, default=20)
|
||||
parser.add_argument(
|
||||
'--method', choices=('accept', 'reject', 'close', 'owner'), default='reject'
|
||||
)
|
||||
arguments = parser.parse_args()
|
||||
print(json.dumps(run(arguments.iterations, arguments.method)))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -426,6 +426,7 @@ class SettingsPageOrganizationTest(unittest.TestCase):
|
||||
)
|
||||
page = SettingsPage(
|
||||
tunSettingsDialogFactory=mock.Mock(),
|
||||
singTunSettingsDialogFactory=mock.Mock(),
|
||||
proxyBypassDialog=proxyBypassDialog,
|
||||
networkTestDialog=networkTestDialog,
|
||||
**callbacks,
|
||||
@@ -449,6 +450,20 @@ class SettingsPageOrganizationTest(unittest.TestCase):
|
||||
]
|
||||
|
||||
self.assertEqual(page.generalSection.cards, expectedGeneralCards)
|
||||
expectedTUNCards = [
|
||||
page.tunBackendCard,
|
||||
page.singTunSettingsCard,
|
||||
page.tunSettingsCard,
|
||||
]
|
||||
self.assertEqual(page.tunSection.titleLabel.text(), 'TUN')
|
||||
self.assertEqual(page.tunSection.cards, expectedTUNCards)
|
||||
self.assertFalse(page.tunSection.isHidden())
|
||||
self.assertTrue(
|
||||
all(card.parent() is page.tunSection for card in expectedTUNCards)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(card not in page.connectionSection.cards for card in expectedTUNCards)
|
||||
)
|
||||
self.assertEqual(
|
||||
page.applicationSection.cards,
|
||||
[page.updateCard, page.aboutCard],
|
||||
@@ -473,11 +488,17 @@ class SettingsPageOrganizationTest(unittest.TestCase):
|
||||
|
||||
page.restartCard.button.click()
|
||||
page.openFolderCard.button.click()
|
||||
page.tunSettingsCard.button.click()
|
||||
page.singTunSettingsCard.button.click()
|
||||
|
||||
callbacks['restartAsAdmin'].assert_called_once_with()
|
||||
callbacks['openApplicationFolder'].assert_called_once_with()
|
||||
callbacks['checkForUpdates'].assert_not_called()
|
||||
callbacks['openAboutPage'].assert_not_called()
|
||||
page._tunSettingsDialogFactory.assert_called_once_with(parent=page)
|
||||
page._tunSettingsDialogFactory.return_value.open.assert_called_once_with()
|
||||
page._singTunSettingsDialogFactory.assert_called_once_with(parent=page)
|
||||
page._singTunSettingsDialogFactory.return_value.open.assert_called_once_with()
|
||||
|
||||
page.close()
|
||||
page.deleteLater()
|
||||
@@ -494,6 +515,13 @@ class SettingsPageOrganizationTest(unittest.TestCase):
|
||||
|
||||
self.assertIn(page.tunModeCard, page.generalSection.cards)
|
||||
self.assertFalse(page.tunModeCard.checkBox.isEnabled())
|
||||
self.assertTrue(page.tunSection.isHidden())
|
||||
self.assertTrue(
|
||||
all(
|
||||
card.parent() is page.tunSection
|
||||
for card in page.tunSection.cards
|
||||
)
|
||||
)
|
||||
|
||||
page.setConnectionControlsEnabled(True)
|
||||
self.assertFalse(page.tunModeCard.checkBox.isEnabled())
|
||||
|
||||
Reference in New Issue
Block a user