mirror of https://github.com/jetkvm/kvm.git
Compare commits
45 Commits
release/0.
...
main
Author | SHA1 | Date |
---|---|---|
|
fe127ed41c | |
|
3e7d8fb0f5 | |
|
0d7f47c109 | |
|
254c001572 | |
|
6f037a832d | |
|
ccba27cedd | |
|
cf9c6e5cc8 | |
|
ffeaf8cced | |
|
a1ed28c676 | |
|
1674a6666c | |
|
772527849f | |
|
19871517ec | |
|
b822b73a03 | |
|
58ade3b551 | |
|
3cc119c646 | |
|
c494cf26ef | |
|
4bfbc66ea7 | |
|
0636cc9aff | |
|
4f6026e182 | |
|
89f3bc8c40 | |
|
91171d9bf7 | |
|
0d955a8d95 | |
|
a40d26ab9b | |
|
9bd587b52e | |
|
7ef9a7ba93 | |
|
bfbc1a5a57 | |
|
abb4350316 | |
|
52825da68d | |
|
9d2abd9fb0 | |
|
52dd675e52 | |
|
e95e30e48c | |
|
eaa58492ab | |
|
f4bb47c544 | |
|
a7693df92c | |
|
8d77d75294 | |
|
718b343713 | |
|
1f7c5c94d8 | |
|
55d7f22c47 | |
|
a28676cd94 | |
|
2ec061b3a8 | |
|
7e64a529f8 | |
|
1b5062c504 | |
|
c1d771cced | |
|
019934d33e | |
|
0c5c69f2d3 |
|
@ -9,6 +9,19 @@
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": [
|
||||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
|
||||||
]
|
],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"GitHub.vscode-pull-request-github",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"golang.go",
|
||||||
|
"ms-vscode.makefile-tools",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"github.vscode-github-actions"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /ui
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
|
@ -23,9 +23,9 @@ jobs:
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: "**/package-lock.json"
|
cache-dependency-path: "**/package-lock.json"
|
||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.3"
|
go-version: "1.24.4"
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
make frontend
|
make frontend
|
||||||
|
|
|
@ -24,9 +24,9 @@ jobs:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4.2.1
|
||||||
with:
|
with:
|
||||||
go-version: 1.24.3
|
go-version: 1.24.4
|
||||||
- name: Create empty resource directory
|
- name: Create empty resource directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p static && touch static/.gitkeep
|
mkdir -p static && touch static/.gitkeep
|
||||||
|
|
|
@ -104,9 +104,9 @@ jobs:
|
||||||
EOF
|
EOF
|
||||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.3"
|
go-version: "1.24.4"
|
||||||
- name: Golang Test Report
|
- name: Golang Test Report
|
||||||
uses: becheran/go-testreport@v0.3.2
|
uses: becheran/go-testreport@v0.3.2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"tailwindCSS.classFunctions": ["cva", "cx"]
|
||||||
|
}
|
11
Makefile
11
Makefile
|
@ -2,12 +2,14 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
||||||
BUILDTS ?= $(shell date -u +%s)
|
BUILDTS ?= $(shell date -u +%s)
|
||||||
REVISION ?= $(shell git rev-parse HEAD)
|
REVISION ?= $(shell git rev-parse HEAD)
|
||||||
VERSION_DEV := 0.4.2-dev$(shell date +%Y%m%d%H%M)
|
VERSION_DEV ?= 0.4.6-dev$(shell date +%Y%m%d%H%M)
|
||||||
VERSION := 0.4.1
|
VERSION ?= 0.4.5
|
||||||
|
|
||||||
PROMETHEUS_TAG := github.com/prometheus/common/version
|
PROMETHEUS_TAG := github.com/prometheus/common/version
|
||||||
KVM_PKG_NAME := github.com/jetkvm/kvm
|
KVM_PKG_NAME := github.com/jetkvm/kvm
|
||||||
|
|
||||||
|
GO_BUILD_ARGS := -tags netgo
|
||||||
|
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
|
||||||
GO_LDFLAGS := \
|
GO_LDFLAGS := \
|
||||||
-s -w \
|
-s -w \
|
||||||
-X $(PROMETHEUS_TAG).Branch=$(BRANCH) \
|
-X $(PROMETHEUS_TAG).Branch=$(BRANCH) \
|
||||||
|
@ -27,7 +29,7 @@ build_dev: hash_resource
|
||||||
@echo "Building..."
|
@echo "Building..."
|
||||||
$(GO_CMD) build \
|
$(GO_CMD) build \
|
||||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
||||||
-trimpath \
|
$(GO_RELEASE_BUILD_ARGS) \
|
||||||
-o $(BIN_DIR)/jetkvm_app cmd/main.go
|
-o $(BIN_DIR)/jetkvm_app cmd/main.go
|
||||||
|
|
||||||
build_test2json:
|
build_test2json:
|
||||||
|
@ -50,6 +52,7 @@ build_dev_test: build_test2json build_gotestsum
|
||||||
test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \
|
test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \
|
||||||
$(GO_CMD) test -v \
|
$(GO_CMD) test -v \
|
||||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
||||||
|
$(GO_BUILD_ARGS) \
|
||||||
-c -o $(BIN_DIR)/tests/$$test_filename $$test; \
|
-c -o $(BIN_DIR)/tests/$$test_filename $$test; \
|
||||||
echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \
|
echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \
|
||||||
done; \
|
done; \
|
||||||
|
@ -71,7 +74,7 @@ build_release: frontend hash_resource
|
||||||
@echo "Building release..."
|
@echo "Building release..."
|
||||||
$(GO_CMD) build \
|
$(GO_CMD) build \
|
||||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \
|
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \
|
||||||
-trimpath \
|
$(GO_RELEASE_BUILD_ARGS) \
|
||||||
-o bin/jetkvm_app cmd/main.go
|
-o bin/jetkvm_app cmd/main.go
|
||||||
|
|
||||||
release:
|
release:
|
||||||
|
|
22
cloud.go
22
cloud.go
|
@ -51,34 +51,34 @@ var (
|
||||||
)
|
)
|
||||||
metricCloudConnectionEstablishedTimestamp = promauto.NewGauge(
|
metricCloudConnectionEstablishedTimestamp = promauto.NewGauge(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_cloud_connection_established_timestamp",
|
Name: "jetkvm_cloud_connection_established_timestamp_seconds",
|
||||||
Help: "The timestamp when the cloud connection was established",
|
Help: "The timestamp when the cloud connection was established",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricConnectionLastPingTimestamp = promauto.NewGaugeVec(
|
metricConnectionLastPingTimestamp = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_connection_last_ping_timestamp",
|
Name: "jetkvm_connection_last_ping_timestamp_seconds",
|
||||||
Help: "The timestamp when the last ping response was received",
|
Help: "The timestamp when the last ping response was received",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec(
|
metricConnectionLastPingReceivedTimestamp = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_connection_last_ping_received_timestamp",
|
Name: "jetkvm_connection_last_ping_received_timestamp_seconds",
|
||||||
Help: "The timestamp when the last ping request was received",
|
Help: "The timestamp when the last ping request was received",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]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_seconds",
|
||||||
Help: "The duration of the last ping response",
|
Help: "The duration of the last ping response",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionPingDuration = promauto.NewHistogramVec(
|
metricConnectionPingDuration = promauto.NewHistogramVec(
|
||||||
prometheus.HistogramOpts{
|
prometheus.HistogramOpts{
|
||||||
Name: "jetkvm_connection_ping_duration",
|
Name: "jetkvm_connection_ping_duration_seconds",
|
||||||
Help: "The duration of the ping response",
|
Help: "The duration of the ping response",
|
||||||
Buckets: []float64{
|
Buckets: []float64{
|
||||||
0.1, 0.5, 1, 10,
|
0.1, 0.5, 1, 10,
|
||||||
|
@ -88,28 +88,28 @@ var (
|
||||||
)
|
)
|
||||||
metricConnectionTotalPingSentCount = promauto.NewCounterVec(
|
metricConnectionTotalPingSentCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_connection_total_ping_sent",
|
Name: "jetkvm_connection_ping_sent_total",
|
||||||
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(
|
metricConnectionTotalPingReceivedCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_connection_total_ping_received",
|
Name: "jetkvm_connection_ping_received_total",
|
||||||
Help: "The total number of pings received from the connection",
|
Help: "The total number of pings received from the connection",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionSessionRequestCount = promauto.NewCounterVec(
|
metricConnectionSessionRequestCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_connection_session_total_requests",
|
Name: "jetkvm_connection_session_requests_total",
|
||||||
Help: "The total number of session requests received",
|
Help: "The total number of session requests received",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
)
|
)
|
||||||
metricConnectionSessionRequestDuration = promauto.NewHistogramVec(
|
metricConnectionSessionRequestDuration = promauto.NewHistogramVec(
|
||||||
prometheus.HistogramOpts{
|
prometheus.HistogramOpts{
|
||||||
Name: "jetkvm_connection_session_request_duration",
|
Name: "jetkvm_connection_session_request_duration_seconds",
|
||||||
Help: "The duration of session requests",
|
Help: "The duration of session requests",
|
||||||
Buckets: []float64{
|
Buckets: []float64{
|
||||||
0.1, 0.5, 1, 10,
|
0.1, 0.5, 1, 10,
|
||||||
|
@ -119,7 +119,7 @@ var (
|
||||||
)
|
)
|
||||||
metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec(
|
metricConnectionLastSessionRequestTimestamp = promauto.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
Name: "jetkvm_connection_last_session_request_timestamp",
|
Name: "jetkvm_connection_last_session_request_timestamp_seconds",
|
||||||
Help: "The timestamp of the last session request",
|
Help: "The timestamp of the last session request",
|
||||||
},
|
},
|
||||||
[]string{"type", "source"},
|
[]string{"type", "source"},
|
||||||
|
@ -133,7 +133,7 @@ var (
|
||||||
)
|
)
|
||||||
metricCloudConnectionFailureCount = promauto.NewCounter(
|
metricCloudConnectionFailureCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_cloud_connection_failure_count",
|
Name: "jetkvm_cloud_connection_failure_total",
|
||||||
Help: "The number of times the cloud connection has failed",
|
Help: "The number of times the cloud connection has failed",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -85,6 +85,7 @@ type Config struct {
|
||||||
HashedPassword string `json:"hashed_password"`
|
HashedPassword string `json:"hashed_password"`
|
||||||
LocalAuthToken string `json:"local_auth_token"`
|
LocalAuthToken string `json:"local_auth_token"`
|
||||||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||||
|
LocalLoopbackOnly bool `json:"local_loopback_only"`
|
||||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||||
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
|
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
|
||||||
KeyboardLayout string `json:"keyboard_layout"`
|
KeyboardLayout string `json:"keyboard_layout"`
|
||||||
|
@ -110,7 +111,7 @@ var defaultConfig = &Config{
|
||||||
ActiveExtension: "",
|
ActiveExtension: "",
|
||||||
KeyboardMacros: []KeyboardMacro{},
|
KeyboardMacros: []KeyboardMacro{},
|
||||||
DisplayRotation: "270",
|
DisplayRotation: "270",
|
||||||
KeyboardLayout: "en-US",
|
KeyboardLayout: "en_US",
|
||||||
DisplayMaxBrightness: 64,
|
DisplayMaxBrightness: 64,
|
||||||
DisplayDimAfterSec: 120, // 2 minutes
|
DisplayDimAfterSec: 120, // 2 minutes
|
||||||
DisplayOffAfterSec: 1800, // 30 minutes
|
DisplayOffAfterSec: 1800, // 30 minutes
|
||||||
|
|
|
@ -174,7 +174,7 @@ cd "${REMOTE_PATH}"
|
||||||
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=${LOG_TRACE_SCOPES} ./jetkvm_app_debug
|
PION_LOG_TRACE=${LOG_TRACE_SCOPES} GODEBUG=netdns=1 ./jetkvm_app_debug
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Deployment complete."
|
echo "Deployment complete."
|
15
display.go
15
display.go
|
@ -339,10 +339,18 @@ func startBacklightTickers() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
// Stop existing tickers to prevent multiple active instances on repeated calls
|
||||||
|
if dimTicker != nil {
|
||||||
|
dimTicker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if offTicker != nil {
|
||||||
|
offTicker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DisplayDimAfterSec != 0 {
|
||||||
displayLogger.Info().Msg("dim_ticker has started")
|
displayLogger.Info().Msg("dim_ticker has started")
|
||||||
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
defer dimTicker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for { //nolint:staticcheck
|
for { //nolint:staticcheck
|
||||||
|
@ -354,10 +362,9 @@ func startBacklightTickers() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
if config.DisplayOffAfterSec != 0 {
|
||||||
displayLogger.Info().Msg("off_ticker has started")
|
displayLogger.Info().Msg("off_ticker has started")
|
||||||
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
defer offTicker.Stop()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for { //nolint:staticcheck
|
for { //nolint:staticcheck
|
||||||
|
|
56
go.mod
56
go.mod
|
@ -5,33 +5,33 @@ go 1.23.4
|
||||||
toolchain go1.24.3
|
toolchain go1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.3.0
|
github.com/Masterminds/semver/v3 v3.3.1
|
||||||
github.com/beevik/ntp v1.3.1
|
github.com/beevik/ntp v1.4.3
|
||||||
github.com/coder/websocket v1.8.13
|
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/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/gin-contrib/logger v1.2.5
|
github.com/gin-contrib/logger v1.2.6
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/guregu/null/v6 v6.0.0
|
github.com/guregu/null/v6 v6.0.0
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
github.com/hanwen/go-fuse/v2 v2.8.0
|
||||||
github.com/pion/logging v0.2.2
|
github.com/pion/logging v0.2.3
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
github.com/pion/webrtc/v4 v4.0.0
|
github.com/pion/webrtc/v4 v4.0.16
|
||||||
github.com/pojntfx/go-nbd v0.3.2
|
github.com/pojntfx/go-nbd v0.3.2
|
||||||
github.com/prometheus/client_golang v1.21.0
|
github.com/prometheus/client_golang v1.22.0
|
||||||
github.com/prometheus/common v0.62.0
|
github.com/prometheus/common v0.62.0
|
||||||
github.com/prometheus/procfs v0.15.1
|
github.com/prometheus/procfs v0.16.1
|
||||||
github.com/psanford/httpreadat v0.1.0
|
github.com/psanford/httpreadat v0.1.0
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/vishvananda/netlink v1.3.0
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.bug.st/serial v1.6.2
|
go.bug.st/serial v1.6.2
|
||||||
golang.org/x/crypto v0.38.0
|
golang.org/x/crypto v0.39.0
|
||||||
golang.org/x/net v0.40.0
|
golang.org/x/net v0.41.0
|
||||||
golang.org/x/sys v0.33.0
|
golang.org/x/sys v0.33.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -45,16 +45,14 @@ require (
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/creack/goselect v0.1.2 // indirect
|
github.com/creack/goselect v0.1.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
@ -62,21 +60,21 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/pilebones/go-udev v0.9.0 // indirect
|
github.com/pilebones/go-udev v0.9.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.9 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.3 // indirect
|
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||||
github.com/pion/ice/v4 v4.0.2 // indirect
|
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||||
github.com/pion/interceptor v0.1.37 // indirect
|
github.com/pion/interceptor v0.1.40 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.14 // indirect
|
github.com/pion/rtcp v1.2.15 // indirect
|
||||||
github.com/pion/rtp v1.8.9 // indirect
|
github.com/pion/rtp v1.8.18 // indirect
|
||||||
github.com/pion/sctp v1.8.33 // indirect
|
github.com/pion/sctp v1.8.39 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
|
@ -84,9 +82,9 @@ require (
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/arch v0.15.0 // indirect
|
golang.org/x/arch v0.17.0 // indirect
|
||||||
golang.org/x/oauth2 v0.24.0 // indirect
|
golang.org/x/oauth2 v0.24.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
122
go.sum
122
go.sum
|
@ -1,7 +1,7 @@
|
||||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM=
|
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||||
github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw=
|
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
|
@ -30,16 +30,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM=
|
github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc=
|
||||||
github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I=
|
github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64=
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
@ -60,12 +60,12 @@ github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||||
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA=
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
@ -77,7 +77,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
|
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
@ -89,8 +88,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
|
||||||
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
|
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
@ -98,53 +97,53 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
||||||
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||||
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/pion/dtls/v3 v3.0.3 h1:j5ajZbQwff7Z8k3pE3S+rQ4STvKvXUdKsi/07ka+OWM=
|
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||||
github.com/pion/dtls/v3 v3.0.3/go.mod h1:weOTUyIV4z0bQaVzKe8kpaP17+us3yAuiQsEAG1STMU=
|
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||||
github.com/pion/ice/v4 v4.0.2 h1:1JhBRX8iQLi0+TfcavTjPjI6GO41MFn4CeTBX+Y9h5s=
|
github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
||||||
github.com/pion/ice/v4 v4.0.2/go.mod h1:DCdqyzgtsDNYN6/3U8044j3U7qsJ9KFJC92VnOWHvXg=
|
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||||
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||||
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||||
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||||
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||||
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||||
github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
|
github.com/pion/webrtc/v4 v4.0.16 h1:5f8QMVIbNvJr2mPRGi2QamkPa/LVUB6NWolOCwphKHA=
|
||||||
github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
|
github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zNY6VSJno=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
@ -157,14 +156,11 @@ github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzr
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
@ -179,16 +175,14 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
||||||
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
@ -196,8 +190,8 @@ golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
@ -95,16 +95,27 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now
|
||||||
} else if errors.Is(err, context.Canceled) {
|
} else if errors.Is(err, context.Canceled) {
|
||||||
metricHttpCancelCount.WithLabelValues(url).Inc()
|
metricHttpCancelCount.WithLabelValues(url).Inc()
|
||||||
metricHttpTotalCancelCount.Inc()
|
metricHttpTotalCancelCount.Inc()
|
||||||
|
results <- nil
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Warn().
|
scopedLogger.Warn().
|
||||||
Str("error", err.Error()).
|
Str("error", err.Error()).
|
||||||
Int("status", status).
|
Int("status", status).
|
||||||
Msg("failed to query HTTP server")
|
Msg("failed to query HTTP server")
|
||||||
|
results <- nil
|
||||||
}
|
}
|
||||||
}(url)
|
}(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <-results
|
for range urls {
|
||||||
|
result := <-results
|
||||||
|
if result == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
now = result
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func queryHttpTime(
|
func queryHttpTime(
|
||||||
|
|
|
@ -14,44 +14,44 @@ var (
|
||||||
)
|
)
|
||||||
metricTimeSyncCount = promauto.NewCounter(
|
metricTimeSyncCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_count",
|
Name: "jetkvm_timesync_total",
|
||||||
Help: "The number of times the timesync has been run",
|
Help: "The number of times the timesync has been run",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricTimeSyncSuccessCount = promauto.NewCounter(
|
metricTimeSyncSuccessCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_success_count",
|
Name: "jetkvm_timesync_success_total",
|
||||||
Help: "The number of times the timesync has been successful",
|
Help: "The number of times the timesync has been successful",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricRTCUpdateCount = promauto.NewCounter( //nolint:unused
|
metricRTCUpdateCount = promauto.NewCounter( //nolint:unused
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_rtc_update_count",
|
Name: "jetkvm_timesync_rtc_update_total",
|
||||||
Help: "The number of times the RTC has been updated",
|
Help: "The number of times the RTC has been updated",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricNtpTotalSuccessCount = promauto.NewCounter(
|
metricNtpTotalSuccessCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_ntp_total_success_count",
|
Name: "jetkvm_timesync_ntp_total_success_total",
|
||||||
Help: "The total number of successful NTP requests",
|
Help: "The total number of successful NTP requests",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricNtpTotalRequestCount = promauto.NewCounter(
|
metricNtpTotalRequestCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_ntp_total_request_count",
|
Name: "jetkvm_timesync_ntp_total_request_total",
|
||||||
Help: "The total number of NTP requests sent",
|
Help: "The total number of NTP requests sent",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricNtpSuccessCount = promauto.NewCounterVec(
|
metricNtpSuccessCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_ntp_success_count",
|
Name: "jetkvm_timesync_ntp_success_total",
|
||||||
Help: "The number of successful NTP requests",
|
Help: "The number of successful NTP requests",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
)
|
)
|
||||||
metricNtpRequestCount = promauto.NewCounterVec(
|
metricNtpRequestCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_ntp_request_count",
|
Name: "jetkvm_timesync_ntp_request_total",
|
||||||
Help: "The number of NTP requests sent to the server",
|
Help: "The number of NTP requests sent to the server",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
|
@ -83,39 +83,39 @@ var (
|
||||||
|
|
||||||
metricHttpTotalSuccessCount = promauto.NewCounter(
|
metricHttpTotalSuccessCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_total_success_count",
|
Name: "jetkvm_timesync_http_total_success_total",
|
||||||
Help: "The total number of successful HTTP requests",
|
Help: "The total number of successful HTTP requests",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricHttpTotalRequestCount = promauto.NewCounter(
|
metricHttpTotalRequestCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_total_request_count",
|
Name: "jetkvm_timesync_http_total_request_total",
|
||||||
Help: "The total number of HTTP requests sent",
|
Help: "The total number of HTTP requests sent",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricHttpTotalCancelCount = promauto.NewCounter(
|
metricHttpTotalCancelCount = promauto.NewCounter(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_total_cancel_count",
|
Name: "jetkvm_timesync_http_total_cancel_total",
|
||||||
Help: "The total number of HTTP requests cancelled",
|
Help: "The total number of HTTP requests cancelled",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
metricHttpSuccessCount = promauto.NewCounterVec(
|
metricHttpSuccessCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_success_count",
|
Name: "jetkvm_timesync_http_success_total",
|
||||||
Help: "The number of successful HTTP requests",
|
Help: "The number of successful HTTP requests",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
)
|
)
|
||||||
metricHttpRequestCount = promauto.NewCounterVec(
|
metricHttpRequestCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_request_count",
|
Name: "jetkvm_timesync_http_request_total",
|
||||||
Help: "The number of HTTP requests sent to the server",
|
Help: "The number of HTTP requests sent to the server",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
)
|
)
|
||||||
metricHttpCancelCount = promauto.NewCounterVec(
|
metricHttpCancelCount = promauto.NewCounterVec(
|
||||||
prometheus.CounterOpts{
|
prometheus.CounterOpts{
|
||||||
Name: "jetkvm_timesync_http_cancel_count",
|
Name: "jetkvm_timesync_http_cancel_total",
|
||||||
Help: "The number of HTTP requests cancelled",
|
Help: "The number of HTTP requests cancelled",
|
||||||
},
|
},
|
||||||
[]string{"url"},
|
[]string{"url"},
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
|
@ -149,6 +150,12 @@ func (c *DHCPClient) loadLeaseFile() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
isFirstLoad := c.lease == nil
|
isFirstLoad := c.lease == nil
|
||||||
|
|
||||||
|
// Skip processing if lease hasn't changed to avoid unnecessary wake-ups.
|
||||||
|
if reflect.DeepEqual(c.lease, lease) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
c.lease = lease
|
c.lease = lease
|
||||||
|
|
||||||
if lease.IPAddress == nil {
|
if lease.IPAddress == nil {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var keyboardConfig = gadgetConfigItem{
|
var keyboardConfig = gadgetConfigItem{
|
||||||
|
@ -36,6 +39,7 @@ var keyboardReportDesc = []byte{
|
||||||
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
|
||||||
0x95, 0x05, /* REPORT_COUNT (5) */
|
0x95, 0x05, /* REPORT_COUNT (5) */
|
||||||
0x75, 0x01, /* REPORT_SIZE (1) */
|
0x75, 0x01, /* REPORT_SIZE (1) */
|
||||||
|
|
||||||
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
0x05, 0x08, /* USAGE_PAGE (LEDs) */
|
||||||
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */
|
||||||
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
0x29, 0x05, /* USAGE_MAXIMUM (Kana) */
|
||||||
|
@ -54,23 +58,155 @@ var keyboardReportDesc = []byte{
|
||||||
0xc0, /* END_COLLECTION */
|
0xc0, /* END_COLLECTION */
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
const (
|
||||||
if u.keyboardHidFile == nil {
|
hidReadBufferSize = 8
|
||||||
var err error
|
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
|
||||||
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
// https://www.usb.org/sites/default/files/hut1_2.pdf
|
||||||
if err != nil {
|
KeyboardLedMaskNumLock = 1 << 0
|
||||||
return fmt.Errorf("failed to open hidg0: %w", err)
|
KeyboardLedMaskCapsLock = 1 << 1
|
||||||
|
KeyboardLedMaskScrollLock = 1 << 2
|
||||||
|
KeyboardLedMaskCompose = 1 << 3
|
||||||
|
KeyboardLedMaskKana = 1 << 4
|
||||||
|
ValidKeyboardLedMasks = KeyboardLedMaskNumLock | KeyboardLedMaskCapsLock | KeyboardLedMaskScrollLock | KeyboardLedMaskCompose | KeyboardLedMaskKana
|
||||||
|
)
|
||||||
|
|
||||||
|
// Synchronization between LED states and CAPS LOCK, NUM LOCK, SCROLL LOCK,
|
||||||
|
// COMPOSE, and KANA events is maintained by the host and NOT the keyboard. If
|
||||||
|
// using the keyboard descriptor in Appendix B, LED states are set by sending a
|
||||||
|
// 5-bit absolute report to the keyboard via a Set_Report(Output) request.
|
||||||
|
type KeyboardState struct {
|
||||||
|
NumLock bool `json:"num_lock"`
|
||||||
|
CapsLock bool `json:"caps_lock"`
|
||||||
|
ScrollLock bool `json:"scroll_lock"`
|
||||||
|
Compose bool `json:"compose"`
|
||||||
|
Kana bool `json:"kana"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyboardState(b byte) KeyboardState {
|
||||||
|
// should we check if it's the correct usage page?
|
||||||
|
return KeyboardState{
|
||||||
|
NumLock: b&KeyboardLedMaskNumLock != 0,
|
||||||
|
CapsLock: b&KeyboardLedMaskCapsLock != 0,
|
||||||
|
ScrollLock: b&KeyboardLedMaskScrollLock != 0,
|
||||||
|
Compose: b&KeyboardLedMaskCompose != 0,
|
||||||
|
Kana: b&KeyboardLedMaskKana != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) updateKeyboardState(b byte) {
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
if b&^ValidKeyboardLedMasks != 0 {
|
||||||
|
u.log.Trace().Uint8("b", b).Msg("contains invalid bits, ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := getKeyboardState(b)
|
||||||
|
if reflect.DeepEqual(u.keyboardState, newState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.log.Info().Interface("old", u.keyboardState).Interface("new", newState).Msg("keyboardState updated")
|
||||||
|
u.keyboardState = newState
|
||||||
|
|
||||||
|
if u.onKeyboardStateChange != nil {
|
||||||
|
(*u.onKeyboardStateChange)(newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) SetOnKeyboardStateChange(f func(state KeyboardState)) {
|
||||||
|
u.onKeyboardStateChange = &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) GetKeyboardState() KeyboardState {
|
||||||
|
u.keyboardStateLock.Lock()
|
||||||
|
defer u.keyboardStateLock.Unlock()
|
||||||
|
|
||||||
|
return u.keyboardState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) listenKeyboardEvents() {
|
||||||
|
var path string
|
||||||
|
if u.keyboardHidFile != nil {
|
||||||
|
path = u.keyboardHidFile.Name()
|
||||||
|
}
|
||||||
|
l := u.log.With().Str("listener", "keyboardEvents").Str("path", path).Logger()
|
||||||
|
l.Trace().Msg("starting")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, hidReadBufferSize)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-u.keyboardStateCtx.Done():
|
||||||
|
l.Info().Msg("context done")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
l.Trace().Msg("reading from keyboard")
|
||||||
|
if u.keyboardHidFile == nil {
|
||||||
|
u.logWithSupression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
|
||||||
|
// show the error every 100 times to avoid spamming the logs
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// reset the counter
|
||||||
|
u.resetLogSuppressionCounter("keyboardHidFileNil")
|
||||||
|
|
||||||
|
n, err := u.keyboardHidFile.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
u.logWithSupression("keyboardHidFileRead", 100, &l, err, "failed to read")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.resetLogSuppressionCounter("keyboardHidFileRead")
|
||||||
|
|
||||||
|
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
|
||||||
|
if n != 1 {
|
||||||
|
l.Trace().Int("n", n).Msg("expected 1 byte, got")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.updateKeyboardState(buf[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) openKeyboardHidFile() error {
|
||||||
|
if u.keyboardHidFile != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
u.keyboardHidFile, err = os.OpenFile("/dev/hidg0", os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open hidg0: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.keyboardStateCancel != nil {
|
||||||
|
u.keyboardStateCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.keyboardStateCtx, u.keyboardStateCancel = context.WithCancel(context.Background())
|
||||||
|
u.listenKeyboardEvents()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) OpenKeyboardHidFile() error {
|
||||||
|
return u.openKeyboardHidFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||||
|
if err := u.openKeyboardHidFile(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := u.keyboardHidFile.Write(data)
|
_, err := u.keyboardHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg0")
|
u.logWithSupression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||||
u.keyboardHidFile.Close()
|
u.keyboardHidFile.Close()
|
||||||
u.keyboardHidFile = nil
|
u.keyboardHidFile = nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
u.resetLogSuppressionCounter("keyboardWriteHidFile")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ var absoluteMouseConfig = gadgetConfigItem{
|
||||||
configPath: []string{"hid.usb1"},
|
configPath: []string{"hid.usb1"},
|
||||||
attrs: gadgetAttributes{
|
attrs: gadgetAttributes{
|
||||||
"protocol": "2",
|
"protocol": "2",
|
||||||
"subclass": "1",
|
"subclass": "0",
|
||||||
"report_length": "6",
|
"report_length": "6",
|
||||||
},
|
},
|
||||||
reportDesc: absoluteMouseCombinedReportDesc,
|
reportDesc: absoluteMouseCombinedReportDesc,
|
||||||
|
@ -75,11 +75,12 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
||||||
|
|
||||||
_, err := u.absMouseHidFile.Write(data)
|
_, err := u.absMouseHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg1")
|
u.logWithSupression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
||||||
u.absMouseHidFile.Close()
|
u.absMouseHidFile.Close()
|
||||||
u.absMouseHidFile = nil
|
u.absMouseHidFile = nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
u.resetLogSuppressionCounter("absMouseWriteHidFile")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,11 +65,12 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
||||||
|
|
||||||
_, err := u.relMouseHidFile.Write(data)
|
_, err := u.relMouseHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg2")
|
u.logWithSupression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
||||||
u.relMouseHidFile.Close()
|
u.relMouseHidFile.Close()
|
||||||
u.relMouseHidFile = nil
|
u.relMouseHidFile = nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
u.resetLogSuppressionCounter("relMouseWriteHidFile")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -59,6 +60,11 @@ type UsbGadget struct {
|
||||||
relMouseHidFile *os.File
|
relMouseHidFile *os.File
|
||||||
relMouseLock sync.Mutex
|
relMouseLock sync.Mutex
|
||||||
|
|
||||||
|
keyboardState KeyboardState
|
||||||
|
keyboardStateLock sync.Mutex
|
||||||
|
keyboardStateCtx context.Context
|
||||||
|
keyboardStateCancel context.CancelFunc
|
||||||
|
|
||||||
enabledDevices Devices
|
enabledDevices Devices
|
||||||
|
|
||||||
strictMode bool // only intended for testing for now
|
strictMode bool // only intended for testing for now
|
||||||
|
@ -70,7 +76,11 @@ type UsbGadget struct {
|
||||||
tx *UsbGadgetTransaction
|
tx *UsbGadgetTransaction
|
||||||
txLock sync.Mutex
|
txLock sync.Mutex
|
||||||
|
|
||||||
|
onKeyboardStateChange *func(state KeyboardState)
|
||||||
|
|
||||||
log *zerolog.Logger
|
log *zerolog.Logger
|
||||||
|
|
||||||
|
logSuppressionCounter map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
const configFSPath = "/sys/kernel/config"
|
const configFSPath = "/sys/kernel/config"
|
||||||
|
@ -96,23 +106,30 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
||||||
config = &Config{isEmpty: true}
|
config = &Config{isEmpty: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyboardCtx, keyboardCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
g := &UsbGadget{
|
g := &UsbGadget{
|
||||||
name: name,
|
name: name,
|
||||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||||
configMap: configMap,
|
configMap: configMap,
|
||||||
customConfig: *config,
|
customConfig: *config,
|
||||||
configLock: sync.Mutex{},
|
configLock: sync.Mutex{},
|
||||||
keyboardLock: sync.Mutex{},
|
keyboardLock: sync.Mutex{},
|
||||||
absMouseLock: sync.Mutex{},
|
absMouseLock: sync.Mutex{},
|
||||||
relMouseLock: sync.Mutex{},
|
relMouseLock: sync.Mutex{},
|
||||||
txLock: sync.Mutex{},
|
txLock: sync.Mutex{},
|
||||||
enabledDevices: *enabledDevices,
|
keyboardStateCtx: keyboardCtx,
|
||||||
lastUserInput: time.Now(),
|
keyboardStateCancel: keyboardCancel,
|
||||||
log: logger,
|
keyboardState: KeyboardState{},
|
||||||
|
enabledDevices: *enabledDevices,
|
||||||
|
lastUserInput: time.Now(),
|
||||||
|
log: logger,
|
||||||
|
|
||||||
strictMode: config.strictMode,
|
strictMode: config.strictMode,
|
||||||
|
|
||||||
|
logSuppressionCounter: make(map[string]int),
|
||||||
|
|
||||||
absMouseAccumulatedWheelY: 0,
|
absMouseAccumulatedWheelY: 0,
|
||||||
}
|
}
|
||||||
if err := g.Init(); err != nil {
|
if err := g.Init(); err != nil {
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func joinPath(basePath string, paths []string) string {
|
func joinPath(basePath string, paths []string) string {
|
||||||
|
@ -78,3 +80,27 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) logWithSupression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...interface{}) {
|
||||||
|
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||||
|
u.logSuppressionCounter[counterName] = 0
|
||||||
|
} else {
|
||||||
|
u.logSuppressionCounter[counterName]++
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logger.With().Int("counter", u.logSuppressionCounter[counterName]).Logger()
|
||||||
|
|
||||||
|
if u.logSuppressionCounter[counterName]%every == 0 {
|
||||||
|
if err != nil {
|
||||||
|
l.Error().Err(err).Msgf(msg, args...)
|
||||||
|
} else {
|
||||||
|
l.Error().Msgf(msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) resetLogSuppressionCounter(counterName string) {
|
||||||
|
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||||
|
u.logSuppressionCounter[counterName] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package websecure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fixtureEd25519Certificate = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG
|
||||||
|
A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1
|
||||||
|
MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV
|
||||||
|
BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev
|
||||||
|
bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy
|
||||||
|
r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U
|
||||||
|
C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
|
||||||
|
fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB
|
||||||
|
-----END PRIVATE KEY-----`
|
||||||
|
|
||||||
|
certStore *CertStore
|
||||||
|
certSigner *SelfSigner
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
tlsStorePath, err := os.MkdirTemp("", "jktls.*")
|
||||||
|
if err != nil {
|
||||||
|
defaultLogger.Fatal().Err(err).Msg("failed to create temp directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
certStore = NewCertStore(tlsStorePath, nil)
|
||||||
|
certStore.LoadCertificates()
|
||||||
|
|
||||||
|
certSigner = NewSelfSigner(
|
||||||
|
certStore,
|
||||||
|
nil,
|
||||||
|
"ci.jetkvm.com",
|
||||||
|
"JetKVM",
|
||||||
|
"JetKVM",
|
||||||
|
"JetKVM",
|
||||||
|
)
|
||||||
|
|
||||||
|
m.Run()
|
||||||
|
|
||||||
|
os.RemoveAll(tlsStorePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveEd25519Certificate(t *testing.T) {
|
||||||
|
err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to save certificate: %v", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package websecure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
@ -37,11 +38,15 @@ func keyToFile(cert *tls.Certificate, filename string) error {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return fmt.Errorf("failed to marshal EC private key: %v", e)
|
return fmt.Errorf("failed to marshal EC private key: %v", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyBlock = pem.Block{
|
keyBlock = pem.Block{
|
||||||
Type: "EC PRIVATE KEY",
|
Type: "EC PRIVATE KEY",
|
||||||
Bytes: b,
|
Bytes: b,
|
||||||
}
|
}
|
||||||
|
case ed25519.PrivateKey:
|
||||||
|
keyBlock = pem.Block{
|
||||||
|
Type: "ED25519 PRIVATE KEY",
|
||||||
|
Bytes: k,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown private key type: %T", k)
|
return fmt.Errorf("unknown private key type: %T", k)
|
||||||
}
|
}
|
||||||
|
|
22
jsonrpc.go
22
jsonrpc.go
|
@ -1006,6 +1006,25 @@ func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetLocalLoopbackOnly() (bool, error) {
|
||||||
|
return config.LocalLoopbackOnly, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetLocalLoopbackOnly(enabled bool) error {
|
||||||
|
// Check if the setting is actually changing
|
||||||
|
if config.LocalLoopbackOnly == enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the setting
|
||||||
|
config.LocalLoopbackOnly = enabled
|
||||||
|
if err := SaveConfig(); err != nil {
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var rpcHandlers = map[string]RPCHandler{
|
var rpcHandlers = map[string]RPCHandler{
|
||||||
"ping": {Func: rpcPing},
|
"ping": {Func: rpcPing},
|
||||||
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
||||||
|
@ -1017,6 +1036,7 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
||||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
||||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||||
|
"getKeyboardLedState": {Func: rpcGetKeyboardLedState},
|
||||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||||
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
||||||
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
"wheelReport": {Func: rpcWheelReport, Params: []string{"wheelY"}},
|
||||||
|
@ -1082,4 +1102,6 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
|
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
|
||||||
"getKeyboardMacros": {Func: getKeyboardMacros},
|
"getKeyboardMacros": {Func: getKeyboardMacros},
|
||||||
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
||||||
|
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
||||||
|
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
||||||
}
|
}
|
||||||
|
|
99
native.go
99
native.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -41,6 +42,11 @@ var ongoingRequests = make(map[int32]chan *CtrlResponse)
|
||||||
|
|
||||||
var lock = &sync.Mutex{}
|
var lock = &sync.Mutex{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
nativeCmd *exec.Cmd
|
||||||
|
nativeCmdLock = &sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
|
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
|
@ -129,16 +135,26 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
|
||||||
scopedLogger.Info().Msg("server listening")
|
scopedLogger.Info().Msg("server listening")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := listener.Accept()
|
for {
|
||||||
listener.Close()
|
conn, err := listener.Accept()
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isCtrl {
|
||||||
|
// check if the channel is closed
|
||||||
|
select {
|
||||||
|
case <-ctrlClientConnected:
|
||||||
|
scopedLogger.Debug().Msg("ctrl client reconnected")
|
||||||
|
default:
|
||||||
|
close(ctrlClientConnected)
|
||||||
|
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go handleClient(conn)
|
||||||
}
|
}
|
||||||
if isCtrl {
|
|
||||||
close(ctrlClientConnected)
|
|
||||||
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
|
||||||
}
|
|
||||||
handleClient(conn)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return listener
|
return listener
|
||||||
|
@ -235,6 +251,51 @@ func handleVideoClient(conn net.Conn) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) {
|
||||||
|
nativeCmdLock.Lock()
|
||||||
|
defer nativeCmdLock.Unlock()
|
||||||
|
|
||||||
|
cmd, err := startNativeBinary(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nativeCmd = cmd
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartNativeBinary(binaryPath string) error {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
// restart the binary
|
||||||
|
nativeLogger.Info().Msg("restarting jetkvm_native binary")
|
||||||
|
cmd, err := startNativeBinary(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
nativeLogger.Warn().Err(err).Msg("failed to restart binary")
|
||||||
|
}
|
||||||
|
nativeCmd = cmd
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func superviseNativeBinary(binaryPath string) error {
|
||||||
|
nativeCmdLock.Lock()
|
||||||
|
defer nativeCmdLock.Unlock()
|
||||||
|
|
||||||
|
if nativeCmd == nil || nativeCmd.Process == nil {
|
||||||
|
return restartNativeBinary(binaryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := nativeCmd.Wait()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
nativeLogger.Info().Err(err).Msg("jetkvm_native binary exited with no error")
|
||||||
|
} else if exiterr, ok := err.(*exec.ExitError); ok {
|
||||||
|
nativeLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("jetkvm_native binary exited with error")
|
||||||
|
} else {
|
||||||
|
nativeLogger.Warn().Err(err).Msg("jetkvm_native binary exited with unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return restartNativeBinary(binaryPath)
|
||||||
|
}
|
||||||
|
|
||||||
func ExtractAndRunNativeBin() error {
|
func ExtractAndRunNativeBin() error {
|
||||||
binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
|
binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
|
||||||
if err := ensureBinaryUpdated(binaryPath); err != nil {
|
if err := ensureBinaryUpdated(binaryPath); err != nil {
|
||||||
|
@ -246,12 +307,28 @@ func ExtractAndRunNativeBin() error {
|
||||||
return fmt.Errorf("failed to make binary executable: %w", err)
|
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||||
}
|
}
|
||||||
// Run the binary in the background
|
// Run the binary in the background
|
||||||
cmd, err := startNativeBinary(binaryPath)
|
cmd, err := startNativeBinaryWithLock(binaryPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to start binary: %w", err)
|
return fmt.Errorf("failed to start binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: add auto restart
|
// check if the binary is still running every 10 seconds
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-appCtx.Done():
|
||||||
|
nativeLogger.Info().Msg("stopping native binary supervisor")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
err := superviseNativeBinary(binaryPath)
|
||||||
|
if err != nil {
|
||||||
|
nativeLogger.Warn().Err(err).Msg("failed to supervise native binary")
|
||||||
|
time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-appCtx.Done()
|
<-appCtx.Done()
|
||||||
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")
|
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")
|
||||||
|
|
|
@ -21,6 +21,7 @@ func networkStateChanged() {
|
||||||
|
|
||||||
// always restart mDNS when the network state changes
|
// always restart mDNS when the network state changes
|
||||||
if mDNS != nil {
|
if mDNS != nil {
|
||||||
|
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
|
||||||
_ = mDNS.SetLocalNames([]string{
|
_ = mDNS.SetLocalNames([]string{
|
||||||
networkState.GetHostname(),
|
networkState.GetHostname(),
|
||||||
networkState.GetFQDN(),
|
networkState.GetFQDN(),
|
||||||
|
@ -54,14 +55,6 @@ func initNetwork() error {
|
||||||
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
||||||
config.NetworkConfig = networkConfig
|
config.NetworkConfig = networkConfig
|
||||||
networkStateChanged()
|
networkStateChanged()
|
||||||
|
|
||||||
if mDNS != nil {
|
|
||||||
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
|
|
||||||
_ = mDNS.SetLocalNames([]string{
|
|
||||||
networkState.GetHostname(),
|
|
||||||
networkState.GetFQDN(),
|
|
||||||
}, true)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
24
terminal.go
24
terminal.go
|
@ -1,6 +1,7 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -55,18 +56,23 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if msg.IsString {
|
if msg.IsString {
|
||||||
var size TerminalSize
|
maybeJson := bytes.TrimSpace(msg.Data)
|
||||||
err := json.Unmarshal([]byte(msg.Data), &size)
|
// Cheap check to see if this resembles JSON
|
||||||
if err == nil {
|
if len(maybeJson) > 1 && maybeJson[0] == '{' && maybeJson[len(maybeJson)-1] == '}' {
|
||||||
err = pty.Setsize(ptmx, &pty.Winsize{
|
var size TerminalSize
|
||||||
Rows: uint16(size.Rows),
|
err := json.Unmarshal(maybeJson, &size)
|
||||||
Cols: uint16(size.Cols),
|
|
||||||
})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
err = pty.Setsize(ptmx, &pty.Winsize{
|
||||||
|
Rows: uint16(size.Rows),
|
||||||
|
Cols: uint16(size.Cols),
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
scopedLogger.Info().Int("rows", size.Rows).Int("cols", size.Cols).Msg("Set terminal size")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
||||||
}
|
}
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
|
||||||
}
|
}
|
||||||
_, err := ptmx.Write(msg.Data)
|
_, err := ptmx.Write(msg.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -262,7 +262,23 @@ export default function Actionbar({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* {useSettingsStore().actionBarCtrlAltDel && (
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<Button
|
||||||
|
size="XS"
|
||||||
|
theme="light"
|
||||||
|
text="Ctrl + Alt + Del"
|
||||||
|
LeadingIcon={FaLock}
|
||||||
|
onClick={() => {
|
||||||
|
sendKeyboardEvent(
|
||||||
|
[keys["Delete"]],
|
||||||
|
[modifiers["ControlLeft"], modifiers["AltLeft"]],
|
||||||
|
);
|
||||||
|
setTimeout(resetKeyboardState, 100);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import {
|
import {
|
||||||
ExclamationTriangleIcon,
|
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
InformationCircleIcon,
|
InformationCircleIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
|
||||||
type Variant = "danger" | "success" | "warning" | "info";
|
type Variant = "danger" | "success" | "warning" | "info";
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ interface ConfirmDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: React.ReactNode;
|
||||||
variant?: Variant;
|
variant?: Variant;
|
||||||
confirmText?: string;
|
confirmText?: string;
|
||||||
cancelText?: string | null;
|
cancelText?: string | null;
|
||||||
|
@ -84,8 +84,8 @@ export function ConfirmDialog({
|
||||||
>
|
>
|
||||||
<Icon aria-hidden="true" className={cx("size-6", iconClass)} />
|
<Icon aria-hidden="true" className={cx("size-6", iconClass)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h2 className="text-lg font-bold leading-tight text-black dark:text-white">
|
<h2 className="text-lg leading-tight font-bold text-black dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400">
|
<div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400">
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default function InfoBar() {
|
||||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||||
|
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rpcDataChannel) return;
|
if (!rpcDataChannel) return;
|
||||||
|
@ -36,9 +37,9 @@ export default function InfoBar() {
|
||||||
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
console.log(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
|
||||||
}, [rpcDataChannel]);
|
}, [rpcDataChannel]);
|
||||||
|
|
||||||
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
|
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
||||||
const isNumLockActive = useHidStore(state => state.isNumLockActive);
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
const isScrollLockActive = useHidStore(state => state.isScrollLockActive);
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
|
||||||
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
const isTurnServerInUse = useRTCStore(state => state.isTurnServerInUse);
|
||||||
|
|
||||||
|
@ -97,19 +98,21 @@ export default function InfoBar() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-x-1">
|
{showPressedKeys && (
|
||||||
<span className="text-xs font-semibold">Keys:</span>
|
<div className="flex items-center gap-x-1">
|
||||||
<h2 className="text-xs">
|
<span className="text-xs font-semibold">Keys:</span>
|
||||||
{[
|
<h2 className="text-xs">
|
||||||
...activeKeys.map(
|
{[
|
||||||
x => Object.entries(keys).filter(y => y[1] === x)[0][0],
|
...activeKeys.map(
|
||||||
),
|
x => Object.entries(keys).filter(y => y[1] === x)[0][0],
|
||||||
activeModifiers.map(
|
),
|
||||||
x => Object.entries(modifiers).filter(y => y[1] === x)[0][0],
|
activeModifiers.map(
|
||||||
),
|
x => Object.entries(modifiers).filter(y => y[1] === x)[0][0],
|
||||||
].join(", ")}
|
),
|
||||||
</h2>
|
].join(", ")}
|
||||||
</div>
|
</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
|
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
|
||||||
|
@ -118,10 +121,24 @@ export default function InfoBar() {
|
||||||
Relayed by Cloudflare
|
Relayed by Cloudflare
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{keyboardLedStateSyncAvailable ? (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
|
keyboardLedSync !== "browser"
|
||||||
|
? "text-black dark:text-white"
|
||||||
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
|
)}
|
||||||
|
title={"Your keyboard LED state is managed by" + (keyboardLedSync === "browser" ? " the browser" : " the host")}
|
||||||
|
>
|
||||||
|
{keyboardLedSync === "browser" ? "Browser" : "Host"}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isCapsLockActive
|
keyboardLedState?.caps_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
|
@ -131,7 +148,7 @@ export default function InfoBar() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isNumLockActive
|
keyboardLedState?.num_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
|
@ -141,13 +158,23 @@ export default function InfoBar() {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"shrink-0 p-1 px-1.5 text-xs",
|
"shrink-0 p-1 px-1.5 text-xs",
|
||||||
isScrollLockActive
|
keyboardLedState?.scroll_lock
|
||||||
? "text-black dark:text-white"
|
? "text-black dark:text-white"
|
||||||
: "text-slate-800/20 dark:text-slate-300/20",
|
: "text-slate-800/20 dark:text-slate-300/20",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Scroll Lock
|
Scroll Lock
|
||||||
</div>
|
</div>
|
||||||
|
{keyboardLedState?.compose ? (
|
||||||
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
|
Compose
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{keyboardLedState?.kana ? (
|
||||||
|
<div className="shrink-0 p-1 px-1.5 text-xs">
|
||||||
|
Kana
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -61,9 +61,9 @@ function Terminal({
|
||||||
dataChannel,
|
dataChannel,
|
||||||
type,
|
type,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
readonly title: string;
|
||||||
dataChannel: RTCDataChannel;
|
readonly dataChannel: RTCDataChannel;
|
||||||
type: AvailableTerminalTypes;
|
readonly type: AvailableTerminalTypes;
|
||||||
}) {
|
}) {
|
||||||
const enableTerminal = useUiStore(state => state.terminalType == type);
|
const enableTerminal = useUiStore(state => state.terminalType == type);
|
||||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||||
|
|
|
@ -10,7 +10,7 @@ import LoadingSpinner from "@components/LoadingSpinner";
|
||||||
import Card, { GridCard } from "@components/Card";
|
import Card, { GridCard } from "@components/Card";
|
||||||
|
|
||||||
interface OverlayContentProps {
|
interface OverlayContentProps {
|
||||||
children: React.ReactNode;
|
readonly children: React.ReactNode;
|
||||||
}
|
}
|
||||||
function OverlayContent({ children }: OverlayContentProps) {
|
function OverlayContent({ children }: OverlayContentProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -23,7 +23,7 @@ function OverlayContent({ children }: OverlayContentProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingOverlayProps {
|
interface LoadingOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
||||||
|
@ -57,8 +57,8 @@ export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadingConnectionOverlayProps {
|
interface LoadingConnectionOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
text: string;
|
readonly text: string;
|
||||||
}
|
}
|
||||||
export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
|
export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -91,8 +91,8 @@ export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverla
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectionErrorOverlayProps {
|
interface ConnectionErrorOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
setupPeerConnection: () => Promise<void>;
|
readonly setupPeerConnection: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConnectionFailedOverlay({
|
export function ConnectionFailedOverlay({
|
||||||
|
@ -153,7 +153,7 @@ export function ConnectionFailedOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PeerConnectionDisconnectedOverlay {
|
interface PeerConnectionDisconnectedOverlay {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PeerConnectionDisconnectedOverlay({
|
export function PeerConnectionDisconnectedOverlay({
|
||||||
|
@ -207,8 +207,8 @@ export function PeerConnectionDisconnectedOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HDMIErrorOverlayProps {
|
interface HDMIErrorOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
hdmiState: string;
|
readonly hdmiState: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
|
@ -310,8 +310,8 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NoAutoplayPermissionsOverlayProps {
|
interface NoAutoplayPermissionsOverlayProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
onPlayClick: () => void;
|
readonly onPlayClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoAutoplayPermissionsOverlay({
|
export function NoAutoplayPermissionsOverlay({
|
||||||
|
@ -361,7 +361,7 @@ export function NoAutoplayPermissionsOverlay({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PointerLockBarProps {
|
interface PointerLockBarProps {
|
||||||
show: boolean;
|
readonly show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PointerLockBar({ show }: PointerLockBarProps) {
|
export function PointerLockBar({ show }: PointerLockBarProps) {
|
||||||
|
@ -369,10 +369,10 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
{show ? (
|
{show ? (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute -top-[36px] left-0 right-0 z-20 bg-white"
|
className="flex w-full items-center justify-between bg-transparent"
|
||||||
initial={{ y: 20, opacity: 0, zIndex: 0 }}
|
initial={{ opacity: 0, zIndex: 0 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, zIndex: 20 }}
|
||||||
exit={{ y: 43, zIndex: 0 }}
|
exit={{ opacity: 0, zIndex: 0 }}
|
||||||
transition={{ duration: 0.5, ease: "easeInOut", delay: 0.5 }}
|
transition={{ duration: 0.5, ease: "easeInOut", delay: 0.5 }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import Keyboard from "react-simple-keyboard";
|
|
||||||
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import Keyboard from "react-simple-keyboard";
|
||||||
|
|
||||||
import Card from "@components/Card";
|
import Card from "@components/Card";
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
|
@ -9,12 +10,12 @@ import { Button } from "@components/Button";
|
||||||
|
|
||||||
import "react-simple-keyboard/build/css/index.css";
|
import "react-simple-keyboard/build/css/index.css";
|
||||||
|
|
||||||
import { useHidStore, useUiStore } from "@/hooks/stores";
|
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import { keys, modifiers, keyDisplayMap } from "@/keyboardMappings";
|
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
|
||||||
import DetachIconRaw from "@/assets/detach-icon.svg";
|
|
||||||
import AttachIconRaw from "@/assets/attach-icon.svg";
|
import AttachIconRaw from "@/assets/attach-icon.svg";
|
||||||
|
import DetachIconRaw from "@/assets/detach-icon.svg";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
|
||||||
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
|
import { keyDisplayMap, keys, modifiers } from "@/keyboardMappings";
|
||||||
|
|
||||||
export const DetachIcon = ({ className }: { className?: string }) => {
|
export const DetachIcon = ({ className }: { className?: string }) => {
|
||||||
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
|
||||||
|
@ -40,7 +41,17 @@ function KeyboardWrapper() {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||||
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
const [newPosition, setNewPosition] = useState({ x: 0, y: 0 });
|
||||||
const isCapsLockActive = useHidStore(state => state.isCapsLockActive);
|
|
||||||
|
const isCapsLockActive = useHidStore(useShallow(state => state.keyboardLedState?.caps_lock));
|
||||||
|
|
||||||
|
// HID related states
|
||||||
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
const isKeyboardLedManagedByHost = useMemo(() =>
|
||||||
|
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
||||||
|
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
||||||
|
);
|
||||||
|
|
||||||
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
||||||
|
|
||||||
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
const startDrag = useCallback((e: MouseEvent | TouchEvent) => {
|
||||||
|
@ -157,14 +168,16 @@ function KeyboardWrapper() {
|
||||||
toggleLayout();
|
toggleLayout();
|
||||||
|
|
||||||
if (isCapsLockActive) {
|
if (isCapsLockActive) {
|
||||||
setIsCapsLockActive(false);
|
if (!isKeyboardLedManagedByHost) {
|
||||||
|
setIsCapsLockActive(false);
|
||||||
|
}
|
||||||
sendKeyboardEvent([keys["CapsLock"]], []);
|
sendKeyboardEvent([keys["CapsLock"]], []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle caps lock state change
|
// Handle caps lock state change
|
||||||
if (isKeyCaps) {
|
if (isKeyCaps && !isKeyboardLedManagedByHost) {
|
||||||
setIsCapsLockActive(!isCapsLockActive);
|
setIsCapsLockActive(!isCapsLockActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +196,7 @@ function KeyboardWrapper() {
|
||||||
|
|
||||||
setTimeout(resetKeyboardState, 100);
|
setTimeout(resetKeyboardState, 100);
|
||||||
},
|
},
|
||||||
[isCapsLockActive, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
[isCapsLockActive, isKeyboardLedManagedByHost, sendKeyboardEvent, resetKeyboardState, setIsCapsLockActive],
|
||||||
);
|
);
|
||||||
|
|
||||||
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
const virtualKeyboard = useHidStore(state => state.isVirtualKeyboardEnabled);
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useResizeObserver } from "usehooks-ts";
|
import { useResizeObserver } from "usehooks-ts";
|
||||||
|
|
||||||
|
import VirtualKeyboard from "@components/VirtualKeyboard";
|
||||||
|
import Actionbar from "@components/ActionBar";
|
||||||
|
import MacroBar from "@/components/MacroBar";
|
||||||
|
import InfoBar from "@components/InfoBar";
|
||||||
|
import notifications from "@/notifications";
|
||||||
|
import useKeyboard from "@/hooks/useKeyboard";
|
||||||
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
|
import { cx } from "@/cva.config";
|
||||||
|
import { keys, modifiers } from "@/keyboardMappings";
|
||||||
import {
|
import {
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMouseStore,
|
useMouseStore,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useUiStore,
|
|
||||||
useVideoStore,
|
useVideoStore,
|
||||||
} from "@/hooks/stores";
|
} from "@/hooks/stores";
|
||||||
import { keys, modifiers } from "@/keyboardMappings";
|
|
||||||
import { cx } from "@/cva.config";
|
|
||||||
import VirtualKeyboard from "@components/VirtualKeyboard";
|
|
||||||
import Actionbar from "@components/ActionBar";
|
|
||||||
import MacroBar from "@/components/MacroBar";
|
|
||||||
import InfoBar from "@components/InfoBar";
|
|
||||||
import useKeyboard from "@/hooks/useKeyboard";
|
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|
||||||
import notifications from "@/notifications";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HDMIErrorOverlay,
|
HDMIErrorOverlay,
|
||||||
|
@ -47,6 +46,23 @@ export default function WebRTCVideo() {
|
||||||
clientHeight: videoClientHeight,
|
clientHeight: videoClientHeight,
|
||||||
} = useVideoStore();
|
} = useVideoStore();
|
||||||
|
|
||||||
|
// Video enhancement settings
|
||||||
|
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
||||||
|
const videoBrightness = useSettingsStore(state => state.videoBrightness);
|
||||||
|
const videoContrast = useSettingsStore(state => state.videoContrast);
|
||||||
|
|
||||||
|
// HID related states
|
||||||
|
const keyboardLedStateSyncAvailable = useHidStore(state => state.keyboardLedStateSyncAvailable);
|
||||||
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
const isKeyboardLedManagedByHost = useMemo(() =>
|
||||||
|
keyboardLedSync !== "browser" && keyboardLedStateSyncAvailable,
|
||||||
|
[keyboardLedSync, keyboardLedStateSyncAvailable],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setIsNumLockActive = useHidStore(state => state.setIsNumLockActive);
|
||||||
|
const setIsCapsLockActive = useHidStore(state => state.setIsCapsLockActive);
|
||||||
|
const setIsScrollLockActive = useHidStore(state => state.setIsScrollLockActive);
|
||||||
|
|
||||||
// RTC related states
|
// RTC related states
|
||||||
const peerConnection = useRTCStore(state => state.peerConnection);
|
const peerConnection = useRTCStore(state => state.peerConnection);
|
||||||
|
|
||||||
|
@ -55,12 +71,9 @@ export default function WebRTCVideo() {
|
||||||
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
const hdmiError = ["no_lock", "no_signal", "out_of_range"].includes(hdmiState);
|
||||||
const isVideoLoading = !isPlaying;
|
const isVideoLoading = !isPlaying;
|
||||||
|
|
||||||
// Keyboard related states
|
const [blockWheelEvent, setBlockWheelEvent] = useState(false);
|
||||||
const { setIsNumLockActive, setIsCapsLockActive, setIsScrollLockActive } =
|
|
||||||
useHidStore();
|
|
||||||
|
|
||||||
// Misc states and hooks
|
// Misc states and hooks
|
||||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
// Video-related
|
// Video-related
|
||||||
|
@ -98,32 +111,77 @@ export default function WebRTCVideo() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pointer lock and keyboard lock related
|
// Pointer lock and keyboard lock related
|
||||||
const isPointerLockPossible = window.location.protocol === "https:";
|
const isPointerLockPossible = window.location.protocol === "https:" || window.location.hostname === "localhost";
|
||||||
|
const isFullscreenEnabled = document.fullscreenEnabled;
|
||||||
|
|
||||||
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
||||||
const name = permissionName as PermissionName;
|
if (!navigator.permissions || !navigator.permissions.query) {
|
||||||
const { state } = await navigator.permissions.query({ name });
|
return false; // if can't query permissions, assume NOT granted
|
||||||
return state === "granted";
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const name = permissionName as PermissionName;
|
||||||
|
const { state } = await navigator.permissions.query({ name });
|
||||||
|
return state === "granted";
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
|
return false; // if query fails, assume NOT granted
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const requestPointerLock = useCallback(async () => {
|
const requestPointerLock = useCallback(async () => {
|
||||||
if (document.pointerLockElement) return;
|
if (!isPointerLockPossible
|
||||||
|
|| videoElm.current === null
|
||||||
|
|| document.pointerLockElement) return;
|
||||||
|
|
||||||
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
|
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
|
||||||
|
|
||||||
if (isPointerLockGranted && settings.mouseMode === "relative") {
|
if (isPointerLockGranted && settings.mouseMode === "relative") {
|
||||||
videoElm.current?.requestPointerLock();
|
try {
|
||||||
|
await videoElm.current.requestPointerLock();
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [checkNavigatorPermissions, settings.mouseMode]);
|
}, [checkNavigatorPermissions, isPointerLockPossible, settings.mouseMode]);
|
||||||
|
|
||||||
|
const requestKeyboardLock = useCallback(async () => {
|
||||||
|
if (videoElm.current === null) return;
|
||||||
|
|
||||||
|
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||||
|
|
||||||
|
if (isKeyboardLockGranted && "keyboard" in navigator) {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||||
|
await navigator.keyboard.lock();
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [checkNavigatorPermissions]);
|
||||||
|
|
||||||
|
const releaseKeyboardLock = useCallback(async () => {
|
||||||
|
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
|
||||||
|
|
||||||
|
if ("keyboard" in navigator) {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - keyboard unlock is not supported in all browsers
|
||||||
|
await navigator.keyboard.unlock();
|
||||||
|
} catch {
|
||||||
|
// ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPointerLockPossible || !videoElm.current) return;
|
if (!isPointerLockPossible || !videoElm.current) return;
|
||||||
|
|
||||||
const handlePointerLockChange = () => {
|
const handlePointerLockChange = () => {
|
||||||
if (document.pointerLockElement) {
|
if (document.pointerLockElement) {
|
||||||
notifications.success("Pointer lock Enabled, hold escape to exit");
|
notifications.success("Pointer lock Enabled, press escape to unlock");
|
||||||
setIsPointerLockActive(true);
|
setIsPointerLockActive(true);
|
||||||
} else {
|
} else {
|
||||||
notifications.success("Pointer lock disabled");
|
notifications.success("Pointer lock Disabled");
|
||||||
setIsPointerLockActive(false);
|
setIsPointerLockActive(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -136,27 +194,39 @@ export default function WebRTCVideo() {
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
}, [isPointerLockPossible, videoElm]);
|
}, [isPointerLockPossible]);
|
||||||
|
|
||||||
const requestFullscreen = useCallback(async () => {
|
const requestFullscreen = useCallback(async () => {
|
||||||
videoElm.current?.requestFullscreen({
|
if (!isFullscreenEnabled || !videoElm.current) return;
|
||||||
navigationUI: "show",
|
|
||||||
});
|
|
||||||
|
|
||||||
// we do not care about pointer lock if it's for fullscreen
|
// per https://wicg.github.io/keyboard-lock/#system-key-press-handler
|
||||||
|
// If keyboard lock is activated after fullscreen is already in effect, then the user my
|
||||||
|
// see multiple messages about how to exit fullscreen. For this reason, we recommend that
|
||||||
|
// developers call lock() before they enter fullscreen:
|
||||||
|
await requestKeyboardLock();
|
||||||
await requestPointerLock();
|
await requestPointerLock();
|
||||||
|
|
||||||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
await videoElm.current.requestFullscreen({
|
||||||
if (isKeyboardLockGranted) {
|
navigationUI: "show",
|
||||||
if ("keyboard" in navigator) {
|
});
|
||||||
// @ts-expect-error - keyboard lock is not supported in all browsers
|
}, [isFullscreenEnabled, requestKeyboardLock, requestPointerLock]);
|
||||||
await navigator.keyboard.lock();
|
|
||||||
|
// setup to release the keyboard lock anytime the fullscreen ends
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoElm.current) return;
|
||||||
|
|
||||||
|
const handleFullscreenChange = () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
releaseKeyboardLock();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}, [requestPointerLock, checkNavigatorPermissions]);
|
|
||||||
|
document.addEventListener("fullscreenchange ", handleFullscreenChange);
|
||||||
|
}, [releaseKeyboardLock]);
|
||||||
|
|
||||||
// Mouse-related
|
// Mouse-related
|
||||||
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos);
|
||||||
|
|
||||||
const sendRelMouseMovement = useCallback(
|
const sendRelMouseMovement = useCallback(
|
||||||
(x: number, y: number, buttons: number) => {
|
(x: number, y: number, buttons: number) => {
|
||||||
if (settings.mouseMode !== "relative") return;
|
if (settings.mouseMode !== "relative") return;
|
||||||
|
@ -171,18 +241,13 @@ export default function WebRTCVideo() {
|
||||||
const relMouseMoveHandler = useCallback(
|
const relMouseMoveHandler = useCallback(
|
||||||
(e: MouseEvent) => {
|
(e: MouseEvent) => {
|
||||||
if (settings.mouseMode !== "relative") return;
|
if (settings.mouseMode !== "relative") return;
|
||||||
if (isPointerLockActive === false && isPointerLockPossible === true) return;
|
if (isPointerLockActive === false && isPointerLockPossible) return;
|
||||||
|
|
||||||
// Send mouse movement
|
// Send mouse movement
|
||||||
const { buttons } = e;
|
const { buttons } = e;
|
||||||
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
sendRelMouseMovement(e.movementX, e.movementY, buttons);
|
||||||
},
|
},
|
||||||
[
|
[isPointerLockActive, isPointerLockPossible, sendRelMouseMovement, settings.mouseMode],
|
||||||
isPointerLockActive,
|
|
||||||
isPointerLockPossible,
|
|
||||||
sendRelMouseMovement,
|
|
||||||
settings.mouseMode,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendAbsMouseMovement = useCallback(
|
const sendAbsMouseMovement = useCallback(
|
||||||
|
@ -236,18 +301,16 @@ export default function WebRTCVideo() {
|
||||||
const { buttons } = e;
|
const { buttons } = e;
|
||||||
sendAbsMouseMovement(x, y, buttons);
|
sendAbsMouseMovement(x, y, buttons);
|
||||||
},
|
},
|
||||||
[
|
[settings.mouseMode, videoClientWidth, videoClientHeight, videoWidth, videoHeight, sendAbsMouseMovement],
|
||||||
sendAbsMouseMovement,
|
|
||||||
videoClientHeight,
|
|
||||||
videoClientWidth,
|
|
||||||
videoWidth,
|
|
||||||
videoHeight,
|
|
||||||
settings.mouseMode,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const mouseWheelHandler = useCallback(
|
const mouseWheelHandler = useCallback(
|
||||||
(e: WheelEvent) => {
|
(e: WheelEvent) => {
|
||||||
|
|
||||||
|
if (settings.scrollThrottling && blockWheelEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine if the wheel event is an accel scroll value
|
// Determine if the wheel event is an accel scroll value
|
||||||
const isAccel = Math.abs(e.deltaY) >= 100;
|
const isAccel = Math.abs(e.deltaY) >= 100;
|
||||||
|
|
||||||
|
@ -255,7 +318,7 @@ export default function WebRTCVideo() {
|
||||||
const accelScrollValue = e.deltaY / 100;
|
const accelScrollValue = e.deltaY / 100;
|
||||||
|
|
||||||
// Calculate the no accel scroll value
|
// Calculate the no accel scroll value
|
||||||
const noAccelScrollValue = e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0;
|
const noAccelScrollValue = Math.sign(e.deltaY);
|
||||||
|
|
||||||
// Get scroll value
|
// Get scroll value
|
||||||
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
||||||
|
@ -267,8 +330,14 @@ export default function WebRTCVideo() {
|
||||||
const invertedScrollValue = -clampedScrollValue;
|
const invertedScrollValue = -clampedScrollValue;
|
||||||
|
|
||||||
send("wheelReport", { wheelY: invertedScrollValue });
|
send("wheelReport", { wheelY: invertedScrollValue });
|
||||||
|
|
||||||
|
// Apply blocking delay based of throttling settings
|
||||||
|
if (settings.scrollThrottling && !blockWheelEvent) {
|
||||||
|
setBlockWheelEvent(true);
|
||||||
|
setTimeout(() => setBlockWheelEvent(false), settings.scrollThrottling);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[send],
|
[send, blockWheelEvent, settings],
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetMousePosition = useCallback(() => {
|
const resetMousePosition = useCallback(() => {
|
||||||
|
@ -348,16 +417,11 @@ export default function WebRTCVideo() {
|
||||||
let code = e.code;
|
let code = e.code;
|
||||||
const key = e.key;
|
const key = e.key;
|
||||||
|
|
||||||
// if (document.activeElement?.id !== "videoFocusTrap") {
|
if (!isKeyboardLedManagedByHost) {
|
||||||
// console.log("KEYUP: Not focusing on the video", document.activeElement);
|
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||||
// return;
|
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||||
// }
|
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||||
|
}
|
||||||
// console.log(document.activeElement);
|
|
||||||
|
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
|
||||||
|
|
||||||
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
|
||||||
code = "Backquote";
|
code = "Backquote";
|
||||||
|
@ -388,11 +452,12 @@ export default function WebRTCVideo() {
|
||||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
handleModifierKeys,
|
||||||
|
sendKeyboardEvent,
|
||||||
|
isKeyboardLedManagedByHost,
|
||||||
setIsNumLockActive,
|
setIsNumLockActive,
|
||||||
setIsCapsLockActive,
|
setIsCapsLockActive,
|
||||||
setIsScrollLockActive,
|
setIsScrollLockActive,
|
||||||
handleModifierKeys,
|
|
||||||
sendKeyboardEvent,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -401,9 +466,11 @@ export default function WebRTCVideo() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const prev = useHidStore.getState();
|
const prev = useHidStore.getState();
|
||||||
|
|
||||||
setIsNumLockActive(e.getModifierState("NumLock"));
|
if (!isKeyboardLedManagedByHost) {
|
||||||
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
setIsNumLockActive(e.getModifierState("NumLock"));
|
||||||
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
setIsCapsLockActive(e.getModifierState("CapsLock"));
|
||||||
|
setIsScrollLockActive(e.getModifierState("ScrollLock"));
|
||||||
|
}
|
||||||
|
|
||||||
// Filtering out the key that was just released (keys[e.code])
|
// Filtering out the key that was just released (keys[e.code])
|
||||||
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
|
const newKeys = prev.activeKeys.filter(k => k !== keys[e.code]).filter(Boolean);
|
||||||
|
@ -417,22 +484,25 @@ export default function WebRTCVideo() {
|
||||||
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
sendKeyboardEvent([...new Set(newKeys)], [...new Set(newModifiers)]);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
handleModifierKeys,
|
||||||
|
sendKeyboardEvent,
|
||||||
|
isKeyboardLedManagedByHost,
|
||||||
setIsNumLockActive,
|
setIsNumLockActive,
|
||||||
setIsCapsLockActive,
|
setIsCapsLockActive,
|
||||||
setIsScrollLockActive,
|
setIsScrollLockActive,
|
||||||
handleModifierKeys,
|
|
||||||
sendKeyboardEvent,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
const videoKeyUpHandler = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (!videoElm.current) return;
|
||||||
|
|
||||||
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
|
// In fullscreen mode in chrome & safari, the space key is used to pause/play the video
|
||||||
// there is no way to prevent this, so we need to simply force play the video when it's paused.
|
// there is no way to prevent this, so we need to simply force play the video when it's paused.
|
||||||
// Fix only works in chrome based browsers.
|
// Fix only works in chrome based browsers.
|
||||||
if (e.code === "Space") {
|
if (e.code === "Space") {
|
||||||
if (videoElm.current?.paused == true) {
|
if (videoElm.current.paused) {
|
||||||
console.log("Force playing video");
|
console.log("Force playing video");
|
||||||
videoElm.current?.play();
|
videoElm.current.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -441,7 +511,6 @@ export default function WebRTCVideo() {
|
||||||
(mediaStream: MediaStream) => {
|
(mediaStream: MediaStream) => {
|
||||||
if (!videoElm.current) return;
|
if (!videoElm.current) return;
|
||||||
const videoElmRefValue = videoElm.current;
|
const videoElmRefValue = videoElm.current;
|
||||||
// console.log("Adding stream to video element", videoElmRefValue);
|
|
||||||
videoElmRefValue.srcObject = mediaStream;
|
videoElmRefValue.srcObject = mediaStream;
|
||||||
updateVideoSizeStore(videoElmRefValue);
|
updateVideoSizeStore(videoElmRefValue);
|
||||||
},
|
},
|
||||||
|
@ -457,7 +526,6 @@ export default function WebRTCVideo() {
|
||||||
peerConnection.addEventListener(
|
peerConnection.addEventListener(
|
||||||
"track",
|
"track",
|
||||||
(e: RTCTrackEvent) => {
|
(e: RTCTrackEvent) => {
|
||||||
// console.log("Adding stream to video element");
|
|
||||||
addStreamToVideoElm(e.streams[0]);
|
addStreamToVideoElm(e.streams[0]);
|
||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
|
@ -473,7 +541,6 @@ export default function WebRTCVideo() {
|
||||||
useEffect(
|
useEffect(
|
||||||
function updateVideoStream() {
|
function updateVideoStream() {
|
||||||
if (!mediaStream) return;
|
if (!mediaStream) return;
|
||||||
console.log("Updating video stream from mediaStream");
|
|
||||||
// We set the as early as possible
|
// We set the as early as possible
|
||||||
addStreamToVideoElm(mediaStream);
|
addStreamToVideoElm(mediaStream);
|
||||||
},
|
},
|
||||||
|
@ -495,9 +562,6 @@ export default function WebRTCVideo() {
|
||||||
document.addEventListener("keydown", keyDownHandler, { signal });
|
document.addEventListener("keydown", keyDownHandler, { signal });
|
||||||
document.addEventListener("keyup", keyUpHandler, { signal });
|
document.addEventListener("keyup", keyUpHandler, { signal });
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error
|
|
||||||
window.clearKeys = () => sendKeyboardEvent([], []);
|
|
||||||
window.addEventListener("blur", resetKeyboardState, { signal });
|
window.addEventListener("blur", resetKeyboardState, { signal });
|
||||||
document.addEventListener("visibilitychange", resetKeyboardState, { signal });
|
document.addEventListener("visibilitychange", resetKeyboardState, { signal });
|
||||||
|
|
||||||
|
@ -505,7 +569,7 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[keyDownHandler, keyUpHandler, resetKeyboardState, sendKeyboardEvent],
|
[keyDownHandler, keyUpHandler, resetKeyboardState],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Video Event Listeners
|
// Setup Video Event Listeners
|
||||||
|
@ -527,38 +591,42 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[onVideoPlaying, videoKeyUpHandler],
|
||||||
absMouseMoveHandler,
|
|
||||||
resetMousePosition,
|
|
||||||
onVideoPlaying,
|
|
||||||
mouseWheelHandler,
|
|
||||||
videoKeyUpHandler,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Absolute Mouse Events
|
// Setup Mouse Events
|
||||||
useEffect(
|
useEffect(
|
||||||
function setAbsoluteMouseModeEventListeners() {
|
function setMouseModeEventListeners() {
|
||||||
const videoElmRefValue = videoElm.current;
|
const videoElmRefValue = videoElm.current;
|
||||||
if (!videoElmRefValue) return;
|
if (!videoElmRefValue) return;
|
||||||
|
const isRelativeMouseMode = (settings.mouseMode === "relative");
|
||||||
if (settings.mouseMode !== "absolute") return;
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const signal = abortController.signal;
|
const signal = abortController.signal;
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("mousemove", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerdown", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerdown", isRelativeMouseMode ? relMouseMoveHandler : absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("pointerup", absMouseMoveHandler, { signal });
|
videoElmRefValue.addEventListener("pointerup", isRelativeMouseMode ? relMouseMoveHandler :absMouseMoveHandler, { signal });
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
||||||
signal,
|
signal,
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset the mouse position when the window is blurred or the document is hidden
|
if (isRelativeMouseMode) {
|
||||||
const local = resetMousePosition;
|
videoElmRefValue.addEventListener("click",
|
||||||
window.addEventListener("blur", local, { signal });
|
() => {
|
||||||
document.addEventListener("visibilitychange", local, { signal });
|
if (isPointerLockPossible && !isPointerLockActive && !document.pointerLockElement) {
|
||||||
|
requestPointerLock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Reset the mouse position when the window is blurred or the document is hidden
|
||||||
|
window.addEventListener("blur", resetMousePosition, { signal });
|
||||||
|
document.addEventListener("visibilitychange", resetMousePosition, { signal });
|
||||||
|
}
|
||||||
|
|
||||||
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
||||||
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
||||||
|
|
||||||
|
@ -566,65 +634,18 @@ export default function WebRTCVideo() {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[absMouseMoveHandler, mouseWheelHandler, resetMousePosition, settings.mouseMode],
|
[absMouseMoveHandler, isPointerLockActive, isPointerLockPossible, mouseWheelHandler, relMouseMoveHandler, requestPointerLock, resetMousePosition, settings.mouseMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Setup Relative Mouse Events
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(
|
|
||||||
function setupRelativeMouseEventListeners() {
|
|
||||||
if (settings.mouseMode !== "relative") return;
|
|
||||||
// Relative mouse mode should only be active if the pointer lock is active and Pointer Lock is possible
|
|
||||||
|
|
||||||
const videoElmRefValue = videoElm.current;
|
|
||||||
if (!videoElmRefValue) return;
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const signal = abortController.signal;
|
|
||||||
|
|
||||||
videoElmRefValue.addEventListener("mousemove", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener("pointerdown", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener("pointerup", relMouseMoveHandler, { signal });
|
|
||||||
videoElmRefValue.addEventListener(
|
|
||||||
"click",
|
|
||||||
() => {
|
|
||||||
if (isPointerLockPossible && !document.pointerLockElement) {
|
|
||||||
requestPointerLock();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ signal },
|
|
||||||
);
|
|
||||||
videoElmRefValue.addEventListener("wheel", mouseWheelHandler, {
|
|
||||||
signal,
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const preventContextMenu = (e: MouseEvent) => e.preventDefault();
|
|
||||||
videoElmRefValue.addEventListener("contextmenu", preventContextMenu, { signal });
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
abortController.abort();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[
|
|
||||||
settings.mouseMode,
|
|
||||||
relMouseMoveHandler,
|
|
||||||
mouseWheelHandler,
|
|
||||||
disableVideoFocusTrap,
|
|
||||||
requestPointerLock,
|
|
||||||
isPointerLockPossible,
|
|
||||||
isPointerLockActive,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasNoAutoPlayPermissions = useMemo(() => {
|
const hasNoAutoPlayPermissions = useMemo(() => {
|
||||||
if (peerConnection?.connectionState !== "connected") return false;
|
if (peerConnection?.connectionState !== "connected") return false;
|
||||||
if (isPlaying) return false;
|
if (isPlaying) return false;
|
||||||
if (hdmiError) return false;
|
if (hdmiError) return false;
|
||||||
if (videoHeight === 0 || videoWidth === 0) return false;
|
if (videoHeight === 0 || videoWidth === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
}, [peerConnection?.connectionState, isPlaying, hdmiError, videoHeight, videoWidth]);
|
}, [hdmiError, isPlaying, peerConnection?.connectionState, videoHeight, videoWidth]);
|
||||||
|
|
||||||
const showPointerLockBar = useMemo(() => {
|
const showPointerLockBar = useMemo(() => {
|
||||||
if (settings.mouseMode !== "relative") return false;
|
if (settings.mouseMode !== "relative") return false;
|
||||||
|
@ -634,15 +655,7 @@ export default function WebRTCVideo() {
|
||||||
if (!isPlaying) return false;
|
if (!isPlaying) return false;
|
||||||
if (videoHeight === 0 || videoWidth === 0) return false;
|
if (videoHeight === 0 || videoWidth === 0) return false;
|
||||||
return true;
|
return true;
|
||||||
}, [
|
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
|
||||||
settings.mouseMode,
|
|
||||||
isPointerLockPossible,
|
|
||||||
isPointerLockActive,
|
|
||||||
isVideoLoading,
|
|
||||||
isPlaying,
|
|
||||||
videoHeight,
|
|
||||||
videoWidth,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||||
|
@ -672,10 +685,10 @@ export default function WebRTCVideo() {
|
||||||
<div className="relative grow overflow-hidden">
|
<div className="relative grow overflow-hidden">
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="grid grow grid-rows-(--grid-bodyFooter) overflow-hidden">
|
<div className="grid grow grid-rows-(--grid-bodyFooter) overflow-hidden">
|
||||||
|
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
|
||||||
|
<PointerLockBar show={showPointerLockBar} />
|
||||||
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
|
<div className="relative mx-4 my-2 flex items-center justify-center overflow-hidden">
|
||||||
<div className="relative flex h-full w-full items-center justify-center">
|
<div className="relative flex h-full w-full items-center justify-center">
|
||||||
{/* In relative mouse mode and under https, we enable the pointer lock, and to do so we need a bar to show the user to click on the video to enable mouse control */}
|
|
||||||
<PointerLockBar show={showPointerLockBar} />
|
|
||||||
<video
|
<video
|
||||||
ref={videoElm}
|
ref={videoElm}
|
||||||
autoPlay={true}
|
autoPlay={true}
|
||||||
|
@ -686,6 +699,9 @@ export default function WebRTCVideo() {
|
||||||
playsInline
|
playsInline
|
||||||
disablePictureInPicture
|
disablePictureInPicture
|
||||||
controlsList="nofullscreen"
|
controlsList="nofullscreen"
|
||||||
|
style={{
|
||||||
|
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
|
||||||
|
}}
|
||||||
className={cx(
|
className={cx(
|
||||||
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { LuCornerDownLeft } from "react-icons/lu";
|
import { LuCornerDownLeft } from "react-icons/lu";
|
||||||
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
|
||||||
import { useClose } from "@headlessui/react";
|
import { useClose } from "@headlessui/react";
|
||||||
|
@ -39,6 +39,13 @@ export default function PasteModal() {
|
||||||
state => state.setKeyboardLayout,
|
state => state.setKeyboardLayout,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// this ensures we always get the original en_US if it hasn't been set yet
|
||||||
|
const safeKeyboardLayout = useMemo(() => {
|
||||||
|
if (keyboardLayout && keyboardLayout.length > 0)
|
||||||
|
return keyboardLayout;
|
||||||
|
return "en_US";
|
||||||
|
}, [keyboardLayout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getKeyboardLayout", {}, resp => {
|
send("getKeyboardLayout", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
|
@ -56,29 +63,28 @@ export default function PasteModal() {
|
||||||
setPasteMode(false);
|
setPasteMode(false);
|
||||||
setDisableVideoFocusTrap(false);
|
setDisableVideoFocusTrap(false);
|
||||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||||
if (!keyboardLayout) return;
|
if (!safeKeyboardLayout) return;
|
||||||
if (!chars[keyboardLayout]) return;
|
if (!chars[safeKeyboardLayout]) return;
|
||||||
|
|
||||||
const text = TextAreaRef.current.value;
|
const text = TextAreaRef.current.value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const char of text) {
|
for (const char of text) {
|
||||||
const { key, shift, altRight, deadKey, accentKey } = chars[keyboardLayout][char]
|
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char]
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
|
|
||||||
const keyz = [ keys[key] ];
|
const keyz = [ keys[key] ];
|
||||||
const modz = [ modifierCode(shift, altRight) ];
|
const modz = [ modifierCode(shift, altRight) ];
|
||||||
|
|
||||||
if (deadKey) {
|
if (deadKey) {
|
||||||
keyz.push(keys["Space"]);
|
keyz.push(keys["Space"]);
|
||||||
modz.push(noModifier);
|
modz.push(noModifier);
|
||||||
}
|
}
|
||||||
if (accentKey) {
|
if (accentKey) {
|
||||||
keyz.unshift(keys[accentKey.key])
|
keyz.unshift(keys[accentKey.key])
|
||||||
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
|
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [index, kei] of keyz.entries()) {
|
for (const [index, kei] of keyz.entries()) {
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
send(
|
send(
|
||||||
"keyboardReport",
|
"keyboardReport",
|
||||||
|
@ -92,13 +98,13 @@ export default function PasteModal() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
notifications.error("Failed to paste text");
|
notifications.error("Failed to paste text");
|
||||||
}
|
}
|
||||||
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, keyboardLayout]);
|
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (TextAreaRef.current) {
|
if (TextAreaRef.current) {
|
||||||
|
@ -125,7 +131,7 @@ export default function PasteModal() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className="w-full" onKeyUp={e => e.stopPropagation()}>
|
<div className="w-full" onKeyUp={e => e.stopPropagation()} onKeyDown={e => e.stopPropagation()}>
|
||||||
<TextAreaWithLabel
|
<TextAreaWithLabel
|
||||||
ref={TextAreaRef}
|
ref={TextAreaRef}
|
||||||
label="Paste from host"
|
label="Paste from host"
|
||||||
|
@ -148,7 +154,7 @@ export default function PasteModal() {
|
||||||
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
||||||
[...new Intl.Segmenter().segment(value)]
|
[...new Intl.Segmenter().segment(value)]
|
||||||
.map(x => x.segment)
|
.map(x => x.segment)
|
||||||
.filter(char => !chars[keyboardLayout][char]),
|
.filter(char => !chars[safeKeyboardLayout][char]),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -167,11 +173,11 @@ export default function PasteModal() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||||
Sending text using keyboard layout: {layouts[keyboardLayout]}
|
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -283,6 +283,8 @@ export const useVideoStore = create<VideoState>(set => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export type KeyboardLedSync = "auto" | "browser" | "host";
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
isCursorHidden: boolean;
|
isCursorHidden: boolean;
|
||||||
setCursorVisibility: (enabled: boolean) => void;
|
setCursorVisibility: (enabled: boolean) => void;
|
||||||
|
@ -305,6 +307,26 @@ interface SettingsState {
|
||||||
|
|
||||||
keyboardLayout: string;
|
keyboardLayout: string;
|
||||||
setKeyboardLayout: (layout: string) => void;
|
setKeyboardLayout: (layout: string) => void;
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: boolean;
|
||||||
|
setActionBarCtrlAltDel: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
keyboardLedSync: KeyboardLedSync;
|
||||||
|
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
||||||
|
|
||||||
|
scrollThrottling: number;
|
||||||
|
setScrollThrottling: (value: number) => void;
|
||||||
|
|
||||||
|
showPressedKeys: boolean;
|
||||||
|
setShowPressedKeys: (show: boolean) => void;
|
||||||
|
|
||||||
|
// Video enhancement settings
|
||||||
|
videoSaturation: number;
|
||||||
|
setVideoSaturation: (value: number) => void;
|
||||||
|
videoBrightness: number;
|
||||||
|
setVideoBrightness: (value: number) => void;
|
||||||
|
videoContrast: number;
|
||||||
|
setVideoContrast: (value: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
export const useSettingsStore = create(
|
||||||
|
@ -336,6 +358,26 @@ export const useSettingsStore = create(
|
||||||
|
|
||||||
keyboardLayout: "en-US",
|
keyboardLayout: "en-US",
|
||||||
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
||||||
|
|
||||||
|
actionBarCtrlAltDel: false,
|
||||||
|
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
|
||||||
|
|
||||||
|
keyboardLedSync: "auto",
|
||||||
|
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
||||||
|
|
||||||
|
scrollThrottling: 0,
|
||||||
|
setScrollThrottling: value => set({ scrollThrottling: value }),
|
||||||
|
|
||||||
|
showPressedKeys: true,
|
||||||
|
setShowPressedKeys: show => set({ showPressedKeys: show }),
|
||||||
|
|
||||||
|
// Video enhancement settings with default values (1.0 = normal)
|
||||||
|
videoSaturation: 1.0,
|
||||||
|
setVideoSaturation: value => set({ videoSaturation: value }),
|
||||||
|
videoBrightness: 1.0,
|
||||||
|
setVideoBrightness: value => set({ videoBrightness: value }),
|
||||||
|
videoContrast: 1.0,
|
||||||
|
setVideoContrast: value => set({ videoContrast: value }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
|
@ -344,17 +386,6 @@ export const useSettingsStore = create(
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface DeviceSettingsState {
|
|
||||||
trackpadSensitivity: number;
|
|
||||||
mouseSensitivity: number;
|
|
||||||
clampMin: number;
|
|
||||||
clampMax: number;
|
|
||||||
blockDelay: number;
|
|
||||||
trackpadThreshold: number;
|
|
||||||
scrollSensitivity: "low" | "default" | "high";
|
|
||||||
setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RemoteVirtualMediaState {
|
export interface RemoteVirtualMediaState {
|
||||||
source: "WebRTC" | "HTTP" | "Storage" | null;
|
source: "WebRTC" | "HTTP" | "Storage" | null;
|
||||||
mode: "CDROM" | "Disk" | null;
|
mode: "CDROM" | "Disk" | null;
|
||||||
|
@ -405,6 +436,21 @@ export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||||
setErrorMessage: message => set({ errorMessage: message }),
|
setErrorMessage: message => set({ errorMessage: message }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export interface KeyboardLedState {
|
||||||
|
num_lock: boolean;
|
||||||
|
caps_lock: boolean;
|
||||||
|
scroll_lock: boolean;
|
||||||
|
compose: boolean;
|
||||||
|
kana: boolean;
|
||||||
|
};
|
||||||
|
const defaultKeyboardLedState: KeyboardLedState = {
|
||||||
|
num_lock: false,
|
||||||
|
caps_lock: false,
|
||||||
|
scroll_lock: false,
|
||||||
|
compose: false,
|
||||||
|
kana: false,
|
||||||
|
};
|
||||||
|
|
||||||
export interface HidState {
|
export interface HidState {
|
||||||
activeKeys: number[];
|
activeKeys: number[];
|
||||||
activeModifiers: number[];
|
activeModifiers: number[];
|
||||||
|
@ -423,18 +469,18 @@ export interface HidState {
|
||||||
altGrCtrlTime: number; // _altGrCtrlTime
|
altGrCtrlTime: number; // _altGrCtrlTime
|
||||||
setAltGrCtrlTime: (time: number) => void;
|
setAltGrCtrlTime: (time: number) => void;
|
||||||
|
|
||||||
isNumLockActive: boolean;
|
keyboardLedState?: KeyboardLedState;
|
||||||
setIsNumLockActive: (enabled: boolean) => void;
|
setKeyboardLedState: (state: KeyboardLedState) => void;
|
||||||
|
setIsNumLockActive: (active: boolean) => void;
|
||||||
|
setIsCapsLockActive: (active: boolean) => void;
|
||||||
|
setIsScrollLockActive: (active: boolean) => void;
|
||||||
|
|
||||||
isScrollLockActive: boolean;
|
keyboardLedStateSyncAvailable: boolean;
|
||||||
setIsScrollLockActive: (enabled: boolean) => void;
|
setKeyboardLedStateSyncAvailable: (available: boolean) => void;
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: boolean;
|
isVirtualKeyboardEnabled: boolean;
|
||||||
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
setVirtualKeyboardEnabled: (enabled: boolean) => void;
|
||||||
|
|
||||||
isCapsLockActive: boolean;
|
|
||||||
setIsCapsLockActive: (enabled: boolean) => void;
|
|
||||||
|
|
||||||
isPasteModeEnabled: boolean;
|
isPasteModeEnabled: boolean;
|
||||||
setPasteModeEnabled: (enabled: boolean) => void;
|
setPasteModeEnabled: (enabled: boolean) => void;
|
||||||
|
|
||||||
|
@ -442,7 +488,7 @@ export interface HidState {
|
||||||
setUsbState: (state: HidState["usbState"]) => void;
|
setUsbState: (state: HidState["usbState"]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHidStore = create<HidState>(set => ({
|
export const useHidStore = create<HidState>((set, get) => ({
|
||||||
activeKeys: [],
|
activeKeys: [],
|
||||||
activeModifiers: [],
|
activeModifiers: [],
|
||||||
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
|
updateActiveKeysAndModifiers: ({ keys, modifiers }) => {
|
||||||
|
@ -458,18 +504,29 @@ export const useHidStore = create<HidState>(set => ({
|
||||||
altGrCtrlTime: 0,
|
altGrCtrlTime: 0,
|
||||||
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
|
setAltGrCtrlTime: time => set({ altGrCtrlTime: time }),
|
||||||
|
|
||||||
isNumLockActive: false,
|
setKeyboardLedState: ledState => set({ keyboardLedState: ledState }),
|
||||||
setIsNumLockActive: enabled => set({ isNumLockActive: enabled }),
|
setIsNumLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.num_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
setIsCapsLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.caps_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
setIsScrollLockActive: active => {
|
||||||
|
const keyboardLedState = { ...(get().keyboardLedState || defaultKeyboardLedState) };
|
||||||
|
keyboardLedState.scroll_lock = active;
|
||||||
|
set({ keyboardLedState });
|
||||||
|
},
|
||||||
|
|
||||||
isScrollLockActive: false,
|
keyboardLedStateSyncAvailable: false,
|
||||||
setIsScrollLockActive: enabled => set({ isScrollLockActive: enabled }),
|
setKeyboardLedStateSyncAvailable: available => set({ keyboardLedStateSyncAvailable: available }),
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: false,
|
isVirtualKeyboardEnabled: false,
|
||||||
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
setVirtualKeyboardEnabled: enabled => set({ isVirtualKeyboardEnabled: enabled }),
|
||||||
|
|
||||||
isCapsLockActive: false,
|
|
||||||
setIsCapsLockActive: enabled => set({ isCapsLockActive: enabled }),
|
|
||||||
|
|
||||||
isPasteModeEnabled: false,
|
isPasteModeEnabled: false,
|
||||||
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
setPasteModeEnabled: enabled => set({ isPasteModeEnabled: enabled }),
|
||||||
|
|
||||||
|
|
|
@ -295,6 +295,10 @@ if (isOnDevice) {
|
||||||
path: "hardware",
|
path: "hardware",
|
||||||
element: <SettingsHardwareRoute />,
|
element: <SettingsHardwareRoute />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "network",
|
||||||
|
element: <SettingsNetworkRoute />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "access",
|
path: "access",
|
||||||
children: [
|
children: [
|
||||||
|
@ -353,7 +357,8 @@ if (isOnDevice) {
|
||||||
{
|
{
|
||||||
path: "devices",
|
path: "devices",
|
||||||
element: <DevicesRoute />,
|
element: <DevicesRoute />,
|
||||||
loader: DevicesRoute.loader },
|
loader: DevicesRoute.loader
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useCallback, useState, useEffect } from "react";
|
|
||||||
|
|
||||||
import { GridCard } from "@components/Card";
|
import { GridCard } from "@components/Card";
|
||||||
|
|
||||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
|
||||||
import Checkbox from "../components/Checkbox";
|
|
||||||
import { useJsonRpc } from "../hooks/useJsonRpc";
|
|
||||||
import notifications from "../notifications";
|
|
||||||
import { TextAreaWithLabel } from "../components/TextArea";
|
|
||||||
import { isOnDevice } from "../main";
|
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
|
import Checkbox from "../components/Checkbox";
|
||||||
|
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||||
|
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||||
|
import { TextAreaWithLabel } from "../components/TextArea";
|
||||||
import { useSettingsStore } from "../hooks/stores";
|
import { useSettingsStore } from "../hooks/stores";
|
||||||
|
import { useJsonRpc } from "../hooks/useJsonRpc";
|
||||||
|
import { isOnDevice } from "../main";
|
||||||
|
import notifications from "../notifications";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
|
@ -22,6 +21,8 @@ export default function SettingsAdvancedRoute() {
|
||||||
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
const setDeveloperMode = useSettingsStore(state => state.setDeveloperMode);
|
||||||
const [devChannel, setDevChannel] = useState(false);
|
const [devChannel, setDevChannel] = useState(false);
|
||||||
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
|
const [usbEmulationEnabled, setUsbEmulationEnabled] = useState(false);
|
||||||
|
const [showLoopbackWarning, setShowLoopbackWarning] = useState(false);
|
||||||
|
const [localLoopbackOnly, setLocalLoopbackOnly] = useState(false);
|
||||||
|
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
|
|
||||||
|
@ -46,6 +47,11 @@ export default function SettingsAdvancedRoute() {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
setDevChannel(resp.result as boolean);
|
setDevChannel(resp.result as boolean);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
send("getLocalLoopbackOnly", {}, resp => {
|
||||||
|
if ("error" in resp) return;
|
||||||
|
setLocalLoopbackOnly(resp.result as boolean);
|
||||||
|
});
|
||||||
}, [send, setDeveloperMode]);
|
}, [send, setDeveloperMode]);
|
||||||
|
|
||||||
const getUsbEmulationState = useCallback(() => {
|
const getUsbEmulationState = useCallback(() => {
|
||||||
|
@ -110,17 +116,62 @@ export default function SettingsAdvancedRoute() {
|
||||||
[send, setDeveloperMode],
|
[send, setDeveloperMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDevChannelChange = (enabled: boolean) => {
|
const handleDevChannelChange = useCallback(
|
||||||
send("setDevChannelState", { enabled }, resp => {
|
(enabled: boolean) => {
|
||||||
if ("error" in resp) {
|
send("setDevChannelState", { enabled }, resp => {
|
||||||
notifications.error(
|
if ("error" in resp) {
|
||||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
notifications.error(
|
||||||
);
|
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||||
return;
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDevChannel(enabled);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[send, setDevChannel],
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyLoopbackOnlyMode = useCallback(
|
||||||
|
(enabled: boolean) => {
|
||||||
|
send("setLocalLoopbackOnly", { enabled }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(
|
||||||
|
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLocalLoopbackOnly(enabled);
|
||||||
|
if (enabled) {
|
||||||
|
notifications.success(
|
||||||
|
"Loopback-only mode enabled. Restart your device to apply.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifications.success(
|
||||||
|
"Loopback-only mode disabled. Restart your device to apply.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[send, setLocalLoopbackOnly],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLoopbackOnlyModeChange = useCallback(
|
||||||
|
(enabled: boolean) => {
|
||||||
|
// If trying to enable loopback-only mode, show warning first
|
||||||
|
if (enabled) {
|
||||||
|
setShowLoopbackWarning(true);
|
||||||
|
} else {
|
||||||
|
// If disabling, just proceed
|
||||||
|
applyLoopbackOnlyMode(false);
|
||||||
}
|
}
|
||||||
setDevChannel(enabled);
|
},
|
||||||
});
|
[applyLoopbackOnlyMode, setShowLoopbackWarning],
|
||||||
};
|
);
|
||||||
|
|
||||||
|
const confirmLoopbackModeEnable = useCallback(() => {
|
||||||
|
applyLoopbackOnlyMode(true);
|
||||||
|
setShowLoopbackWarning(false);
|
||||||
|
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
@ -153,7 +204,7 @@ export default function SettingsAdvancedRoute() {
|
||||||
|
|
||||||
{settings.developerMode && (
|
{settings.developerMode && (
|
||||||
<GridCard>
|
<GridCard>
|
||||||
<div className="flex select-none items-start gap-x-4 p-4">
|
<div className="flex items-start gap-x-4 p-4 select-none">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
@ -187,6 +238,16 @@ export default function SettingsAdvancedRoute() {
|
||||||
</GridCard>
|
</GridCard>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
title="Loopback-Only Mode"
|
||||||
|
description="Restrict web interface access to localhost only (127.0.0.1)"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={localLoopbackOnly}
|
||||||
|
onChange={e => handleLoopbackOnlyModeChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
{isOnDevice && settings.developerMode && (
|
{isOnDevice && settings.developerMode && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
|
@ -261,6 +322,30 @@ export default function SettingsAdvancedRoute() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
open={showLoopbackWarning}
|
||||||
|
onClose={() => {
|
||||||
|
setShowLoopbackWarning(false);
|
||||||
|
}}
|
||||||
|
title="Enable Loopback-Only Mode?"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
WARNING: This will restrict web interface access to localhost (127.0.0.1)
|
||||||
|
only.
|
||||||
|
</p>
|
||||||
|
<p>Before enabling this feature, make sure you have either:</p>
|
||||||
|
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
<li>SSH access configured and tested</li>
|
||||||
|
<li>Cloud access enabled and working</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
variant="warning"
|
||||||
|
confirmText="I Understand, Enable Anyway"
|
||||||
|
onConfirm={confirmLoopbackModeEnable}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
import { Checkbox } from "@/components/Checkbox";
|
||||||
|
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||||
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
|
|
||||||
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
|
export default function SettingsCtrlAltDelRoute() {
|
||||||
|
const enableCtrlAltDel = useSettingsStore(state => state.actionBarCtrlAltDel);
|
||||||
|
const setEnableCtrlAltDel = useSettingsStore(state => state.setActionBarCtrlAltDel);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsPageHeader
|
||||||
|
title="Action Bar"
|
||||||
|
description="Customize the action bar of your JetKVM interface"
|
||||||
|
/>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem title="Enable Ctrl-Alt-Del" description="Enable the Ctrl-Alt-Del key on the virtual keyboard">
|
||||||
|
<Checkbox
|
||||||
|
checked={enableCtrlAltDel}
|
||||||
|
onChange={e => setEnableCtrlAltDel(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -116,6 +116,15 @@ export default function SettingsHardwareRoute() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
{/* <SettingsItem
|
||||||
|
title="Enable Ctrl+Alt+Del Action Bar"
|
||||||
|
description="Enable or disable the action bar action for sending a Ctrl+Alt+Del to the host"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={actionBarConfig.ctrlAltDel}
|
||||||
|
onChange={onActionBarItemChange("ctrlAltDel")}
|
||||||
|
/>
|
||||||
|
</SettingsItem> */}
|
||||||
{settings.backlightSettings.max_brightness != 0 && (
|
{settings.backlightSettings.max_brightness != 0 && (
|
||||||
<>
|
<>
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { useSettingsStore } from "@/hooks/stores";
|
import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
import { layouts } from "@/keyboardLayouts";
|
import { layouts } from "@/keyboardLayouts";
|
||||||
|
import { Checkbox } from "@/components/Checkbox";
|
||||||
|
|
||||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||||
|
|
||||||
|
@ -12,11 +13,31 @@ import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
export default function SettingsKeyboardRoute() {
|
export default function SettingsKeyboardRoute() {
|
||||||
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
|
||||||
|
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
|
||||||
|
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
|
||||||
const setKeyboardLayout = useSettingsStore(
|
const setKeyboardLayout = useSettingsStore(
|
||||||
state => state.setKeyboardLayout,
|
state => state.setKeyboardLayout,
|
||||||
);
|
);
|
||||||
|
const setKeyboardLedSync = useSettingsStore(
|
||||||
|
state => state.setKeyboardLedSync,
|
||||||
|
);
|
||||||
|
const setShowPressedKeys = useSettingsStore(
|
||||||
|
state => state.setShowPressedKeys,
|
||||||
|
);
|
||||||
|
|
||||||
|
// this ensures we always get the original en_US if it hasn't been set yet
|
||||||
|
const safeKeyboardLayout = useMemo(() => {
|
||||||
|
if (keyboardLayout && keyboardLayout.length > 0)
|
||||||
|
return keyboardLayout;
|
||||||
|
return "en_US";
|
||||||
|
}, [keyboardLayout]);
|
||||||
|
|
||||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||||
|
const ledSyncOptions = [
|
||||||
|
{ value: "auto", label: "Automatic" },
|
||||||
|
{ value: "browser", label: "Browser Only" },
|
||||||
|
{ value: "host", label: "Host Only" },
|
||||||
|
];
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
|
@ -47,7 +68,7 @@ export default function SettingsKeyboardRoute() {
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsPageHeader
|
<SettingsPageHeader
|
||||||
title="Keyboard"
|
title="Keyboard"
|
||||||
description="Configure keyboard layout settings for your device"
|
description="Configure keyboard settings for your device"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
@ -60,7 +81,7 @@ export default function SettingsKeyboardRoute() {
|
||||||
size="SM"
|
size="SM"
|
||||||
label=""
|
label=""
|
||||||
fullWidth
|
fullWidth
|
||||||
value={keyboardLayout}
|
value={safeKeyboardLayout}
|
||||||
onChange={onKeyboardLayoutChange}
|
onChange={onKeyboardLayoutChange}
|
||||||
options={layoutOptions}
|
options={layoutOptions}
|
||||||
/>
|
/>
|
||||||
|
@ -69,6 +90,35 @@ export default function SettingsKeyboardRoute() {
|
||||||
Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
Pasting text sends individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{ /* this menu item could be renamed to plain "Keyboard layout" in the future, when also the virtual keyboard layout mappings are being implemented */ }
|
||||||
|
<SettingsItem
|
||||||
|
title="LED state synchronization"
|
||||||
|
description="Synchronize the LED state of the keyboard with the target device"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label=""
|
||||||
|
fullWidth
|
||||||
|
value={keyboardLedSync}
|
||||||
|
onChange={e => setKeyboardLedSync(e.target.value as KeyboardLedSync)}
|
||||||
|
options={ledSyncOptions}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="Show Pressed Keys"
|
||||||
|
description="Display currently pressed keys in the status bar"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={showPressedKeys}
|
||||||
|
onChange={e => setShowPressedKeys(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useSettingsStore } from "@/hooks/stores";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||||
|
|
||||||
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
import { useFeatureFlag } from "../hooks/useFeatureFlag";
|
||||||
import { cx } from "../cva.config";
|
import { cx } from "../cva.config";
|
||||||
|
@ -26,6 +27,19 @@ export default function SettingsMouseRoute() {
|
||||||
|
|
||||||
const [jiggler, setJiggler] = useState(false);
|
const [jiggler, setJiggler] = useState(false);
|
||||||
|
|
||||||
|
const scrollThrottling = useSettingsStore(state => state.scrollThrottling);
|
||||||
|
const setScrollThrottling = useSettingsStore(
|
||||||
|
state => state.setScrollThrottling,
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollThrottlingOptions = [
|
||||||
|
{ value: "0", label: "Off" },
|
||||||
|
{ value: "10", label: "Low" },
|
||||||
|
{ value: "25", label: "Medium" },
|
||||||
|
{ value: "50", label: "High" },
|
||||||
|
{ value: "100", label: "Very High" },
|
||||||
|
];
|
||||||
|
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -65,6 +79,21 @@ export default function SettingsMouseRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
title="Scroll Throttling"
|
||||||
|
description="Reduce the frequency of scroll events"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
label=""
|
||||||
|
className="max-w-[292px]"
|
||||||
|
value={scrollThrottling}
|
||||||
|
fullWidth
|
||||||
|
onChange={e => setScrollThrottling(parseInt(e.target.value))}
|
||||||
|
options={scrollThrottlingOptions}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="Jiggler"
|
title="Jiggler"
|
||||||
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
|
||||||
|
|
|
@ -228,7 +228,6 @@ export default function SettingsNetworkRoute() {
|
||||||
size="SM"
|
size="SM"
|
||||||
value={networkState?.mac_address}
|
value={networkState?.mac_address}
|
||||||
error={""}
|
error={""}
|
||||||
disabled={true}
|
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
className="dark:!text-opacity-60"
|
className="dark:!text-opacity-60"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -269,22 +269,17 @@ export default function SettingsRoute() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsItem({
|
interface SettingsItemProps {
|
||||||
title,
|
readonly title: string;
|
||||||
description,
|
readonly description: string | React.ReactNode;
|
||||||
children,
|
readonly badge?: string;
|
||||||
className,
|
readonly className?: string;
|
||||||
loading,
|
readonly loading?: boolean;
|
||||||
badge,
|
readonly children?: React.ReactNode;
|
||||||
}: {
|
}
|
||||||
title: string;
|
export function SettingsItem(props: SettingsItemProps) {
|
||||||
description: string | React.ReactNode;
|
const { title, description, badge, children, className, loading } = props;
|
||||||
children?: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
name?: string;
|
|
||||||
loading?: boolean;
|
|
||||||
badge?: string;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={cx(
|
className={cx(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Button } from "@/components/Button";
|
||||||
import { TextAreaWithLabel } from "@/components/TextArea";
|
import { TextAreaWithLabel } from "@/components/TextArea";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||||
|
import { useSettingsStore } from "@/hooks/stores";
|
||||||
|
|
||||||
import notifications from "../notifications";
|
import notifications from "../notifications";
|
||||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||||
|
@ -45,6 +46,14 @@ export default function SettingsVideoRoute() {
|
||||||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||||
const [edid, setEdid] = useState<string | null>(null);
|
const [edid, setEdid] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Video enhancement settings from store
|
||||||
|
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
||||||
|
const setVideoSaturation = useSettingsStore(state => state.setVideoSaturation);
|
||||||
|
const videoBrightness = useSettingsStore(state => state.videoBrightness);
|
||||||
|
const setVideoBrightness = useSettingsStore(state => state.setVideoBrightness);
|
||||||
|
const videoContrast = useSettingsStore(state => state.videoContrast);
|
||||||
|
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getStreamQualityFactor", {}, resp => {
|
send("getStreamQualityFactor", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
|
@ -126,6 +135,73 @@ export default function SettingsVideoRoute() {
|
||||||
onChange={e => handleStreamQualityChange(e.target.value)}
|
onChange={e => handleStreamQualityChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
{/* Video Enhancement Settings */}
|
||||||
|
<SettingsItem
|
||||||
|
title="Video Enhancement"
|
||||||
|
description="Adjust color settings to make the video output more vibrant and colorful"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="space-y-4 pl-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="Saturation"
|
||||||
|
description={`Color saturation (${videoSaturation.toFixed(1)}x)`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.5"
|
||||||
|
max="2.0"
|
||||||
|
step="0.1"
|
||||||
|
value={videoSaturation}
|
||||||
|
onChange={e => setVideoSaturation(parseFloat(e.target.value))}
|
||||||
|
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
title="Brightness"
|
||||||
|
description={`Brightness level (${videoBrightness.toFixed(1)}x)`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.5"
|
||||||
|
max="1.5"
|
||||||
|
step="0.1"
|
||||||
|
value={videoBrightness}
|
||||||
|
onChange={e => setVideoBrightness(parseFloat(e.target.value))}
|
||||||
|
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
title="Contrast"
|
||||||
|
description={`Contrast level (${videoContrast.toFixed(1)}x)`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.5"
|
||||||
|
max="2.0"
|
||||||
|
step="0.1"
|
||||||
|
value={videoContrast}
|
||||||
|
onChange={e => setVideoContrast(parseFloat(e.target.value))}
|
||||||
|
className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Reset to Default"
|
||||||
|
onClick={() => {
|
||||||
|
setVideoSaturation(1.0);
|
||||||
|
setVideoBrightness(1.0);
|
||||||
|
setVideoContrast(1.0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="EDID"
|
title="EDID"
|
||||||
description="Adjust the EDID settings for the display"
|
description="Adjust the EDID settings for the display"
|
||||||
|
|
|
@ -19,6 +19,7 @@ import useWebSocket from "react-use-websocket";
|
||||||
import { cx } from "@/cva.config";
|
import { cx } from "@/cva.config";
|
||||||
import {
|
import {
|
||||||
HidState,
|
HidState,
|
||||||
|
KeyboardLedState,
|
||||||
NetworkState,
|
NetworkState,
|
||||||
UpdateState,
|
UpdateState,
|
||||||
useDeviceStore,
|
useDeviceStore,
|
||||||
|
@ -586,6 +587,11 @@ export default function KvmIdRoute() {
|
||||||
const setUsbState = useHidStore(state => state.setUsbState);
|
const setUsbState = useHidStore(state => state.setUsbState);
|
||||||
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
||||||
|
|
||||||
|
const keyboardLedState = useHidStore(state => state.keyboardLedState);
|
||||||
|
const setKeyboardLedState = useHidStore(state => state.setKeyboardLedState);
|
||||||
|
|
||||||
|
const setKeyboardLedStateSyncAvailable = useHidStore(state => state.setKeyboardLedStateSyncAvailable);
|
||||||
|
|
||||||
const [hasUpdated, setHasUpdated] = useState(false);
|
const [hasUpdated, setHasUpdated] = useState(false);
|
||||||
const { navigateTo } = useDeviceUiNavigation();
|
const { navigateTo } = useDeviceUiNavigation();
|
||||||
|
|
||||||
|
@ -607,6 +613,13 @@ export default function KvmIdRoute() {
|
||||||
setNetworkState(resp.params as NetworkState);
|
setNetworkState(resp.params as NetworkState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.method === "keyboardLedState") {
|
||||||
|
const ledState = resp.params as KeyboardLedState;
|
||||||
|
console.log("Setting keyboard led state", ledState);
|
||||||
|
setKeyboardLedState(ledState);
|
||||||
|
setKeyboardLedStateSyncAvailable(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.method === "otaState") {
|
if (resp.method === "otaState") {
|
||||||
const otaState = resp.params as UpdateState["otaState"];
|
const otaState = resp.params as UpdateState["otaState"];
|
||||||
setOtaState(otaState);
|
setOtaState(otaState);
|
||||||
|
@ -643,6 +656,29 @@ export default function KvmIdRoute() {
|
||||||
});
|
});
|
||||||
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
}, [rpcDataChannel?.readyState, send, setHdmiState]);
|
||||||
|
|
||||||
|
// request keyboard led state from the device
|
||||||
|
useEffect(() => {
|
||||||
|
if (rpcDataChannel?.readyState !== "open") return;
|
||||||
|
if (keyboardLedState !== undefined) return;
|
||||||
|
console.log("Requesting keyboard led state");
|
||||||
|
|
||||||
|
send("getKeyboardLedState", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
// -32601 means the method is not supported
|
||||||
|
if (resp.error.code === -32601) {
|
||||||
|
setKeyboardLedStateSyncAvailable(false);
|
||||||
|
console.error("Failed to get keyboard led state, disabling sync", resp.error);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to get keyboard led state", resp.error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Keyboard led state", resp.result);
|
||||||
|
setKeyboardLedState(resp.result as KeyboardLedState);
|
||||||
|
setKeyboardLedStateSyncAvailable(true);
|
||||||
|
});
|
||||||
|
}, [rpcDataChannel?.readyState, send, setKeyboardLedState, setKeyboardLedStateSyncAvailable, keyboardLedState]);
|
||||||
|
|
||||||
// When the update is successful, we need to refresh the client javascript and show a success modal
|
// When the update is successful, we need to refresh the client javascript and show a success modal
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (queryParams.get("updateSuccess")) {
|
if (queryParams.get("updateSuccess")) {
|
||||||
|
@ -679,12 +715,10 @@ export default function KvmIdRoute() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!peerConnection) return;
|
if (!peerConnection) return;
|
||||||
if (!kvmTerminal) {
|
if (!kvmTerminal) {
|
||||||
// console.log('Creating data channel "terminal"');
|
|
||||||
setKvmTerminal(peerConnection.createDataChannel("terminal"));
|
setKvmTerminal(peerConnection.createDataChannel("terminal"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serialConsole) {
|
if (!serialConsole) {
|
||||||
// console.log('Creating data channel "serial"');
|
|
||||||
setSerialConsole(peerConnection.createDataChannel("serial"));
|
setSerialConsole(peerConnection.createDataChannel("serial"));
|
||||||
}
|
}
|
||||||
}, [kvmTerminal, peerConnection, serialConsole]);
|
}, [kvmTerminal, peerConnection, serialConsole]);
|
||||||
|
@ -719,10 +753,10 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
const ConnectionStatusElement = useMemo(() => {
|
const ConnectionStatusElement = useMemo(() => {
|
||||||
const hasConnectionFailed =
|
const hasConnectionFailed =
|
||||||
connectionFailed || ["failed", "closed"].includes(peerConnectionState || "");
|
connectionFailed || ["failed", "closed"].includes(peerConnectionState ?? "");
|
||||||
|
|
||||||
const isPeerConnectionLoading =
|
const isPeerConnectionLoading =
|
||||||
["connecting", "new"].includes(peerConnectionState || "") ||
|
["connecting", "new"].includes(peerConnectionState ?? "") ||
|
||||||
peerConnection === null;
|
peerConnection === null;
|
||||||
|
|
||||||
const isDisconnected = peerConnectionState === "disconnected";
|
const isDisconnected = peerConnectionState === "disconnected";
|
||||||
|
@ -790,7 +824,7 @@ export default function KvmIdRoute() {
|
||||||
isLoggedIn={authMode === "password" || !!user}
|
isLoggedIn={authMode === "password" || !!user}
|
||||||
userEmail={user?.email}
|
userEmail={user?.email}
|
||||||
picture={user?.picture}
|
picture={user?.picture}
|
||||||
kvmName={deviceName || "JetKVM Device"}
|
kvmName={deviceName ?? "JetKVM Device"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative flex h-full w-full overflow-hidden">
|
<div className="relative flex h-full w-full overflow-hidden">
|
||||||
|
@ -810,6 +844,9 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="z-50"
|
className="z-50"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
onMouseUp={e => e.stopPropagation()}
|
||||||
|
onMouseDown={e => e.stopPropagation()}
|
||||||
onKeyUp={e => e.stopPropagation()}
|
onKeyUp={e => e.stopPropagation()}
|
||||||
onKeyDown={e => {
|
onKeyDown={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -833,7 +870,12 @@ export default function KvmIdRoute() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarContainer({ sidebarView }: { sidebarView: string | null }) {
|
interface SidebarContainerProps {
|
||||||
|
readonly sidebarView: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarContainer(props: SidebarContainerProps) {
|
||||||
|
const { sidebarView }= props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
15
usb.go
15
usb.go
|
@ -24,6 +24,17 @@ func initUsbGadget() {
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
gadget.SetOnKeyboardStateChange(func(state usbgadget.KeyboardState) {
|
||||||
|
if currentSession != nil {
|
||||||
|
writeJSONRPCEvent("keyboardLedState", state, currentSession)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// open the keyboard hid file to listen for keyboard events
|
||||||
|
if err := gadget.OpenKeyboardHidFile(); err != nil {
|
||||||
|
usbLogger.Error().Err(err).Msg("failed to open keyboard hid file")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
func rpcKeyboardReport(modifier uint8, keys []uint8) error {
|
||||||
|
@ -42,6 +53,10 @@ func rpcWheelReport(wheelY int8) error {
|
||||||
return gadget.AbsMouseWheelReport(wheelY)
|
return gadget.AbsMouseWheelReport(wheelY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetKeyboardLedState() (state usbgadget.KeyboardState) {
|
||||||
|
return gadget.GetKeyboardState()
|
||||||
|
}
|
||||||
|
|
||||||
var usbState = "unknown"
|
var usbState = "unknown"
|
||||||
|
|
||||||
func rpcGetUSBState() (state string) {
|
func rpcGetUSBState() (state string) {
|
||||||
|
|
20
web.go
20
web.go
|
@ -52,8 +52,9 @@ type ChangePasswordRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalDevice struct {
|
type LocalDevice struct {
|
||||||
AuthMode *string `json:"authMode"`
|
AuthMode *string `json:"authMode"`
|
||||||
DeviceID string `json:"deviceId"`
|
DeviceID string `json:"deviceId"`
|
||||||
|
LoopbackOnly bool `json:"loopbackOnly"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceStatus struct {
|
type DeviceStatus struct {
|
||||||
|
@ -532,7 +533,15 @@ func basicAuthProtectedMiddleware(requireDeveloperMode bool) gin.HandlerFunc {
|
||||||
|
|
||||||
func RunWebServer() {
|
func RunWebServer() {
|
||||||
r := setupRouter()
|
r := setupRouter()
|
||||||
err := r.Run(":80")
|
|
||||||
|
// Determine the binding address based on the config
|
||||||
|
bindAddress := ":80" // Default to all interfaces
|
||||||
|
if config.LocalLoopbackOnly {
|
||||||
|
bindAddress = "localhost:80" // Loopback only (both IPv4 and IPv6)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info().Str("bindAddress", bindAddress).Bool("loopbackOnly", config.LocalLoopbackOnly).Msg("Starting web server")
|
||||||
|
err := r.Run(bindAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -540,8 +549,9 @@ func RunWebServer() {
|
||||||
|
|
||||||
func handleDevice(c *gin.Context) {
|
func handleDevice(c *gin.Context) {
|
||||||
response := LocalDevice{
|
response := LocalDevice{
|
||||||
AuthMode: &config.LocalAuthMode,
|
AuthMode: &config.LocalAuthMode,
|
||||||
DeviceID: GetDeviceID(),
|
DeviceID: GetDeviceID(),
|
||||||
|
LoopbackOnly: config.LocalLoopbackOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
|
|
Loading…
Reference in New Issue