docs: 补充中文代码注释
Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled

This commit is contained in:
2026-06-27 20:15:29 +08:00
parent 7a5664ab89
commit f508ecc1b9
145 changed files with 774 additions and 41 deletions

View File

@@ -1,3 +1,5 @@
// internal/adminaudit/audit.go 持久化 Admin API 变更产生的审计事件,并向管理界面提供查询。
package adminaudit
import (

View File

@@ -1,3 +1,5 @@
// internal/adminaudit/audit_test.go 包含用于约束 audit 行为的测试。
package adminaudit
import (

View File

@@ -1,3 +1,5 @@
// internal/adminconfig/config.go 在 SQLite 中保存服务级运行态配置,并用类型化方法包装 JSON 选项。
package adminconfig
import (

View File

@@ -1,3 +1,5 @@
// internal/adminconfig/config_test.go 包含用于约束 config 行为的测试。
package adminconfig
import "testing"

View File

@@ -1,5 +1,7 @@
//go:build (darwin && (amd64 || arm64)) || (freebsd && (amd64 || arm64)) || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)) || (openbsd && (amd64 || arm64)) || (windows && (386 || amd64 || arm64))
// internal/admindb/db.go 打开跨平台 SQLite 数据库,并应用管理、路由和插件运行态共用的表结构。
package admindb
import (
@@ -7,9 +9,12 @@ import (
"os"
"path/filepath"
// modernc.org/sqlite 是纯 Go SQLite 驱动,便于容器和跨平台构建时避免 CGO 依赖。
_ "modernc.org/sqlite"
)
// Open 创建或打开管理运行态数据库。调用方传入的路径可以包含尚不存在的目录,
// 这里会先创建目录,再打开 SQLite 连接。
func Open(dbPath string) (*sql.DB, error) {
if dir := filepath.Dir(dbPath); dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
@@ -21,12 +26,16 @@ func Open(dbPath string) (*sql.DB, error) {
if err != nil {
return nil, err
}
// SQLite 对单写者最友好;限制连接数可以避免 database/sql 在高并发下
// 打开多条连接后互相争用写锁。
db.SetMaxOpenConns(1)
// WAL 让读请求不会被普通写事务完全阻塞,适合管理端读多写少的状态库。
if _, err := db.Exec(`PRAGMA journal_mode=WAL`); err != nil {
db.Close()
return nil, err
}
// 写锁短暂冲突时等待一小段时间,减少管理端并发操作产生的偶发 busy 错误。
if _, err := db.Exec(`PRAGMA busy_timeout=5000`); err != nil {
db.Close()
return nil, err
@@ -35,6 +44,8 @@ func Open(dbPath string) (*sql.DB, error) {
return db, nil
}
// Migrate 以幂等方式应用当前 schema。所有 CREATE TABLE 都使用
// IF NOT EXISTS后续字段演进通过 ensureColumn 补齐,便于老数据库平滑升级。
func Migrate(db *sql.DB) error {
const schema = `
CREATE TABLE IF NOT EXISTS schema_migrations (
@@ -447,6 +458,8 @@ INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (1, strftime
if _, err := db.Exec(schema); err != nil {
return err
}
// CREATE TABLE 不会修改已存在的表,因此历史版本新增字段需要显式补齐。
// 每个 ensureColumn 都是幂等的,可以安全地在每次启动迁移时执行。
if err := ensureColumn(db, "audit_logs", "metadata_json", "TEXT NOT NULL DEFAULT '{}'"); err != nil {
return err
}
@@ -465,6 +478,8 @@ INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (1, strftime
return nil
}
// ensureColumn 在表缺字段时执行 ALTER TABLE。table/column/definition 只由
// 受控迁移代码传入,不接收外部输入,避免把 PRAGMA 语句做成动态用户入口。
func ensureColumn(db *sql.DB, table, column, definition string) error {
rows, err := db.Query(`PRAGMA table_info(` + table + `)`)
if err != nil {

View File

@@ -1,3 +1,5 @@
// internal/admindb/db_test.go 包含用于约束 db 行为的测试。
package admindb
import (

View File

@@ -1,5 +1,7 @@
//go:build !((darwin && (amd64 || arm64)) || (freebsd && (amd64 || arm64)) || (linux && (386 || amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x)) || (openbsd && (amd64 || arm64)) || (windows && (386 || amd64 || arm64)))
// internal/admindb/db_unsupported.go 在纯 Go SQLite 驱动未被构建标签启用的平台上返回明确错误。
package admindb
import (

View File

@@ -1,3 +1,5 @@
// internal/adminhttp/api.go 构建隔离的进程内 Admin API 服务,供测试和包级消费者使用。
package adminhttp
import (

View File

@@ -1,3 +1,5 @@
// internal/adminhttp/api_test.go 包含用于约束 api 行为的测试。
package adminhttp
import (

View File

@@ -1,3 +1,5 @@
// internal/adminhttp/gateway.go 把仓库驱动的网关依赖适配为 Admin API 处理器集合。
package adminhttp
import (

View File

@@ -1,3 +1,5 @@
// internal/adminhttp/http.go 提供 Admin API 处理器共用的 JSON、路径片段和来源 IP 辅助方法。
package adminhttp
import (

View File

@@ -1,3 +1,5 @@
// internal/adminhttp/http_test.go 包含用于约束 http 行为的测试。
package adminhttp
import (

View File

@@ -1,3 +1,5 @@
// internal/adminhttp/requests.go 定义 Admin API 测试网关和命令处理器接受的请求载荷。
package adminhttp
type LoginRequest struct {

View File

@@ -1,3 +1,5 @@
// internal/adminhttp/requests_test.go 包含用于约束 requests 行为的测试。
package adminhttp
import (

View File

@@ -1,3 +1,5 @@
// internal/adminroute/repository.go 持久化 Minecraft 主机路由记录,并为在线网关返回有序快照。
package adminroute
import (
@@ -18,7 +20,8 @@ type Record struct {
}
type Repository struct {
db *sql.DB
db *sql.DB
// now 可在测试中注入固定时间,避免断言依赖真实时钟。
now func() time.Time
}
@@ -38,6 +41,7 @@ func NewRepositoryWithClock(db *sql.DB, now func() time.Time) Repository {
}
func (r Repository) EnabledMap(ctx context.Context) (map[string]string, error) {
// 只读取启用路由,结果直接用于连接热路径的内存快照。
rows, err := r.db.QueryContext(ctx, `SELECT host, upstream FROM routes WHERE enabled = 1`)
if err != nil {
return nil, err
@@ -61,6 +65,7 @@ SELECT host, upstream, enabled, note, created_at, updated_at, updated_by
FROM routes`
var args []any
if query = strings.TrimSpace(query); query != "" {
// 管理端搜索同时覆盖 host、upstream 和 note便于按服务名或备注定位路由。
sqlQuery += ` WHERE host LIKE ? OR upstream LIKE ? OR note LIKE ?`
like := "%" + query + "%"
args = append(args, like, like, like)
@@ -87,6 +92,7 @@ FROM routes`
}
func (r Repository) Upsert(ctx context.Context, actor, host, upstream string, enabled bool, note string) error {
// 写入前统一校验,避免无效 host/upstream 进入 SQLite 后再被热路径读取。
if err := ValidateHost(host); err != nil {
return err
}
@@ -101,6 +107,7 @@ func (r Repository) Upsert(ctx context.Context, actor, host, upstream string, en
}
defer tx.Rollback()
// host 是主键;重复保存时只更新可变字段并保留 created_at。
if _, err := tx.ExecContext(ctx, `
INSERT INTO routes(host, upstream, enabled, note, created_at, updated_at, updated_by)
VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -117,6 +124,7 @@ ON CONFLICT(host) DO UPDATE SET
}
func (r Repository) Delete(ctx context.Context, host string) error {
// 删除同样校验 host防止管理端路径参数中的非法值直接进入 SQL。
if err := ValidateHost(host); err != nil {
return err
}

View File

@@ -1,3 +1,5 @@
// internal/adminroute/repository_test.go 包含用于约束 repository 行为的测试。
package adminroute
import (

View File

@@ -1,8 +1,11 @@
// internal/adminroute/snapshot.go 把路由行转换为热路径使用的不可变查找映射。
package adminroute
import "sync/atomic"
type Snapshot struct {
// atomic.Value 保存整张路由表,连接热路径读取时无需加锁。
value atomic.Value
}
@@ -14,6 +17,7 @@ func (s *Snapshot) Store(routes map[string]string) {
if routes == nil {
routes = map[string]string{}
}
// Store 前复制 map避免调用方发布后继续修改导致并发读写 map。
copied := make(map[string]string, len(routes))
for host, upstream := range routes {
copied[host] = upstream
@@ -30,6 +34,7 @@ func (s *Snapshot) Clone() map[string]string {
if !ok {
return nil
}
// Clone 返回副本,管理端或测试修改结果不会影响热路径快照。
copied := make(map[string]string, len(routes))
for host, upstream := range routes {
copied[host] = upstream
@@ -50,6 +55,7 @@ func (s *Snapshot) Lookup(host string) (string, bool) {
if ok {
return upstream, true
}
// default 是显式兜底路由,只有精确 host 未命中时才使用。
upstream, ok = routes["default"]
return upstream, ok
}

View File

@@ -1,3 +1,5 @@
// internal/adminroute/snapshot_test.go 包含用于约束 snapshot 行为的测试。
package adminroute
import "testing"

View File

@@ -1,3 +1,5 @@
// internal/adminroute/validate.go 在写入运行态状态前校验路由主机名和上游地址。
package adminroute
import (
@@ -12,6 +14,7 @@ func ValidateHost(host string) error {
if host == "" {
return errors.New("host is required")
}
// host 会出现在 URL path 和 Minecraft 路由键中,因此禁止空白和斜杠。
if strings.ContainsAny(host, " \t\r\n") {
return errors.New("host must not contain whitespace")
}
@@ -27,6 +30,7 @@ func ValidateUpstream(upstream string) error {
return errors.New("upstream is required")
}
// 传输协议前缀只影响拨号方式,去掉前缀后仍必须是 host:port。
for _, prefix := range []string{"kcp://", "quic://", "haproxy://"} {
upstream = strings.TrimPrefix(upstream, prefix)
}

View File

@@ -1,3 +1,5 @@
// internal/adminroute/validate_test.go 包含用于约束 validate 行为的测试。
package adminroute
import "testing"

View File

@@ -1,3 +1,5 @@
// internal/adminservice/repository.go 把监听服务记录和选项保存到 SQLite供管理端驱动配置。
package adminservice
import (
@@ -8,7 +10,8 @@ import (
)
type Repository struct {
db *sql.DB
db *sql.DB
// now 可由测试注入,保证更新时间断言稳定。
now func() time.Time
}
@@ -30,6 +33,7 @@ func NewRepositoryWithClock(db *sql.DB, now func() time.Time) Repository {
func (r Repository) EnsureDefaults(ctx context.Context, tcpAdminPort int) error {
now := r.now().Unix()
for _, service := range DefaultRecords(tcpAdminPort) {
// 默认服务只在缺失时插入,避免覆盖管理员已经保存的运行态配置。
options, err := json.Marshal(service.Options)
if err != nil {
return err
@@ -47,6 +51,7 @@ ON CONFLICT(name) DO NOTHING`,
}
func (r Repository) List(ctx context.Context) ([]Record, error) {
// 固定排序让管理端列表稳定展示:核心入口在前,可选传输在后。
rows, err := r.db.QueryContext(ctx, `
SELECT name, enabled, port, options_json, restart_required, created_at, updated_at, updated_by
FROM services
@@ -83,6 +88,7 @@ func (r Repository) Update(ctx context.Context, actor, name string, enabled bool
return err
}
// 服务配置变更只标记 restart_required当前进程不会在请求中间重启监听器。
optionsJSON, err := json.Marshal(NormalizeOptions(name, options))
if err != nil {
return err

View File

@@ -1,3 +1,5 @@
// internal/adminservice/repository_test.go 包含用于约束 repository 行为的测试。
package adminservice
import (

View File

@@ -1,3 +1,5 @@
// internal/adminservice/service.go 在原始服务仓库之上应用服务校验、默认值和更新语义。
package adminservice
import (
@@ -36,6 +38,8 @@ type Record struct {
Running bool `json:"running"`
}
// DefaultRecords 给新数据库写入可管理的监听服务。只有 TCP/Admin 默认启用,
// 其他传输保留配置但不自动开放端口。
func DefaultRecords(tcpAdminPort int) []Record {
return []Record{
{Name: NameTCPAdmin, Enabled: true, Port: tcpAdminPort, Options: map[string]any{}},
@@ -52,6 +56,7 @@ func DefaultRecords(tcpAdminPort int) []Record {
}
}
// DefaultPort 返回服务的默认端口TCP/Admin 使用启动配置传入的端口。
func DefaultPort(name string, tcpAdminPort int) int {
switch name {
case NameTCPAdmin:
@@ -67,6 +72,7 @@ func DefaultPort(name string, tcpAdminPort int) int {
}
}
// ValidateUpdate 校验管理端提交的服务更新。TCP/Admin 是控制面入口,不能禁用。
func ValidateUpdate(name string, enabled bool, port int) error {
if !IsKnown(name) {
return fmt.Errorf("unknown service %q", name)
@@ -80,6 +86,7 @@ func ValidateUpdate(name string, enabled bool, port int) error {
return nil
}
// IsKnown 判断服务名是否属于当前网关支持的内置监听服务。
func IsKnown(name string) bool {
switch name {
case NameTCPAdmin, NameKCP, NameQUIC, NameWebSocket:
@@ -89,6 +96,7 @@ func IsKnown(name string) bool {
}
}
// DecodeOptions 容错解析 JSON 选项;坏数据不会让整个服务列表不可读。
func DecodeOptions(optionsJSON string) map[string]any {
options := map[string]any{}
if strings.TrimSpace(optionsJSON) == "" {
@@ -100,6 +108,7 @@ func DecodeOptions(optionsJSON string) map[string]any {
return options
}
// NormalizeOptions 为不同服务补齐选项默认值,并修正 WebSocket path 这种可恢复输入。
func NormalizeOptions(name string, options map[string]any) map[string]any {
if options == nil {
options = map[string]any{}
@@ -132,6 +141,7 @@ func NormalizeOptions(name string, options map[string]any) map[string]any {
return normalized
}
// IntOption 从 JSON 解码后的 map 中读取整数,兼容 number 和字符串形式。
func IntOption(options map[string]any, key string, fallback int) int {
value, ok := options[key]
if !ok {
@@ -158,6 +168,7 @@ func IntOption(options map[string]any, key string, fallback int) int {
return fallback
}
// StringOption 从 JSON 选项中读取非空字符串。
func StringOption(options map[string]any, key, fallback string) string {
value, ok := options[key]
if !ok {
@@ -169,6 +180,7 @@ func StringOption(options map[string]any, key, fallback string) string {
return fallback
}
// StringSliceOption 从 JSON 选项中读取字符串数组,兼容 []any 的解码结果。
func StringSliceOption(options map[string]any, key string) []string {
value, ok := options[key]
if !ok {

View File

@@ -1,3 +1,5 @@
// internal/adminservice/service_test.go 包含用于约束 service 行为的测试。
package adminservice
import (

View File

@@ -1,3 +1,5 @@
// internal/adminsession/session.go 管理短生命周期的内存管理会话,并在用户状态变化时让相关会话失效。
package adminsession
import (
@@ -8,6 +10,7 @@ import (
)
type Session struct {
// Token 只保存在内存和客户端,不写入 SQLite进程重启会让所有会话失效。
Token string
Username string
Role string
@@ -17,7 +20,8 @@ type Session struct {
type Manager struct {
mu sync.Mutex
sessions map[string]Session
now func() time.Time
// now 可在测试中注入,便于验证过期清理逻辑。
now func() time.Time
}
func NewManager() *Manager {
@@ -36,6 +40,7 @@ func NewManagerWithClock(now func() time.Time) *Manager {
}
func (m *Manager) Create(username, role string, ttl time.Duration) (Session, error) {
// 32 字节随机数再做 URL 安全 base64足够作为 bearer token 使用。
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
return Session{}, err
@@ -62,6 +67,7 @@ func (m *Manager) Get(token string) (Session, bool) {
return Session{}, false
}
if m.now().After(session.ExpiresAt) {
// 读取时顺手清理过期会话,避免后台清理 goroutine。
delete(m.sessions, token)
return Session{}, false
}
@@ -80,6 +86,7 @@ func (m *Manager) RemoveUser(username string) {
for token, session := range m.sessions {
if session.Username == username {
// 用户密码、角色或禁用状态变化后,调用方用该方法使旧 token 立即失效。
delete(m.sessions, token)
}
}

View File

@@ -1,3 +1,5 @@
// internal/adminsession/session_test.go 包含用于约束 session 行为的测试。
package adminsession
import (

View File

@@ -1,3 +1,5 @@
// internal/adminuser/repository.go 在 SQLite 中保存管理用户和密码哈希,并支持角色与禁用状态筛选。
package adminuser
import (

View File

@@ -1,3 +1,5 @@
// internal/adminuser/repository_test.go 包含用于约束 repository 行为的测试。
package adminuser
import (

View File

@@ -1,3 +1,5 @@
// internal/adminuser/user.go 定义管理用户角色、校验规则、密码哈希和对外用户视图。
package adminuser
import (
@@ -7,6 +9,8 @@ import (
)
const (
// 角色按权限从高到低排列admin 管理所有资源member 管理路由和查看运行态,
// guest 只保留基础只读能力。
RoleAdmin = "admin"
RoleMember = "member"
RoleGuest = "guest"
@@ -24,6 +28,7 @@ func ValidateUsername(username string) error {
if username == "" {
return errors.New("username is required")
}
// 用户名会出现在 URL path 和审计记录中,因此禁止空白和斜杠。
if strings.ContainsAny(username, " \t\r\n/") {
return errors.New("username must not contain whitespace or /")
}
@@ -31,6 +36,7 @@ func ValidateUsername(username string) error {
}
func ValidateRole(role string) error {
// 所有角色必须在这里登记,避免数据库里出现管理端无法解释的角色。
switch role {
case RoleAdmin, RoleMember, RoleGuest:
return nil
@@ -40,6 +46,7 @@ func ValidateRole(role string) error {
}
func ValidatePassword(password string) error {
// 当前只做非空校验;更复杂的密码策略应放在产品策略确定后再补。
if strings.TrimSpace(password) == "" {
return errors.New("password is required")
}
@@ -47,6 +54,7 @@ func ValidatePassword(password string) error {
}
func RoleRank(role string) int {
// rank 让权限判断保持单调:高角色天然包含低角色能力。
switch role {
case RoleAdmin:
return 3
@@ -63,6 +71,7 @@ func HasRole(actual, required string) bool {
return RoleRank(actual) >= RoleRank(required)
}
// Permissions 返回前端可直接消费的权限位;后端仍以角色校验为准。
func Permissions(role string) map[string]bool {
return map[string]bool{
"read_routes": HasRole(role, RoleGuest),

View File

@@ -1,3 +1,5 @@
// internal/adminuser/user_test.go 包含用于约束 user 行为的测试。
package adminuser
import (

View File

@@ -1,3 +1,5 @@
// internal/gatewayconfig/config.go 定义运行态管理数据库可用前使用的静态 TOML 配置。
package gatewayconfig
type Config struct {

View File

@@ -1,3 +1,5 @@
// internal/gatewayconfig/plugin.go 定义进程启动时可加载的静态插件配置项。
package gatewayconfig
import "github.com/mitchellh/mapstructure"

View File

@@ -1,3 +1,5 @@
// internal/gatewayconfig/plugin_test.go 包含用于约束 plugin 行为的测试。
package gatewayconfig
import "testing"

View File

@@ -1,3 +1,5 @@
// internal/gatewaymetrics/metrics.go 用原子计数器记录连接、路由和上游错误等管理状态指标。
package gatewaymetrics
import (
@@ -6,6 +8,7 @@ import (
)
type Counters struct {
// 连接级计数使用原子值,避免转发热路径在每次连接开始/结束时争用锁。
totalConnections atomic.Uint64
activeConnections atomic.Int64
tcpConnections atomic.Uint64
@@ -13,6 +16,7 @@ type Counters struct {
routeMisses atomic.Uint64
upstreamDialErrs atomic.Uint64
// routeHits 按 host 聚合,需要 map因此用一把小锁保护。
routeHitsMu sync.Mutex
routeHits map[string]uint64
}
@@ -62,6 +66,7 @@ func (m *Counters) Snapshot() map[string]any {
}
m.routeHitsMu.Unlock()
// 返回普通 map方便 Admin API 直接 JSON 编码。
return map[string]any{
"total_connections": m.totalConnections.Load(),
"active_connections": m.activeConnections.Load(),

View File

@@ -1,3 +1,5 @@
// internal/gatewaymetrics/metrics_test.go 包含用于约束 metrics 行为的测试。
package gatewaymetrics
import "testing"

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/artifact.go 校验、保存、哈希并描述上传到网关的插件制品包。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/artifact_test.go 包含用于约束 artifact 行为的测试。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/builder.go 把源码插件包构建为网关可加载制品,并记录构建元数据。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/extensions.go 索引扩展点注册信息,并分发连接、路由、状态和提供方钩子。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/future.go 建模未来运行时和分发能力,但不把它们接入当前热路径。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/future_test.go 包含用于约束 future 行为的测试。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/gc.go 选择并删除不再被期望状态或活动运行态引用的插件制品。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/governance.go 在涉及发布风险的操作前评估插件评审、公告、供应链和策略门禁。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/governance_test.go 包含用于约束 governance 行为的测试。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/manager.go 协调插件记录、制品加载、钩子分发快照和生命周期迁移。
package pluginmanager
import (
@@ -25,10 +27,13 @@ type RuntimeAdapter interface {
Load(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord, gateway *Gateway) (api.Plugin, error)
}
// ConfigDryRunAdapter 是运行时适配器的可选能力。支持该能力时,配置保存前
// 可以真正实例化插件并调用 ReloadConfig从而提前发现 schema 之外的错误。
type ConfigDryRunAdapter interface {
DryRunConfig(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord) error
}
// GoPluginAdapter 加载 Go plugin 或内置插件,是当前 in-process 插件运行模式的默认实现。
type GoPluginAdapter struct{}
func (a GoPluginAdapter) Load(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord, gateway *Gateway) (api.Plugin, error) {
@@ -82,6 +87,8 @@ func (a GoPluginAdapter) instantiate(ctx context.Context, artifact ArtifactRecor
if artifact.RuntimeType == RuntimeBuiltin || artifact.PluginID == "official.rule-policy" {
return instantiateBuiltinPlugin(artifact, pluginRecord, gateway, init)
}
// Go plugin 只能加载与当前进程 Go 版本、架构和 ABI 匹配的 .so 文件。
// 这些兼容性检查在制品校验和构建阶段完成,这里只负责打开和实例化。
opened, err := stdplugin.Open(artifact.FilePath)
if err != nil {
return nil, err
@@ -91,6 +98,8 @@ func (a GoPluginAdapter) instantiate(ctx context.Context, artifact ArtifactRecor
if err := json.Unmarshal([]byte(artifact.MetadataJSON), &manifest); err == nil && manifest.Runtime.EntrySymbol != "" {
symbolName = manifest.Runtime.EntrySymbol
}
// 默认入口符号是 Plugin也允许 manifest 指定自定义入口,便于未来兼容
// 不同构建工具生成的插件包。
symbol, err := opened.Lookup(symbolName)
if err != nil {
return nil, err
@@ -100,6 +109,8 @@ func (a GoPluginAdapter) instantiate(ctx context.Context, artifact ArtifactRecor
return nil, fmt.Errorf("plugin symbol %q has invalid signature", symbolName)
}
instance := factory()
// 插件配置对象由插件自己声明;宿主只负责把持久化 JSON 解入该对象,
// 再交给 ReloadConfig 做插件内部校验。
cfg := instance.NewConfigObj()
if cfg != nil && pluginRecord.ConfigJSON != "" && canUnmarshalInto(cfg) {
if err := json.Unmarshal([]byte(pluginRecord.ConfigJSON), cfg); err != nil {
@@ -117,6 +128,8 @@ func (a GoPluginAdapter) instantiate(ctx context.Context, artifact ArtifactRecor
return instance, nil
}
// instantiateBuiltinPlugin 让官方内置插件走同一套 Plugin 接口和配置流程,
// 避免在调用路径上区分内置插件与外部上传插件。
func instantiateBuiltinPlugin(artifact ArtifactRecord, pluginRecord PluginRecord, gateway *Gateway, init bool) (api.Plugin, error) {
var instance api.Plugin
switch artifact.PluginID {
@@ -166,17 +179,21 @@ type Manager struct {
routeCacheMu sync.Mutex
routeCache map[string]routeCacheEntry
// proxyConns 只跟踪由插件托管代理的连接,用于停用插件时的 drain 和强制关闭。
proxyMu sync.Mutex
proxySeq uint64
proxyConns map[uint64]*proxyConnection
drainingIDs map[string]bool
operations *Operations
// serviceMode/hosts 预留给插件运行时从进程内迁移到独立宿主的服务模式。
serviceMode string
hostMu sync.Mutex
hosts map[string]*pluginHostProcess
}
// loadedPlugin 是内存中的插件实例和它注册的扩展快照。数据库记录说明期望状态,
// loadedPlugin 说明当前进程实际已经加载了什么。
type loadedPlugin struct {
record PluginRecord
artifact ArtifactRecord
@@ -186,6 +203,7 @@ type loadedPlugin struct {
extensions pluginExtensions
}
// pluginExtensions 按扩展类型拆分注册结果,便于发布不可变快照给不同热路径使用。
type pluginExtensions struct {
routes []*routeHandler
statuses []*statusHandler
@@ -194,6 +212,7 @@ type pluginExtensions struct {
providers []ProviderSummary
}
// upstreamHandler 包装一个上游连接钩子,并保存调用、错误、超时和代理流量指标。
type upstreamHandler struct {
pluginID string
artifactID string
@@ -256,6 +275,8 @@ type Options struct {
PolicyProfile string
}
// New 构造插件管理器并初始化内存快照。官方内置插件和插件服务模式会在这里
// 尽力注册/应用,失败不会阻止网关启动,后续 Admin API 仍可修复状态。
func New(options Options) *Manager {
adapter := options.Adapter
if adapter == nil {
@@ -277,6 +298,7 @@ func New(options Options) *Manager {
}
manager.operations = NewOperations(manager.repo, options.ArtifactRoot)
if manager.builders == nil {
// 默认同时提供本地进程构建和容器构建能力;部署方可在 Options 中收窄。
manager.builders = map[string]SourceBuilder{
BuilderTypeLocalProcess: LocalProcessBuilder{StoreRoot: options.ArtifactRoot},
BuilderTypeContainer: ContainerBuilder{},
@@ -289,6 +311,8 @@ func New(options Options) *Manager {
return manager
}
// EnsureOfficialPlugins 将内置官方插件登记为普通制品记录。这样 UI、治理、
// 配置和启停流程都可以复用同一套插件管理模型。
func (m *Manager) EnsureOfficialPlugins(ctx context.Context, actor string) error {
now := time.Now().Unix()
manifest := Manifest{
@@ -341,6 +365,8 @@ func (m *Manager) EnsureOfficialPlugins(ctx context.Context, actor string) error
return nil
}
// UploadArtifact 校验并保存二进制插件制品;源码包会转交给源码保存流程,
// 因为源码上传后还需要自动排队构建。
func (m *Manager) UploadArtifact(ctx context.Context, upload ArtifactUpload) (ArtifactRecord, error) {
artifact, err := m.store.ValidateAndStore(upload)
if err != nil {
@@ -362,6 +388,8 @@ func (m *Manager) UploadArtifact(ctx context.Context, upload ArtifactUpload) (Ar
return artifact, nil
}
// UploadSource 保存源码插件包并创建构建记录。真正构建可以立即运行,也可以
// 由管理端稍后触发 RunBuild。
func (m *Manager) UploadSource(ctx context.Context, upload ArtifactUpload) (ArtifactRecord, error) {
artifact, err := m.store.ValidateAndStoreSource(upload)
if err != nil {
@@ -406,6 +434,7 @@ func (m *Manager) CreateBuild(ctx context.Context, actor string, req BuildReques
return BuildRecord{}, fmt.Errorf("artifact %s is %q, want source", source.ID, source.ArtifactType)
}
req = defaultBuildRequest(req, source)
// Go plugin 与宿主进程存在 ABI 约束,目前只允许构建当前网关所在平台的目标。
if req.GOOS != runtime.GOOS || req.GOARCH != runtime.GOARCH {
return BuildRecord{}, fmt.Errorf("build target %s/%s does not match gateway %s/%s", req.GOOS, req.GOARCH, runtime.GOOS, runtime.GOARCH)
}
@@ -449,6 +478,8 @@ func (m *Manager) CreateBuild(ctx context.Context, actor string, req BuildReques
return build, nil
}
// RunBuild 执行已排队的源码构建,并把产出的二进制制品重新写入制品仓库。
// 构建记录始终会落库,失败时也会保存日志摘要,便于管理端诊断。
func (m *Manager) RunBuild(ctx context.Context, actor string, buildID int64) (BuildRecord, error) {
build, err := m.repo.Build(ctx, buildID)
if err != nil {
@@ -559,11 +590,14 @@ func (m *Manager) RunBuild(ctx context.Context, actor string, buildID int64) (Bu
return m.repo.Build(ctx, build.ID)
}
// SetDesired 只修改插件的期望状态,不直接改变当前进程已加载的插件。
// 调用方需要再执行 Enable/Disable/Reconcile 才会推动运行态收敛。
func (m *Manager) SetDesired(ctx context.Context, actor, pluginID, artifactID, desiredState, configJSON string, priority int) (PluginRecord, error) {
if desiredState == "" {
desiredState = DesiredDisabled
}
if desiredState != DesiredDeleted {
// 任何非删除状态都先做配置 dry-run避免把无法加载的配置写成新的期望状态。
if _, err := m.DryRunConfig(ctx, pluginID, artifactID, configJSON); err != nil {
_ = m.repo.RecordOperation(ctx, pluginID, artifactID, "config_dry_run", "failed", actor, err.Error(), map[string]any{
"active_changed": false,
@@ -584,6 +618,8 @@ func (m *Manager) SetDesired(ctx context.Context, actor, pluginID, artifactID, d
return pluginRecord, nil
}
// DryRunConfig 执行保存配置前的完整预检JSON 合法性、制品归属、治理门禁、
// schema、密钥引用以及运行时 ReloadConfig 都会在这里验证。
func (m *Manager) DryRunConfig(ctx context.Context, pluginID, artifactID, configJSON string) (ConfigDryRunResult, error) {
result := ConfigDryRunResult{
OK: false,
@@ -631,6 +667,7 @@ func (m *Manager) DryRunConfig(ctx context.Context, pluginID, artifactID, config
return result, err
}
if dryRunner, ok := m.adapter.(ConfigDryRunAdapter); ok {
// 运行时 dry-run 会实例化插件但不调用 Init避免注册钩子或启动后台任务。
if err := dryRunner.DryRunConfig(ctx, artifact, pluginRecord); err != nil {
result.Error = err.Error()
return result, err
@@ -758,6 +795,8 @@ func (m *Manager) Load(ctx context.Context, actor, pluginID string) (PluginRecor
m.mu.Lock()
defer m.mu.Unlock()
// Load 只把插件实例化到内存并登记为 loaded不发布到热路径。
// 管理端可用它验证制品和配置,而不立即影响在线连接。
pluginRecord, err := m.repo.Plugin(ctx, pluginID)
if err != nil {
return PluginRecord{}, err
@@ -771,6 +810,8 @@ func (m *Manager) Load(ctx context.Context, actor, pluginID string) (PluginRecor
return m.repo.Plugin(ctx, pluginID)
}
// Enable 将期望状态推进为启用,并把插件处理器发布到连接热路径。
// 发布前会先通过治理门禁,避免高风险制品绕过评审直接生效。
func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRecord, error) {
pluginRecord, err := m.repo.Plugin(ctx, pluginID)
if err != nil {
@@ -797,6 +838,8 @@ func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRec
m.mu.Lock()
defer m.mu.Unlock()
// 真正加载与发布都在同一把锁内完成,保证 snapshot、extensions 和 loaded
// 三类内存状态不会被并发读到半更新结果。
loaded, err := m.loadLocked(ctx, pluginRecord)
if err != nil {
_ = m.repo.RecordOperation(ctx, pluginID, pluginRecord.DesiredArtifactID, "enable", "failed", actor, err.Error(), nil)
@@ -817,6 +860,8 @@ func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRec
if err := m.markEnabled(ctx, loaded); err != nil {
return PluginRecord{}, err
}
// 数据库运行态先写成功,再发布内存快照;这样 UI 看到 enabled 时,
// 连接热路径也已经具备对应处理器。
m.markHostStarted(pluginID, loaded.artifact.ID)
m.clearDrainingLocked(pluginID)
m.publish(next)
@@ -830,6 +875,8 @@ func (m *Manager) Enable(ctx context.Context, actor, pluginID string) (PluginRec
return m.repo.Plugin(ctx, pluginID)
}
// Disable 从热路径移除插件并进入 drain。Go plugin 不能从进程卸载,
// 因此这里停止任务、移除分发入口,并等待已有 protocol-proxy 连接结束。
func (m *Manager) Disable(ctx context.Context, actor, pluginID string) (PluginRecord, error) {
m.mu.Lock()
defer m.mu.Unlock()
@@ -844,6 +891,8 @@ func (m *Manager) Disable(ctx context.Context, actor, pluginID string) (PluginRe
}
m.removeFromDispatchLocked(pluginID)
m.removeExtensionsLocked(pluginID)
// 先标记 draining再 Destroy 插件实例,确保后续管理操作能看到仍在
// 转发中的插件代理连接。
m.markDrainingLocked(pluginID)
m.markHostDraining(pluginID)
m.operations.StopPlugin(pluginID)
@@ -866,6 +915,8 @@ func (m *Manager) Disable(ctx context.Context, actor, pluginID string) (PluginRe
return m.repo.Plugin(ctx, pluginID)
}
// Delete 与 Disable 类似,但把期望状态写为 deleted。实际制品清理仍由 GC
// 根据引用关系判断,避免删除仍被快照或历史操作引用的文件。
func (m *Manager) Delete(ctx context.Context, actor, pluginID string) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -892,6 +943,8 @@ func (m *Manager) Delete(ctx context.Context, actor, pluginID string) error {
return nil
}
// Reconcile 根据数据库中的期望启用列表重建内存分发快照,主要用于进程启动
// 或运行态状态漂移后的自愈。
func (m *Manager) Reconcile(ctx context.Context) error {
m.mu.Lock()
defer m.mu.Unlock()
@@ -903,6 +956,7 @@ func (m *Manager) Reconcile(ctx context.Context) error {
nextByPlugin := make(map[string][]*upstreamHandler)
extensionsByPlugin := make(map[string]pluginExtensions)
for _, pluginRecord := range desired {
// 单个插件失败不阻断其他插件收敛;失败会记录到 runtime_state 和操作日志。
decision, err := m.EvaluateGovernance(ctx, pluginRecord.ID, pluginRecord.DesiredArtifactID, GovernanceActionEnable, m.currentPolicyProfile(), pluginRecord.ConfigJSON)
if err == nil && !decision.OK {
err = governanceBlockedError(decision)
@@ -930,11 +984,14 @@ func (m *Manager) Reconcile(ctx context.Context) error {
m.markHostStarted(pluginRecord.ID, loaded.artifact.ID)
m.clearDrainingLocked(pluginRecord.ID)
}
// 所有插件都处理完后一次性发布快照,避免热路径在收敛过程中看到部分插件。
m.publish(flattenHandlers(nextByPlugin))
m.publishExtensionsLocked(extensionsByPlugin)
return nil
}
// ConnectUpstream 依次调用当前快照中的上游连接处理器。处理器返回 ErrPass
// 表示让下一个插件继续尝试,返回连接则由网关使用插件提供的上游。
func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRequest) (UpstreamResult, error) {
value := m.snapshot.Load()
if value == nil {
@@ -949,6 +1006,7 @@ func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRe
}
req.InitialData = append([]byte(nil), req.InitialData...)
for _, handler := range handlers {
// accept 阶段应尽量轻量,用于快速过滤不关心的主机或上游。
accepted, err := handler.accepts(req)
if err != nil {
return UpstreamResult{Handled: true}, err
@@ -983,6 +1041,8 @@ func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRe
return UpstreamResult{Handled: true}, err
}
if conn != nil {
// protocol-proxy 模式由插件代理完整协议流;普通 dialer 模式只提供
// 已连接的上游 net.Conn后续转发仍由网关主流程完成。
if handler.mode == UpstreamModeDialer {
_ = m.repo.SaveTrace(context.Background(), TraceSummary{
PluginID: handler.pluginID,
@@ -1010,6 +1070,8 @@ func (m *Manager) ConnectUpstream(ctx context.Context, req api.UpstreamConnectRe
return UpstreamResult{}, nil
}
// startProtocolProxy 把客户端连接交给插件提供的协议代理端点。网关仍跟踪连接,
// 以便停用插件时可以 drain 或强制关闭。
func (m *Manager) startProtocolProxy(ctx context.Context, handler *upstreamHandler, result UpstreamResult, req api.UpstreamConnectRequest) (UpstreamResult, error) {
endpoint := result.Conn
initial := append([]byte(nil), req.InitialData...)
@@ -1454,6 +1516,7 @@ func (m *Manager) findHandler(pluginID, handlerID string) *upstreamHandler {
}
func (m *Manager) finishProxyConnection(id uint64, stats ProxyConnectionStats) {
// 代理连接结束时汇总字节数和耗时,供 Admin UI 展示插件代理健康情况。
m.proxyMu.Lock()
proxyConn := m.proxyConns[id]
delete(m.proxyConns, id)
@@ -1477,10 +1540,13 @@ func (m *Manager) finishProxyConnection(id uint64, stats ProxyConnectionStats) {
}
}
// loadLocked 加载或复用插件实例。调用方必须持有 m.mu确保 loaded 缓存和
// 运行态标记不会与 Enable/Disable/Reconcile 并发冲突。
func (m *Manager) loadLocked(ctx context.Context, pluginRecord PluginRecord) (*loadedPlugin, error) {
if loaded := m.loaded[pluginRecord.ID]; loaded != nil &&
loaded.artifact.ID == pluginRecord.DesiredArtifactID &&
loaded.record.DesiredGeneration == pluginRecord.DesiredGeneration {
// 同一制品、同一期望代数已经加载时直接复用,避免重复 Init 和重复注册任务。
return loaded, nil
}
artifact, err := m.repo.Artifact(ctx, pluginRecord.DesiredArtifactID)
@@ -1503,6 +1569,7 @@ func (m *Manager) loadLocked(ctx context.Context, pluginRecord PluginRecord) (*l
}
handlers := buildHandlers(pluginRecord, artifact, gateway)
extensions := buildExtensions(pluginRecord, artifact, gateway)
// 钩子和扩展是从 gateway 注册记录中构建出来的;插件 Init 期间完成注册。
loaded := &loadedPlugin{
record: pluginRecord,
artifact: artifact,
@@ -1523,6 +1590,8 @@ func (m *Manager) loadLocked(ctx context.Context, pluginRecord PluginRecord) (*l
return loaded, nil
}
// validateArtifactGate 确认制品能被当前网关进程加载。Go plugin 对 Go 版本和
// 目标平台敏感,沙箱/wasm 运行时则受插件服务模式控制。
func (m *Manager) validateArtifactGate(artifact ArtifactRecord) error {
if artifact.Status == ArtifactStatusDeleted || artifact.Status == ArtifactStatusRejected {
return fmt.Errorf("artifact status %q is not loadable", artifact.Status)

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/manager_test.go 包含用于约束 manager 行为的测试。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/operations.go 实现插件运行时运维能力,包括事件、指标、文件/数据存储、外部客户端、任务和诊断。
package pluginmanager
import (
@@ -26,11 +28,13 @@ import (
)
const (
// 简单熔断状态使用字符串保存,便于直接落库和输出到诊断包。
circuitClosed = "closed"
circuitOpen = "open"
)
var (
// 插件上报的指标名和存储 key 需要收敛到可观测系统容易消费的字符集。
metricNamePattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_.:-]{0,127}$`)
storeKeyPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.:/-]{0,255}$`)
)
@@ -48,9 +52,11 @@ type Operations struct {
repo Repository
root string
// plugins 保存每个插件的运行时运维上下文,按 manifest 重新配置但保留统计摘要。
mu sync.RWMutex
plugins map[string]*PluginOperations
// eventQueue 负责把插件事件异步落库,避免连接热路径被 SQLite 写入阻塞。
eventQueue chan queuedEvent
queued atomic.Uint64
dropped atomic.Uint64
@@ -61,6 +67,7 @@ type Operations struct {
subscriberDropped atomic.Uint64
subscriberDeadLetter atomic.Uint64
// subscribers 是当前启用插件注册的事件订阅者快照,由 Manager 发布。
subscriberMu sync.RWMutex
subscribers []*subscriberHandler
}
@@ -81,6 +88,7 @@ type PluginOperations struct {
artifactID string
manifest Manifest
// 下列 schema 来自 manifest用于在插件运行时限制事件、指标、任务和外部依赖。
mu sync.Mutex
eventSchemas map[string]map[string]bool
metricSchemas map[string]MetricSpec
@@ -100,6 +108,7 @@ type PluginOperations struct {
type externalRuntime struct {
spec ExternalSpec
// 外部依赖统计用于诊断包和熔断策略,全部用原子值减少请求路径锁竞争。
requests atomic.Uint64
errors atomic.Uint64
inflight atomic.Int64
@@ -120,6 +129,7 @@ type taskRuntime struct {
task api.BackgroundTask
confirmToken string
// 每个后台任务独立持有调度状态和 cancel 函数,插件停用时可逐个停止。
mu sync.Mutex
cancel context.CancelFunc
running bool
@@ -132,6 +142,7 @@ type taskRuntime struct {
consecutiveFailures uint64
}
// NewOperations 创建插件运维协调器,并启动事件落库和订阅投递两个后台消费者。
func NewOperations(repo Repository, root string) *Operations {
ops := &Operations{
repo: repo,
@@ -145,6 +156,8 @@ func NewOperations(repo Repository, root string) *Operations {
return ops
}
// SetSubscribers 用新的订阅者快照替换旧快照。Manager 在插件启停后调用它,
// 事件消费者只读取快照副本,不直接依赖 Manager 锁。
func (o *Operations) SetSubscribers(subscribers []*subscriberHandler) {
o.subscriberMu.Lock()
defer o.subscriberMu.Unlock()
@@ -164,6 +177,7 @@ func (o *Operations) ForPlugin(pluginID, artifactID string, manifest Manifest) *
defer o.mu.Unlock()
po := o.plugins[pluginID]
if po == nil {
// 首次看到插件时创建运维上下文;后续版本切换会复用它的近期统计。
po = &PluginOperations{
parent: o,
pluginID: pluginID,
@@ -200,6 +214,7 @@ func (o *Operations) StartTasks(pluginID string) {
func (o *Operations) consumeEvents() {
for event := range o.eventQueue {
o.queued.Add(1)
// 落库使用短超时,避免后台消费者在数据库异常时堆积过久。
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
err := o.repo.SaveEvent(ctx, EventSummary{
PluginID: event.pluginID,
@@ -217,6 +232,7 @@ func (o *Operations) queueEvent(event queuedEvent) {
select {
case o.eventQueue <- event:
default:
// 队列满时仍写一条 dropped 记录,保留“发生过丢弃”的审计线索。
o.dropped.Add(1)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
_ = o.repo.SaveEvent(ctx, EventSummary{
@@ -230,6 +246,7 @@ func (o *Operations) queueEvent(event queuedEvent) {
}
func (o *Operations) queueSubscriberEvent(event queuedEvent) {
// 已标记 dropped 的事件只进入持久化路径,不再交给订阅者重复处理。
o.subscriberMu.RLock()
hasSubscribers := len(o.subscribers) > 0
o.subscriberMu.RUnlock()
@@ -275,6 +292,7 @@ func (o *Operations) deliverSubscriberEvent(subscriber *subscriberHandler, event
if maxRetry <= 0 {
maxRetry = DefaultSubscriberMaxRetry
}
// best_effort 订阅最多投递一次at_least_once 按订阅者配置进行有限重试。
for attempt := 1; attempt <= maxRetry; attempt++ {
req.Attempt = attempt
result, err := subscriber.invoke(req)
@@ -293,6 +311,8 @@ func (o *Operations) deliverSubscriberEvent(subscriber *subscriberHandler, event
})
}
// configure 根据 manifest 重新构建插件运维能力边界。统计对象尽量复用,
// 但 schema、配额和外部依赖声明每次都以当前制品为准。
func (po *PluginOperations) configure(artifactID string, manifest Manifest) {
po.mu.Lock()
defer po.mu.Unlock()
@@ -352,12 +372,15 @@ func (po *PluginOperations) configure(artifactID string, manifest Manifest) {
}
}
if len(po.fileSpecs) == 0 {
// 未声明文件存储时提供默认命名空间,方便简单插件直接使用常见分类。
for _, namespace := range []string{"data", "cache", "tmp", "log", "diagnostic"} {
po.fileSpecs[namespace] = FileStoreSpec{Namespace: namespace, QuotaBytes: DefaultPluginFileQuota}
}
}
}
// EmitEvent 校验并记录插件事件。即使字段非法,也会排队一条 dropped 事件,
// 便于诊断插件为什么没有产出预期事件。
func (po *PluginOperations) EmitEvent(ctx context.Context, name string, fields map[string]string) error {
trace := traceFromContext(ctx)
clean, dropReason, err := po.validateEvent(name, fields)
@@ -385,6 +408,8 @@ func (po *PluginOperations) EmitEvent(ctx context.Context, name string, fields m
return nil
}
// ObserveMetric 校验并更新插件自定义指标的最近摘要;这里不做时序存储,
// 只维护管理界面需要的当前观测值。
func (po *PluginOperations) ObserveMetric(ctx context.Context, name string, value float64, labels map[string]string) error {
_ = ctx
clean, metricType, err := po.validateMetric(name, labels)
@@ -443,6 +468,8 @@ func (po *PluginOperations) RegisterBackgroundTask(task api.BackgroundTask) erro
defer po.mu.Unlock()
spec := po.taskSpecs[task.ID]
if len(po.taskSpecs) > 0 && spec.ID == "" {
// manifest 声明了任务清单时,只允许注册清单中的任务,避免插件运行时
// 动态创建管理端不可见的后台任务。
return fmt.Errorf("background task %q is not declared by manifest", task.ID)
}
if spec.ID == "" {
@@ -471,6 +498,7 @@ func (po *PluginOperations) RegisterBackgroundTask(task api.BackgroundTask) erro
}
rt := po.tasks[task.ID]
if rt == nil {
// confirmToken 用于高风险手动任务的二次确认,避免误点直接执行。
rt = &taskRuntime{
pluginID: po.pluginID,
spec: spec,
@@ -483,6 +511,8 @@ func (po *PluginOperations) RegisterBackgroundTask(task api.BackgroundTask) erro
return nil
}
// StartTasks 启动当前插件注册的后台任务调度器。只在插件 ID 匹配时执行,
// 防止调用方传错 ID 时启动其他插件的任务。
func (po *PluginOperations) StartTasks(pluginID string) {
if pluginID != po.pluginID {
return
@@ -498,6 +528,7 @@ func (po *PluginOperations) StartTasks(pluginID string) {
}
}
// stopTasks 停止所有后台任务调度器。已经在执行的任务通过 cancel 感知停用。
func (po *PluginOperations) stopTasks() {
po.mu.Lock()
tasks := make([]*taskRuntime, 0, len(po.tasks))
@@ -536,6 +567,7 @@ func (po *PluginOperations) startTask(task *taskRuntime) {
}
go func() {
for {
// 抖动值按任务 ID 确定,避免多个网关实例同一时间集中触发相同任务。
delay := interval + deterministicJitter(task.task.Jitter, task.task.ID)
task.mu.Lock()
if !task.schedulerOn {
@@ -560,6 +592,7 @@ func (po *PluginOperations) startTask(task *taskRuntime) {
func (po *PluginOperations) runTask(task *taskRuntime) {
task.mu.Lock()
if task.running {
// 同一任务不并发执行;调度周期追上时只记录跳过次数。
task.skipped++
task.mu.Unlock()
return
@@ -592,6 +625,8 @@ func (po *PluginOperations) runTask(task *taskRuntime) {
task.mu.Unlock()
}
// TriggerTask 手动触发后台任务。confirmToken 来自任务摘要,调用方必须显式回传,
// 用来降低误触发有副作用任务的风险。
func (po *PluginOperations) TriggerTask(taskID, confirmToken string) (BackgroundTaskSummary, error) {
po.mu.Lock()
task := po.tasks[taskID]
@@ -609,6 +644,8 @@ func (po *PluginOperations) TriggerTask(taskID, confirmToken string) (Background
return task.summary(), nil
}
// Snapshot 汇总插件运行态观测信息。内存中的近期摘要和数据库中的历史摘要会合并,
// 形成管理端和诊断包都能使用的一份视图。
func (po *PluginOperations) Snapshot(ctx context.Context, pluginID string, handlers []DispatchHandlerSummary, builds []BuildRecord, gc []GCCandidate) OperationsSnapshot {
_ = ctx
po.mu.Lock()
@@ -632,6 +669,7 @@ func (po *PluginOperations) Snapshot(ctx context.Context, pluginID string, handl
recentEvents, _ := po.parent.repo.RecentEvents(ctx, pluginID, DefaultEventRecentLimit)
if len(recentEvents) > 0 {
// 数据库中的事件能覆盖进程重启前的近期历史,内存摘要则提供当前进程最新值。
events = mergeEventSummaries(events, recentEvents)
}
logs, _ := po.parent.repo.RecentLogs(ctx, pluginID, DefaultLogRecentLimit)
@@ -702,6 +740,8 @@ func (o *Operations) TriggerTask(pluginID, taskID, confirmToken string) (Backgro
return po.TriggerTask(taskID, confirmToken)
}
// DiagnosticPackage 生成可下载的插件诊断包。输出前会统一脱敏,避免把密钥、
// token 或完整协议载荷写入可共享文件。
func (o *Operations) DiagnosticPackage(ctx context.Context, plugin PluginRecord, manifest Manifest, handlers []DispatchHandlerSummary, builds []BuildRecord, gc []GCCandidate) ([]byte, DiagnosticPackageSummary, error) {
snapshot := o.Snapshot(ctx, plugin.ID, handlers, builds, gc)
operations, _ := o.repo.ListOperations(ctx, plugin.ID, 50)
@@ -745,6 +785,8 @@ func (o *Operations) runtimeRoot() string {
func (o *Operations) GCCandidates(ctx context.Context, pluginID string) ([]GCCandidate, error) {
now := time.Now().Unix()
var candidates []GCCandidate
// 插件数据和文件只有在过期后才允许删除;未过期记录作为受保护候选项返回,
// 方便 dry-run 解释为什么没有删除它们。
data, err := o.repo.ListPluginData(ctx, pluginID)
if err != nil {
return nil, err
@@ -799,6 +841,7 @@ func (o *Operations) GCCandidates(ctx context.Context, pluginID string) ([]GCCan
if seenFiles[filePath] {
return nil
}
// 文件系统里存在但仓库没有记录的文件视为孤儿文件,可以由 GC 清理。
info, err := d.Info()
if err != nil {
return nil
@@ -842,6 +885,8 @@ func (o *Operations) GCCandidates(ctx context.Context, pluginID string) ([]GCCan
return candidates, nil
}
// RunGC 执行插件运维数据清理。dryRun 只返回候选项并写操作日志,
// 真正删除时会跳过受保护项。
func (o *Operations) RunGC(ctx context.Context, actor, pluginID string, dryRun bool) ([]GCCandidate, error) {
candidates, err := o.GCCandidates(ctx, pluginID)
if err != nil {
@@ -874,6 +919,8 @@ func (o *Operations) RunGC(ctx context.Context, actor, pluginID string, dryRun b
return removed, nil
}
// validateEvent 校验事件声明和字段集合,并限制字段值基数,避免插件事件把
// 管理端和后续指标系统拖入高基数数据。
func (po *PluginOperations) validateEvent(name string, fields map[string]string) (map[string]string, string, error) {
if !metricNamePattern.MatchString(name) {
return nil, "invalid_event_name", fmt.Errorf("invalid event name %q", name)
@@ -907,6 +954,7 @@ func (po *PluginOperations) validateEvent(name string, fields map[string]string)
return clean, "", nil
}
// validateMetric 校验自定义指标名和标签,确保插件只能上报 manifest 声明过的指标。
func (po *PluginOperations) validateMetric(name string, labels map[string]string) (map[string]string, string, error) {
if !metricNamePattern.MatchString(name) {
return nil, "", fmt.Errorf("invalid metric name %q", name)
@@ -954,6 +1002,7 @@ func (l pluginLogger) write(ctx context.Context, level, message string, fields m
}
trace := traceFromContext(ctx)
clean, _ := sanitizeLabels(fields, nil)
// 插件日志只保存摘要并脱敏,避免把完整请求、密钥或 token 写入运行态数据库。
item := LogSummary{
PluginID: l.ops.pluginID,
Level: level,
@@ -989,6 +1038,7 @@ func (s pluginDataStore) Put(ctx context.Context, record api.DataRecord) error {
}
current, _ := s.ops.parent.repo.PluginDataUsage(ctx, s.ops.pluginID)
old, _, _ := s.ops.parent.repo.GetPluginData(ctx, s.ops.pluginID, key)
// 更新已有 key 时只计算净增长,避免重复写同一 key 被误判为超配额。
nextUsage := current - old.SizeBytes + int64(len(record.Value))
if nextUsage > s.ops.dataQuota {
return fmt.Errorf("plugin_data quota exceeded: %d > %d", nextUsage, s.ops.dataQuota)
@@ -1024,7 +1074,8 @@ func (s pluginDataStore) Get(ctx context.Context, key string) (api.DataRecord, e
return api.DataRecord{}, err
}
return api.DataRecord{
Key: record.Key,
Key: record.Key,
// 返回副本,避免调用方修改仓库层读取出来的缓冲区。
Value: append([]byte(nil), value...),
SchemaVersion: record.SchemaVersion,
DataClass: record.DataClass,
@@ -1060,6 +1111,7 @@ func (s pluginFileStore) ResourcePath(name string) (string, error) {
if !isSubpath(filepath.Join(artifactDir, "resources"), resource) {
return "", errors.New("unsafe resource path")
}
// ResourcePath 只返回随制品发布的只读资源路径,不写运行态文件记录。
return resource, nil
}
@@ -1091,6 +1143,7 @@ func (s pluginFileStore) Write(ctx context.Context, namespace, name string, data
if !isSubpath(root, target) {
return errors.New("unsafe file path")
}
// 路径校验后再创建目录,防止插件通过 ../ 写出自己的命名空间。
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
@@ -1174,6 +1227,7 @@ func (s pluginFileStore) Delete(ctx context.Context, namespace, name string) err
func (s pluginFileStore) namespaceSpec(namespace string) (string, FileStoreSpec, error) {
namespace = strings.TrimSpace(namespace)
if namespace == "" {
// 默认命名空间让简单插件不必显式声明每次读写的分类。
namespace = "data"
}
if !metricNamePattern.MatchString(namespace) {
@@ -1217,6 +1271,7 @@ func (c pluginExternalClient) DoHTTP(ctx context.Context, req api.ExternalReques
if err := c.beforeRequest(); err != nil {
return api.ExternalResponse{}, err
}
// beforeRequest 会增加 inflight后续必须在 defer 中成对减少。
start := time.Now()
defer c.runtime.inflight.Add(-1)
@@ -1239,6 +1294,7 @@ func (c pluginExternalClient) DoHTTP(ctx context.Context, req api.ExternalReques
if spec.Traceparent {
trace := traceFromContext(ctx)
if trace.TraceID != "" {
// 只透传 trace id不暴露内部 connection id 或插件处理器 id。
httpReq.Header.Set("traceparent", "00-"+limitHex(trace.TraceID, 32)+"-0000000000000000-01")
}
}
@@ -1254,6 +1310,7 @@ func (c pluginExternalClient) DoHTTP(ctx context.Context, req api.ExternalReques
if lastErr == nil && resp.StatusCode < 500 {
break
}
// 需要关闭失败响应体,避免重试时泄漏连接。
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
@@ -1297,6 +1354,7 @@ func (c pluginExternalClient) DialTCP(ctx context.Context, address string, timeo
}
start := time.Now()
defer c.runtime.inflight.Add(-1)
// 外部 TCP 连接返回给插件后由插件负责关闭;这里仅记录拨号阶段的观测信息。
var dialer net.Dialer
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
@@ -1308,6 +1366,8 @@ func (c pluginExternalClient) DialTCP(ctx context.Context, address string, timeo
return conn, nil
}
// HealthCheck 使用 manifest 声明的 endpoint 做轻量检查。TCP 依赖只建立后关闭,
// HTTP 依赖优先使用 HEAD避免拉取大响应体。
func (c pluginExternalClient) HealthCheck(ctx context.Context) error {
spec, err := c.declaredSpec()
if err != nil {
@@ -1343,6 +1403,8 @@ func (c pluginExternalClient) declaredSpec() (ExternalSpec, error) {
return spec, nil
}
// beforeRequest 检查熔断窗口并记录并发请求数。成功进入请求路径后,
// 调用方必须在结束时减少 inflight。
func (c pluginExternalClient) beforeRequest() error {
c.runtime.mu.Lock()
defer c.runtime.mu.Unlock()
@@ -1354,6 +1416,8 @@ func (c pluginExternalClient) beforeRequest() error {
return nil
}
// finish 更新外部依赖统计并维护一个简单熔断器。连续三次失败会短暂打开熔断,
// 防止插件把故障依赖打爆。
func (c pluginExternalClient) finish(start time.Time, status string, err error) {
duration := time.Since(start)
c.runtime.durationCount.Add(1)
@@ -1376,6 +1440,7 @@ func (c pluginExternalClient) finish(start time.Time, status string, err error)
c.runtime.circuitUntil = time.Time{}
}
// recordTrace 把外部依赖调用写入 trace 摘要endpoint 和 purpose 会先脱敏。
func (c pluginExternalClient) recordTrace(ctx context.Context, start time.Time, kind, status string) {
trace := traceFromContext(ctx)
_ = c.ops.parent.repo.SaveTrace(context.Background(), TraceSummary{
@@ -1392,6 +1457,7 @@ func (c pluginExternalClient) recordTrace(ctx context.Context, start time.Time,
})
}
// summary 返回外部依赖的可展示状态,并隐藏 endpoint 中可能带账号的信息。
func (rt *externalRuntime) summary(pluginID, name string) ExternalDependencySummary {
rt.mu.Lock()
defer rt.mu.Unlock()
@@ -1420,6 +1486,7 @@ func (rt *externalRuntime) summary(pluginID, name string) ExternalDependencySumm
}
}
// summary 返回后台任务的当前调度状态,供运维快照和管理端展示。
func (rt *taskRuntime) summary() BackgroundTaskSummary {
rt.mu.Lock()
defer rt.mu.Unlock()
@@ -1450,6 +1517,8 @@ func (rt *taskRuntime) summary() BackgroundTaskSummary {
}
}
// WithTraceContext 把插件处理链路信息塞入 context供日志、事件和外部依赖
// 记录复用同一个 trace/connection 标识。
func WithTraceContext(ctx context.Context, pluginID, traceID, connectionID, handlerID string) context.Context {
return context.WithValue(ctx, traceContextKey{}, traceContext{
PluginID: pluginID,
@@ -1467,6 +1536,8 @@ func traceFromContext(ctx context.Context) traceContext {
return trace
}
// sanitizeLabels 过滤插件上报字段:数量、名称、声明范围、敏感字段和单值长度
// 都会被限制,避免低成本插件事件变成高基数或敏感数据出口。
func sanitizeLabels(fields map[string]string, allowed map[string]bool) (map[string]string, error) {
if len(fields) == 0 {
return map[string]string{}, nil
@@ -1497,6 +1568,7 @@ func sanitizeLabels(fields map[string]string, allowed map[string]bool) (map[stri
return clean, nil
}
// cleanStoreKey 校验插件数据存储 key禁止绝对路径、反斜杠和上级目录片段。
func cleanStoreKey(key string) (string, error) {
key = strings.TrimSpace(key)
if key == "" || !storeKeyPattern.MatchString(key) {
@@ -1508,6 +1580,7 @@ func cleanStoreKey(key string) (string, error) {
return key, nil
}
// cleanStorePath 校验插件文件路径,要求传入值已经是规范相对路径。
func cleanStorePath(name string) (string, error) {
if name == "" || strings.Contains(name, `\`) || strings.HasPrefix(name, "/") {
return "", fmt.Errorf("unsafe file path %q", name)
@@ -1519,6 +1592,7 @@ func cleanStorePath(name string) (string, error) {
return clean, nil
}
// isSubpath 判断 target 是否仍在 root 内,作为最终路径穿越保护。
func isSubpath(root, target string) bool {
root = filepath.Clean(root)
target = filepath.Clean(target)
@@ -1526,6 +1600,7 @@ func isSubpath(root, target string) bool {
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}
// retentionDeadline 将保留时间转换为 Unix 时间戳0 表示不过期。
func retentionDeadline(retention time.Duration) int64 {
if retention <= 0 {
return 0
@@ -1533,6 +1608,7 @@ func retentionDeadline(retention time.Duration) int64 {
return time.Now().Add(retention).Unix()
}
// parseDurationDefault 在 manifest 配置缺失或非法时返回默认时长。
func parseDurationDefault(value string, fallback time.Duration) time.Duration {
if value == "" {
return fallback
@@ -1544,6 +1620,7 @@ func parseDurationDefault(value string, fallback time.Duration) time.Duration {
return parsed
}
// deterministicJitter 基于任务 key 生成稳定抖动,避免每次重启后调度时间完全随机。
func deterministicJitter(jitter time.Duration, key string) time.Duration {
if jitter <= 0 || key == "" {
return 0
@@ -1562,6 +1639,8 @@ func limitString(value string, max int) string {
return value[:max]
}
// redactSensitive 对明显敏感的文本做粗粒度脱敏。它不替代结构化密钥管理,
// 只作为日志、诊断和摘要输出前的最后防线。
func redactSensitive(value string) string {
if value == "" {
return ""
@@ -1575,6 +1654,7 @@ func redactSensitive(value string) string {
return value
}
// redactEndpoint 隐藏包含账号信息的 endpoint并限制展示长度。
func redactEndpoint(endpoint string) string {
if endpoint == "" {
return ""
@@ -1585,6 +1665,7 @@ func redactEndpoint(endpoint string) string {
return limitString(endpoint, 256)
}
// randomToken 生成确认令牌;随机源失败时退化为时间戳,保证调用方仍能完成流程。
func randomToken() string {
var data [16]byte
if _, err := rand.Read(data[:]); err != nil {
@@ -1593,6 +1674,7 @@ func randomToken() string {
return hex.EncodeToString(data[:])
}
// limitHex 将 trace id 规范为指定长度的十六进制字符串,用于 traceparent 头。
func limitHex(value string, max int) string {
value = strings.ToLower(value)
var out strings.Builder
@@ -1607,6 +1689,7 @@ func limitHex(value string, max int) string {
return out.String()[:max]
}
// readLimited 读取外部响应时设置硬上限,避免插件依赖返回超大 body 占满内存。
func readLimited(reader io.Reader, max int64) ([]byte, error) {
var buf bytes.Buffer
if _, err := io.CopyN(&buf, reader, max+1); err != nil && !errors.Is(err, io.EOF) {
@@ -1618,6 +1701,7 @@ func readLimited(reader io.Reader, max int64) ([]byte, error) {
return buf.Bytes(), nil
}
// mergeEventSummaries 合并内存事件摘要和数据库近期事件,按插件和事件名聚合计数。
func mergeEventSummaries(current, recent []EventSummary) []EventSummary {
byKey := make(map[string]EventSummary)
for _, event := range current {
@@ -1643,6 +1727,7 @@ func mergeEventSummaries(current, recent []EventSummary) []EventSummary {
return out
}
// noop* 类型用于在插件未启用完整 Operations 时仍返回满足接口的安全空实现。
type noopOperationsLogger struct{}
func (noopOperationsLogger) Debug(context.Context, string, map[string]string) {}

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/repository.go 持久化插件制品、插件记录、快照、构建、密钥、评审、公告和操作日志。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/pluginmanager/types.go 定义仓库、管理器、Admin API 和前端共用的插件管理数据模型。
package pluginmanager
import (

View File

@@ -1,3 +1,5 @@
// internal/tcphttpmux/mux.go 通过窥探首包并回放数据,把同一个监听器拆分给 HTTP 和原始 TCP 处理器。
package tcphttpmux
import (
@@ -11,12 +13,15 @@ import (
)
const (
// 默认只等待一秒首包,避免慢连接长期占住共享监听器的分流协程。
DefaultInitialPacketTimeout = time.Second
DefaultHTTPConnBacklog = 128
DefaultReadBufferSize = 64 * 1024
maxHTTPMethodPrefixLen = len("OPTIONS ")
)
// httpMethodPrefixes 是共享端口识别 HTTP 的方法前缀白名单。Minecraft
// 握手首字节是 VarInt 长度,不会以这些明文方法开头,因此首包前缀足够分流。
var httpMethodPrefixes = [][]byte{
[]byte("GET "),
[]byte("POST "),
@@ -29,6 +34,8 @@ var httpMethodPrefixes = [][]byte{
[]byte("TRACE "),
}
// Options 收集共享监听器的可调参数和观测回调。调用方通过回调接入日志、
// 指标和 socket 选项,避免 tcphttpmux 反向依赖网关主包。
type Options struct {
InitialPacketTimeout time.Duration
HTTPConnBacklog int
@@ -40,6 +47,8 @@ type Options struct {
OnHTTPDeliveryFailed func(net.Conn)
}
// replayConn 先读已经窥探到的首包,再继续读底层连接。这样分流逻辑可以检查
// 首包,同时 HTTP 或 TCP 处理器仍能看到完整原始字节流。
type replayConn struct {
net.Conn
reader io.Reader
@@ -49,6 +58,8 @@ func (c *replayConn) Read(p []byte) (int, error) {
return c.reader.Read(p)
}
// ChanListener 把已识别为 HTTP 的连接投递给 http.Server。它实现 net.Listener
// 但没有真实 accept socket只消费 Deliver 写入的连接。
type ChanListener struct {
conns chan net.Conn
closed chan struct{}
@@ -56,6 +67,7 @@ type ChanListener struct {
addr net.Addr
}
// readBufferPool 降低首包窥探时的临时分配;后续回放给处理器的数据来自 peeked 副本。
var readBufferPool = sync.Pool{
New: func() any {
buf := make([]byte, DefaultReadBufferSize)
@@ -63,9 +75,13 @@ var readBufferPool = sync.Pool{
},
}
// Serve 在同一个底层监听器上同时服务 Admin HTTP 和 Minecraft TCP。每条连接
// 会先读取少量字节判断协议,再被投递给 http.Server 或 tcpHandler。
func Serve(listener net.Listener, handler http.Handler, tcpHandler func(net.Conn), opts Options) error {
defer listener.Close()
// http.Server 仍使用标准库模型,只是它的 listener 是内存通道。
// 这样管理端路由、中间件和超时语义都保持为普通 HTTP 服务。
webListener := NewChanListener(listener.Addr(), opts.normalizedHTTPConnBacklog())
webServer := &http.Server{Handler: handler}
webServerDone := make(chan error, 1)
@@ -100,10 +116,13 @@ func Serve(listener net.Listener, handler http.Handler, tcpHandler func(net.Conn
if opts.SetSocketOptions != nil {
opts.SetSocketOptions(conn)
}
// 每条连接独立分流,避免慢客户端阻塞共享监听器继续 accept。
go HandleConn(conn, webListener, tcpHandler, opts)
}
}
// HandleConn 完成单连接分流。它只负责协议识别和投递,认证、路由和转发
// 仍由 HTTP handler 或 tcpHandler 里的业务层完成。
func HandleConn(conn net.Conn, webListener *ChanListener, tcpHandler func(net.Conn), opts Options) {
peeked, err := ReadInitialPacket(conn, opts.normalizedInitialPacketTimeout())
if err != nil {
@@ -123,6 +142,8 @@ func HandleConn(conn net.Conn, webListener *ChanListener, tcpHandler func(net.Co
replayed := NewReplayConn(conn, peeked)
if IsHTTPInitialPacket(peeked) {
// HTTP 连接投递失败通常意味着 HTTP server 已关闭或通道已满;
// 此时不应降级为 Minecraft TCP直接关闭更明确。
if !webListener.Deliver(replayed) {
if opts.OnHTTPDeliveryFailed != nil {
opts.OnHTTPDeliveryFailed(conn)
@@ -138,6 +159,8 @@ func HandleConn(conn net.Conn, webListener *ChanListener, tcpHandler func(net.Co
tcpHandler(replayed)
}
// ReadInitialPacket 读取足够判断协议的首包片段。对于可能是 HTTP 方法名的
// 短前缀会继续读取,直到确认是 HTTP、确认不是 HTTP 或达到最长方法名前缀。
func ReadInitialPacket(conn net.Conn, timeout time.Duration) ([]byte, error) {
if timeout > 0 {
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
@@ -172,6 +195,7 @@ func ReadInitialPacket(conn net.Conn, timeout time.Duration) ([]byte, error) {
}
}
// NewReplayConn 用已窥探字节包裹连接,让下游处理器无需知道首包曾被提前读取。
func NewReplayConn(conn net.Conn, peeked []byte) net.Conn {
return &replayConn{
Conn: conn,
@@ -179,6 +203,7 @@ func NewReplayConn(conn net.Conn, peeked []byte) net.Conn {
}
}
// IsHTTPInitialPacket 判断首包是否已经完整匹配某个 HTTP 方法前缀。
func IsHTTPInitialPacket(buf []byte) bool {
for _, prefix := range httpMethodPrefixes {
if bytes.HasPrefix(buf, prefix) {
@@ -188,6 +213,8 @@ func IsHTTPInitialPacket(buf []byte) bool {
return false
}
// IsPotentialHTTPInitialPacket 判断当前字节是否仍可能发展成 HTTP 方法名。
// 例如只读到 "GE" 时还不能判定为 Minecraft需要继续等 "GET " 或排除。
func IsPotentialHTTPInitialPacket(buf []byte) bool {
if len(buf) == 0 {
return true
@@ -206,6 +233,7 @@ func getReadBuffer() []byte {
}
func putReadBuffer(buf []byte) {
// 只回收标准容量的缓冲区,避免外部错误切片把异常大小塞回池中。
if cap(buf) != DefaultReadBufferSize {
return
}
@@ -213,6 +241,7 @@ func putReadBuffer(buf []byte) {
readBufferPool.Put(&buf)
}
// NewChanListener 创建供 http.Server 消费的内存 listener。
func NewChanListener(addr net.Addr, backlog int) *ChanListener {
if backlog <= 0 {
backlog = DefaultHTTPConnBacklog
@@ -245,6 +274,8 @@ func (l *ChanListener) Addr() net.Addr {
}
func (l *ChanListener) Deliver(conn net.Conn) bool {
// Deliver 是非阻塞的HTTP accept 队列满时返回 false由调用方关闭连接。
// 这可以保护共享监听器不被管理端慢请求拖住。
select {
case <-l.closed:
return false

View File

@@ -1,3 +1,5 @@
// internal/tcphttpmux/mux_test.go 包含用于约束 mux 行为的测试。
package tcphttpmux
import (

View File

@@ -1,3 +1,5 @@
// internal/upstreamtarget/target.go 把上游目标字符串解析为传输协议和拨号地址。
package upstreamtarget
import "strings"

View File

@@ -1,3 +1,5 @@
// internal/upstreamtarget/target_test.go 包含用于约束 target 行为的测试。
package upstreamtarget
import "testing"