Files
RemLink/cmd/engineer/app_windows.go
T
qsc20001102 142e5dc7d6
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled
初版功能完成
2026-08-29 13:12:17 +08:00

334 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build windows
package main
import (
"context"
"errors"
"fmt"
"net/netip"
"strconv"
"sync"
"time"
"github.com/wailsapp/wails/v2/pkg/runtime"
"remlink/internal/localization"
"remlink/internal/model"
"remlink/internal/nodeagent"
"remlink/internal/protocol"
sessionruntime "remlink/internal/session"
"remlink/internal/siteprofile"
)
type EngineerApp struct {
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
options nodeagent.Options
engine *sessionruntime.EngineerRuntime
profiles *siteprofile.Store
state EngineerState
}
type EngineerState struct {
ServerConnected bool `json:"serverConnected"`
ControlConnected bool `json:"controlConnected"`
ServerURL string `json:"serverURL"`
Version string `json:"version"`
OverlayIP string `json:"overlayIP"`
Sites []protocol.SiteSummary `json:"sites"`
SiteCIDRs map[string][]string `json:"siteCIDRs"`
Session EngineerSessionState `json:"session"`
Logs []EngineerLog `json:"logs"`
}
type EngineerSessionState struct {
ID string `json:"id"`
SiteName string `json:"siteName"`
CIDRs []string `json:"cidrs"`
Status string `json:"status"`
UploadBytes uint64 `json:"uploadBytes"`
DownloadBytes uint64 `json:"downloadBytes"`
UploadPackets uint64 `json:"uploadPackets"`
DownloadPackets uint64 `json:"downloadPackets"`
LatencyMS int `json:"latencyMS"`
StartedAt *time.Time `json:"startedAt,omitempty"`
Reason string `json:"reason,omitempty"`
}
type EngineerLog struct {
Time time.Time `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
}
func NewEngineerApp(options nodeagent.Options, profiles *siteprofile.Store) *EngineerApp {
return &EngineerApp{options: options, profiles: profiles, state: EngineerState{
ServerURL: options.ServerURL, Version: options.Version,
Sites: []protocol.SiteSummary{}, SiteCIDRs: map[string][]string{}, Logs: []EngineerLog{},
Session: EngineerSessionState{CIDRs: []string{}, Status: "IDLE"},
}}
}
func (a *EngineerApp) CheckCIDRs(cidrs []string) error {
a.mu.RLock()
engine := a.engine
a.mu.RUnlock()
if engine == nil {
return errors.New("Engineer 网络运行时尚未就绪")
}
return engine.PreflightCIDRs(cidrs)
}
func (a *EngineerApp) Startup(wailsContext context.Context) {
a.mu.Lock()
a.ctx, a.cancel = context.WithCancel(wailsContext)
ctx := a.ctx
a.mu.Unlock()
if a.profiles != nil {
profiles, err := a.profiles.Load()
if err != nil {
a.appendLog("ERROR", "读取现场网段配置失败:"+err.Error())
} else {
a.mu.Lock()
for siteID, profile := range profiles {
a.state.SiteCIDRs[siteID] = append([]string(nil), profile.CIDRs...)
}
a.mu.Unlock()
}
}
options := a.options
options.OnOverlayReady = func(address netip.Addr) {
a.mu.Lock()
a.state.ServerConnected = true
a.state.OverlayIP = address.String()
a.mu.Unlock()
a.appendLog("INFO", "Overlay 已就绪:"+address.String())
a.emit()
}
options.OnControlState = func(connected bool) {
a.mu.Lock()
a.state.ControlConnected = connected
a.mu.Unlock()
if connected {
a.appendLog("INFO", "Control WebSocket 已连接")
} else {
a.appendLog("WARN", "Control WebSocket 已断开,正在重连")
}
a.emit()
}
options.OnLatency = func(delay time.Duration) {
milliseconds := int(delay.Round(time.Millisecond) / time.Millisecond)
if milliseconds < 1 {
milliseconds = 1
}
a.mu.Lock()
a.state.Session.LatencyMS = milliseconds
a.mu.Unlock()
a.emit()
}
options.OnNodeList = func(payload protocol.NodeListPayload) {
a.mu.Lock()
a.state.Sites = append([]protocol.SiteSummary{}, payload.Sites...)
a.mu.Unlock()
a.emit()
}
options.OnSession = a.onSession
options.OnEngineerReady = func(engine *sessionruntime.EngineerRuntime) {
a.mu.Lock()
a.engine = engine
a.mu.Unlock()
a.emit()
}
go func() {
if err := nodeagent.Run(ctx, options); err != nil && !errors.Is(err, context.Canceled) {
a.appendLog("ERROR", localization.ErrorMessage(err.Error()))
a.mu.Lock()
a.state.ServerConnected = false
a.state.ControlConnected = false
a.mu.Unlock()
a.emit()
}
}()
}
func (a *EngineerApp) Shutdown(context.Context) {
a.mu.Lock()
if a.cancel != nil {
a.cancel()
}
a.mu.Unlock()
}
func (a *EngineerApp) GetState() EngineerState {
a.mu.RLock()
state := a.state
// Wails serialises nil slices as JSON null. The frontend contract requires
// arrays even when they are empty, otherwise the first Vue render fails.
state.Sites = append([]protocol.SiteSummary{}, a.state.Sites...)
state.SiteCIDRs = make(map[string][]string, len(a.state.SiteCIDRs))
for siteID, cidrs := range a.state.SiteCIDRs {
state.SiteCIDRs[siteID] = append([]string(nil), cidrs...)
}
state.Logs = append([]EngineerLog{}, a.state.Logs...)
state.Session.CIDRs = append([]string{}, a.state.Session.CIDRs...)
engine := a.engine
a.mu.RUnlock()
if engine != nil {
snapshot := engine.Snapshot()
if snapshot.ID != 0 {
state.Session.ID = strconv.FormatUint(snapshot.ID, 10)
state.Session.Status = string(snapshot.Status)
state.Session.UploadBytes = snapshot.Counters.UploadBytes
state.Session.DownloadBytes = snapshot.Counters.DownloadBytes
state.Session.UploadPackets = snapshot.Counters.UploadPackets
state.Session.DownloadPackets = snapshot.Counters.DownloadPackets
}
}
return state
}
// SaveSiteCIDRs persists one Site's non-secret Remote CIDR profile beside the
// Engineer executable. An empty list clears only that Site's profile.
func (a *EngineerApp) SaveSiteCIDRs(siteNodeID string, cidrs []string) error {
a.mu.RLock()
engine := a.engine
sites := append([]protocol.SiteSummary(nil), a.state.Sites...)
profiles := a.profiles
a.mu.RUnlock()
if profiles == nil {
return errors.New("现场网段配置存储尚未就绪")
}
siteName := ""
for _, site := range sites {
if site.NodeID == siteNodeID {
siteName = site.Name
break
}
}
if siteName == "" {
return errors.New("所选 Site 不存在")
}
if len(cidrs) > 0 {
if engine == nil {
return errors.New("Engineer 网络运行时尚未就绪")
}
if err := engine.PreflightCIDRs(cidrs); err != nil {
return err
}
}
if err := profiles.Save(siteNodeID, siteName, cidrs); err != nil {
return err
}
a.mu.Lock()
if len(cidrs) == 0 {
delete(a.state.SiteCIDRs, siteNodeID)
} else {
a.state.SiteCIDRs[siteNodeID] = append([]string(nil), cidrs...)
}
a.mu.Unlock()
a.appendLog("INFO", fmt.Sprintf("已保存 %s 的远程网段配置(%d 个)", siteName, len(cidrs)))
a.emit()
return nil
}
func (a *EngineerApp) CreateSession(siteNodeID string, cidrs []string) (string, error) {
a.mu.RLock()
engine, ctx := a.engine, a.ctx
sites := append([]protocol.SiteSummary(nil), a.state.Sites...)
a.mu.RUnlock()
if engine == nil || ctx == nil {
return "", errors.New("Engineer 网络运行时尚未就绪")
}
if len(cidrs) == 0 {
return "", errors.New("至少输入一个 Remote CIDR")
}
siteName := ""
for _, site := range sites {
if site.NodeID == siteNodeID {
if !site.Online {
return "", errors.New("所选 Site 当前离线")
}
if !site.RemoteSubnetCapability {
return "", errors.New("所选 Site 不支持 Remote Subnet")
}
siteName = site.Name
break
}
}
if siteName == "" {
return "", errors.New("所选 Site 不存在")
}
if err := a.SaveSiteCIDRs(siteNodeID, cidrs); err != nil {
return "", err
}
a.mu.Lock()
previousSession := a.state.Session
now := time.Now().UTC()
a.state.Session = EngineerSessionState{
SiteName: siteName, CIDRs: append([]string(nil), cidrs...), Status: "IDLE",
LatencyMS: previousSession.LatencyMS, StartedAt: &now,
}
a.mu.Unlock()
requestID, err := engine.CreateSession(ctx, siteNodeID, cidrs)
if err != nil {
a.mu.Lock()
a.state.Session = previousSession
a.mu.Unlock()
a.appendLog("ERROR", localization.ErrorMessage(err.Error()))
a.emit()
return "", err
}
a.appendLog("INFO", fmt.Sprintf("正在连接 %s%v", siteName, cidrs))
a.emit()
return requestID, nil
}
func (a *EngineerApp) DisconnectSession() error {
a.mu.RLock()
engine, ctx := a.engine, a.ctx
a.mu.RUnlock()
if engine == nil || ctx == nil {
return errors.New("Engineer 网络运行时尚未就绪")
}
return engine.Disconnect(ctx, "ENGINEER_OPERATOR")
}
func (a *EngineerApp) onSession(status model.SessionStatus, sessionID uint64, reason string) {
a.mu.Lock()
a.state.Session.Status = string(status)
if sessionID != 0 {
a.state.Session.ID = strconv.FormatUint(sessionID, 10)
}
a.state.Session.Reason = reason
if status == model.SessionClosed || status == model.SessionFailed {
a.state.Session.StartedAt = nil
}
a.mu.Unlock()
message := fmt.Sprintf("会话 %d → %s", sessionID, localization.SessionStatus(string(status)))
if reason != "" {
message += ";原因:" + localization.Reason(reason)
}
a.appendLog("INFO", message)
a.emit()
}
func (a *EngineerApp) appendLog(level, message string) {
a.mu.Lock()
a.state.Logs = append(a.state.Logs, EngineerLog{Time: time.Now().UTC(), Level: level, Message: message})
if len(a.state.Logs) > 200 {
a.state.Logs = append([]EngineerLog(nil), a.state.Logs[len(a.state.Logs)-200:]...)
}
a.mu.Unlock()
}
func (a *EngineerApp) emit() {
a.mu.RLock()
ctx := a.ctx
a.mu.RUnlock()
if ctx != nil {
runtime.EventsEmit(ctx, "remlink:state")
}
}