feat(general): add heartbeat to websocket control (#460)

* chore: ignore .idea

* feat: add heartbeat event to websocket

* feat: add HeartbeatInterval to websocket config

* feat: send HeartbeatInterval in SYSTEM_INIT message

* feat: add heartbeat logic

* fix: client events is not in WebSocketEvents type
This commit is contained in:
Mmx 2024-12-30 20:44:05 +08:00 committed by GitHub
parent 5d62e56d60
commit e3b6e00648
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 51 additions and 11 deletions

3
.gitignore vendored
View file

@ -33,3 +33,6 @@ bin
# Environment files # Environment files
*.env *.env
# Code Editors
.idea

View file

@ -20,6 +20,7 @@ export interface BaseEvents {
export abstract class BaseClient extends EventEmitter<BaseEvents> { export abstract class BaseClient extends EventEmitter<BaseEvents> {
protected _ws?: WebSocket protected _ws?: WebSocket
protected _ws_heartbeat?: number
protected _peer?: RTCPeerConnection protected _peer?: RTCPeerConnection
protected _channel?: RTCDataChannel protected _channel?: RTCDataChannel
protected _timeout?: number protected _timeout?: number
@ -80,6 +81,11 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._timeout = undefined this._timeout = undefined
} }
if (this._ws_heartbeat) {
clearInterval(this._ws_heartbeat)
this._ws_heartbeat = undefined
}
if (this._ws) { if (this._ws) {
// reset all events // reset all events
this._ws.onmessage = () => {} this._ws.onmessage = () => {}

View file

@ -14,6 +14,9 @@ export const EVENT = {
DISCONNECT: 'system/disconnect', DISCONNECT: 'system/disconnect',
ERROR: 'system/error', ERROR: 'system/error',
}, },
CLIENT: {
HEARTBEAT: 'client/heartbeat'
},
SIGNAL: { SIGNAL: {
OFFER: 'signal/offer', OFFER: 'signal/offer',
ANSWER: 'signal/answer', ANSWER: 'signal/answer',
@ -69,6 +72,7 @@ export type Events = typeof EVENT
export type WebSocketEvents = export type WebSocketEvents =
| SystemEvents | SystemEvents
| ClientEvents
| ControlEvents | ControlEvents
| MemberEvents | MemberEvents
| SignalEvents | SignalEvents
@ -87,6 +91,7 @@ export type ControlEvents =
| typeof EVENT.CONTROL.KEYBOARD | typeof EVENT.CONTROL.KEYBOARD
export type SystemEvents = typeof EVENT.SYSTEM.DISCONNECT export type SystemEvents = typeof EVENT.SYSTEM.DISCONNECT
export type ClientEvents = typeof EVENT.CLIENT.HEARTBEAT
export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED
export type SignalEvents = export type SignalEvents =

View file

@ -134,7 +134,7 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
///////////////////////////// /////////////////////////////
// System Events // System Events
///////////////////////////// /////////////////////////////
protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer }: SystemInitPayload) { protected [EVENT.SYSTEM.INIT]({ implicit_hosting, locks, file_transfer, heartbeat_interval }: SystemInitPayload) {
this.$accessor.remote.setImplicitHosting(implicit_hosting) this.$accessor.remote.setImplicitHosting(implicit_hosting)
this.$accessor.remote.setFileTransfer(file_transfer) this.$accessor.remote.setFileTransfer(file_transfer)
@ -145,6 +145,11 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
id: locks[resource], id: locks[resource],
}) })
} }
if (heartbeat_interval > 0) {
if (this._ws_heartbeat) clearInterval(this._ws_heartbeat)
this._ws_heartbeat = window.setInterval(() => this.sendMessage(EVENT.CLIENT.HEARTBEAT), heartbeat_interval * 1000)
}
} }
protected [EVENT.SYSTEM.DISCONNECT]({ message }: SystemMessagePayload) { protected [EVENT.SYSTEM.DISCONNECT]({ message }: SystemMessagePayload) {

View file

@ -61,6 +61,7 @@ export interface SystemInitPayload {
implicit_hosting: boolean implicit_hosting: boolean
locks: Record<string, string> locks: Record<string, string>
file_transfer: boolean file_transfer: boolean
heartbeat_interval: number
} }
// system/disconnect // system/disconnect

View file

@ -14,6 +14,8 @@ type WebSocket struct {
ControlProtection bool ControlProtection bool
HeartbeatInterval int
FileTransferEnabled bool FileTransferEnabled bool
FileTransferPath string FileTransferPath string
} }
@ -39,6 +41,11 @@ func (WebSocket) Init(cmd *cobra.Command) error {
return err return err
} }
cmd.PersistentFlags().Int("heartbeat_interval", 120, "heartbeat interval in seconds")
if err := viper.BindPFlag("heartbeat_interval", cmd.PersistentFlags().Lookup("heartbeat_interval")); err != nil {
return err
}
// File transfer // File transfer
cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature") cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature")
@ -61,6 +68,8 @@ func (s *WebSocket) Set() {
s.ControlProtection = viper.GetBool("control_protection") s.ControlProtection = viper.GetBool("control_protection")
s.HeartbeatInterval = viper.GetInt("heartbeat_interval")
s.FileTransferEnabled = viper.GetBool("file_transfer_enabled") s.FileTransferEnabled = viper.GetBool("file_transfer_enabled")
s.FileTransferPath = viper.GetString("file_transfer_path") s.FileTransferPath = viper.GetString("file_transfer_path")
s.FileTransferPath = filepath.Clean(s.FileTransferPath) s.FileTransferPath = filepath.Clean(s.FileTransferPath)

View file

@ -6,6 +6,10 @@ const (
SYSTEM_ERROR = "system/error" SYSTEM_ERROR = "system/error"
) )
const (
CLIENT_HEARTBEAT = "client/heartbeat"
)
const ( const (
SIGNAL_OFFER = "signal/offer" SIGNAL_OFFER = "signal/offer"
SIGNAL_ANSWER = "signal/answer" SIGNAL_ANSWER = "signal/answer"

View file

@ -11,10 +11,11 @@ type Message struct {
} }
type SystemInit struct { type SystemInit struct {
Event string `json:"event"` Event string `json:"event"`
ImplicitHosting bool `json:"implicit_hosting"` ImplicitHosting bool `json:"implicit_hosting"`
Locks map[string]string `json:"locks"` Locks map[string]string `json:"locks"`
FileTransfer bool `json:"file_transfer"` FileTransfer bool `json:"file_transfer"`
HeartbeatInterval int `json:"heartbeat_interval"`
} }
type SystemMessage struct { type SystemMessage struct {

View file

@ -74,6 +74,11 @@ func (h *MessageHandler) Message(id string, raw []byte) error {
} }
switch header.Event { switch header.Event {
// Client Events
case event.CLIENT_HEARTBEAT:
// do nothing
return nil
// Signal Events // Signal Events
case event.SIGNAL_OFFER: case event.SIGNAL_OFFER:
payload := &message.SignalOffer{} payload := &message.SignalOffer{}

View file

@ -6,7 +6,7 @@ import (
"m1k1o/neko/internal/types/message" "m1k1o/neko/internal/types/message"
) )
func (h *MessageHandler) SessionCreated(id string, session types.Session) error { func (h *MessageHandler) SessionCreated(id string, heartbeatInterval int, session types.Session) error {
// send sdp and id over to client // send sdp and id over to client
if err := h.signalProvide(id, session); err != nil { if err := h.signalProvide(id, session); err != nil {
return err return err
@ -14,10 +14,11 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
// send initialization information // send initialization information
if err := session.Send(message.SystemInit{ if err := session.Send(message.SystemInit{
Event: event.SYSTEM_INIT, Event: event.SYSTEM_INIT,
ImplicitHosting: h.webrtc.ImplicitControl(), ImplicitHosting: h.webrtc.ImplicitControl(),
Locks: h.state.AllLocked(), Locks: h.state.AllLocked(),
FileTransfer: h.state.FileTransferEnabled(), FileTransfer: h.state.FileTransferEnabled(),
HeartbeatInterval: heartbeatInterval,
}); err != nil { }); err != nil {
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT) h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT)
return err return err

View file

@ -111,7 +111,7 @@ func (ws *WebSocketHandler) Start() {
switch e.Type { switch e.Type {
case types.SESSION_CREATED: case types.SESSION_CREATED:
if err := ws.handler.SessionCreated(e.Id, e.Session); err != nil { if err := ws.handler.SessionCreated(e.Id, ws.conf.HeartbeatInterval, e.Session); err != nil {
ws.logger.Warn().Str("id", e.Id).Err(err).Msg("session created with and error") ws.logger.Warn().Str("id", e.Id).Err(err).Msg("session created with and error")
} else { } else {
ws.logger.Debug().Str("id", e.Id).Msg("session created") ws.logger.Debug().Str("id", e.Id).Msg("session created")