mirror of https://github.com/jetkvm/kvm.git
Compare commits
17 Commits
a58eadee53
...
5452d7c721
| Author | SHA1 | Date |
|---|---|---|
|
|
5452d7c721 | |
|
|
3fbcb7e5c4 | |
|
|
76a1825b02 | |
|
|
4137b82661 | |
|
|
929e3a66d7 | |
|
|
101032b816 | |
|
|
6618ee4c6e | |
|
|
02bf869b99 | |
|
|
d824a1bc86 | |
|
|
ee29cb11bd | |
|
|
bf89e038ee | |
|
|
0b6be9b644 | |
|
|
1656420b3b | |
|
|
7e83e24e07 | |
|
|
58f42e0d16 | |
|
|
e748346e2b | |
|
|
43bf322c75 |
|
|
@ -4,7 +4,7 @@
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
// Should match what is defined in ui/package.json
|
// Should match what is defined in ui/package.json
|
||||||
"version": "22.15.0"
|
"version": "21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": [
|
"mounts": [
|
||||||
|
|
|
||||||
|
|
@ -19,33 +19,21 @@ jobs:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: v21.1.0
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: "**/package-lock.json"
|
cache-dependency-path: "**/package-lock.json"
|
||||||
- name: Set up Golang
|
- name: Set up Golang
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.24.3"
|
go-version: "1.24.0"
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
run: |
|
run: |
|
||||||
make frontend
|
make frontend
|
||||||
- name: Build application
|
- name: Build application
|
||||||
run: |
|
run: |
|
||||||
make build_dev
|
make build_dev
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
go test ./... -json > testreport.json
|
|
||||||
- name: Make test cases
|
|
||||||
run: |
|
|
||||||
make build_dev_test
|
|
||||||
- name: Golang Test Report
|
|
||||||
uses: becheran/go-testreport@v0.3.2
|
|
||||||
with:
|
|
||||||
input: "testreport.json"
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: jetkvm-app
|
name: jetkvm-app
|
||||||
path: |
|
path: bin/jetkvm_app
|
||||||
bin/jetkvm_app
|
|
||||||
device-tests.tar.gz
|
|
||||||
|
|
@ -26,12 +26,12 @@ jobs:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.24.3
|
go-version: 1.23.x
|
||||||
- name: Create empty resource directory
|
- name: Create empty resource directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p static && touch static/.gitkeep
|
mkdir -p static && touch static/.gitkeep
|
||||||
- name: Lint
|
- name: Lint
|
||||||
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
|
uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
|
||||||
with:
|
with:
|
||||||
args: --verbose
|
args: --verbose
|
||||||
version: v2.0.2
|
version: v1.62.0
|
||||||
|
|
|
||||||
|
|
@ -69,54 +69,12 @@ jobs:
|
||||||
CI_USER: ${{ vars.JETKVM_CI_USER }}
|
CI_USER: ${{ vars.JETKVM_CI_USER }}
|
||||||
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
||||||
CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }}
|
CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }}
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
set -e
|
|
||||||
echo "+ Copying device-tests.tar.gz to remote host"
|
|
||||||
ssh jkci "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
|
||||||
echo "+ Running go tests"
|
|
||||||
ssh jkci ash << 'EOF'
|
|
||||||
set -e
|
|
||||||
TMP_DIR=$(mktemp -d)
|
|
||||||
cd ${TMP_DIR}
|
|
||||||
tar zxf /tmp/device-tests.tar.gz
|
|
||||||
./gotestsum --format=testdox \
|
|
||||||
--jsonfile=/tmp/device-tests.json \
|
|
||||||
--post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \
|
|
||||||
--raw-command -- ./run_all_tests -json
|
|
||||||
|
|
||||||
GOTESTSUM_EXIT_CODE=$?
|
|
||||||
if [ $GOTESTSUM_EXIT_CODE -ne 0 ]; then
|
|
||||||
echo "❌ Tests failed (exit code: $GOTESTSUM_EXIT_CODE)"
|
|
||||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TESTS_FAILED=$(cat /tmp/device-tests.failed)
|
|
||||||
if [ "$TESTS_FAILED" -ne 0 ]; then
|
|
||||||
echo "❌ Tests failed $TESTS_FAILED tests failed"
|
|
||||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Tests passed"
|
|
||||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
|
||||||
EOF
|
|
||||||
ssh jkci "cat /tmp/device-tests.json" > device-tests.json
|
|
||||||
- name: Set up Golang
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: "1.24.3"
|
|
||||||
- name: Golang Test Report
|
|
||||||
uses: becheran/go-testreport@v0.3.2
|
|
||||||
with:
|
|
||||||
input: "device-tests.json"
|
|
||||||
- name: Deploy application
|
- name: Deploy application
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
# Copy the binary to the remote host
|
# Copy the binary to the remote host
|
||||||
echo "+ Copying the application to the remote host"
|
echo "+ Copying the application to the remote host"
|
||||||
cat bin/jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz"
|
cat jetkvm_app | gzip | ssh jkci "cat > /userdata/jetkvm/jetkvm_app.update.gz"
|
||||||
# Deploy and run the application on the remote host
|
# Deploy and run the application on the remote host
|
||||||
echo "+ Deploying the application on the remote host"
|
echo "+ Deploying the application on the remote host"
|
||||||
ssh jkci ash <<EOF
|
ssh jkci ash <<EOF
|
||||||
|
|
@ -150,25 +108,15 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "+ Checking the status of the device"
|
echo "+ Checking the status of the device"
|
||||||
curl -v http://$CI_HOST/device/status && echo
|
curl -v http://$CI_HOST/device/status && echo
|
||||||
echo "+ Waiting for 15 seconds to allow all services to start"
|
echo "+ Waiting for 10 seconds to allow all services to start"
|
||||||
sleep 15
|
sleep 10
|
||||||
echo "+ Collecting logs"
|
echo "+ Collecting logs"
|
||||||
local_log_tar=$(mktemp)
|
ssh jkci "cat /userdata/jetkvm/last.log" > last.log
|
||||||
ssh jkci ash > $local_log_tar <<'EOF'
|
cat last.log
|
||||||
log_path=$(mktemp -d)
|
|
||||||
dmesg > $log_path/dmesg.log
|
|
||||||
cp /userdata/jetkvm/last.log $log_path/last.log
|
|
||||||
tar -czf - -C $log_path .
|
|
||||||
EOF
|
|
||||||
tar -xf $local_log_tar
|
|
||||||
cat dmesg.log last.log
|
|
||||||
env:
|
env:
|
||||||
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
||||||
- name: Upload logs
|
- name: Upload logs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: device-logs
|
name: device-logs
|
||||||
path: |
|
path: last.log
|
||||||
last.log
|
|
||||||
dmesg.log
|
|
||||||
device-tests.json
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
bin/*
|
bin/*
|
||||||
static/*
|
static/*
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
device-tests.tar.gz
|
|
||||||
|
|
@ -1,41 +1,22 @@
|
||||||
version: "2"
|
---
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- forbidigo
|
- forbidigo
|
||||||
- misspell
|
- goimports
|
||||||
- whitespace
|
- misspell
|
||||||
- gochecknoinits
|
# - revive
|
||||||
settings:
|
- whitespace
|
||||||
forbidigo:
|
|
||||||
forbid:
|
issues:
|
||||||
- pattern: ^fmt\.Print.*$
|
exclude-rules:
|
||||||
msg: Do not commit print statements. Use logger package.
|
- path: _test.go
|
||||||
- pattern: ^log\.(Fatal|Panic|Print)(f|ln)?.*$
|
linters:
|
||||||
msg: Do not commit log statements. Use logger package.
|
- errcheck
|
||||||
exclusions:
|
|
||||||
generated: lax
|
linters-settings:
|
||||||
presets:
|
forbidigo:
|
||||||
- comments
|
forbid:
|
||||||
- common-false-positives
|
- p: ^fmt\.Print.*$
|
||||||
- legacy
|
msg: Do not commit print statements. Use logger package.
|
||||||
- std-error-handling
|
- p: ^log\.(Fatal|Panic|Print)(f|ln)?.*$
|
||||||
rules:
|
msg: Do not commit log statements. Use logger package.
|
||||||
- linters:
|
|
||||||
- errcheck
|
|
||||||
path: _test.go
|
|
||||||
- linters:
|
|
||||||
- gochecknoinits
|
|
||||||
path: internal/logging/sse.go
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
formatters:
|
|
||||||
enable:
|
|
||||||
- goimports
|
|
||||||
exclusions:
|
|
||||||
generated: lax
|
|
||||||
paths:
|
|
||||||
- third_party$
|
|
||||||
- builtin$
|
|
||||||
- examples$
|
|
||||||
|
|
|
||||||
47
Makefile
47
Makefile
|
|
@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
|
||||||
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
BUILDDATE ?= $(shell date -u +%FT%T%z)
|
||||||
BUILDTS ?= $(shell date -u +%s)
|
BUILDTS ?= $(shell date -u +%s)
|
||||||
REVISION ?= $(shell git rev-parse HEAD)
|
REVISION ?= $(shell git rev-parse HEAD)
|
||||||
VERSION_DEV := 0.4.3-dev$(shell date +%Y%m%d%H%M)
|
VERSION_DEV := 0.4.0-dev$(shell date +%Y%m%d%H%M)
|
||||||
VERSION := 0.4.2
|
VERSION := 0.3.9
|
||||||
|
|
||||||
PROMETHEUS_TAG := github.com/prometheus/common/version
|
PROMETHEUS_TAG := github.com/prometheus/common/version
|
||||||
KVM_PKG_NAME := github.com/jetkvm/kvm
|
KVM_PKG_NAME := github.com/jetkvm/kvm
|
||||||
|
|
@ -15,48 +15,12 @@ GO_LDFLAGS := \
|
||||||
-X $(PROMETHEUS_TAG).Revision=$(REVISION) \
|
-X $(PROMETHEUS_TAG).Revision=$(REVISION) \
|
||||||
-X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS)
|
-X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS)
|
||||||
|
|
||||||
GO_CMD := GOOS=linux GOARCH=arm GOARM=7 go
|
|
||||||
BIN_DIR := $(shell pwd)/bin
|
|
||||||
|
|
||||||
TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u)
|
|
||||||
|
|
||||||
hash_resource:
|
hash_resource:
|
||||||
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
|
||||||
|
|
||||||
build_dev: hash_resource
|
build_dev: hash_resource
|
||||||
@echo "Building..."
|
@echo "Building..."
|
||||||
$(GO_CMD) build \
|
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" -o bin/jetkvm_app cmd/main.go
|
||||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
|
||||||
-trimpath \
|
|
||||||
-o $(BIN_DIR)/jetkvm_app cmd/main.go
|
|
||||||
|
|
||||||
build_test2json:
|
|
||||||
$(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json
|
|
||||||
|
|
||||||
build_gotestsum:
|
|
||||||
@echo "Building gotestsum..."
|
|
||||||
$(GO_CMD) install gotest.tools/gotestsum@latest
|
|
||||||
cp $(shell $(GO_CMD) env GOPATH)/bin/linux_arm/gotestsum $(BIN_DIR)/gotestsum
|
|
||||||
|
|
||||||
build_dev_test: build_test2json build_gotestsum
|
|
||||||
# collect all directories that contain tests
|
|
||||||
@echo "Building tests for devices ..."
|
|
||||||
@rm -rf $(BIN_DIR)/tests && mkdir -p $(BIN_DIR)/tests
|
|
||||||
|
|
||||||
@cat resource/dev_test.sh > $(BIN_DIR)/tests/run_all_tests
|
|
||||||
@for test in $(TEST_DIRS); do \
|
|
||||||
test_pkg_name=$$(echo $$test | sed 's/^.\///g'); \
|
|
||||||
test_pkg_full_name=$(KVM_PKG_NAME)/$$(echo $$test | sed 's/^.\///g'); \
|
|
||||||
test_filename=$$(echo $$test_pkg_name | sed 's/\//__/g')_test; \
|
|
||||||
$(GO_CMD) test -v \
|
|
||||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \
|
|
||||||
-c -o $(BIN_DIR)/tests/$$test_filename $$test; \
|
|
||||||
echo "runTest ./$$test_filename $$test_pkg_full_name" >> $(BIN_DIR)/tests/run_all_tests; \
|
|
||||||
done; \
|
|
||||||
chmod +x $(BIN_DIR)/tests/run_all_tests; \
|
|
||||||
cp $(BIN_DIR)/test2json $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/test2json; \
|
|
||||||
cp $(BIN_DIR)/gotestsum $(BIN_DIR)/tests/ && chmod +x $(BIN_DIR)/tests/gotestsum; \
|
|
||||||
tar czfv device-tests.tar.gz -C $(BIN_DIR)/tests .
|
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
cd ui && npm ci && npm run build:device
|
cd ui && npm ci && npm run build:device
|
||||||
|
|
@ -69,10 +33,7 @@ dev_release: frontend build_dev
|
||||||
|
|
||||||
build_release: frontend hash_resource
|
build_release: frontend hash_resource
|
||||||
@echo "Building release..."
|
@echo "Building release..."
|
||||||
$(GO_CMD) build \
|
GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" -o bin/jetkvm_app cmd/main.go
|
||||||
-ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION)" \
|
|
||||||
-trimpath \
|
|
||||||
-o bin/jetkvm_app cmd/main.go
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \
|
@if rclone lsf r2://jetkvm-update/app/$(VERSION)/ | grep -q "jetkvm_app"; then \
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@
|
||||||
|
|
||||||
[](https://twitter.com/jetkvm)
|
[](https://twitter.com/jetkvm)
|
||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/jetkvm/kvm)
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively.
|
JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively.
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/pojntfx/go-nbd/pkg/client"
|
||||||
"github.com/pojntfx/go-nbd/pkg/server"
|
"github.com/pojntfx/go-nbd/pkg/server"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type remoteImageBackend struct {
|
type remoteImageBackend struct {
|
||||||
|
|
@ -16,8 +16,8 @@ type remoteImageBackend struct {
|
||||||
|
|
||||||
func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) {
|
func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) {
|
||||||
virtualMediaStateMutex.RLock()
|
virtualMediaStateMutex.RLock()
|
||||||
logger.Debug().Interface("currentVirtualMediaState", currentVirtualMediaState).Msg("currentVirtualMediaState")
|
logger.Debugf("currentVirtualMediaState is %v", currentVirtualMediaState)
|
||||||
logger.Debug().Int64("read size", int64(len(p))).Int64("off", off).Msg("read size and off")
|
logger.Debugf("read size: %d, off: %d", len(p), off)
|
||||||
if currentVirtualMediaState == nil {
|
if currentVirtualMediaState == nil {
|
||||||
return 0, errors.New("image not mounted")
|
return 0, errors.New("image not mounted")
|
||||||
}
|
}
|
||||||
|
|
@ -33,17 +33,16 @@ func (r remoteImageBackend) ReadAt(p []byte, off int64) (n int, err error) {
|
||||||
readLen = mountedImageSize - off
|
readLen = mountedImageSize - off
|
||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
switch source {
|
if source == WebRTC {
|
||||||
case WebRTC:
|
|
||||||
data, err = webRTCDiskReader.Read(ctx, off, readLen)
|
data, err = webRTCDiskReader.Read(ctx, off, readLen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
n = copy(p, data)
|
n = copy(p, data)
|
||||||
return n, nil
|
return n, nil
|
||||||
case HTTP:
|
} else if source == HTTP {
|
||||||
return httpRangeReader.ReadAt(p, off)
|
return httpRangeReader.ReadAt(p, off)
|
||||||
default:
|
} else {
|
||||||
return 0, errors.New("unknown image source")
|
return 0, errors.New("unknown image source")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,8 +72,6 @@ type NBDDevice struct {
|
||||||
serverConn net.Conn
|
serverConn net.Conn
|
||||||
clientConn net.Conn
|
clientConn net.Conn
|
||||||
dev *os.File
|
dev *os.File
|
||||||
|
|
||||||
l *zerolog.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNBDDevice() *NBDDevice {
|
func NewNBDDevice() *NBDDevice {
|
||||||
|
|
@ -93,18 +90,10 @@ func (d *NBDDevice) Start() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.l == nil {
|
|
||||||
scopedLogger := nbdLogger.With().
|
|
||||||
Str("socket_path", nbdSocketPath).
|
|
||||||
Str("device_path", nbdDevicePath).
|
|
||||||
Logger()
|
|
||||||
d.l = &scopedLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the socket file if it already exists
|
// Remove the socket file if it already exists
|
||||||
if _, err := os.Stat(nbdSocketPath); err == nil {
|
if _, err := os.Stat(nbdSocketPath); err == nil {
|
||||||
if err := os.Remove(nbdSocketPath); err != nil {
|
if err := os.Remove(nbdSocketPath); err != nil {
|
||||||
d.l.Error().Err(err).Msg("failed to remove existing socket file")
|
logger.Errorf("Failed to remove existing socket file %s: %v", nbdSocketPath, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +134,32 @@ func (d *NBDDevice) runServerConn() {
|
||||||
MaximumBlockSize: uint32(16 * 1024),
|
MaximumBlockSize: uint32(16 * 1024),
|
||||||
SupportsMultiConn: false,
|
SupportsMultiConn: false,
|
||||||
})
|
})
|
||||||
|
logger.Infof("nbd server exited: %v", err)
|
||||||
d.l.Info().Err(err).Msg("nbd server exited")
|
}
|
||||||
|
|
||||||
|
func (d *NBDDevice) runClientConn() {
|
||||||
|
err := client.Connect(d.clientConn, d.dev, &client.Options{
|
||||||
|
ExportName: "jetkvm",
|
||||||
|
BlockSize: uint32(4 * 1024),
|
||||||
|
})
|
||||||
|
logger.Infof("nbd client exited: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *NBDDevice) Close() {
|
||||||
|
if d.dev != nil {
|
||||||
|
err := client.Disconnect(d.dev)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("error disconnecting nbd client: %v", err)
|
||||||
|
}
|
||||||
|
_ = d.dev.Close()
|
||||||
|
}
|
||||||
|
if d.listener != nil {
|
||||||
|
_ = d.listener.Close()
|
||||||
|
}
|
||||||
|
if d.clientConn != nil {
|
||||||
|
_ = d.clientConn.Close()
|
||||||
|
}
|
||||||
|
if d.serverConn != nil {
|
||||||
|
_ = d.serverConn.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pojntfx/go-nbd/pkg/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *NBDDevice) runClientConn() {
|
|
||||||
err := client.Connect(d.clientConn, d.dev, &client.Options{
|
|
||||||
ExportName: "jetkvm",
|
|
||||||
BlockSize: uint32(4 * 1024),
|
|
||||||
})
|
|
||||||
d.l.Info().Err(err).Msg("nbd client exited")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NBDDevice) Close() {
|
|
||||||
if d.dev != nil {
|
|
||||||
err := client.Disconnect(d.dev)
|
|
||||||
if err != nil {
|
|
||||||
d.l.Warn().Err(err).Msg("error disconnecting nbd client")
|
|
||||||
}
|
|
||||||
_ = d.dev.Close()
|
|
||||||
}
|
|
||||||
if d.listener != nil {
|
|
||||||
_ = d.listener.Close()
|
|
||||||
}
|
|
||||||
if d.clientConn != nil {
|
|
||||||
_ = d.clientConn.Close()
|
|
||||||
}
|
|
||||||
if d.serverConn != nil {
|
|
||||||
_ = d.serverConn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
//go:build !linux
|
|
||||||
|
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *NBDDevice) runClientConn() {
|
|
||||||
d.l.Error().Msg("platform not supported")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NBDDevice) Close() {
|
|
||||||
d.l.Error().Msg("platform not supported")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
113
cloud.go
113
cloud.go
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coder/websocket/wsjson"
|
"github.com/coder/websocket/wsjson"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
||||||
|
|
@ -20,7 +19,6 @@ import (
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CloudRegisterRequest struct {
|
type CloudRegisterRequest struct {
|
||||||
|
|
@ -139,40 +137,11 @@ var (
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
type CloudConnectionState uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
CloudConnectionStateNotConfigured CloudConnectionState = iota
|
|
||||||
CloudConnectionStateDisconnected
|
|
||||||
CloudConnectionStateConnecting
|
|
||||||
CloudConnectionStateConnected
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cloudConnectionState CloudConnectionState = CloudConnectionStateNotConfigured
|
|
||||||
cloudConnectionStateLock = &sync.Mutex{}
|
|
||||||
|
|
||||||
cloudDisconnectChan chan error
|
cloudDisconnectChan chan error
|
||||||
cloudDisconnectLock = &sync.Mutex{}
|
cloudDisconnectLock = &sync.Mutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func setCloudConnectionState(state CloudConnectionState) {
|
|
||||||
cloudConnectionStateLock.Lock()
|
|
||||||
defer cloudConnectionStateLock.Unlock()
|
|
||||||
|
|
||||||
if cloudConnectionState == CloudConnectionStateDisconnected &&
|
|
||||||
(config.CloudToken == "" || config.CloudURL == "") {
|
|
||||||
state = CloudConnectionStateNotConfigured
|
|
||||||
}
|
|
||||||
|
|
||||||
previousState := cloudConnectionState
|
|
||||||
cloudConnectionState = state
|
|
||||||
|
|
||||||
go waitCtrlAndRequestDisplayUpdate(
|
|
||||||
previousState != state,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func wsResetMetrics(established bool, sourceType string, source string) {
|
func wsResetMetrics(established bool, sourceType string, source string) {
|
||||||
metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1)
|
metricConnectionLastPingTimestamp.WithLabelValues(sourceType, source).Set(-1)
|
||||||
metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1)
|
metricConnectionLastPingDuration.WithLabelValues(sourceType, source).Set(-1)
|
||||||
|
|
@ -284,14 +253,14 @@ func disconnectCloud(reason error) {
|
||||||
defer cloudDisconnectLock.Unlock()
|
defer cloudDisconnectLock.Unlock()
|
||||||
|
|
||||||
if cloudDisconnectChan == nil {
|
if cloudDisconnectChan == nil {
|
||||||
cloudLogger.Trace().Msg("cloud disconnect channel is not set, no need to disconnect")
|
cloudLogger.Tracef("cloud disconnect channel is not set, no need to disconnect")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// just in case the channel is closed, we don't want to panic
|
// just in case the channel is closed, we don't want to panic
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
cloudLogger.Warn().Interface("reason", r).Msg("cloud disconnect channel is closed, no need to disconnect")
|
cloudLogger.Infof("cloud disconnect channel is closed, no need to disconnect: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
cloudDisconnectChan <- reason
|
cloudDisconnectChan <- reason
|
||||||
|
|
@ -314,78 +283,40 @@ func runWebsocketClient() error {
|
||||||
wsURL.Scheme = "wss"
|
wsURL.Scheme = "wss"
|
||||||
}
|
}
|
||||||
|
|
||||||
setCloudConnectionState(CloudConnectionStateConnecting)
|
|
||||||
|
|
||||||
header := http.Header{}
|
header := http.Header{}
|
||||||
header.Set("X-Device-ID", GetDeviceID())
|
header.Set("X-Device-ID", GetDeviceID())
|
||||||
header.Set("X-App-Version", builtAppVersion)
|
header.Set("X-App-Version", builtAppVersion)
|
||||||
header.Set("Authorization", "Bearer "+config.CloudToken)
|
header.Set("Authorization", "Bearer "+config.CloudToken)
|
||||||
dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout)
|
dialCtx, cancelDial := context.WithTimeout(context.Background(), CloudWebSocketConnectTimeout)
|
||||||
|
|
||||||
l := websocketLogger.With().
|
|
||||||
Str("source", wsURL.Host).
|
|
||||||
Str("sourceType", "cloud").
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
scopedLogger := &l
|
|
||||||
|
|
||||||
defer cancelDial()
|
defer cancelDial()
|
||||||
c, resp, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
|
c, _, err := websocket.Dial(dialCtx, wsURL.String(), &websocket.DialOptions{
|
||||||
HTTPHeader: header,
|
HTTPHeader: header,
|
||||||
OnPingReceived: func(ctx context.Context, payload []byte) bool {
|
OnPingReceived: func(ctx context.Context, payload []byte) bool {
|
||||||
scopedLogger.Debug().Bytes("payload", payload).Int("length", len(payload)).Msg("ping frame received")
|
websocketLogger.Infof("ping frame received: %v, source: %s, sourceType: cloud", payload, wsURL.Host)
|
||||||
|
|
||||||
metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc()
|
metricConnectionTotalPingReceivedCount.WithLabelValues("cloud", wsURL.Host).Inc()
|
||||||
metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime()
|
metricConnectionLastPingReceivedTimestamp.WithLabelValues("cloud", wsURL.Host).SetToCurrentTime()
|
||||||
|
|
||||||
setCloudConnectionState(CloudConnectionStateConnected)
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
var connectionId string
|
|
||||||
if resp != nil {
|
|
||||||
// get the request id from the response header
|
|
||||||
connectionId = resp.Header.Get("X-Request-ID")
|
|
||||||
if connectionId == "" {
|
|
||||||
connectionId = resp.Header.Get("Cf-Ray")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if connectionId == "" {
|
|
||||||
connectionId = uuid.New().String()
|
|
||||||
scopedLogger.Warn().
|
|
||||||
Str("connectionId", connectionId).
|
|
||||||
Msg("no connection id received from the server, generating a new one")
|
|
||||||
}
|
|
||||||
|
|
||||||
lWithConnectionId := scopedLogger.With().
|
|
||||||
Str("connectionID", connectionId).
|
|
||||||
Logger()
|
|
||||||
scopedLogger = &lWithConnectionId
|
|
||||||
|
|
||||||
// if the context is canceled, we don't want to return an error
|
// if the context is canceled, we don't want to return an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
cloudLogger.Info().Msg("websocket connection canceled")
|
cloudLogger.Infof("websocket connection canceled")
|
||||||
setCloudConnectionState(CloudConnectionStateDisconnected)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer c.CloseNow() //nolint:errcheck
|
defer c.CloseNow() //nolint:errcheck
|
||||||
cloudLogger.Info().
|
cloudLogger.Infof("websocket connected to %s", wsURL)
|
||||||
Str("url", wsURL.String()).
|
|
||||||
Str("connectionID", connectionId).
|
|
||||||
Msg("websocket connected")
|
|
||||||
|
|
||||||
// set the metrics when we successfully connect to the cloud.
|
// set the metrics when we successfully connect to the cloud.
|
||||||
wsResetMetrics(true, "cloud", wsURL.Host)
|
wsResetMetrics(true, "cloud", wsURL.Host)
|
||||||
|
|
||||||
// we don't have a source for the cloud connection
|
// we don't have a source for the cloud connection
|
||||||
return handleWebRTCSignalWsMessages(c, true, wsURL.Host, connectionId, scopedLogger)
|
return handleWebRTCSignalWsMessages(c, true, wsURL.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
|
func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest) error {
|
||||||
|
|
@ -396,7 +327,7 @@ func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessi
|
||||||
_ = wsjson.Write(context.Background(), c, gin.H{
|
_ = wsjson.Write(context.Background(), c, gin.H{
|
||||||
"error": fmt.Sprintf("failed to initialize OIDC provider: %v", err),
|
"error": fmt.Sprintf("failed to initialize OIDC provider: %v", err),
|
||||||
})
|
})
|
||||||
cloudLogger.Warn().Err(err).Msg("failed to initialize OIDC provider")
|
cloudLogger.Errorf("failed to initialize OIDC provider: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,14 +350,7 @@ func authenticateSession(ctx context.Context, c *websocket.Conn, req WebRTCSessi
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSessionRequest(
|
func handleSessionRequest(ctx context.Context, c *websocket.Conn, req WebRTCSessionRequest, isCloudConnection bool, source string) error {
|
||||||
ctx context.Context,
|
|
||||||
c *websocket.Conn,
|
|
||||||
req WebRTCSessionRequest,
|
|
||||||
isCloudConnection bool,
|
|
||||||
source string,
|
|
||||||
scopedLogger *zerolog.Logger,
|
|
||||||
) error {
|
|
||||||
var sourceType string
|
var sourceType string
|
||||||
if isCloudConnection {
|
if isCloudConnection {
|
||||||
sourceType = "cloud"
|
sourceType = "cloud"
|
||||||
|
|
@ -452,7 +376,6 @@ func handleSessionRequest(
|
||||||
IsCloud: isCloudConnection,
|
IsCloud: isCloudConnection,
|
||||||
LocalIP: req.IP,
|
LocalIP: req.IP,
|
||||||
ICEServers: req.ICEServers,
|
ICEServers: req.ICEServers,
|
||||||
Logger: scopedLogger,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
|
_ = wsjson.Write(context.Background(), c, gin.H{"error": err})
|
||||||
|
|
@ -473,8 +396,8 @@ func handleSessionRequest(
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
cloudLogger.Info().Interface("session", session).Msg("new session accepted")
|
cloudLogger.Info("new session accepted")
|
||||||
cloudLogger.Trace().Interface("session", session).Msg("new session accepted")
|
cloudLogger.Tracef("new session accepted: %v", session)
|
||||||
currentSession = session
|
currentSession = session
|
||||||
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
|
_ = wsjson.Write(context.Background(), c, gin.H{"type": "answer", "data": sd})
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -489,22 +412,22 @@ func RunWebsocketClient() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the network is not up, well, we can't connect to the cloud.
|
// If the network is not up, well, we can't connect to the cloud.
|
||||||
if !networkState.IsOnline() {
|
if !networkState.Up {
|
||||||
cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds")
|
cloudLogger.Warn("waiting for network to be up, will retry in 3 seconds")
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail.
|
// If the system time is not synchronized, the API request will fail anyway because the TLS handshake will fail.
|
||||||
if isTimeSyncNeeded() && !timeSync.IsSyncSuccess() {
|
if isTimeSyncNeeded() && !timeSyncSuccess {
|
||||||
cloudLogger.Warn().Msg("system time is not synced, will retry in 3 seconds")
|
cloudLogger.Warn("system time is not synced, will retry in 3 seconds")
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err := runWebsocketClient()
|
err := runWebsocketClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cloudLogger.Warn().Err(err).Msg("websocket client error")
|
cloudLogger.Errorf("websocket client error: %v", err)
|
||||||
metricCloudConnectionStatus.Set(0)
|
metricCloudConnectionStatus.Set(0)
|
||||||
metricCloudConnectionFailureCount.Inc()
|
metricCloudConnectionFailureCount.Inc()
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
|
@ -556,11 +479,9 @@ func rpcDeregisterDevice() error {
|
||||||
return fmt.Errorf("failed to save configuration after deregistering: %w", err)
|
return fmt.Errorf("failed to save configuration after deregistering: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cloudLogger.Info().Msg("device deregistered, disconnecting from cloud")
|
cloudLogger.Infof("device deregistered, disconnecting from cloud")
|
||||||
disconnectCloud(fmt.Errorf("device deregistered"))
|
disconnectCloud(fmt.Errorf("device deregistered"))
|
||||||
|
|
||||||
setCloudConnectionState(CloudConnectionStateNotConfigured)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
124
config.go
124
config.go
|
|
@ -6,8 +6,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
|
||||||
"github.com/jetkvm/kvm/internal/network"
|
|
||||||
"github.com/jetkvm/kvm/internal/usbgadget"
|
"github.com/jetkvm/kvm/internal/usbgadget"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16,89 +14,26 @@ type WakeOnLanDevice struct {
|
||||||
MacAddress string `json:"macAddress"`
|
MacAddress string `json:"macAddress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants for keyboard macro limits
|
|
||||||
const (
|
|
||||||
MaxMacrosPerDevice = 25
|
|
||||||
MaxStepsPerMacro = 10
|
|
||||||
MaxKeysPerStep = 10
|
|
||||||
MinStepDelay = 50
|
|
||||||
MaxStepDelay = 2000
|
|
||||||
)
|
|
||||||
|
|
||||||
type KeyboardMacroStep struct {
|
|
||||||
Keys []string `json:"keys"`
|
|
||||||
Modifiers []string `json:"modifiers"`
|
|
||||||
Delay int `json:"delay"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *KeyboardMacroStep) Validate() error {
|
|
||||||
if len(s.Keys) > MaxKeysPerStep {
|
|
||||||
return fmt.Errorf("too many keys in step (max %d)", MaxKeysPerStep)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Delay < MinStepDelay {
|
|
||||||
s.Delay = MinStepDelay
|
|
||||||
} else if s.Delay > MaxStepDelay {
|
|
||||||
s.Delay = MaxStepDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeyboardMacro struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Steps []KeyboardMacroStep `json:"steps"`
|
|
||||||
SortOrder int `json:"sortOrder,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *KeyboardMacro) Validate() error {
|
|
||||||
if m.Name == "" {
|
|
||||||
return fmt.Errorf("macro name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.Steps) == 0 {
|
|
||||||
return fmt.Errorf("macro must have at least one step")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.Steps) > MaxStepsPerMacro {
|
|
||||||
return fmt.Errorf("too many steps in macro (max %d)", MaxStepsPerMacro)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range m.Steps {
|
|
||||||
if err := m.Steps[i].Validate(); err != nil {
|
|
||||||
return fmt.Errorf("invalid step %d: %w", i+1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
CloudURL string `json:"cloud_url"`
|
CloudURL string `json:"cloud_url"`
|
||||||
CloudAppURL string `json:"cloud_app_url"`
|
CloudAppURL string `json:"cloud_app_url"`
|
||||||
CloudToken string `json:"cloud_token"`
|
CloudToken string `json:"cloud_token"`
|
||||||
GoogleIdentity string `json:"google_identity"`
|
GoogleIdentity string `json:"google_identity"`
|
||||||
JigglerEnabled bool `json:"jiggler_enabled"`
|
JigglerEnabled bool `json:"jiggler_enabled"`
|
||||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||||
IncludePreRelease bool `json:"include_pre_release"`
|
IncludePreRelease bool `json:"include_pre_release"`
|
||||||
HashedPassword string `json:"hashed_password"`
|
HashedPassword string `json:"hashed_password"`
|
||||||
LocalAuthToken string `json:"local_auth_token"`
|
LocalAuthToken string `json:"local_auth_token"`
|
||||||
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
|
||||||
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
|
||||||
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
|
EdidString string `json:"hdmi_edid_string"`
|
||||||
KeyboardLayout string `json:"keyboard_layout"`
|
ActiveExtension string `json:"active_extension"`
|
||||||
EdidString string `json:"hdmi_edid_string"`
|
DisplayMaxBrightness int `json:"display_max_brightness"`
|
||||||
ActiveExtension string `json:"active_extension"`
|
DisplayDimAfterSec int `json:"display_dim_after_sec"`
|
||||||
DisplayRotation string `json:"display_rotation"`
|
DisplayOffAfterSec int `json:"display_off_after_sec"`
|
||||||
DisplayMaxBrightness int `json:"display_max_brightness"`
|
TLSMode string `json:"tls_mode"`
|
||||||
DisplayDimAfterSec int `json:"display_dim_after_sec"`
|
UsbConfig *usbgadget.Config `json:"usb_config"`
|
||||||
DisplayOffAfterSec int `json:"display_off_after_sec"`
|
UsbDevices *usbgadget.Devices `json:"usb_devices"`
|
||||||
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
|
|
||||||
UsbConfig *usbgadget.Config `json:"usb_config"`
|
|
||||||
UsbDevices *usbgadget.Devices `json:"usb_devices"`
|
|
||||||
NetworkConfig *network.NetworkConfig `json:"network_config"`
|
|
||||||
DefaultLogLevel string `json:"default_log_level"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configPath = "/userdata/kvm_config.json"
|
const configPath = "/userdata/kvm_config.json"
|
||||||
|
|
@ -108,9 +43,6 @@ var defaultConfig = &Config{
|
||||||
CloudAppURL: "https://app.jetkvm.com",
|
CloudAppURL: "https://app.jetkvm.com",
|
||||||
AutoUpdateEnabled: true, // Set a default value
|
AutoUpdateEnabled: true, // Set a default value
|
||||||
ActiveExtension: "",
|
ActiveExtension: "",
|
||||||
KeyboardMacros: []KeyboardMacro{},
|
|
||||||
DisplayRotation: "270",
|
|
||||||
KeyboardLayout: "en-US",
|
|
||||||
DisplayMaxBrightness: 64,
|
DisplayMaxBrightness: 64,
|
||||||
DisplayDimAfterSec: 120, // 2 minutes
|
DisplayDimAfterSec: 120, // 2 minutes
|
||||||
DisplayOffAfterSec: 1800, // 30 minutes
|
DisplayOffAfterSec: 1800, // 30 minutes
|
||||||
|
|
@ -128,8 +60,6 @@ var defaultConfig = &Config{
|
||||||
Keyboard: true,
|
Keyboard: true,
|
||||||
MassStorage: true,
|
MassStorage: true,
|
||||||
},
|
},
|
||||||
NetworkConfig: &network.NetworkConfig{},
|
|
||||||
DefaultLogLevel: "INFO",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -142,7 +72,7 @@ func LoadConfig() {
|
||||||
defer configLock.Unlock()
|
defer configLock.Unlock()
|
||||||
|
|
||||||
if config != nil {
|
if config != nil {
|
||||||
logger.Debug().Msg("config already loaded, skipping")
|
logger.Info("config already loaded, skipping")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,7 +81,7 @@ func LoadConfig() {
|
||||||
|
|
||||||
file, err := os.Open(configPath)
|
file, err := os.Open(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug().Msg("default config file doesn't exist, using default")
|
logger.Debug("default config file doesn't exist, using default")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
@ -159,7 +89,7 @@ func LoadConfig() {
|
||||||
// load and merge the default config with the user config
|
// load and merge the default config with the user config
|
||||||
loadedConfig := *defaultConfig
|
loadedConfig := *defaultConfig
|
||||||
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
|
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
|
||||||
logger.Warn().Err(err).Msg("config file JSON parsing failed")
|
logger.Errorf("config file JSON parsing failed, %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,23 +102,13 @@ func LoadConfig() {
|
||||||
loadedConfig.UsbDevices = defaultConfig.UsbDevices
|
loadedConfig.UsbDevices = defaultConfig.UsbDevices
|
||||||
}
|
}
|
||||||
|
|
||||||
if loadedConfig.NetworkConfig == nil {
|
|
||||||
loadedConfig.NetworkConfig = defaultConfig.NetworkConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
config = &loadedConfig
|
config = &loadedConfig
|
||||||
|
|
||||||
logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
|
|
||||||
|
|
||||||
logger.Info().Str("path", configPath).Msg("config loaded")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveConfig() error {
|
func SaveConfig() error {
|
||||||
configLock.Lock()
|
configLock.Lock()
|
||||||
defer configLock.Unlock()
|
defer configLock.Unlock()
|
||||||
|
|
||||||
logger.Trace().Str("path", configPath).Msg("Saving config")
|
|
||||||
|
|
||||||
file, err := os.Create(configPath)
|
file, err := os.Create(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create config file: %w", err)
|
return fmt.Errorf("failed to create config file: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,6 @@
|
||||||
# Exit immediately if a command exits with a non-zero status
|
# Exit immediately if a command exits with a non-zero status
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
C_RST="$(tput sgr0)"
|
|
||||||
C_ERR="$(tput setaf 1)"
|
|
||||||
C_OK="$(tput setaf 2)"
|
|
||||||
C_WARN="$(tput setaf 3)"
|
|
||||||
C_INFO="$(tput setaf 5)"
|
|
||||||
|
|
||||||
msg() { printf '%s%s%s\n' $2 "$1" $C_RST; }
|
|
||||||
|
|
||||||
msg_info() { msg "$1" $C_INFO; }
|
|
||||||
msg_ok() { msg "$1" $C_OK; }
|
|
||||||
msg_err() { msg "$1" $C_ERR; }
|
|
||||||
msg_warn() { msg "$1" $C_WARN; }
|
|
||||||
|
|
||||||
# Function to display help message
|
# Function to display help message
|
||||||
show_help() {
|
show_help() {
|
||||||
echo "Usage: $0 [options] -r <remote_ip>"
|
echo "Usage: $0 [options] -r <remote_ip>"
|
||||||
|
|
@ -25,8 +12,6 @@ show_help() {
|
||||||
echo
|
echo
|
||||||
echo "Optional:"
|
echo "Optional:"
|
||||||
echo " -u, --user <remote_user> Remote username (default: root)"
|
echo " -u, --user <remote_user> Remote username (default: root)"
|
||||||
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 " --skip-ui-build Skip frontend/UI build"
|
||||||
echo " --help Display this help message"
|
echo " --help Display this help message"
|
||||||
echo
|
echo
|
||||||
|
|
@ -39,10 +24,6 @@ show_help() {
|
||||||
REMOTE_USER="root"
|
REMOTE_USER="root"
|
||||||
REMOTE_PATH="/userdata/jetkvm/bin"
|
REMOTE_PATH="/userdata/jetkvm/bin"
|
||||||
SKIP_UI_BUILD=false
|
SKIP_UI_BUILD=false
|
||||||
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
|
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
|
|
@ -59,19 +40,6 @@ while [[ $# -gt 0 ]]; do
|
||||||
SKIP_UI_BUILD=true
|
SKIP_UI_BUILD=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--reset-usb-hid)
|
|
||||||
RESET_USB_HID_DEVICE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--run-go-tests)
|
|
||||||
RUN_GO_TESTS=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--run-go-tests-only)
|
|
||||||
RUN_GO_TESTS_ONLY=true
|
|
||||||
RUN_GO_TESTS=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help)
|
--help)
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
|
|
@ -86,75 +54,24 @@ done
|
||||||
|
|
||||||
# Verify required parameters
|
# Verify required parameters
|
||||||
if [ -z "$REMOTE_HOST" ]; then
|
if [ -z "$REMOTE_HOST" ]; then
|
||||||
msg_err "Error: Remote IP is a required parameter"
|
echo "Error: Remote IP is a required parameter"
|
||||||
show_help
|
show_help
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build the development version on the host
|
# Build the development version on the host
|
||||||
if [ "$SKIP_UI_BUILD" = false ]; then
|
if [ "$SKIP_UI_BUILD" = false ]; then
|
||||||
msg_info "▶ Building frontend"
|
|
||||||
make frontend
|
make frontend
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$RUN_GO_TESTS" = true ]; then
|
|
||||||
msg_info "▶ Building go tests"
|
|
||||||
make build_dev_test
|
|
||||||
|
|
||||||
msg_info "▶ Copying device-tests.tar.gz to remote host"
|
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
|
|
||||||
|
|
||||||
msg_info "▶ Running go tests"
|
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
|
|
||||||
set -e
|
|
||||||
TMP_DIR=$(mktemp -d)
|
|
||||||
cd ${TMP_DIR}
|
|
||||||
tar zxf /tmp/device-tests.tar.gz
|
|
||||||
./gotestsum --format=testdox \
|
|
||||||
--jsonfile=/tmp/device-tests.json \
|
|
||||||
--post-run-command 'sh -c "echo $TESTS_FAILED > /tmp/device-tests.failed"' \
|
|
||||||
--raw-command -- ./run_all_tests -json
|
|
||||||
|
|
||||||
GOTESTSUM_EXIT_CODE=$?
|
|
||||||
if [ $GOTESTSUM_EXIT_CODE -ne 0 ]; then
|
|
||||||
echo "❌ Tests failed (exit code: $GOTESTSUM_EXIT_CODE)"
|
|
||||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TESTS_FAILED=$(cat /tmp/device-tests.failed)
|
|
||||||
if [ "$TESTS_FAILED" -ne 0 ]; then
|
|
||||||
echo "❌ Tests failed $TESTS_FAILED tests failed"
|
|
||||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Tests passed"
|
|
||||||
rm -rf ${TMP_DIR} /tmp/device-tests.tar.gz
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ "$RUN_GO_TESTS_ONLY" = true ]; then
|
|
||||||
msg_info "▶ Go tests completed"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg_info "▶ Building go binary"
|
|
||||||
make build_dev
|
make build_dev
|
||||||
|
|
||||||
|
# Change directory to the binary output directory
|
||||||
|
cd bin
|
||||||
|
|
||||||
# Kill any existing instances of the application
|
# Kill any existing instances of the application
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
|
||||||
|
|
||||||
# Copy the binary to the remote host
|
# Copy the binary to the remote host
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < 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
|
# Deploy and run the application on the remote host
|
||||||
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
|
||||||
|
|
@ -174,7 +91,7 @@ cd "${REMOTE_PATH}"
|
||||||
chmod +x jetkvm_app_debug
|
chmod +x jetkvm_app_debug
|
||||||
|
|
||||||
# Run the application in the background
|
# Run the application in the background
|
||||||
PION_LOG_TRACE=${LOG_TRACE_SCOPES} ./jetkvm_app_debug
|
PION_LOG_TRACE=jetkvm,cloud,websocket ./jetkvm_app_debug
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "Deployment complete."
|
echo "Deployment complete."
|
||||||
185
display.go
185
display.go
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -25,7 +24,7 @@ const (
|
||||||
func switchToScreen(screen string) {
|
func switchToScreen(screen string) {
|
||||||
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
_, err := CallCtrlAction("lv_scr_load", map[string]interface{}{"obj": screen})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
displayLogger.Warn().Err(err).Str("screen", screen).Msg("failed to switch to screen")
|
logger.Warnf("failed to switch to screen %s: %v", screen, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentScreen = screen
|
currentScreen = screen
|
||||||
|
|
@ -33,185 +32,62 @@ func switchToScreen(screen string) {
|
||||||
|
|
||||||
var displayedTexts = make(map[string]string)
|
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})
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvObjAddFlag(objName string, flag string) (*CtrlResponse, error) {
|
|
||||||
return CallCtrlAction("lv_obj_add_flag", map[string]interface{}{"obj": objName, "flag": flag})
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvObjClearFlag(objName string, flag string) (*CtrlResponse, error) {
|
|
||||||
return CallCtrlAction("lv_obj_clear_flag", map[string]interface{}{"obj": objName, "flag": flag})
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvObjHide(objName string) (*CtrlResponse, error) {
|
|
||||||
return lvObjAddFlag(objName, "LV_OBJ_FLAG_HIDDEN")
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvObjShow(objName string) (*CtrlResponse, error) {
|
|
||||||
return lvObjClearFlag(objName, "LV_OBJ_FLAG_HIDDEN")
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvObjSetOpacity(objName string, opacity int) (*CtrlResponse, error) { // nolint:unused
|
|
||||||
return CallCtrlAction("lv_obj_set_style_opa_layered", map[string]interface{}{"obj": objName, "opa": opacity})
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvObjFadeIn(objName string, duration uint32) (*CtrlResponse, error) {
|
|
||||||
return CallCtrlAction("lv_obj_fade_in", map[string]interface{}{"obj": objName, "time": duration})
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvObjFadeOut(objName string, duration uint32) (*CtrlResponse, error) {
|
|
||||||
return CallCtrlAction("lv_obj_fade_out", map[string]interface{}{"obj": objName, "time": duration})
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvLabelSetText(objName string, text string) (*CtrlResponse, error) {
|
|
||||||
return CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": text})
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvImgSetSrc(objName string, src string) (*CtrlResponse, error) {
|
|
||||||
return CallCtrlAction("lv_img_set_src", map[string]interface{}{"obj": objName, "src": src})
|
|
||||||
}
|
|
||||||
|
|
||||||
func lvDispSetRotation(rotation string) (*CtrlResponse, error) {
|
|
||||||
return CallCtrlAction("lv_disp_set_rotation", map[string]interface{}{"rotation": rotation})
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateLabelIfChanged(objName string, newText string) {
|
func updateLabelIfChanged(objName string, newText string) {
|
||||||
if newText != "" && newText != displayedTexts[objName] {
|
if newText != "" && newText != displayedTexts[objName] {
|
||||||
_, _ = lvLabelSetText(objName, newText)
|
_, _ = CallCtrlAction("lv_label_set_text", map[string]interface{}{"obj": objName, "text": newText})
|
||||||
displayedTexts[objName] = newText
|
displayedTexts[objName] = newText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchToScreenIfDifferent(screenName string) {
|
func switchToScreenIfDifferent(screenName string) {
|
||||||
|
logger.Infof("switching screen from %s to %s", currentScreen, screenName)
|
||||||
if currentScreen != screenName {
|
if currentScreen != screenName {
|
||||||
displayLogger.Info().Str("from", currentScreen).Str("to", screenName).Msg("switching screen")
|
|
||||||
switchToScreen(screenName)
|
switchToScreen(screenName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
cloudBlinkLock sync.Mutex = sync.Mutex{}
|
|
||||||
cloudBlinkStopped bool
|
|
||||||
cloudBlinkTicker *time.Ticker
|
|
||||||
)
|
|
||||||
|
|
||||||
func updateDisplay() {
|
func updateDisplay() {
|
||||||
updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4String())
|
updateLabelIfChanged("ui_Home_Content_Ip", networkState.IPv4)
|
||||||
if usbState == "configured" {
|
if usbState == "configured" {
|
||||||
updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected")
|
updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Connected")
|
||||||
_, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_DEFAULT")
|
_, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_DEFAULT"})
|
||||||
} else {
|
} else {
|
||||||
updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected")
|
updateLabelIfChanged("ui_Home_Footer_Usb_Status_Label", "Disconnected")
|
||||||
_, _ = lvObjSetState("ui_Home_Footer_Usb_Status_Label", "LV_STATE_USER_2")
|
_, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Usb_Status_Label", "state": "LV_STATE_USER_2"})
|
||||||
}
|
}
|
||||||
if lastVideoState.Ready {
|
if lastVideoState.Ready {
|
||||||
updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected")
|
updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Connected")
|
||||||
_, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_DEFAULT")
|
_, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_DEFAULT"})
|
||||||
} else {
|
} else {
|
||||||
updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected")
|
updateLabelIfChanged("ui_Home_Footer_Hdmi_Status_Label", "Disconnected")
|
||||||
_, _ = lvObjSetState("ui_Home_Footer_Hdmi_Status_Label", "LV_STATE_USER_2")
|
_, _ = CallCtrlAction("lv_obj_set_state", map[string]interface{}{"obj": "ui_Home_Footer_Hdmi_Status_Label", "state": "LV_STATE_USER_2"})
|
||||||
}
|
}
|
||||||
updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions))
|
updateLabelIfChanged("ui_Home_Header_Cloud_Status_Label", fmt.Sprintf("%d active", actionSessions))
|
||||||
|
if networkState.Up {
|
||||||
if networkState.IsUp() {
|
|
||||||
switchToScreenIfDifferent("ui_Home_Screen")
|
switchToScreenIfDifferent("ui_Home_Screen")
|
||||||
} else {
|
} else {
|
||||||
switchToScreenIfDifferent("ui_No_Network_Screen")
|
switchToScreenIfDifferent("ui_No_Network_Screen")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cloudConnectionState == CloudConnectionStateNotConfigured {
|
|
||||||
_, _ = lvObjHide("ui_Home_Header_Cloud_Status_Icon")
|
|
||||||
} else {
|
|
||||||
_, _ = lvObjShow("ui_Home_Header_Cloud_Status_Icon")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cloudConnectionState {
|
|
||||||
case CloudConnectionStateDisconnected:
|
|
||||||
_, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud_disconnected.png")
|
|
||||||
stopCloudBlink()
|
|
||||||
case CloudConnectionStateConnecting:
|
|
||||||
_, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png")
|
|
||||||
startCloudBlink()
|
|
||||||
case CloudConnectionStateConnected:
|
|
||||||
_, _ = lvImgSetSrc("ui_Home_Header_Cloud_Status_Icon", "cloud.png")
|
|
||||||
stopCloudBlink()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startCloudBlink() {
|
var displayInited = false
|
||||||
if cloudBlinkTicker == nil {
|
|
||||||
cloudBlinkTicker = time.NewTicker(2 * time.Second)
|
|
||||||
} else {
|
|
||||||
// do nothing if the blink isn't stopped
|
|
||||||
if cloudBlinkStopped {
|
|
||||||
cloudBlinkLock.Lock()
|
|
||||||
defer cloudBlinkLock.Unlock()
|
|
||||||
|
|
||||||
cloudBlinkStopped = false
|
|
||||||
cloudBlinkTicker.Reset(2 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for range cloudBlinkTicker.C {
|
|
||||||
if cloudConnectionState != CloudConnectionStateConnecting {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
_, _ = lvObjFadeOut("ui_Home_Header_Cloud_Status_Icon", 1000)
|
|
||||||
time.Sleep(1000 * time.Millisecond)
|
|
||||||
_, _ = lvObjFadeIn("ui_Home_Header_Cloud_Status_Icon", 1000)
|
|
||||||
time.Sleep(1000 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopCloudBlink() {
|
|
||||||
if cloudBlinkTicker != nil {
|
|
||||||
cloudBlinkTicker.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudBlinkLock.Lock()
|
|
||||||
defer cloudBlinkLock.Unlock()
|
|
||||||
cloudBlinkStopped = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
displayInited = false
|
|
||||||
displayUpdateLock = sync.Mutex{}
|
|
||||||
waitDisplayUpdate = sync.Mutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func requestDisplayUpdate(shouldWakeDisplay bool) {
|
|
||||||
displayUpdateLock.Lock()
|
|
||||||
defer displayUpdateLock.Unlock()
|
|
||||||
|
|
||||||
|
func requestDisplayUpdate() {
|
||||||
if !displayInited {
|
if !displayInited {
|
||||||
displayLogger.Info().Msg("display not inited, skipping updates")
|
logger.Info("display not inited, skipping updates")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if shouldWakeDisplay {
|
wakeDisplay(false)
|
||||||
wakeDisplay(false)
|
logger.Info("display updating")
|
||||||
}
|
|
||||||
displayLogger.Debug().Msg("display updating")
|
|
||||||
//TODO: only run once regardless how many pending updates
|
//TODO: only run once regardless how many pending updates
|
||||||
updateDisplay()
|
updateDisplay()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool) {
|
|
||||||
waitDisplayUpdate.Lock()
|
|
||||||
defer waitDisplayUpdate.Unlock()
|
|
||||||
|
|
||||||
waitCtrlClientConnected()
|
|
||||||
requestDisplayUpdate(shouldWakeDisplay)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateStaticContents() {
|
func updateStaticContents() {
|
||||||
//contents that never change
|
//contents that never change
|
||||||
updateLabelIfChanged("ui_Home_Content_Mac", networkState.MACString())
|
updateLabelIfChanged("ui_Home_Content_Mac", networkState.MAC)
|
||||||
systemVersion, appVersion, err := GetLocalVersion()
|
systemVersion, appVersion, err := GetLocalVersion()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String())
|
updateLabelIfChanged("ui_About_Content_Operating_System_Version_ContentLabel", systemVersion.String())
|
||||||
|
|
@ -242,7 +118,7 @@ func setDisplayBrightness(brightness int) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
displayLogger.Info().Int("brightness", brightness).Msg("set brightness")
|
logger.Infof("display: set brightness to %v", brightness)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,7 +127,7 @@ func setDisplayBrightness(brightness int) error {
|
||||||
func tick_displayDim() {
|
func tick_displayDim() {
|
||||||
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
|
err := setDisplayBrightness(config.DisplayMaxBrightness / 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
displayLogger.Warn().Err(err).Msg("failed to dim display")
|
logger.Warnf("display: failed to dim display: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dimTicker.Stop()
|
dimTicker.Stop()
|
||||||
|
|
@ -264,7 +140,7 @@ func tick_displayDim() {
|
||||||
func tick_displayOff() {
|
func tick_displayOff() {
|
||||||
err := setDisplayBrightness(0)
|
err := setDisplayBrightness(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
displayLogger.Warn().Err(err).Msg("failed to turn off display")
|
logger.Warnf("display: failed to turn off display: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
offTicker.Stop()
|
offTicker.Stop()
|
||||||
|
|
@ -287,7 +163,7 @@ func wakeDisplay(force bool) {
|
||||||
|
|
||||||
err := setDisplayBrightness(config.DisplayMaxBrightness)
|
err := setDisplayBrightness(config.DisplayMaxBrightness)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
displayLogger.Warn().Err(err).Msg("failed to wake display")
|
logger.Warnf("display wake failed, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.DisplayDimAfterSec != 0 {
|
if config.DisplayDimAfterSec != 0 {
|
||||||
|
|
@ -307,7 +183,7 @@ func wakeDisplay(force bool) {
|
||||||
func watchTsEvents() {
|
func watchTsEvents() {
|
||||||
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
|
ts, err := os.OpenFile(touchscreenDevice, os.O_RDONLY, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
displayLogger.Warn().Err(err).Msg("failed to open touchscreen device")
|
logger.Warnf("display: failed to open touchscreen device: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,7 +196,7 @@ func watchTsEvents() {
|
||||||
for {
|
for {
|
||||||
_, err := ts.Read(buf)
|
_, err := ts.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
displayLogger.Warn().Err(err).Msg("failed to read from touchscreen device")
|
logger.Warnf("display: failed to read from touchscreen device: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,12 +216,12 @@ func startBacklightTickers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
if dimTicker == nil && config.DisplayDimAfterSec != 0 {
|
||||||
displayLogger.Info().Msg("dim_ticker has started")
|
logger.Info("display: dim_ticker has started")
|
||||||
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
|
||||||
defer dimTicker.Stop()
|
defer dimTicker.Stop()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for { //nolint:staticcheck
|
for { //nolint:gosimple
|
||||||
select {
|
select {
|
||||||
case <-dimTicker.C:
|
case <-dimTicker.C:
|
||||||
tick_displayDim()
|
tick_displayDim()
|
||||||
|
|
@ -355,12 +231,12 @@ func startBacklightTickers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
if offTicker == nil && config.DisplayOffAfterSec != 0 {
|
||||||
displayLogger.Info().Msg("off_ticker has started")
|
logger.Info("display: off_ticker has started")
|
||||||
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
|
||||||
defer offTicker.Stop()
|
defer offTicker.Stop()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for { //nolint:staticcheck
|
for { //nolint:gosimple
|
||||||
select {
|
select {
|
||||||
case <-offTicker.C:
|
case <-offTicker.C:
|
||||||
tick_displayOff()
|
tick_displayOff()
|
||||||
|
|
@ -370,18 +246,19 @@ func startBacklightTickers() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDisplay() {
|
func init() {
|
||||||
|
ensureConfigLoaded()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
waitCtrlClientConnected()
|
waitCtrlClientConnected()
|
||||||
displayLogger.Info().Msg("setting initial display contents")
|
logger.Info("setting initial display contents")
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
_, _ = lvDispSetRotation(config.DisplayRotation)
|
|
||||||
updateStaticContents()
|
updateStaticContents()
|
||||||
displayInited = true
|
displayInited = true
|
||||||
displayLogger.Info().Msg("display inited")
|
logger.Info("display inited")
|
||||||
startBacklightTickers()
|
startBacklightTickers()
|
||||||
wakeDisplay(true)
|
wakeDisplay(true)
|
||||||
requestDisplayUpdate(true)
|
requestDisplayUpdate()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go watchTsEvents()
|
go watchTsEvents()
|
||||||
|
|
|
||||||
4
fuse.go
4
fuse.go
|
|
@ -37,7 +37,7 @@ func (f *WebRTCStreamFile) Getattr(ctx context.Context, fh fs.FileHandle, out *f
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
out.Attr = f.Attr
|
out.Attr = f.Attr
|
||||||
out.Size = f.size
|
out.Attr.Size = f.size
|
||||||
return fs.OK
|
return fs.OK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ func RunFuseServer() {
|
||||||
var err error
|
var err error
|
||||||
fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts)
|
fuseServer, err = fs.Mount(fuseMountPoint, &FuseRoot{}, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to mount fuse")
|
logger.Warnf("failed to mount fuse: %v", err)
|
||||||
}
|
}
|
||||||
fuseServer.Wait()
|
fuseServer.Wait()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
go.mod
51
go.mod
|
|
@ -1,8 +1,8 @@
|
||||||
module github.com/jetkvm/kvm
|
module github.com/jetkvm/kvm
|
||||||
|
|
||||||
go 1.23.4
|
go 1.21.0
|
||||||
|
|
||||||
toolchain go1.24.3
|
toolchain go1.21.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.3.0
|
github.com/Masterminds/semver/v3 v3.3.0
|
||||||
|
|
@ -10,59 +10,50 @@ require (
|
||||||
github.com/coder/websocket v1.8.13
|
github.com/coder/websocket v1.8.13
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0
|
github.com/coreos/go-oidc/v3 v3.11.0
|
||||||
github.com/creack/pty v1.1.23
|
github.com/creack/pty v1.1.23
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/gin-contrib/logger v1.2.5
|
|
||||||
github.com/gin-gonic/gin v1.10.0
|
|
||||||
github.com/google/uuid v1.6.0
|
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-20240401182218-3ab9db955caf
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1
|
github.com/hanwen/go-fuse/v2 v2.5.1
|
||||||
|
github.com/hashicorp/go-envparse v0.1.0
|
||||||
github.com/pion/logging v0.2.2
|
github.com/pion/logging v0.2.2
|
||||||
github.com/pion/mdns/v2 v2.0.7
|
github.com/pion/mdns/v2 v2.0.7
|
||||||
github.com/pion/webrtc/v4 v4.0.0
|
github.com/pion/webrtc/v4 v4.0.0
|
||||||
github.com/pojntfx/go-nbd v0.3.2
|
github.com/pojntfx/go-nbd v0.3.2
|
||||||
github.com/prometheus/client_golang v1.21.0
|
github.com/prometheus/client_golang v1.21.0
|
||||||
github.com/prometheus/common v0.62.0
|
github.com/prometheus/common v0.62.0
|
||||||
github.com/prometheus/procfs v0.15.1
|
|
||||||
github.com/psanford/httpreadat v0.1.0
|
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
|
github.com/vishvananda/netlink v1.3.0
|
||||||
go.bug.st/serial v1.6.2
|
go.bug.st/serial v1.6.2
|
||||||
golang.org/x/crypto v0.38.0
|
golang.org/x/crypto v0.31.0
|
||||||
golang.org/x/net v0.40.0
|
golang.org/x/net v0.33.0
|
||||||
golang.org/x/sys v0.33.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/creack/goselect v0.1.2 // indirect
|
github.com/creack/goselect v0.1.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pilebones/go-udev v0.9.0 // indirect
|
github.com/pilebones/go-udev v0.9.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.9 // indirect
|
github.com/pion/datachannel v1.5.9 // indirect
|
||||||
github.com/pion/dtls/v3 v3.0.3 // indirect
|
github.com/pion/dtls/v3 v3.0.3 // indirect
|
||||||
|
|
@ -77,16 +68,16 @@ require (
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/prometheus/client_model v0.6.1 // indirect
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/arch v0.15.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/oauth2 v0.24.0 // indirect
|
golang.org/x/oauth2 v0.24.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
100
go.sum
100
go.sum
|
|
@ -4,23 +4,24 @@ github.com/beevik/ntp v1.3.1 h1:Y/srlT8L1yQr58kyPWFPZIxRL8ttx2SRIpVYJqZIlAM=
|
||||||
github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw=
|
github.com/beevik/ntp v1.3.1/go.mod h1:fT6PylBq86Tsq23ZMEe47b7QQrZfYBFPnpzt0a9kJxw=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
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=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
|
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b h1:dSbDgy72Y1sjLPWLv7vs0fMFuhMBMViiT9PJZiZWZNs=
|
||||||
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk=
|
github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b/go.mod h1:SehHnbi2e8NiSAKby42Itm8SIoS7b+wAprsfPH3qgYk=
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
|
||||||
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
|
||||||
github.com/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 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||||
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
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 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||||
|
|
@ -28,16 +29,12 @@ github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
|
||||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
|
@ -46,29 +43,28 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/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 h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA=
|
||||||
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ=
|
||||||
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs=
|
||||||
|
github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY=
|
||||||
|
github.com/hashicorp/go-envparse v0.1.0/go.mod h1:OHheN1GoygLlAkTlXLXvAdnXdZxy8JUweQ1rAXx1xnc=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
|
@ -82,11 +78,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
|
||||||
|
|
@ -98,8 +89,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
github.com/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q=
|
||||||
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI=
|
||||||
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
||||||
|
|
@ -134,7 +125,6 @@ github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||||
github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
|
github.com/pion/webrtc/v4 v4.0.0 h1:x8ec7uJQPP3D1iI8ojPAiTOylPI7Fa7QgqZrhpLyqZ8=
|
||||||
github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
|
github.com/pion/webrtc/v4 v4.0.0/go.mod h1:SfNn8CcFxR6OUVjLXVslAQ3a3994JhyE3Hw1jAuqEto=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
|
||||||
|
|
@ -147,13 +137,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
||||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
|
||||||
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
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=
|
|
||||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f h1:VgoRCP1efSCEZIcF2THLQ46+pIBzzgNiaUBe9wEDwYU=
|
|
||||||
github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzro7BGorij2WgrjEammtrkbo3+xldxo+KaGLGUiD+Q=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
|
@ -179,27 +164,27 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
|
||||||
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
|
||||||
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|
@ -207,3 +192,4 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
|
|
||||||
18
hw.go
18
hw.go
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
@ -43,7 +42,7 @@ func GetDeviceID() string {
|
||||||
deviceIDOnce.Do(func() {
|
deviceIDOnce.Do(func() {
|
||||||
serial, err := extractSerialNumber()
|
serial, err := extractSerialNumber()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Msg("unknown serial number, the program likely not running on RV1106")
|
logger.Warn("unknown serial number, the program likely not running on RV1106")
|
||||||
deviceID = "unknown_device_id"
|
deviceID = "unknown_device_id"
|
||||||
} else {
|
} else {
|
||||||
deviceID = serial
|
deviceID = serial
|
||||||
|
|
@ -52,19 +51,10 @@ func GetDeviceID() string {
|
||||||
return deviceID
|
return deviceID
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDefaultHostname() string {
|
|
||||||
deviceId := GetDeviceID()
|
|
||||||
if deviceId == "unknown_device_id" {
|
|
||||||
return "jetkvm"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("jetkvm-%s", strings.ToLower(deviceId))
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWatchdog() {
|
func runWatchdog() {
|
||||||
file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0)
|
file, err := os.OpenFile("/dev/watchdog", os.O_WRONLY, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
watchdogLogger.Warn().Err(err).Msg("unable to open /dev/watchdog, skipping watchdog reset")
|
logger.Warnf("unable to open /dev/watchdog: %v, skipping watchdog reset", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
@ -75,13 +65,13 @@ func runWatchdog() {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
_, err = file.Write([]byte{0})
|
_, err = file.Write([]byte{0})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
watchdogLogger.Warn().Err(err).Msg("error writing to /dev/watchdog, system may reboot")
|
logger.Errorf("error writing to /dev/watchdog, system may reboot: %v", err)
|
||||||
}
|
}
|
||||||
case <-appCtx.Done():
|
case <-appCtx.Done():
|
||||||
//disarm watchdog with magic value
|
//disarm watchdog with magic value
|
||||||
_, err := file.Write([]byte("V"))
|
_, err := file.Write([]byte("V"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
watchdogLogger.Warn().Err(err).Msg("failed to disarm watchdog, system may reboot")
|
logger.Errorf("failed to disarm watchdog, system may reboot: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,381 +0,0 @@
|
||||||
package confparser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/guregu/null/v6"
|
|
||||||
"golang.org/x/net/idna"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FieldConfig struct {
|
|
||||||
Name string
|
|
||||||
Required bool
|
|
||||||
RequiredIf map[string]interface{}
|
|
||||||
OneOf []string
|
|
||||||
ValidateTypes []string
|
|
||||||
Defaults interface{}
|
|
||||||
IsEmpty bool
|
|
||||||
CurrentValue interface{}
|
|
||||||
TypeString string
|
|
||||||
Delegated bool
|
|
||||||
shouldUpdateValue bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetDefaultsAndValidate(config interface{}) error {
|
|
||||||
return setDefaultsAndValidate(config, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDefaultsAndValidate(config interface{}, isRoot bool) error {
|
|
||||||
// first we need to check if the config is a pointer
|
|
||||||
if reflect.TypeOf(config).Kind() != reflect.Ptr {
|
|
||||||
return fmt.Errorf("config is not a pointer")
|
|
||||||
}
|
|
||||||
|
|
||||||
// now iterate over the lease struct and set the values
|
|
||||||
configType := reflect.TypeOf(config).Elem()
|
|
||||||
configValue := reflect.ValueOf(config).Elem()
|
|
||||||
|
|
||||||
fields := make(map[string]FieldConfig)
|
|
||||||
|
|
||||||
for i := 0; i < configType.NumField(); i++ {
|
|
||||||
field := configType.Field(i)
|
|
||||||
fieldValue := configValue.Field(i)
|
|
||||||
|
|
||||||
defaultValue := field.Tag.Get("default")
|
|
||||||
|
|
||||||
fieldType := field.Type.String()
|
|
||||||
|
|
||||||
fieldConfig := FieldConfig{
|
|
||||||
Name: field.Name,
|
|
||||||
OneOf: splitString(field.Tag.Get("one_of")),
|
|
||||||
ValidateTypes: splitString(field.Tag.Get("validate_type")),
|
|
||||||
RequiredIf: make(map[string]interface{}),
|
|
||||||
CurrentValue: fieldValue.Interface(),
|
|
||||||
IsEmpty: false,
|
|
||||||
TypeString: fieldType,
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the field is required
|
|
||||||
required := field.Tag.Get("required")
|
|
||||||
if required != "" {
|
|
||||||
requiredBool, _ := strconv.ParseBool(required)
|
|
||||||
fieldConfig.Required = requiredBool
|
|
||||||
}
|
|
||||||
|
|
||||||
var canUseOneOff = false
|
|
||||||
|
|
||||||
// use switch to get the type
|
|
||||||
switch fieldValue.Interface().(type) {
|
|
||||||
case string, null.String:
|
|
||||||
if defaultValue != "" {
|
|
||||||
fieldConfig.Defaults = defaultValue
|
|
||||||
}
|
|
||||||
canUseOneOff = true
|
|
||||||
case []string:
|
|
||||||
if defaultValue != "" {
|
|
||||||
fieldConfig.Defaults = strings.Split(defaultValue, ",")
|
|
||||||
}
|
|
||||||
canUseOneOff = true
|
|
||||||
case int, null.Int:
|
|
||||||
if defaultValue != "" {
|
|
||||||
defaultValueInt, err := strconv.Atoi(defaultValue)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldConfig.Defaults = defaultValueInt
|
|
||||||
}
|
|
||||||
case bool, null.Bool:
|
|
||||||
if defaultValue != "" {
|
|
||||||
defaultValueBool, err := strconv.ParseBool(defaultValue)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid default value for field `%s`: %s", field.Name, defaultValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldConfig.Defaults = defaultValueBool
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if defaultValue != "" {
|
|
||||||
return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", field.Name, fieldType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it's a pointer
|
|
||||||
if fieldValue.Kind() == reflect.Ptr {
|
|
||||||
// check if the pointer is nil
|
|
||||||
if fieldValue.IsNil() {
|
|
||||||
fieldConfig.IsEmpty = true
|
|
||||||
} else {
|
|
||||||
fieldConfig.CurrentValue = fieldValue.Elem().Addr()
|
|
||||||
fieldConfig.Delegated = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fieldConfig.Delegated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now check if the field is nullable interface
|
|
||||||
switch fieldValue.Interface().(type) {
|
|
||||||
case null.String:
|
|
||||||
if fieldValue.Interface().(null.String).IsZero() {
|
|
||||||
fieldConfig.IsEmpty = true
|
|
||||||
}
|
|
||||||
case null.Int:
|
|
||||||
if fieldValue.Interface().(null.Int).IsZero() {
|
|
||||||
fieldConfig.IsEmpty = true
|
|
||||||
}
|
|
||||||
case null.Bool:
|
|
||||||
if fieldValue.Interface().(null.Bool).IsZero() {
|
|
||||||
fieldConfig.IsEmpty = true
|
|
||||||
}
|
|
||||||
case []string:
|
|
||||||
if len(fieldValue.Interface().([]string)) == 0 {
|
|
||||||
fieldConfig.IsEmpty = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now check if the field has required_if
|
|
||||||
requiredIf := field.Tag.Get("required_if")
|
|
||||||
if requiredIf != "" {
|
|
||||||
requiredIfParts := strings.Split(requiredIf, ",")
|
|
||||||
for _, part := range requiredIfParts {
|
|
||||||
partVal := strings.SplitN(part, "=", 2)
|
|
||||||
if len(partVal) != 2 {
|
|
||||||
return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldConfig.RequiredIf[partVal[0]] = partVal[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the field can use one_of
|
|
||||||
if !canUseOneOff && len(fieldConfig.OneOf) > 0 {
|
|
||||||
return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", field.Name, fieldType)
|
|
||||||
}
|
|
||||||
|
|
||||||
fields[field.Name] = fieldConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateFields(config, fields); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFields(config interface{}, fields map[string]FieldConfig) error {
|
|
||||||
// now we can start to validate the fields
|
|
||||||
for _, fieldConfig := range fields {
|
|
||||||
if err := fieldConfig.validate(fields); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldConfig.populate(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldConfig) validate(fields map[string]FieldConfig) error {
|
|
||||||
var required bool
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if required, err = f.validateRequired(fields); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the field needs to be updated and set defaults if needed
|
|
||||||
if err := f.checkIfFieldNeedsUpdate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// then we can check if the field is one_of
|
|
||||||
if err := f.validateOneOf(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// and validate the type
|
|
||||||
if err := f.validateField(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the field is delegated, we need to validate the nested field
|
|
||||||
// but before that, let's check if the field is required
|
|
||||||
if required && f.Delegated {
|
|
||||||
if err := setDefaultsAndValidate(f.CurrentValue.(reflect.Value).Interface(), false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldConfig) populate(config interface{}) {
|
|
||||||
// update the field if it's not empty
|
|
||||||
if !f.shouldUpdateValue {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reflect.ValueOf(config).Elem().FieldByName(f.Name).Set(reflect.ValueOf(f.CurrentValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldConfig) checkIfFieldNeedsUpdate() error {
|
|
||||||
// populate the field if it's empty and has a default value
|
|
||||||
if f.IsEmpty && f.Defaults != nil {
|
|
||||||
switch f.CurrentValue.(type) {
|
|
||||||
case null.String:
|
|
||||||
f.CurrentValue = null.StringFrom(f.Defaults.(string))
|
|
||||||
case null.Int:
|
|
||||||
f.CurrentValue = null.IntFrom(int64(f.Defaults.(int)))
|
|
||||||
case null.Bool:
|
|
||||||
f.CurrentValue = null.BoolFrom(f.Defaults.(bool))
|
|
||||||
case string:
|
|
||||||
f.CurrentValue = f.Defaults.(string)
|
|
||||||
case int:
|
|
||||||
f.CurrentValue = f.Defaults.(int)
|
|
||||||
case bool:
|
|
||||||
f.CurrentValue = f.Defaults.(bool)
|
|
||||||
case []string:
|
|
||||||
f.CurrentValue = f.Defaults.([]string)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("field `%s` cannot use default value: unsupported type: %s", f.Name, f.TypeString)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.shouldUpdateValue = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldConfig) validateRequired(fields map[string]FieldConfig) (bool, error) {
|
|
||||||
var required = f.Required
|
|
||||||
|
|
||||||
// if the field is not required, we need to check if it's required_if
|
|
||||||
if !required && len(f.RequiredIf) > 0 {
|
|
||||||
for key, value := range f.RequiredIf {
|
|
||||||
// check if the field's result matches the required_if
|
|
||||||
// right now we only support string and int
|
|
||||||
requiredField, ok := fields[key]
|
|
||||||
if !ok {
|
|
||||||
return required, fmt.Errorf("required_if field `%s` not found", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch requiredField.CurrentValue.(type) {
|
|
||||||
case string:
|
|
||||||
if requiredField.CurrentValue.(string) == value.(string) {
|
|
||||||
required = true
|
|
||||||
}
|
|
||||||
case int:
|
|
||||||
if requiredField.CurrentValue.(int) == value.(int) {
|
|
||||||
required = true
|
|
||||||
}
|
|
||||||
case null.String:
|
|
||||||
if !requiredField.CurrentValue.(null.String).IsZero() &&
|
|
||||||
requiredField.CurrentValue.(null.String).String == value.(string) {
|
|
||||||
required = true
|
|
||||||
}
|
|
||||||
case null.Int:
|
|
||||||
if !requiredField.CurrentValue.(null.Int).IsZero() &&
|
|
||||||
requiredField.CurrentValue.(null.Int).Int64 == value.(int64) {
|
|
||||||
required = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the field is required, we can break the loop
|
|
||||||
// because we only need one of the required_if fields to be true
|
|
||||||
if required {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if required && f.IsEmpty {
|
|
||||||
return false, fmt.Errorf("field `%s` is required", f.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return required, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkIfSliceContains(slice []string, one_of []string) bool {
|
|
||||||
for _, oneOf := range one_of {
|
|
||||||
if slices.Contains(slice, oneOf) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldConfig) validateOneOf() error {
|
|
||||||
if len(f.OneOf) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var val []string
|
|
||||||
switch f.CurrentValue.(type) {
|
|
||||||
case string:
|
|
||||||
val = []string{f.CurrentValue.(string)}
|
|
||||||
case null.String:
|
|
||||||
val = []string{f.CurrentValue.(null.String).String}
|
|
||||||
case []string:
|
|
||||||
// let's validate the value here
|
|
||||||
val = f.CurrentValue.([]string)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("field `%s` cannot use one_of: unsupported type: %s", f.Name, f.TypeString)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !checkIfSliceContains(val, f.OneOf) {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"field `%s` is not one of the allowed values: %s, current value: %s",
|
|
||||||
f.Name,
|
|
||||||
strings.Join(f.OneOf, ", "),
|
|
||||||
strings.Join(val, ", "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FieldConfig) validateField() error {
|
|
||||||
if len(f.ValidateTypes) == 0 || f.IsEmpty {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := toString(f.CurrentValue)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if val == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, validateType := range f.ValidateTypes {
|
|
||||||
switch validateType {
|
|
||||||
case "ipv4":
|
|
||||||
if net.ParseIP(val).To4() == nil {
|
|
||||||
return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", f.Name, val)
|
|
||||||
}
|
|
||||||
case "ipv6":
|
|
||||||
if net.ParseIP(val).To16() == nil {
|
|
||||||
return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", f.Name, val)
|
|
||||||
}
|
|
||||||
case "hwaddr":
|
|
||||||
if _, err := net.ParseMAC(val); err != nil {
|
|
||||||
return fmt.Errorf("field `%s` is not a valid MAC address: %s", f.Name, val)
|
|
||||||
}
|
|
||||||
case "hostname":
|
|
||||||
if _, err := idna.Lookup.ToASCII(val); err != nil {
|
|
||||||
return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
package confparser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/guregu/null/v6"
|
|
||||||
)
|
|
||||||
|
|
||||||
type testIPv6Address struct { //nolint:unused
|
|
||||||
Address net.IP `json:"address"`
|
|
||||||
Prefix net.IPNet `json:"prefix"`
|
|
||||||
ValidLifetime *time.Time `json:"valid_lifetime"`
|
|
||||||
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
|
||||||
Scope int `json:"scope"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type testIPv4StaticConfig struct {
|
|
||||||
Address null.String `json:"address" validate_type:"ipv4" required:"true"`
|
|
||||||
Netmask null.String `json:"netmask" validate_type:"ipv4" required:"true"`
|
|
||||||
Gateway null.String `json:"gateway" validate_type:"ipv4" required:"true"`
|
|
||||||
DNS []string `json:"dns" validate_type:"ipv4" required:"true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type testIPv6StaticConfig struct {
|
|
||||||
Address null.String `json:"address" validate_type:"ipv6" required:"true"`
|
|
||||||
Prefix null.String `json:"prefix" validate_type:"ipv6" required:"true"`
|
|
||||||
Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"`
|
|
||||||
DNS []string `json:"dns" validate_type:"ipv6" required:"true"`
|
|
||||||
}
|
|
||||||
type testNetworkConfig struct {
|
|
||||||
Hostname null.String `json:"hostname,omitempty"`
|
|
||||||
Domain null.String `json:"domain,omitempty"`
|
|
||||||
|
|
||||||
IPv4Mode null.String `json:"ipv4_mode" one_of:"dhcp,static,disabled" default:"dhcp"`
|
|
||||||
IPv4Static *testIPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
|
||||||
|
|
||||||
IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
|
||||||
IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
|
||||||
|
|
||||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
|
||||||
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"`
|
|
||||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
|
||||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateConfig(t *testing.T) {
|
|
||||||
config := &testNetworkConfig{}
|
|
||||||
|
|
||||||
err := SetDefaultsAndValidate(config)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateIPv4StaticConfigNetmaskRequiredIfStatic(t *testing.T) {
|
|
||||||
config := &testNetworkConfig{
|
|
||||||
IPv4Static: &testIPv4StaticConfig{
|
|
||||||
Address: null.StringFrom("192.168.1.1"),
|
|
||||||
Gateway: null.StringFrom("192.168.1.1"),
|
|
||||||
},
|
|
||||||
IPv4Mode: null.StringFrom("static"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := SetDefaultsAndValidate(config)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateIPv4StaticConfigNetmaskNotRequiredIfStatic(t *testing.T) {
|
|
||||||
config := &testNetworkConfig{
|
|
||||||
IPv4Static: &testIPv4StaticConfig{
|
|
||||||
Address: null.StringFrom("192.168.1.1"),
|
|
||||||
Gateway: null.StringFrom("192.168.1.1"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := SetDefaultsAndValidate(config)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateIPv4StaticConfigRequiredIf(t *testing.T) {
|
|
||||||
config := &testNetworkConfig{
|
|
||||||
IPv4Mode: null.StringFrom("static"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := SetDefaultsAndValidate(config)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateIPv4StaticConfigValidateType(t *testing.T) {
|
|
||||||
config := &testNetworkConfig{
|
|
||||||
IPv4Static: &testIPv4StaticConfig{
|
|
||||||
Address: null.StringFrom("X"),
|
|
||||||
Netmask: null.StringFrom("255.255.255.0"),
|
|
||||||
Gateway: null.StringFrom("192.168.1.1"),
|
|
||||||
DNS: []string{"8.8.8.8", "8.8.4.4"},
|
|
||||||
},
|
|
||||||
IPv4Mode: null.StringFrom("static"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := SetDefaultsAndValidate(config)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error, got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package confparser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/guregu/null/v6"
|
|
||||||
)
|
|
||||||
|
|
||||||
func splitString(s string) []string {
|
|
||||||
if s == "" {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Split(s, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func toString(v interface{}) (string, error) {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case string:
|
|
||||||
return v, nil
|
|
||||||
case null.String:
|
|
||||||
return v.String, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("unsupported type: %s", reflect.TypeOf(v))
|
|
||||||
}
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logger struct {
|
|
||||||
l *zerolog.Logger
|
|
||||||
scopeLoggers map[string]*zerolog.Logger
|
|
||||||
scopeLevels map[string]zerolog.Level
|
|
||||||
scopeLevelMutex sync.Mutex
|
|
||||||
|
|
||||||
defaultLogLevelFromEnv zerolog.Level
|
|
||||||
defaultLogLevelFromConfig zerolog.Level
|
|
||||||
defaultLogLevel zerolog.Level
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultLogLevel = zerolog.ErrorLevel
|
|
||||||
)
|
|
||||||
|
|
||||||
type logOutput struct {
|
|
||||||
mu *sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *logOutput) Write(p []byte) (n int, err error) {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
|
|
||||||
// TODO: write to file or syslog
|
|
||||||
if sseServer != nil {
|
|
||||||
// use a goroutine to avoid blocking the Write method
|
|
||||||
go func() {
|
|
||||||
sseServer.Message <- string(p)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
consoleLogOutput io.Writer = zerolog.ConsoleWriter{
|
|
||||||
Out: os.Stdout,
|
|
||||||
TimeFormat: time.RFC3339,
|
|
||||||
PartsOrder: []string{"time", "level", "scope", "component", "message"},
|
|
||||||
FieldsExclude: []string{"scope", "component"},
|
|
||||||
FormatPartValueByName: func(value interface{}, name string) string {
|
|
||||||
val := fmt.Sprintf("%s", value)
|
|
||||||
if name == "component" {
|
|
||||||
if value == nil {
|
|
||||||
return "-"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
},
|
|
||||||
}
|
|
||||||
fileLogOutput io.Writer = &logOutput{mu: &sync.Mutex{}}
|
|
||||||
defaultLogOutput = zerolog.MultiLevelWriter(consoleLogOutput, fileLogOutput)
|
|
||||||
|
|
||||||
zerologLevels = map[string]zerolog.Level{
|
|
||||||
"DISABLE": zerolog.Disabled,
|
|
||||||
"NOLEVEL": zerolog.NoLevel,
|
|
||||||
"PANIC": zerolog.PanicLevel,
|
|
||||||
"FATAL": zerolog.FatalLevel,
|
|
||||||
"ERROR": zerolog.ErrorLevel,
|
|
||||||
"WARN": zerolog.WarnLevel,
|
|
||||||
"INFO": zerolog.InfoLevel,
|
|
||||||
"DEBUG": zerolog.DebugLevel,
|
|
||||||
"TRACE": zerolog.TraceLevel,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewLogger(zerologLogger zerolog.Logger) *Logger {
|
|
||||||
return &Logger{
|
|
||||||
l: &zerologLogger,
|
|
||||||
scopeLoggers: make(map[string]*zerolog.Logger),
|
|
||||||
scopeLevels: make(map[string]zerolog.Level),
|
|
||||||
scopeLevelMutex: sync.Mutex{},
|
|
||||||
defaultLogLevelFromEnv: -2,
|
|
||||||
defaultLogLevelFromConfig: -2,
|
|
||||||
defaultLogLevel: defaultLogLevel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) updateLogLevel() {
|
|
||||||
l.scopeLevelMutex.Lock()
|
|
||||||
defer l.scopeLevelMutex.Unlock()
|
|
||||||
|
|
||||||
l.scopeLevels = make(map[string]zerolog.Level)
|
|
||||||
|
|
||||||
finalDefaultLogLevel := l.defaultLogLevel
|
|
||||||
|
|
||||||
for name, level := range zerologLevels {
|
|
||||||
env := os.Getenv(fmt.Sprintf("JETKVM_LOG_%s", name))
|
|
||||||
|
|
||||||
if env == "" {
|
|
||||||
env = os.Getenv(fmt.Sprintf("PION_LOG_%s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
if env == "" {
|
|
||||||
env = os.Getenv(fmt.Sprintf("PIONS_LOG_%s", name))
|
|
||||||
}
|
|
||||||
|
|
||||||
if env == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.ToLower(env) == "all" {
|
|
||||||
l.defaultLogLevelFromEnv = level
|
|
||||||
|
|
||||||
if finalDefaultLogLevel > level {
|
|
||||||
finalDefaultLogLevel = level
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
scopes := strings.Split(strings.ToLower(env), ",")
|
|
||||||
for _, scope := range scopes {
|
|
||||||
l.scopeLevels[scope] = level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.defaultLogLevel = finalDefaultLogLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) getScopeLoggerLevel(scope string) zerolog.Level {
|
|
||||||
if l.scopeLevels == nil {
|
|
||||||
l.updateLogLevel()
|
|
||||||
}
|
|
||||||
|
|
||||||
var scopeLevel zerolog.Level
|
|
||||||
if l.defaultLogLevelFromConfig != -2 {
|
|
||||||
scopeLevel = l.defaultLogLevelFromConfig
|
|
||||||
}
|
|
||||||
if l.defaultLogLevelFromEnv != -2 {
|
|
||||||
scopeLevel = l.defaultLogLevelFromEnv
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the scope is not in the map, use the default level from the root logger
|
|
||||||
if level, ok := l.scopeLevels[scope]; ok {
|
|
||||||
scopeLevel = level
|
|
||||||
}
|
|
||||||
|
|
||||||
return scopeLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) newScopeLogger(scope string) zerolog.Logger {
|
|
||||||
scopeLevel := l.getScopeLoggerLevel(scope)
|
|
||||||
logger := l.l.Level(scopeLevel).With().Str("component", scope).Logger()
|
|
||||||
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) getLogger(scope string) *zerolog.Logger {
|
|
||||||
logger, ok := l.scopeLoggers[scope]
|
|
||||||
if !ok || logger == nil {
|
|
||||||
scopeLogger := l.newScopeLogger(scope)
|
|
||||||
l.scopeLoggers[scope] = &scopeLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
return l.scopeLoggers[scope]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) UpdateLogLevel(configDefaultLogLevel string) {
|
|
||||||
needUpdate := false
|
|
||||||
|
|
||||||
if configDefaultLogLevel != "" {
|
|
||||||
if logLevel, ok := zerologLevels[configDefaultLogLevel]; ok {
|
|
||||||
l.defaultLogLevelFromConfig = logLevel
|
|
||||||
} else {
|
|
||||||
l.l.Warn().Str("logLevel", configDefaultLogLevel).Msg("invalid defaultLogLevel from config, using ERROR")
|
|
||||||
}
|
|
||||||
|
|
||||||
if l.defaultLogLevelFromConfig != l.defaultLogLevel {
|
|
||||||
needUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.updateLogLevel()
|
|
||||||
|
|
||||||
if needUpdate {
|
|
||||||
for scope, logger := range l.scopeLoggers {
|
|
||||||
currentLevel := logger.GetLevel()
|
|
||||||
targetLevel := l.getScopeLoggerLevel(scope)
|
|
||||||
if currentLevel != targetLevel {
|
|
||||||
*logger = l.newScopeLogger(scope)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pion/logging"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type pionLogger struct {
|
|
||||||
logger *zerolog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print all messages except trace.
|
|
||||||
func (c pionLogger) Trace(msg string) {
|
|
||||||
c.logger.Trace().Msg(msg)
|
|
||||||
}
|
|
||||||
func (c pionLogger) Tracef(format string, args ...interface{}) {
|
|
||||||
c.logger.Trace().Msgf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c pionLogger) Debug(msg string) {
|
|
||||||
c.logger.Debug().Msg(msg)
|
|
||||||
}
|
|
||||||
func (c pionLogger) Debugf(format string, args ...interface{}) {
|
|
||||||
c.logger.Debug().Msgf(format, args...)
|
|
||||||
}
|
|
||||||
func (c pionLogger) Info(msg string) {
|
|
||||||
c.logger.Info().Msg(msg)
|
|
||||||
}
|
|
||||||
func (c pionLogger) Infof(format string, args ...interface{}) {
|
|
||||||
c.logger.Info().Msgf(format, args...)
|
|
||||||
}
|
|
||||||
func (c pionLogger) Warn(msg string) {
|
|
||||||
c.logger.Warn().Msg(msg)
|
|
||||||
}
|
|
||||||
func (c pionLogger) Warnf(format string, args ...interface{}) {
|
|
||||||
c.logger.Warn().Msgf(format, args...)
|
|
||||||
}
|
|
||||||
func (c pionLogger) Error(msg string) {
|
|
||||||
c.logger.Error().Msg(msg)
|
|
||||||
}
|
|
||||||
func (c pionLogger) Errorf(format string, args ...interface{}) {
|
|
||||||
c.logger.Error().Msgf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// customLoggerFactory satisfies the interface logging.LoggerFactory
|
|
||||||
// This allows us to create different loggers per subsystem. So we can
|
|
||||||
// add custom behavior.
|
|
||||||
type pionLoggerFactory struct{}
|
|
||||||
|
|
||||||
func (c pionLoggerFactory) NewLogger(subsystem string) logging.LeveledLogger {
|
|
||||||
logger := rootLogger.getLogger(subsystem).With().
|
|
||||||
Str("scope", "pion").
|
|
||||||
Str("component", subsystem).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
return pionLogger{logger: &logger}
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultLoggerFactory = &pionLoggerFactory{}
|
|
||||||
|
|
||||||
func GetPionDefaultLoggerFactory() logging.LoggerFactory {
|
|
||||||
return defaultLoggerFactory
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package logging
|
|
||||||
|
|
||||||
import "github.com/rs/zerolog"
|
|
||||||
|
|
||||||
var (
|
|
||||||
rootZerologLogger = zerolog.New(defaultLogOutput).With().
|
|
||||||
Str("scope", "jetkvm").
|
|
||||||
Timestamp().
|
|
||||||
Stack().
|
|
||||||
Logger()
|
|
||||||
rootLogger = NewLogger(rootZerologLogger)
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetRootLogger() *Logger {
|
|
||||||
return rootLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSubsystemLogger(subsystem string) *zerolog.Logger {
|
|
||||||
return rootLogger.getLogger(subsystem)
|
|
||||||
}
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed sse.html
|
|
||||||
var sseHTML embed.FS
|
|
||||||
|
|
||||||
type sseEvent struct {
|
|
||||||
Message chan string
|
|
||||||
NewClients chan chan string
|
|
||||||
ClosedClients chan chan string
|
|
||||||
TotalClients map[chan string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// New event messages are broadcast to all registered client connection channels
|
|
||||||
type sseClientChan chan string
|
|
||||||
|
|
||||||
var (
|
|
||||||
sseServer *sseEvent
|
|
||||||
sseLogger *zerolog.Logger
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
sseServer = newSseServer()
|
|
||||||
sseLogger = GetSubsystemLogger("sse")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize event and Start procnteessing requests
|
|
||||||
func newSseServer() (event *sseEvent) {
|
|
||||||
event = &sseEvent{
|
|
||||||
Message: make(chan string),
|
|
||||||
NewClients: make(chan chan string),
|
|
||||||
ClosedClients: make(chan chan string),
|
|
||||||
TotalClients: make(map[chan string]bool),
|
|
||||||
}
|
|
||||||
|
|
||||||
go event.listen()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// It Listens all incoming requests from clients.
|
|
||||||
// Handles addition and removal of clients and broadcast messages to clients.
|
|
||||||
func (stream *sseEvent) listen() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
// Add new available client
|
|
||||||
case client := <-stream.NewClients:
|
|
||||||
stream.TotalClients[client] = true
|
|
||||||
sseLogger.Info().
|
|
||||||
Int("total_clients", len(stream.TotalClients)).
|
|
||||||
Msg("new client connected")
|
|
||||||
|
|
||||||
// Remove closed client
|
|
||||||
case client := <-stream.ClosedClients:
|
|
||||||
delete(stream.TotalClients, client)
|
|
||||||
close(client)
|
|
||||||
sseLogger.Info().Int("total_clients", len(stream.TotalClients)).Msg("client disconnected")
|
|
||||||
|
|
||||||
// Broadcast message to client
|
|
||||||
case eventMsg := <-stream.Message:
|
|
||||||
for clientMessageChan := range stream.TotalClients {
|
|
||||||
select {
|
|
||||||
case clientMessageChan <- eventMsg:
|
|
||||||
// Message sent successfully
|
|
||||||
default:
|
|
||||||
// Failed to send, dropping message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stream *sseEvent) serveHTTP() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
clientChan := make(sseClientChan)
|
|
||||||
stream.NewClients <- clientChan
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-c.Writer.CloseNotify()
|
|
||||||
|
|
||||||
for range clientChan {
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.ClosedClients <- clientChan
|
|
||||||
}()
|
|
||||||
|
|
||||||
c.Set("clientChan", clientChan)
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sseHeadersMiddleware() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
if c.Request.Method == "GET" && c.NegotiateFormat(gin.MIMEHTML) == gin.MIMEHTML {
|
|
||||||
c.FileFromFS("/sse.html", http.FS(sseHTML))
|
|
||||||
c.Status(http.StatusOK)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
|
||||||
c.Writer.Header().Set("Connection", "keep-alive")
|
|
||||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AttachSSEHandler(router *gin.RouterGroup) {
|
|
||||||
router.StaticFS("/log-stream", http.FS(sseHTML))
|
|
||||||
router.GET("/log-stream", sseHeadersMiddleware(), sseServer.serveHTTP(), func(c *gin.Context) {
|
|
||||||
v, ok := c.Get("clientChan")
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clientChan, ok := v.(sseClientChan)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
|
||||||
if msg, ok := <-clientChan; ok {
|
|
||||||
c.SSEvent("message", msg)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Server Sent Event</title>
|
|
||||||
<style>
|
|
||||||
.main-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
font-family: 'Hack', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry > span {
|
|
||||||
min-width: 0;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-break: break-word;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry > span:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry.log-entry-trace .log-level {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry.log-entry-debug .log-level {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry.log-entry-info .log-level {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry.log-entry-warn .log-level {
|
|
||||||
color: yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry.log-entry-error .log-level,
|
|
||||||
.log-entry.log-entry-fatal .log-level,
|
|
||||||
.log-entry.log-entry-panic .log-level {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-entry.log-entry-info .log-message,
|
|
||||||
.log-entry.log-entry-warn .log-message,
|
|
||||||
.log-entry.log-entry-error .log-message,
|
|
||||||
.log-entry.log-entry-fatal .log-message,
|
|
||||||
.log-entry.log-entry-panic .log-message {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-timestamp {
|
|
||||||
color: #666;
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-level {
|
|
||||||
font-size: 12px;
|
|
||||||
min-width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-scope {
|
|
||||||
font-size: 12px;
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-component {
|
|
||||||
font-size: 12px;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-message {
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-extras {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
.log-extras .log-extras-header {
|
|
||||||
font-weight: bold;
|
|
||||||
color:cornflowerblue;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="main-container">
|
|
||||||
<div id="header">
|
|
||||||
<span id="loading">
|
|
||||||
Connecting to log stream...
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span id="stats">
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div id="event-data">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
class LogStream {
|
|
||||||
constructor(url, eventDataElement, loadingElement, statsElement) {
|
|
||||||
this.url = url;
|
|
||||||
this.eventDataElement = eventDataElement;
|
|
||||||
this.loadingElement = loadingElement;
|
|
||||||
this.statsElement = statsElement;
|
|
||||||
this.stream = null;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
this.maxReconnectAttempts = 10;
|
|
||||||
this.reconnectDelay = 1000; // Start with 1 second
|
|
||||||
this.maxReconnectDelay = 30000; // Max 30 seconds
|
|
||||||
this.isConnecting = false;
|
|
||||||
|
|
||||||
this.totalMessages = 0;
|
|
||||||
|
|
||||||
this.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
if (this.isConnecting) return;
|
|
||||||
this.isConnecting = true;
|
|
||||||
|
|
||||||
this.loadingElement.innerText = "Connecting to log stream...";
|
|
||||||
|
|
||||||
this.stream = new EventSource(this.url);
|
|
||||||
|
|
||||||
this.stream.onopen = () => {
|
|
||||||
this.isConnecting = false;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
this.reconnectDelay = 1000;
|
|
||||||
this.loadingElement.innerText = "Log stream connected.";
|
|
||||||
|
|
||||||
|
|
||||||
this.totalMessages = 0;
|
|
||||||
this.totalBytes = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.stream.onmessage = (event) => {
|
|
||||||
this.totalBytes += event.data.length;
|
|
||||||
this.totalMessages++;
|
|
||||||
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
this.addLogEntry(data);
|
|
||||||
this.updateStats();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.stream.onerror = () => {
|
|
||||||
this.isConnecting = false;
|
|
||||||
this.loadingElement.innerText = "Log stream disconnected.";
|
|
||||||
this.stream.close();
|
|
||||||
this.handleReconnect();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStats() {
|
|
||||||
this.statsElement.innerHTML = `Messages: <strong>${this.totalMessages}</strong>, Bytes: <strong>${this.totalBytes}</strong> `;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleReconnect() {
|
|
||||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
||||||
this.loadingElement.innerText = "Failed to reconnect after multiple attempts";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reconnectAttempts++;
|
|
||||||
this.reconnectDelay = Math.min(this.reconnectDelay * 1, this.maxReconnectDelay);
|
|
||||||
|
|
||||||
this.loadingElement.innerText = `Reconnecting in ${this.reconnectDelay/1000} seconds... (Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.connect();
|
|
||||||
}, this.reconnectDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
addLogEntry(data) {
|
|
||||||
const el = document.createElement("div");
|
|
||||||
el.className = "log-entry log-entry-" + data.level;
|
|
||||||
|
|
||||||
const timestamp = document.createElement("span");
|
|
||||||
timestamp.className = "log-timestamp";
|
|
||||||
timestamp.innerText = data.time;
|
|
||||||
el.appendChild(timestamp);
|
|
||||||
|
|
||||||
const level = document.createElement("span");
|
|
||||||
level.className = "log-level";
|
|
||||||
level.innerText = this.shortLogLevel(data.level);
|
|
||||||
el.appendChild(level);
|
|
||||||
|
|
||||||
const scope = document.createElement("span");
|
|
||||||
scope.className = "log-scope";
|
|
||||||
scope.innerText = data.scope;
|
|
||||||
el.appendChild(scope);
|
|
||||||
|
|
||||||
const component = document.createElement("span");
|
|
||||||
component.className = "log-component";
|
|
||||||
component.innerText = data.component;
|
|
||||||
el.appendChild(component);
|
|
||||||
|
|
||||||
const message = document.createElement("span");
|
|
||||||
message.className = "log-message";
|
|
||||||
message.innerText = data.message;
|
|
||||||
el.appendChild(message);
|
|
||||||
|
|
||||||
this.addLogExtras(el, data);
|
|
||||||
|
|
||||||
this.eventDataElement.appendChild(el);
|
|
||||||
|
|
||||||
window.scrollTo(0, document.body.scrollHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
shortLogLevel(level) {
|
|
||||||
switch (level) {
|
|
||||||
case "trace":
|
|
||||||
return "TRC";
|
|
||||||
case "debug":
|
|
||||||
return "DBG";
|
|
||||||
case "info":
|
|
||||||
return "INF";
|
|
||||||
case "warn":
|
|
||||||
return "WRN";
|
|
||||||
case "error":
|
|
||||||
return "ERR";
|
|
||||||
case "fatal":
|
|
||||||
return "FTL";
|
|
||||||
case "panic":
|
|
||||||
return "PNC";
|
|
||||||
default:
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addLogExtras(el, data) {
|
|
||||||
const excludeKeys = [
|
|
||||||
"timestamp",
|
|
||||||
"time",
|
|
||||||
"level",
|
|
||||||
"scope",
|
|
||||||
"component",
|
|
||||||
"message",
|
|
||||||
];
|
|
||||||
|
|
||||||
const extras = {};
|
|
||||||
for (const key in data) {
|
|
||||||
if (excludeKeys.includes(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
extras[key] = data[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in extras) {
|
|
||||||
const extra = document.createElement("span");
|
|
||||||
extra.className = "log-extras log-extras-" + key;
|
|
||||||
|
|
||||||
const extraKey = document.createElement("span");
|
|
||||||
extraKey.className = "log-extras-header";
|
|
||||||
extraKey.innerText = key + '=';
|
|
||||||
extra.appendChild(extraKey);
|
|
||||||
|
|
||||||
const extraValue = document.createElement("span");
|
|
||||||
extraValue.className = "log-extras-value";
|
|
||||||
|
|
||||||
let value = extras[key];
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
value = JSON.stringify(value);
|
|
||||||
}
|
|
||||||
extraValue.innerText = value;
|
|
||||||
extra.appendChild(extraValue);
|
|
||||||
|
|
||||||
el.appendChild(extra);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
if (this.stream) {
|
|
||||||
this.stream.close();
|
|
||||||
this.stream = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the log stream when the page loads
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const logStream = new LogStream(
|
|
||||||
"/developer/log-stream",
|
|
||||||
document.getElementById("event-data"),
|
|
||||||
document.getElementById("loading"),
|
|
||||||
document.getElementById("stats"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up when the page is unloaded
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
logStream.disconnect();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
|
|
||||||
|
|
||||||
func GetDefaultLogger() *zerolog.Logger {
|
|
||||||
return &defaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error {
|
|
||||||
// TODO: move rootLogger to logging package
|
|
||||||
if l == nil {
|
|
||||||
l = &defaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Error().Err(err).Msgf(format, args...)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return fmt.Errorf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
err_msg := err.Error() + ": %v"
|
|
||||||
err_args := append(args, err)
|
|
||||||
|
|
||||||
return fmt.Errorf(err_msg, err_args...)
|
|
||||||
}
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
package mdns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
|
||||||
pion_mdns "github.com/pion/mdns/v2"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"golang.org/x/net/ipv4"
|
|
||||||
"golang.org/x/net/ipv6"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MDNS struct {
|
|
||||||
conn *pion_mdns.Conn
|
|
||||||
lock sync.Mutex
|
|
||||||
l *zerolog.Logger
|
|
||||||
|
|
||||||
localNames []string
|
|
||||||
listenOptions *MDNSListenOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
type MDNSListenOptions struct {
|
|
||||||
IPv4 bool
|
|
||||||
IPv6 bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type MDNSOptions struct {
|
|
||||||
Logger *zerolog.Logger
|
|
||||||
LocalNames []string
|
|
||||||
ListenOptions *MDNSListenOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultAddressIPv4 = pion_mdns.DefaultAddressIPv4
|
|
||||||
DefaultAddressIPv6 = pion_mdns.DefaultAddressIPv6
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewMDNS(opts *MDNSOptions) (*MDNS, error) {
|
|
||||||
if opts.Logger == nil {
|
|
||||||
opts.Logger = logging.GetDefaultLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.ListenOptions == nil {
|
|
||||||
opts.ListenOptions = &MDNSListenOptions{
|
|
||||||
IPv4: true,
|
|
||||||
IPv6: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MDNS{
|
|
||||||
l: opts.Logger,
|
|
||||||
lock: sync.Mutex{},
|
|
||||||
localNames: opts.LocalNames,
|
|
||||||
listenOptions: opts.ListenOptions,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MDNS) start(allowRestart bool) error {
|
|
||||||
m.lock.Lock()
|
|
||||||
defer m.lock.Unlock()
|
|
||||||
|
|
||||||
if m.conn != nil {
|
|
||||||
if !allowRestart {
|
|
||||||
return fmt.Errorf("mDNS server already running")
|
|
||||||
}
|
|
||||||
|
|
||||||
m.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.listenOptions == nil {
|
|
||||||
return fmt.Errorf("listen options not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.listenOptions.IPv4 && !m.listenOptions.IPv6 {
|
|
||||||
m.l.Info().Msg("mDNS server disabled")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
addr4, addr6 *net.UDPAddr
|
|
||||||
l4, l6 *net.UDPConn
|
|
||||||
p4 *ipv4.PacketConn
|
|
||||||
p6 *ipv6.PacketConn
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if m.listenOptions.IPv4 {
|
|
||||||
addr4, err = net.ResolveUDPAddr("udp4", DefaultAddressIPv4)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l4, err = net.ListenUDP("udp4", addr4)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p4 = ipv4.NewPacketConn(l4)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.listenOptions.IPv6 {
|
|
||||||
addr6, err = net.ResolveUDPAddr("udp6", DefaultAddressIPv6)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l6, err = net.ListenUDP("udp6", addr6)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p6 = ipv6.NewPacketConn(l6)
|
|
||||||
}
|
|
||||||
|
|
||||||
scopeLogger := m.l.With().
|
|
||||||
Interface("local_names", m.localNames).
|
|
||||||
Bool("ipv4", m.listenOptions.IPv4).
|
|
||||||
Bool("ipv6", m.listenOptions.IPv6).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
newLocalNames := make([]string, len(m.localNames))
|
|
||||||
for i, name := range m.localNames {
|
|
||||||
newLocalNames[i] = strings.TrimRight(strings.ToLower(name), ".")
|
|
||||||
if !strings.HasSuffix(newLocalNames[i], ".local") {
|
|
||||||
newLocalNames[i] = newLocalNames[i] + ".local"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mDNSConn, err := pion_mdns.Server(p4, p6, &pion_mdns.Config{
|
|
||||||
LocalNames: newLocalNames,
|
|
||||||
LoggerFactory: logging.GetPionDefaultLoggerFactory(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
scopeLogger.Warn().Err(err).Msg("failed to start mDNS server")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.conn = mDNSConn
|
|
||||||
scopeLogger.Info().Msg("mDNS server started")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MDNS) Start() error {
|
|
||||||
return m.start(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MDNS) Restart() error {
|
|
||||||
return m.start(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MDNS) Stop() error {
|
|
||||||
m.lock.Lock()
|
|
||||||
defer m.lock.Unlock()
|
|
||||||
|
|
||||||
if m.conn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MDNS) SetLocalNames(localNames []string, always bool) error {
|
|
||||||
if reflect.DeepEqual(m.localNames, localNames) && !always {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.localNames = localNames
|
|
||||||
_ = m.Restart()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error {
|
|
||||||
if m.listenOptions != nil &&
|
|
||||||
m.listenOptions.IPv4 == listenOptions.IPv4 &&
|
|
||||||
m.listenOptions.IPv6 == listenOptions.IPv6 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.listenOptions = listenOptions
|
|
||||||
_ = m.Restart()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
package mdns
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/guregu/null/v6"
|
|
||||||
"github.com/jetkvm/kvm/internal/mdns"
|
|
||||||
"golang.org/x/net/idna"
|
|
||||||
)
|
|
||||||
|
|
||||||
type IPv6Address struct {
|
|
||||||
Address net.IP `json:"address"`
|
|
||||||
Prefix net.IPNet `json:"prefix"`
|
|
||||||
ValidLifetime *time.Time `json:"valid_lifetime"`
|
|
||||||
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
|
||||||
Scope int `json:"scope"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPv4StaticConfig struct {
|
|
||||||
Address null.String `json:"address,omitempty" validate_type:"ipv4" required:"true"`
|
|
||||||
Netmask null.String `json:"netmask,omitempty" validate_type:"ipv4" required:"true"`
|
|
||||||
Gateway null.String `json:"gateway,omitempty" validate_type:"ipv4" required:"true"`
|
|
||||||
DNS []string `json:"dns,omitempty" validate_type:"ipv4" required:"true"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type IPv6StaticConfig struct {
|
|
||||||
Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"`
|
|
||||||
Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6" required:"true"`
|
|
||||||
Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"`
|
|
||||||
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"`
|
|
||||||
|
|
||||||
IPv4Mode null.String `json:"ipv4_mode,omitempty" one_of:"dhcp,static,disabled" default:"dhcp"`
|
|
||||||
IPv4Static *IPv4StaticConfig `json:"ipv4_static,omitempty" required_if:"IPv4Mode=static"`
|
|
||||||
|
|
||||||
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
|
||||||
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
|
||||||
|
|
||||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
|
||||||
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"`
|
|
||||||
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
|
|
||||||
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
|
|
||||||
mode := c.MDNSMode.String
|
|
||||||
listenOptions := &mdns.MDNSListenOptions{
|
|
||||||
IPv4: true,
|
|
||||||
IPv6: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch mode {
|
|
||||||
case "ipv4_only":
|
|
||||||
listenOptions.IPv6 = false
|
|
||||||
case "ipv6_only":
|
|
||||||
listenOptions.IPv4 = false
|
|
||||||
case "disabled":
|
|
||||||
listenOptions.IPv4 = false
|
|
||||||
listenOptions.IPv6 = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return listenOptions
|
|
||||||
}
|
|
||||||
func (s *NetworkInterfaceState) GetHostname() string {
|
|
||||||
hostname := ToValidHostname(s.config.Hostname.String)
|
|
||||||
|
|
||||||
if hostname == "" {
|
|
||||||
return s.defaultHostname
|
|
||||||
}
|
|
||||||
|
|
||||||
return hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToValidDomain(domain string) string {
|
|
||||||
ascii, err := idna.Lookup.ToASCII(domain)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return ascii
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) GetDomain() string {
|
|
||||||
domain := ToValidDomain(s.config.Domain.String)
|
|
||||||
|
|
||||||
if domain == "" {
|
|
||||||
lease := s.dhcpClient.GetLease()
|
|
||||||
if lease != nil && lease.Domain != "" {
|
|
||||||
domain = ToValidDomain(lease.Domain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if domain == "" {
|
|
||||||
return "local"
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) GetFQDN() string {
|
|
||||||
return fmt.Sprintf("%s.%s", s.GetHostname(), s.GetDomain())
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
type DhcpTargetState int
|
|
||||||
|
|
||||||
const (
|
|
||||||
DhcpTargetStateDoNothing DhcpTargetState = iota
|
|
||||||
DhcpTargetStateStart
|
|
||||||
DhcpTargetStateStop
|
|
||||||
DhcpTargetStateRenew
|
|
||||||
DhcpTargetStateRelease
|
|
||||||
)
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"golang.org/x/net/idna"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
hostnamePath = "/etc/hostname"
|
|
||||||
hostsPath = "/etc/hosts"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
hostnameLock sync.Mutex = sync.Mutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func updateEtcHosts(hostname string, fqdn string) error {
|
|
||||||
// update /etc/hosts
|
|
||||||
hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
defer hostsFile.Close()
|
|
||||||
|
|
||||||
// read all lines
|
|
||||||
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
lines, err := io.ReadAll(hostsFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newLines := []string{}
|
|
||||||
hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn)
|
|
||||||
hostLineExists := false
|
|
||||||
|
|
||||||
for _, line := range strings.Split(string(lines), "\n") {
|
|
||||||
if strings.HasPrefix(line, "127.0.1.1") {
|
|
||||||
hostLineExists = true
|
|
||||||
line = hostLine
|
|
||||||
}
|
|
||||||
newLines = append(newLines, line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hostLineExists {
|
|
||||||
newLines = append(newLines, hostLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := hostsFile.Truncate(0); err != nil {
|
|
||||||
return fmt.Errorf("failed to truncate %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := hostsFile.Write([]byte(strings.Join(newLines, "\n"))); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", hostsPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToValidHostname(hostname string) string {
|
|
||||||
ascii, err := idna.Lookup.ToASCII(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return ascii
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetHostname(hostname string, fqdn string) error {
|
|
||||||
hostnameLock.Lock()
|
|
||||||
defer hostnameLock.Unlock()
|
|
||||||
|
|
||||||
hostname = ToValidHostname(strings.TrimSpace(hostname))
|
|
||||||
fqdn = ToValidHostname(strings.TrimSpace(fqdn))
|
|
||||||
|
|
||||||
if hostname == "" {
|
|
||||||
return fmt.Errorf("invalid hostname: %s", hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fqdn == "" {
|
|
||||||
fqdn = hostname
|
|
||||||
}
|
|
||||||
|
|
||||||
// update /etc/hostname
|
|
||||||
if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write %s: %w", hostnamePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update /etc/hosts
|
|
||||||
if err := updateEtcHosts(hostname, fqdn); err != nil {
|
|
||||||
return fmt.Errorf("failed to update /etc/hosts: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// run hostname
|
|
||||||
if err := exec.Command("hostname", "-F", hostnamePath).Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to run hostname: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) setHostnameIfNotSame() error {
|
|
||||||
hostname := s.GetHostname()
|
|
||||||
currentHostname, _ := os.Hostname()
|
|
||||||
|
|
||||||
fqdn := fmt.Sprintf("%s.%s", hostname, s.GetDomain())
|
|
||||||
|
|
||||||
if currentHostname == hostname && s.currentFqdn == fqdn && s.currentHostname == hostname {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
scopedLogger := s.l.With().Str("hostname", hostname).Str("fqdn", fqdn).Logger()
|
|
||||||
|
|
||||||
err := SetHostname(hostname, fqdn)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Error().Err(err).Msg("failed to set hostname")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.currentHostname = hostname
|
|
||||||
s.currentFqdn = fqdn
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("hostname set")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,346 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/confparser"
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
|
||||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NetworkInterfaceState struct {
|
|
||||||
interfaceName string
|
|
||||||
interfaceUp bool
|
|
||||||
ipv4Addr *net.IP
|
|
||||||
ipv4Addresses []string
|
|
||||||
ipv6Addr *net.IP
|
|
||||||
ipv6Addresses []IPv6Address
|
|
||||||
ipv6LinkLocal *net.IP
|
|
||||||
macAddr *net.HardwareAddr
|
|
||||||
|
|
||||||
l *zerolog.Logger
|
|
||||||
stateLock sync.Mutex
|
|
||||||
|
|
||||||
config *NetworkConfig
|
|
||||||
dhcpClient *udhcpc.DHCPClient
|
|
||||||
|
|
||||||
defaultHostname string
|
|
||||||
currentHostname string
|
|
||||||
currentFqdn string
|
|
||||||
|
|
||||||
onStateChange func(state *NetworkInterfaceState)
|
|
||||||
onInitialCheck func(state *NetworkInterfaceState)
|
|
||||||
cbConfigChange func(config *NetworkConfig)
|
|
||||||
|
|
||||||
checked bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type NetworkInterfaceOptions struct {
|
|
||||||
InterfaceName string
|
|
||||||
DhcpPidFile string
|
|
||||||
Logger *zerolog.Logger
|
|
||||||
DefaultHostname string
|
|
||||||
OnStateChange func(state *NetworkInterfaceState)
|
|
||||||
OnInitialCheck func(state *NetworkInterfaceState)
|
|
||||||
OnDhcpLeaseChange func(lease *udhcpc.Lease)
|
|
||||||
OnConfigChange func(config *NetworkConfig)
|
|
||||||
NetworkConfig *NetworkConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceState, error) {
|
|
||||||
if opts.NetworkConfig == nil {
|
|
||||||
return nil, fmt.Errorf("NetworkConfig can not be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.DefaultHostname == "" {
|
|
||||||
opts.DefaultHostname = "jetkvm"
|
|
||||||
}
|
|
||||||
|
|
||||||
err := confparser.SetDefaultsAndValidate(opts.NetworkConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l := opts.Logger
|
|
||||||
s := &NetworkInterfaceState{
|
|
||||||
interfaceName: opts.InterfaceName,
|
|
||||||
defaultHostname: opts.DefaultHostname,
|
|
||||||
stateLock: sync.Mutex{},
|
|
||||||
l: l,
|
|
||||||
onStateChange: opts.OnStateChange,
|
|
||||||
onInitialCheck: opts.OnInitialCheck,
|
|
||||||
cbConfigChange: opts.OnConfigChange,
|
|
||||||
config: opts.NetworkConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the dhcp client
|
|
||||||
dhcpClient := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{
|
|
||||||
InterfaceName: opts.InterfaceName,
|
|
||||||
PidFile: opts.DhcpPidFile,
|
|
||||||
Logger: l,
|
|
||||||
OnLeaseChange: func(lease *udhcpc.Lease) {
|
|
||||||
_, err := s.update()
|
|
||||||
if err != nil {
|
|
||||||
opts.Logger.Error().Err(err).Msg("failed to update network state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = s.setHostnameIfNotSame()
|
|
||||||
|
|
||||||
opts.OnDhcpLeaseChange(lease)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
s.dhcpClient = dhcpClient
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IsUp() bool {
|
|
||||||
return s.interfaceUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) HasIPAssigned() bool {
|
|
||||||
return s.ipv4Addr != nil || s.ipv6Addr != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IsOnline() bool {
|
|
||||||
return s.IsUp() && s.HasIPAssigned()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv4() *net.IP {
|
|
||||||
return s.ipv4Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv4String() string {
|
|
||||||
if s.ipv4Addr == nil {
|
|
||||||
return "..."
|
|
||||||
}
|
|
||||||
return s.ipv4Addr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv6() *net.IP {
|
|
||||||
return s.ipv6Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv6String() string {
|
|
||||||
if s.ipv6Addr == nil {
|
|
||||||
return "..."
|
|
||||||
}
|
|
||||||
return s.ipv6Addr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
|
|
||||||
return s.macAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) MACString() string {
|
|
||||||
if s.macAddr == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return s.macAddr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
|
||||||
s.stateLock.Lock()
|
|
||||||
defer s.stateLock.Unlock()
|
|
||||||
|
|
||||||
dhcpTargetState := DhcpTargetStateDoNothing
|
|
||||||
|
|
||||||
iface, err := netlink.LinkByName(s.interfaceName)
|
|
||||||
if err != nil {
|
|
||||||
s.l.Error().Err(err).Msg("failed to get interface")
|
|
||||||
return dhcpTargetState, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// detect if the interface status changed
|
|
||||||
var changed bool
|
|
||||||
attrs := iface.Attrs()
|
|
||||||
state := attrs.OperState
|
|
||||||
newInterfaceUp := state == netlink.OperUp
|
|
||||||
|
|
||||||
// check if the interface is coming up
|
|
||||||
interfaceGoingUp := !s.interfaceUp && newInterfaceUp
|
|
||||||
interfaceGoingDown := s.interfaceUp && !newInterfaceUp
|
|
||||||
|
|
||||||
if s.interfaceUp != newInterfaceUp {
|
|
||||||
s.interfaceUp = newInterfaceUp
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if changed {
|
|
||||||
if interfaceGoingUp {
|
|
||||||
s.l.Info().Msg("interface state transitioned to up")
|
|
||||||
dhcpTargetState = DhcpTargetStateRenew
|
|
||||||
} else if interfaceGoingDown {
|
|
||||||
s.l.Info().Msg("interface state transitioned to down")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the mac address
|
|
||||||
s.macAddr = &attrs.HardwareAddr
|
|
||||||
|
|
||||||
// get the ip addresses
|
|
||||||
addrs, err := netlinkAddrs(iface)
|
|
||||||
if err != nil {
|
|
||||||
return dhcpTargetState, logging.ErrorfL(s.l, "failed to get ip addresses", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ipv4Addresses = make([]net.IP, 0)
|
|
||||||
ipv4AddressesString = make([]string, 0)
|
|
||||||
ipv6Addresses = make([]IPv6Address, 0)
|
|
||||||
// ipv6AddressesString = make([]string, 0)
|
|
||||||
ipv6LinkLocal *net.IP
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if addr.IP.To4() != nil {
|
|
||||||
scopedLogger := s.l.With().Str("ipv4", addr.IP.String()).Logger()
|
|
||||||
if interfaceGoingDown {
|
|
||||||
// remove all IPv4 addresses from the interface.
|
|
||||||
scopedLogger.Info().Msg("state transitioned to down, removing IPv4 address")
|
|
||||||
err := netlink.AddrDel(iface, &addr)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to delete address")
|
|
||||||
}
|
|
||||||
// notify the DHCP client to release the lease
|
|
||||||
dhcpTargetState = DhcpTargetStateRelease
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ipv4Addresses = append(ipv4Addresses, addr.IP)
|
|
||||||
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
|
|
||||||
} else if addr.IP.To16() != nil {
|
|
||||||
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
|
|
||||||
// check if it's a link local address
|
|
||||||
if addr.IP.IsLinkLocalUnicast() {
|
|
||||||
ipv6LinkLocal = &addr.IP
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !addr.IP.IsGlobalUnicast() {
|
|
||||||
scopedLogger.Trace().Msg("not a global unicast address, skipping")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if interfaceGoingDown {
|
|
||||||
scopedLogger.Info().Msg("state transitioned to down, removing IPv6 address")
|
|
||||||
err := netlink.AddrDel(iface, &addr)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to delete address")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ipv6Addresses = append(ipv6Addresses, IPv6Address{
|
|
||||||
Address: addr.IP,
|
|
||||||
Prefix: *addr.IPNet,
|
|
||||||
ValidLifetime: lifetimeToTime(addr.ValidLft),
|
|
||||||
PreferredLifetime: lifetimeToTime(addr.PreferedLft),
|
|
||||||
Scope: addr.Scope,
|
|
||||||
})
|
|
||||||
// ipv6AddressesString = append(ipv6AddressesString, addr.IPNet.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ipv4Addresses) > 0 {
|
|
||||||
// compare the addresses to see if there's a change
|
|
||||||
if s.ipv4Addr == nil || s.ipv4Addr.String() != ipv4Addresses[0].String() {
|
|
||||||
scopedLogger := s.l.With().Str("ipv4", ipv4Addresses[0].String()).Logger()
|
|
||||||
if s.ipv4Addr != nil {
|
|
||||||
scopedLogger.Info().
|
|
||||||
Str("old_ipv4", s.ipv4Addr.String()).
|
|
||||||
Msg("IPv4 address changed")
|
|
||||||
} else {
|
|
||||||
scopedLogger.Info().Msg("IPv4 address found")
|
|
||||||
}
|
|
||||||
s.ipv4Addr = &ipv4Addresses[0]
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.ipv4Addresses = ipv4AddressesString
|
|
||||||
|
|
||||||
if ipv6LinkLocal != nil {
|
|
||||||
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
|
|
||||||
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
|
|
||||||
if s.ipv6LinkLocal != nil {
|
|
||||||
scopedLogger.Info().
|
|
||||||
Str("old_ipv6", s.ipv6LinkLocal.String()).
|
|
||||||
Msg("IPv6 link local address changed")
|
|
||||||
} else {
|
|
||||||
scopedLogger.Info().Msg("IPv6 link local address found")
|
|
||||||
}
|
|
||||||
s.ipv6LinkLocal = ipv6LinkLocal
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.ipv6Addresses = ipv6Addresses
|
|
||||||
|
|
||||||
if len(ipv6Addresses) > 0 {
|
|
||||||
// compare the addresses to see if there's a change
|
|
||||||
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() {
|
|
||||||
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger()
|
|
||||||
if s.ipv6Addr != nil {
|
|
||||||
scopedLogger.Info().
|
|
||||||
Str("old_ipv6", s.ipv6Addr.String()).
|
|
||||||
Msg("IPv6 address changed")
|
|
||||||
} else {
|
|
||||||
scopedLogger.Info().Msg("IPv6 address found")
|
|
||||||
}
|
|
||||||
s.ipv6Addr = &ipv6Addresses[0].Address
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it's the initial check, we'll set changed to false
|
|
||||||
initialCheck := !s.checked
|
|
||||||
if initialCheck {
|
|
||||||
s.checked = true
|
|
||||||
changed = false
|
|
||||||
if dhcpTargetState == DhcpTargetStateRenew {
|
|
||||||
// it's the initial check, we'll start the DHCP client
|
|
||||||
// dhcpTargetState = DhcpTargetStateStart
|
|
||||||
// TODO: manage DHCP client start/stop
|
|
||||||
dhcpTargetState = DhcpTargetStateDoNothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if initialCheck {
|
|
||||||
s.onInitialCheck(s)
|
|
||||||
} else if changed {
|
|
||||||
s.onStateChange(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dhcpTargetState, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
|
|
||||||
dhcpTargetState, err := s.update()
|
|
||||||
if err != nil {
|
|
||||||
return logging.ErrorfL(s.l, "failed to update network state", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch dhcpTargetState {
|
|
||||||
case DhcpTargetStateRenew:
|
|
||||||
s.l.Info().Msg("renewing DHCP lease")
|
|
||||||
_ = s.dhcpClient.Renew()
|
|
||||||
case DhcpTargetStateRelease:
|
|
||||||
s.l.Info().Msg("releasing DHCP lease")
|
|
||||||
_ = s.dhcpClient.Release()
|
|
||||||
case DhcpTargetStateStart:
|
|
||||||
s.l.Warn().Msg("dhcpTargetStateStart not implemented")
|
|
||||||
case DhcpTargetStateStop:
|
|
||||||
s.l.Warn().Msg("dhcpTargetStateStop not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) onConfigChange(config *NetworkConfig) {
|
|
||||||
_ = s.setHostnameIfNotSame()
|
|
||||||
s.cbConfigChange(config)
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
|
||||||
"github.com/vishvananda/netlink/nl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) {
|
|
||||||
if update.Link.Attrs().Name == s.interfaceName {
|
|
||||||
s.l.Info().Interface("update", update).Msg("interface link update received")
|
|
||||||
_ = s.CheckAndUpdateDhcp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) Run() error {
|
|
||||||
updates := make(chan netlink.LinkUpdate)
|
|
||||||
done := make(chan struct{})
|
|
||||||
|
|
||||||
if err := netlink.LinkSubscribe(updates, done); err != nil {
|
|
||||||
s.l.Warn().Err(err).Msg("failed to subscribe to link updates")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = s.setHostnameIfNotSame()
|
|
||||||
|
|
||||||
// run the dhcp client
|
|
||||||
go s.dhcpClient.Run() // nolint:errcheck
|
|
||||||
|
|
||||||
if err := s.CheckAndUpdateDhcp(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case update := <-updates:
|
|
||||||
s.HandleLinkUpdate(update)
|
|
||||||
case <-ticker.C:
|
|
||||||
_ = s.CheckAndUpdateDhcp()
|
|
||||||
case <-done:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) {
|
|
||||||
return netlink.AddrList(iface, nl.FAMILY_ALL)
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
//go:build !linux
|
|
||||||
|
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/vishvananda/netlink"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) HandleLinkUpdate() error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) Run() error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) {
|
|
||||||
return nil, fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/confparser"
|
|
||||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RpcIPv6Address struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
ValidLifetime *time.Time `json:"valid_lifetime,omitempty"`
|
|
||||||
PreferredLifetime *time.Time `json:"preferred_lifetime,omitempty"`
|
|
||||||
Scope int `json:"scope"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RpcNetworkState struct {
|
|
||||||
InterfaceName string `json:"interface_name"`
|
|
||||||
MacAddress string `json:"mac_address"`
|
|
||||||
IPv4 string `json:"ipv4,omitempty"`
|
|
||||||
IPv6 string `json:"ipv6,omitempty"`
|
|
||||||
IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
|
|
||||||
IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
|
|
||||||
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"`
|
|
||||||
DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RpcNetworkSettings struct {
|
|
||||||
NetworkConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) MacAddress() string {
|
|
||||||
if s.macAddr == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.macAddr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv4Address() string {
|
|
||||||
if s.ipv4Addr == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.ipv4Addr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv6Address() string {
|
|
||||||
if s.ipv6Addr == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.ipv6Addr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
|
|
||||||
if s.ipv6LinkLocal == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.ipv6LinkLocal.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
|
|
||||||
ipv6Addresses := make([]RpcIPv6Address, 0)
|
|
||||||
|
|
||||||
if s.ipv6Addresses != nil {
|
|
||||||
for _, addr := range s.ipv6Addresses {
|
|
||||||
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
|
|
||||||
Address: addr.Prefix.String(),
|
|
||||||
ValidLifetime: addr.ValidLifetime,
|
|
||||||
PreferredLifetime: addr.PreferredLifetime,
|
|
||||||
Scope: addr.Scope,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return RpcNetworkState{
|
|
||||||
InterfaceName: s.interfaceName,
|
|
||||||
MacAddress: s.MacAddress(),
|
|
||||||
IPv4: s.IPv4Address(),
|
|
||||||
IPv6: s.IPv6Address(),
|
|
||||||
IPv6LinkLocal: s.IPv6LinkLocalAddress(),
|
|
||||||
IPv4Addresses: s.ipv4Addresses,
|
|
||||||
IPv6Addresses: ipv6Addresses,
|
|
||||||
DHCPLease: s.dhcpClient.GetLease(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings {
|
|
||||||
if s.config == nil {
|
|
||||||
return RpcNetworkSettings{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return RpcNetworkSettings{
|
|
||||||
NetworkConfig: *s.config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error {
|
|
||||||
currentSettings := s.config
|
|
||||||
|
|
||||||
err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if IsSame(currentSettings, settings.NetworkConfig) {
|
|
||||||
// no changes, do nothing
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s.config = &settings.NetworkConfig
|
|
||||||
s.onConfigChange(s.config)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) RpcRenewDHCPLease() error {
|
|
||||||
if s.dhcpClient == nil {
|
|
||||||
return fmt.Errorf("dhcp client not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.dhcpClient.Renew()
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package network
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func lifetimeToTime(lifetime int) *time.Time {
|
|
||||||
if lifetime == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
t := time.Now().Add(time.Duration(lifetime) * time.Second)
|
|
||||||
return &t
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsSame(a, b interface{}) bool {
|
|
||||||
aJSON, err := json.Marshal(a)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
bJSON, err := json.Marshal(b)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return string(aJSON) == string(bJSON)
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
package timesync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultHTTPUrls = []string{
|
|
||||||
"http://www.gstatic.com/generate_204",
|
|
||||||
"http://cp.cloudflare.com/",
|
|
||||||
"http://edge-http.microsoft.com/captiveportal/generate_204",
|
|
||||||
// Firefox, Apple, and Microsoft have inconsistent results, so we don't use it
|
|
||||||
// "http://detectportal.firefox.com/",
|
|
||||||
// "http://www.apple.com/library/test/success.html",
|
|
||||||
// "http://www.msftconnecttest.com/connecttest.txt",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) queryAllHttpTime() (now *time.Time) {
|
|
||||||
chunkSize := 4
|
|
||||||
httpUrls := t.httpUrls
|
|
||||||
|
|
||||||
// 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] })
|
|
||||||
|
|
||||||
for i := 0; i < len(httpUrls); i += chunkSize {
|
|
||||||
chunk := httpUrls[i:min(i+chunkSize, len(httpUrls))]
|
|
||||||
results := t.queryMultipleHttp(chunk, timeSyncTimeout)
|
|
||||||
if results != nil {
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) queryMultipleHttp(urls []string, timeout time.Duration) (now *time.Time) {
|
|
||||||
results := make(chan *time.Time, len(urls))
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
for _, url := range urls {
|
|
||||||
go func(url string) {
|
|
||||||
scopedLogger := t.l.With().
|
|
||||||
Str("http_url", url).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
metricHttpRequestCount.WithLabelValues(url).Inc()
|
|
||||||
metricHttpTotalRequestCount.Inc()
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
now, response, err := queryHttpTime(
|
|
||||||
ctx,
|
|
||||||
url,
|
|
||||||
timeout,
|
|
||||||
)
|
|
||||||
duration := time.Since(startTime)
|
|
||||||
|
|
||||||
metricHttpServerLastRTT.WithLabelValues(url).Set(float64(duration.Milliseconds()))
|
|
||||||
metricHttpServerRttHistogram.WithLabelValues(url).Observe(float64(duration.Milliseconds()))
|
|
||||||
|
|
||||||
status := 0
|
|
||||||
if response != nil {
|
|
||||||
status = response.StatusCode
|
|
||||||
}
|
|
||||||
metricHttpServerInfo.WithLabelValues(
|
|
||||||
url,
|
|
||||||
strconv.Itoa(status),
|
|
||||||
).Set(1)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
metricHttpTotalSuccessCount.Inc()
|
|
||||||
metricHttpSuccessCount.WithLabelValues(url).Inc()
|
|
||||||
|
|
||||||
requestId := response.Header.Get("X-Request-Id")
|
|
||||||
if requestId != "" {
|
|
||||||
requestId = response.Header.Get("X-Msedge-Ref")
|
|
||||||
}
|
|
||||||
if requestId == "" {
|
|
||||||
requestId = response.Header.Get("Cf-Ray")
|
|
||||||
}
|
|
||||||
scopedLogger.Info().
|
|
||||||
Str("time", now.Format(time.RFC3339)).
|
|
||||||
Int("status", status).
|
|
||||||
Str("request_id", requestId).
|
|
||||||
Str("time_taken", duration.String()).
|
|
||||||
Msg("HTTP server returned time")
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
results <- now
|
|
||||||
} else if errors.Is(err, context.Canceled) {
|
|
||||||
metricHttpCancelCount.WithLabelValues(url).Inc()
|
|
||||||
metricHttpTotalCancelCount.Inc()
|
|
||||||
} else {
|
|
||||||
scopedLogger.Warn().
|
|
||||||
Str("error", err.Error()).
|
|
||||||
Int("status", status).
|
|
||||||
Msg("failed to query HTTP server")
|
|
||||||
}
|
|
||||||
}(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <-results
|
|
||||||
}
|
|
||||||
|
|
||||||
func queryHttpTime(
|
|
||||||
ctx context.Context,
|
|
||||||
url string,
|
|
||||||
timeout time.Duration,
|
|
||||||
) (now *time.Time, response *http.Response, err error) {
|
|
||||||
client := http.Client{
|
|
||||||
Timeout: timeout,
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
dateStr := resp.Header.Get("Date")
|
|
||||||
parsedTime, err := time.Parse(time.RFC1123, dateStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return &parsedTime, resp, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
package timesync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
metricTimeSyncStatus = promauto.NewGauge(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "jetkvm_timesync_status",
|
|
||||||
Help: "The status of the timesync, 1 if successful, 0 if not",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricTimeSyncCount = promauto.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_count",
|
|
||||||
Help: "The number of times the timesync has been run",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricTimeSyncSuccessCount = promauto.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_success_count",
|
|
||||||
Help: "The number of times the timesync has been successful",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricRTCUpdateCount = promauto.NewCounter( //nolint:unused
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_rtc_update_count",
|
|
||||||
Help: "The number of times the RTC has been updated",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricNtpTotalSuccessCount = promauto.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_ntp_total_success_count",
|
|
||||||
Help: "The total number of successful NTP requests",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricNtpTotalRequestCount = promauto.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_ntp_total_request_count",
|
|
||||||
Help: "The total number of NTP requests sent",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricNtpSuccessCount = promauto.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_ntp_success_count",
|
|
||||||
Help: "The number of successful NTP requests",
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricNtpRequestCount = promauto.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_ntp_request_count",
|
|
||||||
Help: "The number of NTP requests sent to the server",
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricNtpServerLastRTT = promauto.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "jetkvm_timesync_ntp_server_last_rtt",
|
|
||||||
Help: "The last RTT of the NTP server in milliseconds",
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricNtpServerRttHistogram = promauto.NewHistogramVec(
|
|
||||||
prometheus.HistogramOpts{
|
|
||||||
Name: "jetkvm_timesync_ntp_server_rtt",
|
|
||||||
Help: "The histogram of the RTT of the NTP server in milliseconds",
|
|
||||||
Buckets: []float64{
|
|
||||||
10, 25, 50, 100, 200, 300, 500, 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricNtpServerInfo = promauto.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "jetkvm_timesync_ntp_server_info",
|
|
||||||
Help: "The info of the NTP server",
|
|
||||||
},
|
|
||||||
[]string{"url", "reference", "stratum", "precision"},
|
|
||||||
)
|
|
||||||
|
|
||||||
metricHttpTotalSuccessCount = promauto.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_http_total_success_count",
|
|
||||||
Help: "The total number of successful HTTP requests",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricHttpTotalRequestCount = promauto.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_http_total_request_count",
|
|
||||||
Help: "The total number of HTTP requests sent",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricHttpTotalCancelCount = promauto.NewCounter(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_http_total_cancel_count",
|
|
||||||
Help: "The total number of HTTP requests cancelled",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
metricHttpSuccessCount = promauto.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_http_success_count",
|
|
||||||
Help: "The number of successful HTTP requests",
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricHttpRequestCount = promauto.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_http_request_count",
|
|
||||||
Help: "The number of HTTP requests sent to the server",
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricHttpCancelCount = promauto.NewCounterVec(
|
|
||||||
prometheus.CounterOpts{
|
|
||||||
Name: "jetkvm_timesync_http_cancel_count",
|
|
||||||
Help: "The number of HTTP requests cancelled",
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricHttpServerLastRTT = promauto.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "jetkvm_timesync_http_server_last_rtt",
|
|
||||||
Help: "The last RTT of the HTTP server in milliseconds",
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricHttpServerRttHistogram = promauto.NewHistogramVec(
|
|
||||||
prometheus.HistogramOpts{
|
|
||||||
Name: "jetkvm_timesync_http_server_rtt",
|
|
||||||
Help: "The histogram of the RTT of the HTTP server in milliseconds",
|
|
||||||
Buckets: []float64{
|
|
||||||
10, 25, 50, 100, 200, 300, 500, 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[]string{"url"},
|
|
||||||
)
|
|
||||||
metricHttpServerInfo = promauto.NewGaugeVec(
|
|
||||||
prometheus.GaugeOpts{
|
|
||||||
Name: "jetkvm_timesync_http_server_info",
|
|
||||||
Help: "The info of the HTTP server",
|
|
||||||
},
|
|
||||||
[]string{"url", "http_code"},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
package timesync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand/v2"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/beevik/ntp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultNTPServers = []string{
|
|
||||||
"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",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) {
|
|
||||||
chunkSize := 4
|
|
||||||
ntpServers := t.ntpServers
|
|
||||||
|
|
||||||
// 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] })
|
|
||||||
|
|
||||||
for i := 0; i < len(ntpServers); i += chunkSize {
|
|
||||||
chunk := ntpServers[i:min(i+chunkSize, len(ntpServers))]
|
|
||||||
now, offset := t.queryMultipleNTP(chunk, timeSyncTimeout)
|
|
||||||
if now != nil {
|
|
||||||
return now, offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type ntpResult struct {
|
|
||||||
now *time.Time
|
|
||||||
offset *time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
|
|
||||||
results := make(chan *ntpResult, len(servers))
|
|
||||||
for _, server := range servers {
|
|
||||||
go func(server string) {
|
|
||||||
scopedLogger := t.l.With().
|
|
||||||
Str("server", server).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
// increase request count
|
|
||||||
metricNtpTotalRequestCount.Inc()
|
|
||||||
metricNtpRequestCount.WithLabelValues(server).Inc()
|
|
||||||
|
|
||||||
// query the server
|
|
||||||
now, response, err := queryNtpServer(server, timeout)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().
|
|
||||||
Str("error", err.Error()).
|
|
||||||
Msg("failed to query NTP server")
|
|
||||||
results <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the last RTT
|
|
||||||
metricNtpServerLastRTT.WithLabelValues(
|
|
||||||
server,
|
|
||||||
).Set(float64(response.RTT.Milliseconds()))
|
|
||||||
|
|
||||||
// set the RTT histogram
|
|
||||||
metricNtpServerRttHistogram.WithLabelValues(
|
|
||||||
server,
|
|
||||||
).Observe(float64(response.RTT.Milliseconds()))
|
|
||||||
|
|
||||||
// set the server info
|
|
||||||
metricNtpServerInfo.WithLabelValues(
|
|
||||||
server,
|
|
||||||
response.ReferenceString(),
|
|
||||||
strconv.Itoa(int(response.Stratum)),
|
|
||||||
strconv.Itoa(int(response.Precision)),
|
|
||||||
).Set(1)
|
|
||||||
|
|
||||||
// increase success count
|
|
||||||
metricNtpTotalSuccessCount.Inc()
|
|
||||||
metricNtpSuccessCount.WithLabelValues(server).Inc()
|
|
||||||
|
|
||||||
scopedLogger.Info().
|
|
||||||
Str("time", now.Format(time.RFC3339)).
|
|
||||||
Str("reference", response.ReferenceString()).
|
|
||||||
Str("rtt", response.RTT.String()).
|
|
||||||
Str("clockOffset", response.ClockOffset.String()).
|
|
||||||
Uint8("stratum", response.Stratum).
|
|
||||||
Msg("NTP server returned time")
|
|
||||||
results <- &ntpResult{
|
|
||||||
now: now,
|
|
||||||
offset: &response.ClockOffset,
|
|
||||||
}
|
|
||||||
}(server)
|
|
||||||
}
|
|
||||||
|
|
||||||
for range servers {
|
|
||||||
result := <-results
|
|
||||||
if result == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
now, offset = result.now, result.offset
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func queryNtpServer(server string, timeout time.Duration) (now *time.Time, response *ntp.Response, err error) {
|
|
||||||
resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return &resp.Time, resp, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package timesync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
rtcDeviceSearchPaths = []string{
|
|
||||||
"/dev/rtc",
|
|
||||||
"/dev/rtc0",
|
|
||||||
"/dev/rtc1",
|
|
||||||
"/dev/misc/rtc",
|
|
||||||
"/dev/misc/rtc0",
|
|
||||||
"/dev/misc/rtc1",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func getRtcDevicePath() (string, error) {
|
|
||||||
for _, path := range rtcDeviceSearchPaths {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("rtc device not found")
|
|
||||||
}
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
package timesync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TimetoRtcTime(t time.Time) unix.RTCTime {
|
|
||||||
return unix.RTCTime{
|
|
||||||
Sec: int32(t.Second()),
|
|
||||||
Min: int32(t.Minute()),
|
|
||||||
Hour: int32(t.Hour()),
|
|
||||||
Mday: int32(t.Day()),
|
|
||||||
Mon: int32(t.Month() - 1),
|
|
||||||
Year: int32(t.Year() - 1900),
|
|
||||||
Wday: int32(0),
|
|
||||||
Yday: int32(0),
|
|
||||||
Isdst: int32(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func RtcTimetoTime(t unix.RTCTime) time.Time {
|
|
||||||
return time.Date(
|
|
||||||
int(t.Year)+1900,
|
|
||||||
time.Month(t.Mon+1),
|
|
||||||
int(t.Mday),
|
|
||||||
int(t.Hour),
|
|
||||||
int(t.Min),
|
|
||||||
int(t.Sec),
|
|
||||||
0,
|
|
||||||
time.UTC,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) getRtcDevice() (*os.File, error) {
|
|
||||||
if t.rtcDevice == nil {
|
|
||||||
file, err := os.OpenFile(t.rtcDevicePath, os.O_RDWR, 0666)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t.rtcDevice = file
|
|
||||||
}
|
|
||||||
return t.rtcDevice, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) getRtcDeviceFd() (int, error) {
|
|
||||||
device, err := t.getRtcDevice()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int(device.Fd()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read implements Read for the Linux RTC
|
|
||||||
func (t *TimeSync) readRtcTime() (time.Time, error) {
|
|
||||||
fd, err := t.getRtcDeviceFd()
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, fmt.Errorf("failed to get RTC device fd: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rtcTime, err := unix.IoctlGetRTCTime(fd)
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, fmt.Errorf("failed to get RTC time: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
date := RtcTimetoTime(*rtcTime)
|
|
||||||
|
|
||||||
return date, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set implements Set for the Linux RTC
|
|
||||||
// ...
|
|
||||||
// It might be not accurate as the time consumed by the system call is not taken into account
|
|
||||||
// but it's good enough for our purposes
|
|
||||||
func (t *TimeSync) setRtcTime(tu time.Time) error {
|
|
||||||
rt := TimetoRtcTime(tu)
|
|
||||||
|
|
||||||
fd, err := t.getRtcDeviceFd()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get RTC device fd: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentRtcTime, err := t.readRtcTime()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read RTC time: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.l.Info().
|
|
||||||
Interface("rtc_time", tu).
|
|
||||||
Str("offset", tu.Sub(currentRtcTime).String()).
|
|
||||||
Msg("set rtc time")
|
|
||||||
|
|
||||||
if err := unix.IoctlSetRTCTime(fd, &rt); err != nil {
|
|
||||||
return fmt.Errorf("failed to set RTC time: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
metricRTCUpdateCount.Inc()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
//go:build !linux
|
|
||||||
|
|
||||||
package timesync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t *TimeSync) readRtcTime() (time.Time, error) {
|
|
||||||
return time.Now(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) setRtcTime(tu time.Time) error {
|
|
||||||
return errors.New("not supported")
|
|
||||||
}
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
package timesync
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/network"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
timeSyncRetryStep = 5 * time.Second
|
|
||||||
timeSyncRetryMaxInt = 1 * time.Minute
|
|
||||||
timeSyncWaitNetChkInt = 100 * time.Millisecond
|
|
||||||
timeSyncWaitNetUpInt = 3 * time.Second
|
|
||||||
timeSyncInterval = 1 * time.Hour
|
|
||||||
timeSyncTimeout = 2 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
timeSyncRetryInterval = 0 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimeSync struct {
|
|
||||||
syncLock *sync.Mutex
|
|
||||||
l *zerolog.Logger
|
|
||||||
|
|
||||||
ntpServers []string
|
|
||||||
httpUrls []string
|
|
||||||
networkConfig *network.NetworkConfig
|
|
||||||
|
|
||||||
rtcDevicePath string
|
|
||||||
rtcDevice *os.File //nolint:unused
|
|
||||||
rtcLock *sync.Mutex
|
|
||||||
|
|
||||||
syncSuccess bool
|
|
||||||
|
|
||||||
preCheckFunc func() (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimeSyncOptions struct {
|
|
||||||
PreCheckFunc func() (bool, error)
|
|
||||||
Logger *zerolog.Logger
|
|
||||||
NetworkConfig *network.NetworkConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type SyncMode struct {
|
|
||||||
Ntp bool
|
|
||||||
Http bool
|
|
||||||
Ordering []string
|
|
||||||
NtpUseFallback bool
|
|
||||||
HttpUseFallback bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
|
|
||||||
rtcDevice, err := getRtcDevicePath()
|
|
||||||
if err != nil {
|
|
||||||
opts.Logger.Error().Err(err).Msg("failed to get RTC device path")
|
|
||||||
} else {
|
|
||||||
opts.Logger.Info().Str("path", rtcDevice).Msg("RTC device found")
|
|
||||||
}
|
|
||||||
|
|
||||||
t := &TimeSync{
|
|
||||||
syncLock: &sync.Mutex{},
|
|
||||||
l: opts.Logger,
|
|
||||||
rtcDevicePath: rtcDevice,
|
|
||||||
rtcLock: &sync.Mutex{},
|
|
||||||
preCheckFunc: opts.PreCheckFunc,
|
|
||||||
ntpServers: defaultNTPServers,
|
|
||||||
httpUrls: defaultHTTPUrls,
|
|
||||||
networkConfig: opts.NetworkConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.rtcDevicePath != "" {
|
|
||||||
rtcTime, _ := t.readRtcTime()
|
|
||||||
t.l.Info().Interface("rtc_time", rtcTime).Msg("read RTC time")
|
|
||||||
}
|
|
||||||
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) getSyncMode() SyncMode {
|
|
||||||
syncMode := SyncMode{
|
|
||||||
NtpUseFallback: true,
|
|
||||||
HttpUseFallback: true,
|
|
||||||
}
|
|
||||||
var syncModeString string
|
|
||||||
|
|
||||||
if t.networkConfig != nil {
|
|
||||||
syncModeString = t.networkConfig.TimeSyncMode.String
|
|
||||||
if t.networkConfig.TimeSyncDisableFallback.Bool {
|
|
||||||
syncMode.NtpUseFallback = false
|
|
||||||
syncMode.HttpUseFallback = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch syncModeString {
|
|
||||||
case "ntp_only":
|
|
||||||
syncMode.Ntp = true
|
|
||||||
case "http_only":
|
|
||||||
syncMode.Http = true
|
|
||||||
default:
|
|
||||||
syncMode.Ntp = true
|
|
||||||
syncMode.Http = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return syncMode
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) doTimeSync() {
|
|
||||||
metricTimeSyncStatus.Set(0)
|
|
||||||
for {
|
|
||||||
if ok, err := t.preCheckFunc(); !ok {
|
|
||||||
if err != nil {
|
|
||||||
t.l.Error().Err(err).Msg("pre-check failed")
|
|
||||||
}
|
|
||||||
time.Sleep(timeSyncWaitNetChkInt)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
t.l.Info().Msg("syncing system time")
|
|
||||||
start := time.Now()
|
|
||||||
err := t.Sync()
|
|
||||||
if err != nil {
|
|
||||||
t.l.Error().Str("error", err.Error()).Msg("failed to sync system time")
|
|
||||||
|
|
||||||
// retry after a delay
|
|
||||||
timeSyncRetryInterval += timeSyncRetryStep
|
|
||||||
time.Sleep(timeSyncRetryInterval)
|
|
||||||
// reset the retry interval if it exceeds the max interval
|
|
||||||
if timeSyncRetryInterval > timeSyncRetryMaxInt {
|
|
||||||
timeSyncRetryInterval = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.syncSuccess = true
|
|
||||||
t.l.Info().Str("now", time.Now().Format(time.RFC3339)).
|
|
||||||
Str("time_taken", time.Since(start).String()).
|
|
||||||
Msg("time sync successful")
|
|
||||||
|
|
||||||
metricTimeSyncStatus.Set(1)
|
|
||||||
|
|
||||||
time.Sleep(timeSyncInterval) // after the first sync is done
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) Sync() error {
|
|
||||||
var (
|
|
||||||
now *time.Time
|
|
||||||
offset *time.Duration
|
|
||||||
)
|
|
||||||
|
|
||||||
syncMode := t.getSyncMode()
|
|
||||||
|
|
||||||
metricTimeSyncCount.Inc()
|
|
||||||
|
|
||||||
if syncMode.Ntp {
|
|
||||||
now, offset = t.queryNetworkTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
if syncMode.Http && now == nil {
|
|
||||||
now = t.queryAllHttpTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
if now == nil {
|
|
||||||
return fmt.Errorf("failed to get time from any source")
|
|
||||||
}
|
|
||||||
|
|
||||||
if offset != nil {
|
|
||||||
newNow := time.Now().Add(*offset)
|
|
||||||
now = &newNow
|
|
||||||
}
|
|
||||||
|
|
||||||
err := t.setSystemTime(*now)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set system time: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
metricTimeSyncSuccessCount.Inc()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) IsSyncSuccess() bool {
|
|
||||||
return t.syncSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) Start() {
|
|
||||||
go t.doTimeSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeSync) setSystemTime(now time.Time) error {
|
|
||||||
nowStr := now.Format("2006-01-02 15:04:05")
|
|
||||||
output, err := exec.Command("date", "-s", nowStr).CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to run date -s: %w, %s", err, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.rtcDevicePath != "" {
|
|
||||||
return t.setRtcTime(now)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package udhcpc
|
|
||||||
|
|
||||||
func (u *DHCPClient) GetNtpServers() []string {
|
|
||||||
if u.lease == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
servers := make([]string, len(u.lease.NTPServers))
|
|
||||||
for i, server := range u.lease.NTPServers {
|
|
||||||
servers[i] = server.String()
|
|
||||||
}
|
|
||||||
return servers
|
|
||||||
}
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
package udhcpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Lease struct {
|
|
||||||
// from https://udhcp.busybox.net/README.udhcpc
|
|
||||||
IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP
|
|
||||||
Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask
|
|
||||||
Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network
|
|
||||||
TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network
|
|
||||||
MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network
|
|
||||||
HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname
|
|
||||||
Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network
|
|
||||||
BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option
|
|
||||||
BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option
|
|
||||||
BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option
|
|
||||||
Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC
|
|
||||||
Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers
|
|
||||||
DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers
|
|
||||||
NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers
|
|
||||||
LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers
|
|
||||||
TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete)
|
|
||||||
IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete)
|
|
||||||
LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete)
|
|
||||||
CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete)
|
|
||||||
WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers
|
|
||||||
SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server
|
|
||||||
BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile
|
|
||||||
RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk
|
|
||||||
LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds
|
|
||||||
DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored)
|
|
||||||
ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server
|
|
||||||
Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK
|
|
||||||
TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name
|
|
||||||
BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name
|
|
||||||
Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds
|
|
||||||
LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
|
|
||||||
isEmpty map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Lease) setIsEmpty(m map[string]bool) {
|
|
||||||
l.isEmpty = m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Lease) IsEmpty(key string) bool {
|
|
||||||
return l.isEmpty[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Lease) ToJSON() string {
|
|
||||||
json, err := json.Marshal(l)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Lease) SetLeaseExpiry() (time.Time, error) {
|
|
||||||
if l.Uptime == 0 || l.LeaseTime == 0 {
|
|
||||||
return time.Time{}, fmt.Errorf("uptime or lease time isn't set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the uptime of the device
|
|
||||||
|
|
||||||
file, err := os.Open("/proc/uptime")
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var uptime time.Duration
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
text := scanner.Text()
|
|
||||||
parts := strings.Split(text, " ")
|
|
||||||
uptime, err = time.ParseDuration(parts[0] + "s")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime
|
|
||||||
leaseExpiry := time.Now().Add(relativeLeaseRemaining)
|
|
||||||
|
|
||||||
l.LeaseExpiry = &leaseExpiry
|
|
||||||
|
|
||||||
return leaseExpiry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
|
||||||
// parse the lease file as a map
|
|
||||||
data := make(map[string]string)
|
|
||||||
for _, line := range strings.Split(str, "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
// skip empty lines and comments
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(line, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
key := strings.TrimSpace(parts[0])
|
|
||||||
value := strings.TrimSpace(parts[1])
|
|
||||||
|
|
||||||
data[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// now iterate over the lease struct and set the values
|
|
||||||
leaseType := reflect.TypeOf(lease).Elem()
|
|
||||||
leaseValue := reflect.ValueOf(lease).Elem()
|
|
||||||
|
|
||||||
valuesParsed := make(map[string]bool)
|
|
||||||
|
|
||||||
for i := 0; i < leaseType.NumField(); i++ {
|
|
||||||
field := leaseValue.Field(i)
|
|
||||||
|
|
||||||
// get the env tag
|
|
||||||
key := leaseType.Field(i).Tag.Get("env")
|
|
||||||
if key == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
valuesParsed[key] = false
|
|
||||||
|
|
||||||
// get the value from the data map
|
|
||||||
value, ok := data[key]
|
|
||||||
if !ok || value == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch field.Interface().(type) {
|
|
||||||
case string:
|
|
||||||
field.SetString(value)
|
|
||||||
case int:
|
|
||||||
val, err := strconv.Atoi(value)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
field.SetInt(int64(val))
|
|
||||||
case time.Duration:
|
|
||||||
val, err := time.ParseDuration(value + "s")
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(val))
|
|
||||||
case net.IP:
|
|
||||||
ip := net.ParseIP(value)
|
|
||||||
if ip == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(ip))
|
|
||||||
case []net.IP:
|
|
||||||
val := make([]net.IP, 0)
|
|
||||||
for _, ipStr := range strings.Fields(value) {
|
|
||||||
ip := net.ParseIP(ipStr)
|
|
||||||
if ip == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val = append(val, ip)
|
|
||||||
}
|
|
||||||
field.Set(reflect.ValueOf(val))
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
|
|
||||||
}
|
|
||||||
|
|
||||||
valuesParsed[key] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
lease.setIsEmpty(valuesParsed)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
package udhcpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUnmarshalDHCPCLease(t *testing.T) {
|
|
||||||
lease := &Lease{}
|
|
||||||
err := UnmarshalDHCPCLease(lease, `
|
|
||||||
# generated @ Mon Jan 4 19:31:53 UTC 2021
|
|
||||||
# 19:31:53 up 0 min, 0 users, load average: 0.72, 0.14, 0.04
|
|
||||||
# the date might be inaccurate if the clock is not set
|
|
||||||
ip=192.168.0.240
|
|
||||||
siaddr=192.168.0.1
|
|
||||||
sname=
|
|
||||||
boot_file=
|
|
||||||
subnet=255.255.255.0
|
|
||||||
timezone=
|
|
||||||
router=192.168.0.1
|
|
||||||
timesvr=
|
|
||||||
namesvr=
|
|
||||||
dns=172.19.53.2
|
|
||||||
logsvr=
|
|
||||||
cookiesvr=
|
|
||||||
lprsvr=
|
|
||||||
hostname=
|
|
||||||
bootsize=
|
|
||||||
domain=
|
|
||||||
swapsvr=
|
|
||||||
rootpath=
|
|
||||||
ipttl=
|
|
||||||
mtu=
|
|
||||||
broadcast=
|
|
||||||
ntpsrv=162.159.200.123
|
|
||||||
wins=
|
|
||||||
lease=172800
|
|
||||||
dhcptype=
|
|
||||||
serverid=192.168.0.1
|
|
||||||
message=
|
|
||||||
tftp=
|
|
||||||
bootfile=
|
|
||||||
`)
|
|
||||||
if lease.IPAddress.String() != "192.168.0.240" {
|
|
||||||
t.Fatalf("expected ip to be 192.168.0.240, got %s", lease.IPAddress.String())
|
|
||||||
}
|
|
||||||
if lease.Netmask.String() != "255.255.255.0" {
|
|
||||||
t.Fatalf("expected netmask to be 255.255.255.0, got %s", lease.Netmask.String())
|
|
||||||
}
|
|
||||||
if len(lease.Routers) != 1 {
|
|
||||||
t.Fatalf("expected 1 router, got %d", len(lease.Routers))
|
|
||||||
}
|
|
||||||
if lease.Routers[0].String() != "192.168.0.1" {
|
|
||||||
t.Fatalf("expected router to be 192.168.0.1, got %s", lease.Routers[0].String())
|
|
||||||
}
|
|
||||||
if len(lease.NTPServers) != 1 {
|
|
||||||
t.Fatalf("expected 1 timeserver, got %d", len(lease.NTPServers))
|
|
||||||
}
|
|
||||||
if lease.NTPServers[0].String() != "162.159.200.123" {
|
|
||||||
t.Fatalf("expected timeserver to be 162.159.200.123, got %s", lease.NTPServers[0].String())
|
|
||||||
}
|
|
||||||
if len(lease.DNS) != 1 {
|
|
||||||
t.Fatalf("expected 1 dns, got %d", len(lease.DNS))
|
|
||||||
}
|
|
||||||
if lease.DNS[0].String() != "172.19.53.2" {
|
|
||||||
t.Fatalf("expected dns to be 172.19.53.2, got %s", lease.DNS[0].String())
|
|
||||||
}
|
|
||||||
if lease.LeaseTime != 172800*time.Second {
|
|
||||||
t.Fatalf("expected lease time to be 172800 seconds, got %d", lease.LeaseTime)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
package udhcpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func readFileNoStat(filename string) ([]byte, error) {
|
|
||||||
const maxBufferSize = 1024 * 1024
|
|
||||||
|
|
||||||
f, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
reader := io.LimitReader(f, maxBufferSize)
|
|
||||||
return io.ReadAll(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
func toCmdline(path string) ([]string, error) {
|
|
||||||
data, err := readFileNoStat(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data) < 1 {
|
|
||||||
return []string{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *DHCPClient) findUdhcpcProcess() (int, error) {
|
|
||||||
// read procfs for udhcpc processes
|
|
||||||
// we do not use procfs.AllProcs() because we want to avoid the overhead of reading the entire procfs
|
|
||||||
processes, err := os.ReadDir("/proc")
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterate over the processes
|
|
||||||
for _, d := range processes {
|
|
||||||
// check if file is numeric
|
|
||||||
pid, err := strconv.Atoi(d.Name())
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it's a directory
|
|
||||||
if !d.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdline, err := toCmdline(filepath.Join("/proc", d.Name(), "cmdline"))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cmdline) < 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmdline[0] != "udhcpc" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdlineText := strings.Join(cmdline, " ")
|
|
||||||
|
|
||||||
// check if it's a udhcpc process
|
|
||||||
if strings.Contains(cmdlineText, fmt.Sprintf("-i %s", p.InterfaceName)) {
|
|
||||||
p.logger.Debug().
|
|
||||||
Str("pid", d.Name()).
|
|
||||||
Interface("cmdline", cmdline).
|
|
||||||
Msg("found udhcpc process")
|
|
||||||
return pid, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, errors.New("udhcpc process not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) getProcessPid() (int, error) {
|
|
||||||
var pid int
|
|
||||||
if c.pidFile != "" {
|
|
||||||
// try to read the pid file
|
|
||||||
pidHandle, err := os.ReadFile(c.pidFile)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Warn().Err(err).
|
|
||||||
Str("pidFile", c.pidFile).Msg("failed to read udhcpc pid file")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if it exists, try to read the pid
|
|
||||||
if pidHandle != nil {
|
|
||||||
pidFromFile, err := strconv.Atoi(string(pidHandle))
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Warn().Err(err).
|
|
||||||
Str("pidFile", c.pidFile).Msg("failed to convert pid file to int")
|
|
||||||
}
|
|
||||||
pid = pidFromFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the pid is 0, try to find the pid using procfs
|
|
||||||
if pid == 0 {
|
|
||||||
newPid, err := c.findUdhcpcProcess()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
pid = newPid
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) getProcess() *os.Process {
|
|
||||||
pid, err := c.getProcessPid()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
process, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Warn().Err(err).
|
|
||||||
Int("pid", pid).Msg("failed to find process")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return process
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) GetProcess() *os.Process {
|
|
||||||
if c.process == nil {
|
|
||||||
process := c.getProcess()
|
|
||||||
if process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
c.process = process
|
|
||||||
}
|
|
||||||
|
|
||||||
err := c.process.Signal(syscall.Signal(0))
|
|
||||||
if err != nil && errors.Is(err, os.ErrProcessDone) {
|
|
||||||
oldPid := c.process.Pid
|
|
||||||
|
|
||||||
c.process = nil
|
|
||||||
c.process = c.getProcess()
|
|
||||||
if c.process == nil {
|
|
||||||
c.logger.Error().Msg("failed to find new udhcpc process")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
c.logger.Warn().
|
|
||||||
Int("oldPid", oldPid).
|
|
||||||
Int("newPid", c.process.Pid).
|
|
||||||
Msg("udhcpc process pid changed")
|
|
||||||
} else if err != nil {
|
|
||||||
c.logger.Warn().Err(err).
|
|
||||||
Int("pid", c.process.Pid).Msg("udhcpc process is not running")
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.process
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) KillProcess() error {
|
|
||||||
process := c.GetProcess()
|
|
||||||
if process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return process.Kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) ReleaseProcess() error {
|
|
||||||
process := c.GetProcess()
|
|
||||||
if process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return process.Release()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) signalProcess(sig syscall.Signal) error {
|
|
||||||
process := c.GetProcess()
|
|
||||||
if process == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s := process.Signal(sig)
|
|
||||||
if s != nil {
|
|
||||||
c.logger.Warn().Err(s).
|
|
||||||
Int("pid", process.Pid).
|
|
||||||
Str("signal", sig.String()).
|
|
||||||
Msg("failed to signal udhcpc process")
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) Renew() error {
|
|
||||||
return c.signalProcess(syscall.SIGUSR1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) Release() error {
|
|
||||||
return c.signalProcess(syscall.SIGUSR2)
|
|
||||||
}
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
package udhcpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DHCPLeaseFile = "/run/udhcpc.%s.info"
|
|
||||||
DHCPPidFile = "/run/udhcpc.%s.pid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DHCPClient struct {
|
|
||||||
InterfaceName string
|
|
||||||
leaseFile string
|
|
||||||
pidFile string
|
|
||||||
lease *Lease
|
|
||||||
logger *zerolog.Logger
|
|
||||||
process *os.Process
|
|
||||||
onLeaseChange func(lease *Lease)
|
|
||||||
}
|
|
||||||
|
|
||||||
type DHCPClientOptions struct {
|
|
||||||
InterfaceName string
|
|
||||||
PidFile string
|
|
||||||
Logger *zerolog.Logger
|
|
||||||
OnLeaseChange func(lease *Lease)
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
|
|
||||||
|
|
||||||
func NewDHCPClient(options *DHCPClientOptions) *DHCPClient {
|
|
||||||
if options.Logger == nil {
|
|
||||||
options.Logger = &defaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
l := options.Logger.With().Str("interface", options.InterfaceName).Logger()
|
|
||||||
return &DHCPClient{
|
|
||||||
InterfaceName: options.InterfaceName,
|
|
||||||
logger: &l,
|
|
||||||
leaseFile: fmt.Sprintf(DHCPLeaseFile, options.InterfaceName),
|
|
||||||
pidFile: options.PidFile,
|
|
||||||
onLeaseChange: options.OnLeaseChange,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) getWatchPaths() []string {
|
|
||||||
watchPaths := make(map[string]interface{})
|
|
||||||
watchPaths[filepath.Dir(c.leaseFile)] = nil
|
|
||||||
|
|
||||||
if c.pidFile != "" {
|
|
||||||
watchPaths[filepath.Dir(c.pidFile)] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
paths := make([]string, 0)
|
|
||||||
for path := range watchPaths {
|
|
||||||
paths = append(paths, path)
|
|
||||||
}
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run starts the DHCP client and watches the lease file for changes.
|
|
||||||
// this isn't a blocking call, and the lease file is reloaded when a change is detected.
|
|
||||||
func (c *DHCPClient) Run() error {
|
|
||||||
err := c.loadLeaseFile()
|
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer watcher.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case event, ok := <-watcher.Events:
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.Name == c.leaseFile {
|
|
||||||
c.logger.Debug().
|
|
||||||
Str("event", event.Op.String()).
|
|
||||||
Str("path", event.Name).
|
|
||||||
Msg("udhcpc lease file updated, reloading lease")
|
|
||||||
_ = c.loadLeaseFile()
|
|
||||||
}
|
|
||||||
case err, ok := <-watcher.Errors:
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.logger.Error().Err(err).Msg("error watching lease file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for _, path := range c.getWatchPaths() {
|
|
||||||
err = watcher.Add(path)
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Error().
|
|
||||||
Err(err).
|
|
||||||
Str("path", path).
|
|
||||||
Msg("failed to watch directory")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: update udhcpc pid file
|
|
||||||
// we'll comment this out for now because the pid might change
|
|
||||||
// process := c.GetProcess()
|
|
||||||
// if process == nil {
|
|
||||||
// c.logger.Error().Msg("udhcpc process not found")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// block the goroutine until the lease file is updated
|
|
||||||
<-make(chan struct{})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) loadLeaseFile() error {
|
|
||||||
file, err := os.ReadFile(c.leaseFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data := string(file)
|
|
||||||
if data == "" {
|
|
||||||
c.logger.Debug().Msg("udhcpc lease file is empty")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lease := &Lease{}
|
|
||||||
err = UnmarshalDHCPCLease(lease, string(file))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
isFirstLoad := c.lease == nil
|
|
||||||
c.lease = lease
|
|
||||||
|
|
||||||
if lease.IPAddress == nil {
|
|
||||||
c.logger.Info().
|
|
||||||
Interface("lease", lease).
|
|
||||||
Str("data", string(file)).
|
|
||||||
Msg("udhcpc lease cleared")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := "udhcpc lease updated"
|
|
||||||
if isFirstLoad {
|
|
||||||
msg = "udhcpc lease loaded"
|
|
||||||
}
|
|
||||||
|
|
||||||
leaseExpiry, err := lease.SetLeaseExpiry()
|
|
||||||
if err != nil {
|
|
||||||
c.logger.Error().Err(err).Msg("failed to get dhcp lease expiry")
|
|
||||||
} else {
|
|
||||||
expiresIn := time.Until(leaseExpiry)
|
|
||||||
c.logger.Info().
|
|
||||||
Interface("expiry", leaseExpiry).
|
|
||||||
Str("expiresIn", expiresIn.String()).
|
|
||||||
Msg("current dhcp lease expiry time calculated")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.onLeaseChange(lease)
|
|
||||||
|
|
||||||
c.logger.Info().
|
|
||||||
Str("ip", lease.IPAddress.String()).
|
|
||||||
Str("leaseTime", lease.LeaseTime.String()).
|
|
||||||
Interface("data", lease).
|
|
||||||
Msg(msg)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *DHCPClient) GetLease() *Lease {
|
|
||||||
return c.lease
|
|
||||||
}
|
|
||||||
|
|
@ -1,436 +0,0 @@
|
||||||
package usbgadget
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/prometheus/procfs"
|
|
||||||
"github.com/sourcegraph/tf-dag/dag"
|
|
||||||
)
|
|
||||||
|
|
||||||
// it's a minimalistic implementation of ansible's file module with some modifications
|
|
||||||
// to make it more suitable for our use case
|
|
||||||
// https://docs.ansible.com/ansible/latest/modules/file_module.html
|
|
||||||
|
|
||||||
// we use this to check if the files in the gadget config are in the expected state
|
|
||||||
// and to update them if they are not in the expected state
|
|
||||||
|
|
||||||
type FileState uint8
|
|
||||||
type ChangeState uint8
|
|
||||||
type FileChangeResolvedAction uint8
|
|
||||||
|
|
||||||
type ApplyFunc func(c *ChangeSet, changes []*FileChange) error
|
|
||||||
|
|
||||||
const (
|
|
||||||
FileStateUnknown FileState = iota
|
|
||||||
FileStateAbsent
|
|
||||||
FileStateDirectory
|
|
||||||
FileStateFile
|
|
||||||
FileStateFileContentMatch
|
|
||||||
FileStateFileWrite // update file content without checking
|
|
||||||
FileStateMounted
|
|
||||||
FileStateMountedConfigFS
|
|
||||||
FileStateSymlink
|
|
||||||
FileStateSymlinkInOrderConfigFS // configfs is a shithole, so we need to check if the symlinks are created in the correct order
|
|
||||||
FileStateSymlinkNotInOrderConfigFS
|
|
||||||
FileStateTouch
|
|
||||||
)
|
|
||||||
|
|
||||||
var FileStateString = map[FileState]string{
|
|
||||||
FileStateUnknown: "UNKNOWN",
|
|
||||||
FileStateAbsent: "ABSENT",
|
|
||||||
FileStateDirectory: "DIRECTORY",
|
|
||||||
FileStateFile: "FILE",
|
|
||||||
FileStateFileContentMatch: "FILE_CONTENT_MATCH",
|
|
||||||
FileStateFileWrite: "FILE_WRITE",
|
|
||||||
FileStateMounted: "MOUNTED",
|
|
||||||
FileStateMountedConfigFS: "CONFIGFS_MOUNTED",
|
|
||||||
FileStateSymlink: "SYMLINK",
|
|
||||||
FileStateSymlinkInOrderConfigFS: "SYMLINK_IN_ORDER_CONFIGFS",
|
|
||||||
FileStateTouch: "TOUCH",
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
ChangeStateUnknown ChangeState = iota
|
|
||||||
ChangeStateRequired
|
|
||||||
ChangeStateNotChanged
|
|
||||||
ChangeStateChanged
|
|
||||||
ChangeStateError
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FileChangeResolvedActionUnknown FileChangeResolvedAction = iota
|
|
||||||
FileChangeResolvedActionDoNothing
|
|
||||||
FileChangeResolvedActionRemove
|
|
||||||
FileChangeResolvedActionCreateFile
|
|
||||||
FileChangeResolvedActionWriteFile
|
|
||||||
FileChangeResolvedActionUpdateFile
|
|
||||||
FileChangeResolvedActionAppendFile
|
|
||||||
FileChangeResolvedActionCreateSymlink
|
|
||||||
FileChangeResolvedActionRecreateSymlink
|
|
||||||
FileChangeResolvedActionCreateDirectoryAndSymlinks
|
|
||||||
FileChangeResolvedActionReorderSymlinks
|
|
||||||
FileChangeResolvedActionCreateDirectory
|
|
||||||
FileChangeResolvedActionRemoveDirectory
|
|
||||||
FileChangeResolvedActionTouch
|
|
||||||
FileChangeResolvedActionMountConfigFS
|
|
||||||
)
|
|
||||||
|
|
||||||
var FileChangeResolvedActionString = map[FileChangeResolvedAction]string{
|
|
||||||
FileChangeResolvedActionUnknown: "UNKNOWN",
|
|
||||||
FileChangeResolvedActionDoNothing: "DO_NOTHING",
|
|
||||||
FileChangeResolvedActionRemove: "REMOVE",
|
|
||||||
FileChangeResolvedActionCreateFile: "FILE_CREATE",
|
|
||||||
FileChangeResolvedActionWriteFile: "FILE_WRITE",
|
|
||||||
FileChangeResolvedActionUpdateFile: "FILE_UPDATE",
|
|
||||||
FileChangeResolvedActionAppendFile: "FILE_APPEND",
|
|
||||||
FileChangeResolvedActionCreateSymlink: "SYMLINK_CREATE",
|
|
||||||
FileChangeResolvedActionRecreateSymlink: "SYMLINK_RECREATE",
|
|
||||||
FileChangeResolvedActionCreateDirectoryAndSymlinks: "DIR_CREATE_AND_SYMLINKS",
|
|
||||||
FileChangeResolvedActionReorderSymlinks: "SYMLINK_REORDER",
|
|
||||||
FileChangeResolvedActionCreateDirectory: "DIR_CREATE",
|
|
||||||
FileChangeResolvedActionRemoveDirectory: "DIR_REMOVE",
|
|
||||||
FileChangeResolvedActionTouch: "TOUCH",
|
|
||||||
FileChangeResolvedActionMountConfigFS: "CONFIGFS_MOUNT",
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChangeSet struct {
|
|
||||||
Changes []FileChange
|
|
||||||
}
|
|
||||||
|
|
||||||
type RequestedFileChange struct {
|
|
||||||
Component string
|
|
||||||
Key string
|
|
||||||
Path string // will be used as Key if Key is empty
|
|
||||||
ParamSymlinks []symlink
|
|
||||||
ExpectedState FileState
|
|
||||||
ExpectedContent []byte
|
|
||||||
DependsOn []string
|
|
||||||
BeforeChange []string // if the file is going to be changed, apply the change first
|
|
||||||
Description string
|
|
||||||
IgnoreErrors bool
|
|
||||||
When string // only apply the change if when meets the condition
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileChange struct {
|
|
||||||
RequestedFileChange
|
|
||||||
ActualState FileState
|
|
||||||
ActualContent []byte
|
|
||||||
resolvedDeps []string
|
|
||||||
checked bool
|
|
||||||
changed ChangeState
|
|
||||||
action FileChangeResolvedAction
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *RequestedFileChange) String() string {
|
|
||||||
var s string
|
|
||||||
switch f.ExpectedState {
|
|
||||||
case FileStateDirectory:
|
|
||||||
s = fmt.Sprintf("dir: %s", f.Path)
|
|
||||||
case FileStateFile:
|
|
||||||
s = fmt.Sprintf("file: %s", f.Path)
|
|
||||||
case FileStateSymlink:
|
|
||||||
s = fmt.Sprintf("symlink: %s -> %s", f.Path, f.ExpectedContent)
|
|
||||||
case FileStateSymlinkInOrderConfigFS:
|
|
||||||
s = fmt.Sprintf("symlink_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent)
|
|
||||||
case FileStateSymlinkNotInOrderConfigFS:
|
|
||||||
s = fmt.Sprintf("symlink_not_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent)
|
|
||||||
case FileStateAbsent:
|
|
||||||
s = fmt.Sprintf("absent: %s", f.Path)
|
|
||||||
case FileStateFileContentMatch:
|
|
||||||
s = fmt.Sprintf("file: %s with content [%s]", f.Path, f.ExpectedContent)
|
|
||||||
case FileStateFileWrite:
|
|
||||||
s = fmt.Sprintf("write: %s with content [%s]", f.Path, f.ExpectedContent)
|
|
||||||
case FileStateMountedConfigFS:
|
|
||||||
s = fmt.Sprintf("configfs: %s", f.Path)
|
|
||||||
case FileStateTouch:
|
|
||||||
s = fmt.Sprintf("touch: %s", f.Path)
|
|
||||||
case FileStateUnknown:
|
|
||||||
s = fmt.Sprintf("unknown change for %s", f.Path)
|
|
||||||
default:
|
|
||||||
s = fmt.Sprintf("unknown expected state %d for %s", f.ExpectedState, f.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(f.Description) > 0 {
|
|
||||||
s += fmt.Sprintf(" (%s)", f.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *RequestedFileChange) IsSame(other *RequestedFileChange) bool {
|
|
||||||
return f.Path == other.Path &&
|
|
||||||
f.ExpectedState == other.ExpectedState &&
|
|
||||||
reflect.DeepEqual(f.ExpectedContent, other.ExpectedContent) &&
|
|
||||||
reflect.DeepEqual(f.DependsOn, other.DependsOn) &&
|
|
||||||
f.IgnoreErrors == other.IgnoreErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fc *FileChange) checkIfDirIsMountPoint() error {
|
|
||||||
// check if the file is a mount point
|
|
||||||
mounts, err := procfs.GetMounts()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get mounts")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mount := range mounts {
|
|
||||||
if mount.MountPoint == fc.Path {
|
|
||||||
fc.ActualState = FileStateMounted
|
|
||||||
fc.ActualContent = []byte(mount.Source)
|
|
||||||
|
|
||||||
if mount.FSType == "configfs" {
|
|
||||||
fc.ActualState = FileStateMountedConfigFS
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetActualState returns the actual state of the file at the given path.
|
|
||||||
func (fc *FileChange) getActualState() error {
|
|
||||||
l := defaultLogger.With().Str("path", fc.Path).Logger()
|
|
||||||
|
|
||||||
fi, err := os.Lstat(fc.Path)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
fc.ActualState = FileStateAbsent
|
|
||||||
} else {
|
|
||||||
l.Warn().Err(err).Msg("failed to stat file")
|
|
||||||
fc.ActualState = FileStateUnknown
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the file is a symlink
|
|
||||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
||||||
fc.ActualState = FileStateSymlink
|
|
||||||
// get the target of the symlink
|
|
||||||
target, err := os.Readlink(fc.Path)
|
|
||||||
if err != nil {
|
|
||||||
l.Warn().Err(err).Msg("failed to read symlink")
|
|
||||||
return fmt.Errorf("failed to read symlink")
|
|
||||||
}
|
|
||||||
// check if the target is a relative path
|
|
||||||
if !filepath.IsAbs(target) {
|
|
||||||
// make it absolute
|
|
||||||
target, err = filepath.Abs(filepath.Join(filepath.Dir(fc.Path), target))
|
|
||||||
if err != nil {
|
|
||||||
l.Warn().Err(err).Msg("failed to make symlink target absolute")
|
|
||||||
return fmt.Errorf("failed to make symlink target absolute")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fc.ActualContent = []byte(target)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if fi.IsDir() {
|
|
||||||
fc.ActualState = FileStateDirectory
|
|
||||||
|
|
||||||
switch fc.ExpectedState {
|
|
||||||
case FileStateMountedConfigFS:
|
|
||||||
err := fc.checkIfDirIsMountPoint()
|
|
||||||
if err != nil {
|
|
||||||
l.Warn().Err(err).Msg("failed to check if dir is mount point")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case FileStateSymlinkInOrderConfigFS:
|
|
||||||
state, err := checkIfSymlinksInOrder(fc, &l)
|
|
||||||
if err != nil {
|
|
||||||
l.Warn().Err(err).Msg("failed to check if symlinks are in order")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fc.ActualState = state
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if fi.Mode()&os.ModeDevice == os.ModeDevice {
|
|
||||||
l.Info().Msg("file is a device")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the file is a regular file
|
|
||||||
if fi.Mode().IsRegular() {
|
|
||||||
fc.ActualState = FileStateFile
|
|
||||||
// get the content of the file
|
|
||||||
content, err := os.ReadFile(fc.Path)
|
|
||||||
if err != nil {
|
|
||||||
l.Warn().Err(err).Msg("failed to read file")
|
|
||||||
return fmt.Errorf("failed to read file")
|
|
||||||
}
|
|
||||||
fc.ActualContent = content
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Warn().Interface("file_info", fi.Mode()).Bool("is_dir", fi.IsDir()).Msg("unknown file type")
|
|
||||||
|
|
||||||
return fmt.Errorf("unknown file type")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fc *FileChange) ResetActionResolution() {
|
|
||||||
fc.checked = false
|
|
||||||
fc.action = FileChangeResolvedActionUnknown
|
|
||||||
fc.changed = ChangeStateUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fc *FileChange) Action() FileChangeResolvedAction {
|
|
||||||
if !fc.checked {
|
|
||||||
fc.action = fc.getFileChangeResolvedAction()
|
|
||||||
fc.checked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return fc.action
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fc *FileChange) getFileChangeResolvedAction() FileChangeResolvedAction {
|
|
||||||
l := defaultLogger.With().Str("path", fc.Path).Logger()
|
|
||||||
|
|
||||||
// some actions are not needed to be checked
|
|
||||||
switch fc.ExpectedState {
|
|
||||||
case FileStateFileWrite:
|
|
||||||
return FileChangeResolvedActionWriteFile
|
|
||||||
case FileStateTouch:
|
|
||||||
return FileChangeResolvedActionTouch
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the actual state of the file
|
|
||||||
err := fc.getActualState()
|
|
||||||
if err != nil {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
|
|
||||||
baseName := filepath.Base(fc.Path)
|
|
||||||
|
|
||||||
switch fc.ExpectedState {
|
|
||||||
case FileStateDirectory:
|
|
||||||
// if the file is already a directory, do nothing
|
|
||||||
if fc.ActualState == FileStateDirectory {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionCreateDirectory
|
|
||||||
case FileStateFile:
|
|
||||||
// if the file is already a file, do nothing
|
|
||||||
if fc.ActualState == FileStateFile {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionCreateFile
|
|
||||||
case FileStateFileContentMatch:
|
|
||||||
// if the file is already a file with the expected content, do nothing
|
|
||||||
if fc.ActualState == FileStateFile {
|
|
||||||
looserMatch := baseName == "inquiry_string"
|
|
||||||
if compareFileContent(fc.ActualContent, fc.ExpectedContent, looserMatch) {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
// TODO: move this to somewhere else
|
|
||||||
// this is a workaround for the fact that the file is not updated if it has no content
|
|
||||||
if baseName == "file" &&
|
|
||||||
bytes.Equal(fc.ActualContent, []byte{}) &&
|
|
||||||
bytes.Equal(fc.ExpectedContent, []byte{0x0a}) {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionUpdateFile
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionCreateFile
|
|
||||||
case FileStateSymlink:
|
|
||||||
// if the file is already a symlink, check if the target is the same
|
|
||||||
if fc.ActualState == FileStateSymlink {
|
|
||||||
if reflect.DeepEqual(fc.ActualContent, fc.ExpectedContent) {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionRecreateSymlink
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionCreateSymlink
|
|
||||||
case FileStateSymlinkInOrderConfigFS:
|
|
||||||
// if the file is already a symlink, check if the target is the same
|
|
||||||
if fc.ActualState == FileStateSymlinkInOrderConfigFS {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionReorderSymlinks
|
|
||||||
case FileStateAbsent:
|
|
||||||
if fc.ActualState == FileStateAbsent {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionRemove
|
|
||||||
case FileStateMountedConfigFS:
|
|
||||||
if fc.ActualState == FileStateMountedConfigFS {
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
return FileChangeResolvedActionMountConfigFS
|
|
||||||
default:
|
|
||||||
l.Warn().Interface("file_change", FileStateString[fc.ExpectedState]).Msg("unknown expected state")
|
|
||||||
return FileChangeResolvedActionDoNothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSet) AddFileChangeStruct(r RequestedFileChange) {
|
|
||||||
fc := FileChange{
|
|
||||||
RequestedFileChange: r,
|
|
||||||
}
|
|
||||||
c.Changes = append(c.Changes, fc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSet) AddFileChange(component string, path string, expectedState FileState, expectedContent []byte, dependsOn []string, description string) {
|
|
||||||
c.AddFileChangeStruct(RequestedFileChange{
|
|
||||||
Component: component,
|
|
||||||
Path: path,
|
|
||||||
ExpectedState: expectedState,
|
|
||||||
ExpectedContent: expectedContent,
|
|
||||||
DependsOn: dependsOn,
|
|
||||||
Description: description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSet) ApplyChanges() error {
|
|
||||||
r := ChangeSetResolver{
|
|
||||||
changeset: c,
|
|
||||||
g: &dag.AcyclicGraph{},
|
|
||||||
l: defaultLogger,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSet) applyChange(change *FileChange) error {
|
|
||||||
switch change.Action() {
|
|
||||||
case FileChangeResolvedActionWriteFile:
|
|
||||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
|
||||||
case FileChangeResolvedActionUpdateFile:
|
|
||||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
|
||||||
case FileChangeResolvedActionCreateFile:
|
|
||||||
return os.WriteFile(change.Path, change.ExpectedContent, 0644)
|
|
||||||
case FileChangeResolvedActionCreateSymlink:
|
|
||||||
return os.Symlink(string(change.ExpectedContent), change.Path)
|
|
||||||
case FileChangeResolvedActionRecreateSymlink:
|
|
||||||
if err := os.Remove(change.Path); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove symlink: %w", err)
|
|
||||||
}
|
|
||||||
return os.Symlink(string(change.ExpectedContent), change.Path)
|
|
||||||
case FileChangeResolvedActionReorderSymlinks:
|
|
||||||
return recreateSymlinks(change, nil)
|
|
||||||
case FileChangeResolvedActionCreateDirectory:
|
|
||||||
return os.MkdirAll(change.Path, 0755)
|
|
||||||
case FileChangeResolvedActionRemove:
|
|
||||||
return os.Remove(change.Path)
|
|
||||||
case FileChangeResolvedActionRemoveDirectory:
|
|
||||||
return os.RemoveAll(change.Path)
|
|
||||||
case FileChangeResolvedActionTouch:
|
|
||||||
return os.Chtimes(change.Path, time.Now(), time.Now())
|
|
||||||
case FileChangeResolvedActionMountConfigFS:
|
|
||||||
return mountConfigFS(change.Path)
|
|
||||||
case FileChangeResolvedActionDoNothing:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown action: %d", change.Action())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSet) Apply() error {
|
|
||||||
return c.ApplyChanges()
|
|
||||||
}
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
//go:build arm && linux
|
|
||||||
|
|
||||||
package usbgadget
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
usbConfig = &Config{
|
|
||||||
VendorId: "0x1d6b", //The Linux Foundation
|
|
||||||
ProductId: "0x0104", //Multifunction Composite Gadget
|
|
||||||
SerialNumber: "",
|
|
||||||
Manufacturer: "JetKVM",
|
|
||||||
Product: "USB Emulation Device",
|
|
||||||
strictMode: true,
|
|
||||||
}
|
|
||||||
usbDevices = &Devices{
|
|
||||||
AbsoluteMouse: true,
|
|
||||||
RelativeMouse: true,
|
|
||||||
Keyboard: true,
|
|
||||||
MassStorage: true,
|
|
||||||
}
|
|
||||||
usbGadgetName = "jetkvm"
|
|
||||||
usbGadget *UsbGadget
|
|
||||||
)
|
|
||||||
|
|
||||||
var oldAbsoluteMouseCombinedReportDesc = []byte{
|
|
||||||
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
|
||||||
0x09, 0x02, // Usage (Mouse)
|
|
||||||
0xA1, 0x01, // Collection (Application)
|
|
||||||
|
|
||||||
// Report ID 1: Absolute Mouse Movement
|
|
||||||
0x85, 0x01, // Report ID (1)
|
|
||||||
0x09, 0x01, // Usage (Pointer)
|
|
||||||
0xA1, 0x00, // Collection (Physical)
|
|
||||||
0x05, 0x09, // Usage Page (Button)
|
|
||||||
0x19, 0x01, // Usage Minimum (0x01)
|
|
||||||
0x29, 0x03, // Usage Maximum (0x03)
|
|
||||||
0x15, 0x00, // Logical Minimum (0)
|
|
||||||
0x25, 0x01, // Logical Maximum (1)
|
|
||||||
0x75, 0x01, // Report Size (1)
|
|
||||||
0x95, 0x03, // Report Count (3)
|
|
||||||
0x81, 0x02, // Input (Data, Var, Abs)
|
|
||||||
0x95, 0x01, // Report Count (1)
|
|
||||||
0x75, 0x05, // Report Size (5)
|
|
||||||
0x81, 0x03, // Input (Cnst, Var, Abs)
|
|
||||||
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
|
||||||
0x09, 0x30, // Usage (X)
|
|
||||||
0x09, 0x31, // Usage (Y)
|
|
||||||
0x16, 0x00, 0x00, // Logical Minimum (0)
|
|
||||||
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
|
|
||||||
0x36, 0x00, 0x00, // Physical Minimum (0)
|
|
||||||
0x46, 0xFF, 0x7F, // Physical Maximum (32767)
|
|
||||||
0x75, 0x10, // Report Size (16)
|
|
||||||
0x95, 0x02, // Report Count (2)
|
|
||||||
0x81, 0x02, // Input (Data, Var, Abs)
|
|
||||||
0xC0, // End Collection
|
|
||||||
|
|
||||||
// Report ID 2: Relative Wheel Movement
|
|
||||||
0x85, 0x02, // Report ID (2)
|
|
||||||
0x09, 0x38, // Usage (Wheel)
|
|
||||||
0x15, 0x81, // Logical Minimum (-127)
|
|
||||||
0x25, 0x7F, // Logical Maximum (127)
|
|
||||||
0x75, 0x08, // Report Size (8)
|
|
||||||
0x95, 0x01, // Report Count (1)
|
|
||||||
0x81, 0x06, // Input (Data, Var, Rel)
|
|
||||||
|
|
||||||
0xC0, // End Collection
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsbGadgetInit(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
|
|
||||||
|
|
||||||
assert.NotNil(usbGadget)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsbGadgetStrictModeInitFail(t *testing.T) {
|
|
||||||
usbConfig.strictMode = true
|
|
||||||
u := NewUsbGadget("test", usbDevices, usbConfig, nil)
|
|
||||||
assert.Nil(t, u, "should be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUsbGadgetUDCNotBoundAfterReportDescrChanged(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
|
|
||||||
assert.NotNil(usbGadget)
|
|
||||||
|
|
||||||
// release the usb gadget and create a new one
|
|
||||||
usbGadget = nil
|
|
||||||
|
|
||||||
altGadgetConfig := defaultGadgetConfig
|
|
||||||
|
|
||||||
oldAbsoluteMouseConfig := altGadgetConfig["absolute_mouse"]
|
|
||||||
oldAbsoluteMouseConfig.reportDesc = oldAbsoluteMouseCombinedReportDesc
|
|
||||||
altGadgetConfig["absolute_mouse"] = oldAbsoluteMouseConfig
|
|
||||||
|
|
||||||
usbGadget = newUsbGadget(usbGadgetName, altGadgetConfig, usbDevices, usbConfig, nil)
|
|
||||||
assert.NotNil(usbGadget)
|
|
||||||
|
|
||||||
udcs := getUdcs()
|
|
||||||
assert.Equal(1, len(udcs), "should be only one UDC")
|
|
||||||
// check if the UDC is bound
|
|
||||||
udc := udcs[0]
|
|
||||||
assert.NotNil(udc, "UDC should exist")
|
|
||||||
|
|
||||||
udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/jetkvm/UDC")
|
|
||||||
assert.Nil(err, "usb_gadget/UDC should exist")
|
|
||||||
assert.Equal(strings.TrimSpace(udc), strings.TrimSpace(string(udcStr)), "UDC should be the same")
|
|
||||||
}
|
|
||||||
|
|
@ -1,192 +0,0 @@
|
||||||
package usbgadget
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/sourcegraph/tf-dag/dag"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ChangeSetResolver struct {
|
|
||||||
changeset *ChangeSet
|
|
||||||
|
|
||||||
l *zerolog.Logger
|
|
||||||
g *dag.AcyclicGraph
|
|
||||||
|
|
||||||
changesMap map[string]*FileChange
|
|
||||||
conditionalChangesMap map[string]*FileChange
|
|
||||||
|
|
||||||
orderedChanges []dag.Vertex
|
|
||||||
resolvedChanges []*FileChange
|
|
||||||
additionalResolveRequired bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSetResolver) toOrderedChanges() error {
|
|
||||||
for key, change := range c.changesMap {
|
|
||||||
v := c.g.Add(key)
|
|
||||||
|
|
||||||
for _, dependsOn := range change.DependsOn {
|
|
||||||
c.g.Connect(dag.BasicEdge(dependsOn, v))
|
|
||||||
}
|
|
||||||
for _, dependsOn := range change.resolvedDeps {
|
|
||||||
c.g.Connect(dag.BasicEdge(dependsOn, v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cycles := c.g.Cycles()
|
|
||||||
if len(cycles) > 0 {
|
|
||||||
return fmt.Errorf("cycles detected: %v", cycles)
|
|
||||||
}
|
|
||||||
|
|
||||||
orderedChanges := c.g.TopologicalOrder()
|
|
||||||
c.orderedChanges = orderedChanges
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSetResolver) doResolveChanges(initial bool) error {
|
|
||||||
resolvedChanges := make([]*FileChange, 0)
|
|
||||||
|
|
||||||
for _, key := range c.orderedChanges {
|
|
||||||
change := c.changesMap[key.(string)]
|
|
||||||
if change == nil {
|
|
||||||
c.l.Error().Str("key", key.(string)).Msg("fileChange not found")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !initial {
|
|
||||||
change.ResetActionResolution()
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedAction := change.Action()
|
|
||||||
|
|
||||||
resolvedChanges = append(resolvedChanges, change)
|
|
||||||
// no need to check the triggers if there's no change
|
|
||||||
if resolvedAction == FileChangeResolvedActionDoNothing {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !initial {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if change.BeforeChange != nil {
|
|
||||||
change.resolvedDeps = append(change.resolvedDeps, change.BeforeChange...)
|
|
||||||
c.additionalResolveRequired = true
|
|
||||||
|
|
||||||
// add the dependencies to the changes map
|
|
||||||
for _, dep := range change.BeforeChange {
|
|
||||||
depChange, ok := c.conditionalChangesMap[dep]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("dependency %s not found", dep)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.changesMap[dep] = depChange
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.resolvedChanges = resolvedChanges
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSetResolver) resolveChanges(initial bool) error {
|
|
||||||
// get the ordered changes
|
|
||||||
err := c.toOrderedChanges()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolve the changes
|
|
||||||
err = c.doResolveChanges(initial)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, change := range c.resolvedChanges {
|
|
||||||
c.l.Trace().Str("change", change.String()).Msg("resolved change")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.additionalResolveRequired || !initial {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.resolveChanges(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSetResolver) applyChanges() error {
|
|
||||||
for _, change := range c.resolvedChanges {
|
|
||||||
change.ResetActionResolution()
|
|
||||||
action := change.Action()
|
|
||||||
actionStr := FileChangeResolvedActionString[action]
|
|
||||||
|
|
||||||
l := c.l.Info()
|
|
||||||
if action == FileChangeResolvedActionDoNothing {
|
|
||||||
l = c.l.Trace()
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Str("action", actionStr).Str("change", change.String()).Msg("applying change")
|
|
||||||
|
|
||||||
err := c.changeset.applyChange(change)
|
|
||||||
if err != nil {
|
|
||||||
if change.IgnoreErrors {
|
|
||||||
c.l.Warn().Str("change", change.String()).Err(err).Msg("ignoring error")
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSetResolver) GetChanges() ([]*FileChange, error) {
|
|
||||||
localChanges := c.changeset.Changes
|
|
||||||
changesMap := make(map[string]*FileChange)
|
|
||||||
conditionalChangesMap := make(map[string]*FileChange)
|
|
||||||
|
|
||||||
// build the map of the changes
|
|
||||||
for _, change := range localChanges {
|
|
||||||
key := change.Key
|
|
||||||
if key == "" {
|
|
||||||
key = change.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove it from the map first
|
|
||||||
if change.When != "" {
|
|
||||||
conditionalChangesMap[key] = &change
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := changesMap[key]; ok {
|
|
||||||
if changesMap[key].IsSame(&change.RequestedFileChange) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"duplicate change: %s, current: %s, requested: %s",
|
|
||||||
key,
|
|
||||||
changesMap[key].String(),
|
|
||||||
change.String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
changesMap[key] = &change
|
|
||||||
}
|
|
||||||
|
|
||||||
c.changesMap = changesMap
|
|
||||||
c.conditionalChangesMap = conditionalChangesMap
|
|
||||||
|
|
||||||
err := c.resolveChanges(true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.resolvedChanges, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ChangeSetResolver) Apply() error {
|
|
||||||
if _, err := c.GetChanges(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.applyChanges()
|
|
||||||
}
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
package usbgadget
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type symlink struct {
|
|
||||||
Path string
|
|
||||||
Target string
|
|
||||||
}
|
|
||||||
|
|
||||||
func compareSymlinks(expected []symlink, actual []symlink) bool {
|
|
||||||
if len(expected) != len(actual) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return reflect.DeepEqual(expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkIfSymlinksInOrder(fc *FileChange, logger *zerolog.Logger) (FileState, error) {
|
|
||||||
if logger == nil {
|
|
||||||
logger = defaultLogger
|
|
||||||
}
|
|
||||||
l := logger.With().Str("path", fc.Path).Logger()
|
|
||||||
|
|
||||||
if len(fc.ParamSymlinks) == 0 {
|
|
||||||
return FileStateUnknown, fmt.Errorf("no symlinks to check")
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err := os.Lstat(fc.Path)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return FileStateAbsent, nil
|
|
||||||
} else {
|
|
||||||
l.Warn().Err(err).Msg("failed to stat file")
|
|
||||||
return FileStateUnknown, fmt.Errorf("failed to stat file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fi.IsDir() {
|
|
||||||
return FileStateUnknown, fmt.Errorf("file is not a directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := os.ReadDir(fc.Path)
|
|
||||||
symlinks := make([]symlink, 0)
|
|
||||||
if err != nil {
|
|
||||||
return FileStateUnknown, fmt.Errorf("failed to read directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if file.Type()&os.ModeSymlink != os.ModeSymlink {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
path := filepath.Join(fc.Path, file.Name())
|
|
||||||
target, err := os.Readlink(path)
|
|
||||||
if err != nil {
|
|
||||||
return FileStateUnknown, fmt.Errorf("failed to read symlink")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !filepath.IsAbs(target) {
|
|
||||||
target = filepath.Join(fc.Path, target)
|
|
||||||
newTarget, err := filepath.Abs(target)
|
|
||||||
if err != nil {
|
|
||||||
return FileStateUnknown, fmt.Errorf("failed to get absolute path")
|
|
||||||
}
|
|
||||||
target = newTarget
|
|
||||||
}
|
|
||||||
|
|
||||||
symlinks = append(symlinks, symlink{
|
|
||||||
Path: path,
|
|
||||||
Target: target,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare the symlinks with the expected symlinks
|
|
||||||
if compareSymlinks(fc.ParamSymlinks, symlinks) {
|
|
||||||
return FileStateSymlinkInOrderConfigFS, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace().Interface("expected", fc.ParamSymlinks).Interface("actual", symlinks).Msg("symlinks are not in order")
|
|
||||||
|
|
||||||
return FileStateSymlinkNotInOrderConfigFS, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func recreateSymlinks(fc *FileChange, logger *zerolog.Logger) error {
|
|
||||||
if logger == nil {
|
|
||||||
logger = defaultLogger
|
|
||||||
}
|
|
||||||
// remove all symlinks
|
|
||||||
files, err := os.ReadDir(fc.Path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
l := logger.With().Str("path", fc.Path).Logger()
|
|
||||||
l.Info().Msg("recreate symlinks")
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if file.Type()&os.ModeSymlink != os.ModeSymlink {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
l.Info().Str("name", file.Name()).Msg("remove symlink")
|
|
||||||
err := os.Remove(path.Join(fc.Path, file.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to remove symlink")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Info().Interface("param-symlinks", fc.ParamSymlinks).Msg("create symlinks")
|
|
||||||
|
|
||||||
// create the symlinks
|
|
||||||
for _, symlink := range fc.ParamSymlinks {
|
|
||||||
l.Info().Str("name", symlink.Path).Str("target", symlink.Target).Msg("create symlink")
|
|
||||||
|
|
||||||
path := symlink.Path
|
|
||||||
if !filepath.IsAbs(path) {
|
|
||||||
path = filepath.Join(fc.Path, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.Symlink(symlink.Target, path)
|
|
||||||
if err != nil {
|
|
||||||
l.Warn().Err(err).Msg("failed to create symlink")
|
|
||||||
return fmt.Errorf("failed to create symlink")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,11 @@ package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
type gadgetConfigItem struct {
|
type gadgetConfigItem struct {
|
||||||
|
|
@ -80,7 +84,7 @@ func (u *UsbGadget) isGadgetConfigItemEnabled(itemKey string) bool {
|
||||||
|
|
||||||
func (u *UsbGadget) loadGadgetConfig() {
|
func (u *UsbGadget) loadGadgetConfig() {
|
||||||
if u.customConfig.isEmpty {
|
if u.customConfig.isEmpty {
|
||||||
u.log.Trace().Msg("using default gadget config")
|
u.log.Trace("using default gadget config")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,33 +137,20 @@ func (u *UsbGadget) GetPath(itemKey string) (string, error) {
|
||||||
return joinPath(u.kvmGadgetPath, item.path), nil
|
return joinPath(u.kvmGadgetPath, item.path), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OverrideGadgetConfig overrides the gadget config for the given item and attribute.
|
func mountConfigFS() error {
|
||||||
// It returns an error if the item is not found or the attribute is not found.
|
_, err := os.Stat(gadgetPath)
|
||||||
// It returns true if the attribute is overridden, false otherwise.
|
// TODO: check if it's mounted properly
|
||||||
func (u *UsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) {
|
if err == nil {
|
||||||
u.configLock.Lock()
|
return nil
|
||||||
defer u.configLock.Unlock()
|
|
||||||
|
|
||||||
// get it as a pointer
|
|
||||||
_, ok := u.configMap[itemKey]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("config item %s not found", itemKey), false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.configMap[itemKey].attrs[itemAttr] == value {
|
if os.IsNotExist(err) {
|
||||||
return nil, false
|
err = exec.Command("mount", "-t", "configfs", "none", configFSPath).Run()
|
||||||
}
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to mount configfs: %w", err)
|
||||||
u.configMap[itemKey].attrs[itemAttr] = value
|
}
|
||||||
u.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("overriding gadget config")
|
} else {
|
||||||
|
return fmt.Errorf("unable to access usb gadget path: %w", err)
|
||||||
return nil, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func mountConfigFS(path string) error {
|
|
||||||
err := exec.Command("mount", "-t", "configfs", "none", path).Run()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to mount configfs: %w", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -172,14 +163,26 @@ func (u *UsbGadget) Init() error {
|
||||||
|
|
||||||
udcs := getUdcs()
|
udcs := getUdcs()
|
||||||
if len(udcs) < 1 {
|
if len(udcs) < 1 {
|
||||||
return u.logWarn("no udc found, skipping USB stack init", nil)
|
u.log.Error("no udc found, skipping USB stack init")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
u.udc = udcs[0]
|
u.udc = udcs[0]
|
||||||
|
_, err := os.Stat(u.kvmGadgetPath)
|
||||||
|
if err == nil {
|
||||||
|
u.log.Info("usb gadget already exists")
|
||||||
|
}
|
||||||
|
|
||||||
err := u.configureUsbGadget(false)
|
if err := mountConfigFS(); err != nil {
|
||||||
if err != nil {
|
u.log.Errorf("failed to mount configfs: %v, usb stack might not function properly", err)
|
||||||
return u.logError("unable to initialize USB stack", err)
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(u.configC1Path, 0755); err != nil {
|
||||||
|
u.log.Errorf("failed to create config path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.writeGadgetConfig(); err != nil {
|
||||||
|
u.log.Errorf("failed to start gadget: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -191,22 +194,143 @@ func (u *UsbGadget) UpdateGadgetConfig() error {
|
||||||
|
|
||||||
u.loadGadgetConfig()
|
u.loadGadgetConfig()
|
||||||
|
|
||||||
err := u.configureUsbGadget(true)
|
if err := u.writeGadgetConfig(); err != nil {
|
||||||
if err != nil {
|
u.log.Errorf("failed to update gadget: %v", err)
|
||||||
return u.logError("unable to update gadget config", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) configureUsbGadget(resetUsb bool) error {
|
func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems {
|
||||||
return u.WithTransaction(func() error {
|
items := make([]gadgetConfigItemWithKey, 0)
|
||||||
u.tx.MountConfigFS()
|
for key, item := range u.configMap {
|
||||||
u.tx.CreateConfigPath()
|
items = append(items, gadgetConfigItemWithKey{key, item})
|
||||||
u.tx.WriteGadgetConfig()
|
}
|
||||||
if resetUsb {
|
|
||||||
u.tx.RebindUsb(true)
|
sort.Slice(items, func(i, j int) bool {
|
||||||
}
|
return items[i].item.order < items[j].item.order
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeGadgetConfig() error {
|
||||||
|
// create kvm gadget path
|
||||||
|
err := os.MkdirAll(u.kvmGadgetPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.log.Tracef("writing gadget config")
|
||||||
|
for _, val := range u.getOrderedConfigItems() {
|
||||||
|
key := val.key
|
||||||
|
item := val.item
|
||||||
|
|
||||||
|
// check if the item is enabled in the config
|
||||||
|
if !u.isGadgetConfigItemEnabled(key) {
|
||||||
|
u.log.Tracef("disabling gadget config: %s", key)
|
||||||
|
err = u.disableGadgetItemConfig(item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
u.log.Tracef("writing gadget config: %s", key)
|
||||||
|
err = u.writeGadgetItemConfig(item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = u.writeUDC(); err != nil {
|
||||||
|
u.log.Errorf("failed to write UDC: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = u.rebindUsb(true); err != nil {
|
||||||
|
u.log.Infof("failed to rebind usb: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) disableGadgetItemConfig(item gadgetConfigItem) error {
|
||||||
|
// remove symlink if exists
|
||||||
|
if item.configPath == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := joinPath(u.configC1Path, item.configPath)
|
||||||
|
|
||||||
|
if _, err := os.Lstat(configPath); os.IsNotExist(err) {
|
||||||
|
u.log.Tracef("symlink %s does not exist", item.configPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(configPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove symlink %s: %w", item.configPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeGadgetItemConfig(item gadgetConfigItem) error {
|
||||||
|
// create directory for the item
|
||||||
|
gadgetItemPath := joinPath(u.kvmGadgetPath, item.path)
|
||||||
|
err := os.MkdirAll(gadgetItemPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create path %s: %w", gadgetItemPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(item.attrs) > 0 {
|
||||||
|
// write attributes for the item
|
||||||
|
err = u.writeGadgetAttrs(gadgetItemPath, item.attrs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write attributes for %s: %w", gadgetItemPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write report descriptor if available
|
||||||
|
if item.reportDesc != nil {
|
||||||
|
err = u.writeIfDifferent(path.Join(gadgetItemPath, "report_desc"), item.reportDesc, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create config directory if configAttrs are set
|
||||||
|
if len(item.configAttrs) > 0 {
|
||||||
|
configItemPath := joinPath(u.configC1Path, item.configPath)
|
||||||
|
err = os.MkdirAll(configItemPath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create path %s: %w", configItemPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = u.writeGadgetAttrs(configItemPath, item.configAttrs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write config attributes for %s: %w", configItemPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create symlink if configPath is set
|
||||||
|
if item.configPath != nil && item.configAttrs == nil {
|
||||||
|
configPath := joinPath(u.configC1Path, item.configPath)
|
||||||
|
u.log.Tracef("Creating symlink from %s to %s", configPath, gadgetItemPath)
|
||||||
|
if err := ensureSymlink(configPath, gadgetItemPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeGadgetAttrs(basePath string, attrs gadgetAttributes) error {
|
||||||
|
for key, val := range attrs {
|
||||||
|
filePath := filepath.Join(basePath, key)
|
||||||
|
err := u.writeIfDifferent(filePath, []byte(val), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write to %s: %w", filePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,349 +0,0 @@
|
||||||
package usbgadget
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// no os package should occur in this file
|
|
||||||
|
|
||||||
type UsbGadgetTransaction struct {
|
|
||||||
c *ChangeSet
|
|
||||||
|
|
||||||
// below are the fields that are needed to be set by the caller
|
|
||||||
log *zerolog.Logger
|
|
||||||
udc string
|
|
||||||
dwc3Path string
|
|
||||||
kvmGadgetPath string
|
|
||||||
configC1Path string
|
|
||||||
orderedConfigItems orderedGadgetConfigItems
|
|
||||||
isGadgetConfigItemEnabled func(key string) bool
|
|
||||||
|
|
||||||
reorderSymlinkChanges *RequestedFileChange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UsbGadget) newUsbGadgetTransaction(lock bool) error {
|
|
||||||
if lock {
|
|
||||||
u.txLock.Lock()
|
|
||||||
defer u.txLock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.tx != nil {
|
|
||||||
return fmt.Errorf("transaction already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
tx := &UsbGadgetTransaction{
|
|
||||||
c: &ChangeSet{},
|
|
||||||
log: u.log,
|
|
||||||
udc: u.udc,
|
|
||||||
dwc3Path: dwc3Path,
|
|
||||||
kvmGadgetPath: u.kvmGadgetPath,
|
|
||||||
configC1Path: u.configC1Path,
|
|
||||||
orderedConfigItems: u.getOrderedConfigItems(),
|
|
||||||
isGadgetConfigItemEnabled: u.isGadgetConfigItemEnabled,
|
|
||||||
}
|
|
||||||
u.tx = tx
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UsbGadget) WithTransaction(fn func() error) error {
|
|
||||||
u.txLock.Lock()
|
|
||||||
defer u.txLock.Unlock()
|
|
||||||
|
|
||||||
err := u.newUsbGadgetTransaction(false)
|
|
||||||
if err != nil {
|
|
||||||
u.log.Error().Err(err).Msg("failed to create transaction")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fn(); err != nil {
|
|
||||||
u.log.Error().Err(err).Msg("transaction failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
result := u.tx.Commit()
|
|
||||||
u.tx = nil
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) addFileChange(component string, change RequestedFileChange) string {
|
|
||||||
change.Component = component
|
|
||||||
tx.c.AddFileChangeStruct(change)
|
|
||||||
|
|
||||||
key := change.Key
|
|
||||||
if key == "" {
|
|
||||||
key = change.Path
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) mkdirAll(component string, path string, description string, deps []string) string {
|
|
||||||
return tx.addFileChange(component, RequestedFileChange{
|
|
||||||
Path: path,
|
|
||||||
ExpectedState: FileStateDirectory,
|
|
||||||
Description: description,
|
|
||||||
DependsOn: deps,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) removeFile(component string, path string, description string) string {
|
|
||||||
return tx.addFileChange(component, RequestedFileChange{
|
|
||||||
Path: path,
|
|
||||||
ExpectedState: FileStateAbsent,
|
|
||||||
Description: description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) Commit() error {
|
|
||||||
tx.addFileChange("gadget-finalize", *tx.reorderSymlinkChanges)
|
|
||||||
|
|
||||||
err := tx.c.Apply()
|
|
||||||
if err != nil {
|
|
||||||
tx.log.Error().Err(err).Msg("failed to update usbgadget configuration")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tx.log.Info().Msg("usbgadget configuration updated")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UsbGadget) getOrderedConfigItems() orderedGadgetConfigItems {
|
|
||||||
items := make([]gadgetConfigItemWithKey, 0)
|
|
||||||
for key, item := range u.configMap {
|
|
||||||
items = append(items, gadgetConfigItemWithKey{key, item})
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(items, func(i, j int) bool {
|
|
||||||
return items[i].item.order < items[j].item.order
|
|
||||||
})
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) MountConfigFS() {
|
|
||||||
tx.addFileChange("gadget", RequestedFileChange{
|
|
||||||
Path: configFSPath,
|
|
||||||
ExpectedState: FileStateMountedConfigFS,
|
|
||||||
Description: "mount configfs",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) CreateConfigPath() {
|
|
||||||
tx.mkdirAll(
|
|
||||||
"gadget",
|
|
||||||
tx.configC1Path,
|
|
||||||
"create config path",
|
|
||||||
[]string{configFSPath},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) WriteGadgetConfig() {
|
|
||||||
// create kvm gadget path
|
|
||||||
tx.mkdirAll(
|
|
||||||
"gadget",
|
|
||||||
tx.kvmGadgetPath,
|
|
||||||
"create kvm gadget path",
|
|
||||||
[]string{tx.configC1Path},
|
|
||||||
)
|
|
||||||
|
|
||||||
deps := make([]string, 0)
|
|
||||||
deps = append(deps, tx.kvmGadgetPath)
|
|
||||||
|
|
||||||
for _, val := range tx.orderedConfigItems {
|
|
||||||
key := val.key
|
|
||||||
item := val.item
|
|
||||||
|
|
||||||
// check if the item is enabled in the config
|
|
||||||
if !tx.isGadgetConfigItemEnabled(key) {
|
|
||||||
tx.DisableGadgetItemConfig(item)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
deps = tx.writeGadgetItemConfig(item, deps)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.WriteUDC()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) getDisableKeys() []string {
|
|
||||||
disableKeys := make([]string, 0)
|
|
||||||
for _, item := range tx.orderedConfigItems {
|
|
||||||
if !tx.isGadgetConfigItemEnabled(item.key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if item.item.configPath == nil || item.item.configAttrs != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
disableKeys = append(disableKeys, fmt.Sprintf("disable-%s", item.item.device))
|
|
||||||
}
|
|
||||||
return disableKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) DisableGadgetItemConfig(item gadgetConfigItem) {
|
|
||||||
// remove symlink if exists
|
|
||||||
if item.configPath == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configPath := joinPath(tx.configC1Path, item.configPath)
|
|
||||||
_ = tx.removeFile("gadget", configPath, "remove symlink: disable gadget config")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, deps []string) []string {
|
|
||||||
component := item.device
|
|
||||||
|
|
||||||
// create directory for the item
|
|
||||||
files := make([]string, 0)
|
|
||||||
files = append(files, deps...)
|
|
||||||
|
|
||||||
gadgetItemPath := joinPath(tx.kvmGadgetPath, item.path)
|
|
||||||
if gadgetItemPath != tx.kvmGadgetPath {
|
|
||||||
gadgetItemDir := tx.mkdirAll(component, gadgetItemPath, "create gadget item directory", files)
|
|
||||||
files = append(files, gadgetItemDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeChange := make([]string, 0)
|
|
||||||
disableGadgetItemKey := fmt.Sprintf("disable-%s", item.device)
|
|
||||||
if item.configPath != nil && item.configAttrs == nil {
|
|
||||||
beforeChange = append(beforeChange, tx.getDisableKeys()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(item.attrs) > 0 {
|
|
||||||
// write attributes for the item
|
|
||||||
files = append(files, tx.writeGadgetAttrs(
|
|
||||||
gadgetItemPath,
|
|
||||||
item.attrs,
|
|
||||||
component,
|
|
||||||
beforeChange,
|
|
||||||
)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// write report descriptor if available
|
|
||||||
reportDescPath := path.Join(gadgetItemPath, "report_desc")
|
|
||||||
if item.reportDesc != nil {
|
|
||||||
tx.addFileChange(component, RequestedFileChange{
|
|
||||||
Path: reportDescPath,
|
|
||||||
ExpectedState: FileStateFileContentMatch,
|
|
||||||
ExpectedContent: item.reportDesc,
|
|
||||||
Description: "write report descriptor",
|
|
||||||
BeforeChange: beforeChange,
|
|
||||||
DependsOn: files,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tx.addFileChange(component, RequestedFileChange{
|
|
||||||
Path: reportDescPath,
|
|
||||||
ExpectedState: FileStateAbsent,
|
|
||||||
Description: "remove report descriptor",
|
|
||||||
BeforeChange: beforeChange,
|
|
||||||
DependsOn: files,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
files = append(files, reportDescPath)
|
|
||||||
|
|
||||||
// create config directory if configAttrs are set
|
|
||||||
if len(item.configAttrs) > 0 {
|
|
||||||
configItemPath := joinPath(tx.configC1Path, item.configPath)
|
|
||||||
if configItemPath != tx.configC1Path {
|
|
||||||
configItemDir := tx.mkdirAll(component, configItemPath, "create config item directory", files)
|
|
||||||
files = append(files, configItemDir)
|
|
||||||
}
|
|
||||||
files = append(files, tx.writeGadgetAttrs(
|
|
||||||
configItemPath,
|
|
||||||
item.configAttrs,
|
|
||||||
component,
|
|
||||||
beforeChange,
|
|
||||||
)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create symlink if configPath is set
|
|
||||||
if item.configPath != nil && item.configAttrs == nil {
|
|
||||||
configPath := joinPath(tx.configC1Path, item.configPath)
|
|
||||||
|
|
||||||
// the change will be only applied by `beforeChange`
|
|
||||||
tx.addFileChange(component, RequestedFileChange{
|
|
||||||
Key: disableGadgetItemKey,
|
|
||||||
Path: configPath,
|
|
||||||
ExpectedState: FileStateAbsent,
|
|
||||||
When: "beforeChange", // TODO: make it more flexible
|
|
||||||
Description: "remove symlink",
|
|
||||||
})
|
|
||||||
|
|
||||||
tx.addReorderSymlinkChange(configPath, gadgetItemPath, files)
|
|
||||||
}
|
|
||||||
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) writeGadgetAttrs(basePath string, attrs gadgetAttributes, component string, beforeChange []string) (files []string) {
|
|
||||||
files = make([]string, 0)
|
|
||||||
for key, val := range attrs {
|
|
||||||
filePath := filepath.Join(basePath, key)
|
|
||||||
tx.addFileChange(component, RequestedFileChange{
|
|
||||||
Path: filePath,
|
|
||||||
ExpectedState: FileStateFileContentMatch,
|
|
||||||
ExpectedContent: []byte(val),
|
|
||||||
Description: "write gadget attribute",
|
|
||||||
DependsOn: []string{basePath},
|
|
||||||
BeforeChange: beforeChange,
|
|
||||||
})
|
|
||||||
files = append(files, filePath)
|
|
||||||
}
|
|
||||||
return files
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) addReorderSymlinkChange(path string, target string, deps []string) {
|
|
||||||
tx.log.Trace().Str("path", path).Str("target", target).Msg("add reorder symlink change")
|
|
||||||
|
|
||||||
if tx.reorderSymlinkChanges == nil {
|
|
||||||
tx.reorderSymlinkChanges = &RequestedFileChange{
|
|
||||||
Component: "gadget-finalize",
|
|
||||||
Key: "reorder-symlinks",
|
|
||||||
Path: tx.configC1Path,
|
|
||||||
ExpectedState: FileStateSymlinkInOrderConfigFS,
|
|
||||||
Description: "order symlinks",
|
|
||||||
ParamSymlinks: []symlink{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.reorderSymlinkChanges.DependsOn = append(tx.reorderSymlinkChanges.DependsOn, deps...)
|
|
||||||
tx.reorderSymlinkChanges.ParamSymlinks = append(tx.reorderSymlinkChanges.ParamSymlinks, symlink{
|
|
||||||
Path: path,
|
|
||||||
Target: target,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) WriteUDC() {
|
|
||||||
// bound the gadget to a UDC (USB Device Controller)
|
|
||||||
path := path.Join(tx.kvmGadgetPath, "UDC")
|
|
||||||
tx.addFileChange("udc", RequestedFileChange{
|
|
||||||
Key: "udc",
|
|
||||||
Path: path,
|
|
||||||
ExpectedState: FileStateFileContentMatch,
|
|
||||||
ExpectedContent: []byte(tx.udc),
|
|
||||||
DependsOn: []string{"reorder-symlinks"},
|
|
||||||
Description: "write UDC",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) RebindUsb(ignoreUnbindError bool) {
|
|
||||||
// remove the gadget from the UDC
|
|
||||||
tx.addFileChange("udc", RequestedFileChange{
|
|
||||||
Path: path.Join(tx.dwc3Path, "unbind"),
|
|
||||||
ExpectedState: FileStateFileWrite,
|
|
||||||
ExpectedContent: []byte(tx.udc),
|
|
||||||
Description: "unbind UDC",
|
|
||||||
DependsOn: []string{"udc"},
|
|
||||||
IgnoreErrors: ignoreUnbindError,
|
|
||||||
})
|
|
||||||
// bind the gadget to the UDC
|
|
||||||
tx.addFileChange("udc", RequestedFileChange{
|
|
||||||
Path: path.Join(tx.dwc3Path, "bind"),
|
|
||||||
ExpectedState: FileStateFileWrite,
|
|
||||||
ExpectedContent: []byte(tx.udc),
|
|
||||||
Description: "bind UDC",
|
|
||||||
DependsOn: []string{path.Join(tx.dwc3Path, "unbind")},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -65,7 +65,7 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
|
||||||
|
|
||||||
_, err := u.keyboardHidFile.Write(data)
|
_, err := u.keyboardHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg0")
|
u.log.Errorf("failed to write to hidg0: %w", err)
|
||||||
u.keyboardHidFile.Close()
|
u.keyboardHidFile.Close()
|
||||||
u.keyboardHidFile = nil
|
u.keyboardHidFile = nil
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,6 @@ var absoluteMouseCombinedReportDesc = []byte{
|
||||||
0x09, 0x38, // Usage (Wheel)
|
0x09, 0x38, // Usage (Wheel)
|
||||||
0x15, 0x81, // Logical Minimum (-127)
|
0x15, 0x81, // Logical Minimum (-127)
|
||||||
0x25, 0x7F, // Logical Maximum (127)
|
0x25, 0x7F, // Logical Maximum (127)
|
||||||
0x35, 0x00, // Physical Minimum (0) = Reset Physical Minimum
|
|
||||||
0x45, 0x00, // Physical Maximum (0) = Reset Physical Maximum
|
|
||||||
0x75, 0x08, // Report Size (8)
|
0x75, 0x08, // Report Size (8)
|
||||||
0x95, 0x01, // Report Count (1)
|
0x95, 0x01, // Report Count (1)
|
||||||
0x81, 0x06, // Input (Data, Var, Rel)
|
0x81, 0x06, // Input (Data, Var, Rel)
|
||||||
|
|
@ -75,7 +73,7 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
|
||||||
|
|
||||||
_, err := u.absMouseHidFile.Write(data)
|
_, err := u.absMouseHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg1")
|
u.log.Errorf("failed to write to hidg1: %w", err)
|
||||||
u.absMouseHidFile.Close()
|
u.absMouseHidFile.Close()
|
||||||
u.absMouseHidFile = nil
|
u.absMouseHidFile = nil
|
||||||
return err
|
return err
|
||||||
|
|
@ -107,16 +105,24 @@ func (u *UsbGadget) AbsMouseWheelReport(wheelY int8) error {
|
||||||
u.absMouseLock.Lock()
|
u.absMouseLock.Lock()
|
||||||
defer u.absMouseLock.Unlock()
|
defer u.absMouseLock.Unlock()
|
||||||
|
|
||||||
// Only send a report if the value is non-zero
|
// Accumulate the wheelY value
|
||||||
if wheelY == 0 {
|
u.absMouseAccumulatedWheelY += float64(wheelY) / 8.0
|
||||||
|
|
||||||
|
// Only send a report if the accumulated value is significant
|
||||||
|
if abs(u.absMouseAccumulatedWheelY) < 1.0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scaledWheelY := int8(u.absMouseAccumulatedWheelY)
|
||||||
|
|
||||||
err := u.absMouseWriteHidFile([]byte{
|
err := u.absMouseWriteHidFile([]byte{
|
||||||
2, // Report ID 2
|
2, // Report ID 2
|
||||||
byte(wheelY), // Wheel Y (signed)
|
byte(scaledWheelY), // Scaled Wheel Y (signed)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset the accumulator, keeping any remainder
|
||||||
|
u.absMouseAccumulatedWheelY -= float64(scaledWheelY)
|
||||||
|
|
||||||
u.resetUserInputTime()
|
u.resetUserInputTime()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
|
||||||
|
|
||||||
_, err := u.relMouseHidFile.Write(data)
|
_, err := u.relMouseHidFile.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.log.Error().Err(err).Msg("failed to write to hidg2")
|
u.log.Errorf("failed to write to hidg2: %w", err)
|
||||||
u.relMouseHidFile.Close()
|
u.relMouseHidFile.Close()
|
||||||
u.relMouseHidFile = nil
|
u.relMouseHidFile = nil
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package usbgadget
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *UsbGadget) logWarn(msg string, err error) error {
|
|
||||||
if err == nil {
|
|
||||||
err = errors.New(msg)
|
|
||||||
}
|
|
||||||
if u.strictMode {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
u.log.Warn().Err(err).Msg(msg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UsbGadget) logError(msg string, err error) error {
|
|
||||||
if err == nil {
|
|
||||||
err = errors.New(msg)
|
|
||||||
}
|
|
||||||
if u.strictMode {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
u.log.Error().Err(err).Msg(msg)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -14,13 +14,10 @@ var massStorageLun0Config = gadgetConfigItem{
|
||||||
order: 3001,
|
order: 3001,
|
||||||
path: []string{"functions", "mass_storage.usb0", "lun.0"},
|
path: []string{"functions", "mass_storage.usb0", "lun.0"},
|
||||||
attrs: gadgetAttributes{
|
attrs: gadgetAttributes{
|
||||||
"cdrom": "1",
|
"cdrom": "1",
|
||||||
"ro": "1",
|
"ro": "1",
|
||||||
"removable": "1",
|
"removable": "1",
|
||||||
"file": "\n",
|
"file": "\n",
|
||||||
// the additional whitespace is intentional to avoid the "JetKVM V irtual Media" string
|
"inquiry_string": "JetKVM Virtual Media",
|
||||||
// https://github.com/jetkvm/rv1106-system/blob/778133a1c153041e73f7de86c9c434a2753ea65d/sysdrv/source/uboot/u-boot/drivers/usb/gadget/f_mass_storage.c#L2556
|
|
||||||
// Vendor (8 chars), product (16 chars)
|
|
||||||
"inquiry_string": "JetKVM Virtual Media",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ func rebindUsb(udc string, ignoreUnbindError bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) rebindUsb(ignoreUnbindError bool) error {
|
func (u *UsbGadget) rebindUsb(ignoreUnbindError bool) error {
|
||||||
u.log.Info().Str("udc", u.udc).Msg("rebinding USB gadget to UDC")
|
u.log.Infof("rebinding USB gadget to UDC %s", u.udc)
|
||||||
return rebindUsb(u.udc, ignoreUnbindError)
|
return rebindUsb(u.udc, ignoreUnbindError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +50,18 @@ func (u *UsbGadget) RebindUsb(ignoreUnbindError bool) error {
|
||||||
return u.rebindUsb(ignoreUnbindError)
|
return u.rebindUsb(ignoreUnbindError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UsbGadget) writeUDC() error {
|
||||||
|
path := path.Join(u.kvmGadgetPath, "UDC")
|
||||||
|
|
||||||
|
u.log.Tracef("writing UDC %s to %s", u.udc, path)
|
||||||
|
err := u.writeIfDifferent(path, []byte(u.udc), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write UDC: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetUsbState returns the current state of the USB gadget
|
// GetUsbState returns the current state of the USB gadget
|
||||||
func (u *UsbGadget) GetUsbState() (state string) {
|
func (u *UsbGadget) GetUsbState() (state string) {
|
||||||
stateFile := path.Join("/sys/class/udc", u.udc, "state")
|
stateFile := path.Join("/sys/class/udc", u.udc, "state")
|
||||||
|
|
@ -58,7 +70,7 @@ func (u *UsbGadget) GetUsbState() (state string) {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return "not attached"
|
return "not attached"
|
||||||
} else {
|
} else {
|
||||||
u.log.Trace().Err(err).Msg("failed to read usb state")
|
u.log.Tracef("failed to read usb state: %v", err)
|
||||||
}
|
}
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
"github.com/pion/logging"
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Devices is a struct that represents the USB devices that can be enabled on a USB gadget.
|
// Devices is a struct that represents the USB devices that can be enabled on a USB gadget.
|
||||||
|
|
@ -29,8 +28,7 @@ type Config struct {
|
||||||
Manufacturer string `json:"manufacturer"`
|
Manufacturer string `json:"manufacturer"`
|
||||||
Product string `json:"product"`
|
Product string `json:"product"`
|
||||||
|
|
||||||
strictMode bool // when it's enabled, all warnings will be converted to errors
|
isEmpty bool
|
||||||
isEmpty bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultUsbGadgetDevices = Devices{
|
var defaultUsbGadgetDevices = Devices{
|
||||||
|
|
@ -61,31 +59,22 @@ type UsbGadget struct {
|
||||||
|
|
||||||
enabledDevices Devices
|
enabledDevices Devices
|
||||||
|
|
||||||
strictMode bool // only intended for testing for now
|
|
||||||
|
|
||||||
absMouseAccumulatedWheelY float64
|
absMouseAccumulatedWheelY float64
|
||||||
|
|
||||||
lastUserInput time.Time
|
lastUserInput time.Time
|
||||||
|
|
||||||
tx *UsbGadgetTransaction
|
log logging.LeveledLogger
|
||||||
txLock sync.Mutex
|
|
||||||
|
|
||||||
log *zerolog.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configFSPath = "/sys/kernel/config"
|
const configFSPath = "/sys/kernel/config"
|
||||||
const gadgetPath = "/sys/kernel/config/usb_gadget"
|
const gadgetPath = "/sys/kernel/config/usb_gadget"
|
||||||
|
|
||||||
var defaultLogger = logging.GetSubsystemLogger("usbgadget")
|
var defaultLogger = logging.NewDefaultLoggerFactory().NewLogger("usbgadget")
|
||||||
|
|
||||||
// NewUsbGadget creates a new UsbGadget.
|
// NewUsbGadget creates a new UsbGadget.
|
||||||
func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
|
func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *logging.LeveledLogger) *UsbGadget {
|
||||||
return newUsbGadget(name, defaultGadgetConfig, enabledDevices, config, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
|
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = defaultLogger
|
logger = &defaultLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
if enabledDevices == nil {
|
if enabledDevices == nil {
|
||||||
|
|
@ -100,23 +89,20 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
|
||||||
name: name,
|
name: name,
|
||||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||||
configMap: configMap,
|
configMap: defaultGadgetConfig,
|
||||||
customConfig: *config,
|
customConfig: *config,
|
||||||
configLock: sync.Mutex{},
|
configLock: sync.Mutex{},
|
||||||
keyboardLock: sync.Mutex{},
|
keyboardLock: sync.Mutex{},
|
||||||
absMouseLock: sync.Mutex{},
|
absMouseLock: sync.Mutex{},
|
||||||
relMouseLock: sync.Mutex{},
|
relMouseLock: sync.Mutex{},
|
||||||
txLock: sync.Mutex{},
|
|
||||||
enabledDevices: *enabledDevices,
|
enabledDevices: *enabledDevices,
|
||||||
lastUserInput: time.Now(),
|
lastUserInput: time.Now(),
|
||||||
log: logger,
|
log: *logger,
|
||||||
|
|
||||||
strictMode: config.strictMode,
|
|
||||||
|
|
||||||
absMouseAccumulatedWheelY: 0,
|
absMouseAccumulatedWheelY: 0,
|
||||||
}
|
}
|
||||||
if err := g.Init(); err != nil {
|
if err := g.Init(); err != nil {
|
||||||
logger.Error().Err(err).Msg("failed to init USB gadget")
|
g.log.Errorf("failed to init USB gadget: %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,78 +3,61 @@ package usbgadget
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Helper function to get absolute value of float64
|
||||||
|
func abs(x float64) float64 {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
func joinPath(basePath string, paths []string) string {
|
func joinPath(basePath string, paths []string) string {
|
||||||
pathArr := append([]string{basePath}, paths...)
|
pathArr := append([]string{basePath}, paths...)
|
||||||
return filepath.Join(pathArr...)
|
return filepath.Join(pathArr...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func hexToDecimal(hex string) (int64, error) {
|
func ensureSymlink(linkPath string, target string) error {
|
||||||
decimal, err := strconv.ParseInt(hex, 16, 64)
|
if _, err := os.Lstat(linkPath); err == nil {
|
||||||
if err != nil {
|
currentTarget, err := os.Readlink(linkPath)
|
||||||
return 0, err
|
if err != nil || currentTarget != target {
|
||||||
|
err = os.Remove(linkPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove existing symlink %s: %w", linkPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to check if symlink exists: %w", err)
|
||||||
}
|
}
|
||||||
return decimal, nil
|
|
||||||
|
if err := os.Symlink(target, linkPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to create symlink from %s to %s: %w", linkPath, target, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decimalToOctal(decimal int64) string {
|
func (u *UsbGadget) writeIfDifferent(filePath string, content []byte, permMode os.FileMode) error {
|
||||||
return fmt.Sprintf("%04o", decimal)
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
}
|
oldContent, err := os.ReadFile(filePath)
|
||||||
|
if err == nil {
|
||||||
func hexToOctal(hex string) (string, error) {
|
if bytes.Equal(oldContent, content) {
|
||||||
hex = strings.ToLower(hex)
|
u.log.Tracef("skipping writing to %s as it already has the correct content", filePath)
|
||||||
hex = strings.Replace(hex, "0x", "", 1) //remove 0x or 0X
|
return nil
|
||||||
|
}
|
||||||
decimal, err := hexToDecimal(hex)
|
|
||||||
if err != nil {
|
if len(oldContent) == len(content)+1 &&
|
||||||
return "", err
|
bytes.Equal(oldContent[:len(content)], content) &&
|
||||||
}
|
oldContent[len(content)] == 10 {
|
||||||
|
u.log.Tracef("skipping writing to %s as it already has the correct content", filePath)
|
||||||
// Convert the decimal integer to an octal string.
|
return nil
|
||||||
octal := decimalToOctal(decimal)
|
}
|
||||||
return octal, nil
|
|
||||||
}
|
u.log.Tracef("writing to %s as it has different content old%v new%v", filePath, oldContent, content)
|
||||||
|
}
|
||||||
func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool) bool {
|
}
|
||||||
if bytes.Equal(oldContent, newContent) {
|
return os.WriteFile(filePath, content, permMode)
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(oldContent) == len(newContent)+1 &&
|
|
||||||
bytes.Equal(oldContent[:len(newContent)], newContent) &&
|
|
||||||
oldContent[len(newContent)] == 10 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(newContent) == 4 {
|
|
||||||
if len(oldContent) < 6 || len(oldContent) > 7 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(oldContent) == 7 && oldContent[6] == 0x0a {
|
|
||||||
oldContent = oldContent[:6]
|
|
||||||
}
|
|
||||||
|
|
||||||
oldOctalValue, err := hexToOctal(string(oldContent))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldOctalValue == string(newContent) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if looserMatch {
|
|
||||||
oldContentStr := strings.TrimSpace(string(oldContent))
|
|
||||||
newContentStr := strings.TrimSpace(string(newContent))
|
|
||||||
|
|
||||||
return oldContentStr == newContentStr
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package websecure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
var defaultLogger = zerolog.New(os.Stdout).With().Str("component", "websecure").Logger()
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
package websecure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/elliptic"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"golang.org/x/net/idna"
|
|
||||||
)
|
|
||||||
|
|
||||||
const selfSignerCAMagicName = "__ca__"
|
|
||||||
|
|
||||||
type SelfSigner struct {
|
|
||||||
store *CertStore
|
|
||||||
log *zerolog.Logger
|
|
||||||
|
|
||||||
caInfo pkix.Name
|
|
||||||
|
|
||||||
DefaultDomain string
|
|
||||||
DefaultOrg string
|
|
||||||
DefaultOU string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSelfSigner(
|
|
||||||
store *CertStore,
|
|
||||||
log *zerolog.Logger,
|
|
||||||
defaultDomain,
|
|
||||||
defaultOrg,
|
|
||||||
defaultOU,
|
|
||||||
caName string,
|
|
||||||
) *SelfSigner {
|
|
||||||
return &SelfSigner{
|
|
||||||
store: store,
|
|
||||||
log: log,
|
|
||||||
DefaultDomain: defaultDomain,
|
|
||||||
DefaultOrg: defaultOrg,
|
|
||||||
DefaultOU: defaultOU,
|
|
||||||
caInfo: pkix.Name{
|
|
||||||
CommonName: caName,
|
|
||||||
Organization: []string{defaultOrg},
|
|
||||||
OrganizationalUnit: []string{defaultOU},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SelfSigner) getCA() *tls.Certificate {
|
|
||||||
return s.createSelfSignedCert(selfSignerCAMagicName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SelfSigner) createSelfSignedCert(hostname string) *tls.Certificate {
|
|
||||||
if tlsCert := s.store.certificates[hostname]; tlsCert != nil {
|
|
||||||
return tlsCert
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if hostname is the CA magic name
|
|
||||||
var ca *tls.Certificate
|
|
||||||
if hostname != selfSignerCAMagicName {
|
|
||||||
ca = s.getCA()
|
|
||||||
if ca == nil {
|
|
||||||
s.log.Error().Msg("Failed to get CA certificate")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.log.Info().Str("hostname", hostname).Msg("Creating self-signed certificate")
|
|
||||||
|
|
||||||
// lock the store while creating the certificate (do not move upwards)
|
|
||||||
s.store.certLock.Lock()
|
|
||||||
defer s.store.certLock.Unlock()
|
|
||||||
|
|
||||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to generate private key")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
notBefore := time.Now()
|
|
||||||
notAfter := notBefore.AddDate(1, 0, 0)
|
|
||||||
|
|
||||||
serialNumber, err := generateSerialNumber()
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to generate serial number")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsName := hostname
|
|
||||||
ip := net.ParseIP(hostname)
|
|
||||||
if ip != nil {
|
|
||||||
dnsName = s.DefaultDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up CSR
|
|
||||||
isCA := hostname == selfSignerCAMagicName
|
|
||||||
subject := pkix.Name{
|
|
||||||
CommonName: hostname,
|
|
||||||
Organization: []string{s.DefaultOrg},
|
|
||||||
OrganizationalUnit: []string{s.DefaultOU},
|
|
||||||
}
|
|
||||||
keyUsage := x509.KeyUsageDigitalSignature
|
|
||||||
extKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
|
|
||||||
|
|
||||||
// check if hostname is the CA magic name, and if so, set the subject to the CA info
|
|
||||||
if isCA {
|
|
||||||
subject = s.caInfo
|
|
||||||
keyUsage |= x509.KeyUsageCertSign
|
|
||||||
extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth)
|
|
||||||
notAfter = notBefore.AddDate(10, 0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
cert := x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
Subject: subject,
|
|
||||||
NotBefore: notBefore,
|
|
||||||
NotAfter: notAfter,
|
|
||||||
IsCA: isCA,
|
|
||||||
KeyUsage: keyUsage,
|
|
||||||
ExtKeyUsage: extKeyUsage,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up DNS names and IP addresses
|
|
||||||
if !isCA {
|
|
||||||
cert.DNSNames = []string{dnsName}
|
|
||||||
if ip != nil {
|
|
||||||
cert.IPAddresses = []net.IP{ip}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up parent certificate
|
|
||||||
parent := &cert
|
|
||||||
parentPriv := priv
|
|
||||||
if ca != nil {
|
|
||||||
parent, err = x509.ParseCertificate(ca.Certificate[0])
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to parse parent certificate")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
parentPriv = ca.PrivateKey.(*ecdsa.PrivateKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
certBytes, err := x509.CreateCertificate(rand.Reader, &cert, parent, &priv.PublicKey, parentPriv)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to create certificate")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsCert := &tls.Certificate{
|
|
||||||
Certificate: [][]byte{certBytes},
|
|
||||||
PrivateKey: priv,
|
|
||||||
}
|
|
||||||
if ca != nil {
|
|
||||||
tlsCert.Certificate = append(tlsCert.Certificate, ca.Certificate...)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.store.certificates[hostname] = tlsCert
|
|
||||||
s.store.saveCertificate(hostname)
|
|
||||||
|
|
||||||
return tlsCert
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCertificate returns the certificate for the given hostname
|
|
||||||
// returns nil if the certificate is not found
|
|
||||||
func (s *SelfSigner) GetCertificate(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
var hostname string
|
|
||||||
if info.ServerName != "" && info.ServerName != selfSignerCAMagicName {
|
|
||||||
hostname = info.ServerName
|
|
||||||
} else {
|
|
||||||
hostname = strings.Split(info.Conn.LocalAddr().String(), ":")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
s.log.Info().Str("hostname", hostname).Strs("supported_protos", info.SupportedProtos).Msg("TLS handshake")
|
|
||||||
|
|
||||||
// convert hostname to punycode
|
|
||||||
h, err := idna.Lookup.ToASCII(hostname)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Warn().Str("hostname", hostname).Err(err).Str("remote_addr", info.Conn.RemoteAddr().String()).Msg("Hostname is not valid")
|
|
||||||
hostname = s.DefaultDomain
|
|
||||||
} else {
|
|
||||||
hostname = h
|
|
||||||
}
|
|
||||||
|
|
||||||
cert := s.createSelfSignedCert(hostname)
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
package websecure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CertStore struct {
|
|
||||||
certificates map[string]*tls.Certificate
|
|
||||||
certLock *sync.Mutex
|
|
||||||
|
|
||||||
storePath string
|
|
||||||
|
|
||||||
log *zerolog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCertStore(storePath string, log *zerolog.Logger) *CertStore {
|
|
||||||
if log == nil {
|
|
||||||
log = &defaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CertStore{
|
|
||||||
certificates: make(map[string]*tls.Certificate),
|
|
||||||
certLock: &sync.Mutex{},
|
|
||||||
|
|
||||||
storePath: storePath,
|
|
||||||
log: log,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CertStore) ensureStorePath() error {
|
|
||||||
// check if directory exists
|
|
||||||
stat, err := os.Stat(s.storePath)
|
|
||||||
if err == nil {
|
|
||||||
if stat.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("TLS store path exists but is not a directory: %s", s.storePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
s.log.Trace().Str("path", s.storePath).Msg("TLS store directory does not exist, creating directory")
|
|
||||||
err = os.MkdirAll(s.storePath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create TLS store path: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to check TLS store path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CertStore) LoadCertificates() {
|
|
||||||
err := s.ensureStorePath()
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to ensure store path")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := os.ReadDir(s.storePath)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to read TLS directory")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasSuffix(file.Name(), ".crt") {
|
|
||||||
s.loadCertificate(strings.TrimSuffix(file.Name(), ".crt"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CertStore) loadCertificate(hostname string) {
|
|
||||||
s.certLock.Lock()
|
|
||||||
defer s.certLock.Unlock()
|
|
||||||
|
|
||||||
keyFile := path.Join(s.storePath, hostname+".key")
|
|
||||||
crtFile := path.Join(s.storePath, hostname+".crt")
|
|
||||||
|
|
||||||
cert, err := tls.LoadX509KeyPair(crtFile, keyFile)
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Str("hostname", hostname).Msg("Failed to load certificate")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.certificates[hostname] = &cert
|
|
||||||
|
|
||||||
if hostname == selfSignerCAMagicName {
|
|
||||||
s.log.Info().Msg("loaded CA certificate")
|
|
||||||
} else {
|
|
||||||
s.log.Info().Str("hostname", hostname).Msg("loaded certificate")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCertificate returns the certificate for the given hostname
|
|
||||||
// returns nil if the certificate is not found
|
|
||||||
func (s *CertStore) GetCertificate(hostname string) *tls.Certificate {
|
|
||||||
s.certLock.Lock()
|
|
||||||
defer s.certLock.Unlock()
|
|
||||||
|
|
||||||
return s.certificates[hostname]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAndSaveCertificate validates the certificate and saves it to the store
|
|
||||||
// returns are:
|
|
||||||
// - error: if the certificate is invalid or if there's any error during saving the certificate
|
|
||||||
// - error: if there's any warning or error during saving the certificate
|
|
||||||
func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key string, ignoreWarning bool) (error, error) {
|
|
||||||
tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(key))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to parse certificate: %w", err), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// this can be skipped as current implementation supports one custom certificate only
|
|
||||||
if tlsCert.Leaf != nil {
|
|
||||||
// add recover to avoid panic
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
s.log.Error().Interface("recovered", r).Msg("Failed to verify hostname")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = tlsCert.Leaf.VerifyHostname(hostname); err != nil {
|
|
||||||
if !ignoreWarning {
|
|
||||||
return nil, fmt.Errorf("certificate does not match hostname: %w", err)
|
|
||||||
}
|
|
||||||
s.log.Warn().Err(err).Msg("certificate does not match hostname")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.certLock.Lock()
|
|
||||||
s.certificates[hostname] = &tlsCert
|
|
||||||
s.certLock.Unlock()
|
|
||||||
|
|
||||||
s.saveCertificate(hostname)
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CertStore) saveCertificate(hostname string) {
|
|
||||||
// check if certificate already exists
|
|
||||||
tlsCert := s.certificates[hostname]
|
|
||||||
if tlsCert == nil {
|
|
||||||
s.log.Error().Str("hostname", hostname).Msg("Certificate for hostname does not exist, skipping saving certificate")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := s.ensureStorePath()
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to ensure store path")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
keyFile := path.Join(s.storePath, hostname+".key")
|
|
||||||
crtFile := path.Join(s.storePath, hostname+".crt")
|
|
||||||
|
|
||||||
if err := keyToFile(tlsCert, keyFile); err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to save key file")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := certToFile(tlsCert, crtFile); err != nil {
|
|
||||||
s.log.Error().Err(err).Msg("Failed to save certificate")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.log.Info().Str("hostname", hostname).Msg("Saved certificate")
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
package websecure
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var serialNumberLimit = new(big.Int).Lsh(big.NewInt(1), 4096)
|
|
||||||
|
|
||||||
func withSecretFile(filename string, f func(*os.File) error) error {
|
|
||||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
return f(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyToFile(cert *tls.Certificate, filename string) error {
|
|
||||||
var keyBlock pem.Block
|
|
||||||
switch k := cert.PrivateKey.(type) {
|
|
||||||
case *rsa.PrivateKey:
|
|
||||||
keyBlock = pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(k),
|
|
||||||
}
|
|
||||||
case *ecdsa.PrivateKey:
|
|
||||||
b, e := x509.MarshalECPrivateKey(k)
|
|
||||||
if e != nil {
|
|
||||||
return fmt.Errorf("failed to marshal EC private key: %v", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
keyBlock = pem.Block{
|
|
||||||
Type: "EC PRIVATE KEY",
|
|
||||||
Bytes: b,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown private key type: %T", k)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := withSecretFile(filename, func(file *os.File) error {
|
|
||||||
return pem.Encode(file, &keyBlock)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func certToFile(cert *tls.Certificate, filename string) error {
|
|
||||||
return withSecretFile(filename, func(file *os.File) error {
|
|
||||||
for _, c := range cert.Certificate {
|
|
||||||
block := pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Bytes: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := pem.Encode(file, &block)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to save certificate: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSerialNumber() (*big.Int, error) {
|
|
||||||
return rand.Int(rand.Reader, serialNumberLimit)
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,9 @@ func rpcGetJigglerState() bool {
|
||||||
return jigglerEnabled
|
return jigglerEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func initJiggler() {
|
func init() {
|
||||||
|
ensureConfigLoaded()
|
||||||
|
|
||||||
go runJiggler()
|
go runJiggler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,11 +28,11 @@ func runJiggler() {
|
||||||
//TODO: change to rel mouse
|
//TODO: change to rel mouse
|
||||||
err := rpcAbsMouseReport(1, 1, 0)
|
err := rpcAbsMouseReport(1, 1, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("Failed to jiggle mouse")
|
logger.Warnf("Failed to jiggle mouse: %v", err)
|
||||||
}
|
}
|
||||||
err = rpcAbsMouseReport(0, 0, 0)
|
err = rpcAbsMouseReport(0, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("Failed to reset mouse position")
|
logger.Warnf("Failed to reset mouse position: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
314
jsonrpc.go
314
jsonrpc.go
|
|
@ -38,10 +38,6 @@ type JSONRPCEvent struct {
|
||||||
Params interface{} `json:"params,omitempty"`
|
Params interface{} `json:"params,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DisplayRotationSettings struct {
|
|
||||||
Rotation string `json:"rotation"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BacklightSettings struct {
|
type BacklightSettings struct {
|
||||||
MaxBrightness int `json:"max_brightness"`
|
MaxBrightness int `json:"max_brightness"`
|
||||||
DimAfter int `json:"dim_after"`
|
DimAfter int `json:"dim_after"`
|
||||||
|
|
@ -51,12 +47,12 @@ type BacklightSettings struct {
|
||||||
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
func writeJSONRPCResponse(response JSONRPCResponse, session *Session) {
|
||||||
responseBytes, err := json.Marshal(response)
|
responseBytes, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonRpcLogger.Warn().Err(err).Msg("Error marshalling JSONRPC response")
|
logger.Warnf("Error marshalling JSONRPC response: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = session.RPCChannel.SendText(string(responseBytes))
|
err = session.RPCChannel.SendText(string(responseBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonRpcLogger.Warn().Err(err).Msg("Error sending JSONRPC response")
|
logger.Warnf("Error sending JSONRPC response: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,24 +65,16 @@ func writeJSONRPCEvent(event string, params interface{}, session *Session) {
|
||||||
}
|
}
|
||||||
requestBytes, err := json.Marshal(request)
|
requestBytes, err := json.Marshal(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonRpcLogger.Warn().Err(err).Msg("Error marshalling JSONRPC event")
|
logger.Warnf("Error marshalling JSONRPC event: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if session == nil || session.RPCChannel == nil {
|
if session == nil || session.RPCChannel == nil {
|
||||||
jsonRpcLogger.Info().Msg("RPC channel not available")
|
logger.Info("RPC channel not available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err = session.RPCChannel.SendText(string(requestBytes))
|
||||||
requestString := string(requestBytes)
|
|
||||||
scopedLogger := jsonRpcLogger.With().
|
|
||||||
Str("data", requestString).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("sending JSONRPC event")
|
|
||||||
|
|
||||||
err = session.RPCChannel.SendText(requestString)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("error sending JSONRPC event")
|
logger.Warnf("Error sending JSONRPC event: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,11 +83,6 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
var request JSONRPCRequest
|
var request JSONRPCRequest
|
||||||
err := json.Unmarshal(message.Data, &request)
|
err := json.Unmarshal(message.Data, &request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonRpcLogger.Warn().
|
|
||||||
Str("data", string(message.Data)).
|
|
||||||
Err(err).
|
|
||||||
Msg("Error unmarshalling JSONRPC request")
|
|
||||||
|
|
||||||
errorResponse := JSONRPCResponse{
|
errorResponse := JSONRPCResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
Error: map[string]interface{}{
|
Error: map[string]interface{}{
|
||||||
|
|
@ -112,13 +95,7 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger := jsonRpcLogger.With().
|
//logger.Infof("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID)
|
||||||
Str("method", request.Method).
|
|
||||||
Interface("params", request.Params).
|
|
||||||
Interface("id", request.ID).Logger()
|
|
||||||
|
|
||||||
scopedLogger.Trace().Msg("Received RPC request")
|
|
||||||
|
|
||||||
handler, ok := rpcHandlers[request.Method]
|
handler, ok := rpcHandlers[request.Method]
|
||||||
if !ok {
|
if !ok {
|
||||||
errorResponse := JSONRPCResponse{
|
errorResponse := JSONRPCResponse{
|
||||||
|
|
@ -133,10 +110,8 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger.Trace().Msg("Calling RPC handler")
|
|
||||||
result, err := callRPCHandler(handler, request.Params)
|
result, err := callRPCHandler(handler, request.Params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Error().Err(err).Msg("Error calling RPC handler")
|
|
||||||
errorResponse := JSONRPCResponse{
|
errorResponse := JSONRPCResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
Error: map[string]interface{}{
|
Error: map[string]interface{}{
|
||||||
|
|
@ -150,8 +125,6 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger.Trace().Interface("result", result).Msg("RPC handler returned")
|
|
||||||
|
|
||||||
response := JSONRPCResponse{
|
response := JSONRPCResponse{
|
||||||
JSONRPC: "2.0",
|
JSONRPC: "2.0",
|
||||||
Result: result,
|
Result: result,
|
||||||
|
|
@ -168,30 +141,6 @@ func rpcGetDeviceID() (string, error) {
|
||||||
return GetDeviceID(), nil
|
return GetDeviceID(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcReboot(force bool) error {
|
|
||||||
logger.Info().Msg("Got reboot request from JSONRPC, rebooting...")
|
|
||||||
|
|
||||||
args := []string{}
|
|
||||||
if force {
|
|
||||||
args = append(args, "-f")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("reboot", args...)
|
|
||||||
err := cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Err(err).Msg("failed to reboot")
|
|
||||||
return fmt.Errorf("failed to reboot: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the reboot command is successful, exit the program after 5 seconds
|
|
||||||
go func() {
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
os.Exit(0)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var streamFactor = 1.0
|
var streamFactor = 1.0
|
||||||
|
|
||||||
func rpcGetStreamQualityFactor() (float64, error) {
|
func rpcGetStreamQualityFactor() (float64, error) {
|
||||||
|
|
@ -199,7 +148,7 @@ func rpcGetStreamQualityFactor() (float64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetStreamQualityFactor(factor float64) error {
|
func rpcSetStreamQualityFactor(factor float64) error {
|
||||||
logger.Info().Float64("factor", factor).Msg("Setting stream quality factor")
|
logger.Infof("Setting stream quality factor to: %f", factor)
|
||||||
var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor})
|
var _, err = CallCtrlAction("set_video_quality_factor", map[string]interface{}{"quality_factor": factor})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -235,10 +184,10 @@ func rpcGetEDID() (string, error) {
|
||||||
|
|
||||||
func rpcSetEDID(edid string) error {
|
func rpcSetEDID(edid string) error {
|
||||||
if edid == "" {
|
if edid == "" {
|
||||||
logger.Info().Msg("Restoring EDID to default")
|
logger.Info("Restoring EDID to default")
|
||||||
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
|
edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
|
||||||
} else {
|
} else {
|
||||||
logger.Info().Str("edid", edid).Msg("Setting EDID")
|
logger.Infof("Setting EDID to: %s", edid)
|
||||||
}
|
}
|
||||||
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid})
|
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": edid})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -266,13 +215,8 @@ func rpcSetDevChannelState(enabled bool) error {
|
||||||
func rpcGetUpdateStatus() (*UpdateStatus, error) {
|
func rpcGetUpdateStatus() (*UpdateStatus, error) {
|
||||||
includePreRelease := config.IncludePreRelease
|
includePreRelease := config.IncludePreRelease
|
||||||
updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease)
|
updateStatus, err := GetUpdateStatus(context.Background(), GetDeviceID(), includePreRelease)
|
||||||
// to ensure backwards compatibility,
|
|
||||||
// if there's an error, we won't return an error, but we will set the error field
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if updateStatus == nil {
|
return nil, fmt.Errorf("error checking for updates: %w", err)
|
||||||
return nil, fmt.Errorf("error checking for updates: %w", err)
|
|
||||||
}
|
|
||||||
updateStatus.Error = err.Error()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateStatus, nil
|
return updateStatus, nil
|
||||||
|
|
@ -283,30 +227,12 @@ func rpcTryUpdate() error {
|
||||||
go func() {
|
go func() {
|
||||||
err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
err := TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to try update")
|
logger.Warnf("failed to try update: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetDisplayRotation(params DisplayRotationSettings) error {
|
|
||||||
var err error
|
|
||||||
_, err = lvDispSetRotation(params.Rotation)
|
|
||||||
if err == nil {
|
|
||||||
config.DisplayRotation = params.Rotation
|
|
||||||
if err := SaveConfig(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcGetDisplayRotation() (*DisplayRotationSettings, error) {
|
|
||||||
return &DisplayRotationSettings{
|
|
||||||
Rotation: config.DisplayRotation,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcSetBacklightSettings(params BacklightSettings) error {
|
func rpcSetBacklightSettings(params BacklightSettings) error {
|
||||||
blConfig := params
|
blConfig := params
|
||||||
|
|
||||||
|
|
@ -331,7 +257,7 @@ func rpcSetBacklightSettings(params BacklightSettings) error {
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Int("max_brightness", config.DisplayMaxBrightness).Int("dim_after", config.DisplayDimAfterSec).Int("off_after", config.DisplayOffAfterSec).Msg("rpc: display: settings applied")
|
logger.Infof("rpc: display: settings applied, max_brightness: %d, dim after: %ds, off after: %ds", config.DisplayMaxBrightness, config.DisplayDimAfterSec, config.DisplayOffAfterSec)
|
||||||
|
|
||||||
// If the device started up with auto-dim and/or auto-off set to zero, the display init
|
// If the device started up with auto-dim and/or auto-off set to zero, the display init
|
||||||
// method will not have started the tickers. So in case that has changed, attempt to start the tickers now.
|
// method will not have started the tickers. So in case that has changed, attempt to start the tickers now.
|
||||||
|
|
@ -392,7 +318,7 @@ func rpcSetDevModeState(enabled bool) error {
|
||||||
return fmt.Errorf("failed to create devmode file: %w", err)
|
return fmt.Errorf("failed to create devmode file: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debug().Msg("dev mode already enabled")
|
logger.Debug("dev mode already enabled")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -401,7 +327,7 @@ func rpcSetDevModeState(enabled bool) error {
|
||||||
return fmt.Errorf("failed to remove devmode file: %w", err)
|
return fmt.Errorf("failed to remove devmode file: %w", err)
|
||||||
}
|
}
|
||||||
} else if os.IsNotExist(err) {
|
} else if os.IsNotExist(err) {
|
||||||
logger.Debug().Msg("dev mode already disabled")
|
logger.Debug("dev mode already disabled")
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("error checking dev mode file: %w", err)
|
return fmt.Errorf("error checking dev mode file: %w", err)
|
||||||
|
|
@ -411,7 +337,7 @@ func rpcSetDevModeState(enabled bool) error {
|
||||||
cmd := exec.Command("dropbear.sh")
|
cmd := exec.Command("dropbear.sh")
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Bytes("output", output).Msg("Failed to start/stop SSH")
|
logger.Warnf("Failed to start/stop SSH: %v, %v", err, output)
|
||||||
return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect")
|
return fmt.Errorf("failed to start/stop SSH, you may need to reboot for changes to take effect")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,48 +375,7 @@ func rpcSetSSHKeyState(sshKey string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetTLSState() TLSState {
|
func callRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) {
|
||||||
return getTLSState()
|
|
||||||
}
|
|
||||||
|
|
||||||
func rpcSetTLSState(state TLSState) error {
|
|
||||||
err := setTLSState(state)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to set TLS state: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := SaveConfig(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type RPCHandler struct {
|
|
||||||
Func interface{}
|
|
||||||
Params []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// call the handler but recover from a panic to ensure our RPC thread doesn't collapse on malformed calls
|
|
||||||
func callRPCHandler(handler RPCHandler, params map[string]interface{}) (result interface{}, err error) {
|
|
||||||
// Use defer to recover from a panic
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
// Convert the panic to an error
|
|
||||||
if e, ok := r.(error); ok {
|
|
||||||
err = e
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf("panic occurred: %v", r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Call the handler
|
|
||||||
result, err = riskyCallRPCHandler(handler, params)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (interface{}, error) {
|
|
||||||
handlerValue := reflect.ValueOf(handler.Func)
|
handlerValue := reflect.ValueOf(handler.Func)
|
||||||
handlerType := handlerValue.Type()
|
handlerType := handlerValue.Type()
|
||||||
|
|
||||||
|
|
@ -587,34 +472,36 @@ func riskyCallRPCHandler(handler RPCHandler, params map[string]interface{}) (int
|
||||||
return nil, errors.New("unexpected return values from handler")
|
return nil, errors.New("unexpected return values from handler")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RPCHandler struct {
|
||||||
|
Func interface{}
|
||||||
|
Params []string
|
||||||
|
}
|
||||||
|
|
||||||
func rpcSetMassStorageMode(mode string) (string, error) {
|
func rpcSetMassStorageMode(mode string) (string, error) {
|
||||||
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode)
|
||||||
var cdrom bool
|
var cdrom bool
|
||||||
switch mode {
|
if mode == "cdrom" {
|
||||||
case "cdrom":
|
|
||||||
cdrom = true
|
cdrom = true
|
||||||
case "file":
|
} else if mode != "file" {
|
||||||
cdrom = false
|
logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Invalid mode provided: %s", mode)
|
||||||
default:
|
|
||||||
logger.Info().Str("mode", mode).Msg("Invalid mode provided")
|
|
||||||
return "", fmt.Errorf("invalid mode: %s", mode)
|
return "", fmt.Errorf("invalid mode: %s", mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Str("mode", mode).Msg("Setting mass storage mode")
|
logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Setting mass storage mode to: %s", mode)
|
||||||
|
|
||||||
err := setMassStorageMode(cdrom)
|
err := setMassStorageMode(cdrom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to set mass storage mode: %w", err)
|
return "", fmt.Errorf("failed to set mass storage mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Str("mode", mode).Msg("Mass storage mode set")
|
logger.Infof("[jsonrpc.go:rpcSetMassStorageMode] Mass storage mode set to %s", mode)
|
||||||
|
|
||||||
// Get the updated mode after setting
|
// Get the updated mode after setting
|
||||||
return rpcGetMassStorageMode()
|
return rpcGetMassStorageMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetMassStorageMode() (string, error) {
|
func rpcGetMassStorageMode() (string, error) {
|
||||||
cdrom, err := getMassStorageCDROMEnabled()
|
cdrom, err := getMassStorageMode()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get mass storage mode: %w", err)
|
return "", fmt.Errorf("failed to get mass storage mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -676,7 +563,7 @@ func rpcResetConfig() error {
|
||||||
return fmt.Errorf("failed to reset config: %w", err)
|
return fmt.Errorf("failed to reset config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info().Msg("Configuration reset to default")
|
logger.Info("Configuration reset to default")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -692,7 +579,7 @@ func rpcGetDCPowerState() (DCPowerState, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetDCPowerState(enabled bool) error {
|
func rpcSetDCPowerState(enabled bool) error {
|
||||||
logger.Info().Bool("enabled", enabled).Msg("Setting DC power state")
|
logger.Infof("[jsonrpc.go:rpcSetDCPowerState] Setting DC power state to: %v", enabled)
|
||||||
err := setDCPowerState(enabled)
|
err := setDCPowerState(enabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to set DC power state: %w", err)
|
return fmt.Errorf("failed to set DC power state: %w", err)
|
||||||
|
|
@ -708,36 +595,34 @@ func rpcSetActiveExtension(extensionId string) error {
|
||||||
if config.ActiveExtension == extensionId {
|
if config.ActiveExtension == extensionId {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch config.ActiveExtension {
|
if config.ActiveExtension == "atx-power" {
|
||||||
case "atx-power":
|
|
||||||
_ = unmountATXControl()
|
_ = unmountATXControl()
|
||||||
case "dc-power":
|
} else if config.ActiveExtension == "dc-power" {
|
||||||
_ = unmountDCControl()
|
_ = unmountDCControl()
|
||||||
}
|
}
|
||||||
config.ActiveExtension = extensionId
|
config.ActiveExtension = extensionId
|
||||||
if err := SaveConfig(); err != nil {
|
if err := SaveConfig(); err != nil {
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
}
|
}
|
||||||
switch extensionId {
|
if extensionId == "atx-power" {
|
||||||
case "atx-power":
|
|
||||||
_ = mountATXControl()
|
_ = mountATXControl()
|
||||||
case "dc-power":
|
} else if extensionId == "dc-power" {
|
||||||
_ = mountDCControl()
|
_ = mountDCControl()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetATXPowerAction(action string) error {
|
func rpcSetATXPowerAction(action string) error {
|
||||||
logger.Debug().Str("action", action).Msg("Executing ATX power action")
|
logger.Debugf("[jsonrpc.go:rpcSetATXPowerAction] Executing ATX power action: %s", action)
|
||||||
switch action {
|
switch action {
|
||||||
case "power-short":
|
case "power-short":
|
||||||
logger.Debug().Msg("Simulating short power button press")
|
logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating short power button press")
|
||||||
return pressATXPowerButton(200 * time.Millisecond)
|
return pressATXPowerButton(200 * time.Millisecond)
|
||||||
case "power-long":
|
case "power-long":
|
||||||
logger.Debug().Msg("Simulating long power button press")
|
logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating long power button press")
|
||||||
return pressATXPowerButton(5 * time.Second)
|
return pressATXPowerButton(5 * time.Second)
|
||||||
case "reset":
|
case "reset":
|
||||||
logger.Debug().Msg("Simulating reset button press")
|
logger.Debug("[jsonrpc.go:rpcSetATXPowerAction] Simulating reset button press")
|
||||||
return pressATXResetButton(200 * time.Millisecond)
|
return pressATXResetButton(200 * time.Millisecond)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid action: %s", action)
|
return fmt.Errorf("invalid action: %s", action)
|
||||||
|
|
@ -901,121 +786,22 @@ func rpcSetCloudUrl(apiUrl string, appUrl string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetKeyboardLayout() (string, error) {
|
var currentScrollSensitivity string = "default"
|
||||||
return config.KeyboardLayout, nil
|
|
||||||
|
func rpcGetScrollSensitivity() (string, error) {
|
||||||
|
return currentScrollSensitivity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcSetKeyboardLayout(layout string) error {
|
func rpcSetScrollSensitivity(sensitivity string) error {
|
||||||
config.KeyboardLayout = layout
|
currentScrollSensitivity = sensitivity
|
||||||
if err := SaveConfig(); err != nil {
|
|
||||||
return fmt.Errorf("failed to save config: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getKeyboardMacros() (interface{}, error) {
|
|
||||||
macros := make([]KeyboardMacro, len(config.KeyboardMacros))
|
|
||||||
copy(macros, config.KeyboardMacros)
|
|
||||||
|
|
||||||
return macros, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type KeyboardMacrosParams struct {
|
|
||||||
Macros []interface{} `json:"macros"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) {
|
|
||||||
if params.Macros == nil {
|
|
||||||
return nil, fmt.Errorf("missing or invalid macros parameter")
|
|
||||||
}
|
|
||||||
|
|
||||||
newMacros := make([]KeyboardMacro, 0, len(params.Macros))
|
|
||||||
|
|
||||||
for i, item := range params.Macros {
|
|
||||||
macroMap, ok := item.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("invalid macro at index %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
id, _ := macroMap["id"].(string)
|
|
||||||
if id == "" {
|
|
||||||
id = fmt.Sprintf("macro-%d", time.Now().UnixNano())
|
|
||||||
}
|
|
||||||
|
|
||||||
name, _ := macroMap["name"].(string)
|
|
||||||
|
|
||||||
sortOrder := i + 1
|
|
||||||
if sortOrderFloat, ok := macroMap["sortOrder"].(float64); ok {
|
|
||||||
sortOrder = int(sortOrderFloat)
|
|
||||||
}
|
|
||||||
|
|
||||||
steps := []KeyboardMacroStep{}
|
|
||||||
if stepsArray, ok := macroMap["steps"].([]interface{}); ok {
|
|
||||||
for _, stepItem := range stepsArray {
|
|
||||||
stepMap, ok := stepItem.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
step := KeyboardMacroStep{}
|
|
||||||
|
|
||||||
if keysArray, ok := stepMap["keys"].([]interface{}); ok {
|
|
||||||
for _, k := range keysArray {
|
|
||||||
if keyStr, ok := k.(string); ok {
|
|
||||||
step.Keys = append(step.Keys, keyStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if modsArray, ok := stepMap["modifiers"].([]interface{}); ok {
|
|
||||||
for _, m := range modsArray {
|
|
||||||
if modStr, ok := m.(string); ok {
|
|
||||||
step.Modifiers = append(step.Modifiers, modStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if delay, ok := stepMap["delay"].(float64); ok {
|
|
||||||
step.Delay = int(delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
steps = append(steps, step)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro := KeyboardMacro{
|
|
||||||
ID: id,
|
|
||||||
Name: name,
|
|
||||||
Steps: steps,
|
|
||||||
SortOrder: sortOrder,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := macro.Validate(); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid macro at index %d: %w", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newMacros = append(newMacros, macro)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.KeyboardMacros = newMacros
|
|
||||||
|
|
||||||
if err := SaveConfig(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var rpcHandlers = map[string]RPCHandler{
|
var rpcHandlers = map[string]RPCHandler{
|
||||||
"ping": {Func: rpcPing},
|
"ping": {Func: rpcPing},
|
||||||
"reboot": {Func: rpcReboot, Params: []string{"force"}},
|
|
||||||
"getDeviceID": {Func: rpcGetDeviceID},
|
"getDeviceID": {Func: rpcGetDeviceID},
|
||||||
"deregisterDevice": {Func: rpcDeregisterDevice},
|
"deregisterDevice": {Func: rpcDeregisterDevice},
|
||||||
"getCloudState": {Func: rpcGetCloudState},
|
"getCloudState": {Func: rpcGetCloudState},
|
||||||
"getNetworkState": {Func: rpcGetNetworkState},
|
|
||||||
"getNetworkSettings": {Func: rpcGetNetworkSettings},
|
|
||||||
"setNetworkSettings": {Func: rpcSetNetworkSettings, Params: []string{"settings"}},
|
|
||||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
|
||||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||||
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
"relMouseReport": {Func: rpcRelMouseReport, Params: []string{"dx", "dy", "buttons"}},
|
||||||
|
|
@ -1041,8 +827,6 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},
|
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},
|
||||||
"getSSHKeyState": {Func: rpcGetSSHKeyState},
|
"getSSHKeyState": {Func: rpcGetSSHKeyState},
|
||||||
"setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
|
"setSSHKeyState": {Func: rpcSetSSHKeyState, Params: []string{"sshKey"}},
|
||||||
"getTLSState": {Func: rpcGetTLSState},
|
|
||||||
"setTLSState": {Func: rpcSetTLSState, Params: []string{"state"}},
|
|
||||||
"setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
|
"setMassStorageMode": {Func: rpcSetMassStorageMode, Params: []string{"mode"}},
|
||||||
"getMassStorageMode": {Func: rpcGetMassStorageMode},
|
"getMassStorageMode": {Func: rpcGetMassStorageMode},
|
||||||
"isUpdatePending": {Func: rpcIsUpdatePending},
|
"isUpdatePending": {Func: rpcIsUpdatePending},
|
||||||
|
|
@ -1062,8 +846,6 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
|
"getWakeOnLanDevices": {Func: rpcGetWakeOnLanDevices},
|
||||||
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
|
"setWakeOnLanDevices": {Func: rpcSetWakeOnLanDevices, Params: []string{"params"}},
|
||||||
"resetConfig": {Func: rpcResetConfig},
|
"resetConfig": {Func: rpcResetConfig},
|
||||||
"setDisplayRotation": {Func: rpcSetDisplayRotation, Params: []string{"params"}},
|
|
||||||
"getDisplayRotation": {Func: rpcGetDisplayRotation},
|
|
||||||
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
"setBacklightSettings": {Func: rpcSetBacklightSettings, Params: []string{"params"}},
|
||||||
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
"getBacklightSettings": {Func: rpcGetBacklightSettings},
|
||||||
"getDCPowerState": {Func: rpcGetDCPowerState},
|
"getDCPowerState": {Func: rpcGetDCPowerState},
|
||||||
|
|
@ -1078,8 +860,6 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
"setUsbDevices": {Func: rpcSetUsbDevices, Params: []string{"devices"}},
|
||||||
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
"setUsbDeviceState": {Func: rpcSetUsbDeviceState, Params: []string{"device", "enabled"}},
|
||||||
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
"setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}},
|
||||||
"getKeyboardLayout": {Func: rpcGetKeyboardLayout},
|
"getScrollSensitivity": {Func: rpcGetScrollSensitivity},
|
||||||
"setKeyboardLayout": {Func: rpcSetKeyboardLayout, Params: []string{"layout"}},
|
"setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}},
|
||||||
"getKeyboardMacros": {Func: getKeyboardMacros},
|
|
||||||
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
log.go
35
log.go
|
|
@ -1,32 +1,9 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import "github.com/pion/logging"
|
||||||
"github.com/jetkvm/kvm/internal/logging"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ErrorfL(l *zerolog.Logger, format string, err error, args ...interface{}) error {
|
// we use logging framework from pion
|
||||||
return logging.ErrorfL(l, format, err, args...)
|
// ref: https://github.com/pion/webrtc/wiki/Debugging-WebRTC
|
||||||
}
|
var logger = logging.NewDefaultLoggerFactory().NewLogger("jetkvm")
|
||||||
|
var cloudLogger = logging.NewDefaultLoggerFactory().NewLogger("cloud")
|
||||||
var (
|
var websocketLogger = logging.NewDefaultLoggerFactory().NewLogger("websocket")
|
||||||
logger = logging.GetSubsystemLogger("jetkvm")
|
|
||||||
networkLogger = logging.GetSubsystemLogger("network")
|
|
||||||
cloudLogger = logging.GetSubsystemLogger("cloud")
|
|
||||||
websocketLogger = logging.GetSubsystemLogger("websocket")
|
|
||||||
webrtcLogger = logging.GetSubsystemLogger("webrtc")
|
|
||||||
nativeLogger = logging.GetSubsystemLogger("native")
|
|
||||||
nbdLogger = logging.GetSubsystemLogger("nbd")
|
|
||||||
timesyncLogger = logging.GetSubsystemLogger("timesync")
|
|
||||||
jsonRpcLogger = logging.GetSubsystemLogger("jsonrpc")
|
|
||||||
watchdogLogger = logging.GetSubsystemLogger("watchdog")
|
|
||||||
websecureLogger = logging.GetSubsystemLogger("websecure")
|
|
||||||
otaLogger = logging.GetSubsystemLogger("ota")
|
|
||||||
serialLogger = logging.GetSubsystemLogger("serial")
|
|
||||||
terminalLogger = logging.GetSubsystemLogger("terminal")
|
|
||||||
displayLogger = logging.GetSubsystemLogger("display")
|
|
||||||
wolLogger = logging.GetSubsystemLogger("wol")
|
|
||||||
usbLogger = logging.GetSubsystemLogger("usb")
|
|
||||||
// external components
|
|
||||||
ginLogger = logging.GetSubsystemLogger("gin")
|
|
||||||
)
|
|
||||||
|
|
|
||||||
70
main.go
70
main.go
|
|
@ -14,55 +14,25 @@ import (
|
||||||
var appCtx context.Context
|
var appCtx context.Context
|
||||||
|
|
||||||
func Main() {
|
func Main() {
|
||||||
LoadConfig()
|
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
appCtx, cancel = context.WithCancel(context.Background())
|
appCtx, cancel = context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
logger.Info("Starting JetKvm")
|
||||||
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to get local version")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info().
|
|
||||||
Interface("system_version", systemVersionLocal).
|
|
||||||
Interface("app_version", appVersionLocal).
|
|
||||||
Msg("starting JetKVM")
|
|
||||||
|
|
||||||
go runWatchdog()
|
go runWatchdog()
|
||||||
go confirmCurrentSystem()
|
go confirmCurrentSystem()
|
||||||
|
|
||||||
http.DefaultClient.Timeout = 1 * time.Minute
|
http.DefaultClient.Timeout = 1 * time.Minute
|
||||||
|
LoadConfig()
|
||||||
|
logger.Debug("config loaded")
|
||||||
|
|
||||||
err = rootcerts.UpdateDefaultTransport()
|
err := rootcerts.UpdateDefaultTransport()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to load Root CA certificates")
|
logger.Errorf("failed to load CA certs: %v", err)
|
||||||
}
|
|
||||||
logger.Info().
|
|
||||||
Int("ca_certs_loaded", len(rootcerts.Certs())).
|
|
||||||
Msg("loaded Root CA certificates")
|
|
||||||
|
|
||||||
// Initialize network
|
|
||||||
if err := initNetwork(); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("failed to initialize network")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize time sync
|
go TimeSyncLoop()
|
||||||
initTimeSync()
|
|
||||||
timeSync.Start()
|
|
||||||
|
|
||||||
// Initialize mDNS
|
|
||||||
if err := initMdns(); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("failed to initialize mDNS")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize native ctrl socket server
|
|
||||||
StartNativeCtrlSocketServer()
|
StartNativeCtrlSocketServer()
|
||||||
|
|
||||||
// Initialize native video socket server
|
|
||||||
StartNativeVideoSocketServer()
|
StartNativeVideoSocketServer()
|
||||||
|
|
||||||
initPrometheus()
|
initPrometheus()
|
||||||
|
|
@ -70,54 +40,38 @@ func Main() {
|
||||||
go func() {
|
go func() {
|
||||||
err = ExtractAndRunNativeBin()
|
err = ExtractAndRunNativeBin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to extract and run native bin")
|
logger.Errorf("failed to extract and run native bin: %v", err)
|
||||||
//TODO: prepare an error message screen buffer to show on kvm screen
|
//TODO: prepare an error message screen buffer to show on kvm screen
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// initialize usb gadget
|
|
||||||
initUsbGadget()
|
initUsbGadget()
|
||||||
if err := setInitialVirtualMediaState(); err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := initImagesFolder(); err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to init images folder")
|
|
||||||
}
|
|
||||||
initJiggler()
|
|
||||||
|
|
||||||
// initialize display
|
|
||||||
initDisplay()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(15 * time.Minute)
|
time.Sleep(15 * time.Minute)
|
||||||
for {
|
for {
|
||||||
logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
|
logger.Debugf("UPDATING - Auto update enabled: %v", config.AutoUpdateEnabled)
|
||||||
if !config.AutoUpdateEnabled {
|
if !config.AutoUpdateEnabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if currentSession != nil {
|
if currentSession != nil {
|
||||||
logger.Debug().Msg("skipping update since a session is active")
|
logger.Debugf("skipping update since a session is active")
|
||||||
time.Sleep(1 * time.Minute)
|
time.Sleep(1 * time.Minute)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
includePreRelease := config.IncludePreRelease
|
includePreRelease := config.IncludePreRelease
|
||||||
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Err(err).Msg("failed to auto update")
|
logger.Errorf("failed to auto update: %v", err)
|
||||||
}
|
}
|
||||||
time.Sleep(1 * time.Hour)
|
time.Sleep(1 * time.Hour)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
//go RunFuseServer()
|
//go RunFuseServer()
|
||||||
go RunWebServer()
|
go RunWebServer()
|
||||||
|
|
||||||
go RunWebSecureServer()
|
|
||||||
// Web secure server is started only if TLS mode is enabled
|
|
||||||
if config.TLSMode != "" {
|
if config.TLSMode != "" {
|
||||||
startWebSecureServer()
|
go RunWebSecureServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
// As websocket client already checks if the cloud token is set, we can start it here.
|
// As websocket client already checks if the cloud token is set, we can start it here.
|
||||||
go RunWebsocketClient()
|
go RunWebsocketClient()
|
||||||
|
|
||||||
|
|
@ -125,7 +79,7 @@ func Main() {
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigs
|
<-sigs
|
||||||
logger.Info().Msg("JetKVM Shutting Down")
|
logger.Info("JetKVM Shutting Down")
|
||||||
//if fuseServer != nil {
|
//if fuseServer != nil {
|
||||||
// err := setMassStorageImage(" ")
|
// err := setMassStorageImage(" ")
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
|
|
|
||||||
29
mdns.go
29
mdns.go
|
|
@ -1,29 +0,0 @@
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jetkvm/kvm/internal/mdns"
|
|
||||||
)
|
|
||||||
|
|
||||||
var mDNS *mdns.MDNS
|
|
||||||
|
|
||||||
func initMdns() error {
|
|
||||||
m, err := mdns.NewMDNS(&mdns.MDNSOptions{
|
|
||||||
Logger: logger,
|
|
||||||
LocalNames: []string{
|
|
||||||
networkState.GetHostname(),
|
|
||||||
networkState.GetFQDN(),
|
|
||||||
},
|
|
||||||
ListenOptions: &mdns.MDNSListenOptions{
|
|
||||||
IPv4: true,
|
|
||||||
IPv6: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// do not start the server yet, as we need to wait for the network state to be set
|
|
||||||
mDNS = m
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
106
native.go
106
native.go
|
|
@ -3,12 +3,13 @@ package kvm
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/resource"
|
"github.com/jetkvm/kvm/resource"
|
||||||
|
|
@ -60,33 +61,25 @@ func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse
|
||||||
return nil, fmt.Errorf("error marshaling ctrl action: %w", err)
|
return nil, fmt.Errorf("error marshaling ctrl action: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger := nativeLogger.With().
|
logger.Infof("sending ctrl action: %s", string(jsonData))
|
||||||
Str("action", ctrlAction.Action).
|
|
||||||
Interface("params", ctrlAction.Params).Logger()
|
|
||||||
|
|
||||||
scopedLogger.Debug().Msg("sending ctrl action")
|
|
||||||
|
|
||||||
err = WriteCtrlMessage(jsonData)
|
err = WriteCtrlMessage(jsonData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
delete(ongoingRequests, ctrlAction.Seq)
|
delete(ongoingRequests, ctrlAction.Seq)
|
||||||
return nil, ErrorfL(&scopedLogger, "error writing ctrl message", err)
|
return nil, fmt.Errorf("error writing ctrl message: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case response := <-responseChan:
|
case response := <-responseChan:
|
||||||
delete(ongoingRequests, seq)
|
delete(ongoingRequests, seq)
|
||||||
if response.Error != "" {
|
if response.Error != "" {
|
||||||
return nil, ErrorfL(
|
return nil, fmt.Errorf("error native response: %s", response.Error)
|
||||||
&scopedLogger,
|
|
||||||
"error native response: %s",
|
|
||||||
errors.New(response.Error),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return response, nil
|
return response, nil
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
close(responseChan)
|
close(responseChan)
|
||||||
delete(ongoingRequests, seq)
|
delete(ongoingRequests, seq)
|
||||||
return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil)
|
return nil, fmt.Errorf("timeout waiting for response")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,35 +101,31 @@ func waitCtrlClientConnected() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener {
|
func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener {
|
||||||
scopedLogger := nativeLogger.With().
|
|
||||||
Str("socket_path", socketPath).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
// Remove the socket file if it already exists
|
// Remove the socket file if it already exists
|
||||||
if _, err := os.Stat(socketPath); err == nil {
|
if _, err := os.Stat(socketPath); err == nil {
|
||||||
if err := os.Remove(socketPath); err != nil {
|
if err := os.Remove(socketPath); err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file")
|
logger.Errorf("Failed to remove existing socket file %s: %v", socketPath, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listener, err := net.Listen("unixpacket", socketPath)
|
listener, err := net.Listen("unixpacket", socketPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to start server")
|
logger.Errorf("Failed to start server on %s: %v", socketPath, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger.Info().Msg("server listening")
|
logger.Infof("Server listening on %s", socketPath)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
listener.Close()
|
listener.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
logger.Errorf("failed to accept sock: %v", err)
|
||||||
}
|
}
|
||||||
if isCtrl {
|
if isCtrl {
|
||||||
close(ctrlClientConnected)
|
close(ctrlClientConnected)
|
||||||
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
logger.Debug("first native ctrl socket client connected")
|
||||||
}
|
}
|
||||||
handleClient(conn)
|
handleClient(conn)
|
||||||
}()
|
}()
|
||||||
|
|
@ -146,25 +135,20 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
|
||||||
|
|
||||||
func StartNativeCtrlSocketServer() {
|
func StartNativeCtrlSocketServer() {
|
||||||
nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true)
|
nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true)
|
||||||
nativeLogger.Debug().Msg("native app ctrl sock started")
|
logger.Debug("native app ctrl sock started")
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartNativeVideoSocketServer() {
|
func StartNativeVideoSocketServer() {
|
||||||
nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false)
|
nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false)
|
||||||
nativeLogger.Debug().Msg("native app video sock started")
|
logger.Debug("native app video sock started")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCtrlClient(conn net.Conn) {
|
func handleCtrlClient(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
scopedLogger := nativeLogger.With().
|
logger.Debug("native socket client connected")
|
||||||
Str("addr", conn.RemoteAddr().String()).
|
|
||||||
Str("type", "ctrl").
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("native ctrl socket client connected")
|
|
||||||
if ctrlSocketConn != nil {
|
if ctrlSocketConn != nil {
|
||||||
scopedLogger.Debug().Msg("closing existing native socket connection")
|
logger.Debugf("closing existing native socket connection")
|
||||||
ctrlSocketConn.Close()
|
ctrlSocketConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,19 +161,17 @@ func handleCtrlClient(conn net.Conn) {
|
||||||
for {
|
for {
|
||||||
n, err := conn.Read(readBuf)
|
n, err := conn.Read(readBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("error reading from ctrl sock")
|
logger.Errorf("error reading from ctrl sock: %v", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
readMsg := string(readBuf[:n])
|
readMsg := string(readBuf[:n])
|
||||||
|
logger.Tracef("ctrl sock msg: %v", readMsg)
|
||||||
ctrlResp := CtrlResponse{}
|
ctrlResp := CtrlResponse{}
|
||||||
err = json.Unmarshal([]byte(readMsg), &ctrlResp)
|
err = json.Unmarshal([]byte(readMsg), &ctrlResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing ctrl sock msg")
|
logger.Warnf("error parsing ctrl sock msg: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
scopedLogger.Trace().Interface("data", ctrlResp).Msg("ctrl sock msg")
|
|
||||||
|
|
||||||
if ctrlResp.Seq != 0 {
|
if ctrlResp.Seq != 0 {
|
||||||
responseChan, ok := ongoingRequests[ctrlResp.Seq]
|
responseChan, ok := ongoingRequests[ctrlResp.Seq]
|
||||||
if ok {
|
if ok {
|
||||||
|
|
@ -202,25 +184,20 @@ func handleCtrlClient(conn net.Conn) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scopedLogger.Debug().Msg("ctrl sock disconnected")
|
logger.Debug("ctrl sock disconnected")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleVideoClient(conn net.Conn) {
|
func handleVideoClient(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
scopedLogger := nativeLogger.With().
|
logger.Infof("Native video socket client connected: %v", conn.RemoteAddr())
|
||||||
Str("addr", conn.RemoteAddr().String()).
|
|
||||||
Str("type", "video").
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("native video socket client connected")
|
|
||||||
|
|
||||||
inboundPacket := make([]byte, maxFrameSize)
|
inboundPacket := make([]byte, maxFrameSize)
|
||||||
lastFrame := time.Now()
|
lastFrame := time.Now()
|
||||||
for {
|
for {
|
||||||
n, err := conn.Read(inboundPacket)
|
n, err := conn.Read(inboundPacket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("error during read")
|
logger.Warnf("error during read: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
@ -229,7 +206,7 @@ func handleVideoClient(conn net.Conn) {
|
||||||
if currentSession != nil {
|
if currentSession != nil {
|
||||||
err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame})
|
err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("error writing sample")
|
logger.Warnf("error writing sample: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,36 +223,48 @@ func ExtractAndRunNativeBin() error {
|
||||||
return fmt.Errorf("failed to make binary executable: %w", err)
|
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||||
}
|
}
|
||||||
// Run the binary in the background
|
// Run the binary in the background
|
||||||
cmd, err := startNativeBinary(binaryPath)
|
cmd := exec.Command(binaryPath)
|
||||||
if err != nil {
|
|
||||||
|
// Redirect stdout and stderr to the current process
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
// Set the process group ID so we can kill the process and its children when this process exits
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setpgid: true,
|
||||||
|
Pdeathsig: syscall.SIGKILL,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the command
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("failed to start binary: %w", err)
|
return fmt.Errorf("failed to start binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: add auto restart
|
//TODO: add auto restart
|
||||||
go func() {
|
go func() {
|
||||||
<-appCtx.Done()
|
<-appCtx.Done()
|
||||||
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")
|
logger.Infof("killing process PID: %d", cmd.Process.Pid)
|
||||||
err := cmd.Process.Kill()
|
err := cmd.Process.Kill()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
nativeLogger.Warn().Err(err).Msg("failed to kill process")
|
logger.Errorf("failed to kill process: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("jetkvm_native binary started")
|
logger.Infof("Binary started with PID: %d", cmd.Process.Pid)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldOverwrite(destPath string, srcHash []byte) bool {
|
func shouldOverwrite(destPath string, srcHash []byte) bool {
|
||||||
if srcHash == nil {
|
if srcHash == nil {
|
||||||
nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, doing overwriting")
|
logger.Debug("error reading embedded jetkvm_native.sha256, doing overwriting")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
dstHash, err := os.ReadFile(destPath + ".sha256")
|
dstHash, err := os.ReadFile(destPath + ".sha256")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
nativeLogger.Debug().Msg("error reading existing jetkvm_native.sha256, doing overwriting")
|
logger.Debug("error reading existing jetkvm_native.sha256, doing overwriting")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,16 +280,13 @@ func ensureBinaryUpdated(destPath string) error {
|
||||||
|
|
||||||
srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256")
|
srcHash, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update")
|
logger.Debug("error reading embedded jetkvm_native.sha256, proceeding with update")
|
||||||
srcHash = nil
|
srcHash = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = os.Stat(destPath)
|
_, err = os.Stat(destPath)
|
||||||
if shouldOverwrite(destPath, srcHash) || err != nil {
|
if shouldOverwrite(destPath, srcHash) || err != nil {
|
||||||
nativeLogger.Info().
|
logger.Info("writing jetkvm_native")
|
||||||
Interface("hash", srcHash).
|
|
||||||
Msg("writing jetkvm_native")
|
|
||||||
|
|
||||||
_ = os.Remove(destPath)
|
_ = os.Remove(destPath)
|
||||||
destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755)
|
destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -317,7 +303,7 @@ func ensureBinaryUpdated(destPath string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nativeLogger.Info().Msg("jetkvm_native updated")
|
logger.Info("jetkvm_native updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -327,10 +313,10 @@ func ensureBinaryUpdated(destPath string) error {
|
||||||
// Called after successful connection to jetkvm_native.
|
// Called after successful connection to jetkvm_native.
|
||||||
func restoreHdmiEdid() {
|
func restoreHdmiEdid() {
|
||||||
if config.EdidString != "" {
|
if config.EdidString != "" {
|
||||||
nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID")
|
logger.Infof("Restoring HDMI EDID to %v", config.EdidString)
|
||||||
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString})
|
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID")
|
logger.Errorf("Failed to restore HDMI EDID: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type nativeOutput struct {
|
|
||||||
mu *sync.Mutex
|
|
||||||
logger *zerolog.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *nativeOutput) Write(p []byte) (n int, err error) {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
|
|
||||||
w.logger.Msg(string(p))
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func startNativeBinary(binaryPath string) (*exec.Cmd, error) {
|
|
||||||
// Run the binary in the background
|
|
||||||
cmd := exec.Command(binaryPath)
|
|
||||||
|
|
||||||
nativeOutputLock := sync.Mutex{}
|
|
||||||
nativeStdout := &nativeOutput{
|
|
||||||
mu: &nativeOutputLock,
|
|
||||||
logger: nativeLogger.Info().Str("pipe", "stdout"),
|
|
||||||
}
|
|
||||||
nativeStderr := &nativeOutput{
|
|
||||||
mu: &nativeOutputLock,
|
|
||||||
logger: nativeLogger.Info().Str("pipe", "stderr"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect stdout and stderr to the current process
|
|
||||||
cmd.Stdout = nativeStdout
|
|
||||||
cmd.Stderr = nativeStderr
|
|
||||||
|
|
||||||
// Set the process group ID so we can kill the process and its children when this process exits
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Setpgid: true,
|
|
||||||
Pdeathsig: syscall.SIGKILL,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the command
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to start binary: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
//go:build !linux
|
|
||||||
|
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func startNativeBinary(binaryPath string) (*exec.Cmd, error) {
|
|
||||||
return nil, fmt.Errorf("not supported")
|
|
||||||
}
|
|
||||||
264
network.go
264
network.go
|
|
@ -1,107 +1,227 @@
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/network"
|
"os/exec"
|
||||||
"github.com/jetkvm/kvm/internal/udhcpc"
|
|
||||||
|
"github.com/hashicorp/go-envparse"
|
||||||
|
"github.com/pion/mdns/v2"
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
"golang.org/x/net/ipv6"
|
||||||
|
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
"github.com/vishvananda/netlink/nl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var mDNSConn *mdns.Conn
|
||||||
|
|
||||||
|
var networkState NetworkState
|
||||||
|
|
||||||
|
type NetworkState struct {
|
||||||
|
Up bool
|
||||||
|
IPv4 string
|
||||||
|
IPv6 string
|
||||||
|
MAC string
|
||||||
|
|
||||||
|
checked bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalIpInfo struct {
|
||||||
|
IPv4 string
|
||||||
|
IPv6 string
|
||||||
|
MAC string
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
NetIfName = "eth0"
|
NetIfName = "eth0"
|
||||||
|
DHCPLeaseFile = "/run/udhcpc.%s.info"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// setDhcpClientState sends signals to udhcpc to change it's current mode
|
||||||
networkState *network.NetworkInterfaceState
|
// of operation. Setting active to true will force udhcpc to renew the DHCP lease.
|
||||||
)
|
// Setting active to false will put udhcpc into idle mode.
|
||||||
|
func setDhcpClientState(active bool) {
|
||||||
|
var signal string
|
||||||
|
if active {
|
||||||
|
signal = "-SIGUSR1"
|
||||||
|
} else {
|
||||||
|
signal = "-SIGUSR2"
|
||||||
|
}
|
||||||
|
|
||||||
func networkStateChanged() {
|
cmd := exec.Command("/usr/bin/killall", signal, "udhcpc")
|
||||||
// do not block the main thread
|
if err := cmd.Run(); err != nil {
|
||||||
go waitCtrlAndRequestDisplayUpdate(true)
|
logger.Warnf("network: setDhcpClientState: failed to change udhcpc state: %s", err)
|
||||||
|
|
||||||
// always restart mDNS when the network state changes
|
|
||||||
if mDNS != nil {
|
|
||||||
_ = mDNS.SetLocalNames([]string{
|
|
||||||
networkState.GetHostname(),
|
|
||||||
networkState.GetFQDN(),
|
|
||||||
}, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initNetwork() error {
|
func checkNetworkState() {
|
||||||
ensureConfigLoaded()
|
iface, err := netlink.LinkByName(NetIfName)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("failed to get [%s] interface: %v", NetIfName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{
|
newState := NetworkState{
|
||||||
DefaultHostname: GetDefaultHostname(),
|
Up: iface.Attrs().OperState == netlink.OperUp,
|
||||||
InterfaceName: NetIfName,
|
MAC: iface.Attrs().HardwareAddr.String(),
|
||||||
NetworkConfig: config.NetworkConfig,
|
|
||||||
Logger: networkLogger,
|
|
||||||
OnStateChange: func(state *network.NetworkInterfaceState) {
|
|
||||||
networkStateChanged()
|
|
||||||
},
|
|
||||||
OnInitialCheck: func(state *network.NetworkInterfaceState) {
|
|
||||||
networkStateChanged()
|
|
||||||
},
|
|
||||||
OnDhcpLeaseChange: func(lease *udhcpc.Lease) {
|
|
||||||
networkStateChanged()
|
|
||||||
|
|
||||||
if currentSession == nil {
|
checked: true,
|
||||||
return
|
}
|
||||||
|
|
||||||
|
addrs, err := netlink.AddrList(iface, nl.FAMILY_ALL)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("failed to get addresses for [%s]: %v", NetIfName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the link is going down, put udhcpc into idle mode.
|
||||||
|
// If the link is coming back up, activate udhcpc and force it to renew the lease.
|
||||||
|
if newState.Up != networkState.Up {
|
||||||
|
setDhcpClientState(newState.Up)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if addr.IP.To4() != nil {
|
||||||
|
if !newState.Up && networkState.Up {
|
||||||
|
// If the network is going down, remove all IPv4 addresses from the interface.
|
||||||
|
logger.Infof("network: state transitioned to down, removing IPv4 address %s", addr.IP.String())
|
||||||
|
err := netlink.AddrDel(iface, &addr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("network: failed to delete %s", addr.IP.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
newState.IPv4 = "..."
|
||||||
|
} else {
|
||||||
|
newState.IPv4 = addr.IP.String()
|
||||||
}
|
}
|
||||||
|
} else if addr.IP.To16() != nil && newState.IPv6 == "" {
|
||||||
writeJSONRPCEvent("networkState", networkState.RpcGetNetworkState(), currentSession)
|
newState.IPv6 = addr.IP.String()
|
||||||
},
|
|
||||||
OnConfigChange: func(networkConfig *network.NetworkConfig) {
|
|
||||||
config.NetworkConfig = networkConfig
|
|
||||||
networkStateChanged()
|
|
||||||
|
|
||||||
if mDNS != nil {
|
|
||||||
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
|
|
||||||
_ = mDNS.SetLocalNames([]string{
|
|
||||||
networkState.GetHostname(),
|
|
||||||
networkState.GetFQDN(),
|
|
||||||
}, true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if state == nil {
|
|
||||||
if err == nil {
|
|
||||||
return fmt.Errorf("failed to create NetworkInterfaceState")
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newState != networkState {
|
||||||
|
logger.Info("network state changed")
|
||||||
|
// restart MDNS
|
||||||
|
_ = startMDNS()
|
||||||
|
networkState = newState
|
||||||
|
requestDisplayUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMDNS() error {
|
||||||
|
// If server was previously running, stop it
|
||||||
|
if mDNSConn != nil {
|
||||||
|
logger.Info("Stopping mDNS server")
|
||||||
|
err := mDNSConn.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("failed to stop mDNS server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new server
|
||||||
|
logger.Info("Starting mDNS server on jetkvm.local")
|
||||||
|
addr4, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := state.Run(); err != nil {
|
addr6, err := net.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
networkState = state
|
l4, err := net.ListenUDP("udp4", addr4)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l6, err := net.ListenUDP("udp6", addr6)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mDNSConn, err = mdns.Server(ipv4.NewPacketConn(l4), ipv6.NewPacketConn(l6), &mdns.Config{
|
||||||
|
LocalNames: []string{"jetkvm.local"}, //TODO: make it configurable
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
mDNSConn = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//defer server.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetNetworkState() network.RpcNetworkState {
|
func getNTPServersFromDHCPInfo() ([]string, error) {
|
||||||
return networkState.RpcGetNetworkState()
|
buf, err := os.ReadFile(fmt.Sprintf(DHCPLeaseFile, NetIfName))
|
||||||
}
|
if err != nil {
|
||||||
|
// do not return error if file does not exist
|
||||||
func rpcGetNetworkSettings() network.RpcNetworkSettings {
|
if os.IsNotExist(err) {
|
||||||
return networkState.RpcGetNetworkSettings()
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
return nil, fmt.Errorf("failed to load udhcpc info: %w", err)
|
||||||
func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNetworkSettings, error) {
|
|
||||||
s := networkState.RpcSetNetworkSettings(settings)
|
|
||||||
if s != nil {
|
|
||||||
return nil, s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := SaveConfig(); err != nil {
|
// parse udhcpc info
|
||||||
return nil, err
|
env, err := envparse.Parse(bytes.NewReader(buf))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse udhcpc info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &network.RpcNetworkSettings{NetworkConfig: *config.NetworkConfig}, nil
|
val, ok := env["ntpsrv"]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var servers []string
|
||||||
|
|
||||||
|
for _, server := range strings.Fields(val) {
|
||||||
|
if net.ParseIP(server) == nil {
|
||||||
|
logger.Infof("invalid NTP server IP: %s, ignoring", server)
|
||||||
|
}
|
||||||
|
servers = append(servers, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcRenewDHCPLease() error {
|
func init() {
|
||||||
return networkState.RpcRenewDHCPLease()
|
ensureConfigLoaded()
|
||||||
|
|
||||||
|
updates := make(chan netlink.LinkUpdate)
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
if err := netlink.LinkSubscribe(updates, done); err != nil {
|
||||||
|
logger.Warnf("failed to subscribe to link updates: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
waitCtrlClientConnected()
|
||||||
|
checkNetworkState()
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case update := <-updates:
|
||||||
|
if update.Link.Attrs().Name == NetIfName {
|
||||||
|
logger.Infof("link update: %+v", update)
|
||||||
|
checkNetworkState()
|
||||||
|
}
|
||||||
|
case <-ticker.C:
|
||||||
|
checkNetworkState()
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err := startMDNS()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("failed to run mDNS: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
package kvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/beevik/ntp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
timeSyncRetryStep = 5 * time.Second
|
||||||
|
timeSyncRetryMaxInt = 1 * time.Minute
|
||||||
|
timeSyncWaitNetChkInt = 100 * time.Millisecond
|
||||||
|
timeSyncWaitNetUpInt = 3 * time.Second
|
||||||
|
timeSyncInterval = 1 * time.Hour
|
||||||
|
timeSyncTimeout = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
builtTimestamp string
|
||||||
|
timeSyncRetryInterval = 0 * time.Second
|
||||||
|
timeSyncSuccess = false
|
||||||
|
defaultNTPServers = []string{
|
||||||
|
"time.cloudflare.com",
|
||||||
|
"time.apple.com",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func isTimeSyncNeeded() bool {
|
||||||
|
if builtTimestamp == "" {
|
||||||
|
logger.Warnf("Built timestamp is not set, time sync is needed")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := strconv.Atoi(builtTimestamp)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Failed to parse built timestamp: %v", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// builtTimestamp is UNIX timestamp in seconds
|
||||||
|
builtTime := time.Unix(int64(ts), 0)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
logger.Tracef("Built time: %v, now: %v", builtTime, now)
|
||||||
|
|
||||||
|
if now.Sub(builtTime) < 0 {
|
||||||
|
logger.Warnf("System time is behind the built time, time sync is needed")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TimeSyncLoop() {
|
||||||
|
for {
|
||||||
|
if !networkState.checked {
|
||||||
|
time.Sleep(timeSyncWaitNetChkInt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !networkState.Up {
|
||||||
|
logger.Infof("Waiting for network to come up")
|
||||||
|
time.Sleep(timeSyncWaitNetUpInt)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if time sync is needed, but do nothing for now
|
||||||
|
isTimeSyncNeeded()
|
||||||
|
|
||||||
|
logger.Infof("Syncing system time")
|
||||||
|
start := time.Now()
|
||||||
|
err := SyncSystemTime()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Failed to sync system time: %v", err)
|
||||||
|
|
||||||
|
// retry after a delay
|
||||||
|
timeSyncRetryInterval += timeSyncRetryStep
|
||||||
|
time.Sleep(timeSyncRetryInterval)
|
||||||
|
// reset the retry interval if it exceeds the max interval
|
||||||
|
if timeSyncRetryInterval > timeSyncRetryMaxInt {
|
||||||
|
timeSyncRetryInterval = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
timeSyncSuccess = true
|
||||||
|
logger.Infof("Time sync successful, now is: %v, time taken: %v", time.Now(), time.Since(start))
|
||||||
|
time.Sleep(timeSyncInterval) // after the first sync is done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SyncSystemTime() (err error) {
|
||||||
|
now, err := queryNetworkTime()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query network time: %w", err)
|
||||||
|
}
|
||||||
|
err = setSystemTime(*now)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set system time: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryNetworkTime() (*time.Time, error) {
|
||||||
|
ntpServers, err := getNTPServersFromDHCPInfo()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("failed to get NTP servers from DHCP info: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ntpServers == nil {
|
||||||
|
ntpServers = defaultNTPServers
|
||||||
|
logger.Infof("Using default NTP servers: %v\n", ntpServers)
|
||||||
|
} else {
|
||||||
|
logger.Infof("Using NTP servers from DHCP: %v\n", ntpServers)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range ntpServers {
|
||||||
|
now, err := queryNtpServer(server, timeSyncTimeout)
|
||||||
|
if err == nil {
|
||||||
|
logger.Infof("NTP server [%s] returned time: %v\n", server, now)
|
||||||
|
return now, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpUrls := []string{
|
||||||
|
"http://apple.com",
|
||||||
|
"http://cloudflare.com",
|
||||||
|
}
|
||||||
|
for _, url := range httpUrls {
|
||||||
|
now, err := queryHttpTime(url, timeSyncTimeout)
|
||||||
|
if err == nil {
|
||||||
|
return now, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("failed to query network time")
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryNtpServer(server string, timeout time.Duration) (now *time.Time, err error) {
|
||||||
|
resp, err := ntp.QueryWithOptions(server, ntp.QueryOptions{Timeout: timeout})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp.Time, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryHttpTime(url string, timeout time.Duration) (*time.Time, error) {
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
resp, err := client.Head(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dateStr := resp.Header.Get("Date")
|
||||||
|
now, err := time.Parse(time.RFC1123, dateStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &now, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSystemTime(now time.Time) error {
|
||||||
|
nowStr := now.Format("2006-01-02 15:04:05")
|
||||||
|
output, err := exec.Command("date", "-s", nowStr).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run date -s: %w, %s", err, string(output))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
123
ota.go
123
ota.go
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -17,8 +16,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/gwatts/rootcerts"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UpdateMetadata struct {
|
type UpdateMetadata struct {
|
||||||
|
|
@ -41,9 +38,6 @@ type UpdateStatus struct {
|
||||||
Remote *UpdateMetadata `json:"remote"`
|
Remote *UpdateMetadata `json:"remote"`
|
||||||
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
|
SystemUpdateAvailable bool `json:"systemUpdateAvailable"`
|
||||||
AppUpdateAvailable bool `json:"appUpdateAvailable"`
|
AppUpdateAvailable bool `json:"appUpdateAvailable"`
|
||||||
|
|
||||||
// for backwards compatibility
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
|
const UpdateMetadataUrl = "https://api.jetkvm.com/releases"
|
||||||
|
|
@ -82,7 +76,7 @@ func fetchUpdateMetadata(ctx context.Context, deviceId string, includePreRelease
|
||||||
query.Set("prerelease", fmt.Sprintf("%v", includePreRelease))
|
query.Set("prerelease", fmt.Sprintf("%v", includePreRelease))
|
||||||
updateUrl.RawQuery = query.Encode()
|
updateUrl.RawQuery = query.Encode()
|
||||||
|
|
||||||
logger.Info().Str("url", updateUrl.String()).Msg("Checking for updates")
|
logger.Infof("Checking for updates at: %s", updateUrl)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", updateUrl.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -132,14 +126,10 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
||||||
return fmt.Errorf("error creating request: %w", err)
|
return fmt.Errorf("error creating request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: set a separate timeout for the download but keep the TLS handshake short
|
||||||
|
// use Transport here will cause CA certificate validation failure so we temporarily removed it
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 10 * time.Minute,
|
Timeout: 10 * time.Minute,
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSHandshakeTimeout: 30 * time.Second,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
RootCAs: rootcerts.ServerCertPool(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
|
@ -201,11 +191,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyFile(path string, expectedHash string, verifyProgress *float32, scopedLogger *zerolog.Logger) error {
|
func verifyFile(path string, expectedHash string, verifyProgress *float32) error {
|
||||||
if scopedLogger == nil {
|
|
||||||
scopedLogger = otaLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
unverifiedPath := path + ".unverified"
|
unverifiedPath := path + ".unverified"
|
||||||
fileToHash, err := os.Open(unverifiedPath)
|
fileToHash, err := os.Open(unverifiedPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -249,7 +235,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
|
||||||
}
|
}
|
||||||
|
|
||||||
hashSum := hash.Sum(nil)
|
hashSum := hash.Sum(nil)
|
||||||
scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of")
|
logger.Infof("SHA256 hash of %s: %x", path, hashSum)
|
||||||
|
|
||||||
if hex.EncodeToString(hashSum) != expectedHash {
|
if hex.EncodeToString(hashSum) != expectedHash {
|
||||||
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
|
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
|
||||||
|
|
@ -291,7 +277,7 @@ var otaState = OTAState{}
|
||||||
func triggerOTAStateUpdate() {
|
func triggerOTAStateUpdate() {
|
||||||
go func() {
|
go func() {
|
||||||
if currentSession == nil {
|
if currentSession == nil {
|
||||||
logger.Info().Msg("No active RPC session, skipping update state update")
|
logger.Info("No active RPC session, skipping update state update")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSONRPCEvent("otaState", otaState, currentSession)
|
writeJSONRPCEvent("otaState", otaState, currentSession)
|
||||||
|
|
@ -299,12 +285,7 @@ func triggerOTAStateUpdate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
||||||
scopedLogger := otaLogger.With().
|
logger.Info("Trying to update...")
|
||||||
Str("deviceId", deviceId).
|
|
||||||
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("Trying to update...")
|
|
||||||
if otaState.Updating {
|
if otaState.Updating {
|
||||||
return fmt.Errorf("update already in progress")
|
return fmt.Errorf("update already in progress")
|
||||||
}
|
}
|
||||||
|
|
@ -322,7 +303,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
updateStatus, err := GetUpdateStatus(ctx, deviceId, includePreRelease)
|
updateStatus, err := GetUpdateStatus(ctx, deviceId, includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error checking for updates: %v", err)
|
otaState.Error = fmt.Sprintf("Error checking for updates: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error checking for updates")
|
|
||||||
return fmt.Errorf("error checking for updates: %w", err)
|
return fmt.Errorf("error checking for updates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,15 +320,11 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
rebootNeeded := false
|
rebootNeeded := false
|
||||||
|
|
||||||
if appUpdateAvailable {
|
if appUpdateAvailable {
|
||||||
scopedLogger.Info().
|
logger.Infof("App update available: %s -> %s", local.AppVersion, remote.AppVersion)
|
||||||
Str("local", local.AppVersion).
|
|
||||||
Str("remote", remote.AppVersion).
|
|
||||||
Msg("App update available")
|
|
||||||
|
|
||||||
err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress)
|
err := downloadFile(ctx, "/userdata/jetkvm/jetkvm_app.update", remote.AppUrl, &otaState.AppDownloadProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
|
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error downloading app update")
|
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -357,15 +333,9 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.AppDownloadProgress = 1
|
otaState.AppDownloadProgress = 1
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
err = verifyFile(
|
err = verifyFile("/userdata/jetkvm/jetkvm_app.update", remote.AppHash, &otaState.AppVerificationProgress)
|
||||||
"/userdata/jetkvm/jetkvm_app.update",
|
|
||||||
remote.AppHash,
|
|
||||||
&otaState.AppVerificationProgress,
|
|
||||||
&scopedLogger,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
|
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
|
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -376,22 +346,17 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.AppUpdateProgress = 1
|
otaState.AppUpdateProgress = 1
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("App update downloaded")
|
logger.Info("App update downloaded")
|
||||||
rebootNeeded = true
|
rebootNeeded = true
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("App is up to date")
|
logger.Info("App is up to date")
|
||||||
}
|
}
|
||||||
|
|
||||||
if systemUpdateAvailable {
|
if systemUpdateAvailable {
|
||||||
scopedLogger.Info().
|
logger.Infof("System update available: %s -> %s", local.SystemVersion, remote.SystemVersion)
|
||||||
Str("local", local.SystemVersion).
|
|
||||||
Str("remote", remote.SystemVersion).
|
|
||||||
Msg("System update available")
|
|
||||||
|
|
||||||
err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress)
|
err := downloadFile(ctx, "/userdata/jetkvm/update_system.tar", remote.SystemUrl, &otaState.SystemDownloadProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error downloading system update")
|
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -400,25 +365,18 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.SystemDownloadProgress = 1
|
otaState.SystemDownloadProgress = 1
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
err = verifyFile(
|
err = verifyFile("/userdata/jetkvm/update_system.tar", remote.SystemHash, &otaState.SystemVerificationProgress)
|
||||||
"/userdata/jetkvm/update_system.tar",
|
|
||||||
remote.SystemHash,
|
|
||||||
&otaState.SystemVerificationProgress,
|
|
||||||
&scopedLogger,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
|
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
|
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
scopedLogger.Info().Msg("System update downloaded")
|
logger.Info("System update downloaded")
|
||||||
verifyFinished := time.Now()
|
verifyFinished := time.Now()
|
||||||
otaState.SystemVerifiedAt = &verifyFinished
|
otaState.SystemVerifiedAt = &verifyFinished
|
||||||
otaState.SystemVerificationProgress = 1
|
otaState.SystemVerificationProgress = 1
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("Starting rk_ota command")
|
|
||||||
cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/jetkvm/update_system.tar", "--save_dir=/userdata/jetkvm/ota_save", "--partition=all")
|
cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/jetkvm/update_system.tar", "--save_dir=/userdata/jetkvm/ota_save", "--partition=all")
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
cmd.Stdout = &b
|
cmd.Stdout = &b
|
||||||
|
|
@ -426,7 +384,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err)
|
otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error starting rk_ota command")
|
|
||||||
return fmt.Errorf("error starting rk_ota command: %w", err)
|
return fmt.Errorf("error starting rk_ota command: %w", err)
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
@ -458,30 +415,25 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
output := b.String()
|
output := b.String()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error executing rk_ota command: %v\nOutput: %s", err, output)
|
otaState.Error = fmt.Sprintf("Error executing rk_ota command: %v\nOutput: %s", err, output)
|
||||||
scopedLogger.Error().
|
|
||||||
Err(err).
|
|
||||||
Str("output", output).
|
|
||||||
Int("exitCode", cmd.ProcessState.ExitCode()).
|
|
||||||
Msg("Error executing rk_ota command")
|
|
||||||
return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output)
|
return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output)
|
||||||
}
|
}
|
||||||
scopedLogger.Info().Str("output", output).Msg("rk_ota success")
|
|
||||||
|
logger.Infof("rk_ota success, output: %s", output)
|
||||||
otaState.SystemUpdateProgress = 1
|
otaState.SystemUpdateProgress = 1
|
||||||
otaState.SystemUpdatedAt = &verifyFinished
|
otaState.SystemUpdatedAt = &verifyFinished
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
rebootNeeded = true
|
rebootNeeded = true
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("System is up to date")
|
logger.Info("System is up to date")
|
||||||
}
|
}
|
||||||
|
|
||||||
if rebootNeeded {
|
if rebootNeeded {
|
||||||
scopedLogger.Info().Msg("System Rebooting in 10s")
|
logger.Info("System Rebooting in 10s")
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
cmd := exec.Command("reboot")
|
cmd := exec.Command("reboot")
|
||||||
err := cmd.Start()
|
err := cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Failed to start reboot: %v", err)
|
otaState.Error = fmt.Sprintf("Failed to start reboot: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Failed to start reboot")
|
|
||||||
return fmt.Errorf("failed to start reboot: %w", err)
|
return fmt.Errorf("failed to start reboot: %w", err)
|
||||||
} else {
|
} else {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|
@ -492,47 +444,52 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUpdateStatus(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateStatus, error) {
|
func GetUpdateStatus(ctx context.Context, deviceId string, includePreRelease bool) (*UpdateStatus, error) {
|
||||||
updateStatus := &UpdateStatus{}
|
|
||||||
|
|
||||||
// Get local versions
|
// Get local versions
|
||||||
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
|
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return updateStatus, fmt.Errorf("error getting local version: %w", err)
|
return nil, fmt.Errorf("error getting local version: %w", err)
|
||||||
}
|
|
||||||
updateStatus.Local = &LocalMetadata{
|
|
||||||
AppVersion: appVersionLocal.String(),
|
|
||||||
SystemVersion: systemVersionLocal.String(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get remote metadata
|
// Get remote metadata
|
||||||
remoteMetadata, err := fetchUpdateMetadata(ctx, deviceId, includePreRelease)
|
remoteMetadata, err := fetchUpdateMetadata(ctx, deviceId, includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return updateStatus, fmt.Errorf("error checking for updates: %w", err)
|
return nil, fmt.Errorf("error checking for updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build local UpdateMetadata
|
||||||
|
localMetadata := &LocalMetadata{
|
||||||
|
AppVersion: appVersionLocal.String(),
|
||||||
|
SystemVersion: systemVersionLocal.String(),
|
||||||
}
|
}
|
||||||
updateStatus.Remote = remoteMetadata
|
|
||||||
|
|
||||||
// Get remote versions
|
|
||||||
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return updateStatus, fmt.Errorf("error parsing remote system version: %w", err)
|
return nil, fmt.Errorf("error parsing remote system version: %w", err)
|
||||||
}
|
}
|
||||||
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return updateStatus, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
|
return nil, fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus.SystemUpdateAvailable = systemVersionRemote.GreaterThan(systemVersionLocal)
|
systemUpdateAvailable := systemVersionRemote.GreaterThan(systemVersionLocal)
|
||||||
updateStatus.AppUpdateAvailable = appVersionRemote.GreaterThan(appVersionLocal)
|
appUpdateAvailable := appVersionRemote.GreaterThan(appVersionLocal)
|
||||||
|
|
||||||
// Handle pre-release updates
|
// Handle pre-release updates
|
||||||
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
isRemoteSystemPreRelease := systemVersionRemote.Prerelease() != ""
|
||||||
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
isRemoteAppPreRelease := appVersionRemote.Prerelease() != ""
|
||||||
|
|
||||||
if isRemoteSystemPreRelease && !includePreRelease {
|
if isRemoteSystemPreRelease && !includePreRelease {
|
||||||
updateStatus.SystemUpdateAvailable = false
|
systemUpdateAvailable = false
|
||||||
}
|
}
|
||||||
if isRemoteAppPreRelease && !includePreRelease {
|
if isRemoteAppPreRelease && !includePreRelease {
|
||||||
updateStatus.AppUpdateAvailable = false
|
appUpdateAvailable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus := &UpdateStatus{
|
||||||
|
Local: localMetadata,
|
||||||
|
Remote: remoteMetadata,
|
||||||
|
SystemUpdateAvailable: systemUpdateAvailable,
|
||||||
|
AppUpdateAvailable: appUpdateAvailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateStatus, nil
|
return updateStatus, nil
|
||||||
|
|
@ -546,6 +503,6 @@ func IsUpdatePending() bool {
|
||||||
func confirmCurrentSystem() {
|
func confirmCurrentSystem() {
|
||||||
output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput()
|
output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn().Str("output", string(output)).Msg("failed to set current partition in A/B setup")
|
logger.Warnf("failed to set current partition in A/B setup: %s", string(output))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func (w *WebRTCDiskReader) Read(ctx context.Context, offset int64, size int64) (
|
||||||
return nil, errors.New("not active session")
|
return nil, errors.New("not active session")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Debug().Str("request", string(jsonBytes)).Msg("reading from webrtc")
|
logger.Debugf("reading from webrtc %v", string(jsonBytes))
|
||||||
err = currentSession.DiskChannel.SendText(string(jsonBytes))
|
err = currentSession.DiskChannel.SendText(string(jsonBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
JSON_OUTPUT=false
|
|
||||||
GET_COMMANDS=false
|
|
||||||
if [ "$1" = "-json" ]; then
|
|
||||||
JSON_OUTPUT=true
|
|
||||||
shift
|
|
||||||
fi
|
|
||||||
ADDITIONAL_ARGS=$@
|
|
||||||
EXIT_CODE=0
|
|
||||||
|
|
||||||
runTest() {
|
|
||||||
PKG_ARGS=""
|
|
||||||
if [ "$2" != "" ]; then
|
|
||||||
PKG_ARGS="-p $2"
|
|
||||||
fi
|
|
||||||
if [ "$JSON_OUTPUT" = true ]; then
|
|
||||||
./test2json $PKG_ARGS -t $1 -test.v $ADDITIONAL_ARGS | tee $1.result.json
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
EXIT_CODE=1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
$@
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
EXIT_CODE=1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
function exit_with_code() {
|
|
||||||
if [ $EXIT_CODE -ne 0 ]; then
|
|
||||||
printf "\e[0;31m❌ Test failed\e[0m\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $EXIT_CODE
|
|
||||||
}
|
|
||||||
|
|
||||||
trap exit_with_code EXIT
|
|
||||||
Binary file not shown.
|
|
@ -1 +1 @@
|
||||||
6dabd0e657dd099280d9173069687786a4a8c9c25cf7f9e7ce2f940cab67c521
|
c0803a9185298398eff9a925de69bd0ca882cd5983b989a45b748648146475c6
|
||||||
|
|
|
||||||
53
serial.go
53
serial.go
|
|
@ -35,19 +35,17 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func runATXControl() {
|
func runATXControl() {
|
||||||
scopedLogger := serialLogger.With().Str("service", "atx_control").Logger()
|
|
||||||
|
|
||||||
reader := bufio.NewReader(port)
|
reader := bufio.NewReader(port)
|
||||||
for {
|
for {
|
||||||
line, err := reader.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Error reading from serial port")
|
logger.Errorf("Error reading from serial port: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each line should be 4 binary digits + newline
|
// Each line should be 4 binary digits + newline
|
||||||
if len(line) != 5 {
|
if len(line) != 5 {
|
||||||
scopedLogger.Warn().Int("length", len(line)).Msg("Invalid line length")
|
logger.Warnf("Invalid line length: %d", len(line))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,12 +66,8 @@ func runATXControl() {
|
||||||
newLedPWRState != ledPWRState ||
|
newLedPWRState != ledPWRState ||
|
||||||
newBtnRSTState != btnRSTState ||
|
newBtnRSTState != btnRSTState ||
|
||||||
newBtnPWRState != btnPWRState {
|
newBtnPWRState != btnPWRState {
|
||||||
scopedLogger.Debug().
|
logger.Debugf("Status changed: HDD LED: %v, PWR LED: %v, RST BTN: %v, PWR BTN: %v",
|
||||||
Bool("hdd", newLedHDDState).
|
newLedHDDState, newLedPWRState, newBtnRSTState, newBtnPWRState)
|
||||||
Bool("pwr", newLedPWRState).
|
|
||||||
Bool("rst", newBtnRSTState).
|
|
||||||
Bool("pwr", newBtnPWRState).
|
|
||||||
Msg("Status changed")
|
|
||||||
|
|
||||||
// Update states
|
// Update states
|
||||||
ledHDDState = newLedHDDState
|
ledHDDState = newLedHDDState
|
||||||
|
|
@ -140,46 +134,45 @@ func unmountDCControl() error {
|
||||||
var dcState DCPowerState
|
var dcState DCPowerState
|
||||||
|
|
||||||
func runDCControl() {
|
func runDCControl() {
|
||||||
scopedLogger := serialLogger.With().Str("service", "dc_control").Logger()
|
|
||||||
reader := bufio.NewReader(port)
|
reader := bufio.NewReader(port)
|
||||||
for {
|
for {
|
||||||
line, err := reader.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Error reading from serial port")
|
logger.Errorf("Error reading from serial port: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split the line by semicolon
|
// Split the line by semicolon
|
||||||
parts := strings.Split(strings.TrimSpace(line), ";")
|
parts := strings.Split(strings.TrimSpace(line), ";")
|
||||||
if len(parts) != 4 {
|
if len(parts) != 4 {
|
||||||
scopedLogger.Warn().Str("line", line).Msg("Invalid line")
|
logger.Warnf("Invalid line: %s", line)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse new states
|
// Parse new states
|
||||||
powerState, err := strconv.Atoi(parts[0])
|
powerState, err := strconv.Atoi(parts[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Invalid power state")
|
logger.Warnf("Invalid power state: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dcState.IsOn = powerState == 1
|
dcState.IsOn = powerState == 1
|
||||||
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
milliVolts, err := strconv.ParseFloat(parts[1], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Invalid voltage")
|
logger.Warnf("Invalid voltage: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
volts := milliVolts / 1000 // Convert mV to V
|
volts := milliVolts / 1000 // Convert mV to V
|
||||||
|
|
||||||
milliAmps, err := strconv.ParseFloat(parts[2], 64)
|
milliAmps, err := strconv.ParseFloat(parts[2], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Invalid current")
|
logger.Warnf("Invalid current: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
amps := milliAmps / 1000 // Convert mA to A
|
amps := milliAmps / 1000 // Convert mA to A
|
||||||
|
|
||||||
milliWatts, err := strconv.ParseFloat(parts[3], 64)
|
milliWatts, err := strconv.ParseFloat(parts[3], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Invalid power")
|
logger.Warnf("Invalid power: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
watts := milliWatts / 1000 // Convert mW to W
|
watts := milliWatts / 1000 // Convert mW to W
|
||||||
|
|
@ -219,10 +212,9 @@ var defaultMode = &serial.Mode{
|
||||||
|
|
||||||
func initSerialPort() {
|
func initSerialPort() {
|
||||||
_ = reopenSerialPort()
|
_ = reopenSerialPort()
|
||||||
switch config.ActiveExtension {
|
if config.ActiveExtension == "atx-power" {
|
||||||
case "atx-power":
|
|
||||||
_ = mountATXControl()
|
_ = mountATXControl()
|
||||||
case "dc-power":
|
} else if config.ActiveExtension == "dc-power" {
|
||||||
_ = mountDCControl()
|
_ = mountDCControl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -234,19 +226,12 @@ func reopenSerialPort() error {
|
||||||
var err error
|
var err error
|
||||||
port, err = serial.Open(serialPortPath, defaultMode)
|
port, err = serial.Open(serialPortPath, defaultMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serialLogger.Error().
|
logger.Errorf("Error opening serial port: %v", err)
|
||||||
Err(err).
|
|
||||||
Str("path", serialPortPath).
|
|
||||||
Interface("mode", defaultMode).
|
|
||||||
Msg("Error opening serial port")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSerialChannel(d *webrtc.DataChannel) {
|
func handleSerialChannel(d *webrtc.DataChannel) {
|
||||||
scopedLogger := serialLogger.With().
|
|
||||||
Uint16("data_channel_id", *d.ID()).Logger()
|
|
||||||
|
|
||||||
d.OnOpen(func() {
|
d.OnOpen(func() {
|
||||||
go func() {
|
go func() {
|
||||||
buf := make([]byte, 1024)
|
buf := make([]byte, 1024)
|
||||||
|
|
@ -254,13 +239,13 @@ func handleSerialChannel(d *webrtc.DataChannel) {
|
||||||
n, err := port.Read(buf)
|
n, err := port.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to read from serial port")
|
logger.Errorf("Failed to read from serial port: %v", err)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
err = d.Send(buf[:n])
|
err = d.Send(buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to send serial output")
|
logger.Errorf("Failed to send serial output: %v", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,15 +258,11 @@ func handleSerialChannel(d *webrtc.DataChannel) {
|
||||||
}
|
}
|
||||||
_, err := port.Write(msg.Data)
|
_, err := port.Write(msg.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to write to serial")
|
logger.Errorf("Failed to write to serial: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
d.OnError(func(err error) {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("Serial channel error")
|
|
||||||
})
|
|
||||||
|
|
||||||
d.OnClose(func() {
|
d.OnClose(func() {
|
||||||
scopedLogger.Info().Msg("Serial channel closed")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
terminal.go
18
terminal.go
|
|
@ -16,9 +16,6 @@ type TerminalSize struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTerminalChannel(d *webrtc.DataChannel) {
|
func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
scopedLogger := terminalLogger.With().
|
|
||||||
Uint16("data_channel_id", *d.ID()).Logger()
|
|
||||||
|
|
||||||
var ptmx *os.File
|
var ptmx *os.File
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
d.OnOpen(func() {
|
d.OnOpen(func() {
|
||||||
|
|
@ -26,7 +23,7 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
var err error
|
var err error
|
||||||
ptmx, err = pty.Start(cmd)
|
ptmx, err = pty.Start(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to start pty")
|
logger.Errorf("Failed to start pty: %v", err)
|
||||||
d.Close()
|
d.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -37,13 +34,13 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
n, err := ptmx.Read(buf)
|
n, err := ptmx.Read(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to read from pty")
|
logger.Errorf("Failed to read from pty: %v", err)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
err = d.Send(buf[:n])
|
err = d.Send(buf[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to send pty output")
|
logger.Errorf("Failed to send pty output: %v", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,11 +63,11 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to parse terminal size")
|
logger.Errorf("Failed to parse terminal size: %v", err)
|
||||||
}
|
}
|
||||||
_, err := ptmx.Write(msg.Data)
|
_, err := ptmx.Write(msg.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("Failed to write to pty")
|
logger.Errorf("Failed to write to pty: %v", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -81,10 +78,5 @@ func handleTerminalChannel(d *webrtc.DataChannel) {
|
||||||
if cmd != nil && cmd.Process != nil {
|
if cmd != nil && cmd.Process != nil {
|
||||||
_ = cmd.Process.Kill()
|
_ = cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
scopedLogger.Info().Msg("Terminal channel closed")
|
|
||||||
})
|
|
||||||
|
|
||||||
d.OnError(func(err error) {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("Terminal channel error")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
timesync.go
53
timesync.go
|
|
@ -1,53 +0,0 @@
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/timesync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
timeSync *timesync.TimeSync
|
|
||||||
builtTimestamp string
|
|
||||||
)
|
|
||||||
|
|
||||||
func isTimeSyncNeeded() bool {
|
|
||||||
if builtTimestamp == "" {
|
|
||||||
timesyncLogger.Warn().Msg("built timestamp is not set, time sync is needed")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
ts, err := strconv.Atoi(builtTimestamp)
|
|
||||||
if err != nil {
|
|
||||||
timesyncLogger.Warn().Str("error", err.Error()).Msg("failed to parse built timestamp")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// builtTimestamp is UNIX timestamp in seconds
|
|
||||||
builtTime := time.Unix(int64(ts), 0)
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
if now.Sub(builtTime) < 0 {
|
|
||||||
timesyncLogger.Warn().
|
|
||||||
Str("built_time", builtTime.Format(time.RFC3339)).
|
|
||||||
Str("now", now.Format(time.RFC3339)).
|
|
||||||
Msg("system time is behind the built time, time sync is needed")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func initTimeSync() {
|
|
||||||
timeSync = timesync.NewTimeSync(×ync.TimeSyncOptions{
|
|
||||||
Logger: timesyncLogger,
|
|
||||||
NetworkConfig: config.NetworkConfig,
|
|
||||||
PreCheckFunc: func() (bool, error) {
|
|
||||||
if !networkState.IsOnline() {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/stylistic",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"prettier",
|
||||||
|
],
|
||||||
|
ignorePatterns: ["dist", ".eslintrc.cjs", "tailwind.config.js", "postcss.config.js"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["react-refresh"],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
project: ["./tsconfig.json", "./tsconfig.node.json"],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
*
|
||||||
|
* This keeps imports separate from one another, ensuring that imports are separated
|
||||||
|
* by their relative groups. As you move through the groups, imports become closer
|
||||||
|
* to the current file.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```
|
||||||
|
* import fs from 'fs';
|
||||||
|
*
|
||||||
|
* import package from 'npm-package';
|
||||||
|
*
|
||||||
|
* import xyz from '~/project-file';
|
||||||
|
*
|
||||||
|
* import index from '../';
|
||||||
|
*
|
||||||
|
* import sibling from './foo';
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
||||||
|
"newlines-between": "always",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
"import/resolver": {
|
||||||
|
alias: {
|
||||||
|
map: [
|
||||||
|
["@components", "./src/components"],
|
||||||
|
["@routes", "./src/routes"],
|
||||||
|
["@assets", "./src/assets"],
|
||||||
|
["@", "./src"],
|
||||||
|
],
|
||||||
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"plugins": ["prettier-plugin-tailwindcss"],
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
"tailwindFunctions": ["clsx", "cx"],
|
"tailwindFunctions": ["clsx"],
|
||||||
"printWidth": 90,
|
"printWidth": 90
|
||||||
"tailwindStylesheet": "./src/index.css"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,5 @@ echo "└───────────────────────
|
||||||
|
|
||||||
# Set the environment variable and run Vite
|
# Set the environment variable and run Vite
|
||||||
echo "Starting development server with JetKVM device at: $ip_address"
|
echo "Starting development server with JetKVM device at: $ip_address"
|
||||||
|
|
||||||
# Check if pwd is the current directory of the script
|
|
||||||
if [ "$(pwd)" != "$(dirname "$0")" ]; then
|
|
||||||
pushd "$(dirname "$0")" > /dev/null
|
|
||||||
echo "Changed directory to: $(pwd)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device
|
JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device
|
||||||
|
|
||||||
popd > /dev/null
|
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
const {
|
|
||||||
defineConfig,
|
|
||||||
globalIgnores,
|
|
||||||
} = require("eslint/config");
|
|
||||||
|
|
||||||
const globals = require("globals");
|
|
||||||
|
|
||||||
const {
|
|
||||||
fixupConfigRules,
|
|
||||||
} = require("@eslint/compat");
|
|
||||||
|
|
||||||
const tsParser = require("@typescript-eslint/parser");
|
|
||||||
const reactRefresh = require("eslint-plugin-react-refresh");
|
|
||||||
const js = require("@eslint/js");
|
|
||||||
|
|
||||||
const {
|
|
||||||
FlatCompat,
|
|
||||||
} = require("@eslint/eslintrc");
|
|
||||||
|
|
||||||
const compat = new FlatCompat({
|
|
||||||
baseDirectory: __dirname,
|
|
||||||
recommendedConfig: js.configs.recommended,
|
|
||||||
allConfig: js.configs.all
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = defineConfig([{
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
...globals.browser,
|
|
||||||
},
|
|
||||||
|
|
||||||
parser: tsParser,
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
|
|
||||||
parserOptions: {
|
|
||||||
project: ["./tsconfig.json", "./tsconfig.node.json"],
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
extends: fixupConfigRules(compat.extends(
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:@typescript-eslint/stylistic",
|
|
||||||
"plugin:react-hooks/recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:react/jsx-runtime",
|
|
||||||
"plugin:import/recommended",
|
|
||||||
"prettier",
|
|
||||||
)),
|
|
||||||
|
|
||||||
plugins: {
|
|
||||||
"react-refresh": reactRefresh,
|
|
||||||
},
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
"react-refresh/only-export-components": ["warn", {
|
|
||||||
allowConstantExport: true,
|
|
||||||
}],
|
|
||||||
|
|
||||||
"import/order": ["error", {
|
|
||||||
groups: ["builtin", "external", "internal", "parent", "sibling"],
|
|
||||||
"newlines-between": "always",
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
},
|
|
||||||
"import/resolver": {
|
|
||||||
alias: {
|
|
||||||
map: [
|
|
||||||
["@components", "./src/components"],
|
|
||||||
["@routes", "./src/routes"],
|
|
||||||
["@assets", "./src/assets"],
|
|
||||||
["@", "./src"],
|
|
||||||
],
|
|
||||||
|
|
||||||
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, globalIgnores([
|
|
||||||
"**/dist",
|
|
||||||
"**/.eslintrc.cjs",
|
|
||||||
"**/tailwind.config.js",
|
|
||||||
"**/postcss.config.js",
|
|
||||||
])]);
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,11 +4,10 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "22.15.0"
|
"node": "21.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "./dev_device.sh",
|
"dev": "./dev_device.sh",
|
||||||
"dev:ssl": "USE_SSL=true ./dev_device.sh",
|
|
||||||
"dev:cloud": "vite dev --mode=cloud-development",
|
"dev:cloud": "vite dev --mode=cloud-development",
|
||||||
"build": "npm run build:prod",
|
"build": "npm run build:prod",
|
||||||
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
"build:device": "tsc && vite build --mode=device --emptyOutDir",
|
||||||
|
|
@ -19,67 +18,60 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.3",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@headlessui/tailwindcss": "^0.2.2",
|
"@headlessui/tailwindcss": "^0.2.1",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
|
||||||
"@xterm/addon-clipboard": "^0.1.0",
|
"@xterm/addon-clipboard": "^0.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-unicode11": "^0.8.0",
|
"@xterm/addon-unicode11": "^0.8.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.3",
|
"cva": "^1.0.0-beta.1",
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^11.0.3",
|
"focus-trap-react": "^10.2.3",
|
||||||
"framer-motion": "^12.11.4",
|
"framer-motion": "^11.15.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mini-svg-data-uri": "^1.4.4",
|
"mini-svg-data-uri": "^1.4.4",
|
||||||
"react": "^19.1.0",
|
"react": "^18.2.0",
|
||||||
"react-animate-height": "^3.2.3",
|
"react-animate-height": "^3.2.3",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"react-simple-keyboard": "^3.8.72",
|
"react-simple-keyboard": "^3.7.112",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.9",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.0",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^2.5.5",
|
||||||
"usehooks-ts": "^3.1.1",
|
"usehooks-ts": "^3.1.0",
|
||||||
"validator": "^13.15.0",
|
"validator": "^13.12.0",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.9",
|
"@tailwindcss/forms": "^0.5.9",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@eslint/js": "^9.26.0",
|
"@types/react": "^18.2.66",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@types/semver": "^7.5.8",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@types/validator": "^13.12.2",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||||
"@types/react": "^19.1.4",
|
"@typescript-eslint/parser": "^8.25.0",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||||
"@types/semver": "^7.7.0",
|
"autoprefixer": "^10.4.20",
|
||||||
"@types/validator": "^13.15.0",
|
"eslint": "^8.20.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
|
||||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
|
||||||
"autoprefixer": "^10.4.21",
|
|
||||||
"eslint": "^9.26.0",
|
|
||||||
"eslint-config-prettier": "^10.1.5",
|
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^16.1.0",
|
"postcss": "^8.4.49",
|
||||||
"postcss": "^8.5.3",
|
"prettier": "^3.4.2",
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.7.2",
|
||||||
"vite": "^6.3.5",
|
"vite": "^5.2.0",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
../../internal/logging/sse.html
|
|
||||||
|
|
@ -89,8 +89,8 @@ export default function Actionbar({
|
||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
transition
|
transition
|
||||||
className={cx(
|
className={cx(
|
||||||
"z-10 flex w-[420px] origin-top flex-col overflow-visible!",
|
"z-10 flex w-[420px] origin-top flex-col !overflow-visible",
|
||||||
"flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0",
|
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
|
|
@ -131,8 +131,8 @@ export default function Actionbar({
|
||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
transition
|
transition
|
||||||
className={cx(
|
className={cx(
|
||||||
"z-10 flex w-[420px] origin-top flex-col overflow-visible!",
|
"z-10 flex w-[420px] origin-top flex-col !overflow-visible",
|
||||||
"flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0",
|
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
|
|
@ -183,8 +183,8 @@ export default function Actionbar({
|
||||||
transitionProperty: "opacity",
|
transitionProperty: "opacity",
|
||||||
}}
|
}}
|
||||||
className={cx(
|
className={cx(
|
||||||
"z-10 flex w-[420px] origin-top flex-col overflow-visible!",
|
"z-10 flex w-[420px] origin-top flex-col !overflow-visible",
|
||||||
"flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0",
|
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
|
|
@ -226,8 +226,8 @@ export default function Actionbar({
|
||||||
anchor="bottom start"
|
anchor="bottom start"
|
||||||
transition
|
transition
|
||||||
className={cx(
|
className={cx(
|
||||||
"z-10 flex w-[420px] flex-col overflow-visible!",
|
"z-10 flex w-[420px] flex-col !overflow-visible",
|
||||||
"flex origin-top flex-col transition duration-300 ease-out data-closed:translate-y-8 data-closed:opacity-0",
|
"flex origin-top flex-col transition duration-300 ease-out data-[closed]:translate-y-8 data-[closed]:opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ open }) => {
|
{({ open }) => {
|
||||||
|
|
@ -274,7 +274,7 @@ export default function Actionbar({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hidden items-center gap-x-2 lg:flex">
|
<div className="hidden items-center gap-x-2 lg:flex">
|
||||||
<div className="h-4 w-px bg-slate-300 dark:bg-slate-600" />
|
<div className="h-4 w-[1px] bg-slate-300 dark:bg-slate-600" />
|
||||||
<Button
|
<Button
|
||||||
size="XS"
|
size="XS"
|
||||||
theme="light"
|
theme="light"
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default function AuthLayout({
|
||||||
<>
|
<>
|
||||||
<GridBackground />
|
<GridBackground />
|
||||||
|
|
||||||
<div className="grid min-h-screen grid-rows-(--grid-layout)">
|
<div className="grid min-h-screen grid-rows-layout">
|
||||||
<SimpleNavbar
|
<SimpleNavbar
|
||||||
logoHref="/"
|
logoHref="/"
|
||||||
actionElement={
|
actionElement={
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const AutoHeight = ({ children, ...props }: { children: React.ReactNode }) => {
|
||||||
{...props}
|
{...props}
|
||||||
height={height}
|
height={height}
|
||||||
duration={300}
|
duration={300}
|
||||||
contentClassName="h-fit p-px"
|
contentClassName="h-fit"
|
||||||
contentRef={contentDiv}
|
contentRef={contentDiv}
|
||||||
disableDisplayNone
|
disableDisplayNone
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { JSX } from "react";
|
import React from "react";
|
||||||
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
|
import { FetcherWithComponents, Link, LinkProps, useNavigation } from "react-router-dom";
|
||||||
|
|
||||||
import ExtLink from "@/components/ExtLink";
|
import ExtLink from "@/components/ExtLink";
|
||||||
|
|
@ -16,7 +16,7 @@ const sizes = {
|
||||||
const themes = {
|
const themes = {
|
||||||
primary: cx(
|
primary: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow-sm",
|
"bg-blue-700 dark:border-blue-600 border border-blue-900/60 text-white shadow",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-blue-800",
|
"group-hover:bg-blue-800",
|
||||||
// Active states
|
// Active states
|
||||||
|
|
@ -24,7 +24,7 @@ const themes = {
|
||||||
),
|
),
|
||||||
danger: cx(
|
danger: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-red-600 text-white border-red-700 shadow-xs shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20",
|
"bg-red-600 text-white border-red-700 shadow-sm shadow-red-200/80 dark:border-red-600 dark:shadow-red-900/20",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-red-700 group-hover:border-red-800 dark:group-hover:bg-red-700 dark:group-hover:border-red-600",
|
"group-hover:bg-red-700 group-hover:border-red-800 dark:group-hover:bg-red-700 dark:group-hover:border-red-600",
|
||||||
// Active states
|
// Active states
|
||||||
|
|
@ -34,7 +34,7 @@ const themes = {
|
||||||
),
|
),
|
||||||
light: cx(
|
light: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-white text-black border-slate-800/30 shadow-xs dark:bg-slate-800 dark:border-slate-300/20 dark:text-white",
|
"bg-white text-black border-slate-800/30 shadow dark:bg-slate-800 dark:border-slate-300/20 dark:text-white",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-blue-50/80 dark:group-hover:bg-slate-700",
|
"group-hover:bg-blue-50/80 dark:group-hover:bg-slate-700",
|
||||||
// Active states
|
// Active states
|
||||||
|
|
@ -44,7 +44,7 @@ const themes = {
|
||||||
),
|
),
|
||||||
lightDanger: cx(
|
lightDanger: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-white text-black border-red-400/60 shadow-xs",
|
"bg-white text-black border-red-400/60 shadow-sm",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-red-50/80",
|
"group-hover:bg-red-50/80",
|
||||||
// Active states
|
// Active states
|
||||||
|
|
@ -56,7 +56,7 @@ const themes = {
|
||||||
// Base styles
|
// Base styles
|
||||||
"bg-white/0 text-black border-transparent dark:text-white",
|
"bg-white/0 text-black border-transparent dark:text-white",
|
||||||
// Hover states
|
// Hover states
|
||||||
"group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow-sm dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600",
|
"group-hover:bg-white group-hover:border-slate-800/30 group-hover:shadow dark:group-hover:bg-slate-700 dark:group-hover:border-slate-600",
|
||||||
// Active states
|
// Active states
|
||||||
"group-active:bg-slate-100/80",
|
"group-active:bg-slate-100/80",
|
||||||
),
|
),
|
||||||
|
|
@ -65,15 +65,15 @@ const themes = {
|
||||||
const btnVariants = cva({
|
const btnVariants = cva({
|
||||||
base: cx(
|
base: cx(
|
||||||
// Base styles
|
// Base styles
|
||||||
"border rounded-sm select-none",
|
"border rounded select-none",
|
||||||
// Size classes
|
// Size classes
|
||||||
"justify-center items-center shrink-0",
|
"justify-center items-center shrink-0",
|
||||||
// Transition classes
|
// Transition classes
|
||||||
"outline-hidden transition-all duration-200",
|
"outline-none transition-all duration-200",
|
||||||
// Text classes
|
// Text classes
|
||||||
"font-display text-center font-medium leading-tight",
|
"font-display text-center font-medium leading-tight",
|
||||||
// States
|
// States
|
||||||
"group-focus:outline-hidden group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700",
|
"group-focus:outline-none group-focus:ring-2 group-focus:ring-offset-2 group-focus:ring-blue-700",
|
||||||
"group-disabled:opacity-50 group-disabled:pointer-events-none",
|
"group-disabled:opacity-50 group-disabled:pointer-events-none",
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -126,9 +126,9 @@ function ButtonContent(props: ButtonContentPropsType) {
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"flex w-full min-w-0 items-center gap-x-1.5 text-center",
|
"flex w-full min-w-0 items-center gap-x-1.5 text-center",
|
||||||
textAlign === "left" ? "text-left!" : "",
|
textAlign === "left" ? "!text-left" : "",
|
||||||
textAlign === "center" ? "text-center!" : "",
|
textAlign === "center" ? "!text-center" : "",
|
||||||
textAlign === "right" ? "text-right!" : "",
|
textAlign === "right" ? "!text-right" : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
@ -175,7 +175,7 @@ type ButtonPropsType = Pick<
|
||||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonPropsType>(
|
||||||
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
|
({ type, disabled, onClick, formNoValidate, loading, fetcher, ...props }, ref) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-hidden",
|
"group outline-none",
|
||||||
props.fullWidth ? "w-full" : "",
|
props.fullWidth ? "w-full" : "",
|
||||||
loading ? "pointer-events-none" : "",
|
loading ? "pointer-events-none" : "",
|
||||||
);
|
);
|
||||||
|
|
@ -215,8 +215,8 @@ type LinkPropsType = Pick<LinkProps, "to"> &
|
||||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||||
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
export const LinkButton = ({ to, ...props }: LinkPropsType) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-hidden",
|
"group outline-none",
|
||||||
props.disabled ? "pointer-events-none opacity-70!" : "",
|
props.disabled ? "pointer-events-none !opacity-70" : "",
|
||||||
props.fullWidth ? "w-full" : "",
|
props.fullWidth ? "w-full" : "",
|
||||||
props.loading ? "pointer-events-none" : "",
|
props.loading ? "pointer-events-none" : "",
|
||||||
props.className,
|
props.className,
|
||||||
|
|
@ -241,8 +241,8 @@ type LabelPropsType = Pick<HTMLLabelElement, "htmlFor"> &
|
||||||
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
React.ComponentProps<typeof ButtonContent> & { disabled?: boolean };
|
||||||
export const LabelButton = ({ htmlFor, ...props }: LabelPropsType) => {
|
export const LabelButton = ({ htmlFor, ...props }: LabelPropsType) => {
|
||||||
const classes = cx(
|
const classes = cx(
|
||||||
"group outline-hidden block cursor-pointer",
|
"group outline-none block cursor-pointer",
|
||||||
props.disabled ? "pointer-events-none opacity-70!" : "",
|
props.disabled ? "pointer-events-none !opacity-70" : "",
|
||||||
props.fullWidth ? "w-full" : "",
|
props.fullWidth ? "w-full" : "",
|
||||||
props.loading ? "pointer-events-none" : "",
|
props.loading ? "pointer-events-none" : "",
|
||||||
props.className,
|
props.className,
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ export const GridCard = ({
|
||||||
return (
|
return (
|
||||||
<Card className={cx("overflow-hidden", cardClassName)}>
|
<Card className={cx("overflow-hidden", cardClassName)}>
|
||||||
<div className="relative h-full">
|
<div className="relative h-full">
|
||||||
<div className="absolute inset-0 z-0 h-full w-full bg-linear-to-tr from-blue-50/30 to-blue-50/20 transition-colors duration-300 ease-in-out dark:from-slate-800/30 dark:to-slate-800/20" />
|
<div className="absolute inset-0 z-0 h-full w-full bg-gradient-to-tr from-blue-50/30 to-blue-50/20 transition-colors duration-300 ease-in-out dark:from-slate-800/30 dark:to-slate-800/20" />
|
||||||
<div className="absolute inset-0 z-0 h-full w-full rotate-0 bg-grid-blue-100/25 dark:bg-grid-slate-700/7" />
|
<div className="absolute inset-0 z-0 h-full w-full rotate-0 bg-grid-blue-100/[25%] dark:bg-grid-slate-700/[7%]" />
|
||||||
<div className="isolate h-full">{children}</div>
|
<div className="isolate h-full">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -30,7 +30,7 @@ const Card = forwardRef<HTMLDivElement, CardPropsType>(({ children, className },
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cx(
|
className={cx(
|
||||||
"w-full rounded-sm border-none bg-white shadow-xs outline-1 outline-slate-800/30 dark:bg-slate-800 dark:outline-slate-300/20",
|
"w-full rounded border-none bg-white shadow outline outline-1 outline-slate-800/30 dark:bg-slate-800 dark:outline-slate-300/20",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Ref } from "react";
|
import type { Ref } from "react";
|
||||||
import React, { forwardRef, JSX } from "react";
|
import React, { forwardRef } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import FieldLabel from "@/components/FieldLabel";
|
import FieldLabel from "@/components/FieldLabel";
|
||||||
|
|
@ -12,10 +12,10 @@ const sizes = {
|
||||||
|
|
||||||
const checkboxVariants = cva({
|
const checkboxVariants = cva({
|
||||||
base: cx(
|
base: cx(
|
||||||
"form-checkbox block rounded",
|
"block rounded",
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 checked:accent-blue-700 checked:dark:accent-blue-500 transition-colors",
|
"border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-800 text-blue-700 dark:text-blue-500 transition-colors",
|
||||||
|
|
||||||
// Hover
|
// Hover
|
||||||
"hover:bg-slate-200/50 dark:hover:bg-slate-700/50",
|
"hover:bg-slate-200/50 dark:hover:bg-slate-700/50",
|
||||||
|
|
@ -24,7 +24,7 @@ const checkboxVariants = cva({
|
||||||
"active:bg-slate-200 dark:active:bg-slate-700",
|
"active:bg-slate-200 dark:active:bg-slate-700",
|
||||||
|
|
||||||
// Focus
|
// Focus
|
||||||
"focus:border-slate-300 dark:focus:border-slate-600 focus:outline-hidden focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900",
|
"focus:border-slate-300 dark:focus:border-slate-600 focus:outline-none focus:ring-2 focus:ring-blue-700 dark:focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900",
|
||||||
|
|
||||||
// Disabled
|
// Disabled
|
||||||
"disabled:pointer-events-none disabled:opacity-30",
|
"disabled:pointer-events-none disabled:opacity-30",
|
||||||
|
|
@ -37,13 +37,11 @@ type CheckBoxProps = {
|
||||||
} & Omit<JSX.IntrinsicElements["input"], "size" | "type">;
|
} & Omit<JSX.IntrinsicElements["input"], "size" | "type">;
|
||||||
|
|
||||||
const Checkbox = forwardRef<HTMLInputElement, CheckBoxProps>(function Checkbox(
|
const Checkbox = forwardRef<HTMLInputElement, CheckBoxProps>(function Checkbox(
|
||||||
{ size = "MD", className, ...props },
|
{ size = "MD", ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const classes = checkboxVariants({ size });
|
const classes = checkboxVariants({ size });
|
||||||
return (
|
return <input ref={ref} {...props} type="checkbox" className={classes} />;
|
||||||
<input ref={ref} {...props} type="checkbox" className={clsx(classes, className)} />
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
Checkbox.displayName = "Checkbox";
|
Checkbox.displayName = "Checkbox";
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue