Add properties dialog (#250)

* Add properties dialog

* Resize window

* Count TCP and UDP connections

* Resize the first column

* Fix lint error
This commit is contained in:
Gerhard Tan
2025-06-25 10:03:22 +08:00
committed by GitHub
parent 6a66bd3960
commit 325a2e6a5e
20 changed files with 1968 additions and 972 deletions

2
go.mod
View File

@ -66,4 +66,4 @@ require (
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace github.com/lxn/walk => github.com/koho/frpmgr v0.0.0-20250614023804-912b463eb7d6
replace github.com/lxn/walk => github.com/koho/frpmgr v0.0.0-20250619085234-ed99ab60add0

4
go.sum
View File

@ -63,8 +63,8 @@ github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/4
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
github.com/koho/frpmgr v0.0.0-20250614023804-912b463eb7d6 h1:vFU+4JwxgVA1S8200tuSXNRWUzxOMVYppCOfYW3mGE4=
github.com/koho/frpmgr v0.0.0-20250614023804-912b463eb7d6/go.mod h1:BEvTAgZsEET00wLNsOhKg8fX//k6l4b5INTzX2APBb8=
github.com/koho/frpmgr v0.0.0-20250619085234-ed99ab60add0 h1:45SbnvjExN+92n5O4YVWPXfF/sQG0iLvijF247q0kjs=
github.com/koho/frpmgr v0.0.0-20250619085234-ed99ab60add0/go.mod h1:BEvTAgZsEET00wLNsOhKg8fX//k6l4b5INTzX2APBb8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=

File diff suppressed because it is too large Load Diff

View File

@ -401,6 +401,13 @@
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Properties",
"message": "Properties",
"translation": "Properties",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Select all",
"message": "Select all",
@ -1246,16 +1253,16 @@
"fuzzy": true
},
{
"id": "auto",
"message": "auto",
"translation": "auto",
"id": "Auto",
"message": "Auto",
"translation": "Auto",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "default",
"message": "default",
"translation": "default",
"id": "Default",
"message": "Default",
"translation": "Default",
"translatorComment": "Copied from source.",
"fuzzy": true
},
@ -1901,6 +1908,132 @@
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Manual",
"message": "Manual",
"translation": "Manual",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Identifier",
"message": "Identifier",
"translation": "Identifier",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Service Name",
"message": "Service Name",
"translation": "Service Name",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "File Format",
"message": "File Format",
"translation": "File Format",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Number of Proxies",
"message": "Number of Proxies",
"translation": "Number of Proxies",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Start Type",
"message": "Start Type",
"translation": "Start Type",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "{LogFileCount} Files, {LogSizeDesc}",
"message": "{LogFileCount} Files, {LogSizeDesc}",
"translation": "{LogFileCount} Files, {LogSizeDesc}",
"translatorComment": "Copied from source.",
"placeholders": [
{
"id": "LogFileCount",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "logFileCount"
},
{
"id": "LogSizeDesc",
"string": "%[2]s",
"type": "string",
"underlyingType": "string",
"argNum": 2,
"expr": "logSizeDesc"
}
],
"fuzzy": true
},
{
"id": "Created",
"message": "Created",
"translation": "Created",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Modified",
"message": "Modified",
"translation": "Modified",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Started",
"message": "Started",
"translation": "Started",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Number of TCP Connections",
"message": "Number of TCP Connections",
"translation": "Number of TCP Connections",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Number of UDP Connections",
"message": "Number of UDP Connections",
"translation": "Number of UDP Connections",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "{Name} Properties",
"message": "{Name} Properties",
"translation": "{Name} Properties",
"translatorComment": "Copied from source.",
"placeholders": [
{
"id": "Name",
"string": "%[1]s",
"type": "string",
"underlyingType": "string",
"argNum": 1,
"expr": "pd.conf.Name()"
}
],
"fuzzy": true
},
{
"id": "Copy Value",
"message": "Copy Value",
"translation": "Copy Value",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Error",
"message": "Error",

View File

@ -301,6 +301,11 @@
"message": "Export All Configs to ZIP",
"translation": "Exportar todas las configuraciones a ZIP"
},
{
"id": "Properties",
"message": "Properties",
"translation": "Propiedades"
},
{
"id": "Select all",
"message": "Select all",
@ -938,14 +943,14 @@
"translation": "Protocolo proxy"
},
{
"id": "auto",
"message": "auto",
"translation": "auto"
"id": "Auto",
"message": "Auto",
"translation": "Auto"
},
{
"id": "default",
"message": "default",
"translation": "por defecto"
"id": "Default",
"message": "Default",
"translation": "Por defecto"
},
{
"id": "Keep Tunnel",
@ -1417,6 +1422,104 @@
"message": "Log retention",
"translation": "Retención de registros"
},
{
"id": "Manual",
"message": "Manual",
"translation": "Manual"
},
{
"id": "Identifier",
"message": "Identifier",
"translation": "Identificador"
},
{
"id": "Service Name",
"message": "Service Name",
"translation": "Nombre del servicio"
},
{
"id": "File Format",
"message": "File Format",
"translation": "Formato de archivo"
},
{
"id": "Number of Proxies",
"message": "Number of Proxies",
"translation": "Número de proxies"
},
{
"id": "Start Type",
"message": "Start Type",
"translation": "Tipo de inicio"
},
{
"id": "{LogFileCount} Files, {LogSizeDesc}",
"message": "{LogFileCount} Files, {LogSizeDesc}",
"translation": "{LogFileCount} archivos, {LogSizeDesc}",
"placeholders": [
{
"id": "LogFileCount",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "logFileCount"
},
{
"id": "LogSizeDesc",
"string": "%[2]s",
"type": "string",
"underlyingType": "string",
"argNum": 2,
"expr": "logSizeDesc"
}
]
},
{
"id": "Created",
"message": "Created",
"translation": "Creado"
},
{
"id": "Modified",
"message": "Modified",
"translation": "Modificado"
},
{
"id": "Started",
"message": "Started",
"translation": "Empezado"
},
{
"id": "Number of TCP Connections",
"message": "Number of TCP Connections",
"translation": "Número de conexiones TCP"
},
{
"id": "Number of UDP Connections",
"message": "Number of UDP Connections",
"translation": "Número de conexiones UDP"
},
{
"id": "{Name} Properties",
"message": "{Name} Properties",
"translation": "Propiedades de {Name}",
"placeholders": [
{
"id": "Name",
"string": "%[1]s",
"type": "string",
"underlyingType": "string",
"argNum": 1,
"expr": "pd.conf.Name()"
}
]
},
{
"id": "Copy Value",
"message": "Copy Value",
"translation": "Copiar valor"
},
{
"id": "Error",
"message": "Error",

View File

@ -109,7 +109,7 @@
{
"id": "For FRP configuration documentation, please visit the FRP project page:",
"message": "For FRP configuration documentation, please visit the FRP project page:",
"translation": "FRP 設定ドキュメントについては、FRP プロジェクトページにアクセスしてください:"
"translation": "FRP ドキュメントについては、FRP プロジェクト ページをご覧ください:"
},
{
"id": "An error occurred while checking for a software update.",
@ -301,6 +301,11 @@
"message": "Export All Configs to ZIP",
"translation": "すべての設定をZIPにエクスポート"
},
{
"id": "Properties",
"message": "Properties",
"translation": "プロパティ"
},
{
"id": "Select all",
"message": "Select all",
@ -948,13 +953,13 @@
"translation": "プロキシプロトコル"
},
{
"id": "auto",
"message": "auto",
"id": "Auto",
"message": "Auto",
"translation": "自動"
},
{
"id": "default",
"message": "default",
"id": "Default",
"message": "Default",
"translation": "既定値"
},
{
@ -1427,6 +1432,104 @@
"message": "Log retention",
"translation": "ログ保持"
},
{
"id": "Manual",
"message": "Manual",
"translation": "マニュアル"
},
{
"id": "Identifier",
"message": "Identifier",
"translation": "識別子"
},
{
"id": "Service Name",
"message": "Service Name",
"translation": "サービス名"
},
{
"id": "File Format",
"message": "File Format",
"translation": "ファイル形式"
},
{
"id": "Number of Proxies",
"message": "Number of Proxies",
"translation": "プロキシの数"
},
{
"id": "Start Type",
"message": "Start Type",
"translation": "スタートアップの種類"
},
{
"id": "{LogFileCount} Files, {LogSizeDesc}",
"message": "{LogFileCount} Files, {LogSizeDesc}",
"translation": "{LogFileCount} ファイル、{LogSizeDesc}",
"placeholders": [
{
"id": "LogFileCount",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "logFileCount"
},
{
"id": "LogSizeDesc",
"string": "%[2]s",
"type": "string",
"underlyingType": "string",
"argNum": 2,
"expr": "logSizeDesc"
}
]
},
{
"id": "Created",
"message": "Created",
"translation": "作成時間"
},
{
"id": "Modified",
"message": "Modified",
"translation": "修正時間"
},
{
"id": "Started",
"message": "Started",
"translation": "起動時間"
},
{
"id": "Number of TCP Connections",
"message": "Number of TCP Connections",
"translation": "TCP接続数"
},
{
"id": "Number of UDP Connections",
"message": "Number of UDP Connections",
"translation": "UDP接続数"
},
{
"id": "{Name} Properties",
"message": "{Name} Properties",
"translation": "{Name}のプロパティ",
"placeholders": [
{
"id": "Name",
"string": "%[1]s",
"type": "string",
"underlyingType": "string",
"argNum": 1,
"expr": "pd.conf.Name()"
}
]
},
{
"id": "Copy Value",
"message": "Copy Value",
"translation": "コピー値"
},
{
"id": "Error",
"message": "Error",

View File

@ -301,6 +301,11 @@
"message": "Export All Configs to ZIP",
"translation": "모든 구성을 ZIP 으로 내보내기"
},
{
"id": "Properties",
"message": "Properties",
"translation": "속성"
},
{
"id": "Select all",
"message": "Select all",
@ -938,13 +943,13 @@
"translation": "프록시 프로토콜"
},
{
"id": "auto",
"message": "auto",
"id": "Auto",
"message": "Auto",
"translation": "자동"
},
{
"id": "default",
"message": "default",
"id": "Default",
"message": "Default",
"translation": "기본값"
},
{
@ -1417,6 +1422,104 @@
"message": "Log retention",
"translation": "로그 보존"
},
{
"id": "Manual",
"message": "Manual",
"translation": "매뉴얼"
},
{
"id": "Identifier",
"message": "Identifier",
"translation": "식별자"
},
{
"id": "Service Name",
"message": "Service Name",
"translation": "서비스 이름"
},
{
"id": "File Format",
"message": "File Format",
"translation": "파일 형식"
},
{
"id": "Number of Proxies",
"message": "Number of Proxies",
"translation": "프록시 수"
},
{
"id": "Start Type",
"message": "Start Type",
"translation": "시작 유형"
},
{
"id": "{LogFileCount} Files, {LogSizeDesc}",
"message": "{LogFileCount} Files, {LogSizeDesc}",
"translation": "{LogFileCount}개 파일, {LogSizeDesc}",
"placeholders": [
{
"id": "LogFileCount",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "logFileCount"
},
{
"id": "LogSizeDesc",
"string": "%[2]s",
"type": "string",
"underlyingType": "string",
"argNum": 2,
"expr": "logSizeDesc"
}
]
},
{
"id": "Created",
"message": "Created",
"translation": "창조 시간"
},
{
"id": "Modified",
"message": "Modified",
"translation": "수정 시간"
},
{
"id": "Started",
"message": "Started",
"translation": "시작 시간"
},
{
"id": "Number of TCP Connections",
"message": "Number of TCP Connections",
"translation": "TCP 연결 수"
},
{
"id": "Number of UDP Connections",
"message": "Number of UDP Connections",
"translation": "UDP 연결 수"
},
{
"id": "{Name} Properties",
"message": "{Name} Properties",
"translation": "{Name} 속성",
"placeholders": [
{
"id": "Name",
"string": "%[1]s",
"type": "string",
"underlyingType": "string",
"argNum": 1,
"expr": "pd.conf.Name()"
}
]
},
{
"id": "Copy Value",
"message": "Copy Value",
"translation": "복사 값"
},
{
"id": "Error",
"message": "Error",

View File

@ -301,6 +301,11 @@
"message": "Export All Configs to ZIP",
"translation": "导出所有配置 (ZIP 压缩包)"
},
{
"id": "Properties",
"message": "Properties",
"translation": "属性"
},
{
"id": "Select all",
"message": "Select all",
@ -938,13 +943,13 @@
"translation": "代理协议"
},
{
"id": "auto",
"message": "auto",
"id": "Auto",
"message": "Auto",
"translation": "自动"
},
{
"id": "default",
"message": "default",
"id": "Default",
"message": "Default",
"translation": "默认"
},
{
@ -1417,6 +1422,104 @@
"message": "Log retention",
"translation": "日志保留"
},
{
"id": "Manual",
"message": "Manual",
"translation": "手动"
},
{
"id": "Identifier",
"message": "Identifier",
"translation": "标识符"
},
{
"id": "Service Name",
"message": "Service Name",
"translation": "服务名称"
},
{
"id": "File Format",
"message": "File Format",
"translation": "文件格式"
},
{
"id": "Number of Proxies",
"message": "Number of Proxies",
"translation": "代理数量"
},
{
"id": "Start Type",
"message": "Start Type",
"translation": "启动类型"
},
{
"id": "{LogFileCount} Files, {LogSizeDesc}",
"message": "{LogFileCount} Files, {LogSizeDesc}",
"translation": "{LogFileCount} 个文件,{LogSizeDesc}",
"placeholders": [
{
"id": "LogFileCount",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "logFileCount"
},
{
"id": "LogSizeDesc",
"string": "%[2]s",
"type": "string",
"underlyingType": "string",
"argNum": 2,
"expr": "logSizeDesc"
}
]
},
{
"id": "Created",
"message": "Created",
"translation": "创建时间"
},
{
"id": "Modified",
"message": "Modified",
"translation": "修改时间"
},
{
"id": "Started",
"message": "Started",
"translation": "启动时间"
},
{
"id": "Number of TCP Connections",
"message": "Number of TCP Connections",
"translation": "TCP 连接数"
},
{
"id": "Number of UDP Connections",
"message": "Number of UDP Connections",
"translation": "UDP 连接数"
},
{
"id": "{Name} Properties",
"message": "{Name} Properties",
"translation": "{Name} 属性",
"placeholders": [
{
"id": "Name",
"string": "%[1]s",
"type": "string",
"underlyingType": "string",
"argNum": 1,
"expr": "pd.conf.Name()"
}
]
},
{
"id": "Copy Value",
"message": "Copy Value",
"translation": "复制值"
},
{
"id": "Error",
"message": "Error",

View File

@ -301,6 +301,11 @@
"message": "Export All Configs to ZIP",
"translation": "導出所有配置 (ZIP 壓縮檔)"
},
{
"id": "Properties",
"message": "Properties",
"translation": "內容"
},
{
"id": "Select all",
"message": "Select all",
@ -938,13 +943,13 @@
"translation": "代理協定"
},
{
"id": "auto",
"message": "auto",
"id": "Auto",
"message": "Auto",
"translation": "自動"
},
{
"id": "default",
"message": "default",
"id": "Default",
"message": "Default",
"translation": "預設"
},
{
@ -1417,6 +1422,104 @@
"message": "Log retention",
"translation": "日誌保留"
},
{
"id": "Manual",
"message": "Manual",
"translation": "手動"
},
{
"id": "Identifier",
"message": "Identifier",
"translation": "識別符"
},
{
"id": "Service Name",
"message": "Service Name",
"translation": "服務名稱"
},
{
"id": "File Format",
"message": "File Format",
"translation": "檔案格式"
},
{
"id": "Number of Proxies",
"message": "Number of Proxies",
"translation": "代理數量"
},
{
"id": "Start Type",
"message": "Start Type",
"translation": "啟動類型"
},
{
"id": "{LogFileCount} Files, {LogSizeDesc}",
"message": "{LogFileCount} Files, {LogSizeDesc}",
"translation": "{LogFileCount} 個文件,{LogSizeDesc}",
"placeholders": [
{
"id": "LogFileCount",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "logFileCount"
},
{
"id": "LogSizeDesc",
"string": "%[2]s",
"type": "string",
"underlyingType": "string",
"argNum": 2,
"expr": "logSizeDesc"
}
]
},
{
"id": "Created",
"message": "Created",
"translation": "建立日期"
},
{
"id": "Modified",
"message": "Modified",
"translation": "修改日期"
},
{
"id": "Started",
"message": "Started",
"translation": "啟動日期"
},
{
"id": "Number of TCP Connections",
"message": "Number of TCP Connections",
"translation": "TCP 連線數"
},
{
"id": "Number of UDP Connections",
"message": "Number of UDP Connections",
"translation": "UDP 連線數"
},
{
"id": "{Name} Properties",
"message": "{Name} Properties",
"translation": "{Name} - 內容",
"placeholders": [
{
"id": "Name",
"string": "%[1]s",
"type": "string",
"underlyingType": "string",
"argNum": 1,
"expr": "pd.conf.Name()"
}
]
},
{
"id": "Copy Value",
"message": "Copy Value",
"translation": "複製值"
},
{
"id": "Error",
"message": "Error",

View File

@ -1,6 +1,7 @@
package util
import (
"fmt"
"reflect"
"strings"
)
@ -72,3 +73,18 @@ func MoveSlice[S ~[]E, E any](s S, i, j int) {
}
s[j] = x
}
// ByteCountIEC converts a size in bytes to a human-readable string in IEC (binary) format.
func ByteCountIEC(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB",
float64(b)/float64(div), "KMGTPE"[exp])
}

View File

@ -2,12 +2,23 @@ package util
import (
"context"
"errors"
"fmt"
"io"
"mime"
"net/http"
"path"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
var (
modIPHelp = syscall.NewLazyDLL("iphlpapi.dll")
procGetExtendedTcpTable = modIPHelp.NewProc("GetExtendedTcpTable")
procGetExtendedUdpTable = modIPHelp.NewProc("GetExtendedUdpTable")
)
// DownloadFile downloads a file from the given url
@ -45,3 +56,105 @@ func DownloadFile(ctx context.Context, url string) (filename, mediaType string,
return "", "", nil, err
}
}
//nolint:unused
type mibTCPRowOwnerPid struct {
dwState uint32
dwLocalAddr uint32
dwLocalPort uint32
dwRemoteAddr uint32
dwRemotePort uint32
dwOwningPid uint32
}
//nolint:unused
type mibTCP6RowOwnerPid struct {
ucLocalAddr [16]byte
dwLocalScopeId uint32
dwLocalPort uint32
ucRemoteAddr [16]byte
dwRemoteScopeId uint32
dwRemotePort uint32
dwState uint32
dwOwningPid uint32
}
//nolint:unused
type mibUDPRowOwnerPid struct {
dwLocalAddr uint32
dwLocalPort uint32
dwOwningPid uint32
}
//nolint:unused
type mibUDP6RowOwnerPid struct {
ucLocalAddr [16]byte
dwLocalScopeId uint32
dwLocalPort uint32
dwOwningPid uint32
}
type mibTableOwnerPid[T any] struct {
dwNumEntries uint32
table [1]T
}
// countConnections returns the number of IPv4 and IPv6 connections that match the given filter.
// - https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable
// - https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedudptable
func countConnections[R4, R6 any](proc *syscall.LazyProc, tableClass uintptr, filter4 func(R4) bool, filter6 func(R6) bool) (count int) {
var size uint32
var buf []byte
getTable := func(af uintptr) bool {
for {
var pTable *byte
if len(buf) > 0 {
pTable = &buf[0]
}
ret, _, _ := proc.Call(uintptr(unsafe.Pointer(pTable)), uintptr(unsafe.Pointer(&size)), 0, af, tableClass, 0)
if ret != 0 {
if errors.Is(syscall.Errno(ret), syscall.ERROR_INSUFFICIENT_BUFFER) {
buf = make([]byte, int(size))
continue
}
return false
}
return true
}
}
if getTable(windows.AF_INET) {
table := (*mibTableOwnerPid[R4])(unsafe.Pointer(&buf[0]))
for _, conn := range unsafe.Slice(&table.table[0], table.dwNumEntries) {
if filter4(conn) {
count++
}
}
}
if getTable(windows.AF_INET6) {
table := (*mibTableOwnerPid[R6])(unsafe.Pointer(&buf[0]))
for _, conn := range unsafe.Slice(&table.table[0], table.dwNumEntries) {
if filter6(conn) {
count++
}
}
}
return
}
// CountTCPConnections returns the number of connected TCP endpoints for a given process.
func CountTCPConnections(pid uint32) int {
return countConnections(procGetExtendedTcpTable, 4, func(r4 mibTCPRowOwnerPid) bool {
return r4.dwOwningPid == pid
}, func(r6 mibTCP6RowOwnerPid) bool {
return r6.dwOwningPid == pid
})
}
// CountUDPConnections returns the number of UDP endpoints for a given process.
func CountUDPConnections(pid uint32) int {
return countConnections(procGetExtendedUdpTable, 1, func(r4 mibUDPRowOwnerPid) bool {
return r4.dwOwningPid == pid
}, func(r6 mibUDP6RowOwnerPid) bool {
return r6.dwOwningPid == pid
})
}

View File

@ -13,7 +13,7 @@ type PasswordValidator struct {
func (p *PasswordValidator) Validate(v interface{}) error {
text := v.(string)
if text == "" {
return silentErr
return errSilent
}
if (*p.Password).Text() == text {
return nil

View File

@ -6,7 +6,7 @@ import (
"github.com/lxn/walk"
)
var silentErr = errors.New("")
var errSilent = errors.New("")
type ToolTipErrorPresenter struct {
*walk.ToolTipErrorPresenter
@ -21,7 +21,7 @@ func NewToolTipErrorPresenter() (*ToolTipErrorPresenter, error) {
}
func (ttep *ToolTipErrorPresenter) PresentError(err error, widget walk.Widget) {
if errors.Is(err, silentErr) {
if errors.Is(err, errSilent) {
ttep.ToolTipErrorPresenter.PresentError(nil, widget)
} else {
ttep.ToolTipErrorPresenter.PresentError(err, widget)

View File

@ -20,7 +20,7 @@ func NewRegexpValidator(pattern string) (*RegexpValidator, error) {
func (rv *RegexpValidator) Validate(v interface{}) error {
err := rv.RegexpValidator.Validate(v)
if str, ok := v.(string); ok && str == "" && err != nil {
return silentErr
return errSilent
}
return err
}

View File

@ -10,8 +10,8 @@ func TestRegexp(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err = r.Validate(""); !errors.Is(err, silentErr) {
t.Errorf("Expected: %v, got: %v", silentErr, err)
if err = r.Validate(""); !errors.Is(err, errSilent) {
t.Errorf("Expected: %v, got: %v", errSilent, err)
}
tests := []struct {
input string

View File

@ -116,3 +116,26 @@ func UninstallService(configPath string, wait bool) error {
}
return err2
}
// QueryStartInfo returns the start type and process id of the given service.
func QueryStartInfo(configPath string) (uint32, uint32, error) {
m, err := serviceManager()
if err != nil {
return 0, 0, err
}
serviceName := ServiceNameOfClient(configPath)
service, err := m.OpenService(serviceName)
if err != nil {
return 0, 0, err
}
defer service.Close()
cfg, err := service.Config()
if err != nil {
return 0, 0, err
}
var pid uint32
if status, err := service.Query(); err == nil {
pid = status.ProcessId
}
return cfg.StartType, pid, nil
}

View File

@ -144,6 +144,15 @@ func (cv *ConfView) View() Widget {
Enabled: Bind("confView.ItemCount > 0"),
OnTriggered: cv.onExport,
},
Action{
Text: i18n.Sprintf("Properties"),
Enabled: Bind("confView.SelectedCount == 1"),
OnTriggered: func() {
if conf := getCurrentConf(); conf != nil {
NewPropertiesDialog(conf).Run(cv.Form())
}
},
},
Separator{},
Action{
Enabled: Bind("confView.SelectedCount < confView.ItemCount"),

View File

@ -295,7 +295,7 @@ func (pd *EditProxyDialog) advancedProxyPage() TabPage {
Label{Visible: Bind("vm.PluginEnable"), Text: i18n.SprintfColon("Proxy Protocol")},
ComboBox{
Visible: Bind("vm.PluginEnable"),
Model: NewListModel([]string{"", "v1", "v2"}, i18n.Sprintf("auto")),
Model: NewListModel([]string{"", "v1", "v2"}, i18n.Sprintf("Auto")),
BindingMember: "Value",
DisplayMember: "Title",
Value: Bind("ProxyProtocolVersion"),
@ -303,7 +303,7 @@ func (pd *EditProxyDialog) advancedProxyPage() TabPage {
Label{Visible: xtcpVisitor, Text: i18n.SprintfColon("Protocol")},
ComboBox{
Visible: xtcpVisitor,
Model: NewListModel([]string{"", consts.ProtoQUIC, consts.ProtoKCP}, i18n.Sprintf("default")),
Model: NewListModel([]string{"", consts.ProtoQUIC, consts.ProtoKCP}, i18n.Sprintf("Default")),
BindingMember: "Value",
DisplayMember: "Title",
Value: Bind("Protocol"),

View File

@ -79,7 +79,7 @@ func (nd *NATDiscoveryDialog) discover() (err error) {
return err
}
if len(addrs) < 2 {
return fmt.Errorf("can not get enough addresses, need 2, got: %v\n", addrs)
return fmt.Errorf("can not get enough addresses")
}
localIPs, _ := nathole.ListLocalIPsForNatHole(10)

131
ui/properties.go Normal file
View File

@ -0,0 +1,131 @@
package ui
import (
"os"
"reflect"
"strconv"
"strings"
"syscall"
"time"
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
"golang.org/x/sys/windows"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/util"
"github.com/koho/frpmgr/services"
)
type PropertiesDialog struct {
*walk.Dialog
table *walk.TableView
conf *Conf
}
func NewPropertiesDialog(conf *Conf) *PropertiesDialog {
return &PropertiesDialog{conf: conf}
}
func (pd *PropertiesDialog) logFileStat() (count int, size int64) {
if logs, _, err := util.FindLogFiles(pd.conf.Data.GetLogFile()); err == nil {
for _, logFile := range logs {
if fileInfo, err := os.Stat(logFile); err == nil {
count++
size += fileInfo.Size()
}
}
}
return
}
func (pd *PropertiesDialog) Run(owner walk.Form) (int, error) {
logFileCount, logFileSize := pd.logFileStat()
logSizeDesc := util.ByteCountIEC(logFileSize)
var startTypeDesc string
startType, pid, _ := services.QueryStartInfo(pd.conf.Path)
switch startType {
case windows.SERVICE_AUTO_START:
startTypeDesc = i18n.Sprintf("Auto")
case windows.SERVICE_DEMAND_START:
startTypeDesc = i18n.Sprintf("Manual")
default:
startTypeDesc = i18n.Sprintf("None")
}
items := []*ListItem{
{Title: i18n.Sprintf("Name"), Value: pd.conf.Name()},
{Title: i18n.Sprintf("Identifier"), Value: util.FileNameWithoutExt(pd.conf.Path)},
{Title: i18n.Sprintf("Service Name"), Value: services.ServiceNameOfClient(pd.conf.Path)},
{Title: i18n.Sprintf("File Format"), Value: strings.ToUpper(pd.conf.Data.Ext()[1:])},
{Title: i18n.Sprintf("Number of Proxies"), Value: strconv.Itoa(reflect.ValueOf(pd.conf.Data.Items()).Len())},
{Title: i18n.Sprintf("Start Type"), Value: startTypeDesc},
{Title: i18n.Sprintf("Log"), Value: i18n.Sprintf("%d Files, %s", logFileCount, logSizeDesc)},
}
if pid > 0 {
items = append(items, &ListItem{
Title: i18n.Sprintf("Number of TCP Connections"),
Value: strconv.Itoa(util.CountTCPConnections(pid)),
}, &ListItem{
Title: i18n.Sprintf("Number of UDP Connections"),
Value: strconv.Itoa(util.CountUDPConnections(pid)),
})
if process, err := syscall.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid); err == nil {
var creationTime, unusedTime syscall.Filetime
if err = syscall.GetProcessTimes(process, &creationTime, &unusedTime, &unusedTime, &unusedTime); err == nil {
items = append(items, &ListItem{
Title: i18n.Sprintf("Started"),
Value: time.Unix(0, creationTime.Nanoseconds()).Format(time.DateTime),
})
}
syscall.CloseHandle(process)
}
}
if info, err := os.Stat(pd.conf.Path); err == nil {
created := time.Unix(0, info.Sys().(*syscall.Win32FileAttributeData).CreationTime.Nanoseconds())
modified := info.ModTime()
items = append(items, &ListItem{
Title: i18n.Sprintf("Created"),
Value: created.Format(time.DateTime),
}, &ListItem{
Title: i18n.Sprintf("Modified"),
Value: modified.Format(time.DateTime),
})
}
dlg := NewBasicDialog(&pd.Dialog, i18n.Sprintf("%s Properties", pd.conf.Name()),
loadIcon(res.IconFile, 32),
DataBinder{}, nil,
TableView{
AssignTo: &pd.table,
Name: "properties",
Columns: []TableViewColumn{
{Title: i18n.Sprintf("Item"), DataMember: "Title"},
{Title: i18n.Sprintf("Value"), DataMember: "Value", Width: 180},
},
ColumnsOrderable: false,
Model: NewNonSortedModel(items),
ContextMenuItems: []MenuItem{
Action{
Text: i18n.Sprintf("Copy Value"),
Enabled: Bind("properties.CurrentIndex >= 0"),
Visible: Bind("properties.CurrentIndex >= 0"),
OnTriggered: func() {
if idx := pd.table.CurrentIndex(); idx >= 0 && idx < len(items) {
walk.Clipboard().SetText(items[idx].Value)
}
},
},
},
},
)
dlg.MinSize = Size{Width: 420, Height: 350}
if err := dlg.Create(owner); err != nil {
return 0, err
}
pd.table.BoundsChanged().Once(func() {
pd.table.FitColumn(0, 140)
})
return pd.Dialog.Run(), nil
}