Feature(Go): Web auth, WebSocket signaling and live device list (Phase 3)
This commit is contained in:
217
server/go/hub/hub.go
Normal file
217
server/go/hub/hub.go
Normal file
@@ -0,0 +1,217 @@
|
||||
// Package hub maintains the registry of currently online devices and acts as
|
||||
// the bridge between the TCP server (which sees raw client connections) and
|
||||
// the web server (which serves browser clients).
|
||||
//
|
||||
// The TCP side calls RegisterDevice / UnregisterDevice as clients come and go.
|
||||
// The web side calls ListDevices / GetDevice / (Phase 4) SendToDevice.
|
||||
// Neither side imports the other — both depend only on this package.
|
||||
//
|
||||
// Phase 3 scope: device list only. Frame/cursor pub-sub and SendToDevice are
|
||||
// added in later phases as features need them.
|
||||
package hub
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/connection"
|
||||
)
|
||||
|
||||
// Device is the internal record for one logical end-device (keyed by MasterID).
|
||||
// A single device may use multiple TCP sub-connections (screen, terminal …);
|
||||
// only the main login connection is stored here.
|
||||
//
|
||||
// PCName from LOGIN_INFOR is interpreted as "ComputerName/Group" and
|
||||
// ModuleVersion as "Version-Capability"; the split halves live in separate
|
||||
// fields so the front-end can render them independently.
|
||||
type Device struct {
|
||||
ID string // MasterID — stable identifier the client reports at login
|
||||
Name string // PCName before '/' (real computer name)
|
||||
Group string // PCName after '/' (group label; may be empty)
|
||||
Version string // ModuleVersion before '-' (semantic version)
|
||||
Capability string // ModuleVersion after '-' (capability tags; may be empty)
|
||||
OS string // OS version string
|
||||
CPU string // from LOGIN_INFOR reserved field 2
|
||||
FilePath string // from LOGIN_INFOR reserved field 4
|
||||
InstallTime string // from LOGIN_INFOR reserved field 6 (or StartTime)
|
||||
Location string // client-reported geo string (reserved field 10)
|
||||
PeerIP string // network-level remote address as seen by the server
|
||||
PublicIP string // client-reported public IP (reserved field 11)
|
||||
ConnectedAt time.Time
|
||||
|
||||
// Live fields refreshed on every heartbeat. Protected by hub.mu.
|
||||
RTT int // network latency in ms (Heartbeat.Ping)
|
||||
ActiveWindow string // foreground window title (Heartbeat.ActiveWnd, decoded)
|
||||
|
||||
// conn is the main connection's context. Web side will use it in Phase 4
|
||||
// to push COMMAND_SCREEN_SPY and similar commands via the hub.
|
||||
conn *connection.Context
|
||||
}
|
||||
|
||||
// DeviceInfo is the JSON-safe projection of Device for the /api/devices
|
||||
// endpoint and the WS device_list message. Field names match what the
|
||||
// existing browser front-end expects.
|
||||
type DeviceInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Group string `json:"group,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Capability string `json:"capability,omitempty"`
|
||||
OS string `json:"os"`
|
||||
CPU string `json:"cpu,omitempty"`
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
InstallTime string `json:"install_time,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
IP string `json:"ip"` // client-reported public IP (matches C++ key)
|
||||
PeerIP string `json:"peer_ip,omitempty"`
|
||||
RTT int `json:"rtt"`
|
||||
ActiveWindow string `json:"activeWindow,omitempty"`
|
||||
ConnectedAt int64 `json:"connected_at"`
|
||||
Online bool `json:"online"`
|
||||
}
|
||||
|
||||
// EventHandler receives notifications about device lifecycle and per-tick
|
||||
// live updates. Methods are invoked synchronously from Register / Unregister /
|
||||
// UpdateLive — implementations must be non-blocking (typically just write to
|
||||
// a channel or queue).
|
||||
type EventHandler interface {
|
||||
OnDeviceOnline(d DeviceInfo)
|
||||
OnDeviceOffline(id string)
|
||||
OnDeviceUpdate(id string, rtt int, activeWindow string)
|
||||
}
|
||||
|
||||
// Hub is a thread-safe registry of online devices.
|
||||
type Hub struct {
|
||||
mu sync.RWMutex
|
||||
devices map[string]*Device
|
||||
subMu sync.RWMutex
|
||||
subscribers []EventHandler
|
||||
}
|
||||
|
||||
// New returns an empty Hub.
|
||||
func New() *Hub {
|
||||
return &Hub{devices: make(map[string]*Device)}
|
||||
}
|
||||
|
||||
// Subscribe registers an EventHandler. The returned func removes it.
|
||||
// Multiple handlers are supported; each receives every event.
|
||||
func (h *Hub) Subscribe(eh EventHandler) (unsubscribe func()) {
|
||||
h.subMu.Lock()
|
||||
h.subscribers = append(h.subscribers, eh)
|
||||
h.subMu.Unlock()
|
||||
return func() {
|
||||
h.subMu.Lock()
|
||||
defer h.subMu.Unlock()
|
||||
for i, x := range h.subscribers {
|
||||
if x == eh {
|
||||
h.subscribers = append(h.subscribers[:i], h.subscribers[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) snapshotSubscribers() []EventHandler {
|
||||
h.subMu.RLock()
|
||||
defer h.subMu.RUnlock()
|
||||
out := make([]EventHandler, len(h.subscribers))
|
||||
copy(out, h.subscribers)
|
||||
return out
|
||||
}
|
||||
|
||||
// Register records a device as online. Re-registering an existing ID overwrites
|
||||
// the previous entry (e.g. a client reconnect with the same MasterID).
|
||||
// A nil device or empty ID is silently ignored.
|
||||
// Subscribers are notified after the device is added.
|
||||
func (h *Hub) Register(d *Device) {
|
||||
if d == nil || d.ID == "" {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.devices[d.ID] = d
|
||||
info := deviceToInfo(d)
|
||||
h.mu.Unlock()
|
||||
for _, s := range h.snapshotSubscribers() {
|
||||
s.OnDeviceOnline(info)
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister removes a device by ID. No-op if not present.
|
||||
// Subscribers are notified after the device is removed (only if it existed).
|
||||
func (h *Hub) Unregister(id string) {
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
_, existed := h.devices[id]
|
||||
delete(h.devices, id)
|
||||
h.mu.Unlock()
|
||||
if !existed {
|
||||
return
|
||||
}
|
||||
for _, s := range h.snapshotSubscribers() {
|
||||
s.OnDeviceOffline(id)
|
||||
}
|
||||
}
|
||||
|
||||
// ListDevices returns a fresh snapshot slice. The caller may mutate it freely;
|
||||
// it shares no state with the hub.
|
||||
func (h *Hub) ListDevices() []DeviceInfo {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
out := make([]DeviceInfo, 0, len(h.devices))
|
||||
for _, d := range h.devices {
|
||||
out = append(out, deviceToInfo(d))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func deviceToInfo(d *Device) DeviceInfo {
|
||||
return DeviceInfo{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Group: d.Group,
|
||||
Version: d.Version,
|
||||
Capability: d.Capability,
|
||||
OS: d.OS,
|
||||
CPU: d.CPU,
|
||||
FilePath: d.FilePath,
|
||||
InstallTime: d.InstallTime,
|
||||
Location: d.Location,
|
||||
IP: d.PublicIP,
|
||||
PeerIP: d.PeerIP,
|
||||
RTT: d.RTT,
|
||||
ActiveWindow: d.ActiveWindow,
|
||||
ConnectedAt: d.ConnectedAt.Unix(),
|
||||
Online: true, // a device that's in the map is by definition online
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateLive applies a heartbeat-derived RTT and active-window title to the
|
||||
// device's live fields, then notifies subscribers. No-op if the device is
|
||||
// not registered (e.g. heartbeat arriving for a connection that never sent
|
||||
// TOKEN_LOGIN or has already disconnected).
|
||||
func (h *Hub) UpdateLive(id string, rtt int, activeWindow string) {
|
||||
if id == "" {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
d, ok := h.devices[id]
|
||||
if !ok {
|
||||
h.mu.Unlock()
|
||||
return
|
||||
}
|
||||
d.RTT = rtt
|
||||
d.ActiveWindow = activeWindow
|
||||
h.mu.Unlock()
|
||||
for _, s := range h.snapshotSubscribers() {
|
||||
s.OnDeviceUpdate(id, rtt, activeWindow)
|
||||
}
|
||||
}
|
||||
|
||||
// Count returns the current number of online devices.
|
||||
func (h *Hub) Count() int {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return len(h.devices)
|
||||
}
|
||||
Reference in New Issue
Block a user