implement client heartbeat #460.

This commit is contained in:
Miroslav Šedivý 2024-12-30 13:41:22 +01:00
parent 5169e0aad3
commit 3082d3241b
14 changed files with 62 additions and 7 deletions

View file

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

View file

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

View file

@ -134,7 +134,7 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
/////////////////////////////
// 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.setFileTransfer(file_transfer)
@ -145,6 +145,11 @@ export class NekoClient extends BaseClient implements EventEmitter<NekoEvents> {
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) {

View file

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

View file

@ -18,6 +18,7 @@ type Session struct {
ImplicitHosting bool
InactiveCursors bool
MercifulReconnect bool
HeartbeatInterval int
APIToken string
CookieEnabled bool
@ -67,6 +68,11 @@ func (Session) Init(cmd *cobra.Command) error {
return err
}
cmd.PersistentFlags().Int("session.heartbeat_interval", 120, "interval in seconds for sending heartbeat messages")
if err := viper.BindPFlag("session.heartbeat_interval", cmd.PersistentFlags().Lookup("session.heartbeat_interval")); err != nil {
return err
}
cmd.PersistentFlags().String("session.api_token", "", "API token for interacting with external services")
if err := viper.BindPFlag("session.api_token", cmd.PersistentFlags().Lookup("session.api_token")); err != nil {
return err
@ -112,6 +118,11 @@ func (Session) InitV2(cmd *cobra.Command) error {
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
}
return nil
}
@ -125,6 +136,7 @@ func (s *Session) Set() {
s.ImplicitHosting = viper.GetBool("session.implicit_hosting")
s.InactiveCursors = viper.GetBool("session.inactive_cursors")
s.MercifulReconnect = viper.GetBool("session.merciful_reconnect")
s.HeartbeatInterval = viper.GetInt("session.heartbeat_interval")
s.APIToken = viper.GetString("session.api_token")
s.CookieEnabled = viper.GetBool("session.cookie.enabled")
@ -156,4 +168,8 @@ func (s *Session) SetV2() {
s.ControlProtection = viper.GetBool("control_protection")
log.Warn().Msg("you are using v2 configuration 'NEKO_CONTROL_PROTECTION' which is deprecated, please use 'NEKO_SESSION_CONTROL_PROTECTION' instead")
}
if viper.IsSet("heartbeat_interval") {
s.HeartbeatInterval = viper.GetInt("heartbeat_interval")
log.Warn().Msg("you are using v2 configuration 'NEKO_HEARTBEAT_INTERVAL' which is deprecated, please use 'NEKO_SESSION_HEARTBEAT_INTERVAL' instead")
}
}

View file

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

View file

@ -15,6 +15,7 @@ type SystemInit struct {
Locks map[string]string `json:"locks"`
ImplicitHosting bool `json:"implicit_hosting"`
FileTransfer bool `json:"file_transfer"`
HeartbeatInterval int `json:"heartbeat_interval"`
}
type SystemMessage struct {

View file

@ -26,6 +26,11 @@ func (s *session) wsToBackend(msg []byte) error {
}
switch header.Event {
// Client Events
case oldEvent.CLIENT_HEARTBEAT:
// do nothing
return nil
// Signal Events
case oldEvent.SIGNAL_OFFER:
request := &oldMessage.SignalOffer{}

View file

@ -260,6 +260,7 @@ func (s *session) wsToClient(msg []byte) error {
Locks: locks,
// TODO: hack - we don't know if file transfer is enabled, we would need to check the global config.
FileTransfer: viper.GetBool("filetransfer.enabled") || (viper.GetBool("legacy") && viper.GetBool("file_transfer_enabled")),
HeartbeatInterval: request.Settings.HeartbeatInterval,
})
case event.SYSTEM_ADMIN:

View file

@ -27,6 +27,7 @@ func New(config *config.Session) *SessionManagerCtx {
ImplicitHosting: config.ImplicitHosting,
InactiveCursors: config.InactiveCursors,
MercifulReconnect: config.MercifulReconnect,
HeartbeatInterval: config.HeartbeatInterval,
},
tokens: make(map[string]string),
sessions: make(map[string]*SessionCtx),

View file

@ -36,6 +36,10 @@ type MessageHandlerCtx struct {
func (h *MessageHandlerCtx) Message(session types.Session, data types.WebSocketMessage) bool {
var err error
switch data.Event {
// Client Events
case event.CLIENT_HEARTBEAT:
// do nothing
// System Events
case event.SYSTEM_LOGS:
payload := &message.SystemLogs{}

View file

@ -31,8 +31,9 @@ const maxPayloadLogLength = 10_000
var nologEvents = []string{
// don't log twice
event.SYSTEM_LOGS,
// don't log heartbeat
// don't log heartbeats
event.SYSTEM_HEARTBEAT,
event.CLIENT_HEARTBEAT,
// don't log every cursor update
event.SESSION_CURSORS,
}

View file

@ -9,6 +9,10 @@ const (
SYSTEM_HEARTBEAT = "system/heartbeat"
)
const (
CLIENT_HEARTBEAT = "client/heartbeat"
)
const (
SIGNAL_REQUEST = "signal/request"
SIGNAL_RESTART = "signal/restart"

View file

@ -47,6 +47,7 @@ type Settings struct {
ImplicitHosting bool `json:"implicit_hosting"`
InactiveCursors bool `json:"inactive_cursors"`
MercifulReconnect bool `json:"merciful_reconnect"`
HeartbeatInterval int `json:"heartbeat_interval"`
// plugin scope
Plugins PluginSettings `json:"plugins"`