mirror of https://github.com/jetkvm/kvm.git
Compare commits
34 Commits
7667b61ef0
...
ae954db162
Author | SHA1 | Date |
---|---|---|
|
ae954db162 | |
|
00ef587eeb | |
|
f1953fddbc | |
|
9ba97ebe67 | |
|
5fb8d866ba | |
|
3359f8fca4 | |
|
ef95643a86 | |
|
1fc603b553 | |
|
aada3d95e0 | |
|
d704fcc6c7 | |
|
ab3dda6dee | |
|
4a23f22a55 | |
|
11a095c0f6 | |
|
584768bacf | |
|
488276f3a8 | |
|
7267347261 | |
|
393bc122d4 | |
|
6d13e1be12 | |
|
bde0a086ab | |
|
9c9335da31 | |
|
090e0b4b47 | |
|
48a7a638a3 | |
|
e4f6a713a5 | |
|
9fcf74b398 | |
|
353099001f | |
|
73f5659618 | |
|
960f555790 | |
|
fe127ed41c | |
|
3e7d8fb0f5 | |
|
0d7f47c109 | |
|
254c001572 | |
|
6f037a832d | |
|
ccba27cedd | |
|
cf9c6e5cc8 |
|
@ -10,9 +10,9 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
name: Build
|
||||
if: "github.event.review.state == 'approved' || github.event.event_type != 'pull_request_review'"
|
||||
if: github.event_name != 'pull_request_review' || github.event.review.state == 'approved'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
@ -23,7 +23,7 @@ jobs:
|
|||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v5.5.0
|
||||
with:
|
||||
go-version: "1.24.4"
|
||||
- name: Build frontend
|
||||
|
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@19bb51245e9c80abacb2e91cc42b33fa478b8639 # v4.2.1
|
||||
uses: actions/setup-go@fa96338abe5531f6e34c5cc0bbe28c1a533d5505 # v4.2.1
|
||||
with:
|
||||
go-version: 1.24.4
|
||||
- name: Create empty resource directory
|
||||
|
|
|
@ -104,7 +104,7 @@ jobs:
|
|||
EOF
|
||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
||||
- name: Set up Golang
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v5.5.0
|
||||
with:
|
||||
go-version: "1.24.4"
|
||||
- name: Golang Test Report
|
||||
|
|
|
@ -14,16 +14,16 @@ permissions:
|
|||
jobs:
|
||||
ui-lint:
|
||||
name: UI Lint
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v21.1.0
|
||||
node-version: "22"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "ui/package-lock.json"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd ui
|
||||
|
|
|
@ -23,6 +23,9 @@ linters:
|
|||
- linters:
|
||||
- errcheck
|
||||
path: _test.go
|
||||
- linters:
|
||||
- forbidigo
|
||||
path: cmd/main.go
|
||||
- linters:
|
||||
- gochecknoinits
|
||||
path: internal/logging/sse.go
|
||||
|
|
4
Makefile
4
Makefile
|
@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
|||
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
||||
BUILDTS ?= $(shell date -u +%s)
|
||||
REVISION ?= $(shell git rev-parse HEAD)
|
||||
VERSION_DEV ?= 0.4.5-dev$(shell date +%Y%m%d%H%M)
|
||||
VERSION ?= 0.4.4
|
||||
VERSION_DEV ?= 0.4.7-dev$(shell date +%Y%m%d%H%M)
|
||||
VERSION ?= 0.4.6
|
||||
|
||||
PROMETHEUS_TAG := github.com/prometheus/common/version
|
||||
KVM_PKG_NAME := github.com/jetkvm/kvm
|
||||
|
|
18
cmd/main.go
18
cmd/main.go
|
@ -1,9 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jetkvm/kvm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
versionPtr := flag.Bool("version", false, "print version and exit")
|
||||
versionJsonPtr := flag.Bool("version-json", false, "print version as json and exit")
|
||||
flag.Parse()
|
||||
|
||||
if *versionPtr || *versionJsonPtr {
|
||||
versionData, err := kvm.GetVersionData(*versionJsonPtr)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to get version data: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println(string(versionData))
|
||||
return
|
||||
}
|
||||
|
||||
kvm.Main()
|
||||
}
|
||||
|
|
25
config.go
25
config.go
|
@ -9,6 +9,8 @@ import (
|
|||
"github.com/jetkvm/kvm/internal/logging"
|
||||
"github.com/jetkvm/kvm/internal/network"
|
||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
type WakeOnLanDevice struct {
|
||||
|
@ -111,7 +113,7 @@ var defaultConfig = &Config{
|
|||
ActiveExtension: "",
|
||||
KeyboardMacros: []KeyboardMacro{},
|
||||
DisplayRotation: "270",
|
||||
KeyboardLayout: "en-US",
|
||||
KeyboardLayout: "en_US",
|
||||
DisplayMaxBrightness: 64,
|
||||
DisplayDimAfterSec: 120, // 2 minutes
|
||||
DisplayOffAfterSec: 1800, // 30 minutes
|
||||
|
@ -138,6 +140,21 @@ var (
|
|||
configLock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
var (
|
||||
configSuccess = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_config_last_reload_successful",
|
||||
Help: "The last configuration load succeeded",
|
||||
},
|
||||
)
|
||||
configSuccessTime = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_config_last_reload_success_timestamp_seconds",
|
||||
Help: "Timestamp of last successful config load",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
func LoadConfig() {
|
||||
configLock.Lock()
|
||||
defer configLock.Unlock()
|
||||
|
@ -153,6 +170,8 @@ func LoadConfig() {
|
|||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
logger.Debug().Msg("default config file doesn't exist, using default")
|
||||
configSuccess.Set(1.0)
|
||||
configSuccessTime.SetToCurrentTime()
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
@ -161,6 +180,7 @@ func LoadConfig() {
|
|||
loadedConfig := *defaultConfig
|
||||
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
|
||||
logger.Warn().Err(err).Msg("config file JSON parsing failed")
|
||||
configSuccess.Set(0.0)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -181,6 +201,9 @@ func LoadConfig() {
|
|||
|
||||
logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
|
||||
|
||||
configSuccess.Set(1.0)
|
||||
configSuccessTime.SetToCurrentTime()
|
||||
|
||||
logger.Info().Str("path", configPath).Msg("config loaded")
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
dcCurrentGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_current_amperes",
|
||||
Help: "Current DC power consumption in amperes",
|
||||
})
|
||||
|
||||
dcPowerGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_power_watts",
|
||||
Help: "DC power consumption in watts",
|
||||
})
|
||||
|
||||
dcVoltageGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_voltage_volts",
|
||||
Help: "DC voltage in volts",
|
||||
})
|
||||
|
||||
dcStateGauge = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "jetkvm_dc_power_state",
|
||||
Help: "DC power state (1 = on, 0 = off)",
|
||||
})
|
||||
|
||||
dcMetricsRegistered sync.Once
|
||||
)
|
||||
|
||||
// registerDCMetrics registers the DC power metrics with Prometheus (called once when DC control is mounted)
|
||||
func registerDCMetrics() {
|
||||
dcMetricsRegistered.Do(func() {
|
||||
prometheus.MustRegister(dcCurrentGauge)
|
||||
prometheus.MustRegister(dcPowerGauge)
|
||||
prometheus.MustRegister(dcVoltageGauge)
|
||||
prometheus.MustRegister(dcStateGauge)
|
||||
})
|
||||
}
|
||||
|
||||
// updateDCMetrics updates the Prometheus metrics with current DC power state values
|
||||
func updateDCMetrics(state DCPowerState) {
|
||||
dcCurrentGauge.Set(state.Current)
|
||||
dcPowerGauge.Set(state.Power)
|
||||
dcVoltageGauge.Set(state.Voltage)
|
||||
if state.IsOn {
|
||||
dcStateGauge.Set(1)
|
||||
} else {
|
||||
dcStateGauge.Set(0)
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ show_help() {
|
|||
echo " --run-go-tests Run go tests"
|
||||
echo " --run-go-tests-only Run go tests and exit"
|
||||
echo " --skip-ui-build Skip frontend/UI build"
|
||||
echo " -i, --install Build for release and install the app"
|
||||
echo " --help Display this help message"
|
||||
echo
|
||||
echo "Example:"
|
||||
|
@ -43,6 +44,7 @@ RESET_USB_HID_DEVICE=false
|
|||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||
RUN_GO_TESTS=false
|
||||
RUN_GO_TESTS_ONLY=false
|
||||
INSTALL_APP=false
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
|
@ -72,6 +74,10 @@ while [[ $# -gt 0 ]]; do
|
|||
RUN_GO_TESTS=true
|
||||
shift
|
||||
;;
|
||||
-i|--install)
|
||||
INSTALL_APP=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_help
|
||||
exit 0
|
||||
|
@ -139,25 +145,36 @@ EOF
|
|||
fi
|
||||
fi
|
||||
|
||||
msg_info "▶ Building go binary"
|
||||
make build_dev
|
||||
|
||||
# Kill any existing instances of the application
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
# Copy the binary to the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
if [ "$INSTALL_APP" = true ]
|
||||
then
|
||||
msg_info "▶ Building release binary"
|
||||
make build_release
|
||||
|
||||
# Copy the binary to the remote host as if we were the OTA updater.
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
|
||||
|
||||
# Reboot the device, the new app will be deployed by the startup process.
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
|
||||
else
|
||||
msg_info "▶ Building development binary"
|
||||
make build_dev
|
||||
|
||||
# Kill any existing instances of the application
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||
|
||||
# Copy the binary to the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
||||
|
||||
if [ "$RESET_USB_HID_DEVICE" = true ]; then
|
||||
msg_info "▶ Resetting USB HID device"
|
||||
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
|
||||
# Remove the old USB gadget configuration
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
|
||||
fi
|
||||
|
||||
# Deploy and run the application on the remote host
|
||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||
set -e
|
||||
|
||||
# Set the library path to include the directory where librockit.so is located
|
||||
|
@ -174,7 +191,8 @@ cd "${REMOTE_PATH}"
|
|||
chmod +x jetkvm_app_debug
|
||||
|
||||
# Run the application in the background
|
||||
PION_LOG_TRACE=${LOG_TRACE_SCOPES} GODEBUG=netdns=1 ./jetkvm_app_debug
|
||||
PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug | tee -a /tmp/jetkvm_app_debug.log
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Deployment complete."
|
||||
echo "Deployment complete."
|
||||
|
|
25
display.go
25
display.go
|
@ -9,9 +9,14 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
var currentScreen = "ui_Boot_Screen"
|
||||
var backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
|
||||
|
||||
var (
|
||||
currentScreen = "ui_Boot_Screen"
|
||||
displayedTexts = make(map[string]string)
|
||||
screenStateLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
var (
|
||||
dimTicker *time.Ticker
|
||||
offTicker *time.Ticker
|
||||
|
@ -22,6 +27,8 @@ const (
|
|||
backlightControlClass string = "/sys/class/backlight/backlight/brightness"
|
||||
)
|
||||
|
||||
// do not call this function directly, use switchToScreenIfDifferent instead
|
||||
// this function is not thread safe
|
||||
func switchToScreen(screen string) {
|
||||
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
||||
if err != nil {
|
||||
|
@ -31,8 +38,6 @@ func switchToScreen(screen string) {
|
|||
currentScreen = screen
|
||||
}
|
||||
|
||||
var displayedTexts = make(map[string]string)
|
||||
|
||||
func lvObjSetState(objName string, state string) (*CtrlResponse, error) {
|
||||
return CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": objName, "state": state})
|
||||
}
|
||||
|
@ -78,6 +83,9 @@ func lvDispSetRotation(rotation string) (*CtrlResponse, error) {
|
|||
}
|
||||
|
||||
func updateLabelIfChanged(objName string, newText string) {
|
||||
screenStateLock.Lock()
|
||||
defer screenStateLock.Unlock()
|
||||
|
||||
if newText != "" && newText != displayedTexts[objName] {
|
||||
_, _ = lvLabelSetText(objName, newText)
|
||||
displayedTexts[objName] = newText
|
||||
|
@ -85,12 +93,23 @@ func updateLabelIfChanged(objName string, newText string) {
|
|||
}
|
||||
|
||||
func switchToScreenIfDifferent(screenName string) {
|
||||
screenStateLock.Lock()
|
||||
defer screenStateLock.Unlock()
|
||||
|
||||
if currentScreen != screenName {
|
||||
displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen")
|
||||
switchToScreen(screenName)
|
||||
}
|
||||
}
|
||||
|
||||
func clearDisplayState() {
|
||||
screenStateLock.Lock()
|
||||
defer screenStateLock.Unlock()
|
||||
|
||||
displayedTexts = make(map[string]string)
|
||||
currentScreen = "ui_Boot_Screen"
|
||||
}
|
||||
|
||||
var (
|
||||
cloudBlinkLock sync.Mutex = sync.Mutex{}
|
||||
cloudBlinkStopped bool
|
||||
|
|
48
go.mod
48
go.mod
|
@ -1,35 +1,33 @@
|
|||
module github.com/jetkvm/kvm
|
||||
|
||||
go 1.23.4
|
||||
|
||||
toolchain go1.24.3
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.3.1
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/coder/websocket v1.8.13
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/creack/pty v1.1.23
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-contrib/logger v1.2.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/google/uuid v1.6.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-20250601184604-370a9a75f341
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0
|
||||
github.com/pion/logging v0.2.3
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/mdns/v2 v2.0.7
|
||||
github.com/pion/webrtc/v4 v4.0.16
|
||||
github.com/pion/webrtc/v4 v4.1.3
|
||||
github.com/pojntfx/go-nbd v0.3.2
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/common v0.62.0
|
||||
github.com/prometheus/common v0.65.0
|
||||
github.com/prometheus/procfs v0.16.1
|
||||
github.com/psanford/httpreadat v0.1.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
go.bug.st/serial v1.6.2
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.bug.st/serial v1.6.4
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/sys v0.33.0
|
||||
|
@ -39,15 +37,15 @@ replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20
|
|||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/creack/goselect v0.1.2 // indirect
|
||||
github.com/creack/goselect v0.1.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
|
@ -61,29 +59,29 @@ require (
|
|||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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.1 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/interceptor v0.1.40 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.18 // indirect
|
||||
github.com/pion/rtp v1.8.20 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.13 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.5 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.14 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.6 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/arch v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.24.0 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
88
go.sum
88
go.sum
|
@ -1,11 +1,11 @@
|
|||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||
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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
|
@ -18,13 +18,13 @@ github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
|
|||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/creack/goselect v0.1.3 h1:MaGNMclRo7P2Jl21hBpR1Cn33ITSbKP6E49RtfblLKc=
|
||||
github.com/creack/goselect v0.1.3/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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=
|
||||
|
@ -38,8 +38,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
|
|||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
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/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
@ -58,8 +58,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/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-20250601184604-370a9a75f341 h1:zPrkLSKi7kKJoNJH4uUmsQ86+0/QqpwEns0NyNLwKv0=
|
||||
github.com/gwatts/rootcerts v0.0.0-20250601184604-370a9a75f341/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs=
|
||||
github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
|
@ -99,8 +99,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
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/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
|
||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
|
@ -109,45 +109,45 @@ github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4=
|
|||
github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=
|
||||
github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
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/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU=
|
||||
github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=
|
||||
github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=
|
||||
github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=
|
||||
github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4=
|
||||
github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.5 h1:8XLB6Dt3QXkMkRFpoqC3314BemkpMQK2mZeJc4pUKqo=
|
||||
github.com/pion/srtp/v3 v3.0.5/go.mod h1:r1G7y5r1scZRLe2QJI/is+/O83W2d+JoEsuIexpw+uM=
|
||||
github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI=
|
||||
github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4=
|
||||
github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY=
|
||||
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/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps=
|
||||
github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs=
|
||||
github.com/pion/webrtc/v4 v4.0.16 h1:5f8QMVIbNvJr2mPRGi2QamkPa/LVUB6NWolOCwphKHA=
|
||||
github.com/pion/webrtc/v4 v4.0.16/go.mod h1:C3uTCPzVafUA0eUzru9f47OgNt3nEO7ZJ6zNY6VSJno=
|
||||
github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c=
|
||||
github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
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/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
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/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
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/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/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
|
@ -165,24 +165,24 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
|||
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/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
|
||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
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/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
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/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
@ -3,6 +3,7 @@ package confparser
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
@ -372,6 +373,10 @@ func (f *FieldConfig) validateField() error {
|
|||
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
||||
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
|
||||
}
|
||||
case "proxy":
|
||||
if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" {
|
||||
return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
||||
}
|
||||
|
|
|
@ -43,9 +43,11 @@ type testNetworkConfig struct {
|
|||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
|
||||
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||
}
|
||||
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
|
|
|
@ -3,6 +3,8 @@ package network
|
|||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/guregu/null/v6"
|
||||
|
@ -32,8 +34,9 @@ type IPv6StaticConfig struct {
|
|||
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
|
||||
}
|
||||
type NetworkConfig struct {
|
||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
|
||||
HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"`
|
||||
Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
|
||||
|
||||
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
||||
|
@ -45,9 +48,11 @@ type NetworkConfig struct {
|
|||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
|
||||
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
|
||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
||||
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
|
||||
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||
}
|
||||
|
||||
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
||||
|
@ -69,6 +74,18 @@ func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
|||
|
||||
return listenOptions
|
||||
}
|
||||
|
||||
func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||
return func(*http.Request) (*url.URL, error) {
|
||||
if s.HTTPProxy.String == "" {
|
||||
return nil, nil
|
||||
} else {
|
||||
proxyUrl, _ := url.Parse(s.HTTPProxy.String)
|
||||
return proxyUrl, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) GetHostname() string {
|
||||
hostname := ToValidHostname(s.config.Hostname.String)
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ type NetworkInterfaceState struct {
|
|||
ipv6Addr *net.IP
|
||||
ipv6Addresses []IPv6Address
|
||||
ipv6LinkLocal *net.IP
|
||||
ntpAddresses []*net.IP
|
||||
macAddr *net.HardwareAddr
|
||||
|
||||
l *zerolog.Logger
|
||||
|
@ -47,7 +48,7 @@ type NetworkInterfaceOptions struct {
|
|||
DefaultHostname string
|
||||
OnStateChange func(state *NetworkInterfaceState)
|
||||
OnInitialCheck func(state *NetworkInterfaceState)
|
||||
OnDhcpLeaseChange func(lease *udhcpc.Lease)
|
||||
OnDhcpLeaseChange func(lease *udhcpc.Lease, state *NetworkInterfaceState)
|
||||
OnConfigChange func(config *NetworkConfig)
|
||||
NetworkConfig *NetworkConfig
|
||||
}
|
||||
|
@ -76,6 +77,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
onInitialCheck: opts.OnInitialCheck,
|
||||
cbConfigChange: opts.OnConfigChange,
|
||||
config: opts.NetworkConfig,
|
||||
ntpAddresses: make([]*net.IP, 0),
|
||||
}
|
||||
|
||||
// create the dhcp client
|
||||
|
@ -89,10 +91,10 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
|
|||
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.updateNtpServersFromLease(lease)
|
||||
_ = s.setHostnameIfNotSame()
|
||||
|
||||
opts.OnDhcpLeaseChange(lease)
|
||||
opts.OnDhcpLeaseChange(lease, s)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -135,6 +137,27 @@ func (s *NetworkInterfaceState) IPv6String() string {
|
|||
return s.ipv6Addr.String()
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) NtpAddresses() []*net.IP {
|
||||
return s.ntpAddresses
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) NtpAddressesString() []string {
|
||||
ntpServers := []string{}
|
||||
|
||||
if s != nil {
|
||||
s.l.Debug().Any("s", s).Msg("getting NTP address strings")
|
||||
|
||||
if len(s.ntpAddresses) > 0 {
|
||||
for _, server := range s.ntpAddresses {
|
||||
s.l.Debug().IPAddr("server", *server).Msg("converting NTP address")
|
||||
ntpServers = append(ntpServers, server.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ntpServers
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
|
||||
return s.macAddr
|
||||
}
|
||||
|
@ -318,6 +341,25 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|||
return dhcpTargetState, nil
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error {
|
||||
if lease != nil && len(lease.NTPServers) > 0 {
|
||||
s.l.Info().Msg("lease found, updating DHCP NTP addresses")
|
||||
s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers))
|
||||
|
||||
for _, ntpServer := range lease.NTPServers {
|
||||
if ntpServer != nil {
|
||||
s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease")
|
||||
s.ntpAddresses = append(s.ntpAddresses, &ntpServer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.l.Info().Msg("no NTP servers found in lease")
|
||||
s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
||||
dhcpTargetState, err := s.update()
|
||||
if err != nil {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
@ -19,9 +20,9 @@ var defaultHTTPUrls = []string{
|
|||
// "http://www.msftconnecttest.com/connecttest.txt",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryAllHttpTime() (now *time.Time) {
|
||||
chunkSize := 4
|
||||
httpUrls := t.httpUrls
|
||||
func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) {
|
||||
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||
t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs")
|
||||
|
||||
// shuffle the http urls to avoid always querying the same servers
|
||||
rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] })
|
||||
|
@ -57,6 +58,7 @@ func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now
|
|||
ctx,
|
||||
url,
|
||||
timeout,
|
||||
t.networkConfig.GetTransportProxyFunc(),
|
||||
)
|
||||
duration := time.Since(startTime)
|
||||
|
||||
|
@ -122,10 +124,16 @@ func queryHttpTime(
|
|||
ctx context.Context,
|
||||
url string,
|
||||
timeout time.Duration,
|
||||
proxyFunc func(*http.Request) (*url.URL, error),
|
||||
) (now *time.Time, response *http.Response, err error) {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = proxyFunc
|
||||
|
||||
client := http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: transport,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
@ -73,6 +73,7 @@ var (
|
|||
},
|
||||
[]string{"url"},
|
||||
)
|
||||
|
||||
metricNtpServerInfo = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "jetkvm_timesync_ntp_server_info",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package timesync
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -8,22 +9,37 @@ import (
|
|||
"github.com/beevik/ntp"
|
||||
)
|
||||
|
||||
var defaultNTPServers = []string{
|
||||
var defaultNTPServerIPs = []string{
|
||||
// These servers are known by static IP and as such don't need DNS lookups
|
||||
// These are from Google and Cloudflare since if they're down, the internet
|
||||
// is broken anyway
|
||||
"162.159.200.1", // time.cloudflare.com IPv4
|
||||
"162.159.200.123", // time.cloudflare.com IPv4
|
||||
"2606:4700:f1::1", // time.cloudflare.com IPv6
|
||||
"2606:4700:f1::123", // time.cloudflare.com IPv6
|
||||
"216.239.35.0", // time.google.com IPv4
|
||||
"216.239.35.4", // time.google.com IPv4
|
||||
"216.239.35.8", // time.google.com IPv4
|
||||
"216.239.35.12", // time.google.com IPv4
|
||||
"2001:4860:4806::", // time.google.com IPv6
|
||||
"2001:4860:4806:4::", // time.google.com IPv6
|
||||
"2001:4860:4806:8::", // time.google.com IPv6
|
||||
"2001:4860:4806:c::", // time.google.com IPv6
|
||||
}
|
||||
|
||||
var defaultNTPServerHostnames = []string{
|
||||
// should use something from https://github.com/jauderho/public-ntp-servers
|
||||
"time.apple.com",
|
||||
"time.aws.com",
|
||||
"time.windows.com",
|
||||
"time.google.com",
|
||||
"162.159.200.123", // time.cloudflare.com IPv4
|
||||
"2606:4700:f1::123", // time.cloudflare.com IPv6
|
||||
"0.pool.ntp.org",
|
||||
"1.pool.ntp.org",
|
||||
"2.pool.ntp.org",
|
||||
"3.pool.ntp.org",
|
||||
"time.cloudflare.com",
|
||||
"pool.ntp.org",
|
||||
}
|
||||
|
||||
func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) {
|
||||
chunkSize := 4
|
||||
ntpServers := t.ntpServers
|
||||
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
|
||||
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
|
||||
t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers")
|
||||
|
||||
// shuffle the ntp servers to avoid always querying the same servers
|
||||
rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] })
|
||||
|
@ -46,6 +62,10 @@ type ntpResult struct {
|
|||
|
||||
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
|
||||
results := make(chan *ntpResult, len(servers))
|
||||
|
||||
_, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
for _, server := range servers {
|
||||
go func(server string) {
|
||||
scopedLogger := t.l.With().
|
||||
|
@ -66,15 +86,25 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
return
|
||||
}
|
||||
|
||||
if response.IsKissOfDeath() {
|
||||
scopedLogger.Warn().
|
||||
Str("kiss_code", response.KissCode).
|
||||
Msg("ignoring NTP server kiss of death")
|
||||
results <- nil
|
||||
return
|
||||
}
|
||||
|
||||
rtt := float64(response.RTT.Milliseconds())
|
||||
|
||||
// set the last RTT
|
||||
metricNtpServerLastRTT.WithLabelValues(
|
||||
server,
|
||||
).Set(float64(response.RTT.Milliseconds()))
|
||||
).Set(rtt)
|
||||
|
||||
// set the RTT histogram
|
||||
metricNtpServerRttHistogram.WithLabelValues(
|
||||
server,
|
||||
).Observe(float64(response.RTT.Milliseconds()))
|
||||
).Observe(rtt)
|
||||
|
||||
// set the server info
|
||||
metricNtpServerInfo.WithLabelValues(
|
||||
|
@ -91,10 +121,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
|
|||
scopedLogger.Info().
|
||||
Str("time", now.Format(time.RFC3339)).
|
||||
Str("reference", response.ReferenceString()).
|
||||
Str("rtt", response.RTT.String()).
|
||||
Float64("rtt", rtt).
|
||||
Str("clockOffset", response.ClockOffset.String()).
|
||||
Uint8("stratum", response.Stratum).
|
||||
Msg("NTP server returned time")
|
||||
|
||||
cancel()
|
||||
|
||||
results <- &ntpResult{
|
||||
now: now,
|
||||
offset: &response.ClockOffset,
|
||||
|
|
|
@ -28,9 +28,8 @@ type TimeSync struct {
|
|||
syncLock *sync.Mutex
|
||||
l *zerolog.Logger
|
||||
|
||||
ntpServers []string
|
||||
httpUrls []string
|
||||
networkConfig *network.NetworkConfig
|
||||
networkConfig *network.NetworkConfig
|
||||
dhcpNtpAddresses []string
|
||||
|
||||
rtcDevicePath string
|
||||
rtcDevice *os.File //nolint:unused
|
||||
|
@ -64,14 +63,13 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
}
|
||||
|
||||
t := &TimeSync{
|
||||
syncLock: &sync.Mutex{},
|
||||
l: opts.Logger,
|
||||
rtcDevicePath: rtcDevice,
|
||||
rtcLock: &sync.Mutex{},
|
||||
preCheckFunc: opts.PreCheckFunc,
|
||||
ntpServers: defaultNTPServers,
|
||||
httpUrls: defaultHTTPUrls,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
syncLock: &sync.Mutex{},
|
||||
l: opts.Logger,
|
||||
dhcpNtpAddresses: []string{},
|
||||
rtcDevicePath: rtcDevice,
|
||||
rtcLock: &sync.Mutex{},
|
||||
preCheckFunc: opts.PreCheckFunc,
|
||||
networkConfig: opts.NetworkConfig,
|
||||
}
|
||||
|
||||
if t.rtcDevicePath != "" {
|
||||
|
@ -82,34 +80,42 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|||
return t
|
||||
}
|
||||
|
||||
func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) {
|
||||
t.dhcpNtpAddresses = addresses
|
||||
}
|
||||
|
||||
func (t *TimeSync) getSyncMode() SyncMode {
|
||||
syncMode := SyncMode{
|
||||
Ntp: true,
|
||||
Http: true,
|
||||
Ordering: []string{"ntp_dhcp", "ntp", "http"},
|
||||
NtpUseFallback: true,
|
||||
HttpUseFallback: true,
|
||||
}
|
||||
var syncModeString string
|
||||
|
||||
if t.networkConfig != nil {
|
||||
syncModeString = t.networkConfig.TimeSyncMode.String
|
||||
switch t.networkConfig.TimeSyncMode.String {
|
||||
case "ntp_only":
|
||||
syncMode.Http = false
|
||||
case "http_only":
|
||||
syncMode.Ntp = false
|
||||
}
|
||||
|
||||
if t.networkConfig.TimeSyncDisableFallback.Bool {
|
||||
syncMode.NtpUseFallback = false
|
||||
syncMode.HttpUseFallback = false
|
||||
}
|
||||
|
||||
var syncOrdering = t.networkConfig.TimeSyncOrdering
|
||||
if len(syncOrdering) > 0 {
|
||||
syncMode.Ordering = syncOrdering
|
||||
}
|
||||
}
|
||||
|
||||
switch syncModeString {
|
||||
case "ntp_only":
|
||||
syncMode.Ntp = true
|
||||
case "http_only":
|
||||
syncMode.Http = true
|
||||
default:
|
||||
syncMode.Ntp = true
|
||||
syncMode.Http = true
|
||||
}
|
||||
t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode")
|
||||
|
||||
return syncMode
|
||||
}
|
||||
|
||||
func (t *TimeSync) doTimeSync() {
|
||||
metricTimeSyncStatus.Set(0)
|
||||
for {
|
||||
|
@ -154,16 +160,61 @@ func (t *TimeSync) Sync() error {
|
|||
offset *time.Duration
|
||||
)
|
||||
|
||||
syncMode := t.getSyncMode()
|
||||
|
||||
metricTimeSyncCount.Inc()
|
||||
|
||||
if syncMode.Ntp {
|
||||
now, offset = t.queryNetworkTime()
|
||||
}
|
||||
syncMode := t.getSyncMode()
|
||||
|
||||
if syncMode.Http && now == nil {
|
||||
now = t.queryAllHttpTime()
|
||||
Orders:
|
||||
for _, mode := range syncMode.Ordering {
|
||||
switch mode {
|
||||
case "ntp_user_provided":
|
||||
if syncMode.Ntp {
|
||||
t.l.Info().Msg("using NTP custom servers")
|
||||
now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "ntp_dhcp":
|
||||
if syncMode.Ntp {
|
||||
t.l.Info().Msg("using NTP servers from DHCP")
|
||||
now, offset = t.queryNetworkTime(t.dhcpNtpAddresses)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP DHCP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "ntp":
|
||||
if syncMode.Ntp && syncMode.NtpUseFallback {
|
||||
t.l.Info().Msg("using NTP fallback")
|
||||
now, offset = t.queryNetworkTime(defaultNTPServerIPs)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "NTP fallback").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "http_user_provided":
|
||||
if syncMode.Http {
|
||||
t.l.Info().Msg("using HTTP custom URLs")
|
||||
now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "HTTP").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
case "http":
|
||||
if syncMode.Http && syncMode.HttpUseFallback {
|
||||
t.l.Info().Msg("using HTTP fallback")
|
||||
now = t.queryAllHttpTime(defaultHTTPUrls)
|
||||
if now != nil {
|
||||
t.l.Info().Str("source", "HTTP fallback").Time("now", *now).Msg("time obtained")
|
||||
break Orders
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.l.Warn().Str("mode", mode).Msg("unknown time sync mode, skipping")
|
||||
}
|
||||
}
|
||||
|
||||
if now == nil {
|
||||
|
|
|
@ -30,8 +30,8 @@ var defaultGadgetConfig = map[string]gadgetConfigItem{
|
|||
attrs: gadgetAttributes{
|
||||
"bcdUSB": "0x0200", // USB 2.0
|
||||
"idVendor": "0x1d6b", // The Linux Foundation
|
||||
"idProduct": "0104", // Multifunction Composite Gadget
|
||||
"bcdDevice": "0100",
|
||||
"idProduct": "0x0104", // Multifunction Composite Gadget
|
||||
"bcdDevice": "0x0100", // USB2
|
||||
},
|
||||
configAttrs: gadgetAttributes{
|
||||
"MaxPower": "250", // in unit of 2mA
|
||||
|
|
|
@ -14,9 +14,10 @@ var keyboardConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb0"},
|
||||
configPath: []string{"hid.usb0"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "1",
|
||||
"subclass": "1",
|
||||
"report_length": "8",
|
||||
"protocol": "1",
|
||||
"subclass": "1",
|
||||
"report_length": "8",
|
||||
"no_out_endpoint": "0",
|
||||
},
|
||||
reportDesc: keyboardReportDesc,
|
||||
}
|
||||
|
@ -143,15 +144,21 @@ func (u *UsbGadget) listenKeyboardEvents() {
|
|||
default:
|
||||
l.Trace().Msg("reading from keyboard")
|
||||
if u.keyboardHidFile == nil {
|
||||
l.Error().Msg("keyboardHidFile is nil")
|
||||
u.logWithSuppression("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 {
|
||||
l.Error().Err(err).Msg("failed to read")
|
||||
u.logWithSuppression("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")
|
||||
|
@ -195,12 +202,12 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
|||
|
||||
_, err := u.keyboardHidFile.Write(data)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg0")
|
||||
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
|
||||
u.keyboardHidFile.Close()
|
||||
u.keyboardHidFile = nil
|
||||
return err
|
||||
}
|
||||
|
||||
u.resetLogSuppressionCounter("keyboardWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -11,9 +11,10 @@ var absoluteMouseConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb1"},
|
||||
configPath: []string{"hid.usb1"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"report_length": "6",
|
||||
"protocol": "2",
|
||||
"subclass": "0",
|
||||
"report_length": "6",
|
||||
"no_out_endpoint": "1",
|
||||
},
|
||||
reportDesc: absoluteMouseCombinedReportDesc,
|
||||
}
|
||||
|
@ -75,11 +76,12 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
|||
|
||||
_, err := u.absMouseHidFile.Write(data)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg1")
|
||||
u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
|
||||
u.absMouseHidFile.Close()
|
||||
u.absMouseHidFile = nil
|
||||
return err
|
||||
}
|
||||
u.resetLogSuppressionCounter("absMouseWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -11,9 +11,10 @@ var relativeMouseConfig = gadgetConfigItem{
|
|||
path: []string{"functions", "hid.usb2"},
|
||||
configPath: []string{"hid.usb2"},
|
||||
attrs: gadgetAttributes{
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"report_length": "4",
|
||||
"protocol": "2",
|
||||
"subclass": "1",
|
||||
"report_length": "4",
|
||||
"no_out_endpoint": "1",
|
||||
},
|
||||
reportDesc: relativeMouseCombinedReportDesc,
|
||||
}
|
||||
|
@ -65,11 +66,12 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
|||
|
||||
_, err := u.relMouseHidFile.Write(data)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("failed to write to hidg2")
|
||||
u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
|
||||
u.relMouseHidFile.Close()
|
||||
u.relMouseHidFile = nil
|
||||
return err
|
||||
}
|
||||
u.resetLogSuppressionCounter("relMouseWriteHidFile")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,9 @@ type UsbGadget struct {
|
|||
onKeyboardStateChange *func(state KeyboardState)
|
||||
|
||||
log *zerolog.Logger
|
||||
|
||||
logSuppressionCounter map[string]int
|
||||
logSuppressionLock sync.Mutex
|
||||
}
|
||||
|
||||
const configFSPath = "/sys/kernel/config"
|
||||
|
@ -126,6 +129,8 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
|||
|
||||
strictMode: config.strictMode,
|
||||
|
||||
logSuppressionCounter: make(map[string]int),
|
||||
|
||||
absMouseAccumulatedWheelY: 0,
|
||||
}
|
||||
if err := g.Init(); err != nil {
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func joinPath(basePath string, paths []string) string {
|
||||
|
@ -78,3 +80,33 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool)
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *UsbGadget) logWithSuppression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...interface{}) {
|
||||
u.logSuppressionLock.Lock()
|
||||
defer u.logSuppressionLock.Unlock()
|
||||
|
||||
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) {
|
||||
u.logSuppressionLock.Lock()
|
||||
defer u.logSuppressionLock.Unlock()
|
||||
|
||||
if _, ok := u.logSuppressionCounter[counterName]; !ok {
|
||||
u.logSuppressionCounter[counterName] = 0
|
||||
}
|
||||
}
|
||||
|
|
19
jsonrpc.go
19
jsonrpc.go
|
@ -681,10 +681,11 @@ func rpcResetConfig() error {
|
|||
}
|
||||
|
||||
type DCPowerState struct {
|
||||
IsOn bool `json:"isOn"`
|
||||
Voltage float64 `json:"voltage"`
|
||||
Current float64 `json:"current"`
|
||||
Power float64 `json:"power"`
|
||||
IsOn bool `json:"isOn"`
|
||||
Voltage float64 `json:"voltage"`
|
||||
Current float64 `json:"current"`
|
||||
Power float64 `json:"power"`
|
||||
RestoreState int `json:"restoreState"`
|
||||
}
|
||||
|
||||
func rpcGetDCPowerState() (DCPowerState, error) {
|
||||
|
@ -700,6 +701,15 @@ func rpcSetDCPowerState(enabled bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func rpcSetDCRestoreState(state int) error {
|
||||
logger.Info().Int("state", state).Msg("Setting DC restore state")
|
||||
err := setDCRestoreState(state)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set DC restore state: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rpcGetActiveExtension() (string, error) {
|
||||
return config.ActiveExtension, nil
|
||||
}
|
||||
|
@ -1088,6 +1098,7 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
||||
"getDCPowerState": {Func: rpcGetDCPowerState},
|
||||
"setDCPowerState": {Func: rpcSetDCPowerState, Params: []string{"enabled"}},
|
||||
"setDCRestoreState": {Func: rpcSetDCRestoreState, Params: []string{"state"}},
|
||||
"getActiveExtension": {Func: rpcGetActiveExtension},
|
||||
"setActiveExtension": {Func: rpcSetActiveExtension, Params: []string{"extensionId"}},
|
||||
"getATXState": {Func: rpcGetATXState},
|
||||
|
|
9
main.go
9
main.go
|
@ -96,16 +96,25 @@ func Main() {
|
|||
if !config.AutoUpdateEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
|
||||
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
|
||||
time.Sleep(30 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
if currentSession != nil {
|
||||
logger.Debug().Msg("skipping update since a session is active")
|
||||
time.Sleep(1 * time.Minute)
|
||||
continue
|
||||
}
|
||||
|
||||
includePreRelease := config.IncludePreRelease
|
||||
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to auto update")
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Hour)
|
||||
}
|
||||
}()
|
||||
|
|
125
native.go
125
native.go
|
@ -8,6 +8,8 @@ import (
|
|||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -41,6 +43,11 @@ var ongoingRequests = make(map[int32]chan *CtrlResponse)
|
|||
|
||||
var lock = &sync.Mutex{}
|
||||
|
||||
var (
|
||||
nativeCmd *exec.Cmd
|
||||
nativeCmdLock = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
@ -129,16 +136,26 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
|
|||
scopedLogger.Info().Msg("server listening")
|
||||
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
listener.Close()
|
||||
if err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
|
||||
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
|
||||
|
@ -235,6 +252,58 @@ 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
|
||||
|
||||
// reset the display state
|
||||
time.Sleep(1 * time.Second)
|
||||
clearDisplayState()
|
||||
updateStaticContents()
|
||||
requestDisplayUpdate(true)
|
||||
|
||||
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 {
|
||||
binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
|
||||
if err := ensureBinaryUpdated(binaryPath); err != nil {
|
||||
|
@ -246,12 +315,28 @@ func ExtractAndRunNativeBin() error {
|
|||
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||
}
|
||||
// Run the binary in the background
|
||||
cmd, err := startNativeBinary(binaryPath)
|
||||
cmd, err := startNativeBinaryWithLock(binaryPath)
|
||||
if err != nil {
|
||||
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() {
|
||||
<-appCtx.Done()
|
||||
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")
|
||||
|
@ -282,6 +367,22 @@ func shouldOverwrite(destPath string, srcHash []byte) bool {
|
|||
return !bytes.Equal(srcHash, dstHash)
|
||||
}
|
||||
|
||||
func getNativeSha256() ([]byte, error) {
|
||||
version, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func GetNativeVersion() (string, error) {
|
||||
version, err := getNativeSha256()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(version)), nil
|
||||
}
|
||||
|
||||
func ensureBinaryUpdated(destPath string) error {
|
||||
srcFile, err := resource.ResourceFS.Open("jetkvm_native")
|
||||
if err != nil {
|
||||
|
@ -289,7 +390,7 @@ func ensureBinaryUpdated(destPath string) error {
|
|||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256")
|
||||
srcHash, err := getNativeSha256()
|
||||
if err != nil {
|
||||
nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update")
|
||||
srcHash = nil
|
||||
|
|
30
network.go
30
network.go
|
@ -15,17 +15,35 @@ var (
|
|||
networkState *network.NetworkInterfaceState
|
||||
)
|
||||
|
||||
func networkStateChanged() {
|
||||
func networkStateChanged(isOnline bool) {
|
||||
// do not block the main thread
|
||||
go waitCtrlAndRequestDisplayUpdate(true)
|
||||
|
||||
if timeSync != nil {
|
||||
if networkState != nil {
|
||||
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
|
||||
}
|
||||
|
||||
if err := timeSync.Sync(); err != nil {
|
||||
networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
|
||||
}
|
||||
}
|
||||
|
||||
// always restart mDNS when the network state changes
|
||||
if mDNS != nil {
|
||||
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
|
||||
_ = mDNS.SetLocalNames([]string{
|
||||
networkState.GetHostname(),
|
||||
networkState.GetFQDN(),
|
||||
}, true)
|
||||
}
|
||||
|
||||
// if the network is now online, trigger an NTP sync if still needed
|
||||
if isOnline && timeSync != nil && (isTimeSyncNeeded() || !timeSync.IsSyncSuccess()) {
|
||||
if err := timeSync.Sync(); err != nil {
|
||||
logger.Warn().Str("error", err.Error()).Msg("unable to sync time on network state change")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initNetwork() error {
|
||||
|
@ -37,13 +55,13 @@ func initNetwork() error {
|
|||
NetworkConfig: config.NetworkConfig,
|
||||
Logger: networkLogger,
|
||||
OnStateChange: func(state *network.NetworkInterfaceState) {
|
||||
networkStateChanged()
|
||||
networkStateChanged(state.IsOnline())
|
||||
},
|
||||
OnInitialCheck: func(state *network.NetworkInterfaceState) {
|
||||
networkStateChanged()
|
||||
networkStateChanged(state.IsOnline())
|
||||
},
|
||||
OnDhcpLeaseChange: func(lease *udhcpc.Lease) {
|
||||
networkStateChanged()
|
||||
OnDhcpLeaseChange: func(lease *udhcpc.Lease, state *network.NetworkInterfaceState) {
|
||||
networkStateChanged(state.IsOnline())
|
||||
|
||||
if currentSession == nil {
|
||||
return
|
||||
|
@ -53,7 +71,7 @@ func initNetwork() error {
|
|||
},
|
||||
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
||||
config.NetworkConfig = networkConfig
|
||||
networkStateChanged()
|
||||
networkStateChanged(false)
|
||||
|
||||
if mDNS != nil {
|
||||
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
|
||||
|
|
14
ota.go
14
ota.go
|
@ -50,6 +50,10 @@ const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
|
|||
|
||||
var builtAppVersion = "0.1.0+dev"
|
||||
|
||||
func GetBuiltAppVersion() string {
|
||||
return builtAppVersion
|
||||
}
|
||||
|
||||
func GetLocalVersion() (systemVersion *semver.Version, appVersion *semver.Version, err error) {
|
||||
appVersion, err = semver.NewVersion(builtAppVersion)
|
||||
if err != nil {
|
||||
|
@ -89,7 +93,14 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease
|
|||
return nil, fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = config.NetworkConfig.GetTransportProxyFunc()
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error sending request: %w", err)
|
||||
}
|
||||
|
@ -135,6 +146,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
|||
client := http.Client{
|
||||
Timeout: 10 * time.Minute,
|
||||
Transport: &http.Transport{
|
||||
Proxy: config.NetworkConfig.GetTransportProxyFunc(),
|
||||
TLSHandshakeTimeout: 30 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: rootcerts.ServerCertPool(),
|
||||
|
|
43
serial.go
43
serial.go
|
@ -128,6 +128,7 @@ func pressATXResetButton(duration time.Duration) error {
|
|||
|
||||
func mountDCControl() error {
|
||||
_ = port.SetMode(defaultMode)
|
||||
registerDCMetrics()
|
||||
go runDCControl()
|
||||
return nil
|
||||
}
|
||||
|
@ -142,6 +143,7 @@ var dcState DCPowerState
|
|||
func runDCControl() {
|
||||
scopedLogger := serialLogger.With().Str("service", "dc_control").Logger()
|
||||
reader := bufio.NewReader(port)
|
||||
hasRestoreFeature := false
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
|
@ -151,7 +153,13 @@ func runDCControl() {
|
|||
|
||||
// Split the line by semicolon
|
||||
parts := strings.Split(strings.TrimSpace(line), ";")
|
||||
if len(parts) != 4 {
|
||||
if len(parts) == 5 {
|
||||
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension with restore feature")
|
||||
hasRestoreFeature = true
|
||||
} else if len(parts) == 4 {
|
||||
scopedLogger.Debug().Str("line", line).Msg("Detected DC extension without restore feature")
|
||||
hasRestoreFeature = false
|
||||
} else {
|
||||
scopedLogger.Warn().Str("line", line).Msg("Invalid line")
|
||||
continue
|
||||
}
|
||||
|
@ -163,6 +171,17 @@ func runDCControl() {
|
|||
continue
|
||||
}
|
||||
dcState.IsOn = powerState == 1
|
||||
if hasRestoreFeature {
|
||||
restoreState, err := strconv.Atoi(parts[4])
|
||||
if err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("Invalid restore state")
|
||||
continue
|
||||
}
|
||||
dcState.RestoreState = restoreState
|
||||
} else {
|
||||
// -1 means not supported
|
||||
dcState.RestoreState = -1
|
||||
}
|
||||
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
||||
if err != nil {
|
||||
scopedLogger.Warn().Err(err).Msg("Invalid voltage")
|
||||
|
@ -188,6 +207,9 @@ func runDCControl() {
|
|||
dcState.Current = amps
|
||||
dcState.Power = watts
|
||||
|
||||
// Update Prometheus metrics
|
||||
updateDCMetrics(dcState)
|
||||
|
||||
if currentSession != nil {
|
||||
writeJSONRPCEvent("dcState", dcState, currentSession)
|
||||
}
|
||||
|
@ -210,6 +232,25 @@ func setDCPowerState(on bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func setDCRestoreState(state int) error {
|
||||
_, err := port.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
command := "RESTORE_MODE_OFF\n"
|
||||
switch state {
|
||||
case 1:
|
||||
command = "RESTORE_MODE_ON\n"
|
||||
case 2:
|
||||
command = "RESTORE_MODE_LAST_STATE\n"
|
||||
}
|
||||
_, err = port.Write([]byte(command))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultMode = &serial.Mode{
|
||||
BaudRate: 115200,
|
||||
DataBits: 8,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,21 +19,21 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.3",
|
||||
"@headlessui/react": "^2.2.4",
|
||||
"@headlessui/tailwindcss": "^0.2.2",
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
||||
"@vitejs/plugin-basic-ssl": "^2.1.0",
|
||||
"@xterm/addon-clipboard": "^0.1.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-unicode11": "^0.8.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/addon-webgl": "^0.18.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"cva": "^1.0.0-beta.3",
|
||||
"cva": "^1.0.0-beta.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"focus-trap-react": "^11.0.3",
|
||||
"framer-motion": "^12.11.4",
|
||||
"focus-trap-react": "^11.0.4",
|
||||
"framer-motion": "^12.23.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"react": "^19.1.0",
|
||||
|
@ -42,42 +42,42 @@
|
|||
"react-hot-toast": "^2.5.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-simple-keyboard": "^3.8.72",
|
||||
"react-simple-keyboard": "^3.8.89",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^2.15.3",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"validator": "^13.15.0",
|
||||
"validator": "^13.15.15",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.9",
|
||||
"@eslint/compat": "^1.3.1",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@types/react": "^19.1.4",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/validator": "^13.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"@types/validator": "^13.15.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||
"@typescript-eslint/parser": "^8.35.1",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.1.0",
|
||||
"postcss": "^8.5.3",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.1.7",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.13",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
|
|
|
@ -262,23 +262,6 @@ export default function Actionbar({
|
|||
}}
|
||||
/>
|
||||
</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>
|
||||
<Button
|
||||
size="XS"
|
||||
|
|
|
@ -67,19 +67,19 @@ function Terminal({
|
|||
}) {
|
||||
const enableTerminal = useUiStore(state => state.terminalType == type);
|
||||
const setTerminalType = useUiStore(state => state.setTerminalType);
|
||||
const setDisableKeyboardFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
|
||||
const { instance, ref } = useXTerm({ options: TERMINAL_CONFIG });
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setDisableKeyboardFocusTrap(enableTerminal);
|
||||
setDisableVideoFocusTrap(enableTerminal);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
setDisableKeyboardFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
};
|
||||
}, [ref, instance, enableTerminal, setDisableKeyboardFocusTrap, type]);
|
||||
}, [enableTerminal, setDisableVideoFocusTrap]);
|
||||
|
||||
const readyState = dataChannel.readyState;
|
||||
useEffect(() => {
|
||||
|
@ -116,7 +116,7 @@ function Terminal({
|
|||
const { domEvent } = e;
|
||||
if (domEvent.key === "Escape") {
|
||||
setTerminalType("none");
|
||||
setDisableKeyboardFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
domEvent.preventDefault();
|
||||
}
|
||||
});
|
||||
|
@ -131,7 +131,7 @@ function Terminal({
|
|||
onDataHandler.dispose();
|
||||
onKeyHandler.dispose();
|
||||
};
|
||||
}, [instance, dataChannel, readyState, setDisableKeyboardFocusTrap, setTerminalType]);
|
||||
}, [dataChannel, instance, readyState, setDisableVideoFocusTrap, setTerminalType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instance) return;
|
||||
|
@ -158,7 +158,7 @@ function Terminal({
|
|||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [ref, instance]);
|
||||
}, [instance]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -115,9 +115,18 @@ export default function WebRTCVideo() {
|
|||
const isFullscreenEnabled = document.fullscreenEnabled;
|
||||
|
||||
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
|
||||
const name = permissionName as PermissionName;
|
||||
const { state } = await navigator.permissions.query({ name });
|
||||
return state === "granted";
|
||||
if (!navigator.permissions || !navigator.permissions.query) {
|
||||
return false; // if can't query permissions, assume NOT 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 () => {
|
||||
|
@ -128,7 +137,11 @@ export default function WebRTCVideo() {
|
|||
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
|
||||
|
||||
if (isPointerLockGranted && settings.mouseMode === "relative") {
|
||||
await videoElm.current.requestPointerLock();
|
||||
try {
|
||||
await videoElm.current.requestPointerLock();
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
}, [checkNavigatorPermissions, isPointerLockPossible, settings.mouseMode]);
|
||||
|
||||
|
@ -136,10 +149,13 @@ export default function WebRTCVideo() {
|
|||
if (videoElm.current === null) return;
|
||||
|
||||
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
|
||||
if (isKeyboardLockGranted) {
|
||||
if ("keyboard" in navigator) {
|
||||
|
||||
if (isKeyboardLockGranted && "keyboard" in navigator) {
|
||||
try {
|
||||
// @ts-expect-error - keyboard lock is not supported in all browsers
|
||||
await navigator.keyboard.lock();
|
||||
await navigator.keyboard.lock();
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
}, [checkNavigatorPermissions]);
|
||||
|
@ -148,8 +164,12 @@ export default function WebRTCVideo() {
|
|||
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
|
||||
|
||||
if ("keyboard" in navigator) {
|
||||
// @ts-expect-error - keyboard unlock is not supported in all browsers
|
||||
await navigator.keyboard.unlock();
|
||||
try {
|
||||
// @ts-expect-error - keyboard unlock is not supported in all browsers
|
||||
await navigator.keyboard.unlock();
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
@ -637,6 +657,16 @@ export default function WebRTCVideo() {
|
|||
return true;
|
||||
}, [isPlaying, isPointerLockActive, isPointerLockPossible, isVideoLoading, settings.mouseMode, videoHeight, videoWidth]);
|
||||
|
||||
// Conditionally set the filter style so we don't fallback to software rendering if these values are default of 1.0
|
||||
const videoStyle = useMemo(() => {
|
||||
const isDefault = videoSaturation === 1.0 && videoBrightness === 1.0 && videoContrast === 1.0;
|
||||
return isDefault
|
||||
? {} // No filter if all settings are default (1.0)
|
||||
: {
|
||||
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
|
||||
};
|
||||
}, [videoSaturation, videoBrightness, videoContrast]);
|
||||
|
||||
return (
|
||||
<div className="grid h-full w-full grid-rows-(--grid-layout)">
|
||||
<div className="flex min-h-[39.5px] flex-col">
|
||||
|
@ -671,17 +701,15 @@ export default function WebRTCVideo() {
|
|||
<div className="relative flex h-full w-full items-center justify-center">
|
||||
<video
|
||||
ref={videoElm}
|
||||
autoPlay={true}
|
||||
autoPlay
|
||||
controls={false}
|
||||
onPlaying={onVideoPlaying}
|
||||
onPlay={onVideoPlaying}
|
||||
muted={true}
|
||||
muted
|
||||
playsInline
|
||||
disablePictureInPicture
|
||||
controlsList="nofullscreen"
|
||||
style={{
|
||||
filter: `saturate(${videoSaturation}) brightness(${videoBrightness}) contrast(${videoContrast})`,
|
||||
}}
|
||||
style={videoStyle}
|
||||
className={cx(
|
||||
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
|
||||
{
|
||||
|
|
|
@ -8,12 +8,14 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
|
|||
import notifications from "@/notifications";
|
||||
import FieldLabel from "@components/FieldLabel";
|
||||
import LoadingSpinner from "@components/LoadingSpinner";
|
||||
import {SelectMenuBasic} from "@components/SelectMenuBasic";
|
||||
|
||||
interface DCPowerState {
|
||||
isOn: boolean;
|
||||
voltage: number;
|
||||
current: number;
|
||||
power: number;
|
||||
restoreState: number;
|
||||
}
|
||||
|
||||
export function DCPowerControl() {
|
||||
|
@ -43,6 +45,20 @@ export function DCPowerControl() {
|
|||
getDCPowerState(); // Refresh state after change
|
||||
});
|
||||
};
|
||||
const handleRestoreChange = (state: number) => {
|
||||
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
|
||||
send("setDCRestoreState", { state }, resp => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
getDCPowerState(); // Refresh state after change
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getDCPowerState();
|
||||
|
@ -63,7 +79,7 @@ export function DCPowerControl() {
|
|||
<LoadingSpinner className="h-6 w-6 text-blue-500 dark:text-blue-400" />
|
||||
</Card>
|
||||
) : (
|
||||
<Card className="h-[160px] animate-fadeIn opacity-0">
|
||||
<Card className="animate-fadeIn opacity-0">
|
||||
<div className="space-y-4 p-3">
|
||||
{/* Power Controls */}
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@ -84,6 +100,21 @@ export function DCPowerControl() {
|
|||
onClick={() => handlePowerToggle(false)}
|
||||
/>
|
||||
</div>
|
||||
{powerState.restoreState > -1 ? (
|
||||
<div className="flex items-center">
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label="Restore Power Loss"
|
||||
value={powerState.restoreState}
|
||||
onChange={e => handleRestoreChange(parseInt(e.target.value))}
|
||||
options={[
|
||||
{ value: '0', label: "Power OFF" },
|
||||
{ value: '1', label: "Power ON" },
|
||||
{ value: '2', label: "Last State" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<hr className="border-slate-700/30 dark:border-slate-600/30" />
|
||||
|
||||
{/* Status Display */}
|
||||
|
|
|
@ -10,11 +10,11 @@ import { SettingsPageHeader } from "@components/SettingsPageheader";
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { useHidStore, useRTCStore, useUiStore, useSettingsStore } from "@/hooks/stores";
|
||||
import { keys, modifiers } from "@/keyboardMappings";
|
||||
import { layouts, chars } from "@/keyboardLayouts";
|
||||
import { KeyStroke, KeyboardLayout, selectedKeyboard } from "@/keyboardLayouts";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
const hidKeyboardPayload = (keys: number[], modifier: number) => {
|
||||
return { keys, modifier };
|
||||
const hidKeyboardPayload = (modifier: number, keys: number[]) => {
|
||||
return { modifier, keys };
|
||||
};
|
||||
|
||||
const modifierCode = (shift?: boolean, altRight?: boolean) => {
|
||||
|
@ -39,11 +39,11 @@ export default function PasteModal() {
|
|||
state => state.setKeyboardLayout,
|
||||
);
|
||||
|
||||
// this ensures we always get the original en-US if it hasn't been set yet
|
||||
// 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";
|
||||
return "en_US";
|
||||
}, [keyboardLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -62,49 +62,56 @@ export default function PasteModal() {
|
|||
const onConfirmPaste = useCallback(async () => {
|
||||
setPasteMode(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
|
||||
if (rpcDataChannel?.readyState !== "open" || !TextAreaRef.current) return;
|
||||
if (!safeKeyboardLayout) return;
|
||||
if (!chars[safeKeyboardLayout]) return;
|
||||
const keyboard: KeyboardLayout = selectedKeyboard(safeKeyboardLayout);
|
||||
if (!keyboard) return;
|
||||
|
||||
const text = TextAreaRef.current.value;
|
||||
|
||||
try {
|
||||
for (const char of text) {
|
||||
const { key, shift, altRight, deadKey, accentKey } = chars[safeKeyboardLayout][char]
|
||||
const keyprops = keyboard.chars[char];
|
||||
if (!keyprops) continue;
|
||||
|
||||
const { key, shift, altRight, deadKey, accentKey } = keyprops;
|
||||
if (!key) continue;
|
||||
|
||||
const keyz = [ keys[key] ];
|
||||
const modz = [ modifierCode(shift, altRight) ];
|
||||
|
||||
if (deadKey) {
|
||||
keyz.push(keys["Space"]);
|
||||
modz.push(noModifier);
|
||||
}
|
||||
// if this is an accented character, we need to send that accent FIRST
|
||||
if (accentKey) {
|
||||
keyz.unshift(keys[accentKey.key])
|
||||
modz.unshift(modifierCode(accentKey.shift, accentKey.altRight))
|
||||
await sendKeystroke({modifier: modifierCode(accentKey.shift, accentKey.altRight), keys: [ keys[accentKey.key] ] })
|
||||
}
|
||||
|
||||
for (const [index, kei] of keyz.entries()) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload([kei], modz[index]),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
send("keyboardReport", hidKeyboardPayload([], 0), params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
// now send the actual key
|
||||
await sendKeystroke({ modifier: modifierCode(shift, altRight), keys: [ keys[key] ]});
|
||||
|
||||
// if what was requested was a dead key, we need to send an unmodified space to emit
|
||||
// just the accent character
|
||||
if (deadKey) {
|
||||
await sendKeystroke({ modifier: noModifier, keys: [ keys["Space"] ] });
|
||||
}
|
||||
|
||||
// now send a message with no keys down to "release" the keys
|
||||
await sendKeystroke({ modifier: 0, keys: [] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error("Failed to paste text:", error);
|
||||
notifications.error("Failed to paste text");
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, send, setDisableVideoFocusTrap, setPasteMode, safeKeyboardLayout]);
|
||||
|
||||
async function sendKeystroke(stroke: KeyStroke) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
send(
|
||||
"keyboardReport",
|
||||
hidKeyboardPayload(stroke.modifier, stroke.keys),
|
||||
params => {
|
||||
if ("error" in params) return reject(params.error);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [rpcDataChannel?.readyState, safeKeyboardLayout, send, setDisableVideoFocusTrap, setPasteMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (TextAreaRef.current) {
|
||||
|
@ -154,7 +161,7 @@ export default function PasteModal() {
|
|||
// @ts-expect-error TS doesn't recognize Intl.Segmenter in some environments
|
||||
[...new Intl.Segmenter().segment(value)]
|
||||
.map(x => x.segment)
|
||||
.filter(char => !chars[safeKeyboardLayout][char]),
|
||||
.filter(char => !selectedKeyboard(safeKeyboardLayout).chars[char]),
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -175,7 +182,7 @@ export default function PasteModal() {
|
|||
</div>
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
Sending text using keyboard layout: {layouts[safeKeyboardLayout]}
|
||||
Sending text using keyboard layout: {selectedKeyboard(safeKeyboardLayout).name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ import AddDeviceForm from "./AddDeviceForm";
|
|||
export default function WakeOnLanModal() {
|
||||
const [storedDevices, setStoredDevices] = useState<StoredDevice[]>([]);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const setDisableFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
const setDisableVideoFocusTrap = useUiStore(state => state.setDisableVideoFocusTrap);
|
||||
|
||||
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
|
||||
|
||||
|
@ -24,9 +24,9 @@ export default function WakeOnLanModal() {
|
|||
const [addDeviceErrorMessage, setAddDeviceErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const onCancelWakeOnLanModal = useCallback(() => {
|
||||
setDisableVideoFocusTrap(false);
|
||||
close();
|
||||
setDisableFocusTrap(false);
|
||||
}, [close, setDisableFocusTrap]);
|
||||
}, [close, setDisableVideoFocusTrap]);
|
||||
|
||||
const onSendMagicPacket = useCallback(
|
||||
(macAddress: string) => {
|
||||
|
@ -43,12 +43,12 @@ export default function WakeOnLanModal() {
|
|||
}
|
||||
} else {
|
||||
notifications.success("Magic Packet sent successfully");
|
||||
setDisableFocusTrap(false);
|
||||
setDisableVideoFocusTrap(false);
|
||||
close();
|
||||
}
|
||||
});
|
||||
},
|
||||
[close, rpcDataChannel?.readyState, send, setDisableFocusTrap],
|
||||
[close, rpcDataChannel?.readyState, send, setDisableVideoFocusTrap],
|
||||
);
|
||||
|
||||
const syncStoredDevices = useCallback(() => {
|
||||
|
@ -78,7 +78,7 @@ export default function WakeOnLanModal() {
|
|||
}
|
||||
});
|
||||
},
|
||||
[storedDevices, send, syncStoredDevices],
|
||||
[send, storedDevices, syncStoredDevices],
|
||||
);
|
||||
|
||||
const onAddDevice = useCallback(
|
||||
|
|
|
@ -308,9 +308,6 @@ interface SettingsState {
|
|||
keyboardLayout: string;
|
||||
setKeyboardLayout: (layout: string) => void;
|
||||
|
||||
actionBarCtrlAltDel: boolean;
|
||||
setActionBarCtrlAltDel: (enabled: boolean) => void;
|
||||
|
||||
keyboardLedSync: KeyboardLedSync;
|
||||
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
|
||||
|
||||
|
@ -359,9 +356,6 @@ export const useSettingsStore = create(
|
|||
keyboardLayout: "en-US",
|
||||
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
|
||||
|
||||
actionBarCtrlAltDel: false,
|
||||
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
|
||||
|
||||
keyboardLedSync: "auto",
|
||||
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
|
||||
|
||||
|
@ -753,6 +747,7 @@ export type TimeSyncMode =
|
|||
export interface NetworkSettings {
|
||||
hostname: string;
|
||||
domain: string;
|
||||
http_proxy: string;
|
||||
ipv4_mode: IPv4Mode;
|
||||
ipv6_mode: IPv6Mode;
|
||||
lldp_mode: LLDPMode;
|
||||
|
@ -941,5 +936,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
},
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -1,45 +1,32 @@
|
|||
import { chars as chars_fr_BE, name as name_fr_BE } from "@/keyboardLayouts/fr_BE"
|
||||
import { chars as chars_cs_CZ, name as name_cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||
import { chars as chars_en_UK, name as name_en_UK } from "@/keyboardLayouts/en_UK"
|
||||
import { chars as chars_en_US, name as name_en_US } from "@/keyboardLayouts/en_US"
|
||||
import { chars as chars_fr_FR, name as name_fr_FR } from "@/keyboardLayouts/fr_FR"
|
||||
import { chars as chars_de_DE, name as name_de_DE } from "@/keyboardLayouts/de_DE"
|
||||
import { chars as chars_it_IT, name as name_it_IT } from "@/keyboardLayouts/it_IT"
|
||||
import { chars as chars_nb_NO, name as name_nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||
import { chars as chars_es_ES, name as name_es_ES } from "@/keyboardLayouts/es_ES"
|
||||
import { chars as chars_sv_SE, name as name_sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
import { chars as chars_fr_CH, name as name_fr_CH } from "@/keyboardLayouts/fr_CH"
|
||||
import { chars as chars_de_CH, name as name_de_CH } from "@/keyboardLayouts/de_CH"
|
||||
export interface KeyStroke { modifier: number; keys: number[]; }
|
||||
export interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export interface KeyCombo extends KeyInfo { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
export interface KeyboardLayout { isoCode: string, name: string, chars: Record<string, KeyCombo> }
|
||||
|
||||
interface KeyInfo { key: string | number; shift?: boolean, altRight?: boolean }
|
||||
export type KeyCombo = KeyInfo & { deadKey?: boolean, accentKey?: KeyInfo }
|
||||
// to add a new layout, create a file like the above and add it to the list
|
||||
import { cs_CZ } from "@/keyboardLayouts/cs_CZ"
|
||||
import { de_CH } from "@/keyboardLayouts/de_CH"
|
||||
import { de_DE } from "@/keyboardLayouts/de_DE"
|
||||
import { en_US } from "@/keyboardLayouts/en_US"
|
||||
import { en_UK } from "@/keyboardLayouts/en_UK"
|
||||
import { es_ES } from "@/keyboardLayouts/es_ES"
|
||||
import { fr_BE } from "@/keyboardLayouts/fr_BE"
|
||||
import { fr_CH } from "@/keyboardLayouts/fr_CH"
|
||||
import { fr_FR } from "@/keyboardLayouts/fr_FR"
|
||||
import { it_IT } from "@/keyboardLayouts/it_IT"
|
||||
import { nb_NO } from "@/keyboardLayouts/nb_NO"
|
||||
import { sv_SE } from "@/keyboardLayouts/sv_SE"
|
||||
|
||||
export const layouts: Record<string, string> = {
|
||||
be_FR: name_fr_BE,
|
||||
cs_CZ: name_cs_CZ,
|
||||
en_UK: name_en_UK,
|
||||
en_US: name_en_US,
|
||||
fr_FR: name_fr_FR,
|
||||
de_DE: name_de_DE,
|
||||
it_IT: name_it_IT,
|
||||
nb_NO: name_nb_NO,
|
||||
es_ES: name_es_ES,
|
||||
sv_SE: name_sv_SE,
|
||||
fr_CH: name_fr_CH,
|
||||
de_CH: name_de_CH,
|
||||
}
|
||||
export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE ];
|
||||
|
||||
export const chars: Record<string, Record<string, KeyCombo>> = {
|
||||
be_FR: chars_fr_BE,
|
||||
cs_CZ: chars_cs_CZ,
|
||||
en_UK: chars_en_UK,
|
||||
en_US: chars_en_US,
|
||||
fr_FR: chars_fr_FR,
|
||||
de_DE: chars_de_DE,
|
||||
it_IT: chars_it_IT,
|
||||
nb_NO: chars_nb_NO,
|
||||
es_ES: chars_es_ES,
|
||||
sv_SE: chars_sv_SE,
|
||||
fr_CH: chars_fr_CH,
|
||||
de_CH: chars_de_CH,
|
||||
export const selectedKeyboard = (isoCode: string): KeyboardLayout => {
|
||||
// fallback to original behaviour of en-US if no isoCode given
|
||||
return keyboards.find(keyboard => keyboard.isoCode == isoCode)
|
||||
?? keyboards.find(keyboard => keyboard.isoCode == "en-US")!;
|
||||
};
|
||||
|
||||
export const keyboardOptions = () => {
|
||||
return keyboards.map((keyboard) => {
|
||||
return { label: keyboard.name, value: keyboard.isoCode }
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Čeština";
|
||||
const name = "Čeština";
|
||||
|
||||
const keyTrema = { key: "Backslash" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -13,7 +13,7 @@ const keyOverdot = { key: "Digit8", shift: true, altRight: true } // overdot (do
|
|||
const keyHook = { key: "Digit6", shift: true, altRight: true } // ogonoek (little hook), mark ˛ placed beneath a letter
|
||||
const keyCedille = { key: "Equal", shift: true, altRight: true } // accent cedille (cedilla), mark ¸ placed beneath a letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -242,3 +242,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const cs_CZ: KeyboardLayout = {
|
||||
isoCode: "cs-CZ",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Schwiizerdütsch";
|
||||
const name = "Schwiizerdütsch";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Minus", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "Equal" } // accent circonflexe (accent hat), mark ^ place
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Equal", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -163,3 +163,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const de_CH: KeyboardLayout = {
|
||||
isoCode: "de-CH",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Deutsch";
|
||||
const name = "Deutsch";
|
||||
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
const keyHat = { key: "Backquote" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
|
@ -150,3 +150,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const de_DE: KeyboardLayout = {
|
||||
isoCode: "de-DE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (UK)";
|
||||
const name = "English (UK)";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -105,3 +105,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>
|
||||
|
||||
export const en_UK: KeyboardLayout = {
|
||||
isoCode: "en-UK",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "English (US)";
|
||||
const name = "English (US)";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -111,3 +111,9 @@ export const chars = {
|
|||
Insert: { key: "Insert", shift: false },
|
||||
Delete: { key: "Delete", shift: false },
|
||||
} as Record<string, KeyCombo>
|
||||
|
||||
export const en_US: KeyboardLayout = {
|
||||
isoCode: "en-US",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Español";
|
||||
const name = "Español";
|
||||
|
||||
const keyTrema = { key: "Quote", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Quote" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "BracketRight" } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Key4", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -166,3 +166,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const es_ES: KeyboardLayout = {
|
||||
isoCode: "es-ES",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Belgisch Nederlands";
|
||||
const name = "Belgisch Nederlands";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyAcute = { key: "Semicolon", altRight: true } // accent aigu (acute acce
|
|||
const keyGrave = { key: "Quote", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "Slash", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
|
@ -165,3 +165,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_BE: KeyboardLayout = {
|
||||
isoCode: "fr-BE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,11 +1,11 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
import { chars as chars_de_CH } from "./de_CH"
|
||||
import { de_CH } from "./de_CH"
|
||||
|
||||
export const name = "Français de Suisse";
|
||||
const name = "Français de Suisse";
|
||||
|
||||
export const chars = {
|
||||
...chars_de_CH,
|
||||
const chars = {
|
||||
...de_CH.chars,
|
||||
"è": { key: "BracketLeft" },
|
||||
"ü": { key: "BracketLeft", shift: true },
|
||||
"é": { key: "Semicolon" },
|
||||
|
@ -13,3 +13,9 @@ export const chars = {
|
|||
"à": { key: "Quote" },
|
||||
"ä": { key: "Quote", shift: true },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_CH: KeyboardLayout = {
|
||||
isoCode: "fr-CH",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Français";
|
||||
const name = "Français";
|
||||
|
||||
const keyTrema = { key: "BracketLeft", shift: true } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyHat = { key: "BracketLeft" } // accent circonflexe (accent hat), mark ^ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyQ", shift: true },
|
||||
"Ä": { key: "KeyQ", shift: true, accentKey: keyTrema },
|
||||
"Â": { key: "KeyQ", shift: true, accentKey: keyHat },
|
||||
|
@ -137,3 +137,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const fr_FR: KeyboardLayout = {
|
||||
isoCode: "fr-FR",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Italiano";
|
||||
const name = "Italiano";
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
B: { key: "KeyB", shift: true },
|
||||
C: { key: "KeyC", shift: true },
|
||||
|
@ -111,3 +111,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const it_IT: KeyboardLayout = {
|
||||
isoCode: "it-IT",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Norsk bokmål";
|
||||
const name = "Norsk bokmål";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal", altRight: true } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Ä": { key: "KeyA", shift: true, accentKey: keyTrema },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
|
@ -165,3 +165,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const nb_NO: KeyboardLayout = {
|
||||
isoCode: "nb-NO",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyCombo } from "../keyboardLayouts"
|
||||
import { KeyboardLayout, KeyCombo } from "../keyboardLayouts"
|
||||
|
||||
export const name = "Svenska";
|
||||
const name = "Svenska";
|
||||
|
||||
const keyTrema = { key: "BracketRight" } // tréma (umlaut), two dots placed above a vowel
|
||||
const keyAcute = { key: "Equal" } // accent aigu (acute accent), mark ´ placed above the letter
|
||||
|
@ -8,7 +8,7 @@ const keyHat = { key: "BracketRight", shift: true } // accent circonflexe (accen
|
|||
const keyGrave = { key: "Equal", shift: true } // accent grave, mark ` placed above the letter
|
||||
const keyTilde = { key: "BracketRight", altRight: true } // tilde, mark ~ placed above the letter
|
||||
|
||||
export const chars = {
|
||||
const chars = {
|
||||
A: { key: "KeyA", shift: true },
|
||||
"Á": { key: "KeyA", shift: true, accentKey: keyAcute },
|
||||
"Â": { key: "KeyA", shift: true, accentKey: keyHat },
|
||||
|
@ -162,3 +162,9 @@ export const chars = {
|
|||
Enter: { key: "Enter" },
|
||||
Tab: { key: "Tab" },
|
||||
} as Record<string, KeyCombo>;
|
||||
|
||||
export const sv_SE: KeyboardLayout = {
|
||||
isoCode: "sv-SE",
|
||||
name: name,
|
||||
chars: chars
|
||||
};
|
|
@ -1,17 +1,19 @@
|
|||
// Key codes and modifiers correspond to definitions in the
|
||||
// [Linux USB HID gadget driver](https://www.kernel.org/doc/Documentation/usb/gadget_hid.txt)
|
||||
// [Section 10. Keyboard/Keypad Page 0x07](https://usb.org/sites/default/files/hut1_21.pdf)
|
||||
export const keys = {
|
||||
ArrowDown: 0x51,
|
||||
ArrowLeft: 0x50,
|
||||
ArrowRight: 0x4f,
|
||||
ArrowUp: 0x52,
|
||||
Backquote: 0x35,
|
||||
Backquote: 0x35, // aka Grave
|
||||
Backslash: 0x31,
|
||||
Backspace: 0x2a,
|
||||
BracketLeft: 0x2f,
|
||||
BracketRight: 0x30,
|
||||
BracketLeft: 0x2f, // aka LeftBrace
|
||||
BracketRight: 0x30, // aka RightBrace
|
||||
CapsLock: 0x39,
|
||||
Comma: 0x36,
|
||||
Compose: 0x65,
|
||||
ContextMenu: 0,
|
||||
Delete: 0x4c,
|
||||
Digit0: 0x27,
|
||||
|
@ -40,10 +42,21 @@ export const keys = {
|
|||
F10: 0x43,
|
||||
F11: 0x44,
|
||||
F12: 0x45,
|
||||
F13: 0x68,
|
||||
F14: 0x69,
|
||||
F15: 0x6a,
|
||||
F16: 0x6b,
|
||||
F17: 0x6c,
|
||||
F18: 0x6d,
|
||||
F19: 0x6e,
|
||||
F20: 0x6f,
|
||||
F21: 0x70,
|
||||
F22: 0x71,
|
||||
F23: 0x72,
|
||||
F24: 0x73,
|
||||
Home: 0x4a,
|
||||
HashTilde: 0x32, // non-US # and ~
|
||||
Insert: 0x49,
|
||||
IntlBackslash: 0x64,
|
||||
IntlBackslash: 0x64, // non-US \ and |
|
||||
KeyA: 0x04,
|
||||
KeyB: 0x05,
|
||||
KeyC: 0x06,
|
||||
|
@ -72,30 +85,35 @@ export const keys = {
|
|||
KeyZ: 0x1d,
|
||||
KeypadExclamation: 0xcf,
|
||||
Minus: 0x2d,
|
||||
NumLock: 0x53,
|
||||
Numpad0: 0x62,
|
||||
Numpad1: 0x59,
|
||||
Numpad2: 0x5a,
|
||||
Numpad3: 0x5b,
|
||||
Numpad4: 0x5c,
|
||||
None: 0x00,
|
||||
NumLock: 0x53, // and Clear
|
||||
Numpad0: 0x62, // and Insert
|
||||
Numpad1: 0x59, // and End
|
||||
Numpad2: 0x5a, // and Down Arrow
|
||||
Numpad3: 0x5b, // and Page Down
|
||||
Numpad4: 0x5c, // and Left Arrow
|
||||
Numpad5: 0x5d,
|
||||
Numpad6: 0x5e,
|
||||
Numpad7: 0x5f,
|
||||
Numpad8: 0x60,
|
||||
Numpad9: 0x61,
|
||||
Numpad6: 0x5e, // and Right Arrow
|
||||
Numpad7: 0x5f, // and Home
|
||||
Numpad8: 0x60, // and Up Arrow
|
||||
Numpad9: 0x61, // and Page Up
|
||||
NumpadAdd: 0x57,
|
||||
NumpadComma: 0x85,
|
||||
NumpadDecimal: 0x63,
|
||||
NumpadDivide: 0x54,
|
||||
NumpadEnter: 0x58,
|
||||
NumpadEqual: 0x67,
|
||||
NumpadLeftParen: 0xb6,
|
||||
NumpadMultiply: 0x55,
|
||||
NumpadRightParen: 0xb7,
|
||||
NumpadSubtract: 0x56,
|
||||
NumpadDecimal: 0x63,
|
||||
PageDown: 0x4e,
|
||||
PageUp: 0x4b,
|
||||
Period: 0x37,
|
||||
PrintScreen: 0x46,
|
||||
Pause: 0x48,
|
||||
Quote: 0x34,
|
||||
Power: 0x66,
|
||||
Quote: 0x34, // aka Single Quote or Apostrophe
|
||||
ScrollLock: 0x47,
|
||||
Semicolon: 0x33,
|
||||
Slash: 0x38,
|
||||
|
|
|
@ -42,6 +42,7 @@ import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
|
|||
import SettingsVideoRoute from "./routes/devices.$id.settings.video";
|
||||
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
|
||||
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
|
||||
import SettingsGeneralRebootRoute from "./routes/devices.$id.settings.general.reboot";
|
||||
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
|
||||
import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
|
||||
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
|
||||
|
@ -140,6 +141,10 @@ if (isOnDevice) {
|
|||
index: true,
|
||||
element: <SettingsGeneralIndexRoute.default />,
|
||||
},
|
||||
{
|
||||
path: "reboot",
|
||||
element: <SettingsGeneralRebootRoute />,
|
||||
},
|
||||
{
|
||||
path: "update",
|
||||
element: <SettingsGeneralUpdateRoute />,
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
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>
|
||||
);
|
||||
}
|
|
@ -92,6 +92,21 @@ export default function SettingsGeneralRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center justify-between gap-x-2">
|
||||
<SettingsItem
|
||||
title="Reboot Device"
|
||||
description="Power cycle the JetKVM"
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reboot Device"
|
||||
onClick={() => navigateTo("./reboot")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
|
||||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
const [send] = useJsonRpc();
|
||||
|
||||
const onConfirmUpdate = useCallback(() => {
|
||||
// This is where we send the RPC to the golang binary
|
||||
send("reboot", {force: true});
|
||||
}, [send]);
|
||||
|
||||
{
|
||||
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
|
||||
}
|
||||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
onClose,
|
||||
onConfirmUpdate,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onConfirmUpdate: () => void;
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
<div>
|
||||
<ConfirmationBox
|
||||
onYes={onConfirmUpdate}
|
||||
onNo={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmationBox({
|
||||
onYes,
|
||||
onNo,
|
||||
}: {
|
||||
onYes: () => void;
|
||||
onNo: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start space-y-4 text-left">
|
||||
<div className="text-left">
|
||||
<p className="text-base font-semibold text-black dark:text-white">
|
||||
Reboot JetKVM
|
||||
</p>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Do you want to proceed with rebooting the system?
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex gap-x-2">
|
||||
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
|
||||
<Button size="SM" theme="blank" text="No" onClick={onNo} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -116,15 +116,6 @@ export default function SettingsHardwareRoute() {
|
|||
}}
|
||||
/>
|
||||
</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 && (
|
||||
<>
|
||||
<SettingsItem
|
||||
|
|
|
@ -4,7 +4,7 @@ import { KeyboardLedSync, useSettingsStore } from "@/hooks/stores";
|
|||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import notifications from "@/notifications";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { layouts } from "@/keyboardLayouts";
|
||||
import { keyboardOptions } from "@/keyboardLayouts";
|
||||
import { Checkbox } from "@/components/Checkbox";
|
||||
|
||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||
|
@ -25,14 +25,14 @@ export default function SettingsKeyboardRoute() {
|
|||
state => state.setShowPressedKeys,
|
||||
);
|
||||
|
||||
// this ensures we always get the original en-US if it hasn't been set yet
|
||||
// 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";
|
||||
return "en_US";
|
||||
}, [keyboardLayout]);
|
||||
|
||||
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })
|
||||
const layoutOptions = keyboardOptions();
|
||||
const ledSyncOptions = [
|
||||
{ value: "auto", label: "Automatic" },
|
||||
{ value: "browser", label: "Browser Only" },
|
||||
|
@ -46,7 +46,7 @@ export default function SettingsKeyboardRoute() {
|
|||
if ("error" in resp) return;
|
||||
setKeyboardLayout(resp.result as string);
|
||||
});
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [send, setKeyboardLayout]);
|
||||
|
||||
const onKeyboardLayoutChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
|
|
|
@ -34,6 +34,7 @@ dayjs.extend(relativeTime);
|
|||
|
||||
const defaultNetworkSettings: NetworkSettings = {
|
||||
hostname: "",
|
||||
http_proxy: "",
|
||||
domain: "",
|
||||
ipv4_mode: "unknown",
|
||||
ipv6_mode: "unknown",
|
||||
|
@ -185,6 +186,10 @@ export default function SettingsNetworkRoute() {
|
|||
setNetworkSettings({ ...networkSettings, hostname: value });
|
||||
};
|
||||
|
||||
const handleProxyChange = (value: string) => {
|
||||
setNetworkSettings({ ...networkSettings, http_proxy: value });
|
||||
};
|
||||
|
||||
const handleDomainChange = (value: string) => {
|
||||
setNetworkSettings({ ...networkSettings, domain: value });
|
||||
};
|
||||
|
@ -253,6 +258,26 @@ export default function SettingsNetworkRoute() {
|
|||
</div>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="HTTP Proxy"
|
||||
description="Proxy server for outgoing HTTP(S) requests from the device. Blank for none."
|
||||
>
|
||||
<div className="relative">
|
||||
<div>
|
||||
<InputField
|
||||
size="SM"
|
||||
type="text"
|
||||
placeholder="http://proxy.example.com:8080/"
|
||||
defaultValue={networkSettings.http_proxy}
|
||||
onChange={e => {
|
||||
handleProxyChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
|
|
|
@ -79,7 +79,7 @@ export default function SettingsRoute() {
|
|||
return () => {
|
||||
setDisableVideoFocusTrap(false);
|
||||
};
|
||||
}, [setDisableVideoFocusTrap, sendKeyboardEvent]);
|
||||
}, [sendKeyboardEvent, setDisableVideoFocusTrap]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto max-w-4xl translate-x-0 transform text-left dark:text-white">
|
||||
|
@ -151,7 +151,6 @@ export default function SettingsRoute() {
|
|||
className={({ isActive }) => (isActive ? "active" : "")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
|
||||
<LuMouse className="h-4 w-4 shrink-0" />
|
||||
<h1>Mouse</h1>
|
||||
</div>
|
||||
|
@ -163,7 +162,7 @@ export default function SettingsRoute() {
|
|||
to="keyboard"
|
||||
className={({ isActive }) => (isActive ? "active" : "")}
|
||||
>
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 [.active_&]:bg-blue-50 [.active_&]:!text-blue-700 md:[.active_&]:bg-transparent dark:[.active_&]:bg-blue-900 dark:[.active_&]:!text-blue-200 dark:md:[.active_&]:bg-transparent">
|
||||
<div className="flex items-center gap-x-2 rounded-md px-2.5 py-2.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700 in-[.active]:bg-blue-50 in-[.active]:text-blue-700! md:in-[.active]:bg-transparent dark:in-[.active]:bg-blue-900 dark:in-[.active]:text-blue-200! dark:md:in-[.active]:bg-transparent">
|
||||
<LuKeyboard className="h-4 w-4 shrink-0" />
|
||||
<h1>Keyboard</h1>
|
||||
</div>
|
||||
|
|
|
@ -707,7 +707,7 @@ export default function KvmIdRoute() {
|
|||
}, [diskChannel, file]);
|
||||
|
||||
// System update
|
||||
const disableKeyboardFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||
const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap);
|
||||
|
||||
const [kvmTerminal, setKvmTerminal] = useState<RTCDataChannel | null>(null);
|
||||
const [serialConsole, setSerialConsole] = useState<RTCDataChannel | null>(null);
|
||||
|
@ -805,7 +805,7 @@ export default function KvmIdRoute() {
|
|||
)}
|
||||
<div className="relative h-full">
|
||||
<FocusTrap
|
||||
paused={disableKeyboardFocusTrap}
|
||||
paused={disableVideoFocusTrap}
|
||||
focusTrapOptions={{
|
||||
allowOutsideClick: true,
|
||||
escapeDeactivates: false,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"runtime"
|
||||
|
||||
"github.com/prometheus/common/version"
|
||||
)
|
||||
|
||||
var versionInfoTmpl = `
|
||||
JetKVM Application, version {{.version}} (branch: {{.branch}}, revision: {{.revision}})
|
||||
build date: {{.buildDate}}
|
||||
go version: {{.goVersion}}
|
||||
platform: {{.platform}}
|
||||
|
||||
{{if .nativeVersion}}
|
||||
JetKVM Native, version {{.nativeVersion}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
func GetVersionData(isJson bool) ([]byte, error) {
|
||||
version.Version = GetBuiltAppVersion()
|
||||
|
||||
m := map[string]string{
|
||||
"version": version.Version,
|
||||
"revision": version.GetRevision(),
|
||||
"branch": version.Branch,
|
||||
"buildDate": version.BuildDate,
|
||||
"goVersion": version.GoVersion,
|
||||
"platform": runtime.GOOS + "/" + runtime.GOARCH,
|
||||
}
|
||||
|
||||
nativeVersion, err := GetNativeVersion()
|
||||
if err == nil {
|
||||
m["nativeVersion"] = nativeVersion
|
||||
}
|
||||
|
||||
if isJson {
|
||||
jsonData, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
t := template.Must(template.New("version").Parse(versionInfoTmpl))
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := t.ExecuteTemplate(&buf, "version", m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
18
web.go
18
web.go
|
@ -97,9 +97,6 @@ func setupRouter() *gin.Engine {
|
|||
// We use this to determine if the device is setup
|
||||
r.GET("/device/status", handleDeviceStatus)
|
||||
|
||||
// We use this to provide the UI with the device configuration
|
||||
r.GET("/device/ui-config.js", handleDeviceUIConfig)
|
||||
|
||||
// We use this to setup the device in the welcome page
|
||||
r.POST("/device/setup", handleSetup)
|
||||
|
||||
|
@ -694,21 +691,6 @@ func handleCloudState(c *gin.Context) {
|
|||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func handleDeviceUIConfig(c *gin.Context) {
|
||||
config, _ := json.Marshal(gin.H{
|
||||
"CLOUD_API": config.CloudURL,
|
||||
"DEVICE_VERSION": builtAppVersion,
|
||||
})
|
||||
if config == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to marshal config"})
|
||||
return
|
||||
}
|
||||
|
||||
response := fmt.Sprintf("window.JETKVM_CONFIG = %s;", config)
|
||||
|
||||
c.Data(http.StatusOK, "text/javascript; charset=utf-8", []byte(response))
|
||||
}
|
||||
|
||||
func handleSetup(c *gin.Context) {
|
||||
// Check if the device is already set up
|
||||
if config.LocalAuthMode != "" || config.HashedPassword != "" {
|
||||
|
|
|
@ -108,6 +108,9 @@ func setTLSState(s TLSState) error {
|
|||
isChanged = true
|
||||
}
|
||||
// parse pem to cert and key
|
||||
if certStore == nil {
|
||||
initCertStore()
|
||||
}
|
||||
err, _ := certStore.ValidateAndSaveCertificate(webSecureCustomCertificateName, s.Certificate, s.PrivateKey, true)
|
||||
// warn doesn't matter as ... we don't know the hostname yet
|
||||
if err != nil {
|
||||
|
|
22
wol.go
22
wol.go
|
@ -4,6 +4,24 @@ import (
|
|||
"bytes"
|
||||
"encoding/binary"
|
||||
"net"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
wolPackets = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_wol_sent_packets_total",
|
||||
Help: "Total number of Wake-on-LAN magic packets sent.",
|
||||
},
|
||||
)
|
||||
wolErrors = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "jetkvm_wol_sent_packet_errors_total",
|
||||
Help: "Total number of Wake-on-LAN magic packets errors.",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
// SendWOLMagicPacket sends a Wake-on-LAN magic packet to the specified MAC address
|
||||
|
@ -11,6 +29,7 @@ func rpcSendWOLMagicPacket(macAddress string) error {
|
|||
// Parse the MAC address
|
||||
mac, err := net.ParseMAC(macAddress)
|
||||
if err != nil {
|
||||
wolErrors.Inc()
|
||||
return ErrorfL(wolLogger, "invalid MAC address", err)
|
||||
}
|
||||
|
||||
|
@ -20,6 +39,7 @@ func rpcSendWOLMagicPacket(macAddress string) error {
|
|||
// Set up UDP connection
|
||||
conn, err := net.Dial("udp", "255.255.255.255:9")
|
||||
if err != nil {
|
||||
wolErrors.Inc()
|
||||
return ErrorfL(wolLogger, "failed to establish UDP connection", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
@ -27,10 +47,12 @@ func rpcSendWOLMagicPacket(macAddress string) error {
|
|||
// Send the packet
|
||||
_, err = conn.Write(packet)
|
||||
if err != nil {
|
||||
wolErrors.Inc()
|
||||
return ErrorfL(wolLogger, "failed to send WOL packet", err)
|
||||
}
|
||||
|
||||
wolLogger.Info().Str("mac", macAddress).Msg("WOL packet sent")
|
||||
wolPackets.Inc()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue