This commit is contained in:
ryan
2026-06-19 15:13:24 +08:00
parent 0b34792709
commit 32861c5db9
376 changed files with 3648 additions and 19957 deletions
+7 -1
View File
@@ -1,3 +1,4 @@
// Package config provides shared configuration types for edge applications.
package config
import (
@@ -8,8 +9,11 @@ import (
"time"
)
// MillisecondDuration is a time.Duration that marshals to/from JSON as an integer number of milliseconds
// or as a Go duration string (e.g. "1s", "500ms").
type MillisecondDuration time.Duration
// Duration returns the underlying time.Duration value.
func (d MillisecondDuration) Duration() time.Duration {
return time.Duration(d)
}
@@ -18,6 +22,7 @@ func (d MillisecondDuration) String() string {
return time.Duration(d).String()
}
// UnmarshalJSON decodes either a numeric millisecond value or a quoted Go duration string.
func (d *MillisecondDuration) UnmarshalJSON(data []byte) error {
raw := strings.TrimSpace(string(data))
if raw == "" || raw == "null" {
@@ -49,6 +54,7 @@ func (d *MillisecondDuration) UnmarshalJSON(data []byte) error {
return nil
}
// MarshalJSON encodes the duration as an integer number of milliseconds.
func (d MillisecondDuration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).Milliseconds())
}
}
+4 -1
View File
@@ -1,3 +1,4 @@
// Package heartbeat handles periodic heartbeat and update checks.
package heartbeat
import (
@@ -8,6 +9,7 @@ import (
edgeupdater "github.com/Rain-kl/Wavelet/internal/apps/edge/updater"
)
// AutoUpdateSettings defines the settings for automatic edge updates.
type AutoUpdateSettings struct {
AutoUpdate bool
UpdateNow bool
@@ -16,6 +18,7 @@ type AutoUpdateSettings struct {
UpdateTag string
}
// TryAutoUpdate attempts to check and apply auto updates for the edge service.
func TryAutoUpdate(ctx context.Context, updater *edgeupdater.Service, settings *AutoUpdateSettings, logLabel string) {
if settings == nil || updater == nil {
return
@@ -38,4 +41,4 @@ func TryAutoUpdate(ctx context.Context, updater *edgeupdater.Service, settings *
if err != nil {
slog.Error(logLabel+" update check failed", "error", err)
}
}
}
+14 -5
View File
@@ -1,3 +1,4 @@
// Package httpclient provides an authenticated HTTP client for edge services.
package httpclient
import (
@@ -12,6 +13,7 @@ import (
"time"
)
// Client is an HTTP client wrapper for communicating with remote HTTP services.
type Client struct {
baseURL string
token string
@@ -19,6 +21,7 @@ type Client struct {
httpClient *http.Client
}
// New creates a new Client instance.
func New(baseURL, token string, timeout time.Duration, authHeader string) *Client {
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
@@ -28,11 +31,13 @@ func New(baseURL, token string, timeout time.Duration, authHeader string) *Clien
}
}
// SetToken updates the client auth token.
func (c *Client) SetToken(token string) {
c.token = strings.TrimSpace(token)
slog.Debug("http client token updated")
}
// GetJSON sends a GET request and decodes the response body into target.
func (c *Client) GetJSON(ctx context.Context, path string, target any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
if err != nil {
@@ -42,6 +47,7 @@ func (c *Client) GetJSON(ctx context.Context, path string, target any) error {
return c.do(req, target)
}
// PostJSON sends a POST request with JSON body and decodes the response body into target.
func (c *Client) PostJSON(ctx context.Context, path string, body any, target any) error {
data, err := json.Marshal(body)
if err != nil {
@@ -56,6 +62,7 @@ func (c *Client) PostJSON(ctx context.Context, path string, body any, target any
return c.do(req, target)
}
// DoRaw performs an HTTP request with custom headers and returns the raw response.
func (c *Client) DoRaw(ctx context.Context, method, path string, headers map[string]string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, nil)
if err != nil {
@@ -80,12 +87,11 @@ func (c *Client) do(req *http.Request, target any) error {
slog.Error("http request failed", "method", req.Method, "path", req.URL.Path, "error", err)
return err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
defer func() {
if err := res.Body.Close(); err != nil {
slog.Error("failed to close response body", "error", err)
}
}(res.Body)
}()
body, err := io.ReadAll(res.Body)
if err != nil {
@@ -106,6 +112,7 @@ func (c *Client) do(req *http.Request, target any) error {
return nil
}
// APIError creates a new API error with the given message if it is not empty.
func APIError(msg string) error {
if strings.TrimSpace(msg) == "" {
return nil
@@ -113,6 +120,7 @@ func APIError(msg string) error {
return errors.New(msg)
}
// ReadBodyError parses the error message from the response body, or returns the fallback message.
func ReadBodyError(body []byte, fallback string) error {
var errBody struct {
ErrorMsg string `json:"error_msg"`
@@ -123,10 +131,11 @@ func ReadBodyError(body []byte, fallback string) error {
return errors.New(fallback)
}
// ReadHTTPError reads the error message from the HTTP response.
func ReadHTTPError(res *http.Response) error {
body, err := io.ReadAll(res.Body)
if err != nil {
return errors.New(res.Status)
}
return ReadBodyError(body, res.Status)
}
}
+5 -1
View File
@@ -1,3 +1,4 @@
// Package logging configures structured logging for edge applications.
package logging
import (
@@ -6,10 +7,12 @@ import (
"strings"
)
// Options holds configuration options for the structured logger.
type Options struct {
AddSource bool
}
// Setup initialises the default slog handler using the given options and the LOG_LEVEL environment variable.
func Setup(opts Options) {
handlerOpts := &slog.HandlerOptions{
AddSource: opts.AddSource,
@@ -19,6 +22,7 @@ func Setup(opts Options) {
slog.SetDefault(slog.New(handler))
}
// ParseLevel converts a log-level string (e.g. "debug", "warn") to the corresponding slog.Level.
func ParseLevel(value string) slog.Level {
switch strings.ToLower(strings.TrimSpace(value)) {
case "debug":
@@ -30,4 +34,4 @@ func ParseLevel(value string) slog.Level {
default:
return slog.LevelInfo
}
}
}
+23 -5
View File
@@ -1,3 +1,4 @@
// Package nodeip detects the preferred public IP address for edge nodes.
package nodeip
import (
@@ -9,20 +10,36 @@ import (
"github.com/Rain-kl/Wavelet/pkg/geoip/iputil"
)
const (
outboundIPLookupTimeout = 5 * time.Second
publicIPPriorityScore = 2 // matches iputil.Score for public IPv4 addresses
)
// LookupOutboundIP and LookupLocalIP are the provider functions used to detect the node's outbound/local IP.
// They are package-level variables so they can be overridden in tests.
var (
LookupOutboundIP = geoip.GetOutboundIP
LookupLocalIP = DetectLocal
)
// Detect returns the best available outbound or local IPv4 address for this node.
func Detect() string {
if ip := detectOutbound(); ip != "" {
if ip := detectOutbound(context.Background()); ip != "" {
return ip
}
return LookupLocalIP()
}
func detectOutbound() string {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// DetectWithContext returns the best available outbound or local IPv4 address, respecting ctx for cancellation.
func DetectWithContext(ctx context.Context) string {
if ip := detectOutbound(ctx); ip != "" {
return ip
}
return LookupLocalIP()
}
func detectOutbound(ctx context.Context) string {
ctx, cancel := context.WithTimeout(ctx, outboundIPLookupTimeout)
defer cancel()
ip, err := LookupOutboundIP(ctx)
if err != nil || ip == nil {
@@ -31,6 +48,7 @@ func detectOutbound() string {
return ip.String()
}
// DetectLocal returns the highest-priority non-loopback local IPv4 address found on system interfaces.
func DetectLocal() string {
interfaces, err := net.Interfaces()
if err != nil {
@@ -60,10 +78,10 @@ func DetectLocal() string {
bestIP = ipv4.String()
bestPriority = priority
}
if bestPriority == 2 {
if bestPriority == publicIPPriorityScore {
return bestIP
}
}
}
return bestIP
}
}
+32 -13
View File
@@ -1,7 +1,9 @@
// Package observability provides helpers that read Linux /proc and /sys metrics for system monitoring.
package observability
import (
"bufio"
"math"
"os"
"path/filepath"
"runtime"
@@ -10,13 +12,20 @@ import (
"syscall"
)
const (
memInfoMinFieldCount = 2
cpuStatMinFieldCount = 5
netDevMinFieldCount = 16
diskStatsMinFieldCount = 14
)
// ReadLinuxOSRelease returns the OS name and version from /etc/os-release.
func ReadLinuxOSRelease() (string, string) {
file, err := os.Open("/etc/os-release")
if err != nil {
return runtime.GOOS, ""
}
defer file.Close()
defer func() { _ = file.Close() }()
values := make(map[string]string)
scanner := bufio.NewScanner(file)
@@ -47,7 +56,7 @@ func ReadLinuxCPUModel() string {
if err != nil {
return ""
}
defer file.Close()
defer func() { _ = file.Close() }()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
@@ -68,7 +77,7 @@ func ReadMemInfo() (int64, int64) {
if err != nil {
return 0, 0
}
defer file.Close()
defer func() { _ = file.Close() }()
var memTotalKB int64
var memAvailableKB int64
@@ -96,7 +105,7 @@ func ReadMemInfo() (int64, int64) {
func parseMemInfoValue(line string) int64 {
fields := strings.Fields(line)
if len(fields) < 2 {
if len(fields) < memInfoMinFieldCount {
return 0
}
value, err := strconv.ParseInt(fields[1], 10, 64)
@@ -135,7 +144,7 @@ func ReadLinuxCPUStat() (uint64, uint64) {
continue
}
fields := strings.Fields(line)
if len(fields) < 5 {
if len(fields) < cpuStatMinFieldCount {
return 0, 0
}
var total uint64
@@ -161,7 +170,7 @@ func ReadLinuxNetworkTotals() (int64, int64) {
if err != nil {
return 0, 0
}
defer file.Close()
defer func() { _ = file.Close() }()
var rx int64
var tx int64
@@ -179,7 +188,7 @@ func ReadLinuxNetworkTotals() (int64, int64) {
continue
}
fields := strings.Fields(data)
if len(fields) < 16 {
if len(fields) < netDevMinFieldCount {
continue
}
rxValue, err := strconv.ParseInt(fields[0], 10, 64)
@@ -200,14 +209,14 @@ func ReadLinuxDiskTotals() (int64, int64) {
if err != nil {
return 0, 0
}
defer file.Close()
defer func() { _ = file.Close() }()
var readBytes int64
var writeBytes int64
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 14 {
if len(fields) < diskStatsMinFieldCount {
continue
}
device := fields[2]
@@ -249,8 +258,8 @@ func StatFilesystem(path string) (int64, int64) {
if err := syscall.Statfs(absPath, &stat); err != nil {
return 0, 0
}
total := int64(stat.Blocks) * int64(stat.Bsize)
free := int64(stat.Bavail) * int64(stat.Bsize)
total := multiplyUint64ToInt64(stat.Blocks, uint64(stat.Bsize))
free := multiplyUint64ToInt64(stat.Bavail, uint64(stat.Bsize))
used := total - free
if used < 0 {
used = 0
@@ -258,11 +267,21 @@ func StatFilesystem(path string) (int64, int64) {
return total, used
}
func multiplyUint64ToInt64(a uint64, b uint64) int64 {
if a == 0 || b == 0 {
return 0
}
if a > math.MaxInt64/b {
return math.MaxInt64
}
return int64(a * b) //nolint:gosec // product is bounded to math.MaxInt64 above
}
// ReadFirstLine reads and returns the trimmed first line of a file.
func ReadFirstLine(path string) string {
content, err := os.ReadFile(path)
content, err := os.ReadFile(path) //nolint:gosec // path is a fixed /proc or /sys path from internal callers, not user input
if err != nil {
return ""
}
return strings.TrimSpace(string(content))
}
}
+7 -1
View File
@@ -1,3 +1,4 @@
// Package runner provides shared WebSocket reconnect helpers for edge daemons.
package runner
import (
@@ -6,10 +7,12 @@ import (
"time"
)
// WSConnection defines the minimum interface required for a WebSocket connection that can be closed.
type WSConnection interface {
Close() error
}
// WSReconnectConfig specifies configuration parameters for the WebSocket reconnect loop.
type WSReconnectConfig struct {
ComponentName string
ConnectBackoff time.Duration
@@ -17,6 +20,7 @@ type WSReconnectConfig struct {
OnShutdown func()
}
// RunWSReconnectLoop runs a loop that attempts to keep a WebSocket connection active, automatically reconnecting when closed or failed.
func RunWSReconnectLoop(ctx context.Context, cfg WSReconnectConfig,
connect func(context.Context) (WSConnection, error),
handle func(context.Context, WSConnection),
@@ -40,6 +44,7 @@ func RunWSReconnectLoop(ctx context.Context, cfg WSReconnectConfig,
}
return ctx.Err()
default:
// Continue reconnect loop
}
conn, err := connect(ctx)
@@ -56,9 +61,10 @@ func RunWSReconnectLoop(ctx context.Context, cfg WSReconnectConfig,
}
}
// SleepContext pauses execution for the given duration or until the context is canceled.
func SleepContext(ctx context.Context, d time.Duration) {
select {
case <-ctx.Done():
case <-time.After(d):
}
}
}
+3 -2
View File
@@ -1,5 +1,6 @@
//go:build !windows
// Package updater provides capabilities to check for, download, and apply updates.
package updater
import (
@@ -33,7 +34,7 @@ func replaceAndRestart(execPath string, tmpPath string) error {
if err := removeBackupBinary(backupPath); err != nil {
return err
}
if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil {
if err := syscall.Exec(execPath, os.Args, os.Environ()); err != nil { //nolint:gosec // execPath is the validated edge updater binary path
return fmt.Errorf("exec restart: %w", err)
}
return fmt.Errorf("unreachable after exec")
@@ -48,4 +49,4 @@ func removeBackupBinary(path string) error {
return err
}
return nil
}
}
+28 -18
View File
@@ -1,3 +1,4 @@
// Package updater provides capabilities to check for, download, and apply updates.
package updater
import (
@@ -17,16 +18,23 @@ import (
"github.com/Rain-kl/Wavelet/pkg/utils"
)
const maxChecksumAssetSize = 64 * 1024
const (
maxChecksumAssetSize = 64 * 1024
goosWindows = "windows"
updateTmpFilePerm = 0o600
updateBinaryFilePerm = 0o755
)
var replaceAndRestartFunc = replaceAndRestart
// Config defines the configuration for the update service.
type Config struct {
LocalVersion string
AssetPrefix string
LogLabel string
}
// Service handles checking and applying application binary updates.
type Service struct {
httpClient *http.Client
lastCheckKey string
@@ -35,6 +43,7 @@ type Service struct {
logLabel string
}
// New creates a new updater Service with the provided configuration.
func New(cfg Config) *Service {
return &Service{
httpClient: &http.Client{Timeout: 30 * time.Second},
@@ -44,6 +53,7 @@ func New(cfg Config) *Service {
}
}
// UpdateOptions specifies parameters for checking and applying updates.
type UpdateOptions struct {
Channel string
TagName string
@@ -62,6 +72,7 @@ type githubAsset struct {
BrowserDownloadURL string `json:"browser_download_url"`
}
// CheckAndUpdate checks for a newer release on GitHub and performs an update if available.
func (s *Service) CheckAndUpdate(ctx context.Context, repo string, options UpdateOptions) error {
release, err := s.getRelease(ctx, repo, options)
if err != nil {
@@ -152,7 +163,7 @@ func (s *Service) getLatestPreviewRelease(ctx context.Context, repo string) (*gi
if err != nil {
return nil, err
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("github api returned %s", resp.Status)
@@ -188,12 +199,11 @@ func (s *Service) fetchReleaseFromURL(ctx context.Context, url string) (*githubR
if err != nil {
return nil, err
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
defer func() {
if err := resp.Body.Close(); err != nil {
slog.Error("failed to close response body", "error", err)
}
}(resp.Body)
}()
if resp.StatusCode == http.StatusNotFound {
return nil, nil
@@ -222,7 +232,7 @@ func (s *Service) downloadChecksum(ctx context.Context, url string, assetName st
if err != nil {
return "", err
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("checksum download returned %s", resp.Status)
@@ -309,37 +319,37 @@ func (s *Service) downloadAndRestart(ctx context.Context, url string, expectedCh
if err != nil {
return err
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download returned %s", resp.Status)
}
tmpPath := targetPath + ".update"
if runtime.GOOS == "windows" && !strings.HasSuffix(strings.ToLower(tmpPath), ".exe") {
if runtime.GOOS == goosWindows && !strings.HasSuffix(strings.ToLower(tmpPath), ".exe") {
tmpPath += ".exe"
}
tmpFile, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
tmpFile, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, updateTmpFilePerm) //nolint:gosec // tmpPath is derived from the configured updater binary location
if err != nil {
return err
}
hasher := sha256.New()
if _, err = io.Copy(io.MultiWriter(tmpFile, hasher), resp.Body); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
return err
}
if err = tmpFile.Close(); err != nil {
os.Remove(tmpPath)
_ = os.Remove(tmpPath)
return err
}
actualChecksum := hex.EncodeToString(hasher.Sum(nil))
if actualChecksum != expectedChecksum {
os.Remove(tmpPath)
_ = os.Remove(tmpPath)
return fmt.Errorf("sha256 checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum)
}
if err = os.Chmod(tmpPath, 0o755); err != nil && runtime.GOOS != "windows" {
os.Remove(tmpPath)
if err = os.Chmod(tmpPath, updateBinaryFilePerm); err != nil && runtime.GOOS != goosWindows { //nolint:gosec // downloaded edge binary must remain executable
_ = os.Remove(tmpPath)
return fmt.Errorf("set executable permission: %w", err)
}
@@ -349,7 +359,7 @@ func (s *Service) downloadAndRestart(ctx context.Context, url string, expectedCh
func (s *Service) assetNameForGOOSGOARCH(goos string, goarch string) string {
name := fmt.Sprintf("%s-%s-%s", s.assetPrefix, goos, goarch)
if goos == "windows" {
if goos == goosWindows {
return name + ".exe"
}
return name
@@ -378,4 +388,4 @@ func buildReleaseCheckKey(options UpdateOptions, remoteVersion string) string {
func compareVersions(local string, remote string) int {
return utils.CompareVersions(local, remote)
}
}
+27 -1
View File
@@ -1,3 +1,4 @@
// Package wsclient provides WebSocket client abstractions for edge node communication.
package wsclient
import (
@@ -8,14 +9,21 @@ import (
shared "github.com/Rain-kl/Wavelet/pkg/wsclient"
)
// WSMessage is an alias for the shared WebSocket message type.
type WSMessage = shared.WSMessage
// MessageHandler is an alias for the shared WebSocket message handler type.
type MessageHandler = shared.MessageHandler
// Preset represents a predefined connection configuration for a specific edge role.
type Preset int
const (
// PresetAgent is the configuration preset for agent connections.
PresetAgent Preset = iota
// PresetRelay is the configuration preset for relay connections.
PresetRelay
// PresetFlared is the configuration preset for flared (tunnel) connections.
PresetFlared
)
@@ -30,18 +38,22 @@ var presets = map[Preset]presetConfig{
PresetFlared: {HeaderKey: "X-Tunnel-Token", WSPath: "/api/v1/tunnel/ws"},
}
// PresetHeaderKey returns the HTTP header key used for authentication with the given preset.
func PresetHeaderKey(preset Preset) string {
return presets[preset].HeaderKey
}
// PresetWSPath returns the WebSocket path used for the given preset.
func PresetWSPath(preset Preset) string {
return presets[preset].WSPath
}
// Client is a WebSocket client configured for a specific edge preset.
type Client struct {
sharedClient *shared.Client
}
// New creates a new Client for the given preset, base URL, token, and timeout.
func New(preset Preset, baseURL, token string, timeout time.Duration) *Client {
cfg := presets[preset]
return &Client{
@@ -55,18 +67,22 @@ func New(preset Preset, baseURL, token string, timeout time.Duration) *Client {
}
}
// SetToken updates the authentication token used by the client.
func (c *Client) SetToken(token string) {
c.sharedClient.SetToken(token)
}
// URL returns the fully resolved WebSocket URL for this client.
func (c *Client) URL() string {
return c.sharedClient.URL()
}
// Connection represents an established WebSocket connection to an edge node.
type Connection struct {
sharedConn *shared.Connection
}
// Connect establishes a WebSocket connection using the client configuration.
func (c *Client) Connect(ctx context.Context) (*Connection, error) {
conn, err := c.sharedClient.Connect(ctx)
if err != nil {
@@ -75,10 +91,12 @@ func (c *Client) Connect(ctx context.Context) (*Connection, error) {
return &Connection{sharedConn: conn}, nil
}
// AgentConnection is a Connection specialized for agent node communication.
type AgentConnection struct {
Connection
}
// ConnectAgent establishes a WebSocket connection and returns it as an AgentConnection.
func (c *Client) ConnectAgent(ctx context.Context) (*AgentConnection, error) {
conn, err := c.Connect(ctx)
if err != nil {
@@ -87,6 +105,7 @@ func (c *Client) ConnectAgent(ctx context.Context) (*AgentConnection, error) {
return &AgentConnection{Connection: *conn}, nil
}
// URL returns the resolved WebSocket URL of this connection.
func (conn *Connection) URL() string {
if conn == nil || conn.sharedConn == nil {
return ""
@@ -94,18 +113,22 @@ func (conn *Connection) URL() string {
return conn.sharedConn.URL
}
// SendPing sends a ping message over the connection.
func (conn *Connection) SendPing() error {
return conn.sharedConn.SendMessage(pkgprotocol.WSMessageTypePing, nil)
}
// SendPong sends a pong message over the connection.
func (conn *Connection) SendPong() error {
return conn.sharedConn.SendMessage(pkgprotocol.WSMessageTypePong, nil)
}
// SendMessage sends a typed message with an optional payload over the connection.
func (conn *Connection) SendMessage(msgType string, payload any) error {
return conn.sharedConn.SendMessage(msgType, payload)
}
// Receive reads the next message from the connection.
func (conn *Connection) Receive() (pkgprotocol.WSMessage, error) {
var message pkgprotocol.WSMessage
if err := conn.sharedConn.Receive(&message); err != nil {
@@ -114,10 +137,12 @@ func (conn *Connection) Receive() (pkgprotocol.WSMessage, error) {
return message, nil
}
// RunReceiveLoop blocks and dispatches incoming messages to the handler until the context is canceled.
func (conn *Connection) RunReceiveLoop(ctx context.Context, handler MessageHandler) error {
return conn.sharedConn.RunReceiveLoop(ctx, handler)
}
// Close gracefully closes the WebSocket connection.
func (conn *Connection) Close() error {
if conn == nil || conn.sharedConn == nil {
return nil
@@ -125,6 +150,7 @@ func (conn *Connection) Close() error {
return conn.sharedConn.Close()
}
// SendStatus sends a node status payload over the agent connection.
func (conn *AgentConnection) SendStatus(payload pkgprotocol.NodePayload) error {
return conn.sharedConn.SendMessage(pkgprotocol.WSMessageTypeStatus, payload)
}
}