// 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) }