跨平台 GUI 客户端配置与生命周期
基于 Fyne 的 client-ui 把代理监听、远端连接和 USTB VPN 认证设置汇集在窗口中,并通过 extra.TaskHandles 启停客户端。本页以用户可见配置、偏好保存及启动/停止过程为边界。
用途与范围
适合需要理解 GUI 如何把表单值传入客户端、配置何时持久化,以及窗口关闭时如何清理任务的开发者。这里只讨论跨平台 client-ui;命令行客户端、macOS 原生 SwiftUI 客户端和 VPN 协议/认证服务端的实现属于相邻主题,不在本页展开。VPN 认证窗口与二维码交互仅解释其 GUI 接口;底层 VPN 登录和 extra.TaskHandles 内部实现不在已读取的源码范围内。
概述
入口 main 以 AppId 创建 Fyne 应用、设置主题并建立主窗口;预填 SOCKS5 地址 127.0.0.1:1080、HTTP 地址 127.0.0.1:1086,然后从 fyne.Preferences 恢复基本设置。主表单含远端地址、认证令牌输入框、HTTP 开关和 TLS 校验开关;启动选项的实际组装见下文:在已读取的启动代码中,认证令牌输入框没有被传入 extra.Options,因此不要仅凭表单存在就推断它参与认证。主窗口与默认值、选项组装。
架构
Source: main.go, preferences.go, vpn_settings_ui.go
main 持有主窗口、代理表单和 TaskHandles;VPN 设置按钮调用 VpnSettingsUI.OpenVpnSettings 打开单独的窗口。两组设置均通过同一 Fyne 偏好接口读写,而运行时选择通过 onLoadValue() 进入 extra.Options.UstbVpn。图中的 extra.Options 表示传给 StartWssocks 的输入,并不表示 TaskHandles 创建该类型。主窗口、VPN 设置窗口。
主窗口的运行生命周期
启动按钮只处理 btnStopped 与 btnRunning 两个入口状态:运行时先标记 btnStopping、设置 ignoreWaitErr、通知关闭,然后立即重置按钮;停止时读取当前表单与 onLoadValue(),调用 StartWssocks,成功后标记运行并起一个 goroutine 等待任务结束。btnStarting、btnStopping 期间的点击没有相应分支。启动失败展示错误对话框并恢复为停止态;等待返回的错误只有在 ignoreWaitErr 为 false 时才展示。注意此标志和按钮状态同时被回调与 goroutine 修改;已读取代码没有展示同步机制,不应假定这里具有无竞争保证。启动与等待。
启动选项是点击时从控件抓取,而非修改表单时持续写入;HTTP 勾选变化仅启用或禁用 HTTP 地址输入框。退出主窗口时,如果状态仍为运行中则调用 NotifyCloseWrapper(),随后写入基本偏好并调用 onVpnClose();源码没有在该关闭回调中等待任务退出,也没有在此分支把状态恢复为停止。HTTP 控件、窗口关闭。
配置读取与保存
PrefHasPreference 是恢复配置的门闩:首次运行时基本配置保留控件初值并禁用 HTTP 地址框;VPN 主开关保留默认勾选;VPN 详细配置不覆盖新窗口的初值。保存基本配置会先把该标志写为 true。之后读取地址、主机、用户名与浏览器路径时会跳过空白字符串并使用 strings.TrimSpace;布尔配置通过 Bool 恢复。VPN 详情只在已有偏好标志时保存,因此首次打开并关闭 VPN 设置窗口但尚未关闭主窗口时,该保存路径会提前返回。基本读取及保存、门闩和加载。
| 设置键/控件 | 类型 | 首次显示值 | 行为 |
|---|---|---|---|
local_addr | string | 127.0.0.1:1080 | SOCKS5 监听地址;关闭主窗口保存 |
remote_addr | string | 空 | 远端地址;关闭主窗口保存 |
http_enable | bool | false | 控制 HTTP 地址控件的可用状态 |
http_local_addr | string | 127.0.0.1:1086 | HTTP 监听地址 |
skip_TSL_verify | bool | false | 控件名/偏好键使用 TSL 拼写;实际传给 SkipTLSVerify |
vpn_enable | bool | true | VPN 主开关;其保存由 VPN 关闭回调承担 |
vpn_force_logout, vpn_host_encrypt | bool | true、true | VPN 设置窗口勾选框 |
vpn_host | string | n.ustb.edu.cn | VPN 主机输入 |
vpn_username, vpn_password | string | 空、空 | 用户名可保存;密码保存和恢复语句被注释 |
auth_method | int | 取决于 RadioGroup 未选中时的状态 | 选项映射为 vpn.VpnAuthMethod* 整数;未选中回落到 Webview |
chrome_path | string | 空 | 文件选择器得到 URI path 后存入偏好 |
以上默认值是控件构造值而非统一偏好默认配置;auth_method 未明确设置初始选中项,不能将其认定为默认密码认证。主窗口控件、VPN 控件、偏好键与映射。
使用示例:从仓库提取的关键代码
以下片段是客户端现有实现,不是需要额外编写的调用示例。主窗口启动时把代理设置和 VPN 配置打包后提交;uiAuthToken 虽在表单里创建,下面的结构体赋值并未引用它。
1options := extra.Options{
2 Options: client.Options{
3 LocalSocks5Addr: uiLocalAddr.Text,
4 HttpEnabled: uiHttpEnable.Checked,
5 LocalHttpAddr: uiHttpLocalAddr.Text,
6 SkipTLSVerify: uiSkipTSLVerify.Checked,
7 },
8 UstbVpn: onLoadValue(),
9 RemoteAddr: uiRemoteAddr.Text,
10}
11btnStatus = btnStarting
12btnStart.SetText("Loading")
13if err := handles.StartWssocks(options); err != nil {
14 // log error
15 dialog.ShowError(err, w)
16 btnStart.SetText("Start")
17 btnStatus = btnStopped
18 return
19}Source: main.go
下面的基本设置写入发生在窗口关闭阶段。PrefHasPreference 先置位,使下次启动的加载路径生效;代码没有实时保存输入变化。
1func saveBasicPreference(pref fyne.Preferences, uiLocalAddr, uiRemoteAddr,
2 uiHttpLocalAddr *widget.Entry, uiHttpEnable *widget.Check,
3 uiSkipTSLVerify *widget.Check) {
4 pref.SetBool(PrefHasPreference, true)
5 pref.SetString(PrefLocalAddr, uiLocalAddr.Text)
6 pref.SetString(PrefRemoteAddr, uiRemoteAddr.Text)
7
8 pref.SetBool(PrefHttpEnable, uiHttpEnable.Checked)
9 pref.SetString(PrefHttpLocalAddr, uiHttpLocalAddr.Text)
10 pref.SetBool(PrefSkipTSLVerify, uiSkipTSLVerify.Checked)
11}Source: preferences.go
VPN 设置在单独的窗口关闭时写入;密码输入框用于运行时认证结构体,但持久化密码的 SetString 被注释,避免误认为“记住密码”已实现。
1func (v *VpnSettingsUI) LoadSettingsValues(values *vpn.UstbVpn) {
2 values.ForceLogout = v.uiVpnForceLogout.Checked
3 values.HostEncrypt = v.uiVpnHostEncrypt.Checked
4 values.TargetVpn = v.uiVpnHostInput.Text
5 values.AuthMethod = getAuthMethodInt(v.uiVpnAuthMethod)
6 values.PasswdAuth = passwd.UstbVpnPasswdAuth{
7 Username: v.uiVpnUsername.Text,
8 Password: v.uiVpnPassword.Text,
9 }
10}Source: vpn_settings_ui.go
VPN 设置及认证交互
OpenVpnSettings 每次建立控件和题为 VPN Auth Settings 的新窗口,先用 loadVpnPreference 覆盖已有配置,再放入 Password、QR Code、Webview 三个标签页。QR 标签页在此设置窗口中仅显示 World! 标签;实际二维码认证界面由 FyneQrCodeAuth.ShowQrCodeAndWait 创建,二者不是同一个窗口。浏览器文件选择器对取消和错误直接返回,成功后将 reader.URI().Path() 写入禁用的路径输入框;关闭设置窗口触发 saveVPNPreference。设置窗口、二维码窗口。
二维码认证先编码 qr.GenQrCodeContent(),打开含二维码和 Finish 按钮的窗口,然后在 30 秒超时与 scanned 通道之间选择。按 Finish 后关闭窗口并调用 WaitStatus;超时则关闭窗口并返回错误。WaitStatus 调用 qrcode2.WaitQrState(qr.Sid) 再调用 RedirectToLogin,出错打印并向上传递,成功返回 nil, nil;函数内还留有 HTTP 取消的 TODO,因此 30 秒限制只直接包围等待按钮的阶段,不能推断覆盖后续网络调用。二维码编码与超时、状态查询。
API 速览
| 方法/函数 | 输入 | 返回及已验证的行为 |
|---|---|---|
saveBasicPreference(pref fyne.Preferences, uiLocalAddr, uiRemoteAddr, uiHttpLocalAddr *widget.Entry, uiHttpEnable, uiSkipTSLVerify *widget.Check) | 偏好存储和控件 | 无返回值;写入主表单并标记已配置 |
loadBasicPreference(pref fyne.Preferences, uiLocalAddr, uiRemoteAddr, uiHttpLocalAddr *widget.Entry, uiHttpEnable, uiSkipTSLVerify *widget.Check) | 同上 | 无返回值;首启禁用 HTTP 地址,已有配置时按键恢复 |
(*VpnSettingsUI).OpenVpnSettings(wssApp *fyne.App, pref fyne.Preferences) | 应用指针与偏好 | 无返回值;显示独立窗口,关闭时保存 VPN 详情 |
(*VpnSettingsUI).LoadSettingsValues(values *vpn.UstbVpn) | 待填充 VPN 配置指针 | 无返回值;写入运行时认证字段 |
NewQrCodeAuth(app *fyne.App) qrcode2.QrCodeAuth | 应用指针 | 返回 Fyne 二维码认证实现 |
(*FyneQrCodeAuth).ShowQrCodeAndWait(client *http.Client, cookies []*http.Cookie, qr qrcode2.QrImg) ([]*http.Cookie, error) | HTTP 客户端、Cookie、二维码数据 | 编码/超时/后续状态或重定向失败时返回错误;成功路径见 WaitStatus |
故障、边界与运维注意事项
- 启动和自然退出:
StartWssocks返回错误时只弹出错误并复位;成功后独立 goroutine 调用Wait()。人工停止前将ignoreWaitErr置为 true,用于抑制该路径的等待错误提示,但异步读写没有在所见代码中受锁保护。状态回调。 - **关闭行为:**关闭主窗口只有在
btnRunning时显式通知包装器关闭;btnStarting和btnStopping不匹配该分支。随后保存基本配置并调用 VPN 关闭回调,未在该回调里等待退出。关闭回调。 - 首次配置的写入顺序:
saveVPNPreference在has_preference为 false 时直接返回,而主窗口关闭时才由saveBasicPreference将其设为 true。对于首次打开 VPN 设置后立即关闭设置窗口的用户,不能期待那次关闭已保存其详细设置。偏好保存。 - **隐私与未接线输入:**密码控件设为
Password: true,密码写入/恢复偏好的语句被注释;基本表单的 auth token 在已读取的启动选项赋值中没有使用。维护时应分别检查控件、偏好与extra.Options的映射,不要从标签名称推断已生效。密码控件、未持久化密码、选项构造。 - **二维码等待:**二维码编码错误立即返回;未在 30 秒内点 Finish 时返回超时错误;点 Finish 后的查询/重定向错误继续向上传播。
WaitStatus中的 HTTP 取消尚标记 TODO。scanned是容量为 1 的局部通道。二维码实现。 - **扩展配置:**增加新 GUI 字段时,需分别考虑控件初始值、
load*Preference/save*Preference、启动时extra.Options映射及窗口关闭时机;VPN 方法的 UI 字符串映射在getAuthMethodInt和偏好写入/读取逻辑中均出现,新增方法需要检查这些分支。VPN 映射、偏好映射。
相关链接
- 跨平台客户端入口与主窗口
- VPN 设置窗口与浏览器选择
- 二维码认证交互
- 项目客户端类型概述(CLI 和 SwiftUI 客户端的详细使用属于各自主题)