初版功能完成
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
//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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user