mirror of https://github.com/jetkvm/kvm.git
				
				
				
			release 0.3.9 (#349)
This commit is contained in:
		
						commit
						5452d7c721
					
				
							
								
								
									
										43
									
								
								cloud.go
								
								
								
								
							
							
						
						
									
										43
									
								
								cloud.go
								
								
								
								
							| 
						 | 
					@ -4,6 +4,7 @@ import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
| 
						 | 
					@ -59,6 +60,13 @@ var (
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		[]string{"type", "source"},
 | 
							[]string{"type", "source"},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
						metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec(
 | 
				
			||||||
 | 
							prometheus.GaugeOpts{
 | 
				
			||||||
 | 
								Name: "jetkvm_connection_last_ping_received_timestamp",
 | 
				
			||||||
 | 
								Help: "The timestamp when the last ping request was received",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							[]string{"type", "source"},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	metricConnectionLastPingDuration = promauto.NewGaugeVec(
 | 
						metricConnectionLastPingDuration = promauto.NewGaugeVec(
 | 
				
			||||||
		prometheus.GaugeOpts{
 | 
							prometheus.GaugeOpts{
 | 
				
			||||||
			Name: "jetkvm_connection_last_ping_duration",
 | 
								Name: "jetkvm_connection_last_ping_duration",
 | 
				
			||||||
| 
						 | 
					@ -76,16 +84,23 @@ var (
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		[]string{"type", "source"},
 | 
							[]string{"type", "source"},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	metricConnectionTotalPingCount = promauto.NewCounterVec(
 | 
						metricConnectionTotalPingSentCount = promauto.NewCounterVec(
 | 
				
			||||||
		prometheus.CounterOpts{
 | 
							prometheus.CounterOpts{
 | 
				
			||||||
			Name: "jetkvm_connection_total_ping_count",
 | 
								Name: "jetkvm_connection_total_ping_sent",
 | 
				
			||||||
			Help: "The total number of pings sent to the connection",
 | 
								Help: "The total number of pings sent to the connection",
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		[]string{"type", "source"},
 | 
							[]string{"type", "source"},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
						metricConnectionTotalPingReceivedCount = promauto.NewCounterVec(
 | 
				
			||||||
 | 
							prometheus.CounterOpts{
 | 
				
			||||||
 | 
								Name: "jetkvm_connection_total_ping_received",
 | 
				
			||||||
 | 
								Help: "The total number of pings received from the connection",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							[]string{"type", "source"},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
	metricConnectionSessionRequestCount = promauto.NewCounterVec(
 | 
						metricConnectionSessionRequestCount = promauto.NewCounterVec(
 | 
				
			||||||
		prometheus.CounterOpts{
 | 
							prometheus.CounterOpts{
 | 
				
			||||||
			Name: "jetkvm_connection_session_total_request_count",
 | 
								Name: "jetkvm_connection_session_total_requests",
 | 
				
			||||||
			Help: "The total number of session requests received",
 | 
								Help: "The total number of session requests received",
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		[]string{"type", "source"},
 | 
							[]string{"type", "source"},
 | 
				
			||||||
| 
						 | 
					@ -131,6 +146,8 @@ func wsResetMetrics(established bool, sourceType string, source string) {
 | 
				
			||||||
	metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1)
 | 
						metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1)
 | 
				
			||||||
	metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1)
 | 
						metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).Set(-1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1)
 | 
						metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).Set(-1)
 | 
				
			||||||
	metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1)
 | 
						metricConnectionLastSessionRequestDuration.WithLabelValues(sourceType, source).Set(-1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -275,18 +292,31 @@ func runWebsocketClient() error {
 | 
				
			||||||
	defer cancelDial()
 | 
						defer cancelDial()
 | 
				
			||||||
	c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
 | 
						c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
 | 
				
			||||||
		HTTPHeader: header,
 | 
							HTTPHeader: header,
 | 
				
			||||||
 | 
							OnPingReceived: func(ctx context.Context, payload []byte) bool {
 | 
				
			||||||
 | 
								websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: cloud", payload, wsURL.Host)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc()
 | 
				
			||||||
 | 
								metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
						// if the context is canceled, we don't want to return an error
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							if errors.Is(err, context.Canceled) {
 | 
				
			||||||
 | 
								cloudLogger.Infof("websocket connection canceled")
 | 
				
			||||||
 | 
								return nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer c.CloseNow() //nolint:errcheck
 | 
						defer c.CloseNow() //nolint:errcheck
 | 
				
			||||||
	cloudLogger.Infof("websocket connected to %s", wsURL)
 | 
						cloudLogger.Infof("websocket connected to %s", wsURL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set the metrics when we successfully connect to the cloud.
 | 
						// set the metrics when we successfully connect to the cloud.
 | 
				
			||||||
	wsResetMetrics(true, "cloud", "")
 | 
						wsResetMetrics(true, "cloud", wsURL.Host)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// we don't have a source for the cloud connection
 | 
						// we don't have a source for the cloud connection
 | 
				
			||||||
	return handleWebRTCSignalWsMessages(c, true, "")
 | 
						return handleWebRTCSignalWsMessages(c, true, wsURL.Host)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
 | 
					func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
 | 
				
			||||||
| 
						 | 
					@ -375,9 +405,6 @@ func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func RunWebsocketClient() {
 | 
					func RunWebsocketClient() {
 | 
				
			||||||
	for {
 | 
						for {
 | 
				
			||||||
		// reset the metrics when we start the websocket client.
 | 
					 | 
				
			||||||
		wsResetMetrics(false, "cloud", "")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// If the cloud token is not set, we don't need to run the websocket client.
 | 
							// If the cloud token is not set, we don't need to run the websocket client.
 | 
				
			||||||
		if config.CloudToken == "" {
 | 
							if config.CloudToken == "" {
 | 
				
			||||||
			time.Sleep(5 * time.Second)
 | 
								time.Sleep(5 * time.Second)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,5 @@
 | 
				
			||||||
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
# Exit immediately if a command exits with a non-zero status
 | 
					# Exit immediately if a command exits with a non-zero status
 | 
				
			||||||
set -e
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +18,6 @@ show_help() {
 | 
				
			||||||
    echo "Example:"
 | 
					    echo "Example:"
 | 
				
			||||||
    echo "  $0 -r 192.168.0.17"
 | 
					    echo "  $0 -r 192.168.0.17"
 | 
				
			||||||
    echo "  $0 -r 192.168.0.17 -u admin"
 | 
					    echo "  $0 -r 192.168.0.17 -u admin"
 | 
				
			||||||
    exit 0
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Default values
 | 
					# Default values
 | 
				
			||||||
| 
						 | 
					@ -70,10 +71,10 @@ cd bin
 | 
				
			||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
 | 
					ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Copy the binary to the remote host
 | 
					# Copy the binary to the remote host
 | 
				
			||||||
cat jetkvm_app | ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > $REMOTE_PATH/jetkvm_app_debug"
 | 
					ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < jetkvm_app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Deploy and run the application on the remote host
 | 
					# Deploy and run the application on the remote host
 | 
				
			||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash <<EOF
 | 
					ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
 | 
				
			||||||
set -e
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Set the library path to include the directory where librockit.so is located
 | 
					# Set the library path to include the directory where librockit.so is located
 | 
				
			||||||
| 
						 | 
					@ -84,13 +85,13 @@ killall jetkvm_app || true
 | 
				
			||||||
killall jetkvm_app_debug || true
 | 
					killall jetkvm_app_debug || true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Navigate to the directory where the binary will be stored
 | 
					# Navigate to the directory where the binary will be stored
 | 
				
			||||||
cd "$REMOTE_PATH"
 | 
					cd "${REMOTE_PATH}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Make the new binary executable
 | 
					# Make the new binary executable
 | 
				
			||||||
chmod +x jetkvm_app_debug
 | 
					chmod +x jetkvm_app_debug
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Run the application in the background
 | 
					# Run the application in the background
 | 
				
			||||||
PION_LOG_TRACE=jetkvm,cloud ./jetkvm_app_debug
 | 
					PION_LOG_TRACE=jetkvm,cloud,websocket ./jetkvm_app_debug
 | 
				
			||||||
EOF
 | 
					EOF
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "Deployment complete."
 | 
					echo "Deployment complete."
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										2
									
								
								go.mod
								
								
								
								
							| 
						 | 
					@ -7,7 +7,7 @@ toolchain go1.21.1
 | 
				
			||||||
require (
 | 
					require (
 | 
				
			||||||
	github.com/Masterminds/semver/v3 v3.3.0
 | 
						github.com/Masterminds/semver/v3 v3.3.0
 | 
				
			||||||
	github.com/beevik/ntp v1.3.1
 | 
						github.com/beevik/ntp v1.3.1
 | 
				
			||||||
	github.com/coder/websocket v1.8.12
 | 
						github.com/coder/websocket v1.8.13
 | 
				
			||||||
	github.com/coreos/go-oidc/v3 v3.11.0
 | 
						github.com/coreos/go-oidc/v3 v3.11.0
 | 
				
			||||||
	github.com/creack/pty v1.1.23
 | 
						github.com/creack/pty v1.1.23
 | 
				
			||||||
	github.com/gin-gonic/gin v1.9.1
 | 
						github.com/gin-gonic/gin v1.9.1
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										2
									
								
								go.sum
								
								
								
								
							| 
						 | 
					@ -18,6 +18,8 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
 | 
				
			||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 | 
					github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 | 
				
			||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
 | 
					github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
 | 
				
			||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
 | 
					github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
 | 
				
			||||||
 | 
					github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
 | 
				
			||||||
 | 
					github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
 | 
				
			||||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
 | 
					github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
 | 
				
			||||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
 | 
					github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
 | 
				
			||||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
 | 
					github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								ota.go
								
								
								
								
							
							
						
						
									
										6
									
								
								ota.go
								
								
								
								
							| 
						 | 
					@ -126,12 +126,10 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
 | 
				
			||||||
		return fmt.Errorf("error creating request: %w", err)
 | 
							return fmt.Errorf("error creating request: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: set a separate timeout for the download but keep the TLS handshake short
 | 
				
			||||||
 | 
						// use Transport here will cause CA certificate validation failure so we temporarily removed it
 | 
				
			||||||
	client := http.Client{
 | 
						client := http.Client{
 | 
				
			||||||
		// allow a longer timeout for the download but keep the TLS handshake short
 | 
					 | 
				
			||||||
		Timeout: 10 * time.Minute,
 | 
							Timeout: 10 * time.Minute,
 | 
				
			||||||
		Transport: &http.Transport{
 | 
					 | 
				
			||||||
			TLSHandshakeTimeout: 1 * time.Minute,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := client.Do(req)
 | 
						resp, err := client.Do(req)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
#!/bin/bash
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Check if a commit message was provided
 | 
					# Check if a commit message was provided
 | 
				
			||||||
if [ -z "$1" ]; then
 | 
					if [ -z "$1" ]; then
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ git checkout -b release-temp
 | 
				
			||||||
if git ls-remote --heads public main | grep -q 'refs/heads/main'; then
 | 
					if git ls-remote --heads public main | grep -q 'refs/heads/main'; then
 | 
				
			||||||
    git reset --soft public/main
 | 
					    git reset --soft public/main
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    git reset --soft $(git rev-list --max-parents=0 HEAD)
 | 
					    git reset --soft "$(git rev-list --max-parents=0 HEAD)"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Merge changes from main
 | 
					# Merge changes from main
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
#!/bin/bash
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Check if an IP address was provided as an argument
 | 
					# Check if an IP address was provided as an argument
 | 
				
			||||||
if [ -z "$1" ]; then
 | 
					if [ -z "$1" ]; then
 | 
				
			||||||
| 
						 | 
					@ -16,4 +16,4 @@ echo "└───────────────────────
 | 
				
			||||||
# Set the environment variable and run Vite
 | 
					# Set the environment variable and run Vite
 | 
				
			||||||
echo "Starting development server with JetKVM device at: $ip_address"
 | 
					echo "Starting development server with JetKVM device at: $ip_address"
 | 
				
			||||||
sleep 1
 | 
					sleep 1
 | 
				
			||||||
JETKVM_PROXY_URL="http://$ip_address" npx vite dev --mode=device
 | 
					JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,12 +15,12 @@ const Modal = React.memo(function Modal({
 | 
				
			||||||
  onClose: () => void;
 | 
					  onClose: () => void;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Dialog open={open} onClose={onClose} className="relative z-10">
 | 
					    <Dialog open={open} onClose={onClose} className="relative z-20">
 | 
				
			||||||
      <DialogBackdrop
 | 
					      <DialogBackdrop
 | 
				
			||||||
        transition
 | 
					        transition
 | 
				
			||||||
        className="fixed inset-0 bg-gray-500/75 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-500 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in dark:bg-slate-900/90"
 | 
					        className="fixed inset-0 bg-gray-500/75 transition-opacity data-[closed]:opacity-0 data-[enter]:duration-500 data-[leave]:duration-200 data-[enter]:ease-out data-[leave]:ease-in dark:bg-slate-900/90"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <div className="fixed inset-0 z-10 w-screen overflow-y-auto">
 | 
					      <div className="fixed inset-0 z-20 w-screen overflow-y-auto">
 | 
				
			||||||
        {/* TODO: This doesn't work well with other-sessions */}
 | 
					        {/* TODO: This doesn't work well with other-sessions */}
 | 
				
			||||||
        <div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
 | 
					        <div className="flex min-h-full items-end justify-center p-4 text-center md:items-baseline md:p-4">
 | 
				
			||||||
          <DialogPanel
 | 
					          <DialogPanel
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ export default function WebRTCVideo() {
 | 
				
			||||||
  const videoElm = useRef<HTMLVideoElement>(null);
 | 
					  const videoElm = useRef<HTMLVideoElement>(null);
 | 
				
			||||||
  const mediaStream = useRTCStore(state => state.mediaStream);
 | 
					  const mediaStream = useRTCStore(state => state.mediaStream);
 | 
				
			||||||
  const [isPlaying, setIsPlaying] = useState(false);
 | 
					  const [isPlaying, setIsPlaying] = useState(false);
 | 
				
			||||||
 | 
					  const peerConnectionState = useRTCStore(state => state.peerConnectionState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Store hooks
 | 
					  // Store hooks
 | 
				
			||||||
  const settings = useSettingsStore();
 | 
					  const settings = useSettingsStore();
 | 
				
			||||||
| 
						 | 
					@ -601,7 +602,10 @@ export default function WebRTCVideo() {
 | 
				
			||||||
                            "cursor-none":
 | 
					                            "cursor-none":
 | 
				
			||||||
                              settings.mouseMode === "absolute" &&
 | 
					                              settings.mouseMode === "absolute" &&
 | 
				
			||||||
                              settings.isCursorHidden,
 | 
					                              settings.isCursorHidden,
 | 
				
			||||||
                            "opacity-0": isVideoLoading || hdmiError,
 | 
					                            "opacity-0":
 | 
				
			||||||
 | 
					                              isVideoLoading ||
 | 
				
			||||||
 | 
					                              hdmiError ||
 | 
				
			||||||
 | 
					                              peerConnectionState !== "connected",
 | 
				
			||||||
                            "animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
 | 
					                            "animate-slideUpFade border border-slate-800/30 opacity-0 shadow dark:border-slate-300/20":
 | 
				
			||||||
                              isPlaying,
 | 
					                              isPlaying,
 | 
				
			||||||
                          },
 | 
					                          },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -243,7 +243,7 @@ export default function KvmIdRoute() {
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      heartbeat: true,
 | 
					      heartbeat: true,
 | 
				
			||||||
      retryOnError: true,
 | 
					      retryOnError: true,
 | 
				
			||||||
      reconnectAttempts: 5,
 | 
					      reconnectAttempts: 15,
 | 
				
			||||||
      reconnectInterval: 1000,
 | 
					      reconnectInterval: 1000,
 | 
				
			||||||
      onReconnectStop: () => {
 | 
					      onReconnectStop: () => {
 | 
				
			||||||
        console.log("Reconnect stopped");
 | 
					        console.log("Reconnect stopped");
 | 
				
			||||||
| 
						 | 
					@ -398,11 +398,6 @@ export default function KvmIdRoute() {
 | 
				
			||||||
    setConnectionFailed(false);
 | 
					    setConnectionFailed(false);
 | 
				
			||||||
    setLoadingMessage("Connecting to device...");
 | 
					    setLoadingMessage("Connecting to device...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (peerConnection?.signalingState === "stable") {
 | 
					 | 
				
			||||||
      console.log("[setupPeerConnection] Peer connection already established");
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let pc: RTCPeerConnection;
 | 
					    let pc: RTCPeerConnection;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      console.log("[setupPeerConnection] Creating peer connection");
 | 
					      console.log("[setupPeerConnection] Creating peer connection");
 | 
				
			||||||
| 
						 | 
					@ -499,7 +494,6 @@ export default function KvmIdRoute() {
 | 
				
			||||||
    cleanupAndStopReconnecting,
 | 
					    cleanupAndStopReconnecting,
 | 
				
			||||||
    iceConfig?.iceServers,
 | 
					    iceConfig?.iceServers,
 | 
				
			||||||
    legacyHTTPSignaling,
 | 
					    legacyHTTPSignaling,
 | 
				
			||||||
    peerConnection?.signalingState,
 | 
					 | 
				
			||||||
    sendWebRTCSignal,
 | 
					    sendWebRTCSignal,
 | 
				
			||||||
    setDiskChannel,
 | 
					    setDiskChannel,
 | 
				
			||||||
    setMediaMediaStream,
 | 
					    setMediaMediaStream,
 | 
				
			||||||
| 
						 | 
					@ -791,6 +785,7 @@ export default function KvmIdRoute() {
 | 
				
			||||||
            <button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
 | 
					            <button className="absolute top-0" tabIndex={-1} id="videoFocusTrap" />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </FocusTrap>
 | 
					        </FocusTrap>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className="grid h-full select-none grid-rows-headerBody">
 | 
					        <div className="grid h-full select-none grid-rows-headerBody">
 | 
				
			||||||
          <DashboardNavbar
 | 
					          <DashboardNavbar
 | 
				
			||||||
            primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
 | 
					            primaryLinks={isOnDevice ? [] : [{ title: "Cloud Devices", to: "/devices" }]}
 | 
				
			||||||
| 
						 | 
					@ -801,21 +796,23 @@ export default function KvmIdRoute() {
 | 
				
			||||||
            kvmName={deviceName || "JetKVM Device"}
 | 
					            kvmName={deviceName || "JetKVM Device"}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <div className="flex h-full w-full overflow-hidden">
 | 
					          <div className="relative flex h-full w-full overflow-hidden">
 | 
				
			||||||
            <div className="pointer-events-none fixed inset-0 isolate z-20 flex h-full w-full items-center justify-center">
 | 
					            <WebRTCVideo />
 | 
				
			||||||
              <div className="my-2 h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
 | 
					            <div
 | 
				
			||||||
 | 
					              style={{ animationDuration: "500ms" }}
 | 
				
			||||||
 | 
					              className="pointer-events-none absolute inset-0 flex animate-slideUpFade items-center justify-center p-4 opacity-0"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <div className="relative h-full max-h-[720px] w-full max-w-[1280px] rounded-md">
 | 
				
			||||||
                {!!ConnectionStatusElement && ConnectionStatusElement}
 | 
					                {!!ConnectionStatusElement && ConnectionStatusElement}
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
            {peerConnectionState === "connected" && <WebRTCVideo />}
 | 
					 | 
				
			||||||
            <SidebarContainer sidebarView={sidebarView} />
 | 
					            <SidebarContainer sidebarView={sidebarView} />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        className="isolate"
 | 
					        className="z-50"
 | 
				
			||||||
        onKeyUp={e => e.stopPropagation()}
 | 
					        onKeyUp={e => e.stopPropagation()}
 | 
				
			||||||
        onKeyDown={e => {
 | 
					        onKeyDown={e => {
 | 
				
			||||||
          e.stopPropagation();
 | 
					          e.stopPropagation();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										120
									
								
								web.go
								
								
								
								
							
							
						
						
									
										120
									
								
								web.go
								
								
								
								
							| 
						 | 
					@ -1,9 +1,11 @@
 | 
				
			||||||
package kvm
 | 
					package kvm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
	"embed"
 | 
						"embed"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/fs"
 | 
						"io/fs"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
| 
						 | 
					@ -99,6 +101,22 @@ func setupRouter() *gin.Engine {
 | 
				
			||||||
	protected := r.Group("/")
 | 
						protected := r.Group("/")
 | 
				
			||||||
	protected.Use(protectedMiddleware())
 | 
						protected.Use(protectedMiddleware())
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
 | 
							/*
 | 
				
			||||||
 | 
							 * Legacy WebRTC session endpoint
 | 
				
			||||||
 | 
							 *
 | 
				
			||||||
 | 
							 * This endpoint is maintained for backward compatibility when users upgrade from a version
 | 
				
			||||||
 | 
							 * using the legacy HTTP-based signaling method to the new WebSocket-based signaling method.
 | 
				
			||||||
 | 
							 *
 | 
				
			||||||
 | 
							 * During the upgrade process, when the "Rebooting device after update..." message appears,
 | 
				
			||||||
 | 
							 * the browser still runs the previous JavaScript code which polls this endpoint to establish
 | 
				
			||||||
 | 
							 * a new WebRTC session. Once the session is established, the page will automatically reload
 | 
				
			||||||
 | 
							 * with the updated code.
 | 
				
			||||||
 | 
							 *
 | 
				
			||||||
 | 
							 * Without this endpoint, the stale JavaScript would fail to establish a connection,
 | 
				
			||||||
 | 
							 * causing users to see the "Rebooting device after update..." message indefinitely
 | 
				
			||||||
 | 
							 * until they manually refresh the page, leading to a confusing user experience.
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							protected.POST("/webrtc/session", handleWebRTCSession)
 | 
				
			||||||
		protected.GET("/webrtc/signaling/client", handleLocalWebRTCSignal)
 | 
							protected.GET("/webrtc/signaling/client", handleLocalWebRTCSignal)
 | 
				
			||||||
		protected.POST("/cloud/register", handleCloudRegister)
 | 
							protected.POST("/cloud/register", handleCloudRegister)
 | 
				
			||||||
		protected.GET("/cloud/state", handleCloudState)
 | 
							protected.GET("/cloud/state", handleCloudState)
 | 
				
			||||||
| 
						 | 
					@ -126,11 +144,59 @@ func setupRouter() *gin.Engine {
 | 
				
			||||||
// TODO: support multiple sessions?
 | 
					// TODO: support multiple sessions?
 | 
				
			||||||
var currentSession *Session
 | 
					var currentSession *Session
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func handleWebRTCSession(c *gin.Context) {
 | 
				
			||||||
 | 
						var req WebRTCSessionRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := c.ShouldBindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session, err := newSession(SessionConfig{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, gin.H{"error": err})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sd, err := session.ExchangeOffer(req.Sd)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, gin.H{"error": err})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if currentSession != nil {
 | 
				
			||||||
 | 
							writeJSONRPCEvent("otherSessionConnected", nil, currentSession)
 | 
				
			||||||
 | 
							peerConn := currentSession.peerConnection
 | 
				
			||||||
 | 
							go func() {
 | 
				
			||||||
 | 
								time.Sleep(1 * time.Second)
 | 
				
			||||||
 | 
								_ = peerConn.Close()
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						currentSession = session
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, gin.H{"sd": sd})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						pingMessage = []byte("ping")
 | 
				
			||||||
 | 
						pongMessage = []byte("pong")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func handleLocalWebRTCSignal(c *gin.Context) {
 | 
					func handleLocalWebRTCSignal(c *gin.Context) {
 | 
				
			||||||
	cloudLogger.Infof("new websocket connection established")
 | 
						cloudLogger.Infof("new websocket connection established")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// get the source from the request
 | 
				
			||||||
 | 
						source := c.ClientIP()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create WebSocket options with InsecureSkipVerify to bypass origin check
 | 
						// Create WebSocket options with InsecureSkipVerify to bypass origin check
 | 
				
			||||||
	wsOptions := &websocket.AcceptOptions{
 | 
						wsOptions := &websocket.AcceptOptions{
 | 
				
			||||||
		InsecureSkipVerify: true, // Allow connections from any origin
 | 
							InsecureSkipVerify: true, // Allow connections from any origin
 | 
				
			||||||
 | 
							OnPingReceived: func(ctx context.Context, payload []byte) bool {
 | 
				
			||||||
 | 
								websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: local", payload, source)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								metricConnectionTotalPingReceivedCount.WithLabelValues("local", source).Inc()
 | 
				
			||||||
 | 
								metricConnectionLastPingReceivedTimestamp.WithLabelValues("local", source).SetToCurrentTime()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	wsCon, err := websocket.Accept(c.Writer, c.Request, wsOptions)
 | 
						wsCon, err := websocket.Accept(c.Writer, c.Request, wsOptions)
 | 
				
			||||||
| 
						 | 
					@ -139,9 +205,6 @@ func handleLocalWebRTCSignal(c *gin.Context) {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// get the source from the request
 | 
					 | 
				
			||||||
	source := c.ClientIP()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Now use conn for websocket operations
 | 
						// Now use conn for websocket operations
 | 
				
			||||||
	defer wsCon.Close(websocket.StatusNormalClosure, "")
 | 
						defer wsCon.Close(websocket.StatusNormalClosure, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -164,7 +227,6 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Add connection tracking to detect reconnections
 | 
						// Add connection tracking to detect reconnections
 | 
				
			||||||
	connectionID := uuid.New().String()
 | 
						connectionID := uuid.New().String()
 | 
				
			||||||
	cloudLogger.Infof("new websocket connection established with ID: %s", connectionID)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// connection type
 | 
						// connection type
 | 
				
			||||||
	var sourceType string
 | 
						var sourceType string
 | 
				
			||||||
| 
						 | 
					@ -176,29 +238,40 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// probably we can use a better logging framework here
 | 
						// probably we can use a better logging framework here
 | 
				
			||||||
	logInfof := func(format string, args ...interface{}) {
 | 
						logInfof := func(format string, args ...interface{}) {
 | 
				
			||||||
		args = append(args, source, sourceType)
 | 
							args = append(args, source, sourceType, connectionID)
 | 
				
			||||||
		websocketLogger.Infof(format+", source: %s, sourceType: %s", args...)
 | 
							websocketLogger.Infof(format+", source: %s, sourceType: %s, id: %s", args...)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	logWarnf := func(format string, args ...interface{}) {
 | 
						logWarnf := func(format string, args ...interface{}) {
 | 
				
			||||||
		args = append(args, source, sourceType)
 | 
							args = append(args, source, sourceType, connectionID)
 | 
				
			||||||
		websocketLogger.Warnf(format+", source: %s, sourceType: %s", args...)
 | 
							websocketLogger.Warnf(format+", source: %s, sourceType: %s, id: %s", args...)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	logTracef := func(format string, args ...interface{}) {
 | 
						logTracef := func(format string, args ...interface{}) {
 | 
				
			||||||
		args = append(args, source, sourceType)
 | 
							args = append(args, source, sourceType, connectionID)
 | 
				
			||||||
		websocketLogger.Tracef(format+", source: %s, sourceType: %s", args...)
 | 
							websocketLogger.Tracef(format+", source: %s, sourceType: %s, id: %s", args...)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logInfof("new websocket connection established")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go func() {
 | 
						go func() {
 | 
				
			||||||
		for {
 | 
							for {
 | 
				
			||||||
			time.Sleep(WebsocketPingInterval)
 | 
								time.Sleep(WebsocketPingInterval)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if ctxErr := runCtx.Err(); ctxErr != nil {
 | 
				
			||||||
 | 
									if !errors.Is(ctxErr, context.Canceled) {
 | 
				
			||||||
 | 
										logWarnf("websocket connection closed: %v", ctxErr)
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										logTracef("websocket connection closed as the context was canceled: %v")
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// set the timer for the ping duration
 | 
								// set the timer for the ping duration
 | 
				
			||||||
			timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
 | 
								timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
 | 
				
			||||||
				metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(v)
 | 
									metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(v)
 | 
				
			||||||
				metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v)
 | 
									metricConnectionPingDuration.WithLabelValues(sourceType, source).Observe(v)
 | 
				
			||||||
			}))
 | 
								}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			logInfof("pinging websocket")
 | 
								logTracef("sending ping frame")
 | 
				
			||||||
			err := wsCon.Ping(runCtx)
 | 
								err := wsCon.Ping(runCtx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
| 
						 | 
					@ -208,10 +281,12 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// dont use `defer` here because we want to observe the duration of the ping
 | 
								// dont use `defer` here because we want to observe the duration of the ping
 | 
				
			||||||
			timer.ObserveDuration()
 | 
								duration := timer.ObserveDuration()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			metricConnectionTotalPingCount.WithLabelValues(sourceType, source).Inc()
 | 
								metricConnectionTotalPingSentCount.WithLabelValues(sourceType, source).Inc()
 | 
				
			||||||
			metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
 | 
								metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								logTracef("received pong frame, duration: %v", duration)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}()
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -249,6 +324,20 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
 | 
				
			||||||
			Data json.RawMessage `json:"data"`
 | 
								Data json.RawMessage `json:"data"`
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if bytes.Equal(msg, pingMessage) {
 | 
				
			||||||
 | 
								logInfof("ping message received: %s", string(msg))
 | 
				
			||||||
 | 
								err = wsCon.Write(context.Background(), websocket.MessageText, pongMessage)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logWarnf("unable to write pong message: %v", err)
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								metricConnectionTotalPingReceivedCount.WithLabelValues(sourceType, source).Inc()
 | 
				
			||||||
 | 
								metricConnectionLastPingReceivedTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err = json.Unmarshal(msg, &message)
 | 
							err = json.Unmarshal(msg, &message)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			logWarnf("unable to parse ws message: %v", err)
 | 
								logWarnf("unable to parse ws message: %v", err)
 | 
				
			||||||
| 
						 | 
					@ -264,8 +353,9 @@ func handleWebRTCSignalWsMessages(wsCon *websocket.Conn, isCloudConnection bool,
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			logInfof("new session request: %v", req.OidcGoogle)
 | 
								if req.OidcGoogle != "" {
 | 
				
			||||||
			logTracef("session request info: %v", req)
 | 
									logInfof("new session request with OIDC Google: %v", req.OidcGoogle)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc()
 | 
								metricConnectionSessionRequestCount.WithLabelValues(sourceType, source).Inc()
 | 
				
			||||||
			metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
 | 
								metricConnectionLastSessionRequestTimestamp.WithLabelValues(sourceType, source).SetToCurrentTime()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue